@nasti-toolchain/nasti 2.5.0 → 2.5.2
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 +34 -2
- package/dist/build-CENae4VM.js +748 -0
- package/dist/build-CENae4VM.js.map +1 -0
- package/dist/build-vjh6h7PB.cjs +786 -0
- package/dist/build-vjh6h7PB.cjs.map +1 -0
- package/dist/cli.cjs +166 -7608
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +1 -2
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +166 -7606
- package/dist/cli.js.map +1 -1
- package/dist/client/hmr.cjs +0 -19
- package/dist/client/hmr.d.cts +14 -12
- package/dist/client/hmr.d.cts.map +1 -0
- package/dist/client/hmr.d.ts +14 -12
- package/dist/client/hmr.d.ts.map +1 -0
- package/dist/client/hmr.js +1 -1
- package/dist/dev-engine-DJL9sfv5.cjs +473 -0
- package/dist/dev-engine-DJL9sfv5.cjs.map +1 -0
- package/dist/dev-engine-DapuTPIz.js +469 -0
- package/dist/dev-engine-DapuTPIz.js.map +1 -0
- package/dist/electron-Bj8H-BJ6.js +278 -0
- package/dist/electron-Bj8H-BJ6.js.map +1 -0
- package/dist/electron-D0PSXz-a.cjs +316 -0
- package/dist/electron-D0PSXz-a.cjs.map +1 -0
- package/dist/electron-dev-BzI2IFnR.cjs +272 -0
- package/dist/electron-dev-BzI2IFnR.cjs.map +1 -0
- package/dist/electron-dev-CM15gsFp.js +251 -0
- package/dist/electron-dev-CM15gsFp.js.map +1 -0
- package/dist/env-GY7K1zPK.js +833 -0
- package/dist/env-GY7K1zPK.js.map +1 -0
- package/dist/env-lOj3yw8v.cjs +924 -0
- package/dist/env-lOj3yw8v.cjs.map +1 -0
- package/dist/environment-B2jCrhY2.js +1361 -0
- package/dist/environment-B2jCrhY2.js.map +1 -0
- package/dist/environment-CjmvoHIG.cjs +1443 -0
- package/dist/environment-CjmvoHIG.cjs.map +1 -0
- package/dist/index.cjs +453 -7976
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +872 -817
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +872 -817
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +418 -7939
- package/dist/index.js.map +1 -1
- package/dist/logger-BVZwdqqT.js +116 -0
- package/dist/logger-BVZwdqqT.js.map +1 -0
- package/dist/logger-M7AuVVTV.cjs +171 -0
- package/dist/logger-M7AuVVTV.cjs.map +1 -0
- package/dist/runnable-environment-BjoOvdDa.js +1345 -0
- package/dist/runnable-environment-BjoOvdDa.js.map +1 -0
- package/dist/runnable-environment-ByA3DHQ6.cjs +1351 -0
- package/dist/runnable-environment-ByA3DHQ6.cjs.map +1 -0
- package/dist/server-Bqgl25z0.js +1871 -0
- package/dist/server-Bqgl25z0.js.map +1 -0
- package/dist/server-BynlMdFc.cjs +1895 -0
- package/dist/server-BynlMdFc.cjs.map +1 -0
- package/package.json +20 -10
- package/dist/client/hmr.cjs.map +0 -1
- package/dist/client/hmr.js.map +0 -1
|
@@ -0,0 +1,1871 @@
|
|
|
1
|
+
import { a as __exportAll, i as printServerUrls } from "./logger-BVZwdqqT.js";
|
|
2
|
+
import { c as transformReactCode, f as resolveConfig, i as ssrDefineOverrides, n as loadEnv, o as shouldTransform, p as getPluginApi, r as replaceEnvInCode, s as transformCode, t as buildEnvDefine } from "./env-GY7K1zPK.js";
|
|
3
|
+
import { a as removeTimestampQuery, c as processHtml, i as createWsHotChannel, l as readHtmlFile, o as resolvePluginList, t as NastiEnvironment, u as isAssetFile } from "./environment-B2jCrhY2.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
import http from "node:http";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import connect from "connect";
|
|
12
|
+
import sirv from "sirv";
|
|
13
|
+
import { watch } from "chokidar";
|
|
14
|
+
import { WebSocketServer } from "ws";
|
|
15
|
+
//#region src/server/ws.ts
|
|
16
|
+
function createWebSocketServer(server) {
|
|
17
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
18
|
+
const clients = /* @__PURE__ */ new Set();
|
|
19
|
+
server.on("upgrade", (req, socket, head) => {
|
|
20
|
+
if (req.headers["sec-websocket-protocol"] === "nasti-hmr") wss.handleUpgrade(req, socket, head, (ws) => {
|
|
21
|
+
wss.emit("connection", ws, req);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
wss.on("connection", (ws) => {
|
|
25
|
+
clients.add(ws);
|
|
26
|
+
ws.send(JSON.stringify({ type: "connected" }));
|
|
27
|
+
ws.on("close", () => {
|
|
28
|
+
clients.delete(ws);
|
|
29
|
+
});
|
|
30
|
+
ws.on("error", (err) => {
|
|
31
|
+
console.error("[nasti] WebSocket error:", err);
|
|
32
|
+
clients.delete(ws);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
send(payload) {
|
|
37
|
+
const data = JSON.stringify(payload);
|
|
38
|
+
for (const client of clients) if (client.readyState === 1) client.send(data);
|
|
39
|
+
},
|
|
40
|
+
close() {
|
|
41
|
+
clients.clear();
|
|
42
|
+
wss.close();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/server/fs-allow.ts
|
|
48
|
+
/** True when `abs` is a real file/dir strictly inside `root` (not root itself). */
|
|
49
|
+
function isUnderRoot(abs, root) {
|
|
50
|
+
const rel = path.relative(root, abs);
|
|
51
|
+
return !!rel && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
52
|
+
}
|
|
53
|
+
const NM = `${path.sep}node_modules${path.sep}`;
|
|
54
|
+
/**
|
|
55
|
+
* Walk `node_modules` from `projectRoot` (and nested package roots) following
|
|
56
|
+
* symlinks. Return real paths of packages that live *outside* the project root
|
|
57
|
+
* and outside any `node_modules` tree — i.e. workspace / `file:` linked
|
|
58
|
+
* packages. Regular pnpm store entries (under `.pnpm/.../node_modules/`) are
|
|
59
|
+
* skipped: they are not user-editable watch targets and already pass the
|
|
60
|
+
* `node_modules` segment check for `?id=`.
|
|
61
|
+
*/
|
|
62
|
+
function discoverLinkedPackageRoots(projectRoot, maxDepth = 4) {
|
|
63
|
+
const results = [];
|
|
64
|
+
const seenReal = /* @__PURE__ */ new Set();
|
|
65
|
+
const queued = /* @__PURE__ */ new Set([projectRoot]);
|
|
66
|
+
const queue = [projectRoot];
|
|
67
|
+
for (let depth = 0; depth < maxDepth && queue.length > 0; depth++) {
|
|
68
|
+
const levelCount = queue.length;
|
|
69
|
+
for (let i = 0; i < levelCount; i++) {
|
|
70
|
+
const dir = queue.shift();
|
|
71
|
+
const nm = path.join(dir, "node_modules");
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = fs.readdirSync(nm, { withFileTypes: true });
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (const ent of entries) {
|
|
79
|
+
if (ent.name.startsWith(".") || ent.name === "node_modules") continue;
|
|
80
|
+
const pkgNames = ent.name.startsWith("@") ? listScopedPackages(nm, ent.name) : [ent.name];
|
|
81
|
+
for (const pkgName of pkgNames) {
|
|
82
|
+
const pkgPath = path.join(nm, pkgName);
|
|
83
|
+
let real;
|
|
84
|
+
try {
|
|
85
|
+
real = fs.realpathSync(pkgPath);
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (seenReal.has(real)) continue;
|
|
90
|
+
seenReal.add(real);
|
|
91
|
+
if (!queued.has(real)) {
|
|
92
|
+
queued.add(real);
|
|
93
|
+
queue.push(real);
|
|
94
|
+
}
|
|
95
|
+
if (real !== projectRoot && !isUnderRoot(real, projectRoot) && !real.includes(NM)) results.push(real);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
function listScopedPackages(nm, scope) {
|
|
103
|
+
try {
|
|
104
|
+
return fs.readdirSync(path.join(nm, scope)).filter((name) => !name.startsWith(".")).map((name) => path.join(scope, name));
|
|
105
|
+
} catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const linkedRootsCache = /* @__PURE__ */ new Map();
|
|
110
|
+
/** Cached discoverLinkedPackageRoots; invalidated when root/node_modules mtime changes. */
|
|
111
|
+
function getLinkedPackageRoots(projectRoot) {
|
|
112
|
+
let mtimeMs = 0;
|
|
113
|
+
try {
|
|
114
|
+
mtimeMs = fs.statSync(path.join(projectRoot, "node_modules")).mtimeMs;
|
|
115
|
+
} catch {
|
|
116
|
+
mtimeMs = 0;
|
|
117
|
+
}
|
|
118
|
+
const cached = linkedRootsCache.get(projectRoot);
|
|
119
|
+
if (cached && cached.mtimeMs === mtimeMs) return cached.roots;
|
|
120
|
+
const roots = discoverLinkedPackageRoots(projectRoot);
|
|
121
|
+
linkedRootsCache.set(projectRoot, {
|
|
122
|
+
roots,
|
|
123
|
+
mtimeMs
|
|
124
|
+
});
|
|
125
|
+
return roots;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Clear the linked-roots cache. Call from the file watcher when any
|
|
129
|
+
* `node_modules` tree changes (including nested ones under linked packages).
|
|
130
|
+
*/
|
|
131
|
+
function clearLinkedPackageRootsCache() {
|
|
132
|
+
linkedRootsCache.clear();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether a realpath'd absolute file may be served via `/@modules/...?id=`.
|
|
136
|
+
*
|
|
137
|
+
* Allowed:
|
|
138
|
+
* 1. under the project root (includes the app's own `node_modules`)
|
|
139
|
+
* 2. under a `node_modules` directory on the walk up from the project root
|
|
140
|
+
* (monorepo-hoisted stores such as `<repo>/node_modules/.pnpm/...`)
|
|
141
|
+
* 3. under a workspace / file: package linked from the project's
|
|
142
|
+
* node_modules tree (realpath leaves node_modules)
|
|
143
|
+
*
|
|
144
|
+
* A forged path that merely contains a `node_modules` segment under an
|
|
145
|
+
* unrelated directory (e.g. `/tmp/other/node_modules/x`) is rejected.
|
|
146
|
+
*/
|
|
147
|
+
function isAllowedDevModulePath(realId, projectRoot) {
|
|
148
|
+
if (realId === projectRoot || isUnderRoot(realId, projectRoot)) return true;
|
|
149
|
+
for (const pkgRoot of getLinkedPackageRoots(projectRoot)) if (realId === pkgRoot || realId.startsWith(pkgRoot + path.sep)) return true;
|
|
150
|
+
let dir = projectRoot;
|
|
151
|
+
for (;;) {
|
|
152
|
+
const nm = path.join(dir, "node_modules");
|
|
153
|
+
if (realId === nm || realId.startsWith(nm + path.sep)) return true;
|
|
154
|
+
const parent = path.dirname(dir);
|
|
155
|
+
if (parent === dir) break;
|
|
156
|
+
dir = parent;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Walk up from `file` looking for the nearest directory with a package.json
|
|
162
|
+
* that has a `name` field (a package root).
|
|
163
|
+
*/
|
|
164
|
+
function findNearestPackageRoot(file) {
|
|
165
|
+
let dir = path.dirname(file);
|
|
166
|
+
for (;;) {
|
|
167
|
+
const pkgJson = path.join(dir, "package.json");
|
|
168
|
+
if (fs.existsSync(pkgJson)) try {
|
|
169
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
|
|
170
|
+
if (typeof pkg?.name === "string" && pkg.name) return dir;
|
|
171
|
+
} catch {}
|
|
172
|
+
const parent = path.dirname(dir);
|
|
173
|
+
if (parent === dir) return null;
|
|
174
|
+
dir = parent;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/server/middleware.ts
|
|
179
|
+
const __dirname_esm = path.dirname(fileURLToPath(import.meta.url));
|
|
180
|
+
const __require = createRequire(import.meta.url);
|
|
181
|
+
/**
|
|
182
|
+
* 将 react-refresh 的 CJS 运行时包装成浏览器可用的 ESM。
|
|
183
|
+
* 原 CJS 通过 `exports.xxx = fn` 写入导出;我们用假的 module/exports/process
|
|
184
|
+
* 对象跑一遍,再显式 re-export 出来。
|
|
185
|
+
*
|
|
186
|
+
* 读一次缓存一次:dev server 生命周期内不会变。
|
|
187
|
+
*/
|
|
188
|
+
let __refreshRuntimeCache = null;
|
|
189
|
+
function getReactRefreshRuntimeEsm(includeBoundaryHelpers = false) {
|
|
190
|
+
if (__refreshRuntimeCache) return includeBoundaryHelpers ? __refreshRuntimeCache + REACT_REFRESH_BOUNDARY_HELPERS : __refreshRuntimeCache;
|
|
191
|
+
let cjsPath;
|
|
192
|
+
try {
|
|
193
|
+
const pkgPath = __require.resolve("react-refresh/package.json");
|
|
194
|
+
cjsPath = path.join(path.dirname(pkgPath), "cjs", "react-refresh-runtime.development.js");
|
|
195
|
+
} catch (err) {
|
|
196
|
+
cjsPath = path.resolve(__dirname_esm, "../../node_modules/react-refresh/cjs/react-refresh-runtime.development.js");
|
|
197
|
+
if (!fs.existsSync(cjsPath)) {
|
|
198
|
+
const origMsg = err instanceof Error ? err.message : String(err);
|
|
199
|
+
throw new Error(`[nasti] Missing dependency "react-refresh". Install it with: npm install react-refresh\nOriginal resolve error: ${origMsg}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
__refreshRuntimeCache = `// Wrapped react-refresh runtime -> ESM
|
|
203
|
+
const exports = {};
|
|
204
|
+
const module = { exports };
|
|
205
|
+
const process = { env: { NODE_ENV: 'development' } };
|
|
206
|
+
${fs.readFileSync(cjsPath, "utf-8")}
|
|
207
|
+
const __rt = module.exports;
|
|
208
|
+
export const injectIntoGlobalHook = __rt.injectIntoGlobalHook;
|
|
209
|
+
export const register = __rt.register;
|
|
210
|
+
export const createSignatureFunctionForTransform = __rt.createSignatureFunctionForTransform;
|
|
211
|
+
export const performReactRefresh = __rt.performReactRefresh;
|
|
212
|
+
export const isLikelyComponentType = __rt.isLikelyComponentType;
|
|
213
|
+
export const hasUnrecoverableErrors = __rt.hasUnrecoverableErrors;
|
|
214
|
+
export const setSignature = __rt.setSignature;
|
|
215
|
+
export const getFamilyByID = __rt.getFamilyByID;
|
|
216
|
+
export const getFamilyByType = __rt.getFamilyByType;
|
|
217
|
+
export const findAffectedHostInstances = __rt.findAffectedHostInstances;
|
|
218
|
+
export const collectCustomHooksForSignature = __rt.collectCustomHooksForSignature;
|
|
219
|
+
export default __rt;
|
|
220
|
+
`;
|
|
221
|
+
return includeBoundaryHelpers ? __refreshRuntimeCache + REACT_REFRESH_BOUNDARY_HELPERS : __refreshRuntimeCache;
|
|
222
|
+
}
|
|
223
|
+
/** @vitejs/plugin-react 同类的 Refresh boundary 校验,避免非组件导出静默保持旧值。 */
|
|
224
|
+
const REACT_REFRESH_BOUNDARY_HELPERS = `
|
|
225
|
+
function __nastiIsPlainObject(obj) {
|
|
226
|
+
return Object.prototype.toString.call(obj) === '[object Object]' &&
|
|
227
|
+
(obj.constructor === Object || obj.constructor === undefined);
|
|
228
|
+
}
|
|
229
|
+
function __nastiIsCompoundComponent(type) {
|
|
230
|
+
if (!__nastiIsPlainObject(type)) return false;
|
|
231
|
+
for (const key in type) {
|
|
232
|
+
if (!isLikelyComponentType(type[key])) return false;
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
export function registerExportsForReactRefresh(filename, moduleExports) {
|
|
237
|
+
for (const key in moduleExports) {
|
|
238
|
+
if (key === '__esModule') continue;
|
|
239
|
+
const value = moduleExports[key];
|
|
240
|
+
if (isLikelyComponentType(value)) {
|
|
241
|
+
register(value, filename + ' export ' + key);
|
|
242
|
+
} else if (__nastiIsCompoundComponent(value)) {
|
|
243
|
+
for (const subKey in value) {
|
|
244
|
+
register(value[subKey], filename + ' export ' + key + '-' + subKey);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
let __nastiRefreshTimer;
|
|
250
|
+
function __nastiEnqueueRefresh() {
|
|
251
|
+
clearTimeout(__nastiRefreshTimer);
|
|
252
|
+
__nastiRefreshTimer = setTimeout(() => performReactRefresh(), 16);
|
|
253
|
+
}
|
|
254
|
+
function __nastiCheckExports(ignored, exports, predicate) {
|
|
255
|
+
for (const key in exports) {
|
|
256
|
+
if (ignored.includes(key)) continue;
|
|
257
|
+
if (!predicate(key, exports[key])) return key;
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
export function validateRefreshBoundaryAndEnqueueUpdate(id, prevExports, nextExports) {
|
|
262
|
+
const ignored = window.__getReactRefreshIgnoredExports?.({ id }) ?? [];
|
|
263
|
+
if (__nastiCheckExports(ignored, prevExports, (key) => key in nextExports) !== true) {
|
|
264
|
+
return 'Could not Fast Refresh (export removed)';
|
|
265
|
+
}
|
|
266
|
+
if (__nastiCheckExports(ignored, nextExports, (key) => key in prevExports) !== true) {
|
|
267
|
+
return 'Could not Fast Refresh (new export)';
|
|
268
|
+
}
|
|
269
|
+
let hasExports = false;
|
|
270
|
+
const compatible = __nastiCheckExports(ignored, nextExports, (key, value) => {
|
|
271
|
+
hasExports = true;
|
|
272
|
+
return isLikelyComponentType(value) ||
|
|
273
|
+
__nastiIsCompoundComponent(value) ||
|
|
274
|
+
prevExports[key] === value;
|
|
275
|
+
});
|
|
276
|
+
if (!hasExports) {
|
|
277
|
+
return 'Could not Fast Refresh (no exports)';
|
|
278
|
+
}
|
|
279
|
+
if (compatible === true) {
|
|
280
|
+
__nastiEnqueueRefresh();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
return 'Could not Fast Refresh ("' + compatible + '" export is incompatible)';
|
|
284
|
+
}
|
|
285
|
+
export const __hmr_import = (module) => import(module);
|
|
286
|
+
`;
|
|
287
|
+
`
|
|
288
|
+
import RefreshRuntime from "/@react-refresh";
|
|
289
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
290
|
+
window.$RefreshReg$ = () => {};
|
|
291
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
292
|
+
window.__vite_plugin_react_preamble_installed__ = true;
|
|
293
|
+
`.trim();
|
|
294
|
+
/**
|
|
295
|
+
* 单模块的 Fast Refresh 包装。
|
|
296
|
+
*
|
|
297
|
+
* 包装前先把 transformed 代码里的 `import.meta.hot` 替换为本地 `__nasti_hot__`
|
|
298
|
+
* 变量 —— 规避 `import.meta` 属性赋值在某些引擎下不可写的问题,并统一 JSX/非 JSX
|
|
299
|
+
* 路径的 hot 来源。
|
|
300
|
+
*/
|
|
301
|
+
function buildReactRefreshWrapper(moduleUrl, transformedCode) {
|
|
302
|
+
const urlLit = JSON.stringify(moduleUrl);
|
|
303
|
+
return `import * as RefreshRuntime from "/@react-refresh";
|
|
304
|
+
import { createHotContext as __nasti_createHotContext__ } from "/@nasti/client";
|
|
305
|
+
const __nasti_hot__ = __nasti_createHotContext__(${urlLit});
|
|
306
|
+
|
|
307
|
+
if (!window.__vite_plugin_react_preamble_installed__) {
|
|
308
|
+
throw new Error("[nasti] React Fast Refresh preamble missing. Make sure nasti:html is wired with framework: 'react'.");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const prevRefreshReg = window.$RefreshReg$;
|
|
312
|
+
const prevRefreshSig = window.$RefreshSig$;
|
|
313
|
+
window.$RefreshReg$ = (type, id) => {
|
|
314
|
+
RefreshRuntime.register(type, ${urlLit} + " " + id);
|
|
315
|
+
};
|
|
316
|
+
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
|
|
317
|
+
|
|
318
|
+
${transformedCode.replace(/\bimport\.meta\.hot\b/g, "__nasti_hot__")}
|
|
319
|
+
|
|
320
|
+
window.$RefreshReg$ = prevRefreshReg;
|
|
321
|
+
window.$RefreshSig$ = prevRefreshSig;
|
|
322
|
+
|
|
323
|
+
if (__nasti_hot__) {
|
|
324
|
+
let __nasti_current_exports__;
|
|
325
|
+
__nasti_hot__.accept((nextExports) => {
|
|
326
|
+
if (!nextExports) return;
|
|
327
|
+
if (!__nasti_current_exports__) {
|
|
328
|
+
__nasti_hot__.invalidate('Could not Fast Refresh (previous exports unavailable)');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(
|
|
332
|
+
${urlLit},
|
|
333
|
+
__nasti_current_exports__,
|
|
334
|
+
nextExports,
|
|
335
|
+
);
|
|
336
|
+
if (invalidateMessage) __nasti_hot__.invalidate(invalidateMessage);
|
|
337
|
+
});
|
|
338
|
+
RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
|
|
339
|
+
__nasti_current_exports__ = currentExports;
|
|
340
|
+
RefreshRuntime.registerExportsForReactRefresh(${urlLit}, currentExports);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
`;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* 非 JSX 模块里用户显式写了 import.meta.hot —— 注入一段 hot context 头部并替换属性访问为本地变量。
|
|
347
|
+
*/
|
|
348
|
+
function injectImportMetaHot(code, moduleUrl) {
|
|
349
|
+
const matches = [...maskStringsAndComments(code).matchAll(/\bimport\.meta\.hot\b/g)];
|
|
350
|
+
if (matches.length === 0) return code;
|
|
351
|
+
for (const match of matches.reverse()) {
|
|
352
|
+
const start = match.index;
|
|
353
|
+
code = code.slice(0, start) + "__nasti_hot__" + code.slice(start + match[0].length);
|
|
354
|
+
}
|
|
355
|
+
return `import { createHotContext as __nasti_createHotContext__ } from "/@nasti/client";
|
|
356
|
+
const __nasti_hot__ = __nasti_createHotContext__(${JSON.stringify(moduleUrl)});
|
|
357
|
+
` + code;
|
|
358
|
+
}
|
|
359
|
+
/** 主转译中间件 - 处理模块请求 */
|
|
360
|
+
function transformMiddleware(ctx) {
|
|
361
|
+
ctx.envDefine = buildEnvDefine(loadEnv(ctx.config.mode, ctx.config.root, ctx.config.envPrefix), ctx.config.mode, ssrDefineOverrides(ctx.environment?.consumer ?? "client"));
|
|
362
|
+
return async (req, res, next) => {
|
|
363
|
+
const url = req.url ?? "/";
|
|
364
|
+
if (ctx.config.server.cors) {
|
|
365
|
+
const origin = req.headers.origin ?? "*";
|
|
366
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
367
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
|
368
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
369
|
+
if (req.method === "OPTIONS") {
|
|
370
|
+
res.statusCode = 204;
|
|
371
|
+
res.end();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (req.method !== "GET") return next();
|
|
376
|
+
if (url === "/@nasti/client") {
|
|
377
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
378
|
+
res.end(getHmrClientCode());
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (url === "/" || url.endsWith(".html")) {
|
|
382
|
+
const html = await readHtmlFile(ctx.config.root, ctx.config.environments.client?.html);
|
|
383
|
+
if (html) {
|
|
384
|
+
let processedHtml = html;
|
|
385
|
+
for (const plugin of ctx.config.plugins) if (plugin.transformIndexHtml) {
|
|
386
|
+
const result = await plugin.transformIndexHtml(processedHtml);
|
|
387
|
+
if (typeof result === "string") processedHtml = result;
|
|
388
|
+
else if (result && "html" in result) processedHtml = processHtml(result.html, result.tags);
|
|
389
|
+
else if (Array.isArray(result)) processedHtml = processHtml(processedHtml, result);
|
|
390
|
+
}
|
|
391
|
+
res.setHeader("Content-Type", "text/html");
|
|
392
|
+
res.end(processedHtml);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (isModuleRequest(url, req.headers["sec-fetch-dest"])) try {
|
|
397
|
+
const result = await transformRequest(url, ctx);
|
|
398
|
+
if (result) {
|
|
399
|
+
const contentType = url.endsWith(".css") ? "application/javascript" : "application/javascript";
|
|
400
|
+
res.setHeader("Content-Type", contentType);
|
|
401
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
402
|
+
res.end(typeof result === "string" ? result : result.code);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
ctx.config.logger.error(pc.red(`Transform error: ${url}\n`) + (err.stack ?? err.message), {
|
|
407
|
+
timestamp: true,
|
|
408
|
+
error: err
|
|
409
|
+
});
|
|
410
|
+
res.statusCode = 500;
|
|
411
|
+
res.end(`Transform error: ${err.message}`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
next();
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** 转译单个模块 */
|
|
418
|
+
async function transformRequest(url, ctx) {
|
|
419
|
+
const { config, pluginContainer, moduleGraph } = ctx;
|
|
420
|
+
url = removeTimestampQuery(url);
|
|
421
|
+
const cleanReqUrl = url.split("?")[0];
|
|
422
|
+
const cached = moduleGraph.getModuleByUrl(url);
|
|
423
|
+
if (cached?.transformResult) return cached.transformResult;
|
|
424
|
+
if (cleanReqUrl === "/@react-refresh") return { code: getReactRefreshRuntimeEsm(true) };
|
|
425
|
+
if (cleanReqUrl.startsWith("/@modules/") && url.includes("?")) {
|
|
426
|
+
const idParam = new URLSearchParams(url.slice(url.indexOf("?") + 1)).get("id");
|
|
427
|
+
let realId = null;
|
|
428
|
+
let realIdValid = false;
|
|
429
|
+
try {
|
|
430
|
+
if (idParam) {
|
|
431
|
+
realId = fs.realpathSync(idParam);
|
|
432
|
+
realIdValid = fs.statSync(realId).isFile() && isAllowedDevModulePath(realId, config.root);
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
realId = null;
|
|
436
|
+
realIdValid = false;
|
|
437
|
+
}
|
|
438
|
+
if (realId && realIdValid) {
|
|
439
|
+
const mod = await moduleGraph.ensureEntryFromUrl(url);
|
|
440
|
+
moduleGraph.registerModule(mod, realId);
|
|
441
|
+
const transformResult = { code: await bundlePackageAsEsm(realId, config.root) };
|
|
442
|
+
mod.transformResult = transformResult;
|
|
443
|
+
return transformResult;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (cleanReqUrl.startsWith("/@modules/")) {
|
|
447
|
+
const virtual = await loadVirtualModule(cleanReqUrl.slice(10), ctx);
|
|
448
|
+
if (virtual) {
|
|
449
|
+
const mod = await moduleGraph.ensureEntryFromUrl(url);
|
|
450
|
+
moduleGraph.setModuleId(mod, virtual.id);
|
|
451
|
+
mod.transformResult = virtual.result;
|
|
452
|
+
return virtual.result;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const rawQuery = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
|
|
456
|
+
if (rawQuery && !/^t=\d+$/.test(rawQuery)) {
|
|
457
|
+
const mod = await moduleGraph.ensureEntryFromUrl(url);
|
|
458
|
+
const transformVersion = mod.invalidationVersion;
|
|
459
|
+
const loaded = await pluginContainer.load(url);
|
|
460
|
+
if (loaded != null) {
|
|
461
|
+
let code = typeof loaded === "string" ? loaded : loaded.code;
|
|
462
|
+
let map = typeof loaded === "string" ? void 0 : loaded.map;
|
|
463
|
+
const transformed = await pluginContainer.transform(code, url);
|
|
464
|
+
if (transformed != null) {
|
|
465
|
+
code = typeof transformed === "string" ? transformed : transformed.code;
|
|
466
|
+
if (typeof transformed !== "string" && transformed.map != null) map = transformed.map;
|
|
467
|
+
}
|
|
468
|
+
const parentFile = resolveUrlToFile(cleanReqUrl, config.root) ?? cleanReqUrl;
|
|
469
|
+
moduleGraph.registerModule(mod, parentFile);
|
|
470
|
+
const hotInfo = rewriteHotAcceptDeps(code, config, parentFile);
|
|
471
|
+
code = injectImportMetaHot(hotInfo.code, url);
|
|
472
|
+
code = replaceEnvInCode(code, ctx.envDefine ?? buildEnvDefine(loadEnv(config.mode, config.root, config.envPrefix), config.mode, ssrDefineOverrides(ctx.environment?.consumer ?? "client")));
|
|
473
|
+
const importedUrls = /* @__PURE__ */ new Set();
|
|
474
|
+
code = rewriteImports(code, config, parentFile, importedUrls, moduleGraph);
|
|
475
|
+
const pruned = await moduleGraph.updateModuleInfo(mod, importedUrls, hotInfo.acceptedUrls, hotInfo.isSelfAccepting, transformVersion);
|
|
476
|
+
const transformResult = {
|
|
477
|
+
code,
|
|
478
|
+
map
|
|
479
|
+
};
|
|
480
|
+
if (pruned) {
|
|
481
|
+
if (pruned.size > 0) ctx.onPrune?.([...pruned].map((item) => item.url));
|
|
482
|
+
mod.transformResult = transformResult;
|
|
483
|
+
}
|
|
484
|
+
return transformResult;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const filePath = resolveUrlToFile(url, config.root);
|
|
488
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
489
|
+
const mod = await moduleGraph.ensureEntryFromUrl(url);
|
|
490
|
+
moduleGraph.registerModule(mod, filePath);
|
|
491
|
+
const transformVersion = mod.invalidationVersion;
|
|
492
|
+
if (cleanReqUrl.startsWith("/@modules/")) {
|
|
493
|
+
const transformResult = { code: await bundlePackageAsEsm(filePath, config.root) };
|
|
494
|
+
mod.transformResult = transformResult;
|
|
495
|
+
return transformResult;
|
|
496
|
+
}
|
|
497
|
+
const loaded = await pluginContainer.load(filePath);
|
|
498
|
+
let code = loaded == null ? fs.readFileSync(filePath, "utf-8") : typeof loaded === "string" ? loaded : loaded.code;
|
|
499
|
+
let map = loaded && typeof loaded !== "string" ? loaded.map : void 0;
|
|
500
|
+
const pluginResult = await pluginContainer.transform(code, filePath);
|
|
501
|
+
if (pluginResult) {
|
|
502
|
+
code = typeof pluginResult === "string" ? pluginResult : pluginResult.code;
|
|
503
|
+
if (typeof pluginResult !== "string") map = pluginResult.map;
|
|
504
|
+
}
|
|
505
|
+
const stableUrl = cleanReqUrl;
|
|
506
|
+
let wrappedWithRefresh = false;
|
|
507
|
+
if (config.framework === "react") {
|
|
508
|
+
const useRefresh = (ctx.environment?.consumer ?? "client") === "client" && config.server.hmr !== false && (!!config.react.compiler || /\.[jt]sx$/.test(filePath));
|
|
509
|
+
const result = await transformReactCode(filePath, code, {
|
|
510
|
+
react: config.react,
|
|
511
|
+
consumer: ctx.environment?.consumer ?? "client",
|
|
512
|
+
development: true,
|
|
513
|
+
reactRefresh: useRefresh,
|
|
514
|
+
sourcemap: true,
|
|
515
|
+
target: ctx.environment?.options.build.target ?? config.build.target,
|
|
516
|
+
onWarning: (message) => config.logger.warn(`[nasti:react] ${message}`)
|
|
517
|
+
});
|
|
518
|
+
if (result) {
|
|
519
|
+
code = result.code;
|
|
520
|
+
if (result.map) map = JSON.parse(result.map);
|
|
521
|
+
if (useRefresh) {
|
|
522
|
+
code = buildReactRefreshWrapper(stableUrl, code);
|
|
523
|
+
wrappedWithRefresh = true;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} else if (shouldTransform(filePath)) {
|
|
527
|
+
const result = transformCode(filePath, code, {
|
|
528
|
+
sourcemap: true,
|
|
529
|
+
jsxRuntime: "automatic",
|
|
530
|
+
jsxImportSource: "vue",
|
|
531
|
+
target: ctx.environment?.options.build.target ?? config.build.target
|
|
532
|
+
});
|
|
533
|
+
code = result.code;
|
|
534
|
+
if (result.map) map = JSON.parse(result.map);
|
|
535
|
+
}
|
|
536
|
+
const hotInfo = rewriteHotAcceptDeps(code, config, filePath);
|
|
537
|
+
code = hotInfo.code;
|
|
538
|
+
if (!wrappedWithRefresh) code = injectImportMetaHot(code, stableUrl);
|
|
539
|
+
const envDefine = ctx.envDefine ?? buildEnvDefine(loadEnv(config.mode, config.root, config.envPrefix), config.mode, ssrDefineOverrides(ctx.environment?.consumer ?? "client"));
|
|
540
|
+
code = replaceEnvInCode(code, envDefine);
|
|
541
|
+
const importedUrls = /* @__PURE__ */ new Set();
|
|
542
|
+
code = rewriteImports(code, config, filePath, importedUrls, moduleGraph);
|
|
543
|
+
const pruned = await moduleGraph.updateModuleInfo(mod, importedUrls, hotInfo.acceptedUrls, wrappedWithRefresh || hotInfo.isSelfAccepting, transformVersion);
|
|
544
|
+
const transformResult = {
|
|
545
|
+
code,
|
|
546
|
+
map
|
|
547
|
+
};
|
|
548
|
+
if (pruned) {
|
|
549
|
+
if (pruned.size > 0) ctx.onPrune?.([...pruned].map((item) => item.url));
|
|
550
|
+
mod.transformResult = transformResult;
|
|
551
|
+
}
|
|
552
|
+
return transformResult;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Run the plugin `resolveId` → `load` → `transform` pipeline for a bare specifier
|
|
556
|
+
* that arrived as `/@modules/<spec>`, and return the prepared JS module if (and
|
|
557
|
+
* only if) the result is a virtual module — i.e. its resolved id either uses the
|
|
558
|
+
* Vite-convention `\0` null-byte prefix or does not exist on disk.
|
|
559
|
+
*
|
|
560
|
+
* Real-file resolutions (e.g. an alias plugin remapping a bare specifier onto a
|
|
561
|
+
* source file under the project) are intentionally skipped here so they fall
|
|
562
|
+
* through to the normal `resolveUrlToFile` / `bundlePackageAsEsm` paths; this
|
|
563
|
+
* keeps the npm-package loader and the React Refresh wrapper in charge of those.
|
|
564
|
+
*/
|
|
565
|
+
async function loadVirtualModule(spec, ctx) {
|
|
566
|
+
const { config, pluginContainer } = ctx;
|
|
567
|
+
const resolved = await pluginContainer.resolveId(spec);
|
|
568
|
+
if (resolved == null) return null;
|
|
569
|
+
const resolvedId = typeof resolved === "string" ? resolved : resolved.id;
|
|
570
|
+
if (!(resolvedId.startsWith("\0") || !fs.existsSync(resolvedId))) return null;
|
|
571
|
+
const loadResult = await pluginContainer.load(resolvedId);
|
|
572
|
+
if (loadResult == null) return null;
|
|
573
|
+
let code = typeof loadResult === "string" ? loadResult : loadResult.code;
|
|
574
|
+
const transformed = await pluginContainer.transform(code, resolvedId);
|
|
575
|
+
if (transformed != null) code = typeof transformed === "string" ? transformed : transformed.code;
|
|
576
|
+
code = replaceEnvInCode(code, ctx.envDefine ?? buildEnvDefine(loadEnv(config.mode, config.root, config.envPrefix), config.mode, ssrDefineOverrides(ctx.environment?.consumer ?? "client")));
|
|
577
|
+
const anchor = path.join(config.root, "__nasti_virtual__.ts");
|
|
578
|
+
code = rewriteImports(code, config, anchor);
|
|
579
|
+
return {
|
|
580
|
+
id: resolvedId,
|
|
581
|
+
result: { code }
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
/** 用 rolldown 将 node_modules 包打包为浏览器可用的 ESM(含 CJS→ESM 转换) */
|
|
585
|
+
const esmBundleCache = /* @__PURE__ */ new Map();
|
|
586
|
+
async function bundlePackageAsEsm(entryFile, root) {
|
|
587
|
+
if (!esmBundleCache.has(entryFile)) esmBundleCache.set(entryFile, doBundlePackage(entryFile, root));
|
|
588
|
+
return esmBundleCache.get(entryFile);
|
|
589
|
+
}
|
|
590
|
+
async function doBundlePackage(entryFile, root) {
|
|
591
|
+
const shim = await tryGenerateSubpathShim(entryFile, root);
|
|
592
|
+
if (shim != null) return shim;
|
|
593
|
+
const { rolldown } = await import("rolldown");
|
|
594
|
+
const bundle = await rolldown({
|
|
595
|
+
input: entryFile,
|
|
596
|
+
external: (id) => {
|
|
597
|
+
if (id.startsWith(".") || id.startsWith("/") || /^[A-Za-z]:\\/.test(id)) return false;
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
const result = await bundle.generate({
|
|
602
|
+
format: "esm",
|
|
603
|
+
exports: "named"
|
|
604
|
+
});
|
|
605
|
+
await bundle.close();
|
|
606
|
+
let code = result.output[0].code;
|
|
607
|
+
code = code.replace(/process\.env\.NODE_ENV/g, "\"development\"");
|
|
608
|
+
const externalBaseDir = path.dirname(entryFile);
|
|
609
|
+
code = code.replace(/^(import\b[^;'"]*?\bfrom\s+)(['"])([^'"./][^'"]*)(\2)/gm, (_, prefix, q, spec) => `${prefix}${q}${externalSpecToModuleUrl(spec, externalBaseDir, root)}${q}`).replace(/^(export\b[^;'"]*?\bfrom\s+)(['"])([^'"./][^'"]*)(\2)/gm, (_, prefix, q, spec) => `${prefix}${q}${externalSpecToModuleUrl(spec, externalBaseDir, root)}${q}`).replace(/^(import\s+)(['"])([^'"./][^'"]*)(\2)/gm, (_, prefix, q, spec) => `${prefix}${q}${externalSpecToModuleUrl(spec, externalBaseDir, root)}${q}`);
|
|
610
|
+
code = rewriteExternalRequires(code, externalBaseDir, root);
|
|
611
|
+
if (code.includes("__commonJSMin")) code = await injectCjsNamedExports(code, entryFile);
|
|
612
|
+
return code;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* 当入口是 npm 包的「子路径」(即 `pkg/sub` 形式,对应 `node_modules/pkg/`
|
|
616
|
+
* 内部的某个非主入口文件)时,尝试改用一个薄薄的 re-export shim 替代独立 bundle,
|
|
617
|
+
* shim 通过 `/@modules/<pkgName>` 复用主入口 bundle 的导出。
|
|
618
|
+
*
|
|
619
|
+
* 这样做是为了避免「同一份代码被打到多个 bundle 里」造成的运行时 bug:
|
|
620
|
+
* 例如 `react-aria-components/Select` 与 `react-aria-components` 主入口都内部
|
|
621
|
+
* 依赖 `private/Select.*`,而 rolldown 会把这份相对依赖内联到各自的 bundle 中。
|
|
622
|
+
* 结果是浏览器侧出现两个独立的 `SelectContext = createContext(null)` 实例 ——
|
|
623
|
+
* `<Select>` 写入的 Provider 与下方 `<SelectValue>` 读到的 Consumer 不再指向
|
|
624
|
+
* 同一个 context,`useContext` 返回 null 直接崩。React Aria 的 Tabs / Collection
|
|
625
|
+
* / DialogTrigger 等都属于这一类对 context identity 敏感的设计。
|
|
626
|
+
*
|
|
627
|
+
* 安全前提:shim 仅在主入口 **完整覆盖** 子路径所有具名导出、且各导出值
|
|
628
|
+
* 与主入口下的对应值是 **同一引用**(identity 相等)时生成;否则回退到普通
|
|
629
|
+
* bundling,避免错误改写有副作用的子路径(如真正分裂的入口、wrapped exports)。
|
|
630
|
+
*
|
|
631
|
+
* 仅 dev server 使用:build 模式走单一 rolldown 整图打包,本身不会重复内联。
|
|
632
|
+
*/
|
|
633
|
+
async function tryGenerateSubpathShim(entryFile, root) {
|
|
634
|
+
const NM = `${path.sep}node_modules${path.sep}`;
|
|
635
|
+
if (!entryFile.includes(NM)) return null;
|
|
636
|
+
let pkgDir = null;
|
|
637
|
+
let pkgName = null;
|
|
638
|
+
let dir = path.dirname(entryFile);
|
|
639
|
+
while (true) {
|
|
640
|
+
const pkgJsonPath = path.join(dir, "package.json");
|
|
641
|
+
if (fs.existsSync(pkgJsonPath)) try {
|
|
642
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
643
|
+
if (typeof pkg?.name === "string" && pkg.name) {
|
|
644
|
+
pkgDir = dir;
|
|
645
|
+
pkgName = pkg.name;
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
} catch {}
|
|
649
|
+
const parent = path.dirname(dir);
|
|
650
|
+
if (parent === dir) return null;
|
|
651
|
+
dir = parent;
|
|
652
|
+
if (!dir.includes(NM)) return null;
|
|
653
|
+
}
|
|
654
|
+
if (!pkgDir || !pkgName) return null;
|
|
655
|
+
const entryExt = path.extname(entryFile);
|
|
656
|
+
const mainEntry = pickMainEntryByExtension(pkgDir, entryExt);
|
|
657
|
+
if (!mainEntry) return null;
|
|
658
|
+
if (path.resolve(mainEntry) === path.resolve(entryFile)) return null;
|
|
659
|
+
let mainNs;
|
|
660
|
+
let subNs;
|
|
661
|
+
try {
|
|
662
|
+
mainNs = await import(pathToFileURL(mainEntry).href);
|
|
663
|
+
subNs = await import(pathToFileURL(entryFile).href);
|
|
664
|
+
} catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
if (!mainNs || typeof mainNs !== "object") return null;
|
|
668
|
+
if (!subNs || typeof subNs !== "object") return null;
|
|
669
|
+
const subKeys = Object.keys(subNs).filter((k) => k !== "__esModule" && k !== "default" && VALID_IDENT.test(k));
|
|
670
|
+
if (subKeys.length === 0) return null;
|
|
671
|
+
for (const k of subKeys) {
|
|
672
|
+
if (!(k in mainNs)) return null;
|
|
673
|
+
if (mainNs[k] !== subNs[k]) return null;
|
|
674
|
+
}
|
|
675
|
+
if ("default" in subNs) {
|
|
676
|
+
if (!("default" in mainNs)) return null;
|
|
677
|
+
if (mainNs["default"] !== subNs["default"]) return null;
|
|
678
|
+
}
|
|
679
|
+
const rootMain = resolveNodeModule(root, pkgName);
|
|
680
|
+
const mainEntryUrl = rootMain && rootMain.startsWith(pkgDir + path.sep) ? `/@modules/${pkgName}` : `/@modules/${pkgName}?id=${encodeURIComponent(mainEntry)}`;
|
|
681
|
+
const lines = [`// Nasti subpath shim → ${pkgName} (avoid duplicate bundling)`, `import * as __pkg from "${mainEntryUrl}";`];
|
|
682
|
+
for (const k of subKeys) lines.push(`export const ${k} = __pkg[${JSON.stringify(k)}];`);
|
|
683
|
+
if ("default" in subNs) lines.push(`export default ("default" in __pkg ? __pkg["default"] : __pkg);`);
|
|
684
|
+
return lines.join("\n") + "\n";
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* 解析包主入口绝对路径,**优先匹配指定扩展名**,再回退到任意可用主入口。
|
|
688
|
+
* 主入口候选来自 `exports[\".\"]`(按 import / module / default / require / node 顺序展开嵌套条件)
|
|
689
|
+
* 以及顶层 `module` / `main` 字段。
|
|
690
|
+
*/
|
|
691
|
+
function pickMainEntryByExtension(pkgDir, preferredExt) {
|
|
692
|
+
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
693
|
+
let pkg;
|
|
694
|
+
try {
|
|
695
|
+
pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
696
|
+
} catch {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
const candidates = [];
|
|
700
|
+
const collectFromExportObject = (obj) => {
|
|
701
|
+
if (!obj || typeof obj !== "object") return;
|
|
702
|
+
for (const cond of [
|
|
703
|
+
"import",
|
|
704
|
+
"module",
|
|
705
|
+
"default",
|
|
706
|
+
"require",
|
|
707
|
+
"node"
|
|
708
|
+
]) {
|
|
709
|
+
const v = obj[cond];
|
|
710
|
+
if (typeof v === "string") candidates.push(v);
|
|
711
|
+
else if (v && typeof v === "object") collectFromExportObject(v);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
const dot = pkg?.exports?.["."];
|
|
715
|
+
if (typeof dot === "string") candidates.push(dot);
|
|
716
|
+
else if (dot && typeof dot === "object") collectFromExportObject(dot);
|
|
717
|
+
if (typeof pkg.module === "string") candidates.push(pkg.module);
|
|
718
|
+
if (typeof pkg.main === "string") candidates.push(pkg.main);
|
|
719
|
+
for (const cand of candidates) if (path.extname(cand) === preferredExt) {
|
|
720
|
+
const full = path.resolve(pkgDir, cand);
|
|
721
|
+
if (fs.existsSync(full)) return full;
|
|
722
|
+
}
|
|
723
|
+
for (const cand of candidates) {
|
|
724
|
+
const full = path.resolve(pkgDir, cand);
|
|
725
|
+
if (fs.existsSync(full)) return full;
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
/** 将 rolldown 生成的 __require("pkg") 调用转换为顶层 ESM import
|
|
730
|
+
* 使用 namespace import + default 回退,兼容 CJS 和 ESM 模块:
|
|
731
|
+
* - CJS 包有 default export(__cjsMod)→ 取 .default
|
|
732
|
+
* - ESM 包只有 named exports → 取 namespace 本身 */
|
|
733
|
+
function rewriteExternalRequires(code, baseDir, root) {
|
|
734
|
+
const pkgs = /* @__PURE__ */ new Set();
|
|
735
|
+
const re = /__require\(["']([^"']+)["']\)/g;
|
|
736
|
+
let m;
|
|
737
|
+
while ((m = re.exec(code)) !== null) pkgs.add(m[1]);
|
|
738
|
+
if (pkgs.size === 0) return code;
|
|
739
|
+
let result = code;
|
|
740
|
+
const imports = [];
|
|
741
|
+
for (const pkg of pkgs) {
|
|
742
|
+
const safe = pkg.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
743
|
+
imports.push(`import * as __ns_${safe} from "${externalSpecToModuleUrl(pkg, baseDir, root)}";`);
|
|
744
|
+
imports.push(`var __req_${safe} = "default" in __ns_${safe} ? __ns_${safe}["default"] : __ns_${safe};`);
|
|
745
|
+
result = result.replaceAll(`__require("${pkg}")`, `__req_${safe}`);
|
|
746
|
+
result = result.replaceAll(`__require('${pkg}')`, `__req_${safe}`);
|
|
747
|
+
}
|
|
748
|
+
return imports.join("\n") + "\n" + result;
|
|
749
|
+
}
|
|
750
|
+
const VALID_IDENT = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
751
|
+
async function injectCjsNamedExports(code, entryFile) {
|
|
752
|
+
try {
|
|
753
|
+
const { createRequire } = await import("module");
|
|
754
|
+
const cjsExports = createRequire(entryFile)(entryFile);
|
|
755
|
+
if (!cjsExports || typeof cjsExports !== "object" && typeof cjsExports !== "function" || Array.isArray(cjsExports)) return code;
|
|
756
|
+
const namedKeys = Object.keys(cjsExports).filter((k) => k !== "__esModule" && k !== "default" && VALID_IDENT.test(k));
|
|
757
|
+
const hasEsmInterop = cjsExports.__esModule === true && "default" in cjsExports;
|
|
758
|
+
if (!hasEsmInterop && namedKeys.length === 0) return code;
|
|
759
|
+
return code.replace(/^export default (\w+\(\));?\s*$/m, (_, call) => [
|
|
760
|
+
`const __cjsMod = ${call};`,
|
|
761
|
+
hasEsmInterop ? `export default __cjsMod.default;` : `export default __cjsMod;`,
|
|
762
|
+
...namedKeys.map((k) => `export const ${k} = __cjsMod[${JSON.stringify(k)}];`)
|
|
763
|
+
].join("\n"));
|
|
764
|
+
} catch {
|
|
765
|
+
return code;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* 重写 import/export 语句的模块规范:
|
|
770
|
+
* - alias(如 `@/lib/api`)→ 解析后的项目内绝对 URL(带扩展名)
|
|
771
|
+
* - 相对路径(`./x`、`../x`)→ 同样解析后回写为绝对 URL
|
|
772
|
+
* - 项目内绝对路径(`/src/x`)→ 补扩展名
|
|
773
|
+
* - 其余 bare specifier → `/@modules/<spec>`
|
|
774
|
+
*
|
|
775
|
+
* 浏览器加载无扩展名 URL(如 `/src/i18n`)时,里面的相对路径 `./locales/en`
|
|
776
|
+
* 会按字面拼接得到 `/src/locales/en`,不会自动补 `.ts` 也不知道导入文件原先在
|
|
777
|
+
* `/src/i18n/`。直接按 alias / fs 解析、回写绝对 URL,浏览器就能命中正确文件。
|
|
778
|
+
*/
|
|
779
|
+
function rewriteImports(code, config, filePath, importedUrls, moduleGraph) {
|
|
780
|
+
const resolveSpec = createModuleSpecifierResolver(config, filePath);
|
|
781
|
+
const transformSpec = (spec) => {
|
|
782
|
+
const resolved = removeTimestampQuery(resolveSpec(spec));
|
|
783
|
+
importedUrls?.add(resolved);
|
|
784
|
+
const timestamp = moduleGraph?.getModuleByUrl(resolved)?.lastHMRTimestamp ?? 0;
|
|
785
|
+
return timestamp > 0 ? appendTimestampQuery(resolved, timestamp) : resolved;
|
|
786
|
+
};
|
|
787
|
+
return code.replace(/\bfrom\s+(['"])([^'"]+)\1/g, (_m, q, s) => `from ${q}${transformSpec(s)}${q}`).replace(/\bimport\s+(['"])([^'"]+)\1/g, (_m, q, s) => `import ${q}${transformSpec(s)}${q}`).replace(/\bimport\s*\(\s*(['"])([^'"]+)\1\s*\)/g, (_m, q, s) => `import(${q}${transformSpec(s)}${q})`);
|
|
788
|
+
}
|
|
789
|
+
function createModuleSpecifierResolver(config, filePath) {
|
|
790
|
+
const root = config.root;
|
|
791
|
+
const fileDir = path.dirname(filePath);
|
|
792
|
+
const aliasEntries = Object.entries(config.resolve.alias).sort(([a], [b]) => b.length - a.length);
|
|
793
|
+
const toServableUrl = (abs) => {
|
|
794
|
+
if (isUnderRoot(abs, root)) return "/" + path.relative(root, abs).replace(/\\/g, "/");
|
|
795
|
+
for (const pkgRoot of getLinkedPackageRoots(root)) if (abs === pkgRoot || abs.startsWith(pkgRoot + path.sep)) {
|
|
796
|
+
const normalized = abs.replace(/\\/g, "/");
|
|
797
|
+
return "/@fs/" + (normalized.startsWith("/") ? normalized.slice(1) : normalized);
|
|
798
|
+
}
|
|
799
|
+
return null;
|
|
800
|
+
};
|
|
801
|
+
return (specifier) => {
|
|
802
|
+
const suffixMatch = specifier.match(/[?#].*$/);
|
|
803
|
+
const suffix = suffixMatch ? suffixMatch[0] : "";
|
|
804
|
+
const baseSpec = suffix ? specifier.slice(0, -suffix.length) : specifier;
|
|
805
|
+
for (const [key, value] of aliasEntries) if (baseSpec === key || baseSpec.startsWith(key + "/")) {
|
|
806
|
+
const aliasBase = resolveAliasTarget(value, root);
|
|
807
|
+
const sub = baseSpec.slice(key.length).replace(/^\//, "");
|
|
808
|
+
const resolved = tryResolveDiskPath(sub ? path.join(aliasBase, sub) : aliasBase);
|
|
809
|
+
const url = resolved ? toServableUrl(resolved) : null;
|
|
810
|
+
return url ? url + suffix : specifier;
|
|
811
|
+
}
|
|
812
|
+
if (baseSpec.startsWith("./") || baseSpec.startsWith("../")) {
|
|
813
|
+
const resolved = tryResolveDiskPath(path.resolve(fileDir, baseSpec));
|
|
814
|
+
const url = resolved ? toServableUrl(resolved) : null;
|
|
815
|
+
return url ? url + suffix : specifier;
|
|
816
|
+
}
|
|
817
|
+
if (baseSpec.startsWith("/") && !baseSpec.startsWith("/@")) {
|
|
818
|
+
const resolved = tryResolveDiskPath(path.join(root, baseSpec.replace(/^\//, "")));
|
|
819
|
+
const url = resolved ? toServableUrl(resolved) : null;
|
|
820
|
+
return url ? url + suffix : specifier;
|
|
821
|
+
}
|
|
822
|
+
if (baseSpec.startsWith("/")) return specifier;
|
|
823
|
+
return `/@modules/${specifier}`;
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* 解析并规范化 import.meta.hot.accept() 的第一参数。
|
|
828
|
+
* HMR API 只允许字符串字面量或字面量数组,因此无需引入完整 AST。
|
|
829
|
+
*/
|
|
830
|
+
function rewriteHotAcceptDeps(code, config, filePath) {
|
|
831
|
+
const acceptedUrls = /* @__PURE__ */ new Set();
|
|
832
|
+
const edits = [];
|
|
833
|
+
const resolveSpec = createModuleSpecifierResolver(config, filePath);
|
|
834
|
+
const acceptRE = /(?:\bimport\.meta\.hot|\b__nasti_hot__)(?:(?:\?\.)|\.)accept\s*\(/g;
|
|
835
|
+
const searchableCode = maskStringsAndComments(code);
|
|
836
|
+
let isSelfAccepting = false;
|
|
837
|
+
let match;
|
|
838
|
+
while (match = acceptRE.exec(searchableCode)) {
|
|
839
|
+
let cursor = match.index + match[0].length;
|
|
840
|
+
const skipTrivia = () => {
|
|
841
|
+
while (cursor < code.length) {
|
|
842
|
+
if (/\s/.test(code[cursor])) {
|
|
843
|
+
cursor++;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (code[cursor] === "/" && code[cursor + 1] === "/") {
|
|
847
|
+
cursor += 2;
|
|
848
|
+
while (cursor < code.length && code[cursor] !== "\n") cursor++;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (code[cursor] === "/" && code[cursor + 1] === "*") {
|
|
852
|
+
cursor += 2;
|
|
853
|
+
while (cursor < code.length && !(code[cursor] === "*" && code[cursor + 1] === "/")) cursor++;
|
|
854
|
+
cursor += 2;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
skipTrivia();
|
|
861
|
+
const first = code[cursor];
|
|
862
|
+
if (!first || first === ")" || first !== "[" && first !== "'" && first !== "\"" && first !== "`") {
|
|
863
|
+
isSelfAccepting = true;
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const readLiteral = () => {
|
|
867
|
+
const quote = code[cursor];
|
|
868
|
+
if (quote !== "'" && quote !== "\"" && quote !== "`") return;
|
|
869
|
+
const start = cursor;
|
|
870
|
+
cursor++;
|
|
871
|
+
let raw = "";
|
|
872
|
+
while (cursor < code.length) {
|
|
873
|
+
const char = code[cursor];
|
|
874
|
+
if (char === "\\") {
|
|
875
|
+
raw += code[cursor + 1] ?? "";
|
|
876
|
+
cursor += 2;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (char === quote) {
|
|
880
|
+
cursor++;
|
|
881
|
+
const resolved = removeTimestampQuery(resolveSpec(raw));
|
|
882
|
+
acceptedUrls.add(resolved);
|
|
883
|
+
edits.push({
|
|
884
|
+
start,
|
|
885
|
+
end: cursor,
|
|
886
|
+
value: JSON.stringify(resolved)
|
|
887
|
+
});
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (quote === "`" && char === "$" && code[cursor + 1] === "{") return;
|
|
891
|
+
raw += char;
|
|
892
|
+
cursor++;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
if (first === "[") {
|
|
896
|
+
cursor++;
|
|
897
|
+
while (cursor < code.length) {
|
|
898
|
+
skipTrivia();
|
|
899
|
+
if (code[cursor] === ",") {
|
|
900
|
+
cursor++;
|
|
901
|
+
skipTrivia();
|
|
902
|
+
}
|
|
903
|
+
if (code[cursor] === "]") break;
|
|
904
|
+
const before = cursor;
|
|
905
|
+
readLiteral();
|
|
906
|
+
if (cursor === before) break;
|
|
907
|
+
}
|
|
908
|
+
} else readLiteral();
|
|
909
|
+
}
|
|
910
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) code = code.slice(0, edit.start) + edit.value + code.slice(edit.end);
|
|
911
|
+
return {
|
|
912
|
+
code,
|
|
913
|
+
acceptedUrls,
|
|
914
|
+
isSelfAccepting
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
/** 用等长空格遮蔽字符串、模板字面量和注释,避免把文档文本误识别为 HMR 调用。 */
|
|
918
|
+
function maskStringsAndComments(code) {
|
|
919
|
+
const masked = code.split("");
|
|
920
|
+
let state = "code";
|
|
921
|
+
const isRegexStart = (index) => {
|
|
922
|
+
let previous = index - 1;
|
|
923
|
+
while (previous >= 0 && /\s/.test(code[previous])) previous--;
|
|
924
|
+
return previous < 0 || "=(:,!&|?{};[]+-*%^~<>".includes(code[previous]);
|
|
925
|
+
};
|
|
926
|
+
for (let i = 0; i < code.length; i++) {
|
|
927
|
+
const char = code[i];
|
|
928
|
+
const next = code[i + 1];
|
|
929
|
+
if (state === "code") {
|
|
930
|
+
if (char === "'") state = "single";
|
|
931
|
+
else if (char === "\"") state = "double";
|
|
932
|
+
else if (char === "`") state = "template";
|
|
933
|
+
else if (char === "/" && next === "/") state = "line-comment";
|
|
934
|
+
else if (char === "/" && next === "*") state = "block-comment";
|
|
935
|
+
else if (char === "/" && isRegexStart(i)) state = "regex";
|
|
936
|
+
else continue;
|
|
937
|
+
masked[i] = " ";
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (state === "line-comment") {
|
|
941
|
+
if (char === "\n") state = "code";
|
|
942
|
+
else masked[i] = " ";
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
if (state === "block-comment") {
|
|
946
|
+
masked[i] = char === "\n" ? "\n" : " ";
|
|
947
|
+
if (char === "*" && next === "/") {
|
|
948
|
+
masked[i + 1] = " ";
|
|
949
|
+
i++;
|
|
950
|
+
state = "code";
|
|
951
|
+
}
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (state === "regex" || state === "regex-class") {
|
|
955
|
+
masked[i] = char === "\n" ? "\n" : " ";
|
|
956
|
+
if (char === "\\") {
|
|
957
|
+
if (i + 1 < code.length) masked[++i] = " ";
|
|
958
|
+
} else if (state === "regex" && char === "[") state = "regex-class";
|
|
959
|
+
else if (state === "regex-class" && char === "]") state = "regex";
|
|
960
|
+
else if (state === "regex" && char === "/") state = "code";
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
masked[i] = char === "\n" ? "\n" : " ";
|
|
964
|
+
if (char === "\\") {
|
|
965
|
+
if (i + 1 < code.length) masked[++i] = " ";
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (state === "single" && char === "'" || state === "double" && char === "\"" || state === "template" && char === "`") state = "code";
|
|
969
|
+
}
|
|
970
|
+
return masked.join("");
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* 把 alias 值统一成磁盘绝对路径。
|
|
974
|
+
* - `/Users/.../src` 这类绝对路径直接用
|
|
975
|
+
* - `/src` 这类用户写的「项目根相对」路径按 `<root>/src` 处理
|
|
976
|
+
* - 其余字符串相对 root 解析
|
|
977
|
+
*/
|
|
978
|
+
function resolveAliasTarget(value, root) {
|
|
979
|
+
if (path.isAbsolute(value) && fs.existsSync(value)) return value;
|
|
980
|
+
if (value.startsWith("/")) return path.join(root, value.slice(1));
|
|
981
|
+
return path.resolve(root, value);
|
|
982
|
+
}
|
|
983
|
+
/** 把磁盘上的目标路径补全为存在的文件(扩展名 / index 兜底)。 */
|
|
984
|
+
function tryResolveDiskPath(target) {
|
|
985
|
+
if (fs.existsSync(target) && fs.statSync(target).isFile()) return target;
|
|
986
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
987
|
+
const withExt = target + ext;
|
|
988
|
+
if (fs.existsSync(withExt) && fs.statSync(withExt).isFile()) return withExt;
|
|
989
|
+
}
|
|
990
|
+
if (fs.existsSync(target) && fs.statSync(target).isDirectory()) for (const ext of RESOLVE_EXTENSIONS) {
|
|
991
|
+
const idx = path.join(target, "index" + ext);
|
|
992
|
+
if (fs.existsSync(idx) && fs.statSync(idx).isFile()) return idx;
|
|
993
|
+
}
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
function appendTimestampQuery(url, timestamp) {
|
|
997
|
+
const hashIndex = url.indexOf("#");
|
|
998
|
+
const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
|
|
999
|
+
const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
|
1000
|
+
return `${withoutHash}${withoutHash.includes("?") ? "&" : "?"}t=${timestamp}${hash}`;
|
|
1001
|
+
}
|
|
1002
|
+
const RESOLVE_EXTENSIONS = [
|
|
1003
|
+
".tsx",
|
|
1004
|
+
".ts",
|
|
1005
|
+
".jsx",
|
|
1006
|
+
".js",
|
|
1007
|
+
".mjs",
|
|
1008
|
+
".json",
|
|
1009
|
+
".vue"
|
|
1010
|
+
];
|
|
1011
|
+
const ESM_CONDITIONS = [
|
|
1012
|
+
"import",
|
|
1013
|
+
"browser",
|
|
1014
|
+
"module",
|
|
1015
|
+
"default"
|
|
1016
|
+
];
|
|
1017
|
+
/**
|
|
1018
|
+
* 把 bundle 中被 external 化的 bare specifier 解析为浏览器可加载的 URL。
|
|
1019
|
+
*
|
|
1020
|
+
* 关键:相对「正在打包的包自身的真实目录」解析(而非项目根)。pnpm 严格布局下,
|
|
1021
|
+
* 传递依赖不会被提升到顶层 node_modules(只有直接依赖以 symlink 形式出现在那里),
|
|
1022
|
+
* 真实文件在 `.pnpm/<pkg>@<ver>/node_modules/` 内,其依赖是同目录下的兄弟包。
|
|
1023
|
+
* 从项目根向上走只能找到直接依赖 → 传递依赖(如 react-router 的 @tanstack/router-core)
|
|
1024
|
+
* 必然 404。改为从 importer 包的真实目录向上走即可命中。
|
|
1025
|
+
*
|
|
1026
|
+
* URL 去重(关键,关系到 React 等对实例唯一性敏感的库能否工作):用户源码里的裸 import
|
|
1027
|
+
* 被 rewriteImports 改写成裸 `/@modules/<spec>`,并由 dev server 从项目根解析。若从 importer
|
|
1028
|
+
* 真实目录解析与从项目根解析命中**同一真实文件**(npm/yarn 扁平布局,或依赖已被提升到顶层
|
|
1029
|
+
* node_modules),就必须同样发裸 URL —— 浏览器按 URL 字符串去重,带 `?id=` 会被当成另一个
|
|
1030
|
+
* 模块重复实例化。否则 app 的 `/@modules/react` 与 react-dom 外部化得到的
|
|
1031
|
+
* `/@modules/react?id=...` 会变成两份 React → 二者 dispatcher 不互通 → useEffect 读到 null →
|
|
1032
|
+
* "Invalid hook call. Hooks can only be called inside of the body of a function component"。
|
|
1033
|
+
*
|
|
1034
|
+
* 仅当项目根解析不到、或解析到的是**不同文件**(pnpm 未提升的传递依赖、多版本并存)时,
|
|
1035
|
+
* 才把 importer 相对解析到的绝对路径编码进 `?id=`,dev server 据此直接打包该文件,无需再次
|
|
1036
|
+
* 从 root 解析(见 transformRequest 中的 `/@modules/...?id=` 分支)。
|
|
1037
|
+
*/
|
|
1038
|
+
function externalSpecToModuleUrl(spec, baseDir, root) {
|
|
1039
|
+
const resolved = resolveNodeModule(baseDir, spec);
|
|
1040
|
+
if (!resolved) return `/@modules/${spec}`;
|
|
1041
|
+
const rootResolved = resolveNodeModule(root, spec);
|
|
1042
|
+
if (rootResolved && rootResolved === resolved) return `/@modules/${spec}`;
|
|
1043
|
+
return `/@modules/${spec}?id=${encodeURIComponent(resolved)}`;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* ESM-aware node_modules 解析:支持 package.json exports 字段的 import/browser/module/default 条件,
|
|
1047
|
+
* 兼容 ESM-only 包(如只有 "import" 条件而无 "require" 的包)。
|
|
1048
|
+
* createRequire 使用 CJS 解析逻辑,遇到 ESM-only exports 会抛异常,因此不能用于此场景。
|
|
1049
|
+
*
|
|
1050
|
+
* **返回真实路径**(解析 symlink):pnpm 把直接依赖以 symlink 放在顶层 node_modules,
|
|
1051
|
+
* 真实文件在 `.pnpm/.../node_modules/` 下。后续打包阶段用 `dirname(entryFile)` 向上找
|
|
1052
|
+
* 传递依赖,只有从真实路径出发才能命中那些未被提升的兄弟包。
|
|
1053
|
+
*/
|
|
1054
|
+
function resolveNodeModule(baseDir, moduleName) {
|
|
1055
|
+
const resolved = resolveNodeModuleEntry(baseDir, moduleName);
|
|
1056
|
+
if (!resolved) return null;
|
|
1057
|
+
try {
|
|
1058
|
+
return fs.realpathSync(resolved);
|
|
1059
|
+
} catch {
|
|
1060
|
+
return resolved;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function resolveNodeModuleEntry(root, moduleName) {
|
|
1064
|
+
let pkgName;
|
|
1065
|
+
let subpath;
|
|
1066
|
+
if (moduleName.startsWith("@")) {
|
|
1067
|
+
const parts = moduleName.split("/");
|
|
1068
|
+
pkgName = parts.slice(0, 2).join("/");
|
|
1069
|
+
subpath = parts.slice(2).join("/");
|
|
1070
|
+
} else {
|
|
1071
|
+
const slash = moduleName.indexOf("/");
|
|
1072
|
+
pkgName = slash === -1 ? moduleName : moduleName.slice(0, slash);
|
|
1073
|
+
subpath = slash === -1 ? "" : moduleName.slice(slash + 1);
|
|
1074
|
+
}
|
|
1075
|
+
let pkgDir = null;
|
|
1076
|
+
let dir = root;
|
|
1077
|
+
for (;;) {
|
|
1078
|
+
const candidate = path.join(dir, "node_modules", pkgName);
|
|
1079
|
+
if (fs.existsSync(candidate)) {
|
|
1080
|
+
pkgDir = candidate;
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
const parent = path.dirname(dir);
|
|
1084
|
+
if (parent === dir) break;
|
|
1085
|
+
dir = parent;
|
|
1086
|
+
}
|
|
1087
|
+
if (!pkgDir) return null;
|
|
1088
|
+
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
1089
|
+
if (!fs.existsSync(pkgJsonPath)) return null;
|
|
1090
|
+
let pkg;
|
|
1091
|
+
try {
|
|
1092
|
+
pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
1093
|
+
} catch {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
if (pkg.exports) {
|
|
1097
|
+
const exportKey = subpath ? `./${subpath}` : ".";
|
|
1098
|
+
const resolved = resolvePackageExports(pkg.exports, exportKey, pkgDir);
|
|
1099
|
+
if (resolved) return resolved;
|
|
1100
|
+
}
|
|
1101
|
+
if (subpath) {
|
|
1102
|
+
const subDirs = [""];
|
|
1103
|
+
for (const field of ["module", "main"]) if (typeof pkg[field] === "string") {
|
|
1104
|
+
const dir = path.dirname(pkg[field]);
|
|
1105
|
+
if (dir && dir !== "." && !subDirs.includes(dir)) subDirs.push(dir);
|
|
1106
|
+
}
|
|
1107
|
+
for (const dir of subDirs) {
|
|
1108
|
+
const direct = path.join(pkgDir, dir, subpath);
|
|
1109
|
+
if (fs.existsSync(direct) && fs.statSync(direct).isFile()) return direct;
|
|
1110
|
+
for (const ext of RESOLVE_EXTENSIONS) if (fs.existsSync(direct + ext)) return direct + ext;
|
|
1111
|
+
}
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
for (const field of [
|
|
1115
|
+
"module",
|
|
1116
|
+
"jsnext:main",
|
|
1117
|
+
"jsnext",
|
|
1118
|
+
"main"
|
|
1119
|
+
]) if (typeof pkg[field] === "string") {
|
|
1120
|
+
const entry = path.join(pkgDir, pkg[field]);
|
|
1121
|
+
if (fs.existsSync(entry)) return entry;
|
|
1122
|
+
}
|
|
1123
|
+
const indexFallback = path.join(pkgDir, "index.js");
|
|
1124
|
+
if (fs.existsSync(indexFallback)) return indexFallback;
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
function resolvePackageExports(exports, key, pkgDir) {
|
|
1128
|
+
if (typeof exports === "string") return key === "." ? path.join(pkgDir, exports) : null;
|
|
1129
|
+
const entry = exports[key];
|
|
1130
|
+
if (entry === void 0) {
|
|
1131
|
+
if (key === "." && typeof exports === "object" && !Object.keys(exports).some((k) => k.startsWith("."))) return resolveExportValue(exports, pkgDir);
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
return resolveExportValue(entry, pkgDir);
|
|
1135
|
+
}
|
|
1136
|
+
function resolveExportValue(value, pkgDir) {
|
|
1137
|
+
if (typeof value === "string") return path.join(pkgDir, value);
|
|
1138
|
+
if (Array.isArray(value)) {
|
|
1139
|
+
for (const item of value) {
|
|
1140
|
+
const r = resolveExportValue(item, pkgDir);
|
|
1141
|
+
if (r) return r;
|
|
1142
|
+
}
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
if (value && typeof value === "object") {
|
|
1146
|
+
for (const cond of ESM_CONDITIONS) if (cond in value) {
|
|
1147
|
+
const r = resolveExportValue(value[cond], pkgDir);
|
|
1148
|
+
if (r) return r;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
function resolveUrlToFile(url, root) {
|
|
1154
|
+
const cleanUrl = url.split(/[?#]/)[0];
|
|
1155
|
+
if (cleanUrl.startsWith("/@modules/")) return resolveNodeModule(root, cleanUrl.slice(10));
|
|
1156
|
+
if (cleanUrl.startsWith("/@fs/")) {
|
|
1157
|
+
let abs = cleanUrl.slice(5);
|
|
1158
|
+
if (process.platform === "win32") abs = abs.replace(/\//g, path.sep);
|
|
1159
|
+
else if (!abs.startsWith("/")) abs = "/" + abs;
|
|
1160
|
+
try {
|
|
1161
|
+
const real = fs.realpathSync(abs);
|
|
1162
|
+
if (fs.statSync(real).isFile() && isAllowedDevModulePath(real, root)) return real;
|
|
1163
|
+
} catch {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
const filePath = path.resolve(root, cleanUrl.replace(/^\//, ""));
|
|
1169
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) return filePath;
|
|
1170
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
1171
|
+
const withExt = filePath + ext;
|
|
1172
|
+
if (fs.existsSync(withExt)) return withExt;
|
|
1173
|
+
}
|
|
1174
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
1175
|
+
const indexFile = path.join(filePath, "index" + ext);
|
|
1176
|
+
if (fs.existsSync(indexFile)) return indexFile;
|
|
1177
|
+
}
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
function isModuleRequest(url, destination) {
|
|
1181
|
+
const cleanUrl = url.split(/[?#]/)[0];
|
|
1182
|
+
if (/\.(ts|tsx|jsx|js|mjs|vue|css|json)$/.test(cleanUrl)) return true;
|
|
1183
|
+
if (cleanUrl.startsWith("/@modules/")) return true;
|
|
1184
|
+
if (cleanUrl.startsWith("/@fs/")) return true;
|
|
1185
|
+
if (isAssetFile(cleanUrl)) {
|
|
1186
|
+
const qIdx = url.indexOf("?");
|
|
1187
|
+
const hIdx = url.indexOf("#");
|
|
1188
|
+
const queryEnd = hIdx === -1 ? url.length : hIdx;
|
|
1189
|
+
const query = qIdx === -1 || qIdx > queryEnd ? "" : url.slice(qIdx + 1, queryEnd);
|
|
1190
|
+
return /(?:^|&)(?:url|raw)(?:&|$)/.test(query) || destination === "script";
|
|
1191
|
+
}
|
|
1192
|
+
if (!path.extname(cleanUrl)) return true;
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
function getHmrClientCode() {
|
|
1196
|
+
return `
|
|
1197
|
+
// Nasti HMR Client
|
|
1198
|
+
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
1199
|
+
const socket = new WebSocket(socketProtocol + '://' + location.host, 'nasti-hmr');
|
|
1200
|
+
const hotModulesMap = new Map();
|
|
1201
|
+
const disposeMap = new Map();
|
|
1202
|
+
const pruneMap = new Map();
|
|
1203
|
+
const dataMap = new Map();
|
|
1204
|
+
const customListenersMap = new Map();
|
|
1205
|
+
let updateQueue = [];
|
|
1206
|
+
let pendingUpdateQueue = false;
|
|
1207
|
+
|
|
1208
|
+
socket.addEventListener('message', async ({ data }) => {
|
|
1209
|
+
const payload = JSON.parse(data);
|
|
1210
|
+
// 默认浏览器 client 只消费自己的 HMR 消息;native/worker 环境通过各自的
|
|
1211
|
+
// HotChannel 或 app-level HMR 协调器处理同一 transport 上的命名消息。
|
|
1212
|
+
if (payload.environment && payload.environment !== 'client') return;
|
|
1213
|
+
switch (payload.type) {
|
|
1214
|
+
case 'connected':
|
|
1215
|
+
console.debug('[nasti] connected.');
|
|
1216
|
+
clearErrorOverlay();
|
|
1217
|
+
break;
|
|
1218
|
+
case 'update':
|
|
1219
|
+
try {
|
|
1220
|
+
// CSS 在 unbundled 模式下也是会注入 <style> 的 JS 模块,和普通 JS
|
|
1221
|
+
// 一样重新 import 才能执行 dispose/accept 并保持页面状态。
|
|
1222
|
+
await Promise.all(payload.updates.map(queueUpdate));
|
|
1223
|
+
clearErrorOverlay();
|
|
1224
|
+
console.debug('[nasti] HMR update complete.');
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
console.error('[nasti] HMR update failed:', err);
|
|
1227
|
+
showErrorOverlay(err);
|
|
1228
|
+
}
|
|
1229
|
+
break;
|
|
1230
|
+
case 'full-reload':
|
|
1231
|
+
console.log('[nasti] full reload');
|
|
1232
|
+
location.reload();
|
|
1233
|
+
break;
|
|
1234
|
+
case 'prune':
|
|
1235
|
+
await Promise.all(payload.paths.map(async (path) => {
|
|
1236
|
+
const data = dataMap.get(path);
|
|
1237
|
+
const dispose = disposeMap.get(path);
|
|
1238
|
+
const prune = pruneMap.get(path);
|
|
1239
|
+
if (dispose) await dispose(data);
|
|
1240
|
+
if (prune) await prune(data);
|
|
1241
|
+
hotModulesMap.delete(path);
|
|
1242
|
+
disposeMap.delete(path);
|
|
1243
|
+
pruneMap.delete(path);
|
|
1244
|
+
dataMap.delete(path);
|
|
1245
|
+
clearCustomListeners(path);
|
|
1246
|
+
}));
|
|
1247
|
+
break;
|
|
1248
|
+
case 'custom': {
|
|
1249
|
+
const listenersByOwner = customListenersMap.get(payload.event);
|
|
1250
|
+
if (!listenersByOwner) break;
|
|
1251
|
+
const results = await Promise.allSettled(
|
|
1252
|
+
[...listenersByOwner.values()]
|
|
1253
|
+
.flatMap((listeners) => [...listeners])
|
|
1254
|
+
.map((listener) => Promise.resolve().then(() => listener(payload.data)))
|
|
1255
|
+
);
|
|
1256
|
+
for (const result of results) {
|
|
1257
|
+
if (result.status === 'rejected') {
|
|
1258
|
+
console.error('[nasti] custom HMR event listener failed:', result.reason);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
break;
|
|
1262
|
+
}
|
|
1263
|
+
case 'error':
|
|
1264
|
+
console.error('[nasti] error:', payload.err.message);
|
|
1265
|
+
showErrorOverlay(payload.err);
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
// 服务重启后旧模块图已失效,重连时整页刷新是必要兜底;正常 update 不再刷新。
|
|
1271
|
+
let reconnectTimer = 0;
|
|
1272
|
+
socket.addEventListener('close', () => {
|
|
1273
|
+
clearTimeout(reconnectTimer);
|
|
1274
|
+
reconnectTimer = setTimeout(() => location.reload(), 1000);
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* 同一批更新先全部拉取,再按服务端消息顺序执行 accept 回调,避免 HTTP 往返速度
|
|
1279
|
+
* 改变模块应用顺序。这与 Vite HMRClient 的 fetch/apply 两阶段一致。
|
|
1280
|
+
*/
|
|
1281
|
+
async function queueUpdate(update) {
|
|
1282
|
+
updateQueue.push(fetchUpdate(update));
|
|
1283
|
+
if (pendingUpdateQueue) return;
|
|
1284
|
+
|
|
1285
|
+
pendingUpdateQueue = true;
|
|
1286
|
+
await Promise.resolve();
|
|
1287
|
+
pendingUpdateQueue = false;
|
|
1288
|
+
const loading = updateQueue;
|
|
1289
|
+
updateQueue = [];
|
|
1290
|
+
const applyUpdates = await Promise.all(loading);
|
|
1291
|
+
for (const apply of applyUpdates) {
|
|
1292
|
+
if (apply) apply();
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
async function fetchUpdate(update) {
|
|
1297
|
+
const mod = hotModulesMap.get(update.path);
|
|
1298
|
+
// 尚未在当前页面加载的动态模块不需要更新。
|
|
1299
|
+
if (!mod) return;
|
|
1300
|
+
|
|
1301
|
+
// 必须在重新 import 前确定旧回调;新模块执行 createHotContext 时会清空并注册新回调。
|
|
1302
|
+
const qualifiedCallbacks = mod.callbacks.filter(({ deps }) =>
|
|
1303
|
+
deps.includes(update.acceptedPath)
|
|
1304
|
+
);
|
|
1305
|
+
const isSelfUpdate = update.path === update.acceptedPath;
|
|
1306
|
+
if (!isSelfUpdate && qualifiedCallbacks.length === 0) return;
|
|
1307
|
+
|
|
1308
|
+
const dispose = disposeMap.get(update.acceptedPath);
|
|
1309
|
+
if (dispose) await dispose(dataMap.get(update.acceptedPath));
|
|
1310
|
+
const newMod = await import(appendTimestampQuery(update.acceptedPath, update.timestamp));
|
|
1311
|
+
|
|
1312
|
+
return () => {
|
|
1313
|
+
for (const { deps, fn } of qualifiedCallbacks) {
|
|
1314
|
+
fn(deps.map((dep) => dep === update.acceptedPath ? newMod : undefined));
|
|
1315
|
+
}
|
|
1316
|
+
const detail = isSelfUpdate
|
|
1317
|
+
? update.path
|
|
1318
|
+
: update.acceptedPath + ' via ' + update.path;
|
|
1319
|
+
console.debug('[nasti] hot updated:', detail);
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function appendTimestampQuery(url, timestamp) {
|
|
1324
|
+
const hashIndex = url.indexOf('#');
|
|
1325
|
+
const hash = hashIndex >= 0 ? url.slice(hashIndex) : '';
|
|
1326
|
+
const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
|
1327
|
+
return withoutHash + (withoutHash.includes('?') ? '&' : '?') + 't=' + timestamp + hash;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function clearErrorOverlay() {
|
|
1331
|
+
const el = document.getElementById('nasti-error-overlay');
|
|
1332
|
+
if (el) el.remove();
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function showErrorOverlay(err) {
|
|
1336
|
+
clearErrorOverlay();
|
|
1337
|
+
const overlay = document.createElement('div');
|
|
1338
|
+
overlay.id = 'nasti-error-overlay';
|
|
1339
|
+
overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.85);color:#fff;font-family:monospace;padding:2rem;overflow:auto;';
|
|
1340
|
+
const title = document.createElement('h2');
|
|
1341
|
+
title.style.color = '#ff5555';
|
|
1342
|
+
title.textContent = 'Build Error';
|
|
1343
|
+
const pre = document.createElement('pre');
|
|
1344
|
+
pre.textContent = err.message + '\\n' + (err.stack || '');
|
|
1345
|
+
const btn = document.createElement('button');
|
|
1346
|
+
btn.style.cssText = 'margin-top:1rem;padding:0.5rem 1rem;cursor:pointer';
|
|
1347
|
+
btn.textContent = 'Close';
|
|
1348
|
+
btn.onclick = () => overlay.remove();
|
|
1349
|
+
overlay.appendChild(title);
|
|
1350
|
+
overlay.appendChild(pre);
|
|
1351
|
+
overlay.appendChild(btn);
|
|
1352
|
+
document.body.appendChild(overlay);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
export function createHotContext(ownerPath) {
|
|
1356
|
+
if (!dataMap.has(ownerPath)) dataMap.set(ownerPath, {});
|
|
1357
|
+
|
|
1358
|
+
// 模块重新执行时丢弃旧 accept 回调,但保留同一个 hot.data 对象。
|
|
1359
|
+
const existing = hotModulesMap.get(ownerPath);
|
|
1360
|
+
if (existing) existing.callbacks = [];
|
|
1361
|
+
clearCustomListeners(ownerPath);
|
|
1362
|
+
|
|
1363
|
+
const acceptDeps = (deps, callback = () => {}) => {
|
|
1364
|
+
const mod = hotModulesMap.get(ownerPath) || { id: ownerPath, callbacks: [] };
|
|
1365
|
+
mod.callbacks.push({ deps, fn: callback });
|
|
1366
|
+
hotModulesMap.set(ownerPath, mod);
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
return {
|
|
1370
|
+
accept(deps, callback) {
|
|
1371
|
+
if (typeof deps === 'function' || deps === undefined) {
|
|
1372
|
+
acceptDeps([ownerPath], ([mod]) => deps?.(mod));
|
|
1373
|
+
} else if (typeof deps === 'string') {
|
|
1374
|
+
acceptDeps([deps], ([mod]) => callback?.(mod));
|
|
1375
|
+
} else if (Array.isArray(deps)) {
|
|
1376
|
+
acceptDeps(deps, callback);
|
|
1377
|
+
} else {
|
|
1378
|
+
throw new Error('invalid hot.accept() usage');
|
|
1379
|
+
}
|
|
1380
|
+
},
|
|
1381
|
+
prune(callback) {
|
|
1382
|
+
pruneMap.set(ownerPath, callback);
|
|
1383
|
+
},
|
|
1384
|
+
dispose(callback) {
|
|
1385
|
+
disposeMap.set(ownerPath, callback);
|
|
1386
|
+
},
|
|
1387
|
+
on(event, callback) {
|
|
1388
|
+
let listenersByOwner = customListenersMap.get(event);
|
|
1389
|
+
if (!listenersByOwner) {
|
|
1390
|
+
listenersByOwner = new Map();
|
|
1391
|
+
customListenersMap.set(event, listenersByOwner);
|
|
1392
|
+
}
|
|
1393
|
+
let listeners = listenersByOwner.get(ownerPath);
|
|
1394
|
+
if (!listeners) {
|
|
1395
|
+
listeners = new Set();
|
|
1396
|
+
listenersByOwner.set(ownerPath, listeners);
|
|
1397
|
+
}
|
|
1398
|
+
listeners.add(callback);
|
|
1399
|
+
},
|
|
1400
|
+
off(event, callback) {
|
|
1401
|
+
const listenersByOwner = customListenersMap.get(event);
|
|
1402
|
+
const listeners = listenersByOwner?.get(ownerPath);
|
|
1403
|
+
listeners?.delete(callback);
|
|
1404
|
+
if (listeners?.size === 0) listenersByOwner.delete(ownerPath);
|
|
1405
|
+
if (listenersByOwner?.size === 0) customListenersMap.delete(event);
|
|
1406
|
+
},
|
|
1407
|
+
invalidate() {
|
|
1408
|
+
location.reload();
|
|
1409
|
+
},
|
|
1410
|
+
data: dataMap.get(ownerPath),
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function clearCustomListeners(ownerPath) {
|
|
1415
|
+
for (const [event, listenersByOwner] of customListenersMap) {
|
|
1416
|
+
listenersByOwner.delete(ownerPath);
|
|
1417
|
+
if (listenersByOwner.size === 0) customListenersMap.delete(event);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
`;
|
|
1421
|
+
}
|
|
1422
|
+
//#endregion
|
|
1423
|
+
//#region src/server/hmr.ts
|
|
1424
|
+
async function handleFileChange(file, server, environmentName = "client", timestamp = Date.now()) {
|
|
1425
|
+
const { config } = server;
|
|
1426
|
+
const environment = server.environments[environmentName];
|
|
1427
|
+
if (!environment) throw new Error(`[nasti] unknown dev environment "${environmentName}"`);
|
|
1428
|
+
const moduleGraph = environment.moduleGraph;
|
|
1429
|
+
const logger = config.logger;
|
|
1430
|
+
const relativePath = "/" + path.relative(config.root, file);
|
|
1431
|
+
const shortFile = path.relative(config.root, file);
|
|
1432
|
+
let mods = moduleGraph.getModulesByFile(file);
|
|
1433
|
+
if (!mods || mods.size === 0) try {
|
|
1434
|
+
const real = fs.realpathSync(file);
|
|
1435
|
+
if (real !== file) mods = moduleGraph.getModulesByFile(real);
|
|
1436
|
+
if (mods && mods.size > 0) file = real;
|
|
1437
|
+
} catch {}
|
|
1438
|
+
if (!mods || mods.size === 0) {
|
|
1439
|
+
const packageRoot = findNearestPackageRoot(file);
|
|
1440
|
+
if (packageRoot && getLinkedPackageRoots(config.root).some((r) => packageRoot === r || packageRoot.startsWith(r + path.sep))) {
|
|
1441
|
+
const under = moduleGraph.getModulesWithFileUnder(packageRoot);
|
|
1442
|
+
if (under.size > 0) mods = under;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (!mods || mods.size === 0) return null;
|
|
1446
|
+
const updates = [];
|
|
1447
|
+
const graph = moduleGraph;
|
|
1448
|
+
const invalidatedModules = /* @__PURE__ */ new Set();
|
|
1449
|
+
const affectedSet = /* @__PURE__ */ new Set();
|
|
1450
|
+
let fullReload = false;
|
|
1451
|
+
for (const mod of mods) {
|
|
1452
|
+
graph.invalidateModuleAndImporters(mod, timestamp, invalidatedModules);
|
|
1453
|
+
const ctx = {
|
|
1454
|
+
file,
|
|
1455
|
+
timestamp,
|
|
1456
|
+
modules: [mod],
|
|
1457
|
+
read: () => fs.readFileSync(file, "utf-8"),
|
|
1458
|
+
server,
|
|
1459
|
+
environment
|
|
1460
|
+
};
|
|
1461
|
+
let affectedModules = [mod];
|
|
1462
|
+
for (const plugin of environment.plugins) if (plugin.handleHotUpdate) {
|
|
1463
|
+
const result = await plugin.handleHotUpdate(ctx);
|
|
1464
|
+
if (result) affectedModules = result;
|
|
1465
|
+
}
|
|
1466
|
+
for (const affected of affectedModules) {
|
|
1467
|
+
affectedSet.add(affected);
|
|
1468
|
+
graph.invalidateModuleAndImporters(affected, timestamp, invalidatedModules);
|
|
1469
|
+
const boundaries = graph.getHmrBoundaries(affected);
|
|
1470
|
+
if (boundaries.length === 0) {
|
|
1471
|
+
fullReload = true;
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
for (const { boundary, acceptedVia } of boundaries) {
|
|
1475
|
+
const update = {
|
|
1476
|
+
type: boundary.type === "css" ? "css-update" : "js-update",
|
|
1477
|
+
path: boundary.url,
|
|
1478
|
+
acceptedPath: acceptedVia.url,
|
|
1479
|
+
timestamp
|
|
1480
|
+
};
|
|
1481
|
+
if (!updates.some((existing) => existing.type === update.type && existing.path === update.path && existing.acceptedPath === update.acceptedPath)) updates.push(update);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
const transformed = await Promise.all([...affectedSet].map(async (module) => ({
|
|
1486
|
+
module,
|
|
1487
|
+
result: await environment.transformRequest(module.url)
|
|
1488
|
+
})));
|
|
1489
|
+
const logPrefix = environmentName === "client" ? "" : `[${environmentName}] `;
|
|
1490
|
+
if (fullReload) {
|
|
1491
|
+
logger.info(pc.green(`${logPrefix}reload `) + pc.dim(shortFile), { timestamp: true });
|
|
1492
|
+
environment.hot.send({
|
|
1493
|
+
type: "full-reload",
|
|
1494
|
+
path: relativePath
|
|
1495
|
+
});
|
|
1496
|
+
} else if (updates.length > 0) {
|
|
1497
|
+
logger.info(updates.map((u) => pc.green(`${logPrefix}hmr update `) + pc.dim(u.path)).join("\n"), { timestamp: true });
|
|
1498
|
+
environment.hot.send({
|
|
1499
|
+
type: "update",
|
|
1500
|
+
updates
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
return {
|
|
1504
|
+
environment,
|
|
1505
|
+
modules: [...affectedSet],
|
|
1506
|
+
updates,
|
|
1507
|
+
transformed,
|
|
1508
|
+
fullReload
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
//#endregion
|
|
1512
|
+
//#region src/server/index.ts
|
|
1513
|
+
var server_exports = /* @__PURE__ */ __exportAll({ createServer: () => createServer });
|
|
1514
|
+
async function createServer(inlineConfig = {}) {
|
|
1515
|
+
const startTime = performance.now();
|
|
1516
|
+
const config = await resolveConfig(inlineConfig, "serve");
|
|
1517
|
+
const logger = config.logger;
|
|
1518
|
+
const allPlugins = resolvePluginList(config, config.plugins, { environmentName: "client" });
|
|
1519
|
+
const configWithPlugins = {
|
|
1520
|
+
...config,
|
|
1521
|
+
plugins: allPlugins
|
|
1522
|
+
};
|
|
1523
|
+
const app = connect();
|
|
1524
|
+
const httpServer = http.createServer(app);
|
|
1525
|
+
const ws = createWebSocketServer(httpServer);
|
|
1526
|
+
const pluginApi = getPluginApi(config);
|
|
1527
|
+
const clientEnv = new NastiEnvironment("client", config, {
|
|
1528
|
+
hot: createWsHotChannel(ws, "client"),
|
|
1529
|
+
mode: "dev",
|
|
1530
|
+
plugins: allPlugins,
|
|
1531
|
+
pluginApi
|
|
1532
|
+
});
|
|
1533
|
+
await clientEnv.init();
|
|
1534
|
+
const environments = { client: clientEnv };
|
|
1535
|
+
for (const name of Object.keys(config.environments)) {
|
|
1536
|
+
if (name === "client") continue;
|
|
1537
|
+
const consumer = config.environments[name].consumer;
|
|
1538
|
+
const envPlugins = resolvePluginList(config, config.plugins, {
|
|
1539
|
+
consumer,
|
|
1540
|
+
environmentName: name
|
|
1541
|
+
});
|
|
1542
|
+
environments[name] = new NastiEnvironment(name, config, {
|
|
1543
|
+
hot: consumer === "client" ? createWsHotChannel(ws, name) : void 0,
|
|
1544
|
+
mode: "dev",
|
|
1545
|
+
plugins: envPlugins,
|
|
1546
|
+
pluginApi
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
for (const [name, environment] of Object.entries(environments)) if (name === "client" || environment.consumer === "client" || environment.options.driver) await environment.init();
|
|
1550
|
+
const transformContexts = /* @__PURE__ */ new Map();
|
|
1551
|
+
for (const environment of Object.values(environments)) {
|
|
1552
|
+
if (environment.consumer !== "client" || environment.driver) continue;
|
|
1553
|
+
const environmentConfig = {
|
|
1554
|
+
...configWithPlugins,
|
|
1555
|
+
resolve: environment.options.resolve,
|
|
1556
|
+
build: environment.options.build,
|
|
1557
|
+
plugins: environment.plugins
|
|
1558
|
+
};
|
|
1559
|
+
const context = {
|
|
1560
|
+
config: environmentConfig,
|
|
1561
|
+
pluginContainer: environment.pluginContainer,
|
|
1562
|
+
moduleGraph: environment.moduleGraph,
|
|
1563
|
+
environment,
|
|
1564
|
+
envDefine: buildEnvDefine(loadEnv(environmentConfig.mode, environmentConfig.root, environmentConfig.envPrefix), environmentConfig.mode, ssrDefineOverrides(environment.consumer)),
|
|
1565
|
+
onPrune: (paths) => environment.hot.send({
|
|
1566
|
+
type: "prune",
|
|
1567
|
+
paths
|
|
1568
|
+
})
|
|
1569
|
+
};
|
|
1570
|
+
transformContexts.set(environment.name, context);
|
|
1571
|
+
environment.configureDevPipeline((url) => transformRequest(url, context));
|
|
1572
|
+
}
|
|
1573
|
+
let ssrRunner = null;
|
|
1574
|
+
async function getSsrRunner() {
|
|
1575
|
+
if (ssrRunner) return ssrRunner;
|
|
1576
|
+
const ssrEnv = environments.ssr;
|
|
1577
|
+
if (!ssrEnv) throw new Error("[nasti] no \"ssr\" environment configured (config.environments.ssr)");
|
|
1578
|
+
await ssrEnv.init();
|
|
1579
|
+
const { createModuleRunner } = await import("./runnable-environment-BjoOvdDa.js");
|
|
1580
|
+
ssrRunner = createModuleRunner(ssrEnv);
|
|
1581
|
+
return ssrRunner;
|
|
1582
|
+
}
|
|
1583
|
+
const moduleGraph = clientEnv.moduleGraph;
|
|
1584
|
+
let bundledServer = null;
|
|
1585
|
+
if (config.experimental.bundledDev) {
|
|
1586
|
+
const { createBundledDevServer } = await import("./dev-engine-DapuTPIz.js");
|
|
1587
|
+
bundledServer = await createBundledDevServer({
|
|
1588
|
+
config: configWithPlugins,
|
|
1589
|
+
clientEnv,
|
|
1590
|
+
httpServer
|
|
1591
|
+
});
|
|
1592
|
+
app.use(bundledServer.middleware);
|
|
1593
|
+
}
|
|
1594
|
+
const ignoredSegments = /* @__PURE__ */ new Set([
|
|
1595
|
+
"node_modules",
|
|
1596
|
+
".git",
|
|
1597
|
+
".nasti"
|
|
1598
|
+
]);
|
|
1599
|
+
const outDirAbs = path.resolve(config.root, config.build.outDir);
|
|
1600
|
+
const linkedPackageRoots = getLinkedPackageRoots(config.root).filter((r) => r !== config.root && !isUnderRoot(config.root, r));
|
|
1601
|
+
const watchTargets = [config.root, ...linkedPackageRoots];
|
|
1602
|
+
const watcher = watch(watchTargets, {
|
|
1603
|
+
ignored: (filePath) => {
|
|
1604
|
+
if (filePath === outDirAbs || filePath.startsWith(outDirAbs + path.sep)) return true;
|
|
1605
|
+
for (const watchRoot of watchTargets) {
|
|
1606
|
+
if (filePath === watchRoot) return false;
|
|
1607
|
+
const rel = path.relative(watchRoot, filePath);
|
|
1608
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
1609
|
+
for (const seg of rel.split(path.sep)) if (ignoredSegments.has(seg)) return true;
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
return false;
|
|
1613
|
+
},
|
|
1614
|
+
ignoreInitial: true
|
|
1615
|
+
});
|
|
1616
|
+
await new Promise((resolve, reject) => {
|
|
1617
|
+
const onReady = () => {
|
|
1618
|
+
watcher.off("error", onError);
|
|
1619
|
+
resolve();
|
|
1620
|
+
};
|
|
1621
|
+
const onError = (err) => {
|
|
1622
|
+
watcher.off("ready", onReady);
|
|
1623
|
+
reject(err);
|
|
1624
|
+
};
|
|
1625
|
+
watcher.once("ready", onReady);
|
|
1626
|
+
watcher.once("error", onError);
|
|
1627
|
+
});
|
|
1628
|
+
let server;
|
|
1629
|
+
const environmentServices = {};
|
|
1630
|
+
let environmentDriversStarted = false;
|
|
1631
|
+
let devPipelinesStarted = false;
|
|
1632
|
+
const startDevPipelines = async () => {
|
|
1633
|
+
if (devPipelinesStarted) return;
|
|
1634
|
+
devPipelinesStarted = true;
|
|
1635
|
+
for (const environment of Object.values(environments)) {
|
|
1636
|
+
if (!transformContexts.has(environment.name)) continue;
|
|
1637
|
+
await environment.pluginContainer.buildStart();
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
const logCloseError = (target, error) => {
|
|
1641
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1642
|
+
logger.error(`[nasti] failed to close ${target}`, { error: normalized });
|
|
1643
|
+
};
|
|
1644
|
+
const startEnvironmentDrivers = async () => {
|
|
1645
|
+
if (environmentDriversStarted) return;
|
|
1646
|
+
environmentDriversStarted = true;
|
|
1647
|
+
const started = [];
|
|
1648
|
+
const attempted = [];
|
|
1649
|
+
try {
|
|
1650
|
+
for (const [name, environment] of Object.entries(environments)) {
|
|
1651
|
+
if (!environment.driver?.serve) continue;
|
|
1652
|
+
attempted.push(environment);
|
|
1653
|
+
const result = await environment.driver.serve({
|
|
1654
|
+
...environment.getDriverContext(),
|
|
1655
|
+
server
|
|
1656
|
+
});
|
|
1657
|
+
started.push({
|
|
1658
|
+
name,
|
|
1659
|
+
environment,
|
|
1660
|
+
service: result ?? {}
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
for (const { name, service } of started) {
|
|
1664
|
+
environmentServices[name] = service;
|
|
1665
|
+
if (service.middleware) app.use(service.middleware);
|
|
1666
|
+
}
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
environmentDriversStarted = false;
|
|
1669
|
+
for (const { name } of started) delete environmentServices[name];
|
|
1670
|
+
for (const environment of attempted.reverse()) try {
|
|
1671
|
+
await environment.driver?.close?.(environment.getDriverContext());
|
|
1672
|
+
} catch (closeError) {
|
|
1673
|
+
logCloseError(`environment driver "${environment.driver.name}"`, closeError);
|
|
1674
|
+
}
|
|
1675
|
+
throw error;
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
const notifyEnvironmentDrivers = (file, event) => {
|
|
1679
|
+
for (const environment of Object.values(environments)) {
|
|
1680
|
+
if (!environment.driver?.watchChange) continue;
|
|
1681
|
+
Promise.resolve(environment.driver.watchChange(file, event, environment.getDriverContext())).catch((error) => {
|
|
1682
|
+
logger.error(`[nasti] environment driver "${environment.driver.name}" watchChange failed`, { error });
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
const updateClientEnvironments = async (file) => {
|
|
1687
|
+
const timestamp = Date.now();
|
|
1688
|
+
const results = {};
|
|
1689
|
+
for (const environment of Object.values(environments)) {
|
|
1690
|
+
if (environment.consumer !== "client" || environment.driver) continue;
|
|
1691
|
+
try {
|
|
1692
|
+
const result = await handleFileChange(file, server, environment.name, timestamp);
|
|
1693
|
+
if (result) results[environment.name] = result;
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1696
|
+
logger.error(`[nasti] HMR failed for environment "${environment.name}": ${normalized.message}`, { error: normalized });
|
|
1697
|
+
try {
|
|
1698
|
+
environment.hot.send({
|
|
1699
|
+
type: "error",
|
|
1700
|
+
err: {
|
|
1701
|
+
message: normalized.message,
|
|
1702
|
+
stack: normalized.stack
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
} catch (channelError) {
|
|
1706
|
+
const channelFailure = channelError instanceof Error ? channelError : new Error(String(channelError));
|
|
1707
|
+
logger.error(`[nasti] failed to deliver HMR error to environment "${environment.name}"`, { error: channelFailure });
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (Object.keys(results).length === 0) return;
|
|
1712
|
+
const context = {
|
|
1713
|
+
file,
|
|
1714
|
+
timestamp,
|
|
1715
|
+
environments: Object.freeze({ ...results }),
|
|
1716
|
+
server
|
|
1717
|
+
};
|
|
1718
|
+
for (const plugin of config.plugins) await plugin.handleHotUpdateApp?.(context);
|
|
1719
|
+
};
|
|
1720
|
+
const queueClientEnvironmentUpdate = (file) => {
|
|
1721
|
+
updateClientEnvironments(file).catch((error) => {
|
|
1722
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1723
|
+
logger.error(`[nasti] multi-environment HMR failed: ${normalized.message}`, { error: normalized });
|
|
1724
|
+
});
|
|
1725
|
+
};
|
|
1726
|
+
watcher.on("change", (file) => {
|
|
1727
|
+
if (file.includes(`${path.sep}node_modules${path.sep}`) || file.endsWith(`${path.sep}node_modules`)) clearLinkedPackageRootsCache();
|
|
1728
|
+
ssrRunner?.invalidateFile(file);
|
|
1729
|
+
queueClientEnvironmentUpdate(file);
|
|
1730
|
+
notifyEnvironmentDrivers(file, "change");
|
|
1731
|
+
});
|
|
1732
|
+
watcher.on("add", (file) => {
|
|
1733
|
+
if (file.includes(`${path.sep}node_modules${path.sep}`) || file.endsWith(`${path.sep}node_modules`)) clearLinkedPackageRootsCache();
|
|
1734
|
+
ssrRunner?.invalidateFile(file);
|
|
1735
|
+
queueClientEnvironmentUpdate(file);
|
|
1736
|
+
notifyEnvironmentDrivers(file, "add");
|
|
1737
|
+
});
|
|
1738
|
+
watcher.on("unlink", (file) => {
|
|
1739
|
+
if (file.includes(`${path.sep}node_modules${path.sep}`) || file.endsWith(`${path.sep}node_modules`)) clearLinkedPackageRootsCache();
|
|
1740
|
+
ssrRunner?.invalidateFile(file);
|
|
1741
|
+
notifyEnvironmentDrivers(file, "unlink");
|
|
1742
|
+
});
|
|
1743
|
+
server = {
|
|
1744
|
+
config: configWithPlugins,
|
|
1745
|
+
middlewares: app,
|
|
1746
|
+
moduleGraph,
|
|
1747
|
+
watcher,
|
|
1748
|
+
ws,
|
|
1749
|
+
environments,
|
|
1750
|
+
environmentServices,
|
|
1751
|
+
async listen(port) {
|
|
1752
|
+
const finalPort = port ?? config.server.port;
|
|
1753
|
+
const host = config.server.host === true ? "0.0.0.0" : config.server.host;
|
|
1754
|
+
await startDevPipelines();
|
|
1755
|
+
await startEnvironmentDrivers();
|
|
1756
|
+
return new Promise((resolve, reject) => {
|
|
1757
|
+
let currentPort = finalPort;
|
|
1758
|
+
const onListening = () => {
|
|
1759
|
+
const actualPort = httpServer.address()?.port ?? currentPort;
|
|
1760
|
+
config.server.port = actualPort;
|
|
1761
|
+
const localUrl = `http://localhost:${actualPort}/`;
|
|
1762
|
+
const networkUrl = host === "0.0.0.0" ? `http://${getNetworkAddress()}:${actualPort}/` : null;
|
|
1763
|
+
const driverLocalUrls = Object.values(environmentServices).flatMap((service) => service.localUrls ?? []);
|
|
1764
|
+
const driverNetworkUrls = Object.values(environmentServices).flatMap((service) => service.networkUrls ?? []);
|
|
1765
|
+
logger.clearScreen("info");
|
|
1766
|
+
const readyIn = Math.ceil(performance.now() - startTime);
|
|
1767
|
+
logger.info(`\n ${pc.cyan(pc.bold("NASTI"))} ${pc.cyan(`v2.5.2`)} ${pc.dim("ready in")} ${pc.bold(readyIn)} ${pc.dim("ms")}\n`);
|
|
1768
|
+
printServerUrls({
|
|
1769
|
+
local: [localUrl, ...driverLocalUrls],
|
|
1770
|
+
network: [...networkUrl ? [networkUrl] : [], ...driverNetworkUrls]
|
|
1771
|
+
}, logger.info);
|
|
1772
|
+
logger.info("");
|
|
1773
|
+
resolve(server);
|
|
1774
|
+
};
|
|
1775
|
+
httpServer.on("listening", onListening);
|
|
1776
|
+
httpServer.on("error", (err) => {
|
|
1777
|
+
if (err.code === "EADDRINUSE") {
|
|
1778
|
+
currentPort++;
|
|
1779
|
+
logger.info(pc.yellow(`Port ${currentPort - 1} is in use, trying ${currentPort}...`));
|
|
1780
|
+
httpServer.listen(currentPort, host);
|
|
1781
|
+
} else reject(err);
|
|
1782
|
+
});
|
|
1783
|
+
httpServer.listen(currentPort, host);
|
|
1784
|
+
});
|
|
1785
|
+
},
|
|
1786
|
+
async transformRequest(url) {
|
|
1787
|
+
return clientEnv.transformRequest(url);
|
|
1788
|
+
},
|
|
1789
|
+
async transformEnvironmentRequest(environmentName, url) {
|
|
1790
|
+
const environment = environments[environmentName];
|
|
1791
|
+
if (!environment) throw new Error(`[nasti] unknown dev environment "${environmentName}"`);
|
|
1792
|
+
return environment.transformRequest(url);
|
|
1793
|
+
},
|
|
1794
|
+
async ssrLoadModule(url) {
|
|
1795
|
+
return (await getSsrRunner()).import(url);
|
|
1796
|
+
},
|
|
1797
|
+
async close() {
|
|
1798
|
+
if (devPipelinesStarted) for (const environment of Object.values(environments).reverse()) {
|
|
1799
|
+
if (!transformContexts.has(environment.name)) continue;
|
|
1800
|
+
await environment.pluginContainer.buildEnd();
|
|
1801
|
+
}
|
|
1802
|
+
await bundledServer?.close();
|
|
1803
|
+
let environmentCloseFailed = false;
|
|
1804
|
+
let firstEnvironmentCloseError;
|
|
1805
|
+
for (const environment of Object.values(environments).reverse()) try {
|
|
1806
|
+
await environment.close();
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
if (!environmentCloseFailed) {
|
|
1809
|
+
environmentCloseFailed = true;
|
|
1810
|
+
firstEnvironmentCloseError = error;
|
|
1811
|
+
}
|
|
1812
|
+
logCloseError(`environment "${environment.name}"`, error);
|
|
1813
|
+
}
|
|
1814
|
+
await watcher.close();
|
|
1815
|
+
ws.close();
|
|
1816
|
+
httpServer.close();
|
|
1817
|
+
if (environmentCloseFailed) throw firstEnvironmentCloseError;
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
try {
|
|
1821
|
+
await startEnvironmentDrivers();
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
if (bundledServer) try {
|
|
1824
|
+
await bundledServer.close();
|
|
1825
|
+
} catch (closeError) {
|
|
1826
|
+
logCloseError("bundled dev server after driver startup failure", closeError);
|
|
1827
|
+
}
|
|
1828
|
+
try {
|
|
1829
|
+
await watcher.close();
|
|
1830
|
+
} catch (closeError) {
|
|
1831
|
+
logCloseError("file watcher after driver startup failure", closeError);
|
|
1832
|
+
}
|
|
1833
|
+
try {
|
|
1834
|
+
ws.close();
|
|
1835
|
+
} catch (closeError) {
|
|
1836
|
+
logCloseError("WebSocket server after driver startup failure", closeError);
|
|
1837
|
+
}
|
|
1838
|
+
try {
|
|
1839
|
+
httpServer.close();
|
|
1840
|
+
} catch (closeError) {
|
|
1841
|
+
logCloseError("HTTP server after driver startup failure", closeError);
|
|
1842
|
+
}
|
|
1843
|
+
throw error;
|
|
1844
|
+
}
|
|
1845
|
+
app.use(transformMiddleware(transformContexts.get("client")));
|
|
1846
|
+
const publicDir = path.resolve(config.root, "public");
|
|
1847
|
+
app.use(sirv(publicDir, {
|
|
1848
|
+
dev: true,
|
|
1849
|
+
etag: true
|
|
1850
|
+
}));
|
|
1851
|
+
app.use(sirv(config.root, {
|
|
1852
|
+
dev: true,
|
|
1853
|
+
etag: true
|
|
1854
|
+
}));
|
|
1855
|
+
const postMiddlewares = [];
|
|
1856
|
+
for (const plugin of allPlugins) if (plugin.configureServer) {
|
|
1857
|
+
const result = await plugin.configureServer(server);
|
|
1858
|
+
if (typeof result === "function") postMiddlewares.push(result);
|
|
1859
|
+
}
|
|
1860
|
+
for (const run of postMiddlewares) run();
|
|
1861
|
+
return server;
|
|
1862
|
+
}
|
|
1863
|
+
function getNetworkAddress() {
|
|
1864
|
+
const interfaces = os.networkInterfaces();
|
|
1865
|
+
for (const name of Object.keys(interfaces)) for (const iface of interfaces[name] ?? []) if (iface.family === "IPv4" && !iface.internal) return iface.address;
|
|
1866
|
+
return "localhost";
|
|
1867
|
+
}
|
|
1868
|
+
//#endregion
|
|
1869
|
+
export { server_exports as n, getReactRefreshRuntimeEsm as r, createServer as t };
|
|
1870
|
+
|
|
1871
|
+
//# sourceMappingURL=server-Bqgl25z0.js.map
|