@rollup-extras/plugin-serve 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![Coverage](https://raw.githubusercontent.com/kshutkin/rollup-extras/refs/heads/coverage/plugin-serve/badge.svg)
2
+
1
3
  # Plugin Serve
2
4
 
3
5
  Rollup plugin for a dev server.
@@ -122,6 +124,18 @@ Optional, `(server: Server) => void | true`
122
124
 
123
125
  Extension point after the server is live. Please return true to suppress the default banner.
124
126
 
127
+ ### liveReload
128
+
129
+ Optional, `boolean`, default: `false`.
130
+
131
+ > **Note:** Live reload is currently disabled by default. Set to `true` to opt in.
132
+
133
+ When enabled and Rollup is running in watch mode, the plugin exposes a Server-Sent Events endpoint at `/__livereload` and injects a tiny client script into every served `text/html` response. After each rebuild the server broadcasts a `reload` event that triggers `location.reload()` in the browser.
134
+
135
+ Injection happens uniformly for in-memory HTML, disk-served HTML (via `dirs`), and any HTML returned by routes registered in `customize`. Non-HTML responses are left unchanged.
136
+
137
+ Set to `false` (or leave unset) to disable both the endpoint and the script injection.
138
+
125
139
  ## In-Memory Serving
126
140
 
127
141
  ```javascript
@@ -153,6 +167,7 @@ type ServePluginOptions = {
153
167
  pluginName?: string;
154
168
  useWriteBundle?: boolean;
155
169
  inMemory?: boolean;
170
+ liveReload?: boolean;
156
171
  dirs?: string | string[];
157
172
  port?: number;
158
173
  useLogger?: boolean;
package/exports.d.ts CHANGED
@@ -8,6 +8,20 @@ export type ServePluginOptionsObject = {
8
8
  pluginName?: string;
9
9
  useWriteBundle?: boolean;
10
10
  inMemory?: boolean;
11
+ /**
12
+ * Enable live reload over Server-Sent Events. Defaults to `false`.
13
+ *
14
+ * Live reload is currently disabled by default. Set to `true` to opt in.
15
+ *
16
+ * When enabled and Rollup is running in watch mode, a small client script
17
+ * is injected into every served `text/html` response and subscribes to an
18
+ * SSE endpoint at `/__livereload`. After every rebuild the server
19
+ * broadcasts a `reload` event that triggers `location.reload()` in the
20
+ * browser.
21
+ *
22
+ * Set to `false` (or leave unset) to disable injection and the SSE endpoint.
23
+ */
24
+ liveReload?: boolean;
11
25
  dirs?: string | string[];
12
26
  port?: number;
13
27
  useLogger?: boolean;
package/index.d.ts CHANGED
@@ -7,6 +7,20 @@ declare module '@rollup-extras/plugin-serve' {
7
7
  pluginName?: string;
8
8
  useWriteBundle?: boolean;
9
9
  inMemory?: boolean;
10
+ /**
11
+ * Enable live reload over Server-Sent Events. Defaults to `false`.
12
+ *
13
+ * Live reload is currently disabled by default. Set to `true` to opt in.
14
+ *
15
+ * When enabled and Rollup is running in watch mode, a small client script
16
+ * is injected into every served `text/html` response and subscribes to an
17
+ * SSE endpoint at `/__livereload`. After every rebuild the server
18
+ * broadcasts a `reload` event that triggers `location.reload()` in the
19
+ * browser.
20
+ *
21
+ * Set to `false` (or leave unset) to disable injection and the SSE endpoint.
22
+ */
23
+ liveReload?: boolean;
10
24
  dirs?: string | string[];
11
25
  port?: number;
12
26
  useLogger?: boolean;
package/index.js CHANGED
@@ -11,8 +11,12 @@ import logger from '@rollup-extras/utils/logger';
11
11
  import { multiConfigPluginBase } from '@rollup-extras/utils/multi-config-plugin-base';
12
12
  import { getOptions } from '@rollup-extras/utils/options';
13
13
 
14
+ import { createLiveReload } from './livereload.js';
15
+
14
16
  let globalServer;
15
17
 
18
+ let globalLiveReload;
19
+
16
20
  const factories = { logger };
17
21
 
18
22
  export default function (options = {}) {
@@ -23,11 +27,13 @@ export default function (options = {}) {
23
27
  useWriteBundle: true,
24
28
  port: 8080,
25
29
  useLogger: true,
30
+ liveReload: false,
26
31
  },
27
32
  'dirs',
28
33
  factories
29
34
  );
30
- const { pluginName, port, host, https, useLogger, customize, staticOptions, onListen, logger, inMemory } = normalizedOptions;
35
+ const { pluginName, port, host, https, useLogger, customize, staticOptions, onListen, logger, inMemory, liveReload } =
36
+ normalizedOptions;
31
37
  const useWriteBundle = inMemory ? false : normalizedOptions.useWriteBundle;
32
38
  const instance = inMemory
33
39
  ? multiConfigPluginBase(useWriteBundle, pluginName, serve, captureOutput)
@@ -35,6 +41,8 @@ export default function (options = {}) {
35
41
 
36
42
  const memoryFiles = new Map();
37
43
 
44
+ let currentInputOptions;
45
+
38
46
  let { dirs } = normalizedOptions,
39
47
  collectDirs = false,
40
48
  started = false,
@@ -55,8 +63,9 @@ export default function (options = {}) {
55
63
  function renderStart( outputOptions, inputOptions) {
56
64
 
57
65
  ( (instance).renderStart).call(this, outputOptions, inputOptions);
58
- if (inMemory) {
66
+ if (inMemory && currentInputOptions !== inputOptions) {
59
67
  memoryFiles.clear();
68
+ currentInputOptions = inputOptions;
60
69
  }
61
70
  if (collectDirs) {
62
71
  if (outputOptions.dir) {
@@ -88,54 +97,68 @@ export default function (options = {}) {
88
97
  }
89
98
 
90
99
  async function serve() {
91
- if (!started && watchMode) {
92
- started = true;
93
-
94
- if (globalServer) {
95
- await new Promise(resolve => (globalServer).close(resolve));
96
- globalServer = undefined;
100
+ if (!watchMode) {
101
+ return;
102
+ }
103
+ if (started) {
104
+ if (liveReload) {
105
+ globalLiveReload?.broadcast('reload');
97
106
  }
107
+ return;
108
+ }
109
+ started = true;
98
110
 
99
- const app = new Hono();
111
+ if (globalServer) {
112
+ globalLiveReload?.close();
113
+ globalLiveReload = undefined;
114
+ await new Promise(resolve => (globalServer).close(resolve));
115
+ globalServer = undefined;
116
+ }
100
117
 
101
- if (customize) {
102
- customize(app);
103
- }
118
+ const app = new Hono();
104
119
 
105
- if (useLogger) {
106
- app.use('*', honoLogger());
107
- }
120
+ if (customize) {
121
+ customize(app);
122
+ }
108
123
 
109
- if (inMemory) {
110
- app.use('*', inMemoryMiddleware(memoryFiles));
111
- }
124
+ if (liveReload) {
125
+ globalLiveReload = createLiveReload();
126
+ globalLiveReload.register(app);
127
+ }
112
128
 
113
- for (const dir of (dirs)) {
114
- app.use('*', serveStatic({ root: dir, ...staticOptions }));
115
- }
129
+ if (useLogger) {
130
+ app.use('*', honoLogger());
131
+ }
132
+
133
+ if (inMemory) {
134
+ app.use('*', inMemoryMiddleware(memoryFiles));
135
+ }
116
136
 
117
- const server = createAdaptorServer({
118
- fetch: app.fetch,
119
- ...(https ? { createServer: createHttpsServer, serverOptions: https } : {}),
120
- });
121
- globalServer = server;
137
+ for (const dir of (dirs)) {
138
+ app.use('*', serveStatic({ root: dir, ...staticOptions }));
139
+ }
122
140
 
123
- const listenCb = () => internalOnListen(server);
141
+ const server = createAdaptorServer({
142
+ fetch: app.fetch,
143
+ ...(https ? { createServer: createHttpsServer, serverOptions: https } : {}),
144
+ });
145
+ globalServer = server;
124
146
 
125
- if (host) {
126
- server.listen(port, host, listenCb);
127
- } else {
128
- server.listen(port, listenCb);
129
- }
147
+ const listenCb = () => internalOnListen(server);
130
148
 
131
- server.on('error', ( e) => {
132
- if (e.code === 'EADDRINUSE') {
133
- logger.finish('address in use, please try another port', LogLevel.error);
134
- } else {
135
- throw e;
136
- }
137
- });
149
+ if (host) {
150
+ server.listen(port, host, listenCb);
151
+ } else {
152
+ server.listen(port, listenCb);
138
153
  }
154
+
155
+ server.on('error', ( e) => {
156
+ if (e.code === 'EADDRINUSE') {
157
+ logger.finish('address in use, please try another port', LogLevel.error);
158
+ } else {
159
+ throw e;
160
+ }
161
+ });
139
162
  }
140
163
 
141
164
  function internalOnListen(server) {
package/livereload.js ADDED
@@ -0,0 +1,90 @@
1
+ import { streamSSE } from 'hono/streaming';
2
+
3
+ const ENDPOINT = '/__livereload';
4
+ const KEEPALIVE_MS = 25000;
5
+
6
+ const CLIENT_SCRIPT = `<script>(function(){try{var es=new EventSource(${JSON.stringify(ENDPOINT)});es.addEventListener('reload',function(){location.reload();});}catch(e){}})();</script>`;
7
+
8
+ export function createLiveReload() {
9
+
10
+ const clients = new Set();
11
+ let closed = false;
12
+
13
+ return { register, broadcast, close };
14
+
15
+ function register(app) {
16
+ app.get(ENDPOINT, c => {
17
+ return streamSSE(c, async stream => {
18
+ clients.add(stream);
19
+ stream.onAbort(() => {
20
+ clients.delete(stream);
21
+ });
22
+ while (!closed && !stream.aborted && !stream.closed) {
23
+ await new Promise(res => setTimeout(res, KEEPALIVE_MS));
24
+ if (closed || stream.aborted || stream.closed) break;
25
+ try {
26
+ await stream.writeSSE({ event: 'ping', data: '' });
27
+ } catch {
28
+ break;
29
+ }
30
+ }
31
+ clients.delete(stream);
32
+ });
33
+ });
34
+
35
+ app.use('*', async (c, next) => {
36
+ await next();
37
+ const res = c.res;
38
+ if (!res) return;
39
+ const contentType = res.headers.get('content-type') || '';
40
+ if (!contentType.toLowerCase().includes('text/html')) return;
41
+ let body;
42
+ try {
43
+ body = await res.text();
44
+ } catch {
45
+ return;
46
+ }
47
+ const injected = inject(body);
48
+ res.headers.delete('content-length');
49
+ c.res = new Response(injected, {
50
+ status: res.status,
51
+ statusText: res.statusText,
52
+ headers: { 'content-length': String(Buffer.byteLength(injected)) },
53
+ });
54
+ });
55
+ }
56
+
57
+ function broadcast(event) {
58
+ for (const stream of clients) {
59
+ try {
60
+ stream.writeSSE({ event, data: '' });
61
+ } catch {
62
+ clients.delete(stream);
63
+ }
64
+ }
65
+ }
66
+
67
+ function close() {
68
+ closed = true;
69
+ for (const stream of clients) {
70
+ try {
71
+ stream.close();
72
+ } catch {
73
+
74
+ }
75
+ }
76
+ clients.clear();
77
+ }
78
+ }
79
+
80
+ function inject(html) {
81
+ if (html.includes(ENDPOINT)) {
82
+ return html;
83
+ }
84
+ const match = html.match(/<\/body\s*>/i);
85
+ if (match) {
86
+ const idx = (match.index);
87
+ return html.slice(0, idx) + CLIENT_SCRIPT + html.slice(idx);
88
+ }
89
+ return html + CLIENT_SCRIPT;
90
+ }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0",
2
+ "version": "2.3.1",
3
3
  "license": "MIT",
4
4
  "name": "@rollup-extras/plugin-serve",
5
5
  "author": "Konstantin Shutkin",