├── .gitignore
├── demo.gif
├── config
└── serve_livereload.php
├── views
└── script.blade.php
├── src
├── Commands
│ ├── ServeHttpCommand.php
│ ├── ServeCommand.php
│ └── ServeWebSocketsCommand.php
├── ResponseServiceProvider.php
├── Injector.php
├── Socket.php
├── Middleware
│ └── InjectScriptsMiddleware.php
├── Watcher.php
└── CommandServiceProvider.php
├── phpunit.xml.dist
├── LICENSE
├── composer.json
├── readme.md
└── tests
└── InjectScriptsToResponseTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | composer.lock
3 | .phpunit.result.cache
4 | vendor/
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bangnokia/laravel-serve-livereload/HEAD/demo.gif
--------------------------------------------------------------------------------
/config/serve_livereload.php:
--------------------------------------------------------------------------------
1 | ['/app', '/public', '/config', '/routes', '/resources'],
5 | ];
6 |
--------------------------------------------------------------------------------
/views/script.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/Commands/ServeHttpCommand.php:
--------------------------------------------------------------------------------
1 | loadViewsFrom(__DIR__.'/../views/', 'serve_livereload');
14 |
15 | $this->app->make(Kernel::class)
16 | ->prependMiddlewareToGroup('web', InjectScriptsMiddleware::class);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Injector.php:
--------------------------------------------------------------------------------
1 | tag or not.
12 | *
13 | * @param string $content
14 | *
15 | * @return string
16 | */
17 | public function injectScripts($content)
18 | {
19 | $content = (string) view('serve_livereload::script', [
20 | 'host' => '127.0.0.1',
21 | 'port' => ServeWebSocketsCommand::port(),
22 | ]).$content;
23 |
24 | return $content;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./tests
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Socket.php:
--------------------------------------------------------------------------------
1 | attach($conn);
20 | }
21 |
22 | public function onClose(ConnectionInterface $conn)
23 | {
24 | self::$clients->detach($conn);
25 | }
26 |
27 | public function onError(ConnectionInterface $conn, \Exception $e)
28 | {
29 | }
30 |
31 | public function onMessage(ConnectionInterface $from, $msg)
32 | {
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Middleware/InjectScriptsMiddleware.php:
--------------------------------------------------------------------------------
1 | getMethod() === Request::METHOD_GET
21 | && Str::startsWith($response->headers->get('Content-Type'), 'text/html')
22 | && !$request->isXmlHttpRequest()
23 | && !$response instanceof JsonResponse
24 | ) {
25 | $response->setContent(
26 | (new Injector())->injectScripts($response->getContent())
27 | );
28 | }
29 |
30 | return $response;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Nguyen Viet
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bangnokia/laravel-serve-livereload",
3 | "description": "Bring livereload to laravel artisan serve command",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "bangnokia",
8 | "email": "bangnokia@gmail.com"
9 | }
10 | ],
11 | "require": {
12 | "illuminate/console": "^6.0|^7.0|^8",
13 | "cboden/ratchet": "^0.4",
14 | "symfony/process": "^4.4|^5.0",
15 | "yosymfony/resource-watcher": "^2.0"
16 | },
17 | "require-dev": {
18 | "orchestra/testbench": "^5.0"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "BangNokia\\ServeLiveReload\\": "src/"
23 | }
24 | },
25 | "autoload-dev": {
26 | "psr-4": {
27 | "BangNokia\\ServeLiveReload\\Tests\\": "tests/"
28 | }
29 | },
30 | "minimum-stability": "dev",
31 | "extra": {
32 | "laravel": {
33 | "providers": [
34 | "BangNokia\\ServeLiveReload\\CommandServiceProvider",
35 | "BangNokia\\ServeLiveReload\\ResponseServiceProvider"
36 | ]
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Watcher.php:
--------------------------------------------------------------------------------
1 | loop = $loop;
21 |
22 | $this->finder = $finder;
23 | }
24 |
25 | public function startWatching(\Closure $callback)
26 | {
27 | $watcher = new ResourceWatcher(
28 | new ResourceCacheMemory(),
29 | $this->finder,
30 | new Crc32ContentHash()
31 | );
32 |
33 | $this->loop->addPeriodicTimer(0.5, function () use ($watcher, $callback) {
34 | Cache::put('serve_websockets_running', true, 5);
35 | if ($watcher->findChanges()->hasChanges()) {
36 | call_user_func($callback);
37 | }
38 | });
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/CommandServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->singleton('command.serve', function () {
17 | return new ServeCommand();
18 | });
19 |
20 | // register new commands
21 | $this->commands([
22 | ServeCommand::class,
23 | ServeHttpCommand::class,
24 | ServeWebSocketsCommand::class,
25 | ]);
26 |
27 | $this->mergeConfigFrom(__DIR__.'/../config/serve_livereload.php', 'serve_livereload');
28 | }
29 |
30 | public function boot()
31 | {
32 | $this->publishes([
33 | __DIR__.'/../config/serve_livereload.php' => config_path('serve_livereload.php'),
34 | ]);
35 | }
36 |
37 | public function provides()
38 | {
39 | return ['command.serve'];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Laravel Serve LiveReload
2 |
3 |
4 | >**This package is daed, please use the new [Vite integrated](https://laravel.com/docs/9.x/vite#introduction) instead**
5 |
6 |
7 | This package integrates into default `artisan serve` command an WebSockets server for live reloading the application when any file changed. How it works, checkout my [blog post](https://daudau.cc/posts/laravel-live-reload-without-javascript) for explaination if you don't like digging into source code.
8 |
9 | 
10 |
11 | ## Installation
12 |
13 | For laravel 8, please use version ^1.x, and below use version 0.x
14 |
15 | `composer require bangnokia/laravel-serve-livereload --dev`
16 |
17 |
18 |
19 | ## Usage
20 |
21 | Open terminal and run `php artisan serve`
22 |
23 | This package works even when you use custom vhost such as `valet` or `laragon`
24 |
25 | ## Configuration
26 |
27 | By default, this package looking for files changes in these directories:
28 |
29 | ```
30 | /app
31 | /public
32 | /config
33 | /routes
34 | /resources
35 | ```
36 |
37 | If you want to customize the watched forlders, you can publish the configuration file by this commmand:
38 |
39 | ```bash
40 | php artisan vendor:publish --provider="BangNokia\ServeLiveReload\CommandServiceProvider"
41 | ```
42 |
43 | and then you can config what you want in the `config/serve_livereload.php`.
44 |
45 |
--------------------------------------------------------------------------------
/src/Commands/ServeCommand.php:
--------------------------------------------------------------------------------
1 | find(false);
22 | $artisanPath = base_path('artisan');
23 |
24 | $processes = [
25 | $httpProcess = new Process(array_merge([$phpBinaryPath, $artisanPath, 'serve:http'], $this->serveOptions())),
26 | $socketProcess = new Process([$phpBinaryPath, $artisanPath, 'serve:websockets']),
27 | ];
28 |
29 | while (count($processes)) {
30 | /* @var \Symfony\Component\Process\Process $process */
31 | foreach ($processes as $i => $process) {
32 | if (!$process->isStarted()) {
33 | $process->setTimeout(null);
34 | $process->start();
35 | continue;
36 | }
37 |
38 | if ($info = trim($process->getIncrementalOutput())) {
39 | $this->info($info);
40 | }
41 |
42 | if ($error = trim($process->getIncrementalErrorOutput())) {
43 | $this->line($error);
44 | }
45 |
46 | if (!$process->isRunning()) {
47 | unset($processes[$i]);
48 | }
49 | }
50 | sleep(1);
51 | }
52 | }
53 |
54 | public function serveOptions()
55 | {
56 | return collect($this->options())
57 | ->filter(function ($option) {
58 | return $option;
59 | })->map(function ($value, $key) {
60 | if (is_bool($value)) {
61 | return "--{$key}";
62 | }
63 |
64 | return "--{$key}={$value}";
65 | })->values()->toArray();
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/InjectScriptsToResponseTest.php:
--------------------------------------------------------------------------------
1 | router = Route::middleware('web');
20 | }
21 |
22 | public function getPackageProviders($app)
23 | {
24 | return [
25 | \BangNokia\ServeLiveReload\ResponseServiceProvider::class,
26 | ];
27 | }
28 |
29 | protected function resolveApplicationHttpKernel($app)
30 | {
31 | parent::resolveApplicationHttpKernel($app);
32 | $app->make(\Illuminate\Contracts\Http\Kernel::class);
33 | }
34 |
35 | public function test_it_inject_scripts_to_get_request()
36 | {
37 | $this->router->get('/_test/html', function () {
38 | return 'html';
39 | });
40 |
41 | $this->get('/_test/html')
42 | ->assertSee('ws://127.0.0.1:'.ServeWebSocketsCommand::port());
43 | }
44 |
45 | public function test_it_doesnt_inject_script_to_post_request()
46 | {
47 | $this->router->post('/_test/html', function () {
48 | return 'html';
49 | });
50 |
51 | $this->post('/_test/html')
52 | ->assertSee('html')
53 | ->assertDontSee('ws://127.0.0.1:'.ServeWebSocketsCommand::port());
54 | }
55 |
56 | public function test_it_doesnt_inject_scripts_to_json_response()
57 | {
58 | $this->router->get('/_test/json', function () {
59 | return ['name' => 'John Doe'];
60 | });
61 | $this->router->get('/_test/json2', function () {
62 | return response()->json(['name' => 'John Doe2']);
63 | });
64 |
65 | $this->get('/_test/json')
66 | ->assertJson(['name' => 'John Doe']);
67 |
68 | $this->get('/_test/json2')
69 | ->assertJson(['name' => 'John Doe2']);
70 | }
71 |
72 | public function test_it_doesnt_inject_script_to_ajax_response()
73 | {
74 | $this->router->get('/_test/html', function () {
75 | return 'html string';
76 | });
77 |
78 | $this->get('/_test/html', ['HTTP_X-Requested-With' => 'XMLHttpRequest'])
79 | ->assertSee('html string')
80 | ->assertDontSee('ws://127.0.0.1:'.ServeWebSocketsCommand::port());
81 | }
82 |
83 | public function tearDown(): void
84 | {
85 | parent::tearDown();
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Commands/ServeWebSocketsCommand.php:
--------------------------------------------------------------------------------
1 | loop = LoopFactory::create();
57 |
58 | $this->loop->futureTick(function () {
59 | $this->line('Websockets server started: ws://127.0.0.1:'.self::port());
60 | });
61 |
62 | $this->startWatcher();
63 |
64 | $this->startServer();
65 | }
66 |
67 | protected function startWatcher()
68 | {
69 | $finder = (new Finder())->files()->in($this->dirs());
70 |
71 | (new Watcher($this->loop, $finder))->startWatching(function () {
72 | collect(Socket::$clients)->map(function (ConnectionInterface $client) {
73 | $client->send('reload');
74 | });
75 | });
76 | }
77 |
78 | protected function startServer()
79 | {
80 | try {
81 | $this->server = new IoServer(
82 | new HttpServer(new WsServer(new Socket())),
83 | new Reactor('127.0.0.1:'.self::port(), $this->loop),
84 | $this->loop
85 | );
86 |
87 | $this->server->run();
88 | } catch (\Exception $exception) {
89 | if (self::$offset < 10) {
90 | self::$offset++;
91 | $this->startServer();
92 | }
93 | }
94 | }
95 |
96 | protected function dirs()
97 | {
98 | return array_map(function ($dir) {
99 | return base_path('/'.$dir);
100 | }, config('serve_livereload.dirs'));
101 | }
102 |
103 | public static function port()
104 | {
105 | return self::$port + self::$offset;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------