@nasti-toolchain/nasti 1.3.9 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,1415 +8,1475 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
- // src/plugins/html.ts
12
- import path5 from "path";
13
- import fs4 from "fs";
14
- function htmlPlugin(config) {
15
- return {
16
- name: "nasti:html",
17
- enforce: "post",
18
- transformIndexHtml(html) {
19
- const tags = [];
20
- if (config.command === "serve") {
21
- tags.push({
22
- tag: "script",
23
- attrs: { type: "module", src: "/@nasti/client" },
24
- injectTo: "head-prepend"
25
- });
26
- }
27
- return { html, tags };
28
- }
29
- };
30
- }
31
- function processHtml(html, tags) {
32
- const headPrepend = tags.filter((t) => t.injectTo === "head-prepend");
33
- const head = tags.filter((t) => t.injectTo === "head" || !t.injectTo);
34
- const bodyPrepend = tags.filter((t) => t.injectTo === "body-prepend");
35
- const body = tags.filter((t) => t.injectTo === "body");
36
- if (headPrepend.length) {
37
- html = html.replace(/<head([^>]*)>/i, `<head$1>
38
- ${serializeTags(headPrepend)}`);
39
- }
40
- if (head.length) {
41
- html = html.replace(/<\/head>/i, `${serializeTags(head)}
42
- </head>`);
43
- }
44
- if (bodyPrepend.length) {
45
- html = html.replace(/<body([^>]*)>/i, `<body$1>
46
- ${serializeTags(bodyPrepend)}`);
47
- }
48
- if (body.length) {
49
- html = html.replace(/<\/body>/i, `${serializeTags(body)}
50
- </body>`);
11
+ // src/config/defaults.ts
12
+ var defaultResolve, defaultServer, defaultBuild, defaultElectron, defaults;
13
+ var init_defaults = __esm({
14
+ "src/config/defaults.ts"() {
15
+ "use strict";
16
+ defaultResolve = {
17
+ alias: {},
18
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".vue"],
19
+ conditions: ["import", "module", "browser", "default"],
20
+ mainFields: ["module", "jsnext:main", "jsnext", "main"]
21
+ };
22
+ defaultServer = {
23
+ port: 3e3,
24
+ host: "localhost",
25
+ https: false,
26
+ open: false,
27
+ proxy: {},
28
+ cors: true,
29
+ hmr: true
30
+ };
31
+ defaultBuild = {
32
+ outDir: "dist",
33
+ assetsDir: "assets",
34
+ minify: true,
35
+ sourcemap: false,
36
+ target: "es2022",
37
+ rolldownOptions: {},
38
+ emptyOutDir: true
39
+ };
40
+ defaultElectron = {
41
+ main: "src/electron/main.ts",
42
+ preload: "src/electron/preload.ts",
43
+ renderer: "index.html",
44
+ nodeTarget: "node22",
45
+ mainFormat: "cjs",
46
+ preloadFormat: "cjs",
47
+ electronPath: "",
48
+ electronArgs: [],
49
+ autoRestart: true,
50
+ minVersion: 41,
51
+ external: ["electron"]
52
+ };
53
+ defaults = {
54
+ root: ".",
55
+ base: "/",
56
+ mode: "development",
57
+ target: "web",
58
+ framework: "auto",
59
+ resolve: defaultResolve,
60
+ server: defaultServer,
61
+ build: defaultBuild,
62
+ electron: defaultElectron,
63
+ plugins: [],
64
+ envPrefix: ["NASTI_", "VITE_"],
65
+ logLevel: "info"
66
+ };
51
67
  }
52
- return html;
53
- }
54
- function serializeTags(tags) {
55
- return tags.map(serializeTag).join("\n");
56
- }
57
- function serializeTag(tag) {
58
- const attrs = tag.attrs ? " " + Object.entries(tag.attrs).map(([k, v]) => v === true ? k : `${k}="${v}"`).join(" ") : "";
59
- const children = typeof tag.children === "string" ? tag.children : tag.children ? serializeTags(tag.children) : "";
60
- const selfClosing = ["link", "meta", "br", "hr", "img", "input"].includes(tag.tag);
61
- if (selfClosing && !children) {
62
- return ` <${tag.tag}${attrs} />`;
68
+ });
69
+
70
+ // src/config/index.ts
71
+ import { pathToFileURL } from "url";
72
+ import path from "path";
73
+ import fs from "fs";
74
+ function loadTsconfigPaths(root) {
75
+ const tsconfigPath = path.resolve(root, "tsconfig.json");
76
+ if (!fs.existsSync(tsconfigPath)) return {};
77
+ try {
78
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
79
+ const stripped = content.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
80
+ const tsconfig = JSON.parse(stripped);
81
+ const paths = tsconfig?.compilerOptions?.paths ?? {};
82
+ const baseUrl = tsconfig?.compilerOptions?.baseUrl ?? ".";
83
+ const alias = {};
84
+ for (const [pattern, targets] of Object.entries(paths)) {
85
+ if (!targets.length) continue;
86
+ const cleanKey = pattern.replace(/\/\*$/, "");
87
+ const cleanTarget = targets[0].replace(/\/\*$/, "");
88
+ alias[cleanKey] = path.resolve(root, baseUrl, cleanTarget);
89
+ }
90
+ return alias;
91
+ } catch {
92
+ return {};
63
93
  }
64
- return ` <${tag.tag}${attrs}>${children}</${tag.tag}>`;
65
94
  }
66
- async function readHtmlFile(root) {
67
- const htmlPath = path5.resolve(root, "index.html");
68
- if (!fs4.existsSync(htmlPath)) return null;
69
- return fs4.readFileSync(htmlPath, "utf-8");
95
+ function defineConfig(config) {
96
+ return config;
70
97
  }
71
- var init_html = __esm({
72
- "src/plugins/html.ts"() {
73
- "use strict";
98
+ async function loadConfigFromFile(root) {
99
+ for (const file of CONFIG_FILES) {
100
+ const filePath = path.resolve(root, file);
101
+ if (!fs.existsSync(filePath)) continue;
102
+ if (file.endsWith(".ts") || file.endsWith(".mts")) {
103
+ return await loadTsConfig(filePath);
104
+ }
105
+ const mod = await import(pathToFileURL(filePath).href);
106
+ return mod.default ?? mod;
74
107
  }
75
- });
76
-
77
- // src/core/transformer.ts
78
- import { transformSync } from "oxc-transform";
79
- function shouldTransform(id) {
80
- return TS_EXTENSIONS.test(id) || JSX_EXTENSIONS.test(id) || JS_EXTENSIONS.test(id) && false;
108
+ return {};
81
109
  }
82
- function transformCode(filename, code, options = {}) {
83
- const isTS = TS_EXTENSIONS.test(filename) || /\.tsx$/.test(filename);
84
- const isJSX = JSX_EXTENSIONS.test(filename);
85
- const result = transformSync(filename, code, {
86
- typescript: isTS ? {} : void 0,
87
- jsx: isJSX || /\.tsx$/.test(filename) ? {
88
- runtime: options.jsxRuntime ?? "automatic",
89
- importSource: options.jsxImportSource ?? "react",
90
- refresh: options.reactRefresh ?? false
91
- } : void 0,
92
- sourcemap: options.sourcemap ?? true
110
+ async function loadTsConfig(filePath) {
111
+ const { transformSync: transformSync2 } = await import("oxc-transform");
112
+ const code = fs.readFileSync(filePath, "utf-8");
113
+ const result = transformSync2(filePath, code, {
114
+ typescript: {}
93
115
  });
94
- if (result.errors && result.errors.length > 0) {
95
- const msg = result.errors.map((e) => e.message ?? String(e)).join("\n");
96
- throw new Error(`OXC transform failed for ${filename}:
97
- ${msg}`);
116
+ const tmpFile = filePath + ".timestamp-" + Date.now() + ".mjs";
117
+ try {
118
+ fs.writeFileSync(tmpFile, result.code);
119
+ const mod = await import(pathToFileURL(tmpFile).href);
120
+ return mod.default ?? mod;
121
+ } finally {
122
+ fs.unlinkSync(tmpFile);
98
123
  }
99
- return {
100
- code: result.code,
101
- map: result.map ? JSON.stringify(result.map) : null
102
- };
103
124
  }
104
- var JS_EXTENSIONS, TS_EXTENSIONS, JSX_EXTENSIONS;
105
- var init_transformer = __esm({
106
- "src/core/transformer.ts"() {
107
- "use strict";
108
- JS_EXTENSIONS = /\.(js|mjs|cjs)$/;
109
- TS_EXTENSIONS = /\.(ts|mts|cts)$/;
110
- JSX_EXTENSIONS = /\.(jsx|tsx)$/;
111
- }
112
- });
113
-
114
- // src/core/env.ts
115
- import path6 from "path";
116
- import fs5 from "fs";
117
- function loadEnv(mode, root, prefixes) {
118
- const envFiles = [
119
- ".env",
120
- `.env.${mode}`,
121
- ".env.local",
122
- `.env.${mode}.local`
125
+ async function resolveConfig(inlineConfig = {}, command) {
126
+ const root = path.resolve(inlineConfig.root ?? defaults.root);
127
+ const fileConfig = await loadConfigFromFile(root);
128
+ const merged = deepMerge(deepMerge({}, fileConfig), inlineConfig);
129
+ const rawPlugins = [
130
+ ...fileConfig.plugins ?? [],
131
+ ...inlineConfig.plugins ?? []
123
132
  ];
124
- const raw = {};
125
- for (const file of envFiles) {
126
- const filePath = path6.resolve(root, file);
127
- if (!fs5.existsSync(filePath)) continue;
128
- const content = fs5.readFileSync(filePath, "utf-8");
129
- for (const line of content.split("\n")) {
130
- const trimmed = line.trim();
131
- if (!trimmed || trimmed.startsWith("#")) continue;
132
- const eqIdx = trimmed.indexOf("=");
133
- if (eqIdx === -1) continue;
134
- const key = trimmed.slice(0, eqIdx).trim();
135
- let value = trimmed.slice(eqIdx + 1).trim();
136
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
137
- value = value.slice(1, -1);
138
- }
139
- raw[key] = value;
133
+ const env = { mode: merged.mode ?? defaults.mode, command };
134
+ for (const plugin of rawPlugins) {
135
+ if (plugin.config) {
136
+ const result = await plugin.config(merged, env);
137
+ if (result) Object.assign(merged, result);
140
138
  }
141
139
  }
142
- const filtered = {};
143
- for (const [key, value] of Object.entries(raw)) {
144
- if (prefixes.some((prefix) => key.startsWith(prefix))) {
145
- filtered[key] = value;
140
+ const resolved = {
141
+ root,
142
+ base: merged.base ?? defaults.base,
143
+ mode: command === "build" ? "production" : "development",
144
+ target: merged.target ?? defaults.target,
145
+ framework: merged.framework ?? defaults.framework,
146
+ command,
147
+ resolve: {
148
+ // tsconfig paths 优先级最低:tsconfig < defaults < user config
149
+ alias: { ...loadTsconfigPaths(root), ...defaults.resolve.alias, ...merged.resolve?.alias },
150
+ extensions: merged.resolve?.extensions ?? defaults.resolve.extensions,
151
+ conditions: merged.resolve?.conditions ?? defaults.resolve.conditions,
152
+ mainFields: merged.resolve?.mainFields ?? defaults.resolve.mainFields
153
+ },
154
+ plugins: [],
155
+ server: { ...defaults.server, ...merged.server },
156
+ build: { ...defaults.build, ...merged.build },
157
+ electron: { ...defaults.electron, ...merged.electron },
158
+ envPrefix: Array.isArray(merged.envPrefix) ? merged.envPrefix : merged.envPrefix ? [merged.envPrefix] : [...defaults.envPrefix],
159
+ logLevel: merged.logLevel ?? defaults.logLevel
160
+ };
161
+ const filteredPlugins = rawPlugins.filter((p) => {
162
+ if (!p.apply) return true;
163
+ if (typeof p.apply === "function") return p.apply(resolved, env);
164
+ return p.apply === command;
165
+ });
166
+ resolved.plugins = filteredPlugins;
167
+ for (const plugin of resolved.plugins) {
168
+ if (plugin.configResolved) {
169
+ await plugin.configResolved(resolved);
146
170
  }
147
171
  }
148
- return filtered;
149
- }
150
- function buildEnvDefine(env, mode) {
151
- const define = {};
152
- for (const [key, value] of Object.entries(env)) {
153
- define[`import.meta.env.${key}`] = JSON.stringify(value);
154
- }
155
- define["import.meta.env.MODE"] = JSON.stringify(mode);
156
- define["import.meta.env.DEV"] = mode !== "production" ? "true" : "false";
157
- define["import.meta.env.PROD"] = mode === "production" ? "true" : "false";
158
- define["import.meta.env.SSR"] = "false";
159
- return define;
172
+ return resolved;
160
173
  }
161
- function replaceEnvInCode(code, define) {
162
- let result = code;
163
- for (const [key, value] of Object.entries(define)) {
164
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
165
- result = result.replace(new RegExp(escaped, "g"), value);
174
+ function deepMerge(target, source) {
175
+ const result = { ...target };
176
+ for (const key of Object.keys(source)) {
177
+ const val = source[key];
178
+ if (val && typeof val === "object" && !Array.isArray(val)) {
179
+ result[key] = deepMerge(
180
+ result[key] ?? {},
181
+ val
182
+ );
183
+ } else if (val !== void 0) {
184
+ result[key] = val;
185
+ }
166
186
  }
167
187
  return result;
168
188
  }
169
- var init_env = __esm({
170
- "src/core/env.ts"() {
189
+ var CONFIG_FILES;
190
+ var init_config = __esm({
191
+ "src/config/index.ts"() {
171
192
  "use strict";
193
+ init_defaults();
194
+ CONFIG_FILES = [
195
+ "nasti.config.ts",
196
+ "nasti.config.js",
197
+ "nasti.config.mjs",
198
+ "nasti.config.mts"
199
+ ];
172
200
  }
173
201
  });
174
202
 
175
- // src/server/middleware.ts
176
- var middleware_exports = {};
177
- __export(middleware_exports, {
178
- transformMiddleware: () => transformMiddleware,
179
- transformRequest: () => transformRequest
180
- });
181
- import path8 from "path";
182
- import fs7 from "fs";
183
- function transformMiddleware(ctx) {
184
- ctx.envDefine = buildEnvDefine(
185
- loadEnv(ctx.config.mode, ctx.config.root, ctx.config.envPrefix),
186
- ctx.config.mode
187
- );
188
- return async (req, res, next) => {
189
- const url = req.url ?? "/";
190
- if (ctx.config.server.cors) {
191
- const origin = req.headers.origin ?? "*";
192
- res.setHeader("Access-Control-Allow-Origin", origin);
193
- res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
194
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
195
- if (req.method === "OPTIONS") {
196
- res.statusCode = 204;
197
- res.end();
198
- return;
199
- }
200
- }
201
- if (req.method !== "GET") return next();
202
- if (url === "/@nasti/client") {
203
- res.setHeader("Content-Type", "application/javascript");
204
- res.end(getHmrClientCode());
205
- return;
206
- }
207
- if (url === "/" || url.endsWith(".html")) {
208
- const html = await readHtmlFile(ctx.config.root);
209
- if (html) {
210
- let processedHtml = html;
211
- for (const plugin of ctx.config.plugins) {
212
- if (plugin.transformIndexHtml) {
213
- const result = await plugin.transformIndexHtml(processedHtml);
214
- if (typeof result === "string") {
215
- processedHtml = result;
216
- } else if (result && "html" in result) {
217
- processedHtml = processHtml(result.html, result.tags);
218
- } else if (Array.isArray(result)) {
219
- processedHtml = processHtml(processedHtml, result);
220
- }
203
+ // src/plugins/resolve.ts
204
+ import path2 from "path";
205
+ import fs2 from "fs";
206
+ import { createRequire } from "module";
207
+ function resolvePlugin(config) {
208
+ const { alias, extensions } = config.resolve;
209
+ const require2 = createRequire(path2.resolve(config.root, "package.json"));
210
+ return {
211
+ name: "nasti:resolve",
212
+ enforce: "pre",
213
+ resolveId(source, importer) {
214
+ for (const [key, value] of Object.entries(alias)) {
215
+ if (source === key || source.startsWith(key + "/")) {
216
+ source = source.replace(key, value);
217
+ if (!path2.isAbsolute(source)) {
218
+ source = path2.resolve(config.root, source);
221
219
  }
220
+ break;
222
221
  }
223
- res.setHeader("Content-Type", "text/html");
224
- res.end(processedHtml);
225
- return;
226
222
  }
227
- }
228
- if (isModuleRequest(url)) {
229
- try {
230
- const result = await transformRequest(url, ctx);
231
- if (result) {
232
- const contentType = url.endsWith(".css") ? "application/javascript" : "application/javascript";
233
- res.setHeader("Content-Type", contentType);
234
- res.setHeader("Cache-Control", "no-cache");
235
- res.end(typeof result === "string" ? result : result.code);
236
- return;
223
+ if (path2.isAbsolute(source)) {
224
+ const resolved = tryResolveFile(source, extensions);
225
+ if (resolved) return resolved;
226
+ }
227
+ if (source.startsWith(".")) {
228
+ const dir = importer ? path2.dirname(importer) : config.root;
229
+ const absolute = path2.resolve(dir, source);
230
+ const resolved = tryResolveFile(absolute, extensions);
231
+ if (resolved) return resolved;
232
+ }
233
+ if (!source.startsWith("/") && !source.startsWith(".")) {
234
+ try {
235
+ const resolved = require2.resolve(source, {
236
+ paths: [importer ? path2.dirname(importer) : config.root]
237
+ });
238
+ return resolved;
239
+ } catch {
240
+ return null;
237
241
  }
238
- } catch (err) {
239
- console.error(`[nasti] Transform error: ${url}`, err.message);
240
- res.statusCode = 500;
241
- res.end(`Transform error: ${err.message}`);
242
- return;
243
242
  }
243
+ return null;
244
+ },
245
+ load(id) {
246
+ if (!fs2.existsSync(id)) return null;
247
+ if (id.endsWith(".json")) {
248
+ const content = fs2.readFileSync(id, "utf-8");
249
+ return `export default ${content}`;
250
+ }
251
+ return fs2.readFileSync(id, "utf-8");
244
252
  }
245
- next();
246
253
  };
247
254
  }
248
- async function transformRequest(url, ctx) {
249
- const { config, pluginContainer, moduleGraph } = ctx;
250
- const cleanReqUrl = url.split("?")[0];
251
- const cached = moduleGraph.getModuleByUrl(url);
252
- if (cached?.transformResult) {
253
- return cached.transformResult;
254
- }
255
- if (cleanReqUrl === "/@react-refresh") {
256
- return { code: REACT_REFRESH_RUNTIME };
257
- }
258
- const filePath = resolveUrlToFile(url, config.root);
259
- if (!filePath || !fs7.existsSync(filePath)) return null;
260
- const mod = await moduleGraph.ensureEntryFromUrl(url);
261
- moduleGraph.registerModule(mod, filePath);
262
- if (cleanReqUrl.startsWith("/@modules/")) {
263
- const code2 = await bundlePackageAsEsm(filePath);
264
- const transformResult2 = { code: code2 };
265
- mod.transformResult = transformResult2;
266
- return transformResult2;
255
+ function tryResolveFile(file, extensions) {
256
+ if (fs2.existsSync(file) && fs2.statSync(file).isFile()) {
257
+ return file;
267
258
  }
268
- let code = fs7.readFileSync(filePath, "utf-8");
269
- const pluginResult = await pluginContainer.transform(code, filePath);
270
- if (pluginResult) {
271
- code = typeof pluginResult === "string" ? pluginResult : pluginResult.code;
259
+ for (const ext of extensions) {
260
+ const withExt = file + ext;
261
+ if (fs2.existsSync(withExt) && fs2.statSync(withExt).isFile()) {
262
+ return withExt;
263
+ }
272
264
  }
273
- if (shouldTransform(filePath)) {
274
- const isJsx = /\.[jt]sx$/.test(filePath);
275
- const useRefresh = isJsx && config.framework !== "vue";
276
- const result = transformCode(filePath, code, {
277
- sourcemap: true,
278
- jsxRuntime: "automatic",
279
- jsxImportSource: config.framework === "vue" ? "vue" : "react",
280
- reactRefresh: useRefresh
281
- });
282
- code = result.code;
283
- if (useRefresh) {
284
- code = REACT_REFRESH_PREAMBLE + code + REACT_REFRESH_FOOTER;
265
+ if (fs2.existsSync(file) && fs2.statSync(file).isDirectory()) {
266
+ for (const ext of extensions) {
267
+ const indexFile = path2.join(file, "index" + ext);
268
+ if (fs2.existsSync(indexFile)) {
269
+ return indexFile;
270
+ }
285
271
  }
286
272
  }
287
- const envDefine = ctx.envDefine ?? buildEnvDefine(
288
- loadEnv(config.mode, config.root, config.envPrefix),
289
- config.mode
290
- );
291
- code = replaceEnvInCode(code, envDefine);
292
- code = rewriteImports(code, config);
293
- const transformResult = { code };
294
- mod.transformResult = transformResult;
295
- return transformResult;
273
+ return null;
296
274
  }
297
- async function bundlePackageAsEsm(entryFile) {
298
- if (!esmBundleCache.has(entryFile)) {
299
- esmBundleCache.set(entryFile, doBundlePackage(entryFile));
275
+ var init_resolve = __esm({
276
+ "src/plugins/resolve.ts"() {
277
+ "use strict";
300
278
  }
301
- return esmBundleCache.get(entryFile);
279
+ });
280
+
281
+ // src/plugins/css.ts
282
+ import path3 from "path";
283
+ function cssPlugin(config) {
284
+ return {
285
+ name: "nasti:css",
286
+ resolveId(source) {
287
+ if (source.endsWith(".css")) return null;
288
+ return null;
289
+ },
290
+ transform(code, id) {
291
+ if (!id.endsWith(".css")) return null;
292
+ const rewritten = rewriteCssUrls(code, id, config.root);
293
+ if (config.command === "serve") {
294
+ const escaped = JSON.stringify(rewritten);
295
+ return {
296
+ code: `
297
+ const css = ${escaped};
298
+ const __nasti_css_id__ = ${JSON.stringify(id)};
299
+ const __nasti_existing__ = document.querySelector('style[data-nasti-css=' + JSON.stringify(__nasti_css_id__) + ']');
300
+ if (__nasti_existing__) __nasti_existing__.remove();
301
+ const style = document.createElement('style');
302
+ style.setAttribute('data-nasti-css', __nasti_css_id__);
303
+ style.textContent = css;
304
+ document.head.appendChild(style);
305
+
306
+ // HMR
307
+ if (import.meta.hot) {
308
+ import.meta.hot.accept();
309
+ import.meta.hot.prune(() => {
310
+ style.remove();
311
+ });
302
312
  }
303
- async function doBundlePackage(entryFile) {
304
- const { rolldown: rolldown2 } = await import("rolldown");
305
- const bundle = await rolldown2({
306
- input: entryFile,
307
- // 仅将其他 npm 包外部化;相对路径(包内部文件)全部内联打包
308
- external: (id) => {
309
- if (id.startsWith(".") || id.startsWith("/") || /^[A-Za-z]:\\/.test(id)) return false;
310
- return true;
313
+
314
+ export default css;
315
+ `
316
+ };
317
+ }
318
+ return rewritten !== code ? { code: rewritten } : null;
319
+ }
320
+ };
321
+ }
322
+ function rewriteCssUrls(css, from, root) {
323
+ return css.replace(/url\(\s*['"]?([^'")\s]+)['"]?\s*\)/g, (match, url) => {
324
+ if (url.startsWith("/") || url.startsWith("data:") || url.startsWith("http")) {
325
+ return match;
311
326
  }
327
+ const resolved = path3.resolve(path3.dirname(from), url);
328
+ const relative = "/" + path3.relative(root, resolved);
329
+ return `url(${relative})`;
312
330
  });
313
- const result = await bundle.generate({ format: "esm", exports: "named" });
314
- await bundle.close();
315
- let code = result.output[0].code;
316
- code = code.replace(/process\.env\.NODE_ENV/g, '"development"');
317
- code = code.replace(
318
- /^(import\b[^;'"]*?\bfrom\s+)(['"])([^'"./][^'"]*)(\2)/gm,
319
- (_, prefix, q, spec) => `${prefix}${q}/@modules/${spec}${q}`
320
- ).replace(
321
- /^(export\b[^;'"]*?\bfrom\s+)(['"])([^'"./][^'"]*)(\2)/gm,
322
- (_, prefix, q, spec) => `${prefix}${q}/@modules/${spec}${q}`
323
- ).replace(
324
- /^(import\s+)(['"])([^'"./][^'"]*)(\2)/gm,
325
- (_, prefix, q, spec) => `${prefix}${q}/@modules/${spec}${q}`
326
- );
327
- code = rewriteExternalRequires(code);
328
- if (code.includes("__commonJSMin")) {
329
- code = await injectCjsNamedExports(code, entryFile);
330
- }
331
- return code;
332
331
  }
333
- function rewriteExternalRequires(code) {
334
- const pkgs = /* @__PURE__ */ new Set();
335
- const re = /__require\(["']([^"']+)["']\)/g;
336
- let m;
337
- while ((m = re.exec(code)) !== null) {
338
- pkgs.add(m[1]);
339
- }
340
- if (pkgs.size === 0) return code;
341
- let result = code;
342
- const imports = [];
343
- for (const pkg of pkgs) {
344
- const safe = pkg.replace(/[^a-zA-Z0-9_$]/g, "_");
345
- imports.push(`import __req_${safe} from "/@modules/${pkg}";`);
346
- result = result.replaceAll(`__require("${pkg}")`, `__req_${safe}`);
347
- result = result.replaceAll(`__require('${pkg}')`, `__req_${safe}`);
332
+ var init_css = __esm({
333
+ "src/plugins/css.ts"() {
334
+ "use strict";
348
335
  }
349
- return imports.join("\n") + "\n" + result;
350
- }
351
- async function injectCjsNamedExports(code, entryFile) {
352
- try {
353
- const { createRequire: createRequire2 } = await import("module");
354
- const req = createRequire2(entryFile);
355
- const cjsExports = req(entryFile);
356
- if (!cjsExports || typeof cjsExports !== "object" && typeof cjsExports !== "function" || Array.isArray(cjsExports)) return code;
357
- const namedKeys = Object.keys(cjsExports).filter(
358
- (k) => k !== "__esModule" && k !== "default" && VALID_IDENT.test(k)
359
- );
360
- if (namedKeys.length === 0) return code;
361
- return code.replace(
362
- /^export default (\w+\(\));?\s*$/m,
363
- (_, call) => [
364
- `const __cjsMod = ${call};`,
365
- `export default __cjsMod;`,
366
- ...namedKeys.map((k) => `export const ${k} = __cjsMod[${JSON.stringify(k)}];`)
367
- ].join("\n")
368
- );
369
- } catch {
370
- return code;
371
- }
372
- }
373
- function rewriteImports(code, _config) {
374
- return code.replace(
375
- /\bfrom\s+(['"])([^'"./][^'"]*)\1/g,
376
- (match, quote, specifier) => {
377
- return `from ${quote}/@modules/${specifier}${quote}`;
378
- }
379
- ).replace(
380
- // 处理纯副作用导入: import 'bare-specifier'
381
- /\bimport\s+(['"])([^'"./][^'"]*)\1/g,
382
- (match, quote, specifier) => {
383
- return `import ${quote}/@modules/${specifier}${quote}`;
384
- }
385
- ).replace(
386
- // 处理动态导入: import('bare-specifier')
387
- /\bimport\s*\(\s*(['"])([^'"./][^'"]*)\1\s*\)/g,
388
- (match, quote, specifier) => {
389
- return `import(${quote}/@modules/${specifier}${quote})`;
390
- }
391
- );
392
- }
393
- function resolveNodeModule(root, moduleName) {
394
- let pkgName;
395
- let subpath;
396
- if (moduleName.startsWith("@")) {
397
- const parts = moduleName.split("/");
398
- pkgName = parts.slice(0, 2).join("/");
399
- subpath = parts.slice(2).join("/");
400
- } else {
401
- const slash = moduleName.indexOf("/");
402
- pkgName = slash === -1 ? moduleName : moduleName.slice(0, slash);
403
- subpath = slash === -1 ? "" : moduleName.slice(slash + 1);
404
- }
405
- let pkgDir = null;
406
- let dir = root;
407
- for (; ; ) {
408
- const candidate = path8.join(dir, "node_modules", pkgName);
409
- if (fs7.existsSync(candidate)) {
410
- pkgDir = candidate;
411
- break;
412
- }
413
- const parent = path8.dirname(dir);
414
- if (parent === dir) break;
415
- dir = parent;
416
- }
417
- if (!pkgDir) return null;
418
- const pkgJsonPath = path8.join(pkgDir, "package.json");
419
- if (!fs7.existsSync(pkgJsonPath)) return null;
420
- let pkg;
421
- try {
422
- pkg = JSON.parse(fs7.readFileSync(pkgJsonPath, "utf-8"));
423
- } catch {
424
- return null;
425
- }
426
- if (pkg.exports) {
427
- const exportKey = subpath ? `./${subpath}` : ".";
428
- const resolved = resolvePackageExports(pkg.exports, exportKey, pkgDir);
429
- if (resolved) return resolved;
430
- }
431
- if (subpath) {
432
- const subDirs = [""];
433
- for (const field of ["module", "main"]) {
434
- if (typeof pkg[field] === "string") {
435
- const dir2 = path8.dirname(pkg[field]);
436
- if (dir2 && dir2 !== "." && !subDirs.includes(dir2)) subDirs.push(dir2);
336
+ });
337
+
338
+ // src/plugins/assets.ts
339
+ import path4 from "path";
340
+ import fs3 from "fs";
341
+ import crypto from "crypto";
342
+ function assetsPlugin(config) {
343
+ return {
344
+ name: "nasti:assets",
345
+ resolveId(source) {
346
+ if (source.endsWith("?url") || source.endsWith("?raw")) {
347
+ return source;
437
348
  }
438
- }
439
- for (const dir2 of subDirs) {
440
- const direct = path8.join(pkgDir, dir2, subpath);
441
- if (fs7.existsSync(direct) && fs7.statSync(direct).isFile()) return direct;
442
- for (const ext of RESOLVE_EXTENSIONS) {
443
- if (fs7.existsSync(direct + ext)) return direct + ext;
349
+ return null;
350
+ },
351
+ load(id) {
352
+ const ext = path4.extname(id.replace(/\?.*$/, ""));
353
+ if (id.endsWith("?raw")) {
354
+ const file = id.slice(0, -4);
355
+ if (fs3.existsSync(file)) {
356
+ const content = fs3.readFileSync(file, "utf-8");
357
+ return `export default ${JSON.stringify(content)}`;
358
+ }
444
359
  }
360
+ if (id.endsWith("?url") || ASSET_EXTENSIONS.has(ext)) {
361
+ const file = id.replace(/\?.*$/, "");
362
+ if (!fs3.existsSync(file)) return null;
363
+ if (config.command === "serve") {
364
+ const url = "/" + path4.relative(config.root, file);
365
+ return `export default ${JSON.stringify(url)}`;
366
+ }
367
+ const content = fs3.readFileSync(file);
368
+ const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
369
+ const basename = path4.basename(file, ext);
370
+ const hashedName = `${config.build.assetsDir}/${basename}.${hash}${ext}`;
371
+ return `export default ${JSON.stringify(config.base + hashedName)}`;
372
+ }
373
+ return null;
445
374
  }
446
- return null;
447
- }
448
- for (const field of ["module", "jsnext:main", "jsnext", "main"]) {
449
- if (typeof pkg[field] === "string") {
450
- const entry = path8.join(pkgDir, pkg[field]);
451
- if (fs7.existsSync(entry)) return entry;
452
- }
453
- }
454
- const indexFallback = path8.join(pkgDir, "index.js");
455
- if (fs7.existsSync(indexFallback)) return indexFallback;
456
- return null;
457
- }
458
- function resolvePackageExports(exports, key, pkgDir) {
459
- if (typeof exports === "string") {
460
- return key === "." ? path8.join(pkgDir, exports) : null;
461
- }
462
- const entry = exports[key];
463
- if (entry === void 0) return null;
464
- return resolveExportValue(entry, pkgDir);
375
+ };
465
376
  }
466
- function resolveExportValue(value, pkgDir) {
467
- if (typeof value === "string") return path8.join(pkgDir, value);
468
- if (Array.isArray(value)) {
469
- for (const item of value) {
470
- const r = resolveExportValue(item, pkgDir);
471
- if (r) return r;
472
- }
473
- return null;
377
+ var ASSET_EXTENSIONS;
378
+ var init_assets = __esm({
379
+ "src/plugins/assets.ts"() {
380
+ "use strict";
381
+ ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
382
+ ".png",
383
+ ".jpg",
384
+ ".jpeg",
385
+ ".gif",
386
+ ".svg",
387
+ ".ico",
388
+ ".webp",
389
+ ".avif",
390
+ ".mp4",
391
+ ".webm",
392
+ ".ogg",
393
+ ".mp3",
394
+ ".wav",
395
+ ".flac",
396
+ ".aac",
397
+ ".woff",
398
+ ".woff2",
399
+ ".eot",
400
+ ".ttf",
401
+ ".otf",
402
+ ".pdf",
403
+ ".txt"
404
+ ]);
474
405
  }
475
- if (value && typeof value === "object") {
476
- for (const cond of ESM_CONDITIONS) {
477
- if (cond in value) {
478
- const r = resolveExportValue(value[cond], pkgDir);
479
- if (r) return r;
406
+ });
407
+
408
+ // src/plugins/html.ts
409
+ import path5 from "path";
410
+ import fs4 from "fs";
411
+ function htmlPlugin(config) {
412
+ return {
413
+ name: "nasti:html",
414
+ enforce: "post",
415
+ transformIndexHtml(html) {
416
+ const tags = [];
417
+ if (config.command === "serve") {
418
+ tags.push({
419
+ tag: "script",
420
+ attrs: { type: "module", src: "/@nasti/client" },
421
+ injectTo: "head-prepend"
422
+ });
480
423
  }
424
+ return { html, tags };
481
425
  }
482
- }
483
- return null;
426
+ };
484
427
  }
485
- function resolveUrlToFile(url, root) {
486
- const cleanUrl = url.split("?")[0];
487
- if (cleanUrl.startsWith("/@modules/")) {
488
- const moduleName = cleanUrl.slice("/@modules/".length);
489
- return resolveNodeModule(root, moduleName);
428
+ function processHtml(html, tags) {
429
+ const headPrepend = tags.filter((t) => t.injectTo === "head-prepend");
430
+ const head = tags.filter((t) => t.injectTo === "head" || !t.injectTo);
431
+ const bodyPrepend = tags.filter((t) => t.injectTo === "body-prepend");
432
+ const body = tags.filter((t) => t.injectTo === "body");
433
+ if (headPrepend.length) {
434
+ html = html.replace(/<head([^>]*)>/i, `<head$1>
435
+ ${serializeTags(headPrepend)}`);
490
436
  }
491
- const filePath = path8.resolve(root, cleanUrl.replace(/^\//, ""));
492
- if (fs7.existsSync(filePath) && fs7.statSync(filePath).isFile()) {
493
- return filePath;
437
+ if (head.length) {
438
+ html = html.replace(/<\/head>/i, `${serializeTags(head)}
439
+ </head>`);
494
440
  }
495
- for (const ext of RESOLVE_EXTENSIONS) {
496
- const withExt = filePath + ext;
497
- if (fs7.existsSync(withExt)) return withExt;
441
+ if (bodyPrepend.length) {
442
+ html = html.replace(/<body([^>]*)>/i, `<body$1>
443
+ ${serializeTags(bodyPrepend)}`);
498
444
  }
499
- for (const ext of RESOLVE_EXTENSIONS) {
500
- const indexFile = path8.join(filePath, "index" + ext);
501
- if (fs7.existsSync(indexFile)) return indexFile;
445
+ if (body.length) {
446
+ html = html.replace(/<\/body>/i, `${serializeTags(body)}
447
+ </body>`);
502
448
  }
503
- return null;
449
+ return html;
504
450
  }
505
- function isModuleRequest(url) {
506
- const cleanUrl = url.split("?")[0];
507
- if (/\.(ts|tsx|jsx|js|mjs|vue|css|json)$/.test(cleanUrl)) return true;
508
- if (cleanUrl.startsWith("/@modules/")) return true;
509
- if (!path8.extname(cleanUrl)) return true;
510
- return false;
451
+ function serializeTags(tags) {
452
+ return tags.map(serializeTag).join("\n");
511
453
  }
512
- function getHmrClientCode() {
513
- return `
514
- // Nasti HMR Client
515
- const socket = new WebSocket(\`ws://\${location.host}\`, 'nasti-hmr');
516
- const hotModulesMap = new Map();
517
-
518
- socket.addEventListener('message', ({ data }) => {
519
- const payload = JSON.parse(data);
520
- switch (payload.type) {
521
- case 'connected':
522
- console.log('[nasti] connected.');
523
- break;
524
- case 'update':
525
- payload.updates.forEach((update) => {
526
- if (update.type === 'js-update') {
527
- fetchUpdate(update);
528
- } else if (update.type === 'css-update') {
529
- updateCss(update.path);
530
- }
531
- });
532
- break;
533
- case 'full-reload':
534
- console.log('[nasti] full reload');
535
- location.reload();
536
- break;
537
- case 'error':
538
- console.error('[nasti] error:', payload.err.message);
539
- showErrorOverlay(payload.err);
540
- break;
454
+ function serializeTag(tag) {
455
+ const attrs = tag.attrs ? " " + Object.entries(tag.attrs).map(([k, v]) => v === true ? k : `${k}="${v}"`).join(" ") : "";
456
+ const children = typeof tag.children === "string" ? tag.children : tag.children ? serializeTags(tag.children) : "";
457
+ const selfClosing = ["link", "meta", "br", "hr", "img", "input"].includes(tag.tag);
458
+ if (selfClosing && !children) {
459
+ return ` <${tag.tag}${attrs} />`;
460
+ }
461
+ return ` <${tag.tag}${attrs}>${children}</${tag.tag}>`;
462
+ }
463
+ async function readHtmlFile(root) {
464
+ const htmlPath = path5.resolve(root, "index.html");
465
+ if (!fs4.existsSync(htmlPath)) return null;
466
+ return fs4.readFileSync(htmlPath, "utf-8");
467
+ }
468
+ var init_html = __esm({
469
+ "src/plugins/html.ts"() {
470
+ "use strict";
541
471
  }
542
472
  });
543
473
 
544
- async function fetchUpdate(update) {
545
- const mod = hotModulesMap.get(update.path);
546
- if (mod) {
547
- const newMod = await import(update.acceptedPath + '?t=' + update.timestamp);
548
- mod.callbacks.forEach((cb) => cb(newMod));
549
- } else {
550
- // \u6CA1\u6709\u6CE8\u518C hot \u56DE\u8C03\uFF0C\u5C1D\u8BD5\u91CD\u65B0 import
551
- await import(update.path + '?t=' + update.timestamp);
552
- }
474
+ // src/core/transformer.ts
475
+ import { transformSync } from "oxc-transform";
476
+ function shouldTransform(id) {
477
+ return TS_EXTENSIONS.test(id) || JSX_EXTENSIONS.test(id) || JS_EXTENSIONS.test(id) && false;
553
478
  }
554
-
555
- function updateCss(path) {
556
- const el = document.querySelector(\`style[data-nasti-css="\${path}"]\`);
557
- if (el) {
558
- fetch(path + '?t=' + Date.now())
559
- .then(r => r.text())
560
- .then(css => { el.textContent = css; });
479
+ function transformCode(filename, code, options = {}) {
480
+ const isTS = TS_EXTENSIONS.test(filename) || /\.tsx$/.test(filename);
481
+ const isJSX = JSX_EXTENSIONS.test(filename);
482
+ const result = transformSync(filename, code, {
483
+ typescript: isTS ? {} : void 0,
484
+ jsx: isJSX || /\.tsx$/.test(filename) ? {
485
+ runtime: options.jsxRuntime ?? "automatic",
486
+ importSource: options.jsxImportSource ?? "react",
487
+ refresh: options.reactRefresh ?? false
488
+ } : void 0,
489
+ sourcemap: options.sourcemap ?? true
490
+ });
491
+ if (result.errors && result.errors.length > 0) {
492
+ const msg = result.errors.map((e) => e.message ?? String(e)).join("\n");
493
+ throw new Error(`OXC transform failed for ${filename}:
494
+ ${msg}`);
561
495
  }
496
+ return {
497
+ code: result.code,
498
+ map: result.map ? JSON.stringify(result.map) : null
499
+ };
562
500
  }
563
-
564
- function showErrorOverlay(err) {
565
- const overlay = document.createElement('div');
566
- overlay.id = 'nasti-error-overlay';
567
- 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;';
568
- const title = document.createElement('h2');
569
- title.style.color = '#ff5555';
570
- title.textContent = 'Build Error';
571
- const pre = document.createElement('pre');
572
- pre.textContent = err.message + '\\n' + (err.stack || '');
573
- const btn = document.createElement('button');
574
- btn.style.cssText = 'margin-top:1rem;padding:0.5rem 1rem;cursor:pointer';
575
- btn.textContent = 'Close';
576
- btn.onclick = () => overlay.remove();
577
- overlay.appendChild(title);
578
- overlay.appendChild(pre);
579
- overlay.appendChild(btn);
580
- document.body.appendChild(overlay);
581
- }
582
-
583
- // import.meta.hot API
584
- const createHotContext = (ownerPath) => ({
585
- accept(deps, callback) {
586
- if (typeof deps === 'function' || !deps) {
587
- // self-accepting
588
- const callbacks = hotModulesMap.get(ownerPath)?.callbacks || [];
589
- callbacks.push(deps || (() => {}));
590
- hotModulesMap.set(ownerPath, { callbacks });
591
- }
592
- },
593
- prune(callback) {
594
- // \u6A21\u5757\u88AB\u79FB\u9664\u65F6\u6267\u884C
595
- },
596
- dispose(callback) {
597
- // \u6A21\u5757\u66F4\u65B0\u524D\u6267\u884C\u6E05\u7406
598
- },
599
- invalidate() {
600
- location.reload();
601
- },
602
- data: {},
603
- });
604
-
605
- // \u66B4\u9732\u7ED9\u6A21\u5757\u4F7F\u7528
606
- if (!window.__nasti_hot_map) window.__nasti_hot_map = new Map();
607
- window.__NASTI_HMR__ = { createHotContext };
608
- `;
609
- }
610
- var REACT_REFRESH_RUNTIME, REACT_REFRESH_PREAMBLE, REACT_REFRESH_FOOTER, esmBundleCache, VALID_IDENT, RESOLVE_EXTENSIONS, ESM_CONDITIONS;
611
- var init_middleware = __esm({
612
- "src/server/middleware.ts"() {
501
+ var JS_EXTENSIONS, TS_EXTENSIONS, JSX_EXTENSIONS;
502
+ var init_transformer = __esm({
503
+ "src/core/transformer.ts"() {
613
504
  "use strict";
614
- init_transformer();
615
- init_html();
616
- init_env();
617
- REACT_REFRESH_RUNTIME = `
618
- export function createSignatureFunctionForTransform() {
619
- return function(type, key, forceReset, getCustomHooks) { return type; };
620
- }
621
- export function register(type, id) {}
622
- export default { createSignatureFunctionForTransform, register };
623
- `;
624
- REACT_REFRESH_PREAMBLE = `
625
- import RefreshRuntime from '/@react-refresh';
626
- if (!window.$RefreshReg$) {
627
- window.$RefreshReg$ = (type, id) => RefreshRuntime.register(type, import.meta.url + ' ' + id);
628
- window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
629
- }
630
- `;
631
- REACT_REFRESH_FOOTER = `
632
- if (import.meta.hot) {
633
- import.meta.hot.accept();
634
- }
635
- `;
636
- esmBundleCache = /* @__PURE__ */ new Map();
637
- VALID_IDENT = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
638
- RESOLVE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".json", ".vue"];
639
- ESM_CONDITIONS = ["import", "browser", "module", "default"];
505
+ JS_EXTENSIONS = /\.(js|mjs|cjs)$/;
506
+ TS_EXTENSIONS = /\.(ts|mts|cts)$/;
507
+ JSX_EXTENSIONS = /\.(jsx|tsx)$/;
640
508
  }
641
509
  });
642
510
 
643
- // src/config/index.ts
644
- import { pathToFileURL } from "url";
645
- import path from "path";
646
- import fs from "fs";
647
-
648
- // src/config/defaults.ts
649
- var defaultResolve = {
650
- alias: {},
651
- extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".vue"],
652
- conditions: ["import", "module", "browser", "default"],
653
- mainFields: ["module", "jsnext:main", "jsnext", "main"]
654
- };
655
- var defaultServer = {
656
- port: 3e3,
657
- host: "localhost",
658
- https: false,
659
- open: false,
660
- proxy: {},
661
- cors: true,
662
- hmr: true
663
- };
664
- var defaultBuild = {
665
- outDir: "dist",
666
- assetsDir: "assets",
667
- minify: true,
668
- sourcemap: false,
669
- target: "es2022",
670
- rolldownOptions: {},
671
- emptyOutDir: true
672
- };
673
- var defaults = {
674
- root: ".",
675
- base: "/",
676
- mode: "development",
677
- framework: "auto",
678
- resolve: defaultResolve,
679
- server: defaultServer,
680
- build: defaultBuild,
681
- plugins: [],
682
- envPrefix: ["NASTI_", "VITE_"],
683
- logLevel: "info"
684
- };
685
-
686
- // src/config/index.ts
687
- function loadTsconfigPaths(root) {
688
- const tsconfigPath = path.resolve(root, "tsconfig.json");
689
- if (!fs.existsSync(tsconfigPath)) return {};
690
- try {
691
- const content = fs.readFileSync(tsconfigPath, "utf-8");
692
- const stripped = content.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
693
- const tsconfig = JSON.parse(stripped);
694
- const paths = tsconfig?.compilerOptions?.paths ?? {};
695
- const baseUrl = tsconfig?.compilerOptions?.baseUrl ?? ".";
696
- const alias = {};
697
- for (const [pattern, targets] of Object.entries(paths)) {
698
- if (!targets.length) continue;
699
- const cleanKey = pattern.replace(/\/\*$/, "");
700
- const cleanTarget = targets[0].replace(/\/\*$/, "");
701
- alias[cleanKey] = path.resolve(root, baseUrl, cleanTarget);
511
+ // src/core/env.ts
512
+ import path6 from "path";
513
+ import fs5 from "fs";
514
+ function loadEnv(mode, root, prefixes) {
515
+ const envFiles = [
516
+ ".env",
517
+ `.env.${mode}`,
518
+ ".env.local",
519
+ `.env.${mode}.local`
520
+ ];
521
+ const raw = {};
522
+ for (const file of envFiles) {
523
+ const filePath = path6.resolve(root, file);
524
+ if (!fs5.existsSync(filePath)) continue;
525
+ const content = fs5.readFileSync(filePath, "utf-8");
526
+ for (const line of content.split("\n")) {
527
+ const trimmed = line.trim();
528
+ if (!trimmed || trimmed.startsWith("#")) continue;
529
+ const eqIdx = trimmed.indexOf("=");
530
+ if (eqIdx === -1) continue;
531
+ const key = trimmed.slice(0, eqIdx).trim();
532
+ let value = trimmed.slice(eqIdx + 1).trim();
533
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
534
+ value = value.slice(1, -1);
535
+ }
536
+ raw[key] = value;
702
537
  }
703
- return alias;
704
- } catch {
705
- return {};
706
538
  }
707
- }
708
- function defineConfig(config) {
709
- return config;
710
- }
711
- var CONFIG_FILES = [
712
- "nasti.config.ts",
713
- "nasti.config.js",
714
- "nasti.config.mjs",
715
- "nasti.config.mts"
716
- ];
717
- async function loadConfigFromFile(root) {
718
- for (const file of CONFIG_FILES) {
719
- const filePath = path.resolve(root, file);
720
- if (!fs.existsSync(filePath)) continue;
721
- if (file.endsWith(".ts") || file.endsWith(".mts")) {
722
- return await loadTsConfig(filePath);
539
+ const filtered = {};
540
+ for (const [key, value] of Object.entries(raw)) {
541
+ if (prefixes.some((prefix) => key.startsWith(prefix))) {
542
+ filtered[key] = value;
723
543
  }
724
- const mod = await import(pathToFileURL(filePath).href);
725
- return mod.default ?? mod;
726
544
  }
727
- return {};
545
+ return filtered;
728
546
  }
729
- async function loadTsConfig(filePath) {
730
- const { transformSync: transformSync2 } = await import("oxc-transform");
731
- const code = fs.readFileSync(filePath, "utf-8");
732
- const result = transformSync2(filePath, code, {
733
- typescript: {}
734
- });
735
- const tmpFile = filePath + ".timestamp-" + Date.now() + ".mjs";
736
- try {
737
- fs.writeFileSync(tmpFile, result.code);
738
- const mod = await import(pathToFileURL(tmpFile).href);
739
- return mod.default ?? mod;
740
- } finally {
741
- fs.unlinkSync(tmpFile);
547
+ function buildEnvDefine(env, mode) {
548
+ const define = {};
549
+ for (const [key, value] of Object.entries(env)) {
550
+ define[`import.meta.env.${key}`] = JSON.stringify(value);
742
551
  }
552
+ define["import.meta.env.MODE"] = JSON.stringify(mode);
553
+ define["import.meta.env.DEV"] = mode !== "production" ? "true" : "false";
554
+ define["import.meta.env.PROD"] = mode === "production" ? "true" : "false";
555
+ define["import.meta.env.SSR"] = "false";
556
+ return define;
743
557
  }
744
- async function resolveConfig(inlineConfig = {}, command) {
745
- const root = path.resolve(inlineConfig.root ?? defaults.root);
746
- const fileConfig = await loadConfigFromFile(root);
747
- const merged = deepMerge(deepMerge({}, fileConfig), inlineConfig);
748
- const rawPlugins = [
749
- ...fileConfig.plugins ?? [],
750
- ...inlineConfig.plugins ?? []
751
- ];
752
- const env = { mode: merged.mode ?? defaults.mode, command };
753
- for (const plugin of rawPlugins) {
754
- if (plugin.config) {
755
- const result = await plugin.config(merged, env);
756
- if (result) Object.assign(merged, result);
757
- }
758
- }
759
- const resolved = {
760
- root,
761
- base: merged.base ?? defaults.base,
762
- mode: command === "build" ? "production" : "development",
763
- framework: merged.framework ?? defaults.framework,
764
- command,
765
- resolve: {
766
- // tsconfig paths 优先级最低:tsconfig < defaults < user config
767
- alias: { ...loadTsconfigPaths(root), ...defaults.resolve.alias, ...merged.resolve?.alias },
768
- extensions: merged.resolve?.extensions ?? defaults.resolve.extensions,
769
- conditions: merged.resolve?.conditions ?? defaults.resolve.conditions,
770
- mainFields: merged.resolve?.mainFields ?? defaults.resolve.mainFields
771
- },
772
- plugins: [],
773
- server: { ...defaults.server, ...merged.server },
774
- build: { ...defaults.build, ...merged.build },
775
- envPrefix: Array.isArray(merged.envPrefix) ? merged.envPrefix : merged.envPrefix ? [merged.envPrefix] : [...defaults.envPrefix],
776
- logLevel: merged.logLevel ?? defaults.logLevel
777
- };
778
- const filteredPlugins = rawPlugins.filter((p) => {
779
- if (!p.apply) return true;
780
- if (typeof p.apply === "function") return p.apply(resolved, env);
781
- return p.apply === command;
782
- });
783
- resolved.plugins = filteredPlugins;
784
- for (const plugin of resolved.plugins) {
785
- if (plugin.configResolved) {
786
- await plugin.configResolved(resolved);
787
- }
788
- }
789
- return resolved;
790
- }
791
- function deepMerge(target, source) {
792
- const result = { ...target };
793
- for (const key of Object.keys(source)) {
794
- const val = source[key];
795
- if (val && typeof val === "object" && !Array.isArray(val)) {
796
- result[key] = deepMerge(
797
- result[key] ?? {},
798
- val
799
- );
800
- } else if (val !== void 0) {
801
- result[key] = val;
802
- }
558
+ function replaceEnvInCode(code, define) {
559
+ let result = code;
560
+ for (const [key, value] of Object.entries(define)) {
561
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
562
+ result = result.replace(new RegExp(escaped, "g"), value);
803
563
  }
804
564
  return result;
805
565
  }
566
+ var init_env = __esm({
567
+ "src/core/env.ts"() {
568
+ "use strict";
569
+ }
570
+ });
806
571
 
807
- // src/build/index.ts
808
- import path7 from "path";
809
- import fs6 from "fs";
810
- import { rolldown } from "rolldown";
811
-
812
- // src/plugins/resolve.ts
813
- import path2 from "path";
814
- import fs2 from "fs";
815
- import { createRequire } from "module";
816
- function resolvePlugin(config) {
817
- const { alias, extensions } = config.resolve;
818
- const require2 = createRequire(path2.resolve(config.root, "package.json"));
819
- return {
820
- name: "nasti:resolve",
821
- enforce: "pre",
822
- resolveId(source, importer) {
823
- for (const [key, value] of Object.entries(alias)) {
824
- if (source === key || source.startsWith(key + "/")) {
825
- source = source.replace(key, value);
826
- if (!path2.isAbsolute(source)) {
827
- source = path2.resolve(config.root, source);
572
+ // src/core/plugin-container.ts
573
+ function sortPlugins(plugins) {
574
+ const pre = [];
575
+ const normal = [];
576
+ const post = [];
577
+ for (const plugin of plugins) {
578
+ if (plugin.enforce === "pre") pre.push(plugin);
579
+ else if (plugin.enforce === "post") post.push(plugin);
580
+ else normal.push(plugin);
581
+ }
582
+ return [...pre, ...normal, ...post];
583
+ }
584
+ var PluginContainer;
585
+ var init_plugin_container = __esm({
586
+ "src/core/plugin-container.ts"() {
587
+ "use strict";
588
+ PluginContainer = class {
589
+ plugins;
590
+ config;
591
+ ctx;
592
+ emittedFiles = /* @__PURE__ */ new Map();
593
+ constructor(config) {
594
+ this.config = config;
595
+ this.plugins = sortPlugins(config.plugins);
596
+ this.ctx = this.createContext();
597
+ }
598
+ createContext() {
599
+ const container = this;
600
+ return {
601
+ async resolve(source, importer) {
602
+ return container.resolveId(source, importer);
603
+ },
604
+ emitFile(file) {
605
+ const fileName = file.fileName ?? file.name ?? `asset-${container.emittedFiles.size}`;
606
+ const id = `emitted:${fileName}`;
607
+ container.emittedFiles.set(id, {
608
+ fileName,
609
+ source: file.source ?? ""
610
+ });
611
+ return id;
612
+ },
613
+ getModuleInfo(_id) {
614
+ return null;
615
+ }
616
+ };
617
+ }
618
+ /** 返回所有通过 emitFile() 输出的文件 */
619
+ getEmittedFiles() {
620
+ return Array.from(this.emittedFiles.values());
621
+ }
622
+ async buildStart() {
623
+ for (const plugin of this.plugins) {
624
+ if (plugin.buildStart) {
625
+ await plugin.buildStart.call(this.ctx);
828
626
  }
829
- break;
830
627
  }
831
628
  }
832
- if (path2.isAbsolute(source)) {
833
- const resolved = tryResolveFile(source, extensions);
834
- if (resolved) return resolved;
629
+ async buildEnd(error) {
630
+ for (const plugin of this.plugins) {
631
+ if (plugin.buildEnd) {
632
+ await plugin.buildEnd.call(this.ctx, error);
633
+ }
634
+ }
835
635
  }
836
- if (source.startsWith(".")) {
837
- const dir = importer ? path2.dirname(importer) : config.root;
838
- const absolute = path2.resolve(dir, source);
839
- const resolved = tryResolveFile(absolute, extensions);
840
- if (resolved) return resolved;
636
+ async resolveId(source, importer, options = {}) {
637
+ for (const plugin of this.plugins) {
638
+ if (!plugin.resolveId) continue;
639
+ const result = await plugin.resolveId.call(
640
+ this.ctx,
641
+ source,
642
+ importer ?? void 0,
643
+ { isEntry: options.isEntry ?? false, ssr: false }
644
+ );
645
+ if (result != null) return result;
646
+ }
647
+ return null;
841
648
  }
842
- if (!source.startsWith("/") && !source.startsWith(".")) {
843
- try {
844
- const resolved = require2.resolve(source, {
845
- paths: [importer ? path2.dirname(importer) : config.root]
846
- });
847
- return resolved;
848
- } catch {
849
- return null;
649
+ async load(id) {
650
+ for (const plugin of this.plugins) {
651
+ if (!plugin.load) continue;
652
+ const result = await plugin.load.call(this.ctx, id);
653
+ if (result != null) return result;
850
654
  }
655
+ return null;
851
656
  }
852
- return null;
853
- },
854
- load(id) {
855
- if (!fs2.existsSync(id)) return null;
856
- if (id.endsWith(".json")) {
857
- const content = fs2.readFileSync(id, "utf-8");
858
- return `export default ${content}`;
657
+ async transform(code, id) {
658
+ let currentCode = code;
659
+ for (const plugin of this.plugins) {
660
+ if (!plugin.transform) continue;
661
+ const result = await plugin.transform.call(this.ctx, currentCode, id);
662
+ if (result == null) continue;
663
+ if (typeof result === "string") {
664
+ currentCode = result;
665
+ } else {
666
+ currentCode = result.code;
667
+ }
668
+ }
669
+ return currentCode === code ? null : { code: currentCode };
859
670
  }
860
- return fs2.readFileSync(id, "utf-8");
861
- }
862
- };
863
- }
864
- function tryResolveFile(file, extensions) {
865
- if (fs2.existsSync(file) && fs2.statSync(file).isFile()) {
866
- return file;
671
+ /** 完整的模块处理管道: resolveId → load → transform */
672
+ async processModule(source, importer) {
673
+ const resolveResult = await this.resolveId(source, importer, {
674
+ isEntry: !importer
675
+ });
676
+ if (resolveResult == null) return null;
677
+ const id = typeof resolveResult === "string" ? resolveResult : resolveResult.id;
678
+ const loadResult = await this.load(id);
679
+ if (loadResult == null) return null;
680
+ const loadedCode = typeof loadResult === "string" ? loadResult : loadResult.code;
681
+ const transformResult = await this.transform(loadedCode, id);
682
+ const finalCode = transformResult == null ? loadedCode : typeof transformResult === "string" ? transformResult : transformResult.code;
683
+ return { id, code: finalCode };
684
+ }
685
+ getPlugins() {
686
+ return this.plugins;
687
+ }
688
+ };
867
689
  }
868
- for (const ext of extensions) {
869
- const withExt = file + ext;
870
- if (fs2.existsSync(withExt) && fs2.statSync(withExt).isFile()) {
871
- return withExt;
690
+ });
691
+
692
+ // src/build/index.ts
693
+ var build_exports = {};
694
+ __export(build_exports, {
695
+ build: () => build
696
+ });
697
+ import path7 from "path";
698
+ import fs6 from "fs";
699
+ import { rolldown } from "rolldown";
700
+ import pc from "picocolors";
701
+ async function build(inlineConfig = {}) {
702
+ const config = await resolveConfig(inlineConfig, "build");
703
+ const startTime = performance.now();
704
+ console.log(pc.cyan("\n\u{1F528} nasti build") + pc.dim(` v${"1.4.0"}`));
705
+ console.log(pc.dim(` root: ${config.root}`));
706
+ console.log(pc.dim(` mode: ${config.mode}`));
707
+ const outDir = path7.resolve(config.root, config.build.outDir);
708
+ if (config.build.emptyOutDir && fs6.existsSync(outDir)) {
709
+ fs6.rmSync(outDir, { recursive: true, force: true });
710
+ }
711
+ fs6.mkdirSync(outDir, { recursive: true });
712
+ const html = await readHtmlFile(config.root);
713
+ let entryPoints = [];
714
+ if (html) {
715
+ const scriptMatches = html.matchAll(/<script[^>]+src=["']([^"']+)["'][^>]*>/gi);
716
+ for (const match of scriptMatches) {
717
+ const src = match[1];
718
+ if (src && !src.startsWith("http")) {
719
+ entryPoints.push(path7.resolve(config.root, src.replace(/^\//, "")));
720
+ }
872
721
  }
873
722
  }
874
- if (fs2.existsSync(file) && fs2.statSync(file).isDirectory()) {
875
- for (const ext of extensions) {
876
- const indexFile = path2.join(file, "index" + ext);
877
- if (fs2.existsSync(indexFile)) {
878
- return indexFile;
723
+ if (entryPoints.length === 0) {
724
+ const fallbackEntries = ["src/main.ts", "src/main.tsx", "src/main.js", "src/index.ts", "src/index.tsx", "src/index.js"];
725
+ for (const entry of fallbackEntries) {
726
+ const fullPath = path7.resolve(config.root, entry);
727
+ if (fs6.existsSync(fullPath)) {
728
+ entryPoints.push(fullPath);
729
+ break;
879
730
  }
880
731
  }
881
732
  }
882
- return null;
883
- }
884
-
885
- // src/plugins/css.ts
886
- import path3 from "path";
887
- function cssPlugin(config) {
888
- return {
889
- name: "nasti:css",
890
- resolveId(source) {
891
- if (source.endsWith(".css")) return null;
892
- return null;
893
- },
733
+ if (entryPoints.length === 0) {
734
+ throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
735
+ }
736
+ const builtinPlugins = [
737
+ resolvePlugin(config),
738
+ cssPlugin(config),
739
+ assetsPlugin(config)
740
+ ];
741
+ const allPlugins = [...builtinPlugins, ...config.plugins];
742
+ const pluginContainer = new PluginContainer(config);
743
+ await pluginContainer.buildStart();
744
+ const oxcTransformPlugin = {
745
+ name: "nasti:oxc-transform",
894
746
  transform(code, id) {
895
- if (!id.endsWith(".css")) return null;
896
- const rewritten = rewriteCssUrls(code, id, config.root);
897
- if (config.command === "serve") {
898
- const escaped = JSON.stringify(rewritten);
899
- return {
900
- code: `
901
- const css = ${escaped};
902
- const __nasti_css_id__ = ${JSON.stringify(id)};
903
- const __nasti_existing__ = document.querySelector('style[data-nasti-css=' + JSON.stringify(__nasti_css_id__) + ']');
904
- if (__nasti_existing__) __nasti_existing__.remove();
905
- const style = document.createElement('style');
906
- style.setAttribute('data-nasti-css', __nasti_css_id__);
907
- style.textContent = css;
908
- document.head.appendChild(style);
909
-
910
- // HMR
911
- if (import.meta.hot) {
912
- import.meta.hot.accept();
913
- import.meta.hot.prune(() => {
914
- style.remove();
915
- });
916
- }
917
-
918
- export default css;
919
- `
920
- };
921
- }
922
- return rewritten !== code ? { code: rewritten } : null;
747
+ if (!shouldTransform(id)) return null;
748
+ const result = transformCode(id, code, {
749
+ sourcemap: !!config.build.sourcemap,
750
+ jsxRuntime: "automatic",
751
+ jsxImportSource: config.framework === "vue" ? "vue" : "react"
752
+ });
753
+ return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
923
754
  }
924
755
  };
925
- }
926
- function rewriteCssUrls(css, from, root) {
927
- return css.replace(/url\(\s*['"]?([^'")\s]+)['"]?\s*\)/g, (match, url) => {
928
- if (url.startsWith("/") || url.startsWith("data:") || url.startsWith("http")) {
929
- return match;
930
- }
931
- const resolved = path3.resolve(path3.dirname(from), url);
932
- const relative = "/" + path3.relative(root, resolved);
933
- return `url(${relative})`;
756
+ const env = loadEnv(config.mode, config.root, config.envPrefix);
757
+ const envDefine = buildEnvDefine(env, config.mode);
758
+ const bundle = await rolldown({
759
+ input: entryPoints,
760
+ define: envDefine,
761
+ plugins: [
762
+ oxcTransformPlugin,
763
+ // 转换 Nasti 插件为 Rolldown 插件格式
764
+ ...allPlugins.map((p) => ({
765
+ name: p.name,
766
+ resolveId: p.resolveId,
767
+ load: p.load,
768
+ transform: p.transform,
769
+ buildStart: p.buildStart,
770
+ buildEnd: p.buildEnd
771
+ }))
772
+ ],
773
+ ...config.build.rolldownOptions
934
774
  });
935
- }
936
-
937
- // src/plugins/assets.ts
938
- import path4 from "path";
939
- import fs3 from "fs";
940
- import crypto from "crypto";
941
- var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
942
- ".png",
943
- ".jpg",
944
- ".jpeg",
945
- ".gif",
946
- ".svg",
947
- ".ico",
948
- ".webp",
949
- ".avif",
950
- ".mp4",
951
- ".webm",
952
- ".ogg",
953
- ".mp3",
954
- ".wav",
955
- ".flac",
956
- ".aac",
957
- ".woff",
958
- ".woff2",
959
- ".eot",
960
- ".ttf",
961
- ".otf",
962
- ".pdf",
963
- ".txt"
964
- ]);
965
- function assetsPlugin(config) {
966
- return {
967
- name: "nasti:assets",
968
- resolveId(source) {
969
- if (source.endsWith("?url") || source.endsWith("?raw")) {
970
- return source;
971
- }
972
- return null;
973
- },
974
- load(id) {
975
- const ext = path4.extname(id.replace(/\?.*$/, ""));
976
- if (id.endsWith("?raw")) {
977
- const file = id.slice(0, -4);
978
- if (fs3.existsSync(file)) {
979
- const content = fs3.readFileSync(file, "utf-8");
980
- return `export default ${JSON.stringify(content)}`;
775
+ const { output } = await bundle.write({
776
+ dir: outDir,
777
+ format: "esm",
778
+ sourcemap: !!config.build.sourcemap,
779
+ minify: !!config.build.minify,
780
+ entryFileNames: "assets/[name].[hash].js",
781
+ chunkFileNames: "assets/[name].[hash].js",
782
+ assetFileNames: "assets/[name].[hash][extname]"
783
+ });
784
+ await bundle.close();
785
+ await pluginContainer.buildEnd();
786
+ for (const ef of pluginContainer.getEmittedFiles()) {
787
+ const dest = path7.resolve(outDir, ef.fileName);
788
+ fs6.mkdirSync(path7.dirname(dest), { recursive: true });
789
+ fs6.writeFileSync(dest, ef.source);
790
+ }
791
+ if (html) {
792
+ let processedHtml = html;
793
+ const htmlPlugin_ = htmlPlugin(config);
794
+ if (htmlPlugin_.transformIndexHtml) {
795
+ const result = await htmlPlugin_.transformIndexHtml(processedHtml);
796
+ if (typeof result === "string") {
797
+ processedHtml = result;
798
+ } else if (result && "html" in result) {
799
+ processedHtml = processHtml(result.html, result.tags);
800
+ } else if (Array.isArray(result)) {
801
+ processedHtml = processHtml(processedHtml, result);
802
+ }
803
+ }
804
+ for (const chunk of output) {
805
+ if (chunk.type === "chunk" && chunk.isEntry && chunk.facadeModuleId) {
806
+ const originalEntry = path7.relative(config.root, chunk.facadeModuleId);
807
+ processedHtml = processedHtml.replace(
808
+ new RegExp(`(src=["'])/?(${escapeRegExp(originalEntry)})(["'])`, "g"),
809
+ `$1${config.base}${chunk.fileName}$3`
810
+ );
811
+ }
812
+ }
813
+ fs6.writeFileSync(path7.resolve(outDir, "index.html"), processedHtml);
814
+ }
815
+ const elapsed = ((performance.now() - startTime) / 1e3).toFixed(2);
816
+ const totalSize = output.reduce((sum, chunk) => {
817
+ if (chunk.type === "chunk" && chunk.code) return sum + chunk.code.length;
818
+ return sum;
819
+ }, 0);
820
+ console.log(pc.green(`
821
+ \u2713 Built in ${elapsed}s`));
822
+ console.log(pc.dim(` ${output.length} files, ${formatSize(totalSize)} total`));
823
+ console.log(pc.dim(` output: ${config.build.outDir}/
824
+ `));
825
+ return { output };
826
+ }
827
+ function formatSize(bytes) {
828
+ if (bytes < 1024) return `${bytes} B`;
829
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} kB`;
830
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
831
+ }
832
+ function escapeRegExp(string) {
833
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
834
+ }
835
+ var init_build = __esm({
836
+ "src/build/index.ts"() {
837
+ "use strict";
838
+ init_config();
839
+ init_resolve();
840
+ init_css();
841
+ init_assets();
842
+ init_html();
843
+ init_transformer();
844
+ init_env();
845
+ init_plugin_container();
846
+ }
847
+ });
848
+
849
+ // src/core/module-graph.ts
850
+ var ModuleGraph;
851
+ var init_module_graph = __esm({
852
+ "src/core/module-graph.ts"() {
853
+ "use strict";
854
+ ModuleGraph = class {
855
+ urlToModuleMap = /* @__PURE__ */ new Map();
856
+ idToModuleMap = /* @__PURE__ */ new Map();
857
+ fileToModulesMap = /* @__PURE__ */ new Map();
858
+ getModuleByUrl(url) {
859
+ return this.urlToModuleMap.get(url);
860
+ }
861
+ getModuleById(id) {
862
+ return this.idToModuleMap.get(id);
863
+ }
864
+ getModulesByFile(file) {
865
+ return this.fileToModulesMap.get(file);
866
+ }
867
+ async ensureEntryFromUrl(url) {
868
+ let mod = this.urlToModuleMap.get(url);
869
+ if (mod) return mod;
870
+ mod = this.createModule(url);
871
+ this.urlToModuleMap.set(url, mod);
872
+ return mod;
873
+ }
874
+ createModule(url, id) {
875
+ const mod = {
876
+ id: id ?? url,
877
+ file: null,
878
+ url,
879
+ type: url.endsWith(".css") ? "css" : "js",
880
+ importers: /* @__PURE__ */ new Set(),
881
+ importedModules: /* @__PURE__ */ new Set(),
882
+ acceptedHmrDeps: /* @__PURE__ */ new Set(),
883
+ transformResult: null,
884
+ lastHMRTimestamp: 0,
885
+ isSelfAccepting: false
886
+ };
887
+ this.idToModuleMap.set(mod.id, mod);
888
+ return mod;
889
+ }
890
+ /** 注册文件路径到模块的映射 */
891
+ registerModule(mod, file) {
892
+ mod.file = file;
893
+ let mods = this.fileToModulesMap.get(file);
894
+ if (!mods) {
895
+ mods = /* @__PURE__ */ new Set();
896
+ this.fileToModulesMap.set(file, mods);
981
897
  }
898
+ mods.add(mod);
982
899
  }
983
- if (id.endsWith("?url") || ASSET_EXTENSIONS.has(ext)) {
984
- const file = id.replace(/\?.*$/, "");
985
- if (!fs3.existsSync(file)) return null;
986
- if (config.command === "serve") {
987
- const url = "/" + path4.relative(config.root, file);
988
- return `export default ${JSON.stringify(url)}`;
900
+ /** 更新模块依赖关系 */
901
+ updateModuleImports(mod, importedIds) {
902
+ for (const imported of mod.importedModules) {
903
+ imported.importers.delete(mod);
904
+ }
905
+ mod.importedModules.clear();
906
+ for (const id of importedIds) {
907
+ const importedMod = this.idToModuleMap.get(id);
908
+ if (importedMod) {
909
+ mod.importedModules.add(importedMod);
910
+ importedMod.importers.add(mod);
911
+ }
989
912
  }
990
- const content = fs3.readFileSync(file);
991
- const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
992
- const basename = path4.basename(file, ext);
993
- const hashedName = `${config.build.assetsDir}/${basename}.${hash}${ext}`;
994
- return `export default ${JSON.stringify(config.base + hashedName)}`;
995
913
  }
996
- return null;
914
+ /** 使模块的转换缓存失效 */
915
+ invalidateModule(mod) {
916
+ mod.transformResult = null;
917
+ mod.lastHMRTimestamp = Date.now();
918
+ }
919
+ /** 使所有模块缓存失效 */
920
+ invalidateAll() {
921
+ for (const mod of this.idToModuleMap.values()) {
922
+ this.invalidateModule(mod);
923
+ }
924
+ }
925
+ /** 获取 HMR 传播边界 - 从变更模块向上遍历找到接受更新的边界 */
926
+ getHmrBoundaries(mod) {
927
+ const boundaries = [];
928
+ const visited = /* @__PURE__ */ new Set();
929
+ const propagate = (node, via) => {
930
+ if (visited.has(node)) return true;
931
+ visited.add(node);
932
+ if (node.isSelfAccepting) {
933
+ boundaries.push({ boundary: node, acceptedVia: via });
934
+ return true;
935
+ }
936
+ if (node.acceptedHmrDeps.has(via)) {
937
+ boundaries.push({ boundary: node, acceptedVia: via });
938
+ return true;
939
+ }
940
+ if (node.importers.size === 0) return false;
941
+ for (const importer of node.importers) {
942
+ if (!propagate(importer, node)) return false;
943
+ }
944
+ return true;
945
+ };
946
+ if (mod.isSelfAccepting) {
947
+ boundaries.push({ boundary: mod, acceptedVia: mod });
948
+ return boundaries;
949
+ }
950
+ for (const importer of mod.importers) {
951
+ if (!propagate(importer, mod)) {
952
+ return [];
953
+ }
954
+ }
955
+ return boundaries;
956
+ }
957
+ };
958
+ }
959
+ });
960
+
961
+ // src/server/ws.ts
962
+ import { WebSocketServer as WsServer } from "ws";
963
+ function createWebSocketServer(server) {
964
+ const wss = new WsServer({ noServer: true });
965
+ const clients = /* @__PURE__ */ new Set();
966
+ server.on("upgrade", (req, socket, head) => {
967
+ if (req.headers["sec-websocket-protocol"] === "nasti-hmr") {
968
+ wss.handleUpgrade(req, socket, head, (ws) => {
969
+ wss.emit("connection", ws, req);
970
+ });
971
+ }
972
+ });
973
+ wss.on("connection", (ws) => {
974
+ clients.add(ws);
975
+ ws.send(JSON.stringify({ type: "connected" }));
976
+ ws.on("close", () => {
977
+ clients.delete(ws);
978
+ });
979
+ ws.on("error", (err) => {
980
+ console.error("[nasti] WebSocket error:", err);
981
+ clients.delete(ws);
982
+ });
983
+ });
984
+ return {
985
+ send(payload) {
986
+ const data = JSON.stringify(payload);
987
+ for (const client of clients) {
988
+ if (client.readyState === 1) {
989
+ client.send(data);
990
+ }
991
+ }
992
+ },
993
+ close() {
994
+ clients.clear();
995
+ wss.close();
997
996
  }
998
997
  };
999
998
  }
999
+ var init_ws = __esm({
1000
+ "src/server/ws.ts"() {
1001
+ "use strict";
1002
+ }
1003
+ });
1000
1004
 
1001
- // src/build/index.ts
1002
- init_html();
1003
- init_transformer();
1004
- init_env();
1005
-
1006
- // src/core/plugin-container.ts
1007
- var PluginContainer = class {
1008
- plugins;
1009
- config;
1010
- ctx;
1011
- emittedFiles = /* @__PURE__ */ new Map();
1012
- constructor(config) {
1013
- this.config = config;
1014
- this.plugins = sortPlugins(config.plugins);
1015
- this.ctx = this.createContext();
1016
- }
1017
- createContext() {
1018
- const container = this;
1019
- return {
1020
- async resolve(source, importer) {
1021
- return container.resolveId(source, importer);
1022
- },
1023
- emitFile(file) {
1024
- const fileName = file.fileName ?? file.name ?? `asset-${container.emittedFiles.size}`;
1025
- const id = `emitted:${fileName}`;
1026
- container.emittedFiles.set(id, {
1027
- fileName,
1028
- source: file.source ?? ""
1029
- });
1030
- return id;
1031
- },
1032
- getModuleInfo(_id) {
1033
- return null;
1005
+ // src/server/middleware.ts
1006
+ var middleware_exports = {};
1007
+ __export(middleware_exports, {
1008
+ transformMiddleware: () => transformMiddleware,
1009
+ transformRequest: () => transformRequest
1010
+ });
1011
+ import path9 from "path";
1012
+ import fs8 from "fs";
1013
+ function transformMiddleware(ctx) {
1014
+ ctx.envDefine = buildEnvDefine(
1015
+ loadEnv(ctx.config.mode, ctx.config.root, ctx.config.envPrefix),
1016
+ ctx.config.mode
1017
+ );
1018
+ return async (req, res, next) => {
1019
+ const url = req.url ?? "/";
1020
+ if (ctx.config.server.cors) {
1021
+ const origin = req.headers.origin ?? "*";
1022
+ res.setHeader("Access-Control-Allow-Origin", origin);
1023
+ res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
1024
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1025
+ if (req.method === "OPTIONS") {
1026
+ res.statusCode = 204;
1027
+ res.end();
1028
+ return;
1034
1029
  }
1035
- };
1030
+ }
1031
+ if (req.method !== "GET") return next();
1032
+ if (url === "/@nasti/client") {
1033
+ res.setHeader("Content-Type", "application/javascript");
1034
+ res.end(getHmrClientCode());
1035
+ return;
1036
+ }
1037
+ if (url === "/" || url.endsWith(".html")) {
1038
+ const html = await readHtmlFile(ctx.config.root);
1039
+ if (html) {
1040
+ let processedHtml = html;
1041
+ for (const plugin of ctx.config.plugins) {
1042
+ if (plugin.transformIndexHtml) {
1043
+ const result = await plugin.transformIndexHtml(processedHtml);
1044
+ if (typeof result === "string") {
1045
+ processedHtml = result;
1046
+ } else if (result && "html" in result) {
1047
+ processedHtml = processHtml(result.html, result.tags);
1048
+ } else if (Array.isArray(result)) {
1049
+ processedHtml = processHtml(processedHtml, result);
1050
+ }
1051
+ }
1052
+ }
1053
+ res.setHeader("Content-Type", "text/html");
1054
+ res.end(processedHtml);
1055
+ return;
1056
+ }
1057
+ }
1058
+ if (isModuleRequest(url)) {
1059
+ try {
1060
+ const result = await transformRequest(url, ctx);
1061
+ if (result) {
1062
+ const contentType = url.endsWith(".css") ? "application/javascript" : "application/javascript";
1063
+ res.setHeader("Content-Type", contentType);
1064
+ res.setHeader("Cache-Control", "no-cache");
1065
+ res.end(typeof result === "string" ? result : result.code);
1066
+ return;
1067
+ }
1068
+ } catch (err) {
1069
+ console.error(`[nasti] Transform error: ${url}`, err.message);
1070
+ res.statusCode = 500;
1071
+ res.end(`Transform error: ${err.message}`);
1072
+ return;
1073
+ }
1074
+ }
1075
+ next();
1076
+ };
1077
+ }
1078
+ async function transformRequest(url, ctx) {
1079
+ const { config, pluginContainer, moduleGraph } = ctx;
1080
+ const cleanReqUrl = url.split("?")[0];
1081
+ const cached = moduleGraph.getModuleByUrl(url);
1082
+ if (cached?.transformResult) {
1083
+ return cached.transformResult;
1084
+ }
1085
+ if (cleanReqUrl === "/@react-refresh") {
1086
+ return { code: REACT_REFRESH_RUNTIME };
1087
+ }
1088
+ const filePath = resolveUrlToFile(url, config.root);
1089
+ if (!filePath || !fs8.existsSync(filePath)) return null;
1090
+ const mod = await moduleGraph.ensureEntryFromUrl(url);
1091
+ moduleGraph.registerModule(mod, filePath);
1092
+ if (cleanReqUrl.startsWith("/@modules/")) {
1093
+ const code2 = await bundlePackageAsEsm(filePath);
1094
+ const transformResult2 = { code: code2 };
1095
+ mod.transformResult = transformResult2;
1096
+ return transformResult2;
1097
+ }
1098
+ let code = fs8.readFileSync(filePath, "utf-8");
1099
+ const pluginResult = await pluginContainer.transform(code, filePath);
1100
+ if (pluginResult) {
1101
+ code = typeof pluginResult === "string" ? pluginResult : pluginResult.code;
1102
+ }
1103
+ if (shouldTransform(filePath)) {
1104
+ const isJsx = /\.[jt]sx$/.test(filePath);
1105
+ const useRefresh = isJsx && config.framework !== "vue";
1106
+ const result = transformCode(filePath, code, {
1107
+ sourcemap: true,
1108
+ jsxRuntime: "automatic",
1109
+ jsxImportSource: config.framework === "vue" ? "vue" : "react",
1110
+ reactRefresh: useRefresh
1111
+ });
1112
+ code = result.code;
1113
+ if (useRefresh) {
1114
+ code = REACT_REFRESH_PREAMBLE + code + REACT_REFRESH_FOOTER;
1115
+ }
1116
+ }
1117
+ const envDefine = ctx.envDefine ?? buildEnvDefine(
1118
+ loadEnv(config.mode, config.root, config.envPrefix),
1119
+ config.mode
1120
+ );
1121
+ code = replaceEnvInCode(code, envDefine);
1122
+ code = rewriteImports(code, config);
1123
+ const transformResult = { code };
1124
+ mod.transformResult = transformResult;
1125
+ return transformResult;
1126
+ }
1127
+ async function bundlePackageAsEsm(entryFile) {
1128
+ if (!esmBundleCache.has(entryFile)) {
1129
+ esmBundleCache.set(entryFile, doBundlePackage(entryFile));
1130
+ }
1131
+ return esmBundleCache.get(entryFile);
1132
+ }
1133
+ async function doBundlePackage(entryFile) {
1134
+ const { rolldown: rolldown4 } = await import("rolldown");
1135
+ const bundle = await rolldown4({
1136
+ input: entryFile,
1137
+ // 仅将其他 npm 包外部化;相对路径(包内部文件)全部内联打包
1138
+ external: (id) => {
1139
+ if (id.startsWith(".") || id.startsWith("/") || /^[A-Za-z]:\\/.test(id)) return false;
1140
+ return true;
1141
+ }
1142
+ });
1143
+ const result = await bundle.generate({ format: "esm", exports: "named" });
1144
+ await bundle.close();
1145
+ let code = result.output[0].code;
1146
+ code = code.replace(/process\.env\.NODE_ENV/g, '"development"');
1147
+ code = code.replace(
1148
+ /^(import\b[^;'"]*?\bfrom\s+)(['"])([^'"./][^'"]*)(\2)/gm,
1149
+ (_, prefix, q, spec) => `${prefix}${q}/@modules/${spec}${q}`
1150
+ ).replace(
1151
+ /^(export\b[^;'"]*?\bfrom\s+)(['"])([^'"./][^'"]*)(\2)/gm,
1152
+ (_, prefix, q, spec) => `${prefix}${q}/@modules/${spec}${q}`
1153
+ ).replace(
1154
+ /^(import\s+)(['"])([^'"./][^'"]*)(\2)/gm,
1155
+ (_, prefix, q, spec) => `${prefix}${q}/@modules/${spec}${q}`
1156
+ );
1157
+ code = rewriteExternalRequires(code);
1158
+ if (code.includes("__commonJSMin")) {
1159
+ code = await injectCjsNamedExports(code, entryFile);
1036
1160
  }
1037
- /** 返回所有通过 emitFile() 输出的文件 */
1038
- getEmittedFiles() {
1039
- return Array.from(this.emittedFiles.values());
1161
+ return code;
1162
+ }
1163
+ function rewriteExternalRequires(code) {
1164
+ const pkgs = /* @__PURE__ */ new Set();
1165
+ const re = /__require\(["']([^"']+)["']\)/g;
1166
+ let m;
1167
+ while ((m = re.exec(code)) !== null) {
1168
+ pkgs.add(m[1]);
1169
+ }
1170
+ if (pkgs.size === 0) return code;
1171
+ let result = code;
1172
+ const imports = [];
1173
+ for (const pkg of pkgs) {
1174
+ const safe = pkg.replace(/[^a-zA-Z0-9_$]/g, "_");
1175
+ imports.push(`import * as __ns_${safe} from "/@modules/${pkg}";`);
1176
+ imports.push(`var __req_${safe} = "default" in __ns_${safe} ? __ns_${safe}["default"] : __ns_${safe};`);
1177
+ result = result.replaceAll(`__require("${pkg}")`, `__req_${safe}`);
1178
+ result = result.replaceAll(`__require('${pkg}')`, `__req_${safe}`);
1179
+ }
1180
+ return imports.join("\n") + "\n" + result;
1181
+ }
1182
+ async function injectCjsNamedExports(code, entryFile) {
1183
+ try {
1184
+ const { createRequire: createRequire3 } = await import("module");
1185
+ const req = createRequire3(entryFile);
1186
+ const cjsExports = req(entryFile);
1187
+ if (!cjsExports || typeof cjsExports !== "object" && typeof cjsExports !== "function" || Array.isArray(cjsExports)) return code;
1188
+ const namedKeys = Object.keys(cjsExports).filter(
1189
+ (k) => k !== "__esModule" && k !== "default" && VALID_IDENT.test(k)
1190
+ );
1191
+ if (namedKeys.length === 0) return code;
1192
+ return code.replace(
1193
+ /^export default (\w+\(\));?\s*$/m,
1194
+ (_, call) => [
1195
+ `const __cjsMod = ${call};`,
1196
+ `export default __cjsMod;`,
1197
+ ...namedKeys.map((k) => `export const ${k} = __cjsMod[${JSON.stringify(k)}];`)
1198
+ ].join("\n")
1199
+ );
1200
+ } catch {
1201
+ return code;
1040
1202
  }
1041
- async buildStart() {
1042
- for (const plugin of this.plugins) {
1043
- if (plugin.buildStart) {
1044
- await plugin.buildStart.call(this.ctx);
1203
+ }
1204
+ function rewriteImports(code, _config) {
1205
+ return code.replace(
1206
+ /\bfrom\s+(['"])([^'"./][^'"]*)\1/g,
1207
+ (match, quote, specifier) => {
1208
+ return `from ${quote}/@modules/${specifier}${quote}`;
1209
+ }
1210
+ ).replace(
1211
+ // 处理纯副作用导入: import 'bare-specifier'
1212
+ /\bimport\s+(['"])([^'"./][^'"]*)\1/g,
1213
+ (match, quote, specifier) => {
1214
+ return `import ${quote}/@modules/${specifier}${quote}`;
1215
+ }
1216
+ ).replace(
1217
+ // 处理动态导入: import('bare-specifier')
1218
+ /\bimport\s*\(\s*(['"])([^'"./][^'"]*)\1\s*\)/g,
1219
+ (match, quote, specifier) => {
1220
+ return `import(${quote}/@modules/${specifier}${quote})`;
1221
+ }
1222
+ );
1223
+ }
1224
+ function resolveNodeModule(root, moduleName) {
1225
+ let pkgName;
1226
+ let subpath;
1227
+ if (moduleName.startsWith("@")) {
1228
+ const parts = moduleName.split("/");
1229
+ pkgName = parts.slice(0, 2).join("/");
1230
+ subpath = parts.slice(2).join("/");
1231
+ } else {
1232
+ const slash = moduleName.indexOf("/");
1233
+ pkgName = slash === -1 ? moduleName : moduleName.slice(0, slash);
1234
+ subpath = slash === -1 ? "" : moduleName.slice(slash + 1);
1235
+ }
1236
+ let pkgDir = null;
1237
+ let dir = root;
1238
+ for (; ; ) {
1239
+ const candidate = path9.join(dir, "node_modules", pkgName);
1240
+ if (fs8.existsSync(candidate)) {
1241
+ pkgDir = candidate;
1242
+ break;
1243
+ }
1244
+ const parent = path9.dirname(dir);
1245
+ if (parent === dir) break;
1246
+ dir = parent;
1247
+ }
1248
+ if (!pkgDir) return null;
1249
+ const pkgJsonPath = path9.join(pkgDir, "package.json");
1250
+ if (!fs8.existsSync(pkgJsonPath)) return null;
1251
+ let pkg;
1252
+ try {
1253
+ pkg = JSON.parse(fs8.readFileSync(pkgJsonPath, "utf-8"));
1254
+ } catch {
1255
+ return null;
1256
+ }
1257
+ if (pkg.exports) {
1258
+ const exportKey = subpath ? `./${subpath}` : ".";
1259
+ const resolved = resolvePackageExports(pkg.exports, exportKey, pkgDir);
1260
+ if (resolved) return resolved;
1261
+ }
1262
+ if (subpath) {
1263
+ const subDirs = [""];
1264
+ for (const field of ["module", "main"]) {
1265
+ if (typeof pkg[field] === "string") {
1266
+ const dir2 = path9.dirname(pkg[field]);
1267
+ if (dir2 && dir2 !== "." && !subDirs.includes(dir2)) subDirs.push(dir2);
1045
1268
  }
1046
1269
  }
1047
- }
1048
- async buildEnd(error) {
1049
- for (const plugin of this.plugins) {
1050
- if (plugin.buildEnd) {
1051
- await plugin.buildEnd.call(this.ctx, error);
1270
+ for (const dir2 of subDirs) {
1271
+ const direct = path9.join(pkgDir, dir2, subpath);
1272
+ if (fs8.existsSync(direct) && fs8.statSync(direct).isFile()) return direct;
1273
+ for (const ext of RESOLVE_EXTENSIONS) {
1274
+ if (fs8.existsSync(direct + ext)) return direct + ext;
1052
1275
  }
1053
1276
  }
1054
- }
1055
- async resolveId(source, importer, options = {}) {
1056
- for (const plugin of this.plugins) {
1057
- if (!plugin.resolveId) continue;
1058
- const result = await plugin.resolveId.call(
1059
- this.ctx,
1060
- source,
1061
- importer ?? void 0,
1062
- { isEntry: options.isEntry ?? false, ssr: false }
1063
- );
1064
- if (result != null) return result;
1065
- }
1066
1277
  return null;
1067
1278
  }
1068
- async load(id) {
1069
- for (const plugin of this.plugins) {
1070
- if (!plugin.load) continue;
1071
- const result = await plugin.load.call(this.ctx, id);
1072
- if (result != null) return result;
1073
- }
1074
- return null;
1075
- }
1076
- async transform(code, id) {
1077
- let currentCode = code;
1078
- for (const plugin of this.plugins) {
1079
- if (!plugin.transform) continue;
1080
- const result = await plugin.transform.call(this.ctx, currentCode, id);
1081
- if (result == null) continue;
1082
- if (typeof result === "string") {
1083
- currentCode = result;
1084
- } else {
1085
- currentCode = result.code;
1086
- }
1279
+ for (const field of ["module", "jsnext:main", "jsnext", "main"]) {
1280
+ if (typeof pkg[field] === "string") {
1281
+ const entry = path9.join(pkgDir, pkg[field]);
1282
+ if (fs8.existsSync(entry)) return entry;
1087
1283
  }
1088
- return currentCode === code ? null : { code: currentCode };
1089
- }
1090
- /** 完整的模块处理管道: resolveId → load → transform */
1091
- async processModule(source, importer) {
1092
- const resolveResult = await this.resolveId(source, importer, {
1093
- isEntry: !importer
1094
- });
1095
- if (resolveResult == null) return null;
1096
- const id = typeof resolveResult === "string" ? resolveResult : resolveResult.id;
1097
- const loadResult = await this.load(id);
1098
- if (loadResult == null) return null;
1099
- const loadedCode = typeof loadResult === "string" ? loadResult : loadResult.code;
1100
- const transformResult = await this.transform(loadedCode, id);
1101
- const finalCode = transformResult == null ? loadedCode : typeof transformResult === "string" ? transformResult : transformResult.code;
1102
- return { id, code: finalCode };
1103
- }
1104
- getPlugins() {
1105
- return this.plugins;
1106
- }
1107
- };
1108
- function sortPlugins(plugins) {
1109
- const pre = [];
1110
- const normal = [];
1111
- const post = [];
1112
- for (const plugin of plugins) {
1113
- if (plugin.enforce === "pre") pre.push(plugin);
1114
- else if (plugin.enforce === "post") post.push(plugin);
1115
- else normal.push(plugin);
1116
1284
  }
1117
- return [...pre, ...normal, ...post];
1285
+ const indexFallback = path9.join(pkgDir, "index.js");
1286
+ if (fs8.existsSync(indexFallback)) return indexFallback;
1287
+ return null;
1118
1288
  }
1119
-
1120
- // src/build/index.ts
1121
- import pc from "picocolors";
1122
- async function build(inlineConfig = {}) {
1123
- const config = await resolveConfig(inlineConfig, "build");
1124
- const startTime = performance.now();
1125
- console.log(pc.cyan("\n\u{1F528} nasti build") + pc.dim(` v${"1.3.9"}`));
1126
- console.log(pc.dim(` root: ${config.root}`));
1127
- console.log(pc.dim(` mode: ${config.mode}`));
1128
- const outDir = path7.resolve(config.root, config.build.outDir);
1129
- if (config.build.emptyOutDir && fs6.existsSync(outDir)) {
1130
- fs6.rmSync(outDir, { recursive: true, force: true });
1289
+ function resolvePackageExports(exports, key, pkgDir) {
1290
+ if (typeof exports === "string") {
1291
+ return key === "." ? path9.join(pkgDir, exports) : null;
1131
1292
  }
1132
- fs6.mkdirSync(outDir, { recursive: true });
1133
- const html = await readHtmlFile(config.root);
1134
- let entryPoints = [];
1135
- if (html) {
1136
- const scriptMatches = html.matchAll(/<script[^>]+src=["']([^"']+)["'][^>]*>/gi);
1137
- for (const match of scriptMatches) {
1138
- const src = match[1];
1139
- if (src && !src.startsWith("http")) {
1140
- entryPoints.push(path7.resolve(config.root, src.replace(/^\//, "")));
1141
- }
1293
+ const entry = exports[key];
1294
+ if (entry === void 0) return null;
1295
+ return resolveExportValue(entry, pkgDir);
1296
+ }
1297
+ function resolveExportValue(value, pkgDir) {
1298
+ if (typeof value === "string") return path9.join(pkgDir, value);
1299
+ if (Array.isArray(value)) {
1300
+ for (const item of value) {
1301
+ const r = resolveExportValue(item, pkgDir);
1302
+ if (r) return r;
1142
1303
  }
1304
+ return null;
1143
1305
  }
1144
- if (entryPoints.length === 0) {
1145
- const fallbackEntries = ["src/main.ts", "src/main.tsx", "src/main.js", "src/index.ts", "src/index.tsx", "src/index.js"];
1146
- for (const entry of fallbackEntries) {
1147
- const fullPath = path7.resolve(config.root, entry);
1148
- if (fs6.existsSync(fullPath)) {
1149
- entryPoints.push(fullPath);
1150
- break;
1306
+ if (value && typeof value === "object") {
1307
+ for (const cond of ESM_CONDITIONS) {
1308
+ if (cond in value) {
1309
+ const r = resolveExportValue(value[cond], pkgDir);
1310
+ if (r) return r;
1151
1311
  }
1152
1312
  }
1153
1313
  }
1154
- if (entryPoints.length === 0) {
1155
- throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
1314
+ return null;
1315
+ }
1316
+ function resolveUrlToFile(url, root) {
1317
+ const cleanUrl = url.split("?")[0];
1318
+ if (cleanUrl.startsWith("/@modules/")) {
1319
+ const moduleName = cleanUrl.slice("/@modules/".length);
1320
+ return resolveNodeModule(root, moduleName);
1156
1321
  }
1157
- const builtinPlugins = [
1158
- resolvePlugin(config),
1159
- cssPlugin(config),
1160
- assetsPlugin(config)
1161
- ];
1162
- const allPlugins = [...builtinPlugins, ...config.plugins];
1163
- const pluginContainer = new PluginContainer(config);
1164
- await pluginContainer.buildStart();
1165
- const oxcTransformPlugin = {
1166
- name: "nasti:oxc-transform",
1167
- transform(code, id) {
1168
- if (!shouldTransform(id)) return null;
1169
- const result = transformCode(id, code, {
1170
- sourcemap: !!config.build.sourcemap,
1171
- jsxRuntime: "automatic",
1172
- jsxImportSource: config.framework === "vue" ? "vue" : "react"
1173
- });
1174
- return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
1175
- }
1176
- };
1177
- const env = loadEnv(config.mode, config.root, config.envPrefix);
1178
- const envDefine = buildEnvDefine(env, config.mode);
1179
- const bundle = await rolldown({
1180
- input: entryPoints,
1181
- define: envDefine,
1182
- plugins: [
1183
- oxcTransformPlugin,
1184
- // 转换 Nasti 插件为 Rolldown 插件格式
1185
- ...allPlugins.map((p) => ({
1186
- name: p.name,
1187
- resolveId: p.resolveId,
1188
- load: p.load,
1189
- transform: p.transform,
1190
- buildStart: p.buildStart,
1191
- buildEnd: p.buildEnd
1192
- }))
1193
- ],
1194
- ...config.build.rolldownOptions
1195
- });
1196
- const { output } = await bundle.write({
1197
- dir: outDir,
1198
- format: "esm",
1199
- sourcemap: !!config.build.sourcemap,
1200
- minify: !!config.build.minify,
1201
- entryFileNames: "assets/[name].[hash].js",
1202
- chunkFileNames: "assets/[name].[hash].js",
1203
- assetFileNames: "assets/[name].[hash][extname]"
1204
- });
1205
- await bundle.close();
1206
- await pluginContainer.buildEnd();
1207
- for (const ef of pluginContainer.getEmittedFiles()) {
1208
- const dest = path7.resolve(outDir, ef.fileName);
1209
- fs6.mkdirSync(path7.dirname(dest), { recursive: true });
1210
- fs6.writeFileSync(dest, ef.source);
1322
+ const filePath = path9.resolve(root, cleanUrl.replace(/^\//, ""));
1323
+ if (fs8.existsSync(filePath) && fs8.statSync(filePath).isFile()) {
1324
+ return filePath;
1211
1325
  }
1212
- if (html) {
1213
- let processedHtml = html;
1214
- const htmlPlugin_ = htmlPlugin(config);
1215
- if (htmlPlugin_.transformIndexHtml) {
1216
- const result = await htmlPlugin_.transformIndexHtml(processedHtml);
1217
- if (typeof result === "string") {
1218
- processedHtml = result;
1219
- } else if (result && "html" in result) {
1220
- processedHtml = processHtml(result.html, result.tags);
1221
- } else if (Array.isArray(result)) {
1222
- processedHtml = processHtml(processedHtml, result);
1223
- }
1224
- }
1225
- for (const chunk of output) {
1226
- if (chunk.type === "chunk" && chunk.isEntry && chunk.facadeModuleId) {
1227
- const originalEntry = path7.relative(config.root, chunk.facadeModuleId);
1228
- processedHtml = processedHtml.replace(
1229
- new RegExp(`(src=["'])/?(${escapeRegExp(originalEntry)})(["'])`, "g"),
1230
- `$1${config.base}${chunk.fileName}$3`
1231
- );
1232
- }
1233
- }
1234
- fs6.writeFileSync(path7.resolve(outDir, "index.html"), processedHtml);
1326
+ for (const ext of RESOLVE_EXTENSIONS) {
1327
+ const withExt = filePath + ext;
1328
+ if (fs8.existsSync(withExt)) return withExt;
1235
1329
  }
1236
- const elapsed = ((performance.now() - startTime) / 1e3).toFixed(2);
1237
- const totalSize = output.reduce((sum, chunk) => {
1238
- if (chunk.type === "chunk" && chunk.code) return sum + chunk.code.length;
1239
- return sum;
1240
- }, 0);
1241
- console.log(pc.green(`
1242
- \u2713 Built in ${elapsed}s`));
1243
- console.log(pc.dim(` ${output.length} files, ${formatSize(totalSize)} total`));
1244
- console.log(pc.dim(` output: ${config.build.outDir}/
1245
- `));
1246
- return { output };
1247
- }
1248
- function formatSize(bytes) {
1249
- if (bytes < 1024) return `${bytes} B`;
1250
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} kB`;
1251
- return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
1330
+ for (const ext of RESOLVE_EXTENSIONS) {
1331
+ const indexFile = path9.join(filePath, "index" + ext);
1332
+ if (fs8.existsSync(indexFile)) return indexFile;
1333
+ }
1334
+ return null;
1252
1335
  }
1253
- function escapeRegExp(string) {
1254
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1336
+ function isModuleRequest(url) {
1337
+ const cleanUrl = url.split("?")[0];
1338
+ if (/\.(ts|tsx|jsx|js|mjs|vue|css|json)$/.test(cleanUrl)) return true;
1339
+ if (cleanUrl.startsWith("/@modules/")) return true;
1340
+ if (!path9.extname(cleanUrl)) return true;
1341
+ return false;
1255
1342
  }
1343
+ function getHmrClientCode() {
1344
+ return `
1345
+ // Nasti HMR Client
1346
+ const socket = new WebSocket(\`ws://\${location.host}\`, 'nasti-hmr');
1347
+ const hotModulesMap = new Map();
1256
1348
 
1257
- // src/server/index.ts
1258
- import http from "http";
1259
- import path10 from "path";
1260
- import os from "os";
1261
- import connect from "connect";
1262
- import sirv from "sirv";
1263
- import { watch } from "chokidar";
1264
- import pc2 from "picocolors";
1265
-
1266
- // src/core/module-graph.ts
1267
- var ModuleGraph = class {
1268
- urlToModuleMap = /* @__PURE__ */ new Map();
1269
- idToModuleMap = /* @__PURE__ */ new Map();
1270
- fileToModulesMap = /* @__PURE__ */ new Map();
1271
- getModuleByUrl(url) {
1272
- return this.urlToModuleMap.get(url);
1273
- }
1274
- getModuleById(id) {
1275
- return this.idToModuleMap.get(id);
1276
- }
1277
- getModulesByFile(file) {
1278
- return this.fileToModulesMap.get(file);
1279
- }
1280
- async ensureEntryFromUrl(url) {
1281
- let mod = this.urlToModuleMap.get(url);
1282
- if (mod) return mod;
1283
- mod = this.createModule(url);
1284
- this.urlToModuleMap.set(url, mod);
1285
- return mod;
1286
- }
1287
- createModule(url, id) {
1288
- const mod = {
1289
- id: id ?? url,
1290
- file: null,
1291
- url,
1292
- type: url.endsWith(".css") ? "css" : "js",
1293
- importers: /* @__PURE__ */ new Set(),
1294
- importedModules: /* @__PURE__ */ new Set(),
1295
- acceptedHmrDeps: /* @__PURE__ */ new Set(),
1296
- transformResult: null,
1297
- lastHMRTimestamp: 0,
1298
- isSelfAccepting: false
1299
- };
1300
- this.idToModuleMap.set(mod.id, mod);
1301
- return mod;
1302
- }
1303
- /** 注册文件路径到模块的映射 */
1304
- registerModule(mod, file) {
1305
- mod.file = file;
1306
- let mods = this.fileToModulesMap.get(file);
1307
- if (!mods) {
1308
- mods = /* @__PURE__ */ new Set();
1309
- this.fileToModulesMap.set(file, mods);
1310
- }
1311
- mods.add(mod);
1312
- }
1313
- /** 更新模块依赖关系 */
1314
- updateModuleImports(mod, importedIds) {
1315
- for (const imported of mod.importedModules) {
1316
- imported.importers.delete(mod);
1317
- }
1318
- mod.importedModules.clear();
1319
- for (const id of importedIds) {
1320
- const importedMod = this.idToModuleMap.get(id);
1321
- if (importedMod) {
1322
- mod.importedModules.add(importedMod);
1323
- importedMod.importers.add(mod);
1324
- }
1325
- }
1326
- }
1327
- /** 使模块的转换缓存失效 */
1328
- invalidateModule(mod) {
1329
- mod.transformResult = null;
1330
- mod.lastHMRTimestamp = Date.now();
1349
+ socket.addEventListener('message', ({ data }) => {
1350
+ const payload = JSON.parse(data);
1351
+ switch (payload.type) {
1352
+ case 'connected':
1353
+ console.log('[nasti] connected.');
1354
+ break;
1355
+ case 'update':
1356
+ payload.updates.forEach((update) => {
1357
+ if (update.type === 'js-update') {
1358
+ fetchUpdate(update);
1359
+ } else if (update.type === 'css-update') {
1360
+ updateCss(update.path);
1361
+ }
1362
+ });
1363
+ break;
1364
+ case 'full-reload':
1365
+ console.log('[nasti] full reload');
1366
+ location.reload();
1367
+ break;
1368
+ case 'error':
1369
+ console.error('[nasti] error:', payload.err.message);
1370
+ showErrorOverlay(payload.err);
1371
+ break;
1331
1372
  }
1332
- /** 使所有模块缓存失效 */
1333
- invalidateAll() {
1334
- for (const mod of this.idToModuleMap.values()) {
1335
- this.invalidateModule(mod);
1336
- }
1373
+ });
1374
+
1375
+ async function fetchUpdate(update) {
1376
+ const mod = hotModulesMap.get(update.path);
1377
+ if (mod) {
1378
+ const newMod = await import(update.acceptedPath + '?t=' + update.timestamp);
1379
+ mod.callbacks.forEach((cb) => cb(newMod));
1380
+ } else {
1381
+ // \u6CA1\u6709\u6CE8\u518C hot \u56DE\u8C03\uFF0C\u5C1D\u8BD5\u91CD\u65B0 import
1382
+ await import(update.path + '?t=' + update.timestamp);
1337
1383
  }
1338
- /** 获取 HMR 传播边界 - 从变更模块向上遍历找到接受更新的边界 */
1339
- getHmrBoundaries(mod) {
1340
- const boundaries = [];
1341
- const visited = /* @__PURE__ */ new Set();
1342
- const propagate = (node, via) => {
1343
- if (visited.has(node)) return true;
1344
- visited.add(node);
1345
- if (node.isSelfAccepting) {
1346
- boundaries.push({ boundary: node, acceptedVia: via });
1347
- return true;
1348
- }
1349
- if (node.acceptedHmrDeps.has(via)) {
1350
- boundaries.push({ boundary: node, acceptedVia: via });
1351
- return true;
1352
- }
1353
- if (node.importers.size === 0) return false;
1354
- for (const importer of node.importers) {
1355
- if (!propagate(importer, node)) return false;
1356
- }
1357
- return true;
1358
- };
1359
- if (mod.isSelfAccepting) {
1360
- boundaries.push({ boundary: mod, acceptedVia: mod });
1361
- return boundaries;
1362
- }
1363
- for (const importer of mod.importers) {
1364
- if (!propagate(importer, mod)) {
1365
- return [];
1366
- }
1367
- }
1368
- return boundaries;
1384
+ }
1385
+
1386
+ function updateCss(path) {
1387
+ const el = document.querySelector(\`style[data-nasti-css="\${path}"]\`);
1388
+ if (el) {
1389
+ fetch(path + '?t=' + Date.now())
1390
+ .then(r => r.text())
1391
+ .then(css => { el.textContent = css; });
1369
1392
  }
1370
- };
1393
+ }
1371
1394
 
1372
- // src/server/ws.ts
1373
- import { WebSocketServer as WsServer } from "ws";
1374
- function createWebSocketServer(server) {
1375
- const wss = new WsServer({ noServer: true });
1376
- const clients = /* @__PURE__ */ new Set();
1377
- server.on("upgrade", (req, socket, head) => {
1378
- if (req.headers["sec-websocket-protocol"] === "nasti-hmr") {
1379
- wss.handleUpgrade(req, socket, head, (ws) => {
1380
- wss.emit("connection", ws, req);
1381
- });
1382
- }
1383
- });
1384
- wss.on("connection", (ws) => {
1385
- clients.add(ws);
1386
- ws.send(JSON.stringify({ type: "connected" }));
1387
- ws.on("close", () => {
1388
- clients.delete(ws);
1389
- });
1390
- ws.on("error", (err) => {
1391
- console.error("[nasti] WebSocket error:", err);
1392
- clients.delete(ws);
1393
- });
1394
- });
1395
- return {
1396
- send(payload) {
1397
- const data = JSON.stringify(payload);
1398
- for (const client of clients) {
1399
- if (client.readyState === 1) {
1400
- client.send(data);
1401
- }
1402
- }
1403
- },
1404
- close() {
1405
- clients.clear();
1406
- wss.close();
1407
- }
1408
- };
1395
+ function showErrorOverlay(err) {
1396
+ const overlay = document.createElement('div');
1397
+ overlay.id = 'nasti-error-overlay';
1398
+ 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;';
1399
+ const title = document.createElement('h2');
1400
+ title.style.color = '#ff5555';
1401
+ title.textContent = 'Build Error';
1402
+ const pre = document.createElement('pre');
1403
+ pre.textContent = err.message + '\\n' + (err.stack || '');
1404
+ const btn = document.createElement('button');
1405
+ btn.style.cssText = 'margin-top:1rem;padding:0.5rem 1rem;cursor:pointer';
1406
+ btn.textContent = 'Close';
1407
+ btn.onclick = () => overlay.remove();
1408
+ overlay.appendChild(title);
1409
+ overlay.appendChild(pre);
1410
+ overlay.appendChild(btn);
1411
+ document.body.appendChild(overlay);
1409
1412
  }
1410
1413
 
1411
- // src/server/index.ts
1412
- init_middleware();
1414
+ // import.meta.hot API
1415
+ const createHotContext = (ownerPath) => ({
1416
+ accept(deps, callback) {
1417
+ if (typeof deps === 'function' || !deps) {
1418
+ // self-accepting
1419
+ const callbacks = hotModulesMap.get(ownerPath)?.callbacks || [];
1420
+ callbacks.push(deps || (() => {}));
1421
+ hotModulesMap.set(ownerPath, { callbacks });
1422
+ }
1423
+ },
1424
+ prune(callback) {
1425
+ // \u6A21\u5757\u88AB\u79FB\u9664\u65F6\u6267\u884C
1426
+ },
1427
+ dispose(callback) {
1428
+ // \u6A21\u5757\u66F4\u65B0\u524D\u6267\u884C\u6E05\u7406
1429
+ },
1430
+ invalidate() {
1431
+ location.reload();
1432
+ },
1433
+ data: {},
1434
+ });
1435
+
1436
+ // \u66B4\u9732\u7ED9\u6A21\u5757\u4F7F\u7528
1437
+ if (!window.__nasti_hot_map) window.__nasti_hot_map = new Map();
1438
+ window.__NASTI_HMR__ = { createHotContext };
1439
+ `;
1440
+ }
1441
+ var REACT_REFRESH_RUNTIME, REACT_REFRESH_PREAMBLE, REACT_REFRESH_FOOTER, esmBundleCache, VALID_IDENT, RESOLVE_EXTENSIONS, ESM_CONDITIONS;
1442
+ var init_middleware = __esm({
1443
+ "src/server/middleware.ts"() {
1444
+ "use strict";
1445
+ init_transformer();
1446
+ init_html();
1447
+ init_env();
1448
+ REACT_REFRESH_RUNTIME = `
1449
+ export function createSignatureFunctionForTransform() {
1450
+ return function(type, key, forceReset, getCustomHooks) { return type; };
1451
+ }
1452
+ export function register(type, id) {}
1453
+ export default { createSignatureFunctionForTransform, register };
1454
+ `;
1455
+ REACT_REFRESH_PREAMBLE = `
1456
+ import RefreshRuntime from '/@react-refresh';
1457
+ if (!window.$RefreshReg$) {
1458
+ window.$RefreshReg$ = (type, id) => RefreshRuntime.register(type, import.meta.url + ' ' + id);
1459
+ window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
1460
+ }
1461
+ `;
1462
+ REACT_REFRESH_FOOTER = `
1463
+ if (import.meta.hot) {
1464
+ import.meta.hot.accept();
1465
+ }
1466
+ `;
1467
+ esmBundleCache = /* @__PURE__ */ new Map();
1468
+ VALID_IDENT = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
1469
+ RESOLVE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".json", ".vue"];
1470
+ ESM_CONDITIONS = ["import", "browser", "module", "default"];
1471
+ }
1472
+ });
1413
1473
 
1414
1474
  // src/server/hmr.ts
1415
- import path9 from "path";
1416
- import fs8 from "fs";
1475
+ import path10 from "path";
1476
+ import fs9 from "fs";
1417
1477
  async function handleFileChange(file, server) {
1418
1478
  const { moduleGraph, ws, config } = server;
1419
- const relativePath = "/" + path9.relative(config.root, file);
1479
+ const relativePath = "/" + path10.relative(config.root, file);
1420
1480
  const mods = moduleGraph.getModulesByFile(file);
1421
1481
  if (!mods || mods.size === 0) {
1422
1482
  return;
@@ -1429,7 +1489,7 @@ async function handleFileChange(file, server) {
1429
1489
  file,
1430
1490
  timestamp,
1431
1491
  modules: [mod],
1432
- read: () => fs8.readFileSync(file, "utf-8"),
1492
+ read: () => fs9.readFileSync(file, "utf-8"),
1433
1493
  server
1434
1494
  };
1435
1495
  let affectedModules = [mod];
@@ -1461,9 +1521,24 @@ async function handleFileChange(file, server) {
1461
1521
  ws.send({ type: "update", updates });
1462
1522
  }
1463
1523
  }
1524
+ var init_hmr = __esm({
1525
+ "src/server/hmr.ts"() {
1526
+ "use strict";
1527
+ }
1528
+ });
1464
1529
 
1465
1530
  // src/server/index.ts
1466
- init_html();
1531
+ var server_exports = {};
1532
+ __export(server_exports, {
1533
+ createServer: () => createServer
1534
+ });
1535
+ import http from "http";
1536
+ import path11 from "path";
1537
+ import os from "os";
1538
+ import connect from "connect";
1539
+ import sirv from "sirv";
1540
+ import { watch } from "chokidar";
1541
+ import pc3 from "picocolors";
1467
1542
  async function createServer(inlineConfig = {}) {
1468
1543
  const config = await resolveConfig(inlineConfig, "serve");
1469
1544
  const allPlugins = [
@@ -1482,7 +1557,7 @@ async function createServer(inlineConfig = {}) {
1482
1557
  pluginContainer,
1483
1558
  moduleGraph
1484
1559
  }));
1485
- const publicDir = path10.resolve(config.root, "public");
1560
+ const publicDir = path11.resolve(config.root, "public");
1486
1561
  app.use(sirv(publicDir, { dev: true, etag: true }));
1487
1562
  app.use(sirv(config.root, { dev: true, etag: true }));
1488
1563
  const httpServer = http.createServer(app);
@@ -1525,14 +1600,15 @@ async function createServer(inlineConfig = {}) {
1525
1600
  let currentPort = finalPort;
1526
1601
  const onListening = () => {
1527
1602
  const actualPort = httpServer.address()?.port ?? currentPort;
1603
+ config.server.port = actualPort;
1528
1604
  const localUrl = `http://localhost:${actualPort}`;
1529
1605
  const networkUrl = host === "0.0.0.0" ? `http://${getNetworkAddress()}:${actualPort}` : null;
1530
1606
  console.log();
1531
- console.log(pc2.cyan(" nasti dev server") + pc2.dim(` v${"1.3.9"}`));
1607
+ console.log(pc3.cyan(" nasti dev server") + pc3.dim(` v${"1.4.0"}`));
1532
1608
  console.log();
1533
- console.log(` ${pc2.green(">")} Local: ${pc2.cyan(localUrl)}`);
1609
+ console.log(` ${pc3.green(">")} Local: ${pc3.cyan(localUrl)}`);
1534
1610
  if (networkUrl) {
1535
- console.log(` ${pc2.green(">")} Network: ${pc2.cyan(networkUrl)}`);
1611
+ console.log(` ${pc3.green(">")} Network: ${pc3.cyan(networkUrl)}`);
1536
1612
  }
1537
1613
  console.log();
1538
1614
  resolve(server);
@@ -1541,7 +1617,7 @@ async function createServer(inlineConfig = {}) {
1541
1617
  httpServer.on("error", (err) => {
1542
1618
  if (err.code === "EADDRINUSE") {
1543
1619
  currentPort++;
1544
- console.log(pc2.yellow(`Port ${currentPort - 1} is in use, trying ${currentPort}...`));
1620
+ console.log(pc3.yellow(`Port ${currentPort - 1} is in use, trying ${currentPort}...`));
1545
1621
  httpServer.listen(currentPort, host);
1546
1622
  } else {
1547
1623
  reject(err);
@@ -1574,10 +1650,391 @@ function getNetworkAddress() {
1574
1650
  }
1575
1651
  return "localhost";
1576
1652
  }
1653
+ var init_server = __esm({
1654
+ "src/server/index.ts"() {
1655
+ "use strict";
1656
+ init_config();
1657
+ init_plugin_container();
1658
+ init_module_graph();
1659
+ init_ws();
1660
+ init_middleware();
1661
+ init_hmr();
1662
+ init_resolve();
1663
+ init_css();
1664
+ init_assets();
1665
+ init_html();
1666
+ }
1667
+ });
1668
+
1669
+ // src/index.ts
1670
+ init_config();
1671
+ init_build();
1672
+
1673
+ // src/build/electron.ts
1674
+ init_config();
1675
+ init_resolve();
1676
+ import path8 from "path";
1677
+ import fs7 from "fs";
1678
+ import { rolldown as rolldown2 } from "rolldown";
1679
+ import pc2 from "picocolors";
1680
+
1681
+ // src/plugins/electron.ts
1682
+ import { builtinModules } from "module";
1683
+ var NODE_BUILTINS = /* @__PURE__ */ new Set([
1684
+ ...builtinModules,
1685
+ ...builtinModules.map((m) => `node:${m}`)
1686
+ ]);
1687
+ var ELECTRON_MODULES = /* @__PURE__ */ new Set([
1688
+ "electron",
1689
+ "electron/main",
1690
+ "electron/common",
1691
+ "electron/renderer"
1692
+ ]);
1693
+ function electronPlugin(config) {
1694
+ const external = /* @__PURE__ */ new Set([
1695
+ ...ELECTRON_MODULES,
1696
+ ...NODE_BUILTINS,
1697
+ ...config.electron.external ?? []
1698
+ ]);
1699
+ return {
1700
+ name: "nasti:electron",
1701
+ enforce: "pre",
1702
+ resolveId(source) {
1703
+ if (external.has(source)) {
1704
+ return { id: source, external: true };
1705
+ }
1706
+ if (source.startsWith("electron/")) {
1707
+ return { id: source, external: true };
1708
+ }
1709
+ return null;
1710
+ }
1711
+ };
1712
+ }
1713
+
1714
+ // src/build/electron.ts
1715
+ init_transformer();
1716
+ init_env();
1717
+ async function buildElectron(inlineConfig = {}) {
1718
+ const config = await resolveConfig({ ...inlineConfig, target: "electron" }, "build");
1719
+ const startTime = performance.now();
1720
+ assertElectronVersion(config);
1721
+ console.log(pc2.cyan("\n\u26A1 nasti build (electron)") + pc2.dim(` v${"1.4.0"}`));
1722
+ console.log(pc2.dim(` root: ${config.root}`));
1723
+ console.log(pc2.dim(` mode: ${config.mode}`));
1724
+ console.log(pc2.dim(` target: electron (\u2265 ${config.electron.minVersion})`));
1725
+ const outDir = path8.resolve(config.root, config.build.outDir);
1726
+ if (config.build.emptyOutDir && fs7.existsSync(outDir)) {
1727
+ fs7.rmSync(outDir, { recursive: true, force: true });
1728
+ }
1729
+ fs7.mkdirSync(outDir, { recursive: true });
1730
+ const rendererOutDir = path8.join(outDir, "renderer");
1731
+ const { build: build2 } = await Promise.resolve().then(() => (init_build(), build_exports));
1732
+ await build2({
1733
+ ...inlineConfig,
1734
+ target: "web",
1735
+ build: {
1736
+ ...inlineConfig.build,
1737
+ outDir: rendererOutDir,
1738
+ emptyOutDir: false
1739
+ }
1740
+ });
1741
+ const mainEntry = path8.resolve(config.root, config.electron.main);
1742
+ if (!fs7.existsSync(mainEntry)) {
1743
+ throw new Error(
1744
+ `Electron main entry not found: ${config.electron.main}
1745
+ \u5728 nasti.config.ts \u7684 electron.main \u6307\u5B9A\u4E3B\u8FDB\u7A0B\u5165\u53E3\u6587\u4EF6\u3002`
1746
+ );
1747
+ }
1748
+ const mainFile = await bundleNode(config, mainEntry, {
1749
+ outFile: outFileName(outDir, "main", config.electron.mainFormat),
1750
+ format: config.electron.mainFormat,
1751
+ label: "main"
1752
+ });
1753
+ const preloadEntries = normalizePreload(config.electron.preload, config.root);
1754
+ const preloadFiles = [];
1755
+ for (const entry of preloadEntries) {
1756
+ if (!fs7.existsSync(entry)) {
1757
+ console.warn(pc2.yellow(` \u26A0 preload entry not found, skipped: ${entry}`));
1758
+ continue;
1759
+ }
1760
+ const base = path8.basename(entry).replace(/\.[^.]+$/, "");
1761
+ const out = outFileName(outDir, base, config.electron.preloadFormat);
1762
+ await bundleNode(config, entry, {
1763
+ outFile: out,
1764
+ format: config.electron.preloadFormat,
1765
+ label: `preload (${base})`
1766
+ });
1767
+ preloadFiles.push(out);
1768
+ }
1769
+ const elapsed = ((performance.now() - startTime) / 1e3).toFixed(2);
1770
+ console.log(pc2.green(`
1771
+ \u2713 Electron build complete in ${elapsed}s`));
1772
+ console.log(pc2.dim(` renderer: ${path8.relative(config.root, rendererOutDir)}/`));
1773
+ console.log(pc2.dim(` main: ${path8.relative(config.root, mainFile)}`));
1774
+ for (const pf of preloadFiles) {
1775
+ console.log(pc2.dim(` preload: ${path8.relative(config.root, pf)}`));
1776
+ }
1777
+ console.log();
1778
+ return { rendererOutDir, mainFile, preloadFiles };
1779
+ }
1780
+ async function bundleNode(config, entry, opts) {
1781
+ const env = loadEnv(config.mode, config.root, config.envPrefix);
1782
+ const envDefine = {
1783
+ ...buildEnvDefine(env, config.mode),
1784
+ __ELECTRON__: "true",
1785
+ __NASTI_TARGET__: JSON.stringify("electron")
1786
+ };
1787
+ const oxcTransformPlugin = {
1788
+ name: "nasti:oxc-transform",
1789
+ transform(code, id) {
1790
+ if (!shouldTransform(id)) return null;
1791
+ const result = transformCode(id, code, {
1792
+ sourcemap: !!config.build.sourcemap,
1793
+ jsxRuntime: "automatic",
1794
+ jsxImportSource: config.framework === "vue" ? "vue" : "react"
1795
+ });
1796
+ return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
1797
+ }
1798
+ };
1799
+ const bundle = await rolldown2({
1800
+ input: entry,
1801
+ define: envDefine,
1802
+ platform: "node",
1803
+ plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)],
1804
+ ...config.build.rolldownOptions
1805
+ });
1806
+ fs7.mkdirSync(path8.dirname(opts.outFile), { recursive: true });
1807
+ await bundle.write({
1808
+ file: opts.outFile,
1809
+ format: opts.format === "cjs" ? "cjs" : "esm",
1810
+ sourcemap: !!config.build.sourcemap,
1811
+ minify: !!config.build.minify,
1812
+ inlineDynamicImports: true
1813
+ });
1814
+ await bundle.close();
1815
+ console.log(pc2.dim(` \u2713 ${opts.label} \u2192 ${path8.relative(config.root, opts.outFile)}`));
1816
+ return opts.outFile;
1817
+ }
1818
+ function outFileName(outDir, base, format) {
1819
+ const ext = format === "cjs" ? ".cjs" : ".mjs";
1820
+ return path8.join(outDir, base + ext);
1821
+ }
1822
+ function normalizePreload(preload, root) {
1823
+ const list = Array.isArray(preload) ? preload : preload ? [preload] : [];
1824
+ return list.map((p) => path8.resolve(root, p));
1825
+ }
1826
+ function assertElectronVersion(config) {
1827
+ const min = config.electron.minVersion;
1828
+ const installed = detectInstalledElectron(config.root);
1829
+ if (installed && installed < min) {
1830
+ console.warn(
1831
+ pc2.yellow(
1832
+ ` \u26A0 \u68C0\u6D4B\u5230 Electron ${installed}\uFF0CNasti \u8981\u6C42 \u2265 ${min}\u3002\u65E7\u7248\u672C\u53EF\u80FD\u7F3A\u5C11 ESM \u4E3B\u8FDB\u7A0B\u652F\u6301\u3002`
1833
+ )
1834
+ );
1835
+ }
1836
+ }
1837
+ function detectInstalledElectron(root) {
1838
+ try {
1839
+ const pkgPath = path8.resolve(root, "node_modules/electron/package.json");
1840
+ if (!fs7.existsSync(pkgPath)) return null;
1841
+ const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
1842
+ const major = parseInt(String(pkg.version).split(".")[0], 10);
1843
+ return Number.isFinite(major) ? major : null;
1844
+ } catch {
1845
+ return null;
1846
+ }
1847
+ }
1848
+
1849
+ // src/index.ts
1850
+ init_server();
1851
+
1852
+ // src/server/electron-dev.ts
1853
+ init_config();
1854
+ import path12 from "path";
1855
+ import fs10 from "fs";
1856
+ import { createRequire as createRequire2 } from "module";
1857
+ import { spawn } from "child_process";
1858
+ import chokidar from "chokidar";
1859
+ import pc4 from "picocolors";
1860
+ import { rolldown as rolldown3 } from "rolldown";
1861
+ init_resolve();
1862
+ init_transformer();
1863
+ init_env();
1864
+ async function startElectronDev(inlineConfig = {}) {
1865
+ const { noSpawn, ...rest } = inlineConfig;
1866
+ const config = await resolveConfig({ ...rest, target: "electron" }, "serve");
1867
+ warnElectronVersion(config);
1868
+ console.log(pc4.cyan("\n\u26A1 nasti electron dev") + pc4.dim(` v${"1.4.0"}`));
1869
+ const { createServer: createServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
1870
+ const server = await createServer2({ ...rest, target: "electron" });
1871
+ await server.listen();
1872
+ const devUrl = `http://localhost:${server.config.server.port}/`;
1873
+ console.log(pc4.dim(` renderer: ${devUrl}`));
1874
+ const stageDir = path12.resolve(config.root, ".nasti");
1875
+ fs10.mkdirSync(stageDir, { recursive: true });
1876
+ const mainEntry = path12.resolve(config.root, config.electron.main);
1877
+ const preloadEntries = normalizePreload(config.electron.preload, config.root);
1878
+ const builtMainFile = path12.join(stageDir, "main" + extFor(config.electron.mainFormat));
1879
+ const builtPreloadFiles = [];
1880
+ const compileAll = async () => {
1881
+ await compileNode(config, mainEntry, {
1882
+ outFile: builtMainFile,
1883
+ format: config.electron.mainFormat,
1884
+ devUrl
1885
+ });
1886
+ builtPreloadFiles.length = 0;
1887
+ for (const entry of preloadEntries) {
1888
+ if (!fs10.existsSync(entry)) continue;
1889
+ const base = path12.basename(entry).replace(/\.[^.]+$/, "");
1890
+ const out = path12.join(stageDir, base + extFor(config.electron.preloadFormat));
1891
+ await compileNode(config, entry, {
1892
+ outFile: out,
1893
+ format: config.electron.preloadFormat,
1894
+ devUrl
1895
+ });
1896
+ builtPreloadFiles.push(out);
1897
+ }
1898
+ };
1899
+ await compileAll();
1900
+ if (noSpawn) {
1901
+ console.log(pc4.dim(" (noSpawn) \u5DF2\u7F16\u8BD1\u4E3B/preload\uFF0C\u8DF3\u8FC7\u542F\u52A8 Electron\u3002"));
1902
+ return;
1903
+ }
1904
+ const electronBin = resolveElectronBinary(config);
1905
+ if (!electronBin) {
1906
+ console.warn(
1907
+ pc4.yellow(
1908
+ " \u26A0 \u672A\u627E\u5230 Electron \u53EF\u6267\u884C\u6587\u4EF6\uFF0C\u8BF7\u5148\u5B89\u88C5\uFF1Anpm install -D electron\n \u5DF2\u7F16\u8BD1\u4E3B/preload \u81F3 .nasti/\uFF0C\u53EF\u624B\u52A8\u8FD0\u884C\u3002"
1909
+ )
1910
+ );
1911
+ return;
1912
+ }
1913
+ let child = null;
1914
+ const spawnElectron = () => {
1915
+ const args = [builtMainFile, ...config.electron.electronArgs];
1916
+ child = spawn(electronBin, args, {
1917
+ stdio: "inherit",
1918
+ env: { ...process.env, NASTI_DEV_SERVER_URL: devUrl, NASTI_TARGET: "electron" }
1919
+ });
1920
+ child.on("exit", (code) => {
1921
+ if (code !== null && child && child.__nastiKilled !== true) {
1922
+ console.log(pc4.dim(` Electron exited (${code}).`));
1923
+ process.exit(code ?? 0);
1924
+ }
1925
+ });
1926
+ };
1927
+ spawnElectron();
1928
+ if (config.electron.autoRestart) {
1929
+ const watchTargets = [mainEntry, ...preloadEntries].filter(fs10.existsSync);
1930
+ const watcher = chokidar.watch(watchTargets, { ignoreInitial: true });
1931
+ let restarting = null;
1932
+ watcher.on("all", async () => {
1933
+ if (restarting) return;
1934
+ restarting = (async () => {
1935
+ console.log(pc4.cyan("\n \u267B \u4E3B/preload \u53D8\u66F4\uFF0C\u91CD\u542F Electron..."));
1936
+ try {
1937
+ if (child && !child.killed) {
1938
+ ;
1939
+ child.__nastiKilled = true;
1940
+ const dying = child;
1941
+ await new Promise((resolve) => {
1942
+ const timer = setTimeout(() => resolve(), 3e3);
1943
+ dying.once("exit", () => {
1944
+ clearTimeout(timer);
1945
+ resolve();
1946
+ });
1947
+ dying.kill();
1948
+ });
1949
+ }
1950
+ await compileAll();
1951
+ spawnElectron();
1952
+ } finally {
1953
+ restarting = null;
1954
+ }
1955
+ })();
1956
+ });
1957
+ }
1958
+ }
1959
+ function extFor(format) {
1960
+ return format === "cjs" ? ".cjs" : ".mjs";
1961
+ }
1962
+ async function compileNode(config, entry, opts) {
1963
+ const env = loadEnv(config.mode, config.root, config.envPrefix);
1964
+ const envDefine = {
1965
+ ...buildEnvDefine(env, config.mode),
1966
+ __ELECTRON__: "true",
1967
+ __NASTI_TARGET__: JSON.stringify("electron"),
1968
+ __NASTI_DEV_SERVER_URL__: JSON.stringify(opts.devUrl)
1969
+ };
1970
+ const oxcTransformPlugin = {
1971
+ name: "nasti:oxc-transform",
1972
+ transform(code, id) {
1973
+ if (!shouldTransform(id)) return null;
1974
+ const result = transformCode(id, code, {
1975
+ sourcemap: !!config.build.sourcemap,
1976
+ jsxRuntime: "automatic",
1977
+ jsxImportSource: config.framework === "vue" ? "vue" : "react"
1978
+ });
1979
+ return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
1980
+ }
1981
+ };
1982
+ const bundle = await rolldown3({
1983
+ input: entry,
1984
+ transform: { define: envDefine },
1985
+ platform: "node",
1986
+ plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)]
1987
+ });
1988
+ fs10.mkdirSync(path12.dirname(opts.outFile), { recursive: true });
1989
+ await bundle.write({
1990
+ file: opts.outFile,
1991
+ format: opts.format === "cjs" ? "cjs" : "esm",
1992
+ sourcemap: false,
1993
+ minify: false,
1994
+ inlineDynamicImports: true
1995
+ });
1996
+ await bundle.close();
1997
+ }
1998
+ function resolveElectronBinary(config) {
1999
+ if (config.electron.electronPath && fs10.existsSync(config.electron.electronPath)) {
2000
+ return config.electron.electronPath;
2001
+ }
2002
+ try {
2003
+ const require2 = createRequire2(path12.resolve(config.root, "package.json"));
2004
+ const pathFile = require2.resolve("electron");
2005
+ const electronModule = require2(pathFile);
2006
+ if (typeof electronModule === "string" && fs10.existsSync(electronModule)) {
2007
+ return electronModule;
2008
+ }
2009
+ } catch {
2010
+ }
2011
+ return null;
2012
+ }
2013
+ function warnElectronVersion(config) {
2014
+ const installed = detectInstalledElectron(config.root);
2015
+ if (installed === null) {
2016
+ console.warn(
2017
+ pc4.yellow(
2018
+ ` \u26A0 \u672A\u68C0\u6D4B\u5230 Electron\uFF0C\u8BF7\u5B89\u88C5\uFF1Anpm install -D electron@^${config.electron.minVersion}`
2019
+ )
2020
+ );
2021
+ return;
2022
+ }
2023
+ if (installed < config.electron.minVersion) {
2024
+ console.warn(
2025
+ pc4.yellow(
2026
+ ` \u26A0 Electron ${installed} \u4F4E\u4E8E Nasti \u8981\u6C42\u7684 ${config.electron.minVersion}\uFF0C\u67D0\u4E9B\u7279\u6027\uFF08\u5982 ESM \u4E3B\u8FDB\u7A0B\uFF09\u4E0D\u53EF\u7528\u3002`
2027
+ )
2028
+ );
2029
+ }
2030
+ }
1577
2031
  export {
1578
2032
  build,
2033
+ buildElectron,
1579
2034
  createServer,
1580
2035
  defineConfig,
1581
- resolveConfig
2036
+ electronPlugin,
2037
+ resolveConfig,
2038
+ startElectronDev
1582
2039
  };
1583
2040
  //# sourceMappingURL=index.js.map