@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,1443 @@
|
|
|
1
|
+
const require_logger = require("./logger-M7AuVVTV.cjs");
|
|
2
|
+
const require_env = require("./env-lOj3yw8v.cjs");
|
|
3
|
+
let node_url = require("node:url");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
node_path = require_logger.__toESM(node_path, 1);
|
|
6
|
+
let node_fs = require("node:fs");
|
|
7
|
+
node_fs = require_logger.__toESM(node_fs, 1);
|
|
8
|
+
let picocolors = require("picocolors");
|
|
9
|
+
picocolors = require_logger.__toESM(picocolors, 1);
|
|
10
|
+
let node_module = require("node:module");
|
|
11
|
+
let source_map_js = require("source-map-js");
|
|
12
|
+
let node_crypto = require("node:crypto");
|
|
13
|
+
node_crypto = require_logger.__toESM(node_crypto, 1);
|
|
14
|
+
//#region src/core/debug.ts
|
|
15
|
+
const DEBUG = process.env.DEBUG;
|
|
16
|
+
const filter = process.env.NASTI_DEBUG_FILTER || process.env.VITE_DEBUG_FILTER;
|
|
17
|
+
function createDebugger(namespace, options = {}) {
|
|
18
|
+
if (!DEBUG) return void 0;
|
|
19
|
+
const patterns = DEBUG.split(",").map((p) => p.trim());
|
|
20
|
+
if (!patterns.some((p) => {
|
|
21
|
+
if (p === "*" || p === "nasti:*" || p === "vite:*") return true;
|
|
22
|
+
return (p.startsWith("vite:") ? `nasti:${p.slice(5)}` : p) === namespace;
|
|
23
|
+
})) return void 0;
|
|
24
|
+
if (options.onlyWhenFocused) {
|
|
25
|
+
const focus = typeof options.onlyWhenFocused === "string" ? options.onlyWhenFocused : namespace;
|
|
26
|
+
if (!patterns.includes(focus)) return void 0;
|
|
27
|
+
}
|
|
28
|
+
let lastTime = performance.now();
|
|
29
|
+
return (...args) => {
|
|
30
|
+
const now = performance.now();
|
|
31
|
+
const elapsed = now - lastTime;
|
|
32
|
+
lastTime = now;
|
|
33
|
+
const msg = args.map((a) => {
|
|
34
|
+
if (typeof a === "string") return a;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.stringify(a);
|
|
37
|
+
} catch {
|
|
38
|
+
return String(a);
|
|
39
|
+
}
|
|
40
|
+
}).join(" ");
|
|
41
|
+
if (filter && !msg.includes(filter)) return;
|
|
42
|
+
console.debug(`${picocolors.default.magenta(namespace)} ${msg} ${picocolors.default.dim(`+${Math.round(elapsed)}ms`)}`);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/core/css-engine.ts
|
|
47
|
+
const debug$2 = createDebugger("nasti:css");
|
|
48
|
+
function createCssEngine() {
|
|
49
|
+
return {
|
|
50
|
+
styles: /* @__PURE__ */ new Map(),
|
|
51
|
+
modules: /* @__PURE__ */ new Map(),
|
|
52
|
+
chunks: /* @__PURE__ */ new Map(),
|
|
53
|
+
entryCss: /* @__PURE__ */ new Map(),
|
|
54
|
+
allCss: [],
|
|
55
|
+
pendingSingle: [],
|
|
56
|
+
singleFileName: null
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function getCssMetadata(engine) {
|
|
60
|
+
return {
|
|
61
|
+
modules: Object.fromEntries([...engine.modules].map(([id, module]) => [id, { ...module }])),
|
|
62
|
+
chunks: Object.fromEntries([...engine.chunks].map(([fileName, chunk]) => [fileName, {
|
|
63
|
+
fileName,
|
|
64
|
+
moduleIds: [...chunk.moduleIds],
|
|
65
|
+
cssFileNames: [...chunk.cssFileNames]
|
|
66
|
+
}]))
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 规范化模块 id:去掉 Rolldown 虚拟模块的 null-byte 前缀。
|
|
71
|
+
* **保留 query** —— Vue style 子块(`App.vue?vue&type=style&...&lang.css`)与
|
|
72
|
+
* 父模块 `App.vue` 是不同模块,剥 query 会让二者 key 撞车导致 CSS 重复输出。
|
|
73
|
+
* chunk.moduleIds 里的 id 必须与 transform 期注册的 key 一致 —— 两侧都过这个函数。
|
|
74
|
+
*/
|
|
75
|
+
function normalizeCssModuleId(id) {
|
|
76
|
+
return id.startsWith("\0") ? id.slice(1) : id;
|
|
77
|
+
}
|
|
78
|
+
let lightningCss;
|
|
79
|
+
/**
|
|
80
|
+
* 压缩 CSS:优先 Lightning CSS(守卫导入),不可用时回退正则压缩。
|
|
81
|
+
* Tailwind v4 的输出已自行 flatten @import,这里不做 @import 解析。
|
|
82
|
+
*/
|
|
83
|
+
async function minifyCss(css, config) {
|
|
84
|
+
if (lightningCss === void 0) try {
|
|
85
|
+
lightningCss = await import("lightningcss");
|
|
86
|
+
} catch {
|
|
87
|
+
lightningCss = null;
|
|
88
|
+
debug$2?.("lightningcss unavailable, falling back to regex minifier");
|
|
89
|
+
}
|
|
90
|
+
if (lightningCss) try {
|
|
91
|
+
const result = lightningCss.transform({
|
|
92
|
+
filename: "bundle.css",
|
|
93
|
+
code: Buffer.from(css),
|
|
94
|
+
minify: true,
|
|
95
|
+
errorRecovery: true
|
|
96
|
+
});
|
|
97
|
+
for (const w of result.warnings ?? []) config.logger.warnOnce(`[nasti:css] ${w.message}`);
|
|
98
|
+
return result.code.toString();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
config.logger.warnOnce(`[nasti:css] Lightning CSS minify failed (${err.message}), emitting unminified CSS`);
|
|
101
|
+
return css;
|
|
102
|
+
}
|
|
103
|
+
return fallbackMinify(css);
|
|
104
|
+
}
|
|
105
|
+
/** 保守的正则压缩:去注释、压空白。不做任何结构性变换。 */
|
|
106
|
+
function fallbackMinify(css) {
|
|
107
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s*([{}:;,])\s*/g, "$1").replace(/;}/g, "}").replace(/\s+/g, " ").trim();
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/plugins/tailwind.ts
|
|
111
|
+
const TAILWIND_DIRECTIVE_RE = /@(?:import\s+["']tailwindcss(?:\b|\/)|tailwind\b|theme\b|apply\b|plugin\b|source\b|utility\b|variant\b|custom-variant\b|reference\b)/;
|
|
112
|
+
function hasTailwindDirectives(css) {
|
|
113
|
+
const withoutLineComments = css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
114
|
+
return TAILWIND_DIRECTIVE_RE.test(withoutLineComments);
|
|
115
|
+
}
|
|
116
|
+
let cached = null;
|
|
117
|
+
let cachedRoot = null;
|
|
118
|
+
async function loadTailwind(projectRoot) {
|
|
119
|
+
if (cached && cachedRoot === projectRoot) return cached;
|
|
120
|
+
const req = (0, node_module.createRequire)(node_path.default.join(projectRoot, "package.json"));
|
|
121
|
+
let nodePath;
|
|
122
|
+
let oxidePath;
|
|
123
|
+
try {
|
|
124
|
+
nodePath = req.resolve("@tailwindcss/node");
|
|
125
|
+
oxidePath = req.resolve("@tailwindcss/oxide");
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error("[nasti] CSS contains Tailwind v4 directives but `@tailwindcss/node` and/or `@tailwindcss/oxide` are not installed in this project. Install them with: npm i -D tailwindcss @tailwindcss/node @tailwindcss/oxide");
|
|
128
|
+
}
|
|
129
|
+
cached = {
|
|
130
|
+
node: await import((0, node_url.pathToFileURL)(nodePath).href),
|
|
131
|
+
oxide: await import((0, node_url.pathToFileURL)(oxidePath).href)
|
|
132
|
+
};
|
|
133
|
+
cachedRoot = projectRoot;
|
|
134
|
+
return cached;
|
|
135
|
+
}
|
|
136
|
+
async function compileTailwind(css, fromFile, projectRoot) {
|
|
137
|
+
const { node, oxide } = await loadTailwind(projectRoot);
|
|
138
|
+
const dependencies = [];
|
|
139
|
+
const compiler = await node.compile(css, {
|
|
140
|
+
base: node_path.default.dirname(fromFile),
|
|
141
|
+
from: fromFile,
|
|
142
|
+
onDependency: (p) => dependencies.push(p)
|
|
143
|
+
});
|
|
144
|
+
const scanner = new oxide.Scanner({ sources: compiler.sources });
|
|
145
|
+
const candidates = scanner.scan();
|
|
146
|
+
return {
|
|
147
|
+
css: compiler.build(candidates),
|
|
148
|
+
dependencies: [...dependencies, ...scanner.files]
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/plugins/css.ts
|
|
153
|
+
function cssPlugin(config, engine, consumer = "client") {
|
|
154
|
+
return {
|
|
155
|
+
name: "nasti:css",
|
|
156
|
+
resolveId(source) {
|
|
157
|
+
if (source.endsWith(".css")) return null;
|
|
158
|
+
return null;
|
|
159
|
+
},
|
|
160
|
+
async transform(code, id) {
|
|
161
|
+
const [file, query = ""] = id.split("?", 2);
|
|
162
|
+
if (!(file.endsWith(".css") || /\.css$/.test(id))) return null;
|
|
163
|
+
if (query === "raw" || query === "url") return null;
|
|
164
|
+
let cssSource = code;
|
|
165
|
+
if (hasTailwindDirectives(code)) cssSource = (await compileTailwind(code, id, config.root)).css;
|
|
166
|
+
const rewritten = rewriteCssUrls(cssSource, file, config.root);
|
|
167
|
+
const escaped = JSON.stringify(rewritten);
|
|
168
|
+
const normalizedId = normalizeCssModuleId(id);
|
|
169
|
+
const cssModule = {
|
|
170
|
+
id: normalizedId,
|
|
171
|
+
source: code,
|
|
172
|
+
code: rewritten
|
|
173
|
+
};
|
|
174
|
+
const map = config.build.sourcemap ? createIdentitySourceMap(code, id) : void 0;
|
|
175
|
+
engine?.modules.set(normalizedId, cssModule);
|
|
176
|
+
this.environment?.setCssModule?.(cssModule);
|
|
177
|
+
if (query === "inline") return {
|
|
178
|
+
code: `export default ${escaped};\n`,
|
|
179
|
+
map,
|
|
180
|
+
moduleType: "js"
|
|
181
|
+
};
|
|
182
|
+
if (consumer === "server") return {
|
|
183
|
+
code: `export default ${escaped};\n`,
|
|
184
|
+
map,
|
|
185
|
+
moduleType: "js"
|
|
186
|
+
};
|
|
187
|
+
if (config.command === "serve") {
|
|
188
|
+
if (config.build.css.inject === false) return {
|
|
189
|
+
code: `export default ${escaped};\n`,
|
|
190
|
+
map,
|
|
191
|
+
moduleType: "js",
|
|
192
|
+
moduleSideEffects: "no-treeshake"
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
code: `
|
|
196
|
+
const css = ${escaped};
|
|
197
|
+
const __nasti_css_id__ = ${JSON.stringify(id)};
|
|
198
|
+
const __nasti_existing__ = document.querySelector('style[data-nasti-css=' + JSON.stringify(__nasti_css_id__) + ']');
|
|
199
|
+
if (__nasti_existing__) __nasti_existing__.remove();
|
|
200
|
+
const style = document.createElement('style');
|
|
201
|
+
style.setAttribute('data-nasti-css', __nasti_css_id__);
|
|
202
|
+
style.textContent = css;
|
|
203
|
+
document.head.appendChild(style);
|
|
204
|
+
|
|
205
|
+
// HMR(prune 在 bundled 模式的 rolldown hot context 上不存在,须守卫)
|
|
206
|
+
if (import.meta.hot) {
|
|
207
|
+
import.meta.hot.accept();
|
|
208
|
+
if (import.meta.hot.prune) {
|
|
209
|
+
import.meta.hot.prune(() => {
|
|
210
|
+
style.remove();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export default css;
|
|
216
|
+
`,
|
|
217
|
+
map,
|
|
218
|
+
moduleType: "js"
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (engine) {
|
|
222
|
+
engine.styles.set(normalizedId, rewritten);
|
|
223
|
+
return {
|
|
224
|
+
code: `export default '';\n`,
|
|
225
|
+
map,
|
|
226
|
+
moduleType: "js",
|
|
227
|
+
moduleSideEffects: "no-treeshake"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const nonce = (config.build.css || {}).nonce;
|
|
231
|
+
const nonceAttr = nonce ? `style.setAttribute('nonce', ${JSON.stringify(nonce)});` : "";
|
|
232
|
+
return {
|
|
233
|
+
code: `
|
|
234
|
+
const css = ${escaped};
|
|
235
|
+
const style = document.createElement('style');
|
|
236
|
+
style.setAttribute('data-nasti-css', ${JSON.stringify(id)});
|
|
237
|
+
${nonceAttr}
|
|
238
|
+
style.textContent = css;
|
|
239
|
+
document.head.appendChild(style);
|
|
240
|
+
|
|
241
|
+
export default css;
|
|
242
|
+
`,
|
|
243
|
+
map,
|
|
244
|
+
moduleType: "js"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function createIdentitySourceMap(code, id) {
|
|
250
|
+
const map = new source_map_js.SourceMapGenerator({ file: id });
|
|
251
|
+
const lines = code.split("\n");
|
|
252
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) for (let column = 0; column <= lines[lineIndex].length; column++) map.addMapping({
|
|
253
|
+
generated: {
|
|
254
|
+
line: lineIndex + 1,
|
|
255
|
+
column
|
|
256
|
+
},
|
|
257
|
+
original: {
|
|
258
|
+
line: lineIndex + 1,
|
|
259
|
+
column
|
|
260
|
+
},
|
|
261
|
+
source: id
|
|
262
|
+
});
|
|
263
|
+
map.setSourceContent(id, code);
|
|
264
|
+
return map.toJSON();
|
|
265
|
+
}
|
|
266
|
+
/** CSS URL 重写(将相对路径转为绝对路径) */
|
|
267
|
+
function rewriteCssUrls(css, from, root) {
|
|
268
|
+
return css.replace(/url\(\s*['"]?([^'")\s]+)['"]?\s*\)/g, (match, url) => {
|
|
269
|
+
if (url.startsWith("/") || url.startsWith("data:") || url.startsWith("http")) return match;
|
|
270
|
+
const resolved = node_path.default.resolve(node_path.default.dirname(from), url);
|
|
271
|
+
return `url(${"/" + node_path.default.relative(root, resolved).replace(/\\/g, "/")})`;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/plugins/css-post.ts
|
|
276
|
+
/** 按 chunk 模块执行顺序聚合该 chunk 的 CSS(id 两侧都已规范化) */
|
|
277
|
+
function collectChunkCss(chunk, engine) {
|
|
278
|
+
const ids = chunk.moduleIds ?? Object.keys(chunk.modules);
|
|
279
|
+
let css = "";
|
|
280
|
+
const moduleIds = [];
|
|
281
|
+
for (const id of ids) {
|
|
282
|
+
const normalizedId = normalizeCssModuleId(id);
|
|
283
|
+
const styles = engine.styles.get(normalizedId);
|
|
284
|
+
if (styles) {
|
|
285
|
+
css += styles + "\n";
|
|
286
|
+
moduleIds.push(normalizedId);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
css,
|
|
291
|
+
moduleIds
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function cssPostPlugin(config, engine) {
|
|
295
|
+
return {
|
|
296
|
+
name: "nasti:css-post",
|
|
297
|
+
enforce: "post",
|
|
298
|
+
async renderChunk(code, chunk) {
|
|
299
|
+
const { css, moduleIds } = collectChunkCss(chunk, engine);
|
|
300
|
+
if (!css) return null;
|
|
301
|
+
const ownership = {
|
|
302
|
+
moduleIds,
|
|
303
|
+
cssFileNames: []
|
|
304
|
+
};
|
|
305
|
+
engine.chunks.set(chunk.fileName, ownership);
|
|
306
|
+
if (config.build.css.emit === false) return null;
|
|
307
|
+
if (!config.build.cssCodeSplit) {
|
|
308
|
+
engine.pendingSingle.push(css);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const finalCss = config.build.cssMinify ? await minifyCss(css, config) : css;
|
|
312
|
+
const ref = this.emitFile({
|
|
313
|
+
type: "asset",
|
|
314
|
+
name: `${chunk.name}.css`,
|
|
315
|
+
source: finalCss
|
|
316
|
+
});
|
|
317
|
+
const fileName = this.getFileName(ref);
|
|
318
|
+
engine.allCss.push(fileName);
|
|
319
|
+
ownership.cssFileNames.push(fileName);
|
|
320
|
+
if (chunk.isEntry) {
|
|
321
|
+
const key = chunk.facadeModuleId ?? chunk.name;
|
|
322
|
+
const existing = engine.entryCss.get(key) ?? [];
|
|
323
|
+
existing.push(fileName);
|
|
324
|
+
engine.entryCss.set(key, existing);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
if (config.build.css.inject === false) return null;
|
|
328
|
+
return {
|
|
329
|
+
code: code + `\n;(function(){try{var d=document,h=${JSON.stringify(config.base + fileName)};if(!d.querySelector('link[data-nasti-css="'+h+'"]')){var l=d.createElement('link');l.rel='stylesheet';l.href=h;l.setAttribute('data-nasti-css',h);d.head.appendChild(l);}}catch(e){}})();`,
|
|
330
|
+
map: null
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
augmentChunkHash(chunk) {
|
|
334
|
+
const { css } = collectChunkCss(chunk, engine);
|
|
335
|
+
return css || void 0;
|
|
336
|
+
},
|
|
337
|
+
async generateBundle() {
|
|
338
|
+
if (config.build.cssCodeSplit || engine.pendingSingle.length === 0) return;
|
|
339
|
+
const merged = engine.pendingSingle.join("\n");
|
|
340
|
+
const finalCss = config.build.cssMinify ? await minifyCss(merged, config) : merged;
|
|
341
|
+
const ref = this.emitFile({
|
|
342
|
+
type: "asset",
|
|
343
|
+
name: "style.css",
|
|
344
|
+
source: finalCss
|
|
345
|
+
});
|
|
346
|
+
const fileName = this.getFileName(ref);
|
|
347
|
+
engine.singleFileName = fileName;
|
|
348
|
+
engine.allCss.push(fileName);
|
|
349
|
+
for (const ownership of engine.chunks.values()) if (ownership.moduleIds.length > 0) ownership.cssFileNames.push(fileName);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/plugins/assets.ts
|
|
355
|
+
const ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
356
|
+
".png",
|
|
357
|
+
".jpg",
|
|
358
|
+
".jpeg",
|
|
359
|
+
".gif",
|
|
360
|
+
".svg",
|
|
361
|
+
".ico",
|
|
362
|
+
".webp",
|
|
363
|
+
".avif",
|
|
364
|
+
".mp4",
|
|
365
|
+
".webm",
|
|
366
|
+
".ogg",
|
|
367
|
+
".mp3",
|
|
368
|
+
".wav",
|
|
369
|
+
".flac",
|
|
370
|
+
".aac",
|
|
371
|
+
".woff",
|
|
372
|
+
".woff2",
|
|
373
|
+
".eot",
|
|
374
|
+
".ttf",
|
|
375
|
+
".otf",
|
|
376
|
+
".pdf",
|
|
377
|
+
".txt"
|
|
378
|
+
]);
|
|
379
|
+
function assetsPlugin(config) {
|
|
380
|
+
const emittedAssets = /* @__PURE__ */ new Set();
|
|
381
|
+
return {
|
|
382
|
+
name: "nasti:assets",
|
|
383
|
+
resolveId(source) {
|
|
384
|
+
if (source.endsWith("?url") || source.endsWith("?raw")) return source;
|
|
385
|
+
return null;
|
|
386
|
+
},
|
|
387
|
+
load(id) {
|
|
388
|
+
const ext = node_path.default.extname(id.replace(/\?.*$/, ""));
|
|
389
|
+
if (id.endsWith("?raw")) {
|
|
390
|
+
const file = id.slice(0, -4);
|
|
391
|
+
if (node_fs.default.existsSync(file)) {
|
|
392
|
+
const content = node_fs.default.readFileSync(file, "utf-8");
|
|
393
|
+
return `export default ${JSON.stringify(content)}`;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (id.endsWith("?url") || ASSET_EXTENSIONS.has(ext)) {
|
|
397
|
+
const file = id.replace(/\?.*$/, "");
|
|
398
|
+
if (!node_fs.default.existsSync(file)) return null;
|
|
399
|
+
if (config.command === "serve") {
|
|
400
|
+
const url = "/" + node_path.default.relative(config.root, file);
|
|
401
|
+
return `export default ${JSON.stringify(url)}`;
|
|
402
|
+
}
|
|
403
|
+
const content = node_fs.default.readFileSync(file);
|
|
404
|
+
const hash = node_crypto.default.createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
405
|
+
const basename = node_path.default.basename(file, ext);
|
|
406
|
+
const hashedName = `${config.build.assetsDir}/${basename}.${hash}${ext}`;
|
|
407
|
+
const environment = this.environment;
|
|
408
|
+
if (!environment) throw new Error("[nasti:assets] build environment is not initialized");
|
|
409
|
+
if (!emittedAssets.has(hashedName)) {
|
|
410
|
+
this.emitFile({
|
|
411
|
+
type: "asset",
|
|
412
|
+
fileName: hashedName,
|
|
413
|
+
source: content
|
|
414
|
+
});
|
|
415
|
+
emittedAssets.add(hashedName);
|
|
416
|
+
}
|
|
417
|
+
environment.setAssetModule(file, hashedName);
|
|
418
|
+
return `export default ${JSON.stringify(config.base + hashedName)}`;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function isAssetFile(id) {
|
|
425
|
+
const ext = node_path.default.extname(id.replace(/\?.*$/, ""));
|
|
426
|
+
return ASSET_EXTENSIONS.has(ext);
|
|
427
|
+
}
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/plugins/vue.ts
|
|
430
|
+
const VUE_FILE_RE = /\.vue$/;
|
|
431
|
+
const VUE_QUERY_RE = /\.vue\?vue&type=(script|template|style)(&index=\d+)?(&lang[.=]\w+)?/;
|
|
432
|
+
const debug$1 = createDebugger("nasti:vue");
|
|
433
|
+
/** Vapor Mode 测试版免责声明:终端与浏览器控制台共用同一文案 */
|
|
434
|
+
const VAPOR_BETA_WARNING = "Vapor Mode is a beta feature; Zixiao Labs and the Vue team do not provide guarantees against crashes in production environments, and it is not suitable for server-side rendering environments.";
|
|
435
|
+
let compiler = null;
|
|
436
|
+
async function loadVueCompiler() {
|
|
437
|
+
if (compiler) return compiler;
|
|
438
|
+
try {
|
|
439
|
+
compiler = await import("@vue/compiler-sfc");
|
|
440
|
+
return compiler;
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function vuePlugin(config, environmentName = "client") {
|
|
446
|
+
const isDev = config.command === "serve";
|
|
447
|
+
const descriptorCache = /* @__PURE__ */ new Map();
|
|
448
|
+
const vueOptions = config.environments[environmentName]?.vue ?? {};
|
|
449
|
+
return {
|
|
450
|
+
name: "nasti:vue",
|
|
451
|
+
enforce: "pre",
|
|
452
|
+
async resolveId(source) {
|
|
453
|
+
if (VUE_QUERY_RE.test(source)) return source;
|
|
454
|
+
return null;
|
|
455
|
+
},
|
|
456
|
+
async load(id) {
|
|
457
|
+
const match = id.match(/(.+\.vue)\?vue&type=style(?:&index=(\d+))?/);
|
|
458
|
+
if (!match) return null;
|
|
459
|
+
const sfc = await loadVueCompiler();
|
|
460
|
+
if (!sfc) return null;
|
|
461
|
+
const [, filePath, indexStr] = match;
|
|
462
|
+
let cached = descriptorCache.get(filePath);
|
|
463
|
+
if (!cached) try {
|
|
464
|
+
const rawSource = (await import("node:fs")).readFileSync(filePath, "utf-8");
|
|
465
|
+
const transformedSfc = await applySourceTransform(vueOptions.transformSfc, rawSource, {
|
|
466
|
+
filename: filePath,
|
|
467
|
+
environmentName,
|
|
468
|
+
type: "sfc"
|
|
469
|
+
});
|
|
470
|
+
const parsed = sfc.parse(transformedSfc.code, {
|
|
471
|
+
...vueOptions.parse,
|
|
472
|
+
filename: filePath,
|
|
473
|
+
sourceMap: true
|
|
474
|
+
});
|
|
475
|
+
if (parsed.errors.length) return null;
|
|
476
|
+
cached = {
|
|
477
|
+
descriptor: parsed.descriptor,
|
|
478
|
+
sourceMap: transformedSfc.map
|
|
479
|
+
};
|
|
480
|
+
descriptorCache.set(filePath, cached);
|
|
481
|
+
} catch {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
const { descriptor, sourceMap: sfcSourceMap } = cached;
|
|
485
|
+
const index = parseInt(indexStr ?? "0", 10);
|
|
486
|
+
const style = descriptor.styles[index];
|
|
487
|
+
if (!style) return null;
|
|
488
|
+
const scopeId = hashId(filePath);
|
|
489
|
+
const transformedStyle = await applySourceTransform(vueOptions.transformStyle, style.content, {
|
|
490
|
+
filename: filePath,
|
|
491
|
+
environmentName,
|
|
492
|
+
type: "style",
|
|
493
|
+
index
|
|
494
|
+
});
|
|
495
|
+
const wantsStyleSourceMap = !!config.build.sourcemap || transformedStyle.map != null || sfcSourceMap != null;
|
|
496
|
+
const styleInputMap = wantsStyleSourceMap ? composeSourceMapChain([
|
|
497
|
+
transformedStyle.map,
|
|
498
|
+
style.map,
|
|
499
|
+
sfcSourceMap
|
|
500
|
+
], {
|
|
501
|
+
filename: filePath,
|
|
502
|
+
environmentName,
|
|
503
|
+
type: "style",
|
|
504
|
+
index
|
|
505
|
+
}) : void 0;
|
|
506
|
+
const result = await sfc.compileStyleAsync({
|
|
507
|
+
...vueOptions.style,
|
|
508
|
+
source: transformedStyle.code,
|
|
509
|
+
filename: filePath,
|
|
510
|
+
id: `data-v-${scopeId}`,
|
|
511
|
+
scoped: style.scoped ?? false,
|
|
512
|
+
inMap: styleInputMap,
|
|
513
|
+
preprocessLang: style.lang
|
|
514
|
+
});
|
|
515
|
+
if (transformedStyle.map != null && result.map == null) warnUnchainableMap({
|
|
516
|
+
filename: filePath,
|
|
517
|
+
environmentName,
|
|
518
|
+
type: "style",
|
|
519
|
+
index
|
|
520
|
+
}, "compiler-sfc did not return a style map");
|
|
521
|
+
return wantsStyleSourceMap ? {
|
|
522
|
+
code: result.code,
|
|
523
|
+
map: result.map
|
|
524
|
+
} : result.code;
|
|
525
|
+
},
|
|
526
|
+
async transform(code, id) {
|
|
527
|
+
if (!VUE_FILE_RE.test(id) && !VUE_QUERY_RE.test(id)) return null;
|
|
528
|
+
const sfc = await loadVueCompiler();
|
|
529
|
+
if (!sfc) {
|
|
530
|
+
console.warn("[nasti:vue] @vue/compiler-sfc not found. Install it: npm install @vue/compiler-sfc");
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
if (VUE_QUERY_RE.test(id)) return null;
|
|
534
|
+
const transformedSfc = await applySourceTransform(vueOptions.transformSfc, code, {
|
|
535
|
+
filename: id,
|
|
536
|
+
environmentName,
|
|
537
|
+
type: "sfc"
|
|
538
|
+
});
|
|
539
|
+
code = transformedSfc.code;
|
|
540
|
+
const { descriptor, errors } = sfc.parse(code, {
|
|
541
|
+
...vueOptions.parse,
|
|
542
|
+
filename: id,
|
|
543
|
+
sourceMap: true
|
|
544
|
+
});
|
|
545
|
+
if (errors.length) {
|
|
546
|
+
const firstError = errors[0];
|
|
547
|
+
console.error(`[nasti:vue] Parse error in ${id}:`, typeof firstError === "string" ? firstError : firstError.message);
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
descriptorCache.set(id, {
|
|
551
|
+
descriptor,
|
|
552
|
+
sourceMap: transformedSfc.map
|
|
553
|
+
});
|
|
554
|
+
const scopeId = hashId(id);
|
|
555
|
+
const wantsSourceMap = !!config.build.sourcemap || transformedSfc.map != null;
|
|
556
|
+
const vapor = resolveVaporMode(descriptor, vueOptions, sfc, config);
|
|
557
|
+
let scriptCode = "";
|
|
558
|
+
let scriptMap;
|
|
559
|
+
let scriptBindings;
|
|
560
|
+
if (descriptor.script || descriptor.scriptSetup) {
|
|
561
|
+
const inlineTemplate = vueOptions.script?.inlineTemplate !== false;
|
|
562
|
+
const compiled = sfc.compileScript(descriptor, {
|
|
563
|
+
...vueOptions.script,
|
|
564
|
+
id: scopeId,
|
|
565
|
+
isProd: !isDev,
|
|
566
|
+
inlineTemplate,
|
|
567
|
+
sourceMap: wantsSourceMap,
|
|
568
|
+
genDefaultAs: "__sfc__",
|
|
569
|
+
vapor
|
|
570
|
+
});
|
|
571
|
+
scriptCode = compiled.content;
|
|
572
|
+
scriptBindings = compiled.bindings;
|
|
573
|
+
scriptMap = composeSourceMapChain([compiled.map, transformedSfc.map], {
|
|
574
|
+
filename: id,
|
|
575
|
+
environmentName,
|
|
576
|
+
type: "sfc"
|
|
577
|
+
});
|
|
578
|
+
if (transformedSfc.map != null && scriptMap == null) warnUnchainableMap({
|
|
579
|
+
filename: id,
|
|
580
|
+
environmentName,
|
|
581
|
+
type: "sfc"
|
|
582
|
+
}, "compiler-sfc did not return a script map");
|
|
583
|
+
}
|
|
584
|
+
let templateCode = "";
|
|
585
|
+
let templateMap;
|
|
586
|
+
let templateMultiRoot;
|
|
587
|
+
const scriptSetupIsInline = !!descriptor.scriptSetup && vueOptions.script?.inlineTemplate !== false;
|
|
588
|
+
const isTemplateOnlyVapor = vapor && !descriptor.script && !descriptor.scriptSetup;
|
|
589
|
+
if (descriptor.template && !scriptSetupIsInline) {
|
|
590
|
+
const transformedTemplate = await applySourceTransform(vueOptions.transformTemplate, descriptor.template.content, {
|
|
591
|
+
filename: id,
|
|
592
|
+
environmentName,
|
|
593
|
+
type: "template"
|
|
594
|
+
});
|
|
595
|
+
const templateInputMap = composeSourceMapChain([
|
|
596
|
+
transformedTemplate.map,
|
|
597
|
+
descriptor.template.map,
|
|
598
|
+
transformedSfc.map
|
|
599
|
+
], {
|
|
600
|
+
filename: id,
|
|
601
|
+
environmentName,
|
|
602
|
+
type: "template"
|
|
603
|
+
});
|
|
604
|
+
const customCompilerOptions = vueOptions.template?.compilerOptions ?? {};
|
|
605
|
+
const compiled = sfc.compileTemplate({
|
|
606
|
+
...vueOptions.template,
|
|
607
|
+
source: transformedTemplate.code,
|
|
608
|
+
filename: id,
|
|
609
|
+
id: scopeId,
|
|
610
|
+
inMap: templateInputMap,
|
|
611
|
+
vapor,
|
|
612
|
+
compilerOptions: {
|
|
613
|
+
...customCompilerOptions,
|
|
614
|
+
bindingMetadata: customCompilerOptions.bindingMetadata ?? scriptBindings ?? (vapor ? {} : void 0),
|
|
615
|
+
scopeId: `data-v-${scopeId}`
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
templateCode = compiled.code;
|
|
619
|
+
templateMultiRoot = compiled.multiRoot;
|
|
620
|
+
if (wantsSourceMap || transformedTemplate.map != null) templateMap = compiled.map;
|
|
621
|
+
if (transformedTemplate.map != null && templateMap == null) warnUnchainableMap({
|
|
622
|
+
filename: id,
|
|
623
|
+
environmentName,
|
|
624
|
+
type: "template"
|
|
625
|
+
}, "compiler-sfc did not return a template map");
|
|
626
|
+
}
|
|
627
|
+
const outputNode = new source_map_js.SourceNode();
|
|
628
|
+
let hasMappedOutput = false;
|
|
629
|
+
const append = (fragment, map) => {
|
|
630
|
+
const normalizedMap = normalizeSourceMap(map, {
|
|
631
|
+
filename: id,
|
|
632
|
+
environmentName,
|
|
633
|
+
type: "sfc"
|
|
634
|
+
});
|
|
635
|
+
if (!normalizedMap) {
|
|
636
|
+
outputNode.add(fragment);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
outputNode.add(source_map_js.SourceNode.fromStringWithSourceMap(fragment, new source_map_js.SourceMapConsumer(normalizedMap)));
|
|
641
|
+
hasMappedOutput = true;
|
|
642
|
+
} catch (error) {
|
|
643
|
+
warnUnchainableMap({
|
|
644
|
+
filename: id,
|
|
645
|
+
environmentName,
|
|
646
|
+
type: "sfc"
|
|
647
|
+
}, `source-map assembly failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
648
|
+
outputNode.add(fragment);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
append(scriptCode || (vapor ? "const __sfc__ = { __vapor: true }" : "const __sfc__ = {}"), scriptMap);
|
|
652
|
+
if (templateCode) {
|
|
653
|
+
append("\n");
|
|
654
|
+
append(templateCode, templateMap);
|
|
655
|
+
append("\n");
|
|
656
|
+
append("\n__sfc__.render = render\n");
|
|
657
|
+
if (isTemplateOnlyVapor && templateMultiRoot !== void 0) append(`\n__sfc__.__multiRoot = ${JSON.stringify(templateMultiRoot)}\n`);
|
|
658
|
+
}
|
|
659
|
+
if (descriptor.styles.length > 0) for (let i = 0; i < descriptor.styles.length; i++) append(`\nimport "${id}?vue&type=style&index=${i}&lang.css"\n`);
|
|
660
|
+
if (vapor) append(`
|
|
661
|
+
if (!globalThis.__NASTI_VAPOR_BETA_WARNED__) {
|
|
662
|
+
globalThis.__NASTI_VAPOR_BETA_WARNED__ = true
|
|
663
|
+
console.warn(${JSON.stringify(VAPOR_BETA_WARNING)})\n}\n`);
|
|
664
|
+
append(`\n__sfc__.__scopeId = "data-v-${scopeId}"\n`);
|
|
665
|
+
if (isDev) append(`
|
|
666
|
+
__sfc__.__hmrId = ${JSON.stringify(scopeId)}
|
|
667
|
+
if (typeof __VUE_HMR_RUNTIME__ !== 'undefined') {
|
|
668
|
+
__VUE_HMR_RUNTIME__.createRecord(__sfc__.__hmrId, __sfc__)
|
|
669
|
+
}
|
|
670
|
+
if (import.meta.hot) {
|
|
671
|
+
import.meta.hot.accept((mod) => {
|
|
672
|
+
if (!mod) return
|
|
673
|
+
const { default: updated } = mod
|
|
674
|
+
if (typeof __VUE_HMR_RUNTIME__ !== 'undefined') {
|
|
675
|
+
__VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)
|
|
676
|
+
}
|
|
677
|
+
})
|
|
678
|
+
}
|
|
679
|
+
`);
|
|
680
|
+
append("\nexport default __sfc__\n");
|
|
681
|
+
const renderedOutput = outputNode.toStringWithSourceMap({ file: id });
|
|
682
|
+
const output = renderedOutput.code;
|
|
683
|
+
const outputMap = hasMappedOutput ? renderedOutput.map.toJSON() : void 0;
|
|
684
|
+
if (transformedSfc.map != null && outputMap == null) warnUnchainableMap({
|
|
685
|
+
filename: id,
|
|
686
|
+
environmentName,
|
|
687
|
+
type: "sfc"
|
|
688
|
+
}, "the compiled SFC output contained no chainable mappings");
|
|
689
|
+
if ((descriptor.scriptSetup?.lang ?? descriptor.script?.lang) === "ts") {
|
|
690
|
+
const transpiled = require_env.transformCode(`${id}.ts`, output, {
|
|
691
|
+
sourcemap: wantsSourceMap,
|
|
692
|
+
target: config.build.target
|
|
693
|
+
});
|
|
694
|
+
const transpiledMap = transpiled.map ? JSON.parse(transpiled.map) : void 0;
|
|
695
|
+
return {
|
|
696
|
+
code: transpiled.code,
|
|
697
|
+
map: composeSourceMapChain([transpiledMap, outputMap], {
|
|
698
|
+
filename: id,
|
|
699
|
+
environmentName,
|
|
700
|
+
type: "sfc"
|
|
701
|
+
})
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
code: output,
|
|
706
|
+
map: outputMap
|
|
707
|
+
};
|
|
708
|
+
},
|
|
709
|
+
handleHotUpdate(ctx) {
|
|
710
|
+
const { file, modules } = ctx;
|
|
711
|
+
if (VUE_FILE_RE.test(file)) {
|
|
712
|
+
for (const mod of modules) mod.isSelfAccepting = true;
|
|
713
|
+
descriptorCache.delete(file);
|
|
714
|
+
}
|
|
715
|
+
return modules;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* 解析当前 SFC 是否启用 Vapor Mode。
|
|
721
|
+
* - 单文件 opt-in:`descriptor.vapor`(`<script setup vapor>` / `<template vapor>`)
|
|
722
|
+
* - 环境级强制:`vue.features.vapor`(仅对可强制的 SFC 生效)
|
|
723
|
+
* 首次启用时在终端打印测试版免责声明。
|
|
724
|
+
*/
|
|
725
|
+
function resolveVaporMode(descriptor, vueOptions, sfc, config) {
|
|
726
|
+
if (!(!!descriptor.vapor || !!vueOptions.features?.vapor && canForceVaporMode(descriptor))) return false;
|
|
727
|
+
if (!supportsVaporCompiler(sfc)) {
|
|
728
|
+
config.logger.warnOnce("[nasti:vue] Vapor Mode requires @vue/compiler-sfc >= 3.6. Install it: npm install @vue/compiler-sfc@^3.6.0-0");
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
config.logger.warnOnce(VAPOR_BETA_WARNING);
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* `features.vapor` 可强制:纯 template SFC、`<script setup>` SFC。
|
|
736
|
+
* 不可强制仅含普通 `<script>`(无 setup)的 `.vue`,因为 Vapor 不支持 Options API。
|
|
737
|
+
*/
|
|
738
|
+
function canForceVaporMode(descriptor) {
|
|
739
|
+
if (typeof descriptor.filename === "string" && descriptor.filename.endsWith(".vue")) {
|
|
740
|
+
if (descriptor.script && !descriptor.scriptSetup) return false;
|
|
741
|
+
return !!(descriptor.scriptSetup || descriptor.template);
|
|
742
|
+
}
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
/** `@vue/compiler-sfc` 主版本 ≥ 3.6 才具备 Vapor 编译器。 */
|
|
746
|
+
function supportsVaporCompiler(sfc) {
|
|
747
|
+
const version = sfc.version;
|
|
748
|
+
if (!version) return false;
|
|
749
|
+
const [major, minor] = version.split(".").map((part) => Number.parseInt(part, 10));
|
|
750
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor)) return false;
|
|
751
|
+
return major > 3 || major === 3 && minor >= 6;
|
|
752
|
+
}
|
|
753
|
+
async function applySourceTransform(transform, source, context) {
|
|
754
|
+
if (!transform) return { code: source };
|
|
755
|
+
const result = await transform(source, context);
|
|
756
|
+
return typeof result === "string" ? { code: result } : result;
|
|
757
|
+
}
|
|
758
|
+
function normalizeSourceMap(map, context) {
|
|
759
|
+
if (map == null) return void 0;
|
|
760
|
+
try {
|
|
761
|
+
const value = typeof map === "string" ? JSON.parse(map) : map;
|
|
762
|
+
if (value && typeof value === "object" && Array.isArray(value.sources) && Array.isArray(value.names) && typeof value.mappings === "string") return value;
|
|
763
|
+
} catch {}
|
|
764
|
+
warnUnchainableMap(context, "the provided map is not a valid source map");
|
|
765
|
+
}
|
|
766
|
+
function composeSourceMapChain(maps, context) {
|
|
767
|
+
const pending = maps.filter((map) => map != null);
|
|
768
|
+
if (pending.length === 0) return void 0;
|
|
769
|
+
let composed = normalizeSourceMap(pending.shift(), context);
|
|
770
|
+
for (const map of pending) {
|
|
771
|
+
const input = normalizeSourceMap(map, context);
|
|
772
|
+
if (!input) continue;
|
|
773
|
+
if (!composed) {
|
|
774
|
+
composed = input;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
const consumer = new source_map_js.SourceMapConsumer(composed);
|
|
779
|
+
if (consumer.sources.length !== 1) {
|
|
780
|
+
warnUnchainableMap(context, "a generated map has multiple sources and cannot be chained safely");
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const generator = source_map_js.SourceMapGenerator.fromSourceMap(consumer);
|
|
784
|
+
generator.applySourceMap(new source_map_js.SourceMapConsumer(input), consumer.sources[0]);
|
|
785
|
+
composed = generator.toJSON();
|
|
786
|
+
} catch (error) {
|
|
787
|
+
warnUnchainableMap(context, `source-map composition failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return composed;
|
|
791
|
+
}
|
|
792
|
+
function warnUnchainableMap(context, reason) {
|
|
793
|
+
debug$1?.(`source map warning for ${context.filename} (${context.type}, ${context.environmentName}): ${reason}`);
|
|
794
|
+
}
|
|
795
|
+
function hashId(filename) {
|
|
796
|
+
return node_crypto.default.createHash("sha256").update(filename).digest("hex").slice(0, 8);
|
|
797
|
+
}
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region src/plugins/html.ts
|
|
800
|
+
/** 运行在用户脚本之前、安装 React Fast Refresh 全局钩子的 preamble。 */
|
|
801
|
+
const REACT_REFRESH_HTML_PREAMBLE = `
|
|
802
|
+
import RefreshRuntime from "/@react-refresh";
|
|
803
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
804
|
+
window.$RefreshReg$ = () => {};
|
|
805
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
806
|
+
window.__vite_plugin_react_preamble_installed__ = true;
|
|
807
|
+
`.trim();
|
|
808
|
+
function htmlPlugin(config) {
|
|
809
|
+
return {
|
|
810
|
+
name: "nasti:html",
|
|
811
|
+
enforce: "post",
|
|
812
|
+
transformIndexHtml(html) {
|
|
813
|
+
const tags = [];
|
|
814
|
+
if (config.command === "serve") {
|
|
815
|
+
if (config.framework === "react") tags.push({
|
|
816
|
+
tag: "script",
|
|
817
|
+
attrs: { type: "module" },
|
|
818
|
+
children: REACT_REFRESH_HTML_PREAMBLE,
|
|
819
|
+
injectTo: "head-prepend"
|
|
820
|
+
});
|
|
821
|
+
tags.push({
|
|
822
|
+
tag: "script",
|
|
823
|
+
attrs: {
|
|
824
|
+
type: "module",
|
|
825
|
+
src: "/@nasti/client"
|
|
826
|
+
},
|
|
827
|
+
injectTo: "head-prepend"
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
html,
|
|
832
|
+
tags
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
/** 处理 HTML,注入标签并重写 script src */
|
|
838
|
+
function processHtml(html, tags) {
|
|
839
|
+
const headPrepend = tags.filter((t) => t.injectTo === "head-prepend");
|
|
840
|
+
const head = tags.filter((t) => t.injectTo === "head" || !t.injectTo);
|
|
841
|
+
const bodyPrepend = tags.filter((t) => t.injectTo === "body-prepend");
|
|
842
|
+
const body = tags.filter((t) => t.injectTo === "body");
|
|
843
|
+
if (headPrepend.length) html = html.replace(/<head([^>]*)>/i, `<head$1>\n${serializeTags(headPrepend)}`);
|
|
844
|
+
if (head.length) html = html.replace(/<\/head>/i, `${serializeTags(head)}\n</head>`);
|
|
845
|
+
if (bodyPrepend.length) html = html.replace(/<body([^>]*)>/i, `<body$1>\n${serializeTags(bodyPrepend)}`);
|
|
846
|
+
if (body.length) html = html.replace(/<\/body>/i, `${serializeTags(body)}\n</body>`);
|
|
847
|
+
return html;
|
|
848
|
+
}
|
|
849
|
+
function serializeTags(tags) {
|
|
850
|
+
return tags.map(serializeTag).join("\n");
|
|
851
|
+
}
|
|
852
|
+
function serializeTag(tag) {
|
|
853
|
+
const attrs = tag.attrs ? " " + Object.entries(tag.attrs).map(([k, v]) => v === true ? k : `${k}="${v}"`).join(" ") : "";
|
|
854
|
+
const children = typeof tag.children === "string" ? tag.children : tag.children ? serializeTags(tag.children) : "";
|
|
855
|
+
if ([
|
|
856
|
+
"link",
|
|
857
|
+
"meta",
|
|
858
|
+
"br",
|
|
859
|
+
"hr",
|
|
860
|
+
"img",
|
|
861
|
+
"input"
|
|
862
|
+
].includes(tag.tag) && !children) return ` <${tag.tag}${attrs} />`;
|
|
863
|
+
return ` <${tag.tag}${attrs}>${children}</${tag.tag}>`;
|
|
864
|
+
}
|
|
865
|
+
/** 读取 HTML 入口文件;htmlFile 可为相对 root 或绝对路径。 */
|
|
866
|
+
async function readHtmlFile(root, htmlFile = "index.html") {
|
|
867
|
+
const htmlPath = node_path.default.isAbsolute(htmlFile) ? htmlFile : node_path.default.resolve(root, htmlFile);
|
|
868
|
+
if (!node_fs.default.existsSync(htmlPath)) return null;
|
|
869
|
+
return node_fs.default.readFileSync(htmlPath, "utf-8");
|
|
870
|
+
}
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/plugins/builtins.ts
|
|
873
|
+
/**
|
|
874
|
+
* 拼装某一环境的完整插件列表(顺序与 1.x 完全一致):
|
|
875
|
+
* [vue?] → resolve → css → assets → [html(serve)] → 用户插件 → [css-post(build)]
|
|
876
|
+
*/
|
|
877
|
+
function resolvePluginList(config, userPlugins, opts = {}) {
|
|
878
|
+
const isServe = config.command === "serve";
|
|
879
|
+
let environmentOptions;
|
|
880
|
+
if (opts.environmentName) {
|
|
881
|
+
environmentOptions = config.environments[opts.environmentName];
|
|
882
|
+
if (!environmentOptions) throw new Error(`[nasti] unknown environment "${opts.environmentName}" — declare it in config.environments`);
|
|
883
|
+
}
|
|
884
|
+
const pluginConfig = environmentOptions ? {
|
|
885
|
+
...config,
|
|
886
|
+
resolve: environmentOptions.resolve,
|
|
887
|
+
build: environmentOptions.build
|
|
888
|
+
} : config;
|
|
889
|
+
const consumer = opts.consumer ?? environmentOptions?.consumer;
|
|
890
|
+
return [
|
|
891
|
+
...config.framework === "vue" ? [vuePlugin(pluginConfig, opts.environmentName ?? "client")] : [],
|
|
892
|
+
require_env.resolvePlugin(pluginConfig),
|
|
893
|
+
cssPlugin(pluginConfig, opts.cssEngine, consumer),
|
|
894
|
+
assetsPlugin(pluginConfig),
|
|
895
|
+
...isServe ? [htmlPlugin(pluginConfig)] : [],
|
|
896
|
+
...userPlugins,
|
|
897
|
+
...!isServe && opts.cssEngine ? [cssPostPlugin(pluginConfig, opts.cssEngine)] : []
|
|
898
|
+
];
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/core/plugin-container.ts
|
|
902
|
+
var PluginContainer = class {
|
|
903
|
+
plugins;
|
|
904
|
+
config;
|
|
905
|
+
ctx;
|
|
906
|
+
emittedFiles = /* @__PURE__ */ new Map();
|
|
907
|
+
/** Environment API:容器所属环境(未传时为 undefined,行为同 1.x client) */
|
|
908
|
+
environment;
|
|
909
|
+
constructor(config, environment) {
|
|
910
|
+
this.config = config;
|
|
911
|
+
this.environment = environment;
|
|
912
|
+
this.plugins = require_env.orderPlugins(config.plugins);
|
|
913
|
+
this.ctx = this.createContext();
|
|
914
|
+
}
|
|
915
|
+
createContext() {
|
|
916
|
+
const container = this;
|
|
917
|
+
return {
|
|
918
|
+
async resolve(source, importer) {
|
|
919
|
+
return container.resolveId(source, importer);
|
|
920
|
+
},
|
|
921
|
+
emitFile(file) {
|
|
922
|
+
const fileName = file.fileName ?? file.name ?? `asset-${container.emittedFiles.size}`;
|
|
923
|
+
const id = `emitted:${fileName}`;
|
|
924
|
+
container.emittedFiles.set(id, {
|
|
925
|
+
fileName,
|
|
926
|
+
source: file.source ?? ""
|
|
927
|
+
});
|
|
928
|
+
return id;
|
|
929
|
+
},
|
|
930
|
+
getModuleInfo(_id) {
|
|
931
|
+
return null;
|
|
932
|
+
},
|
|
933
|
+
environment: container.environment
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
/** 返回所有通过 emitFile() 输出的文件 */
|
|
937
|
+
getEmittedFiles() {
|
|
938
|
+
return Array.from(this.emittedFiles.values());
|
|
939
|
+
}
|
|
940
|
+
async buildStart() {
|
|
941
|
+
for (const plugin of this.plugins) if (plugin.buildStart) await plugin.buildStart.call(this.ctx);
|
|
942
|
+
}
|
|
943
|
+
async buildEnd(error) {
|
|
944
|
+
for (const plugin of this.plugins) if (plugin.buildEnd) await plugin.buildEnd.call(this.ctx, error);
|
|
945
|
+
}
|
|
946
|
+
async resolveId(source, importer, options = {}) {
|
|
947
|
+
const ssr = this.environment?.consumer === "server";
|
|
948
|
+
for (const plugin of this.plugins) {
|
|
949
|
+
if (!plugin.resolveId) continue;
|
|
950
|
+
const result = await plugin.resolveId.call(this.ctx, source, importer ?? void 0, {
|
|
951
|
+
isEntry: options.isEntry ?? false,
|
|
952
|
+
ssr
|
|
953
|
+
});
|
|
954
|
+
if (result != null) return result;
|
|
955
|
+
}
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
async load(id) {
|
|
959
|
+
for (const plugin of this.plugins) {
|
|
960
|
+
if (!plugin.load) continue;
|
|
961
|
+
const result = await plugin.load.call(this.ctx, id);
|
|
962
|
+
if (result != null) return result;
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
async transform(code, id) {
|
|
967
|
+
let currentCode = code;
|
|
968
|
+
let lastResult;
|
|
969
|
+
for (const plugin of this.plugins) {
|
|
970
|
+
if (!plugin.transform) continue;
|
|
971
|
+
const result = await plugin.transform.call(this.ctx, currentCode, id);
|
|
972
|
+
if (result == null) continue;
|
|
973
|
+
if (typeof result === "string") {
|
|
974
|
+
currentCode = result;
|
|
975
|
+
lastResult = void 0;
|
|
976
|
+
} else {
|
|
977
|
+
currentCode = result.code;
|
|
978
|
+
lastResult = result;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return currentCode === code ? null : {
|
|
982
|
+
...lastResult,
|
|
983
|
+
code: currentCode
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
/** 完整的模块处理管道: resolveId → load → transform */
|
|
987
|
+
async processModule(source, importer) {
|
|
988
|
+
const resolveResult = await this.resolveId(source, importer, { isEntry: !importer });
|
|
989
|
+
if (resolveResult == null) return null;
|
|
990
|
+
const id = typeof resolveResult === "string" ? resolveResult : resolveResult.id;
|
|
991
|
+
const loadResult = await this.load(id);
|
|
992
|
+
if (loadResult == null) return null;
|
|
993
|
+
const loadedCode = typeof loadResult === "string" ? loadResult : loadResult.code;
|
|
994
|
+
const transformResult = await this.transform(loadedCode, id);
|
|
995
|
+
return {
|
|
996
|
+
id,
|
|
997
|
+
code: transformResult == null ? loadedCode : typeof transformResult === "string" ? transformResult : transformResult.code
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
getPlugins() {
|
|
1001
|
+
return this.plugins;
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
//#endregion
|
|
1005
|
+
//#region src/core/url.ts
|
|
1006
|
+
/** 保留语义 query,仅移除浏览器为 HMR 缓存失效附加的 `t` 参数。 */
|
|
1007
|
+
function removeTimestampQuery(url) {
|
|
1008
|
+
const hashIndex = url.indexOf("#");
|
|
1009
|
+
const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
|
|
1010
|
+
const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
|
1011
|
+
const queryIndex = withoutHash.indexOf("?");
|
|
1012
|
+
if (queryIndex < 0) return url;
|
|
1013
|
+
const pathname = withoutHash.slice(0, queryIndex);
|
|
1014
|
+
const query = withoutHash.slice(queryIndex + 1).split("&").filter((part) => !/^t=\d+$/.test(part)).join("&");
|
|
1015
|
+
return pathname + (query ? `?${query}` : "") + hash;
|
|
1016
|
+
}
|
|
1017
|
+
//#endregion
|
|
1018
|
+
//#region src/core/module-graph.ts
|
|
1019
|
+
var ModuleGraph = class {
|
|
1020
|
+
environmentName;
|
|
1021
|
+
urlToModuleMap = /* @__PURE__ */ new Map();
|
|
1022
|
+
idToModuleMap = /* @__PURE__ */ new Map();
|
|
1023
|
+
fileToModulesMap = /* @__PURE__ */ new Map();
|
|
1024
|
+
constructor(environmentName = "client") {
|
|
1025
|
+
this.environmentName = environmentName;
|
|
1026
|
+
}
|
|
1027
|
+
getModuleByUrl(url) {
|
|
1028
|
+
return this.urlToModuleMap.get(removeTimestampQuery(url));
|
|
1029
|
+
}
|
|
1030
|
+
getModuleById(id) {
|
|
1031
|
+
return this.idToModuleMap.get(id);
|
|
1032
|
+
}
|
|
1033
|
+
getModulesByFile(file) {
|
|
1034
|
+
return this.fileToModulesMap.get(file);
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Modules whose registered entry file lives under `dir` (inclusive).
|
|
1038
|
+
* Used when a non-entry source inside a prebundled workspace package changes:
|
|
1039
|
+
* only the package entry was registered, so getModulesByFile(changedFile)
|
|
1040
|
+
* misses — we invalidate every /@modules entry rooted in that package.
|
|
1041
|
+
*/
|
|
1042
|
+
getModulesWithFileUnder(dir) {
|
|
1043
|
+
const result = /* @__PURE__ */ new Set();
|
|
1044
|
+
const normDir = dir.replace(/\\/g, "/");
|
|
1045
|
+
const normPrefix = normDir.endsWith("/") ? normDir : normDir + "/";
|
|
1046
|
+
for (const [file, mods] of this.fileToModulesMap) {
|
|
1047
|
+
const normFile = file.replace(/\\/g, "/");
|
|
1048
|
+
if (normFile === normDir || normFile.startsWith(normPrefix)) for (const m of mods) result.add(m);
|
|
1049
|
+
}
|
|
1050
|
+
return result;
|
|
1051
|
+
}
|
|
1052
|
+
async ensureEntryFromUrl(url) {
|
|
1053
|
+
const normalizedUrl = removeTimestampQuery(url);
|
|
1054
|
+
let mod = this.urlToModuleMap.get(normalizedUrl);
|
|
1055
|
+
if (mod) return mod;
|
|
1056
|
+
mod = this.createModule(normalizedUrl);
|
|
1057
|
+
this.urlToModuleMap.set(normalizedUrl, mod);
|
|
1058
|
+
return mod;
|
|
1059
|
+
}
|
|
1060
|
+
createModule(url, id) {
|
|
1061
|
+
const mod = {
|
|
1062
|
+
id: id ?? url,
|
|
1063
|
+
file: null,
|
|
1064
|
+
url,
|
|
1065
|
+
type: url.endsWith(".css") ? "css" : "js",
|
|
1066
|
+
importers: /* @__PURE__ */ new Set(),
|
|
1067
|
+
importedModules: /* @__PURE__ */ new Set(),
|
|
1068
|
+
acceptedHmrDeps: /* @__PURE__ */ new Set(),
|
|
1069
|
+
transformResult: null,
|
|
1070
|
+
lastHMRTimestamp: 0,
|
|
1071
|
+
invalidationVersion: 0,
|
|
1072
|
+
isSelfAccepting: false,
|
|
1073
|
+
environment: this.environmentName
|
|
1074
|
+
};
|
|
1075
|
+
this.idToModuleMap.set(mod.id, mod);
|
|
1076
|
+
return mod;
|
|
1077
|
+
}
|
|
1078
|
+
/** 注册文件路径到模块的映射 */
|
|
1079
|
+
registerModule(mod, file) {
|
|
1080
|
+
mod.file = file;
|
|
1081
|
+
let mods = this.fileToModulesMap.get(file);
|
|
1082
|
+
if (!mods) {
|
|
1083
|
+
mods = /* @__PURE__ */ new Set();
|
|
1084
|
+
this.fileToModulesMap.set(file, mods);
|
|
1085
|
+
}
|
|
1086
|
+
mods.add(mod);
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Reindex a module under a plugin-provided canonical id (e.g. a `\0virtual:foo`
|
|
1090
|
+
* id returned from `resolveId`). Plugins look up their own virtual modules via
|
|
1091
|
+
* `getModuleById(RESOLVED_ID)` to invalidate them on watcher events; without
|
|
1092
|
+
* this remap they'd never find the node because `ensureEntryFromUrl` keys by
|
|
1093
|
+
* the public URL only.
|
|
1094
|
+
*/
|
|
1095
|
+
setModuleId(mod, id) {
|
|
1096
|
+
if (mod.id === id) return;
|
|
1097
|
+
this.idToModuleMap.delete(mod.id);
|
|
1098
|
+
mod.id = id;
|
|
1099
|
+
this.idToModuleMap.set(id, mod);
|
|
1100
|
+
}
|
|
1101
|
+
/** 更新模块依赖关系 */
|
|
1102
|
+
updateModuleImports(mod, importedIds) {
|
|
1103
|
+
for (const imported of mod.importedModules) imported.importers.delete(mod);
|
|
1104
|
+
mod.importedModules.clear();
|
|
1105
|
+
for (const id of importedIds) {
|
|
1106
|
+
const importedMod = this.idToModuleMap.get(id);
|
|
1107
|
+
if (importedMod) {
|
|
1108
|
+
mod.importedModules.add(importedMod);
|
|
1109
|
+
importedMod.importers.add(mod);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* 用一次转换得到的信息原子更新 import 与 HMR accept 关系。
|
|
1115
|
+
* 依赖节点会在真正被浏览器请求前预先创建,这样入口模块先转换时也能建立完整图。
|
|
1116
|
+
*/
|
|
1117
|
+
async updateModuleInfo(mod, importedUrls, acceptedUrls, isSelfAccepting, expectedInvalidationVersion) {
|
|
1118
|
+
const importedModules = await Promise.all([...importedUrls].map((url) => this.ensureEntryFromUrl(url)));
|
|
1119
|
+
const acceptedModules = await Promise.all([...acceptedUrls].map((url) => this.ensureEntryFromUrl(url)));
|
|
1120
|
+
if (expectedInvalidationVersion !== void 0 && mod.invalidationVersion !== expectedInvalidationVersion) return null;
|
|
1121
|
+
const previousImports = new Set(mod.importedModules);
|
|
1122
|
+
for (const imported of previousImports) imported.importers.delete(mod);
|
|
1123
|
+
mod.importedModules.clear();
|
|
1124
|
+
mod.acceptedHmrDeps.clear();
|
|
1125
|
+
for (const imported of importedModules) {
|
|
1126
|
+
mod.importedModules.add(imported);
|
|
1127
|
+
imported.importers.add(mod);
|
|
1128
|
+
}
|
|
1129
|
+
for (const accepted of acceptedModules) mod.acceptedHmrDeps.add(accepted);
|
|
1130
|
+
mod.isSelfAccepting = isSelfAccepting;
|
|
1131
|
+
const pruned = /* @__PURE__ */ new Set();
|
|
1132
|
+
for (const imported of previousImports) if (!mod.importedModules.has(imported) && imported.importers.size === 0) pruned.add(imported);
|
|
1133
|
+
return pruned;
|
|
1134
|
+
}
|
|
1135
|
+
/** 使模块的转换缓存失效 */
|
|
1136
|
+
invalidateModule(mod, timestamp = Date.now()) {
|
|
1137
|
+
mod.transformResult = null;
|
|
1138
|
+
mod.lastHMRTimestamp = timestamp;
|
|
1139
|
+
mod.invalidationVersion++;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* 仅失效到 HMR 边界:显式接受依赖的模块本身不会重执行;自接受模块需要失效,
|
|
1143
|
+
* 但不再继续影响其 importer。这样既能传播依赖时间戳,也不会隐式重复副作用。
|
|
1144
|
+
*/
|
|
1145
|
+
invalidateModuleAndImporters(mod, timestamp = Date.now(), seen = /* @__PURE__ */ new Set()) {
|
|
1146
|
+
if (seen.has(mod)) return;
|
|
1147
|
+
seen.add(mod);
|
|
1148
|
+
this.invalidateModule(mod, timestamp);
|
|
1149
|
+
for (const importer of mod.importers) {
|
|
1150
|
+
if (importer.acceptedHmrDeps.has(mod)) continue;
|
|
1151
|
+
if (importer.isSelfAccepting) {
|
|
1152
|
+
this.invalidateModule(importer, timestamp);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
this.invalidateModuleAndImporters(importer, timestamp, seen);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
/** 使所有模块缓存失效 */
|
|
1159
|
+
invalidateAll() {
|
|
1160
|
+
for (const mod of this.idToModuleMap.values()) this.invalidateModule(mod);
|
|
1161
|
+
}
|
|
1162
|
+
/** 获取 HMR 传播边界 - 从变更模块向上遍历找到接受更新的边界 */
|
|
1163
|
+
getHmrBoundaries(mod) {
|
|
1164
|
+
const boundaries = [];
|
|
1165
|
+
const traversed = /* @__PURE__ */ new Set();
|
|
1166
|
+
const addBoundary = (boundary, acceptedVia) => {
|
|
1167
|
+
if (!boundaries.some((item) => item.boundary === boundary && item.acceptedVia === acceptedVia)) boundaries.push({
|
|
1168
|
+
boundary,
|
|
1169
|
+
acceptedVia
|
|
1170
|
+
});
|
|
1171
|
+
};
|
|
1172
|
+
const propagate = (node) => {
|
|
1173
|
+
if (traversed.has(node)) return true;
|
|
1174
|
+
traversed.add(node);
|
|
1175
|
+
if (node.isSelfAccepting) {
|
|
1176
|
+
addBoundary(node, node);
|
|
1177
|
+
return true;
|
|
1178
|
+
}
|
|
1179
|
+
if (node.importers.size === 0) return false;
|
|
1180
|
+
for (const importer of node.importers) {
|
|
1181
|
+
if (importer.acceptedHmrDeps.has(node)) {
|
|
1182
|
+
addBoundary(importer, node);
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
if (!propagate(importer)) return false;
|
|
1186
|
+
}
|
|
1187
|
+
return true;
|
|
1188
|
+
};
|
|
1189
|
+
return propagate(mod) ? boundaries : [];
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
//#endregion
|
|
1193
|
+
//#region src/core/hot-channel.ts
|
|
1194
|
+
/** 非 client 环境的占位通道(SSR/edge 可由 runner/driver 接上真实 transport) */
|
|
1195
|
+
function createNoopHotChannel() {
|
|
1196
|
+
return {
|
|
1197
|
+
send() {},
|
|
1198
|
+
on() {},
|
|
1199
|
+
off() {},
|
|
1200
|
+
listen() {},
|
|
1201
|
+
close() {},
|
|
1202
|
+
setInvokeHandler() {}
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* client-consumer 环境的 ws 通道:包装 server/ws.ts 的 WebSocketServer。
|
|
1207
|
+
* 事件监听与 invoke 在现有 ws 协议上预留,payload 以 environment 字段分流。
|
|
1208
|
+
*/
|
|
1209
|
+
function createWsHotChannel(ws, environmentName = "client") {
|
|
1210
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
1211
|
+
return {
|
|
1212
|
+
send(payload) {
|
|
1213
|
+
ws.send({
|
|
1214
|
+
...payload,
|
|
1215
|
+
environment: payload.environment ?? environmentName
|
|
1216
|
+
});
|
|
1217
|
+
},
|
|
1218
|
+
on(event, listener) {
|
|
1219
|
+
let set = listeners.get(event);
|
|
1220
|
+
if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
|
|
1221
|
+
set.add(listener);
|
|
1222
|
+
},
|
|
1223
|
+
off(event, listener) {
|
|
1224
|
+
listeners.get(event)?.delete(listener);
|
|
1225
|
+
},
|
|
1226
|
+
listen() {},
|
|
1227
|
+
close() {},
|
|
1228
|
+
setInvokeHandler(handlers) {}
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
//#endregion
|
|
1232
|
+
//#region src/core/environment.ts
|
|
1233
|
+
const debug = createDebugger("nasti:environment");
|
|
1234
|
+
var NastiEnvironment = class {
|
|
1235
|
+
name;
|
|
1236
|
+
consumer;
|
|
1237
|
+
mode;
|
|
1238
|
+
config;
|
|
1239
|
+
options;
|
|
1240
|
+
hot;
|
|
1241
|
+
driver;
|
|
1242
|
+
/** applyToEnvironment 过滤后的插件(init() 后可用) */
|
|
1243
|
+
plugins = [];
|
|
1244
|
+
/** per-env 插件容器(init() 后可用;dev 管线使用) */
|
|
1245
|
+
pluginContainer = null;
|
|
1246
|
+
/** per-env 模块图(dev 管线使用) */
|
|
1247
|
+
moduleGraph;
|
|
1248
|
+
candidatePlugins;
|
|
1249
|
+
pluginApi;
|
|
1250
|
+
buildMetadata = {};
|
|
1251
|
+
cssModules = /* @__PURE__ */ new Map();
|
|
1252
|
+
assetModules = /* @__PURE__ */ new Map();
|
|
1253
|
+
transformRequestHandler;
|
|
1254
|
+
initialized = false;
|
|
1255
|
+
constructor(name, config, init = {}) {
|
|
1256
|
+
const options = config.environments[name];
|
|
1257
|
+
if (!options) throw new Error(`[nasti] unknown environment "${name}" — declare it in config.environments`);
|
|
1258
|
+
this.name = name;
|
|
1259
|
+
this.consumer = options.consumer;
|
|
1260
|
+
this.mode = init.mode ?? (config.command === "build" ? "build" : "dev");
|
|
1261
|
+
this.config = config;
|
|
1262
|
+
this.options = options;
|
|
1263
|
+
this.hot = init.hot ?? createNoopHotChannel();
|
|
1264
|
+
this.moduleGraph = new ModuleGraph(name);
|
|
1265
|
+
this.candidatePlugins = init.plugins ?? config.plugins;
|
|
1266
|
+
this.pluginApi = init.pluginApi ?? require_env.getPluginApi(config);
|
|
1267
|
+
}
|
|
1268
|
+
/** 过滤插件并建 per-env PluginContainer */
|
|
1269
|
+
async init() {
|
|
1270
|
+
if (this.initialized) return;
|
|
1271
|
+
this.initialized = true;
|
|
1272
|
+
this.plugins = resolveEnvironmentPlugins(this, this.candidatePlugins);
|
|
1273
|
+
this.pluginContainer = new PluginContainer({
|
|
1274
|
+
...this.config,
|
|
1275
|
+
plugins: this.plugins
|
|
1276
|
+
}, this);
|
|
1277
|
+
if (this.options.driver) {
|
|
1278
|
+
const claimed = [];
|
|
1279
|
+
for (const plugin of this.plugins) {
|
|
1280
|
+
const driver = await plugin.createEnvironmentDriver?.(this, this.pluginApi);
|
|
1281
|
+
if (driver) claimed.push({
|
|
1282
|
+
plugin,
|
|
1283
|
+
driver
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
if (claimed.length === 0) throw new Error(`[nasti] environment "${this.name}" requested driver "${this.options.driver}", but no plugin provided it`);
|
|
1287
|
+
if (claimed.length > 1) throw new Error(`[nasti] environment "${this.name}" was claimed by multiple drivers: ` + claimed.map(({ plugin, driver }) => `${plugin.name} (${driver.name})`).join(", "));
|
|
1288
|
+
this.driver = claimed[0].driver;
|
|
1289
|
+
debug?.(`env "${this.name}" uses driver "${this.driver.name}"`);
|
|
1290
|
+
}
|
|
1291
|
+
debug?.(`env "${this.name}" initialized (${this.plugins.length} plugins)`);
|
|
1292
|
+
}
|
|
1293
|
+
getDriverContext() {
|
|
1294
|
+
return {
|
|
1295
|
+
environment: this,
|
|
1296
|
+
config: this.config,
|
|
1297
|
+
api: this.pluginApi,
|
|
1298
|
+
logger: this.config.logger
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
configureDevPipeline(transformRequest) {
|
|
1302
|
+
this.transformRequestHandler = transformRequest;
|
|
1303
|
+
}
|
|
1304
|
+
async transformRequest(url) {
|
|
1305
|
+
if (!this.transformRequestHandler) throw new Error(`[nasti] environment "${this.name}" does not have an initialized dev transform pipeline`);
|
|
1306
|
+
return this.transformRequestHandler(url);
|
|
1307
|
+
}
|
|
1308
|
+
setCssModule(module) {
|
|
1309
|
+
this.cssModules.set(module.id, { ...module });
|
|
1310
|
+
}
|
|
1311
|
+
getCssModule(id) {
|
|
1312
|
+
const module = this.cssModules.get(id);
|
|
1313
|
+
return module ? { ...module } : void 0;
|
|
1314
|
+
}
|
|
1315
|
+
getCssModules() {
|
|
1316
|
+
return Object.freeze(Object.fromEntries([...this.cssModules].map(([id, module]) => [id, { ...module }])));
|
|
1317
|
+
}
|
|
1318
|
+
setAssetModule(id, fileName) {
|
|
1319
|
+
this.assetModules.set(id, fileName);
|
|
1320
|
+
}
|
|
1321
|
+
getAssetModules() {
|
|
1322
|
+
return Object.freeze(Object.fromEntries(this.assetModules));
|
|
1323
|
+
}
|
|
1324
|
+
setBuildMetadata(metadata) {
|
|
1325
|
+
const { entries: currentEntries, ...currentMetadata } = this.buildMetadata;
|
|
1326
|
+
const { entries, ...nextMetadata } = metadata;
|
|
1327
|
+
this.buildMetadata = {
|
|
1328
|
+
...currentMetadata,
|
|
1329
|
+
...nextMetadata,
|
|
1330
|
+
...currentEntries || entries ? { entries: {
|
|
1331
|
+
...currentEntries,
|
|
1332
|
+
...entries
|
|
1333
|
+
} } : {}
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
getBuildMetadata() {
|
|
1337
|
+
const { entries, ...metadata } = this.buildMetadata;
|
|
1338
|
+
return {
|
|
1339
|
+
...metadata,
|
|
1340
|
+
...entries ? { entries: { ...entries } } : {}
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
async close() {
|
|
1344
|
+
try {
|
|
1345
|
+
await this.driver?.close?.(this.getDriverContext());
|
|
1346
|
+
} finally {
|
|
1347
|
+
await this.hot.close?.();
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
/** 按 applyToEnvironment 过滤环境插件(未声明 = 应用于所有环境) */
|
|
1352
|
+
function resolveEnvironmentPlugins(environment, plugins) {
|
|
1353
|
+
return plugins.filter((p) => {
|
|
1354
|
+
if (!p.applyToEnvironment) return true;
|
|
1355
|
+
try {
|
|
1356
|
+
return p.applyToEnvironment(environment);
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
environment.config.logger.error(`[nasti] plugin "${p.name}" applyToEnvironment threw: ${err.message}`, { error: err });
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
//#endregion
|
|
1364
|
+
Object.defineProperty(exports, "NastiEnvironment", {
|
|
1365
|
+
enumerable: true,
|
|
1366
|
+
get: function() {
|
|
1367
|
+
return NastiEnvironment;
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
Object.defineProperty(exports, "createCssEngine", {
|
|
1371
|
+
enumerable: true,
|
|
1372
|
+
get: function() {
|
|
1373
|
+
return createCssEngine;
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
Object.defineProperty(exports, "createDebugger", {
|
|
1377
|
+
enumerable: true,
|
|
1378
|
+
get: function() {
|
|
1379
|
+
return createDebugger;
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
Object.defineProperty(exports, "createNoopHotChannel", {
|
|
1383
|
+
enumerable: true,
|
|
1384
|
+
get: function() {
|
|
1385
|
+
return createNoopHotChannel;
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
Object.defineProperty(exports, "createWsHotChannel", {
|
|
1389
|
+
enumerable: true,
|
|
1390
|
+
get: function() {
|
|
1391
|
+
return createWsHotChannel;
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
Object.defineProperty(exports, "getCssMetadata", {
|
|
1395
|
+
enumerable: true,
|
|
1396
|
+
get: function() {
|
|
1397
|
+
return getCssMetadata;
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
Object.defineProperty(exports, "htmlPlugin", {
|
|
1401
|
+
enumerable: true,
|
|
1402
|
+
get: function() {
|
|
1403
|
+
return htmlPlugin;
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
Object.defineProperty(exports, "isAssetFile", {
|
|
1407
|
+
enumerable: true,
|
|
1408
|
+
get: function() {
|
|
1409
|
+
return isAssetFile;
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
Object.defineProperty(exports, "processHtml", {
|
|
1413
|
+
enumerable: true,
|
|
1414
|
+
get: function() {
|
|
1415
|
+
return processHtml;
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
Object.defineProperty(exports, "readHtmlFile", {
|
|
1419
|
+
enumerable: true,
|
|
1420
|
+
get: function() {
|
|
1421
|
+
return readHtmlFile;
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
Object.defineProperty(exports, "removeTimestampQuery", {
|
|
1425
|
+
enumerable: true,
|
|
1426
|
+
get: function() {
|
|
1427
|
+
return removeTimestampQuery;
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
Object.defineProperty(exports, "resolveEnvironmentPlugins", {
|
|
1431
|
+
enumerable: true,
|
|
1432
|
+
get: function() {
|
|
1433
|
+
return resolveEnvironmentPlugins;
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
Object.defineProperty(exports, "resolvePluginList", {
|
|
1437
|
+
enumerable: true,
|
|
1438
|
+
get: function() {
|
|
1439
|
+
return resolvePluginList;
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
//# sourceMappingURL=environment-CjmvoHIG.cjs.map
|