@modern-js/server 3.5.0 → 3.7.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.
@@ -0,0 +1,186 @@
1
+ import "node:module";
2
+ import { logger } from "@modern-js/utils";
3
+ function _check_private_redeclaration(obj, privateCollection) {
4
+ if (privateCollection.has(obj)) throw new TypeError("Cannot initialize the same private elements twice on an object");
5
+ }
6
+ function _class_apply_descriptor_get(receiver, descriptor) {
7
+ if (descriptor.get) return descriptor.get.call(receiver);
8
+ return descriptor.value;
9
+ }
10
+ function _class_apply_descriptor_set(receiver, descriptor, value) {
11
+ if (descriptor.set) descriptor.set.call(receiver, value);
12
+ else {
13
+ if (!descriptor.writable) throw new TypeError("attempted to set read only private field");
14
+ descriptor.value = value;
15
+ }
16
+ }
17
+ function _class_extract_field_descriptor(receiver, privateMap, action) {
18
+ if (!privateMap.has(receiver)) throw new TypeError("attempted to " + action + " private field on non-instance");
19
+ return privateMap.get(receiver);
20
+ }
21
+ function _class_private_field_get(receiver, privateMap) {
22
+ var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get");
23
+ return _class_apply_descriptor_get(receiver, descriptor);
24
+ }
25
+ function _class_private_field_init(obj, privateMap, value) {
26
+ _check_private_redeclaration(obj, privateMap);
27
+ privateMap.set(obj, value);
28
+ }
29
+ function _class_private_field_set(receiver, privateMap, value) {
30
+ var descriptor = _class_extract_field_descriptor(receiver, privateMap, "set");
31
+ _class_apply_descriptor_set(receiver, descriptor, value);
32
+ return value;
33
+ }
34
+ function _class_private_method_get(receiver, privateSet, fn) {
35
+ if (!privateSet.has(receiver)) throw new TypeError("attempted to get private field on non-instance");
36
+ return fn;
37
+ }
38
+ function _class_private_method_init(obj, privateSet) {
39
+ _check_private_redeclaration(obj, privateSet);
40
+ privateSet.add(obj);
41
+ }
42
+ const DEFAULT_DEBOUNCE_MS = 300;
43
+ const notReadyHandle = ()=>new Response('Dev server is starting…', {
44
+ status: 503,
45
+ headers: {
46
+ 'content-type': 'text/plain; charset=utf-8'
47
+ }
48
+ });
49
+ var _current = /*#__PURE__*/ new WeakMap(), _build = /*#__PURE__*/ new WeakMap(), _debounceMs = /*#__PURE__*/ new WeakMap(), _onReload = /*#__PURE__*/ new WeakMap(), _onError = /*#__PURE__*/ new WeakMap(), _onReloadError = /*#__PURE__*/ new WeakMap(), _debounceTimer = /*#__PURE__*/ new WeakMap(), _running = /*#__PURE__*/ new WeakMap(), _pending = /*#__PURE__*/ new WeakMap(), _runningPromise = /*#__PURE__*/ new WeakMap(), _closed = /*#__PURE__*/ new WeakMap(), _runLoop = /*#__PURE__*/ new WeakSet(), _reportBuildError = /*#__PURE__*/ new WeakSet(), _reportReloadCallbackError = /*#__PURE__*/ new WeakSet();
50
+ class ReloadManager {
51
+ setHandle(handle) {
52
+ _class_private_field_set(this, _current, handle);
53
+ }
54
+ get handle() {
55
+ return (request, ...args)=>_class_private_field_get(this, _current).call(this, request, ...args);
56
+ }
57
+ get currentHandle() {
58
+ return _class_private_field_get(this, _current);
59
+ }
60
+ get isReloading() {
61
+ return _class_private_field_get(this, _running);
62
+ }
63
+ schedule() {
64
+ if (_class_private_field_get(this, _closed)) return;
65
+ if (_class_private_field_get(this, _debounceTimer)) clearTimeout(_class_private_field_get(this, _debounceTimer));
66
+ _class_private_field_set(this, _debounceTimer, setTimeout(()=>{
67
+ _class_private_field_set(this, _debounceTimer, null);
68
+ this.reloadNow();
69
+ }, _class_private_field_get(this, _debounceMs)));
70
+ }
71
+ async reloadNow() {
72
+ if (_class_private_field_get(this, _closed)) return;
73
+ if (_class_private_field_get(this, _running)) {
74
+ _class_private_field_set(this, _pending, true);
75
+ return _class_private_field_get(this, _runningPromise) ?? Promise.resolve();
76
+ }
77
+ _class_private_field_set(this, _running, true);
78
+ _class_private_field_set(this, _runningPromise, _class_private_method_get(this, _runLoop, runLoop).call(this));
79
+ try {
80
+ await _class_private_field_get(this, _runningPromise);
81
+ } finally{
82
+ _class_private_field_set(this, _running, false);
83
+ _class_private_field_set(this, _runningPromise, null);
84
+ }
85
+ }
86
+ close() {
87
+ _class_private_field_set(this, _closed, true);
88
+ _class_private_field_set(this, _pending, false);
89
+ if (_class_private_field_get(this, _debounceTimer)) {
90
+ clearTimeout(_class_private_field_get(this, _debounceTimer));
91
+ _class_private_field_set(this, _debounceTimer, null);
92
+ }
93
+ }
94
+ constructor(options){
95
+ _class_private_method_init(this, _runLoop);
96
+ _class_private_method_init(this, _reportBuildError);
97
+ _class_private_method_init(this, _reportReloadCallbackError);
98
+ _class_private_field_init(this, _current, {
99
+ writable: true,
100
+ value: void 0
101
+ });
102
+ _class_private_field_init(this, _build, {
103
+ writable: true,
104
+ value: void 0
105
+ });
106
+ _class_private_field_init(this, _debounceMs, {
107
+ writable: true,
108
+ value: void 0
109
+ });
110
+ _class_private_field_init(this, _onReload, {
111
+ writable: true,
112
+ value: void 0
113
+ });
114
+ _class_private_field_init(this, _onError, {
115
+ writable: true,
116
+ value: void 0
117
+ });
118
+ _class_private_field_init(this, _onReloadError, {
119
+ writable: true,
120
+ value: void 0
121
+ });
122
+ _class_private_field_init(this, _debounceTimer, {
123
+ writable: true,
124
+ value: void 0
125
+ });
126
+ _class_private_field_init(this, _running, {
127
+ writable: true,
128
+ value: void 0
129
+ });
130
+ _class_private_field_init(this, _pending, {
131
+ writable: true,
132
+ value: void 0
133
+ });
134
+ _class_private_field_init(this, _runningPromise, {
135
+ writable: true,
136
+ value: void 0
137
+ });
138
+ _class_private_field_init(this, _closed, {
139
+ writable: true,
140
+ value: void 0
141
+ });
142
+ _class_private_field_set(this, _debounceTimer, null);
143
+ _class_private_field_set(this, _running, false);
144
+ _class_private_field_set(this, _pending, false);
145
+ _class_private_field_set(this, _runningPromise, null);
146
+ _class_private_field_set(this, _closed, false);
147
+ _class_private_field_set(this, _current, options.initialHandle ?? notReadyHandle);
148
+ _class_private_field_set(this, _build, options.build);
149
+ _class_private_field_set(this, _debounceMs, options.debounceMs ?? DEFAULT_DEBOUNCE_MS);
150
+ _class_private_field_set(this, _onReload, options.onReload);
151
+ _class_private_field_set(this, _onError, options.onError);
152
+ _class_private_field_set(this, _onReloadError, options.onReloadError);
153
+ }
154
+ }
155
+ async function runLoop() {
156
+ var _this, _this1;
157
+ do {
158
+ _class_private_field_set(this, _pending, false);
159
+ let next;
160
+ try {
161
+ next = await _class_private_field_get(this, _build).call(this);
162
+ } catch (error) {
163
+ _class_private_method_get(this, _reportBuildError, reportBuildError).call(this, error);
164
+ continue;
165
+ }
166
+ if (_class_private_field_get(this, _closed)) return;
167
+ _class_private_field_set(this, _current, next);
168
+ try {
169
+ null == (_this = _class_private_field_get(_this1 = this, _onReload)) || _this.call(_this1, next);
170
+ } catch (callbackError) {
171
+ _class_private_method_get(this, _reportReloadCallbackError, reportReloadCallbackError).call(this, callbackError);
172
+ }
173
+ }while (_class_private_field_get(this, _pending) && !_class_private_field_get(this, _closed));
174
+ }
175
+ function reportBuildError(error) {
176
+ if (_class_private_field_get(this, _onError)) _class_private_field_get(this, _onError).call(this, error);
177
+ else logger.error(`[dev-server] runtime reload build failed, keep serving previous handle:\n${error instanceof Error ? error.stack ?? error.message : error}`);
178
+ }
179
+ function reportReloadCallbackError(error) {
180
+ if (_class_private_field_get(this, _onReloadError)) _class_private_field_get(this, _onReloadError).call(this, error);
181
+ else logger.warn(`[dev-server] onReload callback failed after a successful swap (handle is already active):\n${error instanceof Error ? error.stack ?? error.message : error}`);
182
+ }
183
+ function createReloadManager(options) {
184
+ return new ReloadManager(options);
185
+ }
186
+ export { ReloadManager, createReloadManager };
@@ -0,0 +1,36 @@
1
+ import "node:module";
2
+ function createRuntimeServerOptions(base, assetPrefix) {
3
+ const baseConfig = base.config ?? {};
4
+ const baseOutput = baseConfig.output ?? {};
5
+ const baseServerConfig = base.serverConfig ?? {};
6
+ return {
7
+ ...base,
8
+ config: {
9
+ ...baseConfig,
10
+ output: {
11
+ ...baseOutput,
12
+ ...assetPrefix ? {
13
+ assetPrefix
14
+ } : {}
15
+ }
16
+ },
17
+ serverConfig: {
18
+ ...baseServerConfig,
19
+ middlewares: [
20
+ ...baseServerConfig.middlewares ?? []
21
+ ],
22
+ renderMiddlewares: [
23
+ ...baseServerConfig.renderMiddlewares ?? []
24
+ ],
25
+ ...baseServerConfig.plugins ? {
26
+ plugins: [
27
+ ...baseServerConfig.plugins
28
+ ]
29
+ } : {}
30
+ },
31
+ plugins: [
32
+ ...base.plugins ?? []
33
+ ]
34
+ };
35
+ }
36
+ export { createRuntimeServerOptions };
@@ -1,41 +1,17 @@
1
1
  import "node:module";
2
+ import node_path from "node:path";
3
+ import { AGGRED_DIR } from "@modern-js/server-core";
2
4
  import { connectMid2HonoMid } from "@modern-js/server-core/node";
3
- import { API_DIR, SHARED_DIR } from "@modern-js/utils";
5
+ import { API_DIR, SHARED_DIR, logger } from "@modern-js/utils";
4
6
  import { getDevOptions, getMockMiddleware, initFileReader, onRepack, startWatcher } from "./helpers/index.mjs";
5
- const devPlugin = (options, compiler)=>({
7
+ const devRuntimeMiddlewarePlugin = (options, compiler)=>({
6
8
  name: '@modern-js/plugin-dev',
7
9
  setup (api) {
8
- const { config, pwd, builder, builderDevServer } = options;
9
- const closeCb = [];
10
+ const { pwd, builderDevServer } = options;
10
11
  const dev = getDevOptions(options.dev);
11
12
  api.onPrepare(async ()=>{
12
- const { middlewares: builderMiddlewares, close, connectWebSocket } = builderDevServer || {};
13
- close && closeCb.push(close);
14
- const { middlewares, distDirectory, nodeServer, apiDirectory, sharedDirectory, serverBase } = api.getServerContext();
15
- connectWebSocket && nodeServer && connectWebSocket({
16
- server: nodeServer
17
- });
18
- const hooks = api.getHooks();
19
- builder?.onDevCompileDone(({ stats })=>{
20
- if ('server' !== stats.toJson({
21
- all: false
22
- }).name) onRepack(distDirectory, hooks);
23
- });
24
- const { watchOptions } = config.server;
25
- const watcher = startWatcher({
26
- pwd,
27
- distDir: distDirectory,
28
- apiDir: apiDirectory || API_DIR,
29
- sharedDir: sharedDirectory || SHARED_DIR,
30
- watchOptions,
31
- server: serverBase
32
- });
33
- closeCb.push(watcher.close.bind(watcher));
34
- closeCb.length > 0 && nodeServer?.on('close', ()=>{
35
- closeCb.forEach((cb)=>{
36
- cb();
37
- });
38
- });
13
+ const { middlewares: builderMiddlewares } = builderDevServer || {};
14
+ const { middlewares } = api.getServerContext();
39
15
  const before = [];
40
16
  const after = [];
41
17
  const { setupMiddlewares = [] } = dev;
@@ -75,4 +51,58 @@ const devPlugin = (options, compiler)=>({
75
51
  });
76
52
  }
77
53
  });
78
- export { devPlugin };
54
+ function setupDevInfra({ config, pwd, distDir, apiDir, sharedDir, builder, builderDevServer, getRuntimeServer, onFileChange, onClose, nodeServer }) {
55
+ const { close, connectWebSocket } = builderDevServer || {};
56
+ const closeCb = [];
57
+ close && closeCb.push(close);
58
+ connectWebSocket && nodeServer && connectWebSocket({
59
+ server: nodeServer
60
+ });
61
+ builder?.onDevCompileDone(({ stats })=>{
62
+ if ('server' !== stats.toJson({
63
+ all: false
64
+ }).name) {
65
+ const runtimeServer = getRuntimeServer();
66
+ if (runtimeServer) onRepack(distDir, runtimeServer.hooks);
67
+ }
68
+ });
69
+ const { watchOptions } = config.server;
70
+ const mockPath = node_path.normalize(node_path.join(pwd, AGGRED_DIR.mock));
71
+ const watcher = startWatcher({
72
+ pwd,
73
+ distDir,
74
+ apiDir: apiDir || API_DIR,
75
+ sharedDir: sharedDir || SHARED_DIR,
76
+ watchOptions,
77
+ onChange: (filepath, event)=>{
78
+ const runtimeServer = getRuntimeServer();
79
+ if (runtimeServer && !filepath.startsWith(mockPath)) {
80
+ const fileChangeEvent = {
81
+ type: 'file-change',
82
+ payload: [
83
+ {
84
+ filename: filepath,
85
+ event
86
+ }
87
+ ]
88
+ };
89
+ Promise.resolve(runtimeServer.hooks.onReset.call({
90
+ event: fileChangeEvent
91
+ })).catch((error)=>logger.error(error));
92
+ }
93
+ onFileChange(filepath, event);
94
+ }
95
+ });
96
+ closeCb.push(watcher.close.bind(watcher));
97
+ onClose && closeCb.push(onClose);
98
+ const close$ = ()=>{
99
+ closeCb.forEach((cb)=>{
100
+ cb();
101
+ });
102
+ };
103
+ closeCb.length > 0 && nodeServer?.on('close', close$);
104
+ return {
105
+ close: close$
106
+ };
107
+ }
108
+ export { devRuntimeMiddlewarePlugin, setupDevInfra };
@@ -2,40 +2,13 @@ import __rslib_shim_module__ from "node:module";
2
2
  const require = /*#__PURE__*/ __rslib_shim_module__.createRequire(/*#__PURE__*/ (()=>import.meta.url)());
3
3
  import path from "path";
4
4
  import { AGGRED_DIR } from "@modern-js/server-core";
5
- import { SERVER_BUNDLE_DIRECTORY, SERVER_DIR, logger } from "@modern-js/utils";
5
+ import { SERVER_BUNDLE_DIRECTORY, SERVER_DIR } from "@modern-js/utils";
6
6
  import dev_tools_watcher, { mergeWatchOptions } from "../dev-tools/watcher/index.mjs";
7
- import { initOrUpdateMockMiddlewares } from "./mock.mjs";
8
- import { debug } from "./utils.mjs";
9
7
  export * from "./repack.mjs";
10
8
  export * from "./devOptions.mjs";
11
9
  export * from "./fileReader.mjs";
12
10
  export * from "./mock.mjs";
13
- async function onServerChange({ pwd, filepath, event, server }) {
14
- const { mock } = AGGRED_DIR;
15
- const mockPath = path.normalize(path.join(pwd, mock));
16
- const { hooks } = server;
17
- if (filepath.startsWith(mockPath)) {
18
- await initOrUpdateMockMiddlewares(pwd);
19
- logger.info('Finish update the mock handlers');
20
- } else try {
21
- const fileChangeEvent = {
22
- type: 'file-change',
23
- payload: [
24
- {
25
- filename: filepath,
26
- event
27
- }
28
- ]
29
- };
30
- await hooks.onReset.call({
31
- event: fileChangeEvent
32
- });
33
- debug(`Finish reload server, trigger by ${filepath} ${event}`);
34
- } catch (e) {
35
- logger.error(e);
36
- }
37
- }
38
- function startWatcher({ pwd, distDir, apiDir, sharedDir, watchOptions, server }) {
11
+ function startWatcher({ pwd, distDir, apiDir, sharedDir, watchOptions, onChange }) {
39
12
  const { mock } = AGGRED_DIR;
40
13
  const defaultWatched = [
41
14
  `${mock}/**/*`,
@@ -55,12 +28,7 @@ function startWatcher({ pwd, distDir, apiDir, sharedDir, watchOptions, server })
55
28
  if (filepath.includes('-server-loaders.js')) return void delete require.cache[filepath];
56
29
  watcher.updateDepTree();
57
30
  watcher.cleanDepCache(filepath);
58
- onServerChange({
59
- pwd,
60
- filepath,
61
- event,
62
- server
63
- });
31
+ onChange(filepath, event);
64
32
  });
65
33
  return watcher;
66
34
  }
@@ -2,7 +2,7 @@ import "node:module";
2
2
  import node_path from "node:path";
3
3
  import { AGGRED_DIR } from "@modern-js/server-core";
4
4
  import { connectMockMid2HonoMid } from "@modern-js/server-core/node";
5
- import { fs } from "@modern-js/utils";
5
+ import { compatibleRequire, fs } from "@modern-js/utils";
6
6
  import { match } from "path-to-regexp";
7
7
  let mockAPIs = [];
8
8
  let mockConfig;
@@ -35,7 +35,7 @@ const getMockModule = async (pwd)=>{
35
35
  }
36
36
  }
37
37
  if (!mockFilePath) return;
38
- const { default: mockHandlers, config } = await import(mockFilePath);
38
+ const { default: mockHandlers, config } = await compatibleRequire(mockFilePath, false);
39
39
  const enable = config?.enable;
40
40
  if (false === enable) return;
41
41
  if (!mockHandlers) throw new Error(`Mock file ${mockFilePath} parsed failed!`);
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A request handle compatible with `ServerBase.handle` (Hono's `app.fetch`)
3
+ * and the handler accepted by `createNodeServer`.
4
+ */
5
+ export type ReloadableHandle = (request: Request, ...args: any[]) => Response | Promise<Response>;
6
+ export interface ReloadManagerOptions {
7
+ /**
8
+ * The handle used before the first successful build. Optional: when omitted
9
+ * the manager serves a 503 "starting" response until the first build swaps a
10
+ * real handle in (use `setHandle()` for a fail-fast initial boot).
11
+ */
12
+ initialHandle?: ReloadableHandle;
13
+ /**
14
+ * Build a fresh handle (e.g. a brand new runtime `ServerBase`).
15
+ * If this throws, the previously active handle is retained (rollback).
16
+ */
17
+ build: () => Promise<ReloadableHandle>;
18
+ /** Debounce window (ms) used to coalesce rapid `schedule()` calls. */
19
+ debounceMs?: number;
20
+ /**
21
+ * Called after a handle is successfully built and swapped in (e.g. to clean
22
+ * up the previous runtime). The swap has already been committed when this
23
+ * runs, so throwing here does NOT roll back — it is reported via
24
+ * `onReloadError` instead of `onError`.
25
+ */
26
+ onReload?: (handle: ReloadableHandle) => void;
27
+ /**
28
+ * Called when `build()` throws. The previous handle is kept serving. This is
29
+ * the only path that counts as a failed reload.
30
+ */
31
+ onError?: (error: unknown) => void;
32
+ /**
33
+ * Called when the `onReload` callback throws. The new handle is already
34
+ * active; this is a post-swap cleanup/callback failure, never a rollback.
35
+ */
36
+ onReloadError?: (error: unknown) => void;
37
+ }
38
+ /**
39
+ * Owns the single mutable request handle behind a stable forwarding listener.
40
+ *
41
+ * Guarantees required by the dev-server hot-reload design:
42
+ * - Atomic swap: the active handle is replaced by a single assignment, so
43
+ * in-flight requests always run against a consistent handle.
44
+ * - Failure isolation: if `build()` rejects, the previous handle stays active
45
+ * (the dev server never degrades to an empty / broken handle).
46
+ * - Serial execution: reloads never overlap. Requests that arrive while a
47
+ * reload is running are coalesced into a single trailing reload so that the
48
+ * final state always reflects the latest source ("last write wins").
49
+ */
50
+ export declare class ReloadManager {
51
+ #private;
52
+ constructor(options: ReloadManagerOptions);
53
+ /**
54
+ * Replace the active handle directly, bypassing `build()`. Used to seed the
55
+ * initial known-good handle (a fail-fast boot builds the first runtime
56
+ * explicitly so build errors propagate, then seeds it here).
57
+ */
58
+ setHandle(handle: ReloadableHandle): void;
59
+ /**
60
+ * Stable forwarding handle. Pass this to `createNodeServer` once; it always
61
+ * dispatches to the latest active handle, so the Node server never needs to
62
+ * be recreated on reload.
63
+ */
64
+ get handle(): ReloadableHandle;
65
+ /** The currently active resolved handle (introspection / tests). */
66
+ get currentHandle(): ReloadableHandle;
67
+ /** Whether a reload is currently running (introspection / tests). */
68
+ get isReloading(): boolean;
69
+ /**
70
+ * Request a reload, debounced. Multiple calls within the debounce window
71
+ * collapse into a single reload.
72
+ */
73
+ schedule(): void;
74
+ /**
75
+ * Run a reload immediately. If one is already running, mark a trailing
76
+ * reload and return the in-flight promise (which will include the trailing
77
+ * run) so callers can await final settlement.
78
+ */
79
+ reloadNow(): Promise<void>;
80
+ /**
81
+ * Stop the manager: cancel any pending debounced reload and reject all
82
+ * further `schedule()` / `reloadNow()` calls. Wired into the dev server's
83
+ * close chain so a debounce timer that fires after teardown can never
84
+ * rebuild a runtime once the watcher / builder dev server are gone.
85
+ */
86
+ close(): void;
87
+ }
88
+ export declare function createReloadManager(options: ReloadManagerOptions): ReloadManager;
@@ -0,0 +1,27 @@
1
+ interface RuntimeOptionsBase {
2
+ config: Record<string, any>;
3
+ serverConfig?: Record<string, any>;
4
+ plugins?: any[];
5
+ }
6
+ /**
7
+ * Produce a fully-isolated options object for a single runtime build.
8
+ *
9
+ * `buildRuntimeServer` runs for the initial boot AND for every hot reload, so
10
+ * each build must own every container that a plugin or the apply pipeline
11
+ * could append to / rewrite. A shallow `{ ...base }` is NOT enough: `config`,
12
+ * `serverConfig` and `plugins` would still share references, letting one
13
+ * runtime's appended middlewares / plugins leak into the next reload.
14
+ *
15
+ * This clones:
16
+ * - a fresh top-level options object
17
+ * - a fresh `config` with a fresh `config.output` (so writing `assetPrefix`
18
+ * never mutates the caller's original `options.config`)
19
+ * - a fresh `serverConfig` whose `middlewares` / `renderMiddlewares` /
20
+ * `plugins` are new arrays
21
+ * - a fresh top-level `plugins` array
22
+ *
23
+ * Nested config sub-objects that are only read (never appended to) stay shared
24
+ * by reference, which is intentional and cheap.
25
+ */
26
+ export declare function createRuntimeServerOptions<T extends RuntimeOptionsBase>(base: T, assetPrefix?: string): T;
27
+ export {};
@@ -1,9 +1,68 @@
1
+ import type { Server as NodeServer } from 'node:http';
2
+ import type { Http2SecureServer } from 'node:http2';
3
+ import type { Server as NodeHttpsServer } from 'node:https';
1
4
  import type { BuilderInstance, Rspack } from '@modern-js/builder';
2
- import type { ServerBaseOptions, ServerPlugin } from '@modern-js/server-core';
5
+ import type { ServerBase, ServerBaseOptions, ServerPlugin } from '@modern-js/server-core';
6
+ import type { WatchEvent } from './dev-tools/watcher';
3
7
  import type { ModernDevServerOptions } from './types';
4
8
  type BuilderDevServer = Awaited<ReturnType<BuilderInstance['createDevServer']>>;
5
9
  export type DevPluginOptions = ModernDevServerOptions<ServerBaseOptions> & {
6
10
  builderDevServer?: BuilderDevServer;
7
11
  };
8
- export declare const devPlugin: (options: DevPluginOptions, compiler: Rspack.Compiler | Rspack.MultiCompiler | null) => ServerPlugin;
12
+ /**
13
+ * Runtime-level dev middleware injection.
14
+ *
15
+ * This plugin is added to EVERY runtime `ServerBase` that `buildRuntimeServer`
16
+ * creates, so it must be safe to run repeatedly and must NOT touch any
17
+ * process-level resource (the file watcher / websocket / builder hooks / close
18
+ * callbacks all live in `setupDevInfra`). It only pushes request middlewares
19
+ * into the current server's middleware list:
20
+ * - user `dev.setupMiddlewares` (before / after)
21
+ * - the mock middleware
22
+ * - the rsbuild/builder dev middleware (a stable reference, just re-registered)
23
+ * - the file-reader middleware
24
+ */
25
+ export declare const devRuntimeMiddlewarePlugin: (options: DevPluginOptions, compiler: Rspack.Compiler | Rspack.MultiCompiler | null) => ServerPlugin;
26
+ export interface DevInfraOptions {
27
+ config: DevPluginOptions['config'];
28
+ pwd: string;
29
+ distDir: string;
30
+ apiDir?: string;
31
+ sharedDir?: string;
32
+ builder?: BuilderInstance;
33
+ builderDevServer?: BuilderDevServer;
34
+ compiler: Rspack.Compiler | Rspack.MultiCompiler | null;
35
+ nodeServer?: NodeServer | NodeHttpsServer | Http2SecureServer;
36
+ /** Accessor for the currently-active runtime ServerBase (a mutable ref). */
37
+ getRuntimeServer: () => ServerBase | undefined;
38
+ /**
39
+ * Triggered when a watched user server file changes (require cache already
40
+ * busted). Wired to the runtime reload scheduler.
41
+ */
42
+ onFileChange: (filepath: string, event: WatchEvent) => void;
43
+ /**
44
+ * Extra teardown run as part of the dev server close chain (e.g. stopping the
45
+ * reload scheduler so a pending debounced reload can't rebuild after close).
46
+ */
47
+ onClose?: () => void;
48
+ }
49
+ export interface DevInfra {
50
+ /** Tear down every process-level resource. Called when the dev server stops. */
51
+ close: () => void;
52
+ }
53
+ /**
54
+ * Process-level dev infrastructure, created EXACTLY ONCE for the lifetime of
55
+ * the dev server. None of these resources are recreated or torn down by a
56
+ * runtime hot reload:
57
+ * - the rsbuild/builder dev server websocket connection
58
+ * - the builder `onDevCompileDone` -> SSR cache reset hook
59
+ * - the file watcher
60
+ * - close callbacks registered on the Node server's `close` event
61
+ *
62
+ * The watcher / onRepack reach the LIVE runtime hooks through
63
+ * `getRuntimeServer()` (a mutable ref) instead of closing over the initial
64
+ * runtime, so a later phase can swap the trigger to the reload scheduler
65
+ * without leaking a stale closure to a dead runtime.
66
+ */
67
+ export declare function setupDevInfra({ config, pwd, distDir, apiDir, sharedDir, builder, builderDevServer, getRuntimeServer, onFileChange, onClose, nodeServer, }: DevInfraOptions): DevInfra;
9
68
  export {};
@@ -1,15 +1,19 @@
1
- import { type ServerBase } from '@modern-js/server-core';
2
1
  import { type WatchOptions } from '@modern-js/utils';
3
- import Watcher from '../dev-tools/watcher';
2
+ import Watcher, { type WatchEvent } from '../dev-tools/watcher';
4
3
  export * from './repack';
5
4
  export * from './devOptions';
6
5
  export * from './fileReader';
7
6
  export * from './mock';
8
- export declare function startWatcher({ pwd, distDir, apiDir, sharedDir, watchOptions, server, }: {
7
+ export declare function startWatcher({ pwd, distDir, apiDir, sharedDir, watchOptions, onChange, }: {
9
8
  pwd: string;
10
9
  distDir: string;
11
10
  apiDir: string;
12
11
  sharedDir: string;
13
12
  watchOptions?: WatchOptions;
14
- server: ServerBase;
13
+ /**
14
+ * Called after the require cache for a changed user server file has been
15
+ * busted, so the next runtime build re-imports fresh code. Server loader
16
+ * bundles are handled inline (cache drop only) and never reach this.
17
+ */
18
+ onChange: (filepath: string, event: WatchEvent) => void;
15
19
  }): Watcher;
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "modern",
16
16
  "modern.js"
17
17
  ],
18
- "version": "3.5.0",
18
+ "version": "3.7.0",
19
19
  "types": "./dist/types/index.d.ts",
20
20
  "main": "./dist/cjs/index.js",
21
21
  "exports": {
@@ -44,14 +44,14 @@
44
44
  "minimatch": "^3.1.2",
45
45
  "path-to-regexp": "^6.3.0",
46
46
  "ws": "^8.21.0",
47
- "@modern-js/runtime-utils": "3.5.0",
48
- "@modern-js/server-core": "3.5.0",
49
- "@modern-js/server-utils": "3.5.0",
50
- "@modern-js/types": "3.5.0",
51
- "@modern-js/utils": "3.5.0"
47
+ "@modern-js/runtime-utils": "3.7.0",
48
+ "@modern-js/server-core": "3.7.0",
49
+ "@modern-js/server-utils": "3.7.0",
50
+ "@modern-js/types": "3.7.0",
51
+ "@modern-js/utils": "3.7.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@rslib/core": "0.23.0",
54
+ "@rslib/core": "0.23.2",
55
55
  "@types/connect-history-api-fallback": "^1.5.4",
56
56
  "@types/minimatch": "^3.0.5",
57
57
  "@types/node": "^20",
@@ -61,7 +61,7 @@
61
61
  "tsconfig-paths": "4.2.0",
62
62
  "typescript": "^5",
63
63
  "websocket": "^1.0.35",
64
- "@modern-js/builder": "3.5.0",
64
+ "@modern-js/builder": "3.7.0",
65
65
  "@modern-js/rslib": "2.68.10",
66
66
  "@scripts/rstest-config": "2.66.0"
67
67
  },
package/rstest.config.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { withTestPreset } from '@scripts/rstest-config';
2
2
 
3
3
  export default withTestPreset({
4
+ setupFiles: ['@scripts/rstest-config/setup.ts'],
4
5
  globals: true,
5
6
  });