@rollup-extras/plugin-serve 2.3.0 → 2.4.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
@@ -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.
@@ -64,15 +66,15 @@ Option to use `writeBundle` hook instead of `generateBundle`.
64
66
 
65
67
  Optional, `boolean`, default: `false`.
66
68
 
67
- When enabled, serves all emitted assets directly from memory instead of from disk. This is similar to how webpack-dev-server works — Rollup output never needs to hit the filesystem.
69
+ When enabled, serves all emitted assets directly from memory instead of from disk. This controls how files are served; Rollup still writes output when run with `rollup -cw`.
68
70
 
69
71
  When `inMemory` is `true`, the plugin automatically uses the `generateBundle` hook (regardless of `useWriteBundle`) to capture all chunks and assets in memory. A custom Hono middleware serves these files before falling through to any disk-based static serving configured via `dirs`.
70
72
 
71
- For a fully disk-free dev server, set `watch: { skipWrite: true }` in your Rollup config to prevent Rollup from writing files to disk. The plugin does not force this — you control it.
73
+ For disk-free Rollup output, use the package's [`/watch` helper](#in-memory-serving). Setting `watch: { skipWrite: true }` with Rollup's ordinary watcher skips output generation as well as writing, so the plugin receives no files to serve.
72
74
 
73
75
  **Limitations:**
74
76
  - Only covers Rollup's own output. Other plugins (e.g., plugin-copy, plugin-html) may still perform disk I/O independently.
75
- - Plugins that only use the `writeBundle` hook won't participate when `skipWrite` is enabled.
77
+ - Plugins that only use the `writeBundle` hook won't participate with the `/watch` helper. Configure plugins such as plugin-html to use `generateBundle` when their output should be served from memory.
76
78
 
77
79
  ### dirs
78
80
 
@@ -136,6 +138,8 @@ Set to `false` (or leave unset) to disable both the endpoint and the script inje
136
138
 
137
139
  ## In-Memory Serving
138
140
 
141
+ Keep the Rollup configuration in `rollup.config.js`:
142
+
139
143
  ```javascript
140
144
  import serve from "@rollup-extras/plugin-serve";
141
145
 
@@ -145,13 +149,34 @@ export default {
145
149
  format: "es",
146
150
  dir: "dest",
147
151
  },
148
- watch: {
149
- skipWrite: true, // recommended: avoid writing files to disk
150
- },
151
152
  plugins: [serve({ inMemory: true })],
152
153
  };
153
154
  ```
154
155
 
156
+ Create `dev.js` to start a watcher that generates output without writing it to disk:
157
+
158
+ ```javascript
159
+ import watch from "@rollup-extras/plugin-serve/watch";
160
+ import config from "./rollup.config.js";
161
+
162
+ const watcher = watch(config);
163
+
164
+ watcher.on("event", event => {
165
+ if (event.code === "ERROR") console.error(event.error);
166
+ });
167
+
168
+ process.once("SIGINT", () => watcher.close());
169
+ process.once("SIGTERM", () => watcher.close());
170
+ ```
171
+
172
+ Run `node dev.js`. The helper accepts one Rollup configuration or an array, generates all configured outputs on each rebuild, and forces `watch.skipWrite` without mutating the supplied configuration. Configurations with `watch: false` are ignored.
173
+
174
+ It returns a Rollup watcher. Use its `END` event to observe completion of generation and build cleanup. The helper owns the build results, so consumers should not call `event.result.generate()` or `event.result.close()`. Output errors are reported through the usual `ERROR` event, and later file changes can trigger another build. `await watcher.close()` waits for pending generation and shuts down the server.
175
+
176
+ Register watched dependencies in build hooks, such as `buildStart` or `transform`. With `skipWrite`, Rollup collects watch files before `BUNDLE_END`; files first registered in output hooks during the helper's generation are not picked up by the watcher.
177
+
178
+ Running the same configuration with `rollup -cw` also serves from memory, while continuing to write the usual output files.
179
+
155
180
  Hybrid mode — bundle from memory, static assets from disk:
156
181
 
157
182
  ```javascript
package/index.d.ts CHANGED
@@ -41,3 +41,14 @@ declare module '@rollup-extras/plugin-serve' {
41
41
 
42
42
  export {};
43
43
  }
44
+
45
+ declare module '@rollup-extras/plugin-serve/watch' {
46
+ import type { RollupWatcher, RollupWatchOptions } from 'rollup';
47
+ /**
48
+ * Watch and generate Rollup output in memory, closing each build result automatically.
49
+ * Configure `serve({ inMemory: true })` in the supplied Rollup configuration.
50
+ */
51
+ export default function watch(config: RollupWatchOptions | RollupWatchOptions[]): RollupWatcher;
52
+
53
+ export {};
54
+ }
package/index.js CHANGED
@@ -41,12 +41,16 @@ export default function (options = {}) {
41
41
 
42
42
  const memoryFiles = new Map();
43
43
 
44
+ let currentInputOptions;
45
+
46
+ let server;
47
+
44
48
  let { dirs } = normalizedOptions,
45
49
  collectDirs = false,
46
50
  started = false,
47
51
  watchMode = true;
48
52
 
49
- const pluginInstance = { ...instance, outputOptions };
53
+ const pluginInstance = { ...instance, outputOptions, closeWatcher };
50
54
 
51
55
  if (!dirs) {
52
56
  dirs = [];
@@ -61,8 +65,9 @@ export default function (options = {}) {
61
65
  function renderStart( outputOptions, inputOptions) {
62
66
 
63
67
  ( (instance).renderStart).call(this, outputOptions, inputOptions);
64
- if (inMemory) {
68
+ if (inMemory && currentInputOptions !== inputOptions) {
65
69
  memoryFiles.clear();
70
+ currentInputOptions = inputOptions;
66
71
  }
67
72
  if (collectDirs) {
68
73
  if (outputOptions.dir) {
@@ -135,13 +140,13 @@ export default function (options = {}) {
135
140
  app.use('*', serveStatic({ root: dir, ...staticOptions }));
136
141
  }
137
142
 
138
- const server = createAdaptorServer({
143
+ server = createAdaptorServer({
139
144
  fetch: app.fetch,
140
145
  ...(https ? { createServer: createHttpsServer, serverOptions: https } : {}),
141
146
  });
142
147
  globalServer = server;
143
148
 
144
- const listenCb = () => internalOnListen(server);
149
+ const listenCb = () => internalOnListen( (server));
145
150
 
146
151
  if (host) {
147
152
  server.listen(port, host, listenCb);
@@ -158,6 +163,14 @@ export default function (options = {}) {
158
163
  });
159
164
  }
160
165
 
166
+ async function closeWatcher() {
167
+ if (!server || globalServer !== server) return;
168
+ globalLiveReload?.close();
169
+ globalLiveReload = undefined;
170
+ globalServer = undefined;
171
+ await new Promise(resolve => (server).close(resolve));
172
+ }
173
+
161
174
  function internalOnListen(server) {
162
175
  if (!onListen?.( (server))) {
163
176
  logger.finish(`listening on ${linkFromAddress(server.address(), !!https)}`, LogLevel.info);
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.3.0",
2
+ "version": "2.4.0",
3
3
  "license": "MIT",
4
4
  "name": "@rollup-extras/plugin-serve",
5
5
  "author": "Konstantin Shutkin",
@@ -10,6 +10,10 @@
10
10
  "types": "./index.d.ts",
11
11
  "default": "./index.js"
12
12
  },
13
+ "./watch": {
14
+ "types": "./index.d.ts",
15
+ "default": "./watch.js"
16
+ },
13
17
  "./package.json": "./package.json"
14
18
  },
15
19
  "main": "./index.js",
@@ -0,0 +1,7 @@
1
+ import type { RollupWatcher, RollupWatchOptions } from 'rollup';
2
+
3
+ /**
4
+ * Watch and generate Rollup output in memory, closing each build result automatically.
5
+ * Configure `serve({ inMemory: true })` in the supplied Rollup configuration.
6
+ */
7
+ export default function watch(config: RollupWatchOptions | RollupWatchOptions[]): RollupWatcher;
package/watch.js ADDED
@@ -0,0 +1,93 @@
1
+ import { watch as rollupWatch } from 'rollup';
2
+
3
+ export default function watch(config) {
4
+
5
+ let outputs = [];
6
+ let closing = false;
7
+
8
+ let finishBuild;
9
+ let pending = Promise.resolve();
10
+
11
+ let closed;
12
+
13
+ let ready;
14
+ const initialized = new Promise(resolve => {
15
+ ready = () => resolve(undefined);
16
+ });
17
+ const configs = Array.isArray(config) ? config : [config];
18
+ const watcher = rollupWatch(
19
+ configs.map(options => ({
20
+ ...options,
21
+ watch: options.watch === false ? false : { ...options.watch, skipWrite: true },
22
+ plugins: [
23
+ options.plugins,
24
+ {
25
+ name: '@rollup-extras/plugin-serve/watch',
26
+ buildStart() {
27
+ outputs = Array.isArray(options.output) ? options.output : [options.output || {}];
28
+ },
29
+ },
30
+ ],
31
+ }))
32
+ );
33
+ watcher.on('event', async event => {
34
+ if (event.code === 'BUNDLE_START') {
35
+ pending = new Promise(resolve => {
36
+ finishBuild = resolve;
37
+ });
38
+ ready?.();
39
+ } else if (event.code === 'BUNDLE_END') {
40
+ try {
41
+ await generate(event.result, outputs);
42
+ } finally {
43
+ finishBuild?.();
44
+ }
45
+ } else if (event.code === 'ERROR') {
46
+ try {
47
+ await event.result?.close();
48
+ } finally {
49
+ finishBuild?.();
50
+ }
51
+ }
52
+ });
53
+
54
+ function close() {
55
+ closing = true;
56
+ closed ||= (async () => {
57
+
58
+ await initialized;
59
+ await pending;
60
+ await watcher.close();
61
+ })();
62
+ return closed;
63
+ }
64
+
65
+ const managed = new Proxy(watcher, {
66
+ get(target, key) {
67
+ if (key === 'close') return close;
68
+ const value = Reflect.get(target, key);
69
+ if (typeof value !== 'function') return value;
70
+
71
+ function call(...args) {
72
+ const result = value.apply(target, args);
73
+ return result === target ? managed : result;
74
+ }
75
+ return call;
76
+ },
77
+ });
78
+ return managed;
79
+
80
+ async function generate(result, outputOptions) {
81
+ try {
82
+ if (!closing) {
83
+
84
+ const generated = await Promise.allSettled(outputOptions.map(output => result.generate(output)));
85
+ for (const output of generated) {
86
+ if (output.status === 'rejected') throw output.reason;
87
+ }
88
+ }
89
+ } finally {
90
+ await result.close();
91
+ }
92
+ }
93
+ }