@rollup-extras/plugin-serve 2.2.0 → 2.3.0

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
@@ -122,6 +122,18 @@ Optional, `(server: Server) => void | true`
122
122
 
123
123
  Extension point after the server is live. Please return true to suppress the default banner.
124
124
 
125
+ ### liveReload
126
+
127
+ Optional, `boolean`, default: `false`.
128
+
129
+ > **Note:** Live reload is currently disabled by default. Set to `true` to opt in.
130
+
131
+ 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.
132
+
133
+ 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.
134
+
135
+ Set to `false` (or leave unset) to disable both the endpoint and the script injection.
136
+
125
137
  ## In-Memory Serving
126
138
 
127
139
  ```javascript
@@ -153,6 +165,7 @@ type ServePluginOptions = {
153
165
  pluginName?: string;
154
166
  useWriteBundle?: boolean;
155
167
  inMemory?: boolean;
168
+ liveReload?: boolean;
156
169
  dirs?: string | string[];
157
170
  port?: number;
158
171
  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)
@@ -88,54 +94,68 @@ export default function (options = {}) {
88
94
  }
89
95
 
90
96
  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;
97
+ if (!watchMode) {
98
+ return;
99
+ }
100
+ if (started) {
101
+ if (liveReload) {
102
+ globalLiveReload?.broadcast('reload');
97
103
  }
104
+ return;
105
+ }
106
+ started = true;
98
107
 
99
- const app = new Hono();
108
+ if (globalServer) {
109
+ globalLiveReload?.close();
110
+ globalLiveReload = undefined;
111
+ await new Promise(resolve => (globalServer).close(resolve));
112
+ globalServer = undefined;
113
+ }
100
114
 
101
- if (customize) {
102
- customize(app);
103
- }
115
+ const app = new Hono();
104
116
 
105
- if (useLogger) {
106
- app.use('*', honoLogger());
107
- }
117
+ if (customize) {
118
+ customize(app);
119
+ }
108
120
 
109
- if (inMemory) {
110
- app.use('*', inMemoryMiddleware(memoryFiles));
111
- }
121
+ if (liveReload) {
122
+ globalLiveReload = createLiveReload();
123
+ globalLiveReload.register(app);
124
+ }
112
125
 
113
- for (const dir of (dirs)) {
114
- app.use('*', serveStatic({ root: dir, ...staticOptions }));
115
- }
126
+ if (useLogger) {
127
+ app.use('*', honoLogger());
128
+ }
116
129
 
117
- const server = createAdaptorServer({
118
- fetch: app.fetch,
119
- ...(https ? { createServer: createHttpsServer, serverOptions: https } : {}),
120
- });
121
- globalServer = server;
130
+ if (inMemory) {
131
+ app.use('*', inMemoryMiddleware(memoryFiles));
132
+ }
122
133
 
123
- const listenCb = () => internalOnListen(server);
134
+ for (const dir of (dirs)) {
135
+ app.use('*', serveStatic({ root: dir, ...staticOptions }));
136
+ }
124
137
 
125
- if (host) {
126
- server.listen(port, host, listenCb);
127
- } else {
128
- server.listen(port, listenCb);
129
- }
138
+ const server = createAdaptorServer({
139
+ fetch: app.fetch,
140
+ ...(https ? { createServer: createHttpsServer, serverOptions: https } : {}),
141
+ });
142
+ globalServer = server;
143
+
144
+ const listenCb = () => internalOnListen(server);
130
145
 
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
- });
146
+ if (host) {
147
+ server.listen(port, host, listenCb);
148
+ } else {
149
+ server.listen(port, listenCb);
138
150
  }
151
+
152
+ server.on('error', ( e) => {
153
+ if (e.code === 'EADDRINUSE') {
154
+ logger.finish('address in use, please try another port', LogLevel.error);
155
+ } else {
156
+ throw e;
157
+ }
158
+ });
139
159
  }
140
160
 
141
161
  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.0",
3
3
  "license": "MIT",
4
4
  "name": "@rollup-extras/plugin-serve",
5
5
  "author": "Konstantin Shutkin",