@nasti-toolchain/nasti 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -10,10 +10,10 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
10
10
  if (typeof require !== "undefined") return require.apply(this, arguments);
11
11
  throw Error('Dynamic require of "' + x + '" is not supported');
12
12
  });
13
- var __glob = (map) => (path17) => {
14
- var fn = map[path17];
13
+ var __glob = (map) => (path18) => {
14
+ var fn = map[path18];
15
15
  if (fn) return fn();
16
- throw new Error("Module not found in bundle: " + path17);
16
+ throw new Error("Module not found in bundle: " + path18);
17
17
  };
18
18
  var __esm = (fn, res) => function __init() {
19
19
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
@@ -231,6 +231,92 @@ var init_defaults = __esm({
231
231
  }
232
232
  });
233
233
 
234
+ // src/core/plugin-api.ts
235
+ function orderPlugins(plugins) {
236
+ const baseline = plugins.map((plugin, index2) => ({ plugin, index: index2 })).sort((a, b) => enforceRank(a.plugin) - enforceRank(b.plugin) || a.index - b.index).map(({ plugin }) => plugin);
237
+ const indexesByName = /* @__PURE__ */ new Map();
238
+ baseline.forEach((plugin, index2) => {
239
+ const indexes = indexesByName.get(plugin.name) ?? [];
240
+ indexes.push(index2);
241
+ indexesByName.set(plugin.name, indexes);
242
+ });
243
+ const edges = baseline.map(() => /* @__PURE__ */ new Set());
244
+ const indegree = baseline.map(() => 0);
245
+ const addEdge = (from, to) => {
246
+ if (from === to || edges[from].has(to)) return;
247
+ edges[from].add(to);
248
+ indegree[to]++;
249
+ };
250
+ baseline.forEach((plugin, current) => {
251
+ for (const dependency of plugin.pre ?? []) {
252
+ for (const before of indexesByName.get(dependency) ?? []) addEdge(before, current);
253
+ }
254
+ for (const dependency of plugin.post ?? []) {
255
+ for (const after of indexesByName.get(dependency) ?? []) addEdge(current, after);
256
+ }
257
+ });
258
+ const ready = indegree.map((degree, index2) => ({ degree, index: index2 })).filter(({ degree }) => degree === 0).map(({ index: index2 }) => index2);
259
+ const ordered = [];
260
+ while (ready.length > 0) {
261
+ ready.sort((a, b) => a - b);
262
+ const current = ready.shift();
263
+ ordered.push(baseline[current]);
264
+ for (const next of edges[current]) {
265
+ indegree[next]--;
266
+ if (indegree[next] === 0) ready.push(next);
267
+ }
268
+ }
269
+ if (ordered.length !== baseline.length) {
270
+ const cyclic = baseline.filter((_, index2) => indegree[index2] > 0).map((plugin) => plugin.name);
271
+ throw new Error(
272
+ `[nasti] circular plugin setup dependency: ${[...new Set(cyclic)].join(", ")}`
273
+ );
274
+ }
275
+ return ordered;
276
+ }
277
+ async function setupPluginApi(config, plugins) {
278
+ const exposed = /* @__PURE__ */ new Map();
279
+ const api = {
280
+ config,
281
+ logger: config.logger,
282
+ expose(key, value) {
283
+ if (exposed.has(key) && exposed.get(key) !== value) {
284
+ throw new Error(`[nasti] plugin API key already exposed: ${String(key)}`);
285
+ }
286
+ exposed.set(key, value);
287
+ },
288
+ useExposed(key) {
289
+ return exposed.get(key);
290
+ }
291
+ };
292
+ apiByConfig.set(config, api);
293
+ for (const plugin of plugins) {
294
+ await plugin.setup?.(api);
295
+ }
296
+ return api;
297
+ }
298
+ function getPluginApi(config) {
299
+ const api = apiByConfig.get(config);
300
+ if (!api) {
301
+ throw new Error(
302
+ "[nasti] internal: plugin API requested before setup; getPluginApi requires the original ResolvedConfig reference registered by setupPluginApi, not a shallow copy"
303
+ );
304
+ }
305
+ return api;
306
+ }
307
+ function enforceRank(plugin) {
308
+ if (plugin.enforce === "pre") return 0;
309
+ if (plugin.enforce === "post") return 2;
310
+ return 1;
311
+ }
312
+ var apiByConfig;
313
+ var init_plugin_api = __esm({
314
+ "src/core/plugin-api.ts"() {
315
+ "use strict";
316
+ apiByConfig = /* @__PURE__ */ new WeakMap();
317
+ }
318
+ });
319
+
234
320
  // src/config/index.ts
235
321
  import { pathToFileURL } from "url";
236
322
  import path from "path";
@@ -268,6 +354,43 @@ async function loadConfigFromFile(root) {
268
354
  }
269
355
  return {};
270
356
  }
357
+ function detectFramework(root) {
358
+ const sourceRoot = path.resolve(root, "src");
359
+ if (containsVueFile(sourceRoot)) return "vue";
360
+ const packagePath = path.resolve(root, "package.json");
361
+ if (fs.existsSync(packagePath)) {
362
+ try {
363
+ const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
364
+ const dependencies = {
365
+ ...pkg.dependencies ?? {},
366
+ ...pkg.devDependencies ?? {},
367
+ ...pkg.peerDependencies ?? {},
368
+ ...pkg.optionalDependencies ?? {}
369
+ };
370
+ const hasVue = "vue" in dependencies || "@vue/runtime-dom" in dependencies;
371
+ const hasReact = "react" in dependencies || "react-dom" in dependencies;
372
+ if (hasVue && !hasReact) return "vue";
373
+ if (hasReact) return "react";
374
+ if (hasVue) return "vue";
375
+ } catch {
376
+ }
377
+ }
378
+ return "react";
379
+ }
380
+ function containsVueFile(dir, depth = 0) {
381
+ if (depth > 5 || !fs.existsSync(dir)) return false;
382
+ try {
383
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
384
+ if (entry.isFile() && entry.name.endsWith(".vue")) return true;
385
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && containsVueFile(path.join(dir, entry.name), depth + 1)) {
386
+ return true;
387
+ }
388
+ }
389
+ } catch {
390
+ return false;
391
+ }
392
+ return false;
393
+ }
271
394
  async function loadTsConfig(filePath) {
272
395
  const { transformSync: transformSync2 } = await import("oxc-transform");
273
396
  const code = fs.readFileSync(filePath, "utf-8");
@@ -314,7 +437,7 @@ async function resolveConfig(inlineConfig = {}, command) {
314
437
  base: merged.base ?? defaults.base,
315
438
  mode,
316
439
  target: merged.target ?? defaults.target,
317
- framework: merged.framework ?? defaults.framework,
440
+ framework: (merged.framework ?? defaults.framework) === "auto" ? detectFramework(root) : merged.framework,
318
441
  command,
319
442
  resolve: {
320
443
  // tsconfig paths 优先级最低:tsconfig < defaults < user config
@@ -361,7 +484,13 @@ async function resolveConfig(inlineConfig = {}, command) {
361
484
  if (envOptions.build) Object.assign(resolved.build, envOptions.build);
362
485
  resolved.environments.client = {
363
486
  consumer,
364
- entry: [],
487
+ buildEnabled: envOptions.buildEnabled ?? true,
488
+ entry: normalizeEnvironmentEntries(envOptions.entry, root),
489
+ html: path.resolve(
490
+ root,
491
+ envOptions.html ?? (resolved.target === "electron" ? resolved.electron.renderer : "index.html")
492
+ ),
493
+ driver: envOptions.driver,
365
494
  // 同引用 —— 精确镜像(assertClientEnvironmentMirror 校验)
366
495
  resolve: resolved.resolve,
367
496
  build: resolved.build
@@ -370,7 +499,10 @@ async function resolveConfig(inlineConfig = {}, command) {
370
499
  }
371
500
  resolved.environments[name] = {
372
501
  consumer,
373
- entry: (Array.isArray(envOptions.entry) ? envOptions.entry : envOptions.entry ? [envOptions.entry] : []).map((e) => path.resolve(root, e)),
502
+ buildEnabled: envOptions.buildEnabled ?? true,
503
+ entry: normalizeEnvironmentEntries(envOptions.entry, root),
504
+ html: envOptions.consumer === "client" && envOptions.html ? path.resolve(root, envOptions.html) : void 0,
505
+ driver: envOptions.driver,
374
506
  resolve: {
375
507
  alias: { ...resolved.resolve.alias, ...envOptions.resolve?.alias },
376
508
  extensions: envOptions.resolve?.extensions ?? [...resolved.resolve.extensions],
@@ -389,12 +521,13 @@ async function resolveConfig(inlineConfig = {}, command) {
389
521
  };
390
522
  }
391
523
  assertClientEnvironmentMirror(resolved);
392
- const filteredPlugins = rawPlugins.filter((p) => {
524
+ const filteredPlugins = orderPlugins(rawPlugins.filter((p) => {
393
525
  if (!p.apply) return true;
394
526
  if (typeof p.apply === "function") return p.apply(resolved, env);
395
527
  return p.apply === command;
396
- });
528
+ }));
397
529
  resolved.plugins = filteredPlugins;
530
+ await setupPluginApi(resolved, filteredPlugins);
398
531
  if (resolved.target === "electron") {
399
532
  const autoExternal = detectNativeDeps(root);
400
533
  if (autoExternal.length > 0) {
@@ -410,6 +543,10 @@ async function resolveConfig(inlineConfig = {}, command) {
410
543
  }
411
544
  return resolved;
412
545
  }
546
+ function normalizeEnvironmentEntries(entry, root) {
547
+ const entries = Array.isArray(entry) ? entry : entry ? [entry] : [];
548
+ return entries.map((item) => path.resolve(root, item));
549
+ }
413
550
  function detectNativeDeps(root) {
414
551
  const result = /* @__PURE__ */ new Set();
415
552
  const pkgJsonPath = path.resolve(root, "package.json");
@@ -527,6 +664,7 @@ var init_config = __esm({
527
664
  "use strict";
528
665
  init_defaults();
529
666
  init_logger();
667
+ init_plugin_api();
530
668
  CONFIG_FILES = [
531
669
  "nasti.config.ts",
532
670
  "nasti.config.js",
@@ -537,21 +675,11 @@ var init_config = __esm({
537
675
  });
538
676
 
539
677
  // src/core/plugin-container.ts
540
- function sortPlugins(plugins) {
541
- const pre = [];
542
- const normal = [];
543
- const post = [];
544
- for (const plugin of plugins) {
545
- if (plugin.enforce === "pre") pre.push(plugin);
546
- else if (plugin.enforce === "post") post.push(plugin);
547
- else normal.push(plugin);
548
- }
549
- return [...pre, ...normal, ...post];
550
- }
551
678
  var PluginContainer;
552
679
  var init_plugin_container = __esm({
553
680
  "src/core/plugin-container.ts"() {
554
681
  "use strict";
682
+ init_plugin_api();
555
683
  PluginContainer = class {
556
684
  plugins;
557
685
  config;
@@ -562,7 +690,7 @@ var init_plugin_container = __esm({
562
690
  constructor(config, environment) {
563
691
  this.config = config;
564
692
  this.environment = environment;
565
- this.plugins = sortPlugins(config.plugins);
693
+ this.plugins = orderPlugins(config.plugins);
566
694
  this.ctx = this.createContext();
567
695
  }
568
696
  createContext() {
@@ -661,17 +789,35 @@ var init_plugin_container = __esm({
661
789
  }
662
790
  });
663
791
 
792
+ // src/core/url.ts
793
+ function removeTimestampQuery(url) {
794
+ const hashIndex = url.indexOf("#");
795
+ const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
796
+ const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
797
+ const queryIndex = withoutHash.indexOf("?");
798
+ if (queryIndex < 0) return url;
799
+ const pathname = withoutHash.slice(0, queryIndex);
800
+ const query = withoutHash.slice(queryIndex + 1).split("&").filter((part) => !/^t=\d+$/.test(part)).join("&");
801
+ return pathname + (query ? `?${query}` : "") + hash;
802
+ }
803
+ var init_url = __esm({
804
+ "src/core/url.ts"() {
805
+ "use strict";
806
+ }
807
+ });
808
+
664
809
  // src/core/module-graph.ts
665
810
  var ModuleGraph;
666
811
  var init_module_graph = __esm({
667
812
  "src/core/module-graph.ts"() {
668
813
  "use strict";
814
+ init_url();
669
815
  ModuleGraph = class {
670
816
  urlToModuleMap = /* @__PURE__ */ new Map();
671
817
  idToModuleMap = /* @__PURE__ */ new Map();
672
818
  fileToModulesMap = /* @__PURE__ */ new Map();
673
819
  getModuleByUrl(url) {
674
- return this.urlToModuleMap.get(url);
820
+ return this.urlToModuleMap.get(removeTimestampQuery(url));
675
821
  }
676
822
  getModuleById(id) {
677
823
  return this.idToModuleMap.get(id);
@@ -680,10 +826,11 @@ var init_module_graph = __esm({
680
826
  return this.fileToModulesMap.get(file);
681
827
  }
682
828
  async ensureEntryFromUrl(url) {
683
- let mod = this.urlToModuleMap.get(url);
829
+ const normalizedUrl = removeTimestampQuery(url);
830
+ let mod = this.urlToModuleMap.get(normalizedUrl);
684
831
  if (mod) return mod;
685
- mod = this.createModule(url);
686
- this.urlToModuleMap.set(url, mod);
832
+ mod = this.createModule(normalizedUrl);
833
+ this.urlToModuleMap.set(normalizedUrl, mod);
687
834
  return mod;
688
835
  }
689
836
  createModule(url, id) {
@@ -697,6 +844,7 @@ var init_module_graph = __esm({
697
844
  acceptedHmrDeps: /* @__PURE__ */ new Set(),
698
845
  transformResult: null,
699
846
  lastHMRTimestamp: 0,
847
+ invalidationVersion: 0,
700
848
  isSelfAccepting: false
701
849
  };
702
850
  this.idToModuleMap.set(mod.id, mod);
@@ -739,10 +887,64 @@ var init_module_graph = __esm({
739
887
  }
740
888
  }
741
889
  }
890
+ /**
891
+ * 用一次转换得到的信息原子更新 import 与 HMR accept 关系。
892
+ * 依赖节点会在真正被浏览器请求前预先创建,这样入口模块先转换时也能建立完整图。
893
+ */
894
+ async updateModuleInfo(mod, importedUrls, acceptedUrls, isSelfAccepting, expectedInvalidationVersion) {
895
+ const importedModules = await Promise.all(
896
+ [...importedUrls].map((url) => this.ensureEntryFromUrl(url))
897
+ );
898
+ const acceptedModules = await Promise.all(
899
+ [...acceptedUrls].map((url) => this.ensureEntryFromUrl(url))
900
+ );
901
+ if (expectedInvalidationVersion !== void 0 && mod.invalidationVersion !== expectedInvalidationVersion) {
902
+ return null;
903
+ }
904
+ const previousImports = new Set(mod.importedModules);
905
+ for (const imported of previousImports) {
906
+ imported.importers.delete(mod);
907
+ }
908
+ mod.importedModules.clear();
909
+ mod.acceptedHmrDeps.clear();
910
+ for (const imported of importedModules) {
911
+ mod.importedModules.add(imported);
912
+ imported.importers.add(mod);
913
+ }
914
+ for (const accepted of acceptedModules) {
915
+ mod.acceptedHmrDeps.add(accepted);
916
+ }
917
+ mod.isSelfAccepting = isSelfAccepting;
918
+ const pruned = /* @__PURE__ */ new Set();
919
+ for (const imported of previousImports) {
920
+ if (!mod.importedModules.has(imported) && imported.importers.size === 0) {
921
+ pruned.add(imported);
922
+ }
923
+ }
924
+ return pruned;
925
+ }
742
926
  /** 使模块的转换缓存失效 */
743
- invalidateModule(mod) {
927
+ invalidateModule(mod, timestamp = Date.now()) {
744
928
  mod.transformResult = null;
745
- mod.lastHMRTimestamp = Date.now();
929
+ mod.lastHMRTimestamp = timestamp;
930
+ mod.invalidationVersion++;
931
+ }
932
+ /**
933
+ * 仅失效到 HMR 边界:显式接受依赖的模块本身不会重执行;自接受模块需要失效,
934
+ * 但不再继续影响其 importer。这样既能传播依赖时间戳,也不会隐式重复副作用。
935
+ */
936
+ invalidateModuleAndImporters(mod, timestamp = Date.now(), seen = /* @__PURE__ */ new Set()) {
937
+ if (seen.has(mod)) return;
938
+ seen.add(mod);
939
+ this.invalidateModule(mod, timestamp);
940
+ for (const importer of mod.importers) {
941
+ if (importer.acceptedHmrDeps.has(mod)) continue;
942
+ if (importer.isSelfAccepting) {
943
+ this.invalidateModule(importer, timestamp);
944
+ continue;
945
+ }
946
+ this.invalidateModuleAndImporters(importer, timestamp, seen);
947
+ }
746
948
  }
747
949
  /** 使所有模块缓存失效 */
748
950
  invalidateAll() {
@@ -753,34 +955,32 @@ var init_module_graph = __esm({
753
955
  /** 获取 HMR 传播边界 - 从变更模块向上遍历找到接受更新的边界 */
754
956
  getHmrBoundaries(mod) {
755
957
  const boundaries = [];
756
- const visited = /* @__PURE__ */ new Set();
757
- const propagate = (node, via) => {
758
- if (visited.has(node)) return true;
759
- visited.add(node);
760
- if (node.isSelfAccepting) {
761
- boundaries.push({ boundary: node, acceptedVia: via });
762
- return true;
958
+ const traversed = /* @__PURE__ */ new Set();
959
+ const addBoundary = (boundary, acceptedVia) => {
960
+ if (!boundaries.some(
961
+ (item) => item.boundary === boundary && item.acceptedVia === acceptedVia
962
+ )) {
963
+ boundaries.push({ boundary, acceptedVia });
763
964
  }
764
- if (node.acceptedHmrDeps.has(via)) {
765
- boundaries.push({ boundary: node, acceptedVia: via });
965
+ };
966
+ const propagate = (node) => {
967
+ if (traversed.has(node)) return true;
968
+ traversed.add(node);
969
+ if (node.isSelfAccepting) {
970
+ addBoundary(node, node);
766
971
  return true;
767
972
  }
768
973
  if (node.importers.size === 0) return false;
769
974
  for (const importer of node.importers) {
770
- if (!propagate(importer, node)) return false;
975
+ if (importer.acceptedHmrDeps.has(node)) {
976
+ addBoundary(importer, node);
977
+ continue;
978
+ }
979
+ if (!propagate(importer)) return false;
771
980
  }
772
981
  return true;
773
982
  };
774
- if (mod.isSelfAccepting) {
775
- boundaries.push({ boundary: mod, acceptedVia: mod });
776
- return boundaries;
777
- }
778
- for (const importer of mod.importers) {
779
- if (!propagate(importer, mod)) {
780
- return [];
781
- }
782
- }
783
- return boundaries;
983
+ return propagate(mod) ? boundaries : [];
784
984
  }
785
985
  };
786
986
  }
@@ -901,6 +1101,7 @@ var init_environment = __esm({
901
1101
  init_module_graph();
902
1102
  init_hot_channel();
903
1103
  init_debug();
1104
+ init_plugin_api();
904
1105
  debug = createDebugger("nasti:environment");
905
1106
  NastiEnvironment = class {
906
1107
  name;
@@ -909,6 +1110,7 @@ var init_environment = __esm({
909
1110
  config;
910
1111
  options;
911
1112
  hot;
1113
+ driver;
912
1114
  /** applyToEnvironment 过滤后的插件(init() 后可用) */
913
1115
  plugins = [];
914
1116
  /** per-env 插件容器(init() 后可用;dev 管线使用) */
@@ -916,6 +1118,8 @@ var init_environment = __esm({
916
1118
  /** per-env 模块图(dev 管线使用) */
917
1119
  moduleGraph;
918
1120
  candidatePlugins;
1121
+ pluginApi;
1122
+ buildMetadata = {};
919
1123
  initialized = false;
920
1124
  constructor(name, config, init = {}) {
921
1125
  const options = config.environments[name];
@@ -932,6 +1136,7 @@ var init_environment = __esm({
932
1136
  this.hot = init.hot ?? createNoopHotChannel();
933
1137
  this.moduleGraph = new ModuleGraph();
934
1138
  this.candidatePlugins = init.plugins ?? config.plugins;
1139
+ this.pluginApi = init.pluginApi ?? getPluginApi(config);
935
1140
  }
936
1141
  /** 过滤插件并建 per-env PluginContainer */
937
1142
  async init() {
@@ -942,10 +1147,57 @@ var init_environment = __esm({
942
1147
  { ...this.config, plugins: this.plugins },
943
1148
  this
944
1149
  );
1150
+ if (this.options.driver) {
1151
+ const claimed = [];
1152
+ for (const plugin of this.plugins) {
1153
+ const driver = await plugin.createEnvironmentDriver?.(this, this.pluginApi);
1154
+ if (driver) claimed.push({ plugin, driver });
1155
+ }
1156
+ if (claimed.length === 0) {
1157
+ throw new Error(
1158
+ `[nasti] environment "${this.name}" requested driver "${this.options.driver}", but no plugin provided it`
1159
+ );
1160
+ }
1161
+ if (claimed.length > 1) {
1162
+ throw new Error(
1163
+ `[nasti] environment "${this.name}" was claimed by multiple drivers: ` + claimed.map(({ plugin, driver }) => `${plugin.name} (${driver.name})`).join(", ")
1164
+ );
1165
+ }
1166
+ this.driver = claimed[0].driver;
1167
+ debug?.(`env "${this.name}" uses driver "${this.driver.name}"`);
1168
+ }
945
1169
  debug?.(`env "${this.name}" initialized (${this.plugins.length} plugins)`);
946
1170
  }
1171
+ getDriverContext() {
1172
+ return {
1173
+ environment: this,
1174
+ config: this.config,
1175
+ api: this.pluginApi,
1176
+ logger: this.config.logger
1177
+ };
1178
+ }
1179
+ setBuildMetadata(metadata) {
1180
+ const { entries: currentEntries, ...currentMetadata } = this.buildMetadata;
1181
+ const { entries, ...nextMetadata } = metadata;
1182
+ this.buildMetadata = {
1183
+ ...currentMetadata,
1184
+ ...nextMetadata,
1185
+ ...currentEntries || entries ? { entries: { ...currentEntries, ...entries } } : {}
1186
+ };
1187
+ }
1188
+ getBuildMetadata() {
1189
+ const { entries, ...metadata } = this.buildMetadata;
1190
+ return {
1191
+ ...metadata,
1192
+ ...entries ? { entries: { ...entries } } : {}
1193
+ };
1194
+ }
947
1195
  async close() {
948
- await this.hot.close?.();
1196
+ try {
1197
+ await this.driver?.close?.(this.getDriverContext());
1198
+ } finally {
1199
+ await this.hot.close?.();
1200
+ }
949
1201
  }
950
1202
  };
951
1203
  }
@@ -1010,7 +1262,8 @@ function transformCode(filename, code, options = {}) {
1010
1262
  importSource: options.jsxImportSource ?? "react",
1011
1263
  refresh: options.reactRefresh ?? false
1012
1264
  } : void 0,
1013
- sourcemap: options.sourcemap ?? true
1265
+ sourcemap: options.sourcemap ?? true,
1266
+ target: options.target
1014
1267
  });
1015
1268
  if (result.errors && result.errors.length > 0) {
1016
1269
  const msg = result.errors.map((e) => e.message ?? String(e)).join("\n");
@@ -1042,7 +1295,7 @@ function htmlPlugin(config) {
1042
1295
  transformIndexHtml(html) {
1043
1296
  const tags = [];
1044
1297
  if (config.command === "serve") {
1045
- const isReactLike = config.framework === "react" || config.framework === "auto";
1298
+ const isReactLike = config.framework === "react";
1046
1299
  if (isReactLike) {
1047
1300
  tags.push({
1048
1301
  tag: "script",
@@ -1096,8 +1349,8 @@ function serializeTag(tag) {
1096
1349
  }
1097
1350
  return ` <${tag.tag}${attrs}>${children}</${tag.tag}>`;
1098
1351
  }
1099
- async function readHtmlFile(root) {
1100
- const htmlPath = path2.resolve(root, "index.html");
1352
+ async function readHtmlFile(root, htmlFile = "index.html") {
1353
+ const htmlPath = path2.isAbsolute(htmlFile) ? htmlFile : path2.resolve(root, htmlFile);
1101
1354
  if (!fs2.existsSync(htmlPath)) return null;
1102
1355
  return fs2.readFileSync(htmlPath, "utf-8");
1103
1356
  }
@@ -1192,8 +1445,10 @@ import fs4 from "fs";
1192
1445
  import { createRequire } from "module";
1193
1446
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "url";
1194
1447
  import pc3 from "picocolors";
1195
- function getReactRefreshRuntimeEsm() {
1196
- if (__refreshRuntimeCache) return __refreshRuntimeCache;
1448
+ function getReactRefreshRuntimeEsm(includeBoundaryHelpers = false) {
1449
+ if (__refreshRuntimeCache) {
1450
+ return includeBoundaryHelpers ? __refreshRuntimeCache + REACT_REFRESH_BOUNDARY_HELPERS : __refreshRuntimeCache;
1451
+ }
1197
1452
  let cjsPath;
1198
1453
  try {
1199
1454
  const pkgPath = __require2.resolve("react-refresh/package.json");
@@ -1228,7 +1483,7 @@ export const findAffectedHostInstances = __rt.findAffectedHostInstances;
1228
1483
  export const collectCustomHooksForSignature = __rt.collectCustomHooksForSignature;
1229
1484
  export default __rt;
1230
1485
  `;
1231
- return __refreshRuntimeCache;
1486
+ return includeBoundaryHelpers ? __refreshRuntimeCache + REACT_REFRESH_BOUNDARY_HELPERS : __refreshRuntimeCache;
1232
1487
  }
1233
1488
  function buildReactRefreshWrapper(moduleUrl, transformedCode) {
1234
1489
  const urlLit = JSON.stringify(moduleUrl);
@@ -1254,22 +1509,40 @@ window.$RefreshReg$ = prevRefreshReg;
1254
1509
  window.$RefreshSig$ = prevRefreshSig;
1255
1510
 
1256
1511
  if (__nasti_hot__) {
1257
- __nasti_hot__.accept(() => {
1258
- clearTimeout(window.__nasti_refresh_timer__);
1259
- window.__nasti_refresh_timer__ = setTimeout(() => {
1260
- RefreshRuntime.performReactRefresh();
1261
- }, 30);
1512
+ let __nasti_current_exports__;
1513
+ __nasti_hot__.accept((nextExports) => {
1514
+ if (!nextExports) return;
1515
+ if (!__nasti_current_exports__) {
1516
+ __nasti_hot__.invalidate('Could not Fast Refresh (previous exports unavailable)');
1517
+ return;
1518
+ }
1519
+ const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(
1520
+ ${urlLit},
1521
+ __nasti_current_exports__,
1522
+ nextExports,
1523
+ );
1524
+ if (invalidateMessage) __nasti_hot__.invalidate(invalidateMessage);
1525
+ });
1526
+ RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
1527
+ __nasti_current_exports__ = currentExports;
1528
+ RefreshRuntime.registerExportsForReactRefresh(${urlLit}, currentExports);
1262
1529
  });
1263
1530
  }
1264
1531
  `;
1265
1532
  }
1266
1533
  function injectImportMetaHot(code, moduleUrl) {
1267
- if (!/\bimport\.meta\.hot\b/.test(code)) return code;
1534
+ const hotRE = /\bimport\.meta\.hot\b/g;
1535
+ const matches = [...maskStringsAndComments(code).matchAll(hotRE)];
1536
+ if (matches.length === 0) return code;
1537
+ for (const match of matches.reverse()) {
1538
+ const start = match.index;
1539
+ code = code.slice(0, start) + "__nasti_hot__" + code.slice(start + match[0].length);
1540
+ }
1268
1541
  const urlLit = JSON.stringify(moduleUrl);
1269
1542
  const header = `import { createHotContext as __nasti_createHotContext__ } from "/@nasti/client";
1270
1543
  const __nasti_hot__ = __nasti_createHotContext__(${urlLit});
1271
1544
  `;
1272
- return header + code.replace(/\bimport\.meta\.hot\b/g, "__nasti_hot__");
1545
+ return header + code;
1273
1546
  }
1274
1547
  function transformMiddleware(ctx) {
1275
1548
  ctx.envDefine = buildEnvDefine(
@@ -1296,7 +1569,10 @@ function transformMiddleware(ctx) {
1296
1569
  return;
1297
1570
  }
1298
1571
  if (url === "/" || url.endsWith(".html")) {
1299
- const html = await readHtmlFile(ctx.config.root);
1572
+ const html = await readHtmlFile(
1573
+ ctx.config.root,
1574
+ ctx.config.environments.client?.html
1575
+ );
1300
1576
  if (html) {
1301
1577
  let processedHtml = html;
1302
1578
  for (const plugin of ctx.config.plugins) {
@@ -1342,13 +1618,14 @@ function transformMiddleware(ctx) {
1342
1618
  }
1343
1619
  async function transformRequest(url, ctx) {
1344
1620
  const { config, pluginContainer, moduleGraph } = ctx;
1621
+ url = removeTimestampQuery(url);
1345
1622
  const cleanReqUrl = url.split("?")[0];
1346
1623
  const cached2 = moduleGraph.getModuleByUrl(url);
1347
1624
  if (cached2?.transformResult) {
1348
1625
  return cached2.transformResult;
1349
1626
  }
1350
1627
  if (cleanReqUrl === "/@react-refresh") {
1351
- return { code: getReactRefreshRuntimeEsm() };
1628
+ return { code: getReactRefreshRuntimeEsm(true) };
1352
1629
  }
1353
1630
  if (cleanReqUrl.startsWith("/@modules/") && url.includes("?")) {
1354
1631
  const idParam = new URLSearchParams(url.slice(url.indexOf("?") + 1)).get("id");
@@ -1384,6 +1661,8 @@ async function transformRequest(url, ctx) {
1384
1661
  }
1385
1662
  const rawQuery = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
1386
1663
  if (rawQuery && !/^t=\d+$/.test(rawQuery)) {
1664
+ const mod2 = await moduleGraph.ensureEntryFromUrl(url);
1665
+ const transformVersion2 = mod2.invalidationVersion;
1387
1666
  const loaded = await pluginContainer.load(url);
1388
1667
  if (loaded != null) {
1389
1668
  let code2 = typeof loaded === "string" ? loaded : loaded.code;
@@ -1391,16 +1670,28 @@ async function transformRequest(url, ctx) {
1391
1670
  if (transformed != null) {
1392
1671
  code2 = typeof transformed === "string" ? transformed : transformed.code;
1393
1672
  }
1394
- const mod2 = await moduleGraph.ensureEntryFromUrl(url);
1395
- moduleGraph.registerModule(mod2, cleanReqUrl);
1396
- code2 = injectImportMetaHot(code2, url);
1673
+ const parentFile = resolveUrlToFile(cleanReqUrl, config.root) ?? cleanReqUrl;
1674
+ moduleGraph.registerModule(mod2, parentFile);
1675
+ const hotInfo2 = rewriteHotAcceptDeps(code2, config, parentFile);
1676
+ code2 = injectImportMetaHot(hotInfo2.code, url);
1397
1677
  code2 = replaceEnvInCode(code2, ctx.envDefine ?? buildEnvDefine(
1398
1678
  loadEnv(config.mode, config.root, config.envPrefix),
1399
1679
  config.mode
1400
1680
  ));
1401
- code2 = rewriteImports(code2, config, cleanReqUrl);
1681
+ const importedUrls2 = /* @__PURE__ */ new Set();
1682
+ code2 = rewriteImports(code2, config, parentFile, importedUrls2, moduleGraph);
1683
+ const pruned2 = await moduleGraph.updateModuleInfo(
1684
+ mod2,
1685
+ importedUrls2,
1686
+ hotInfo2.acceptedUrls,
1687
+ hotInfo2.isSelfAccepting,
1688
+ transformVersion2
1689
+ );
1402
1690
  const transformResult2 = { code: code2 };
1403
- mod2.transformResult = transformResult2;
1691
+ if (pruned2) {
1692
+ if (pruned2.size > 0) ctx.onPrune?.([...pruned2].map((item) => item.url));
1693
+ mod2.transformResult = transformResult2;
1694
+ }
1404
1695
  return transformResult2;
1405
1696
  }
1406
1697
  }
@@ -1408,6 +1699,7 @@ async function transformRequest(url, ctx) {
1408
1699
  if (!filePath || !fs4.existsSync(filePath)) return null;
1409
1700
  const mod = await moduleGraph.ensureEntryFromUrl(url);
1410
1701
  moduleGraph.registerModule(mod, filePath);
1702
+ const transformVersion = mod.invalidationVersion;
1411
1703
  if (cleanReqUrl.startsWith("/@modules/")) {
1412
1704
  const code2 = await bundlePackageAsEsm(filePath, config.root);
1413
1705
  const transformResult2 = { code: code2 };
@@ -1434,9 +1726,10 @@ async function transformRequest(url, ctx) {
1434
1726
  if (useRefresh) {
1435
1727
  code = buildReactRefreshWrapper(stableUrl, code);
1436
1728
  wrappedWithRefresh = true;
1437
- mod.isSelfAccepting = true;
1438
1729
  }
1439
1730
  }
1731
+ const hotInfo = rewriteHotAcceptDeps(code, config, filePath);
1732
+ code = hotInfo.code;
1440
1733
  if (!wrappedWithRefresh) {
1441
1734
  code = injectImportMetaHot(code, stableUrl);
1442
1735
  }
@@ -1445,9 +1738,20 @@ async function transformRequest(url, ctx) {
1445
1738
  config.mode
1446
1739
  );
1447
1740
  code = replaceEnvInCode(code, envDefine);
1448
- code = rewriteImports(code, config, filePath);
1741
+ const importedUrls = /* @__PURE__ */ new Set();
1742
+ code = rewriteImports(code, config, filePath, importedUrls, moduleGraph);
1743
+ const pruned = await moduleGraph.updateModuleInfo(
1744
+ mod,
1745
+ importedUrls,
1746
+ hotInfo.acceptedUrls,
1747
+ wrappedWithRefresh || hotInfo.isSelfAccepting,
1748
+ transformVersion
1749
+ );
1449
1750
  const transformResult = { code };
1450
- mod.transformResult = transformResult;
1751
+ if (pruned) {
1752
+ if (pruned.size > 0) ctx.onPrune?.([...pruned].map((item) => item.url));
1753
+ mod.transformResult = transformResult;
1754
+ }
1451
1755
  return transformResult;
1452
1756
  }
1453
1757
  async function loadVirtualModule(spec, ctx) {
@@ -1652,49 +1956,202 @@ async function injectCjsNamedExports(code, entryFile) {
1652
1956
  return code;
1653
1957
  }
1654
1958
  }
1655
- function rewriteImports(code, config, filePath) {
1959
+ function rewriteImports(code, config, filePath, importedUrls, moduleGraph) {
1960
+ const resolveSpec = createModuleSpecifierResolver(config, filePath);
1961
+ const transformSpec = (spec) => {
1962
+ const resolved = removeTimestampQuery(resolveSpec(spec));
1963
+ importedUrls?.add(resolved);
1964
+ const timestamp = moduleGraph?.getModuleByUrl(resolved)?.lastHMRTimestamp ?? 0;
1965
+ return timestamp > 0 ? appendTimestampQuery(resolved, timestamp) : resolved;
1966
+ };
1967
+ return code.replace(
1968
+ /\bfrom\s+(['"])([^'"]+)\1/g,
1969
+ (_m, q, s) => `from ${q}${transformSpec(s)}${q}`
1970
+ ).replace(
1971
+ /\bimport\s+(['"])([^'"]+)\1/g,
1972
+ (_m, q, s) => `import ${q}${transformSpec(s)}${q}`
1973
+ ).replace(
1974
+ /\bimport\s*\(\s*(['"])([^'"]+)\1\s*\)/g,
1975
+ (_m, q, s) => `import(${q}${transformSpec(s)}${q})`
1976
+ );
1977
+ }
1978
+ function createModuleSpecifierResolver(config, filePath) {
1656
1979
  const root = config.root;
1657
1980
  const fileDir = path4.dirname(filePath);
1658
1981
  const aliasEntries = Object.entries(config.resolve.alias).sort(
1659
1982
  ([a], [b]) => b.length - a.length
1660
1983
  );
1661
1984
  const toRootUrl = (abs) => "/" + path4.relative(root, abs).replace(/\\/g, "/");
1662
- const transformSpec = (spec) => {
1663
- const suffixMatch = spec.match(/[?#].*$/);
1985
+ return (specifier) => {
1986
+ const suffixMatch = specifier.match(/[?#].*$/);
1664
1987
  const suffix = suffixMatch ? suffixMatch[0] : "";
1665
- const baseSpec = suffix ? spec.slice(0, -suffix.length) : spec;
1988
+ const baseSpec = suffix ? specifier.slice(0, -suffix.length) : specifier;
1666
1989
  for (const [key, value] of aliasEntries) {
1667
1990
  if (baseSpec === key || baseSpec.startsWith(key + "/")) {
1668
1991
  const aliasBase = resolveAliasTarget(value, root);
1669
1992
  const sub = baseSpec.slice(key.length).replace(/^\//, "");
1670
1993
  const target = sub ? path4.join(aliasBase, sub) : aliasBase;
1671
1994
  const resolved = tryResolveDiskPath(target);
1672
- return resolved && isUnderRoot(resolved, root) ? toRootUrl(resolved) + suffix : spec;
1995
+ return resolved && isUnderRoot(resolved, root) ? toRootUrl(resolved) + suffix : specifier;
1673
1996
  }
1674
1997
  }
1675
1998
  if (baseSpec.startsWith("./") || baseSpec.startsWith("../")) {
1676
- const target = path4.resolve(fileDir, baseSpec);
1677
- const resolved = tryResolveDiskPath(target);
1678
- return resolved && isUnderRoot(resolved, root) ? toRootUrl(resolved) + suffix : spec;
1999
+ const resolved = tryResolveDiskPath(path4.resolve(fileDir, baseSpec));
2000
+ return resolved && isUnderRoot(resolved, root) ? toRootUrl(resolved) + suffix : specifier;
1679
2001
  }
1680
2002
  if (baseSpec.startsWith("/") && !baseSpec.startsWith("/@")) {
1681
- const target = path4.join(root, baseSpec.replace(/^\//, ""));
1682
- const resolved = tryResolveDiskPath(target);
1683
- return resolved && isUnderRoot(resolved, root) ? toRootUrl(resolved) + suffix : spec;
2003
+ const resolved = tryResolveDiskPath(path4.join(root, baseSpec.replace(/^\//, "")));
2004
+ return resolved && isUnderRoot(resolved, root) ? toRootUrl(resolved) + suffix : specifier;
1684
2005
  }
1685
- if (baseSpec.startsWith("/")) return spec;
1686
- return `/@modules/${spec}`;
2006
+ if (baseSpec.startsWith("/")) return specifier;
2007
+ return `/@modules/${specifier}`;
1687
2008
  };
1688
- return code.replace(
1689
- /\bfrom\s+(['"])([^'"]+)\1/g,
1690
- (_m, q, s) => `from ${q}${transformSpec(s)}${q}`
1691
- ).replace(
1692
- /\bimport\s+(['"])([^'"]+)\1/g,
1693
- (_m, q, s) => `import ${q}${transformSpec(s)}${q}`
1694
- ).replace(
1695
- /\bimport\s*\(\s*(['"])([^'"]+)\1\s*\)/g,
1696
- (_m, q, s) => `import(${q}${transformSpec(s)}${q})`
1697
- );
2009
+ }
2010
+ function rewriteHotAcceptDeps(code, config, filePath) {
2011
+ const acceptedUrls = /* @__PURE__ */ new Set();
2012
+ const edits = [];
2013
+ const resolveSpec = createModuleSpecifierResolver(config, filePath);
2014
+ const acceptRE = /(?:\bimport\.meta\.hot|\b__nasti_hot__)(?:(?:\?\.)|\.)accept\s*\(/g;
2015
+ const searchableCode = maskStringsAndComments(code);
2016
+ let isSelfAccepting = false;
2017
+ let match;
2018
+ while (match = acceptRE.exec(searchableCode)) {
2019
+ let cursor = match.index + match[0].length;
2020
+ const skipTrivia = () => {
2021
+ while (cursor < code.length) {
2022
+ if (/\s/.test(code[cursor])) {
2023
+ cursor++;
2024
+ continue;
2025
+ }
2026
+ if (code[cursor] === "/" && code[cursor + 1] === "/") {
2027
+ cursor += 2;
2028
+ while (cursor < code.length && code[cursor] !== "\n") cursor++;
2029
+ continue;
2030
+ }
2031
+ if (code[cursor] === "/" && code[cursor + 1] === "*") {
2032
+ cursor += 2;
2033
+ while (cursor < code.length && !(code[cursor] === "*" && code[cursor + 1] === "/")) cursor++;
2034
+ cursor += 2;
2035
+ continue;
2036
+ }
2037
+ break;
2038
+ }
2039
+ };
2040
+ skipTrivia();
2041
+ const first = code[cursor];
2042
+ if (!first || first === ")" || first !== "[" && first !== "'" && first !== '"' && first !== "`") {
2043
+ isSelfAccepting = true;
2044
+ continue;
2045
+ }
2046
+ const readLiteral = () => {
2047
+ const quote = code[cursor];
2048
+ if (quote !== "'" && quote !== '"' && quote !== "`") return;
2049
+ const start = cursor;
2050
+ cursor++;
2051
+ let raw = "";
2052
+ while (cursor < code.length) {
2053
+ const char = code[cursor];
2054
+ if (char === "\\") {
2055
+ raw += code[cursor + 1] ?? "";
2056
+ cursor += 2;
2057
+ continue;
2058
+ }
2059
+ if (char === quote) {
2060
+ cursor++;
2061
+ const resolved = removeTimestampQuery(resolveSpec(raw));
2062
+ acceptedUrls.add(resolved);
2063
+ edits.push({ start, end: cursor, value: JSON.stringify(resolved) });
2064
+ return;
2065
+ }
2066
+ if (quote === "`" && char === "$" && code[cursor + 1] === "{") return;
2067
+ raw += char;
2068
+ cursor++;
2069
+ }
2070
+ };
2071
+ if (first === "[") {
2072
+ cursor++;
2073
+ while (cursor < code.length) {
2074
+ skipTrivia();
2075
+ if (code[cursor] === ",") {
2076
+ cursor++;
2077
+ skipTrivia();
2078
+ }
2079
+ if (code[cursor] === "]") break;
2080
+ const before = cursor;
2081
+ readLiteral();
2082
+ if (cursor === before) break;
2083
+ }
2084
+ } else {
2085
+ readLiteral();
2086
+ }
2087
+ }
2088
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
2089
+ code = code.slice(0, edit.start) + edit.value + code.slice(edit.end);
2090
+ }
2091
+ return { code, acceptedUrls, isSelfAccepting };
2092
+ }
2093
+ function maskStringsAndComments(code) {
2094
+ const masked = code.split("");
2095
+ let state = "code";
2096
+ const isRegexStart = (index2) => {
2097
+ let previous = index2 - 1;
2098
+ while (previous >= 0 && /\s/.test(code[previous])) previous--;
2099
+ return previous < 0 || "=(:,!&|?{};[]+-*%^~<>".includes(code[previous]);
2100
+ };
2101
+ for (let i = 0; i < code.length; i++) {
2102
+ const char = code[i];
2103
+ const next = code[i + 1];
2104
+ if (state === "code") {
2105
+ if (char === "'") state = "single";
2106
+ else if (char === '"') state = "double";
2107
+ else if (char === "`") state = "template";
2108
+ else if (char === "/" && next === "/") state = "line-comment";
2109
+ else if (char === "/" && next === "*") state = "block-comment";
2110
+ else if (char === "/" && isRegexStart(i)) state = "regex";
2111
+ else continue;
2112
+ masked[i] = " ";
2113
+ continue;
2114
+ }
2115
+ if (state === "line-comment") {
2116
+ if (char === "\n") {
2117
+ state = "code";
2118
+ } else {
2119
+ masked[i] = " ";
2120
+ }
2121
+ continue;
2122
+ }
2123
+ if (state === "block-comment") {
2124
+ masked[i] = char === "\n" ? "\n" : " ";
2125
+ if (char === "*" && next === "/") {
2126
+ masked[i + 1] = " ";
2127
+ i++;
2128
+ state = "code";
2129
+ }
2130
+ continue;
2131
+ }
2132
+ if (state === "regex" || state === "regex-class") {
2133
+ masked[i] = char === "\n" ? "\n" : " ";
2134
+ if (char === "\\") {
2135
+ if (i + 1 < code.length) masked[++i] = " ";
2136
+ } else if (state === "regex" && char === "[") {
2137
+ state = "regex-class";
2138
+ } else if (state === "regex-class" && char === "]") {
2139
+ state = "regex";
2140
+ } else if (state === "regex" && char === "/") {
2141
+ state = "code";
2142
+ }
2143
+ continue;
2144
+ }
2145
+ masked[i] = char === "\n" ? "\n" : " ";
2146
+ if (char === "\\") {
2147
+ if (i + 1 < code.length) masked[++i] = " ";
2148
+ continue;
2149
+ }
2150
+ if (state === "single" && char === "'" || state === "double" && char === '"' || state === "template" && char === "`") {
2151
+ state = "code";
2152
+ }
2153
+ }
2154
+ return masked.join("");
1698
2155
  }
1699
2156
  function resolveAliasTarget(value, root) {
1700
2157
  if (path4.isAbsolute(value) && fs4.existsSync(value)) return value;
@@ -1719,6 +2176,12 @@ function isUnderRoot(abs, root) {
1719
2176
  const rel = path4.relative(root, abs);
1720
2177
  return !!rel && !rel.startsWith("..") && !path4.isAbsolute(rel);
1721
2178
  }
2179
+ function appendTimestampQuery(url, timestamp) {
2180
+ const hashIndex = url.indexOf("#");
2181
+ const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
2182
+ const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
2183
+ return `${withoutHash}${withoutHash.includes("?") ? "&" : "?"}t=${timestamp}${hash}`;
2184
+ }
1722
2185
  function externalSpecToModuleUrl(spec, baseDir, root) {
1723
2186
  const resolved = resolveNodeModule(baseDir, spec);
1724
2187
  if (!resolved) return `/@modules/${spec}`;
@@ -1862,30 +2325,29 @@ function isModuleRequest(url) {
1862
2325
  function getHmrClientCode() {
1863
2326
  return `
1864
2327
  // Nasti HMR Client
1865
- const socket = new WebSocket(\`ws://\${location.host}\`, 'nasti-hmr');
2328
+ const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
2329
+ const socket = new WebSocket(socketProtocol + '://' + location.host, 'nasti-hmr');
1866
2330
  const hotModulesMap = new Map();
1867
2331
  const disposeMap = new Map();
1868
2332
  const pruneMap = new Map();
2333
+ const dataMap = new Map();
2334
+ let updateQueue = [];
2335
+ let pendingUpdateQueue = false;
1869
2336
 
1870
2337
  socket.addEventListener('message', async ({ data }) => {
1871
2338
  const payload = JSON.parse(data);
1872
2339
  switch (payload.type) {
1873
2340
  case 'connected':
1874
- console.log('[nasti] connected.');
2341
+ console.debug('[nasti] connected.');
1875
2342
  clearErrorOverlay();
1876
2343
  break;
1877
2344
  case 'update':
1878
2345
  try {
1879
- await Promise.all(payload.updates.map((update) => {
1880
- if (update.type === 'js-update') {
1881
- return fetchUpdate(update);
1882
- } else if (update.type === 'css-update') {
1883
- return updateCss(update.path);
1884
- }
1885
- }));
2346
+ // CSS \u5728 unbundled \u6A21\u5F0F\u4E0B\u4E5F\u662F\u4F1A\u6CE8\u5165 <style> \u7684 JS \u6A21\u5757\uFF0C\u548C\u666E\u901A JS
2347
+ // \u4E00\u6837\u91CD\u65B0 import \u624D\u80FD\u6267\u884C dispose/accept \u5E76\u4FDD\u6301\u9875\u9762\u72B6\u6001\u3002
2348
+ await Promise.all(payload.updates.map(queueUpdate));
1886
2349
  clearErrorOverlay();
1887
- console.log('[nasti] HMR update complete, reloading page');
1888
- location.reload();
2350
+ console.debug('[nasti] HMR update complete.');
1889
2351
  } catch (err) {
1890
2352
  console.error('[nasti] HMR update failed:', err);
1891
2353
  showErrorOverlay(err);
@@ -1896,10 +2358,17 @@ socket.addEventListener('message', async ({ data }) => {
1896
2358
  location.reload();
1897
2359
  break;
1898
2360
  case 'prune':
1899
- payload.paths.forEach((p) => {
1900
- const cb = pruneMap.get(p);
1901
- if (cb) cb();
1902
- });
2361
+ await Promise.all(payload.paths.map(async (path) => {
2362
+ const data = dataMap.get(path);
2363
+ const dispose = disposeMap.get(path);
2364
+ const prune = pruneMap.get(path);
2365
+ if (dispose) await dispose(data);
2366
+ if (prune) await prune(data);
2367
+ hotModulesMap.delete(path);
2368
+ disposeMap.delete(path);
2369
+ pruneMap.delete(path);
2370
+ dataMap.delete(path);
2371
+ }));
1903
2372
  break;
1904
2373
  case 'error':
1905
2374
  console.error('[nasti] error:', payload.err.message);
@@ -1908,33 +2377,64 @@ socket.addEventListener('message', async ({ data }) => {
1908
2377
  }
1909
2378
  });
1910
2379
 
1911
- // \u81EA\u52A8\u91CD\u8FDE\uFF08\u65AD\u7EBF\u65F6\u6307\u6570\u9000\u907F\uFF09
2380
+ // \u670D\u52A1\u91CD\u542F\u540E\u65E7\u6A21\u5757\u56FE\u5DF2\u5931\u6548\uFF0C\u91CD\u8FDE\u65F6\u6574\u9875\u5237\u65B0\u662F\u5FC5\u8981\u515C\u5E95\uFF1B\u6B63\u5E38 update \u4E0D\u518D\u5237\u65B0\u3002
1912
2381
  let reconnectTimer = 0;
1913
2382
  socket.addEventListener('close', () => {
1914
2383
  clearTimeout(reconnectTimer);
1915
2384
  reconnectTimer = setTimeout(() => location.reload(), 1000);
1916
2385
  });
1917
2386
 
2387
+ /**
2388
+ * \u540C\u4E00\u6279\u66F4\u65B0\u5148\u5168\u90E8\u62C9\u53D6\uFF0C\u518D\u6309\u670D\u52A1\u7AEF\u6D88\u606F\u987A\u5E8F\u6267\u884C accept \u56DE\u8C03\uFF0C\u907F\u514D HTTP \u5F80\u8FD4\u901F\u5EA6
2389
+ * \u6539\u53D8\u6A21\u5757\u5E94\u7528\u987A\u5E8F\u3002\u8FD9\u4E0E Vite HMRClient \u7684 fetch/apply \u4E24\u9636\u6BB5\u4E00\u81F4\u3002
2390
+ */
2391
+ async function queueUpdate(update) {
2392
+ updateQueue.push(fetchUpdate(update));
2393
+ if (pendingUpdateQueue) return;
2394
+
2395
+ pendingUpdateQueue = true;
2396
+ await Promise.resolve();
2397
+ pendingUpdateQueue = false;
2398
+ const loading = updateQueue;
2399
+ updateQueue = [];
2400
+ const applyUpdates = await Promise.all(loading);
2401
+ for (const apply of applyUpdates) {
2402
+ if (apply) apply();
2403
+ }
2404
+ }
2405
+
1918
2406
  async function fetchUpdate(update) {
1919
2407
  const mod = hotModulesMap.get(update.path);
1920
- // \u5148\u8DD1 dispose\uFF08\u7ED9\u6A21\u5757\u673A\u4F1A\u6E05\u7406\u526F\u4F5C\u7528\uFF09
1921
- const dispose = disposeMap.get(update.path);
1922
- if (dispose) dispose();
2408
+ // \u5C1A\u672A\u5728\u5F53\u524D\u9875\u9762\u52A0\u8F7D\u7684\u52A8\u6001\u6A21\u5757\u4E0D\u9700\u8981\u66F4\u65B0\u3002
2409
+ if (!mod) return;
1923
2410
 
1924
- const newMod = await import(update.acceptedPath + '?t=' + update.timestamp);
1925
- if (mod) {
1926
- // \u590D\u5236\u56DE\u8C03\u6570\u7EC4\u907F\u514D\u56DE\u8C03\u5185\u90E8\u53C8\u4FEE\u6539 hotModulesMap \u9020\u6210\u8FED\u4EE3\u5F02\u5E38
1927
- [...mod.callbacks].forEach((cb) => cb(newMod));
1928
- }
2411
+ // \u5FC5\u987B\u5728\u91CD\u65B0 import \u524D\u786E\u5B9A\u65E7\u56DE\u8C03\uFF1B\u65B0\u6A21\u5757\u6267\u884C createHotContext \u65F6\u4F1A\u6E05\u7A7A\u5E76\u6CE8\u518C\u65B0\u56DE\u8C03\u3002
2412
+ const qualifiedCallbacks = mod.callbacks.filter(({ deps }) =>
2413
+ deps.includes(update.acceptedPath)
2414
+ );
2415
+ const isSelfUpdate = update.path === update.acceptedPath;
2416
+ if (!isSelfUpdate && qualifiedCallbacks.length === 0) return;
2417
+
2418
+ const dispose = disposeMap.get(update.acceptedPath);
2419
+ if (dispose) await dispose(dataMap.get(update.acceptedPath));
2420
+ const newMod = await import(appendTimestampQuery(update.acceptedPath, update.timestamp));
2421
+
2422
+ return () => {
2423
+ for (const { deps, fn } of qualifiedCallbacks) {
2424
+ fn(deps.map((dep) => dep === update.acceptedPath ? newMod : undefined));
2425
+ }
2426
+ const detail = isSelfUpdate
2427
+ ? update.path
2428
+ : update.acceptedPath + ' via ' + update.path;
2429
+ console.debug('[nasti] hot updated:', detail);
2430
+ };
1929
2431
  }
1930
2432
 
1931
- function updateCss(path) {
1932
- const el = document.querySelector(\`style[data-nasti-css="\${path}"]\`);
1933
- if (el) {
1934
- return fetch(path + '?t=' + Date.now())
1935
- .then(r => r.text())
1936
- .then(css => { el.textContent = css; });
1937
- }
2433
+ function appendTimestampQuery(url, timestamp) {
2434
+ const hashIndex = url.indexOf('#');
2435
+ const hash = hashIndex >= 0 ? url.slice(hashIndex) : '';
2436
+ const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
2437
+ return withoutHash + (withoutHash.includes('?') ? '&' : '?') + 't=' + timestamp + hash;
1938
2438
  }
1939
2439
 
1940
2440
  function clearErrorOverlay() {
@@ -1962,23 +2462,30 @@ function showErrorOverlay(err) {
1962
2462
  document.body.appendChild(overlay);
1963
2463
  }
1964
2464
 
1965
- /**
1966
- * \u751F\u6210 import.meta.hot \u7684 hot context\u3002
1967
- * \u5173\u952E\u7EA6\u675F\uFF1A\u540C\u4E00 ownerPath \u7684 accept \u56DE\u8C03\u5FC5\u987B\u66FF\u6362\uFF08\u4E0D\u662F append\uFF09\u3002
1968
- * \u6BCF\u6B21\u6A21\u5757\u91CD\u65B0 import \u90FD\u4F1A\u8C03\u7528 createHotContext\uFF0C\u65E7\u56DE\u8C03\u4F1A\u88AB fetchUpdate \u8C03\u7528\u540E\u7ACB\u5373\u88AB\u65B0 import
1969
- * \u91CC\u7684 accept \u66FF\u6362\u3002\u4E0D\u66FF\u6362\u7684\u8BDD\u6BCF\u7F16\u8F91\u4E00\u6B21\u5C31\u591A\u4E00\u4E2A\u56DE\u8C03\uFF0C\u8D8A\u8DD1\u8D8A\u6162\u3002
1970
- */
1971
2465
  export function createHotContext(ownerPath) {
2466
+ if (!dataMap.has(ownerPath)) dataMap.set(ownerPath, {});
2467
+
2468
+ // \u6A21\u5757\u91CD\u65B0\u6267\u884C\u65F6\u4E22\u5F03\u65E7 accept \u56DE\u8C03\uFF0C\u4F46\u4FDD\u7559\u540C\u4E00\u4E2A hot.data \u5BF9\u8C61\u3002
2469
+ const existing = hotModulesMap.get(ownerPath);
2470
+ if (existing) existing.callbacks = [];
2471
+
2472
+ const acceptDeps = (deps, callback = () => {}) => {
2473
+ const mod = hotModulesMap.get(ownerPath) || { id: ownerPath, callbacks: [] };
2474
+ mod.callbacks.push({ deps, fn: callback });
2475
+ hotModulesMap.set(ownerPath, mod);
2476
+ };
2477
+
1972
2478
  return {
1973
2479
  accept(deps, callback) {
1974
- // \u81EA\u63A5\u53D7: hot.accept() \u6216 hot.accept(callback)
1975
2480
  if (typeof deps === 'function' || deps === undefined) {
1976
- hotModulesMap.set(ownerPath, { callbacks: [deps || (() => {})] });
1977
- return;
2481
+ acceptDeps([ownerPath], ([mod]) => deps?.(mod));
2482
+ } else if (typeof deps === 'string') {
2483
+ acceptDeps([deps], ([mod]) => callback?.(mod));
2484
+ } else if (Array.isArray(deps)) {
2485
+ acceptDeps(deps, callback);
2486
+ } else {
2487
+ throw new Error('invalid hot.accept() usage');
1978
2488
  }
1979
- // \u4F9D\u8D56\u63A5\u53D7: hot.accept(deps, callback)\uFF0C\u591A\u6B21\u8C03\u7528\u8FFD\u52A0
1980
- const existing = hotModulesMap.get(ownerPath)?.callbacks ?? [];
1981
- hotModulesMap.set(ownerPath, { callbacks: [...existing, callback] });
1982
2489
  },
1983
2490
  prune(callback) {
1984
2491
  pruneMap.set(ownerPath, callback);
@@ -1989,21 +2496,85 @@ export function createHotContext(ownerPath) {
1989
2496
  invalidate() {
1990
2497
  location.reload();
1991
2498
  },
1992
- data: {},
2499
+ data: dataMap.get(ownerPath),
1993
2500
  };
1994
2501
  }
1995
2502
  `;
1996
2503
  }
1997
- var __dirname_esm, __require2, __refreshRuntimeCache, REACT_REFRESH_GLOBAL_PREAMBLE, esmBundleCache, VALID_IDENT, RESOLVE_EXTENSIONS, ESM_CONDITIONS;
2504
+ var __dirname_esm, __require2, __refreshRuntimeCache, REACT_REFRESH_BOUNDARY_HELPERS, REACT_REFRESH_GLOBAL_PREAMBLE, esmBundleCache, VALID_IDENT, RESOLVE_EXTENSIONS, ESM_CONDITIONS;
1998
2505
  var init_middleware = __esm({
1999
2506
  "src/server/middleware.ts"() {
2000
2507
  "use strict";
2001
2508
  init_transformer();
2002
2509
  init_html();
2003
2510
  init_env();
2511
+ init_url();
2004
2512
  __dirname_esm = path4.dirname(fileURLToPath(import.meta.url));
2005
2513
  __require2 = createRequire(import.meta.url);
2006
2514
  __refreshRuntimeCache = null;
2515
+ REACT_REFRESH_BOUNDARY_HELPERS = `
2516
+ function __nastiIsPlainObject(obj) {
2517
+ return Object.prototype.toString.call(obj) === '[object Object]' &&
2518
+ (obj.constructor === Object || obj.constructor === undefined);
2519
+ }
2520
+ function __nastiIsCompoundComponent(type) {
2521
+ if (!__nastiIsPlainObject(type)) return false;
2522
+ for (const key in type) {
2523
+ if (!isLikelyComponentType(type[key])) return false;
2524
+ }
2525
+ return true;
2526
+ }
2527
+ export function registerExportsForReactRefresh(filename, moduleExports) {
2528
+ for (const key in moduleExports) {
2529
+ if (key === '__esModule') continue;
2530
+ const value = moduleExports[key];
2531
+ if (isLikelyComponentType(value)) {
2532
+ register(value, filename + ' export ' + key);
2533
+ } else if (__nastiIsCompoundComponent(value)) {
2534
+ for (const subKey in value) {
2535
+ register(value[subKey], filename + ' export ' + key + '-' + subKey);
2536
+ }
2537
+ }
2538
+ }
2539
+ }
2540
+ let __nastiRefreshTimer;
2541
+ function __nastiEnqueueRefresh() {
2542
+ clearTimeout(__nastiRefreshTimer);
2543
+ __nastiRefreshTimer = setTimeout(() => performReactRefresh(), 16);
2544
+ }
2545
+ function __nastiCheckExports(ignored, exports, predicate) {
2546
+ for (const key in exports) {
2547
+ if (ignored.includes(key)) continue;
2548
+ if (!predicate(key, exports[key])) return key;
2549
+ }
2550
+ return true;
2551
+ }
2552
+ export function validateRefreshBoundaryAndEnqueueUpdate(id, prevExports, nextExports) {
2553
+ const ignored = window.__getReactRefreshIgnoredExports?.({ id }) ?? [];
2554
+ if (__nastiCheckExports(ignored, prevExports, (key) => key in nextExports) !== true) {
2555
+ return 'Could not Fast Refresh (export removed)';
2556
+ }
2557
+ if (__nastiCheckExports(ignored, nextExports, (key) => key in prevExports) !== true) {
2558
+ return 'Could not Fast Refresh (new export)';
2559
+ }
2560
+ let hasExports = false;
2561
+ const compatible = __nastiCheckExports(ignored, nextExports, (key, value) => {
2562
+ hasExports = true;
2563
+ return isLikelyComponentType(value) ||
2564
+ __nastiIsCompoundComponent(value) ||
2565
+ prevExports[key] === value;
2566
+ });
2567
+ if (!hasExports) {
2568
+ return 'Could not Fast Refresh (no exports)';
2569
+ }
2570
+ if (compatible === true) {
2571
+ __nastiEnqueueRefresh();
2572
+ return;
2573
+ }
2574
+ return 'Could not Fast Refresh ("' + compatible + '" export is incompatible)';
2575
+ }
2576
+ export const __hmr_import = (module) => import(module);
2577
+ `;
2007
2578
  REACT_REFRESH_GLOBAL_PREAMBLE = `
2008
2579
  import RefreshRuntime from "/@react-refresh";
2009
2580
  RefreshRuntime.injectIntoGlobalHook(window);
@@ -2033,8 +2604,10 @@ async function handleFileChange(file, server) {
2033
2604
  }
2034
2605
  const updates = [];
2035
2606
  const timestamp = Date.now();
2607
+ const graph = moduleGraph;
2608
+ const invalidatedModules = /* @__PURE__ */ new Set();
2036
2609
  for (const mod of mods) {
2037
- moduleGraph.invalidateModule(mod);
2610
+ graph.invalidateModuleAndImporters(mod, timestamp, invalidatedModules);
2038
2611
  const ctx = {
2039
2612
  file,
2040
2613
  timestamp,
@@ -2052,19 +2625,25 @@ async function handleFileChange(file, server) {
2052
2625
  }
2053
2626
  }
2054
2627
  for (const affected of affectedModules) {
2055
- const boundaries = moduleGraph.getHmrBoundaries(affected);
2628
+ graph.invalidateModuleAndImporters(affected, timestamp, invalidatedModules);
2629
+ const boundaries = graph.getHmrBoundaries(affected);
2056
2630
  if (boundaries.length === 0) {
2057
2631
  logger.info(pc4.green("page reload ") + pc4.dim(shortFile), { timestamp: true });
2058
2632
  ws.send({ type: "full-reload", path: relativePath });
2059
2633
  return;
2060
2634
  }
2061
- for (const { boundary } of boundaries) {
2062
- updates.push({
2635
+ for (const { boundary, acceptedVia } of boundaries) {
2636
+ const update = {
2063
2637
  type: boundary.type === "css" ? "css-update" : "js-update",
2064
2638
  path: boundary.url,
2065
- acceptedPath: affected.url,
2639
+ acceptedPath: acceptedVia.url,
2066
2640
  timestamp
2067
- });
2641
+ };
2642
+ if (!updates.some(
2643
+ (existing) => existing.type === update.type && existing.path === update.path && existing.acceptedPath === update.acceptedPath
2644
+ )) {
2645
+ updates.push(update);
2646
+ }
2068
2647
  }
2069
2648
  }
2070
2649
  }
@@ -2134,6 +2713,7 @@ function resolvePlugin(config) {
2134
2713
  }
2135
2714
  if (!source.startsWith("/") && !source.startsWith(".")) {
2136
2715
  if (vueRuntimeEntry && source === "vue") return vueRuntimeEntry;
2716
+ if (config.command === "build") return null;
2137
2717
  try {
2138
2718
  const resolved = require2.resolve(source, {
2139
2719
  paths: [importer ? path6.dirname(importer) : config.root]
@@ -2222,27 +2802,27 @@ var require_process = __commonJS({
2222
2802
  var require_filesystem = __commonJS({
2223
2803
  "node_modules/detect-libc/lib/filesystem.js"(exports, module) {
2224
2804
  "use strict";
2225
- var fs12 = __require("fs");
2805
+ var fs13 = __require("fs");
2226
2806
  var LDD_PATH = "/usr/bin/ldd";
2227
2807
  var SELF_PATH = "/proc/self/exe";
2228
2808
  var MAX_LENGTH = 2048;
2229
- var readFileSync = (path17) => {
2230
- const fd = fs12.openSync(path17, "r");
2809
+ var readFileSync = (path18) => {
2810
+ const fd = fs13.openSync(path18, "r");
2231
2811
  const buffer = Buffer.alloc(MAX_LENGTH);
2232
- const bytesRead = fs12.readSync(fd, buffer, 0, MAX_LENGTH, 0);
2233
- fs12.close(fd, () => {
2812
+ const bytesRead = fs13.readSync(fd, buffer, 0, MAX_LENGTH, 0);
2813
+ fs13.close(fd, () => {
2234
2814
  });
2235
2815
  return buffer.subarray(0, bytesRead);
2236
2816
  };
2237
- var readFile = (path17) => new Promise((resolve, reject) => {
2238
- fs12.open(path17, "r", (err, fd) => {
2817
+ var readFile = (path18) => new Promise((resolve, reject) => {
2818
+ fs13.open(path18, "r", (err, fd) => {
2239
2819
  if (err) {
2240
2820
  reject(err);
2241
2821
  } else {
2242
2822
  const buffer = Buffer.alloc(MAX_LENGTH);
2243
- fs12.read(fd, buffer, 0, MAX_LENGTH, 0, (_, bytesRead) => {
2823
+ fs13.read(fd, buffer, 0, MAX_LENGTH, 0, (_, bytesRead) => {
2244
2824
  resolve(buffer.subarray(0, bytesRead));
2245
- fs12.close(fd, () => {
2825
+ fs13.close(fd, () => {
2246
2826
  });
2247
2827
  });
2248
2828
  }
@@ -2354,11 +2934,11 @@ var require_detect_libc = __commonJS({
2354
2934
  }
2355
2935
  return null;
2356
2936
  };
2357
- var familyFromInterpreterPath = (path17) => {
2358
- if (path17) {
2359
- if (path17.includes("/ld-musl-")) {
2937
+ var familyFromInterpreterPath = (path18) => {
2938
+ if (path18) {
2939
+ if (path18.includes("/ld-musl-")) {
2360
2940
  return MUSL;
2361
- } else if (path17.includes("/ld-linux-")) {
2941
+ } else if (path18.includes("/ld-linux-")) {
2362
2942
  return GLIBC;
2363
2943
  }
2364
2944
  }
@@ -2405,8 +2985,8 @@ var require_detect_libc = __commonJS({
2405
2985
  cachedFamilyInterpreter = null;
2406
2986
  try {
2407
2987
  const selfContent = await readFile(SELF_PATH);
2408
- const path17 = interpreterPath(selfContent);
2409
- cachedFamilyInterpreter = familyFromInterpreterPath(path17);
2988
+ const path18 = interpreterPath(selfContent);
2989
+ cachedFamilyInterpreter = familyFromInterpreterPath(path18);
2410
2990
  } catch (e) {
2411
2991
  }
2412
2992
  return cachedFamilyInterpreter;
@@ -2418,8 +2998,8 @@ var require_detect_libc = __commonJS({
2418
2998
  cachedFamilyInterpreter = null;
2419
2999
  try {
2420
3000
  const selfContent = readFileSync(SELF_PATH);
2421
- const path17 = interpreterPath(selfContent);
2422
- cachedFamilyInterpreter = familyFromInterpreterPath(path17);
3001
+ const path18 = interpreterPath(selfContent);
3002
+ cachedFamilyInterpreter = familyFromInterpreterPath(path18);
2423
3003
  } catch (e) {
2424
3004
  }
2425
3005
  return cachedFamilyInterpreter;
@@ -3445,8 +4025,8 @@ function vuePlugin(config) {
3445
4025
  let descriptor = descriptorCache.get(filePath);
3446
4026
  if (!descriptor) {
3447
4027
  try {
3448
- const fs12 = await import("fs");
3449
- const source = fs12.readFileSync(filePath, "utf-8");
4028
+ const fs13 = await import("fs");
4029
+ const source = fs13.readFileSync(filePath, "utf-8");
3450
4030
  const parsed = sfc.parse(source, { filename: filePath });
3451
4031
  if (parsed.errors.length) return null;
3452
4032
  descriptor = parsed.descriptor;
@@ -3584,16 +4164,27 @@ var init_vue = __esm({
3584
4164
  // src/plugins/builtins.ts
3585
4165
  function resolvePluginList(config, userPlugins, opts = {}) {
3586
4166
  const isServe = config.command === "serve";
4167
+ let environmentOptions;
4168
+ if (opts.environmentName) {
4169
+ environmentOptions = config.environments[opts.environmentName];
4170
+ if (!environmentOptions) {
4171
+ throw new Error(
4172
+ `[nasti] unknown environment "${opts.environmentName}" \u2014 declare it in config.environments`
4173
+ );
4174
+ }
4175
+ }
4176
+ const pluginConfig = environmentOptions ? { ...config, resolve: environmentOptions.resolve, build: environmentOptions.build } : config;
4177
+ const consumer = opts.consumer ?? environmentOptions?.consumer;
3587
4178
  return [
3588
4179
  // vuePlugin 排最前(enforce: 'pre' 语义):.vue 先编译成 JS 再走后续管道
3589
- ...config.framework === "vue" ? [vuePlugin(config)] : [],
3590
- resolvePlugin(config),
3591
- cssPlugin(config, opts.cssEngine, opts.consumer),
3592
- assetsPlugin(config),
3593
- ...isServe ? [htmlPlugin(config)] : [],
4180
+ ...config.framework === "vue" ? [vuePlugin(pluginConfig)] : [],
4181
+ resolvePlugin(pluginConfig),
4182
+ cssPlugin(pluginConfig, opts.cssEngine, consumer),
4183
+ assetsPlugin(pluginConfig),
4184
+ ...isServe ? [htmlPlugin(pluginConfig)] : [],
3594
4185
  ...userPlugins,
3595
4186
  // cssPostPlugin 最后(enforce: 'post' 语义):renderChunk 聚合抽取
3596
- ...!isServe && opts.cssEngine ? [cssPostPlugin(config, opts.cssEngine)] : []
4187
+ ...!isServe && opts.cssEngine ? [cssPostPlugin(pluginConfig, opts.cssEngine)] : []
3597
4188
  ];
3598
4189
  }
3599
4190
  var init_builtins = __esm({
@@ -3917,16 +4508,145 @@ var init_reporter = __esm({
3917
4508
  }
3918
4509
  });
3919
4510
 
4511
+ // src/core/build-app-context.ts
4512
+ import fs9 from "fs";
4513
+ import path12 from "path";
4514
+ function createBuildAppContext(config, results) {
4515
+ const output = [];
4516
+ const emitted = /* @__PURE__ */ new Set();
4517
+ const outDir = path12.resolve(config.root, config.build.outDir);
4518
+ let environmentArtifacts;
4519
+ return {
4520
+ config,
4521
+ results,
4522
+ get output() {
4523
+ return Object.freeze([...output]);
4524
+ },
4525
+ getResult(environmentName) {
4526
+ return results[environmentName];
4527
+ },
4528
+ getArtifact(environmentName, fileName) {
4529
+ const normalized = normalizeEnvironmentFileName(fileName);
4530
+ return results[environmentName]?.output.find(
4531
+ (artifact) => normalizeEnvironmentFileName(artifact.fileName) === normalized
4532
+ );
4533
+ },
4534
+ getEntry(environmentName, entryName) {
4535
+ const result = results[environmentName];
4536
+ const fileName = result?.entries?.[entryName];
4537
+ if (!fileName) return void 0;
4538
+ return result.output.find(
4539
+ (artifact) => normalizeEnvironmentFileName(artifact.fileName) === normalizeEnvironmentFileName(fileName)
4540
+ );
4541
+ },
4542
+ getManifest(environmentName) {
4543
+ return results[environmentName]?.manifest;
4544
+ },
4545
+ emitFile(file) {
4546
+ const fileName = normalizeAppFileName(file.fileName);
4547
+ const collisionKey = artifactCollisionKey(fileName);
4548
+ if (emitted.has(collisionKey)) {
4549
+ throw new Error(`[nasti] app artifact already emitted: ${fileName}`);
4550
+ }
4551
+ environmentArtifacts ??= collectEnvironmentArtifacts(config, results, outDir);
4552
+ if (environmentArtifacts.has(collisionKey)) {
4553
+ throw new Error(`[nasti] app artifact conflicts with environment output: ${fileName}`);
4554
+ }
4555
+ const target = path12.resolve(outDir, ...fileName.split("/"));
4556
+ const relative = path12.relative(outDir, target);
4557
+ if (relative.startsWith("..") || path12.isAbsolute(relative)) {
4558
+ throw new Error(`[nasti] app artifact must stay inside build.outDir: ${file.fileName}`);
4559
+ }
4560
+ assertNoSymlinkComponents(outDir, fileName);
4561
+ fs9.mkdirSync(path12.dirname(target), { recursive: true });
4562
+ fs9.writeFileSync(target, file.source);
4563
+ const artifact = {
4564
+ ...file,
4565
+ fileName,
4566
+ type: "asset"
4567
+ };
4568
+ emitted.add(collisionKey);
4569
+ output.push(artifact);
4570
+ return fileName;
4571
+ }
4572
+ };
4573
+ }
4574
+ function normalizeEnvironmentFileName(fileName) {
4575
+ return path12.posix.normalize(fileName.replace(/\\/g, "/").replace(/^\.\//, ""));
4576
+ }
4577
+ function isInvalidEnvironmentFileName(fileName) {
4578
+ return !fileName || fileName === "." || fileName === ".." || fileName.startsWith("../") || path12.posix.isAbsolute(fileName) || /^[A-Za-z]:\//.test(fileName);
4579
+ }
4580
+ function normalizeAppFileName(fileName) {
4581
+ const normalized = normalizeEnvironmentFileName(fileName);
4582
+ if (isInvalidEnvironmentFileName(normalized)) {
4583
+ throw new Error(`[nasti] invalid app artifact fileName: ${fileName}`);
4584
+ }
4585
+ return normalized;
4586
+ }
4587
+ function artifactCollisionKey(fileName) {
4588
+ return normalizeEnvironmentFileName(fileName).toLowerCase();
4589
+ }
4590
+ function collectEnvironmentArtifacts(config, results, appOutDir) {
4591
+ const occupied = /* @__PURE__ */ new Set();
4592
+ for (const [environmentName, result] of Object.entries(results)) {
4593
+ const environment = config.environments[environmentName];
4594
+ if (!environment) continue;
4595
+ const environmentOutDir = path12.resolve(config.root, environment.build.outDir);
4596
+ for (const artifact of result.output) {
4597
+ const artifactPath = path12.resolve(
4598
+ environmentOutDir,
4599
+ ...normalizeEnvironmentFileName(artifact.fileName).split("/")
4600
+ );
4601
+ const relative = path12.relative(appOutDir, artifactPath);
4602
+ if (!relative.startsWith("..") && !path12.isAbsolute(relative)) {
4603
+ occupied.add(artifactCollisionKey(relative));
4604
+ }
4605
+ }
4606
+ }
4607
+ return occupied;
4608
+ }
4609
+ function assertNoSymlinkComponents(outDir, fileName) {
4610
+ let current = outDir;
4611
+ for (const segment of fileName.split("/")) {
4612
+ current = path12.join(current, segment);
4613
+ let stats;
4614
+ try {
4615
+ stats = fs9.lstatSync(current);
4616
+ } catch (error) {
4617
+ if (error.code === "ENOENT") continue;
4618
+ throw error;
4619
+ }
4620
+ if (stats.isSymbolicLink()) {
4621
+ throw new Error(`[nasti] app artifact path cannot traverse a symlink: ${fileName}`);
4622
+ }
4623
+ }
4624
+ }
4625
+ function inferEnvironmentEntries(output) {
4626
+ const entries = {};
4627
+ for (const artifact of output) {
4628
+ if (artifact.type !== "chunk" || !artifact.isEntry || !artifact.name) continue;
4629
+ entries[artifact.name] = normalizeEnvironmentFileName(artifact.fileName);
4630
+ }
4631
+ return Object.keys(entries).length > 0 ? entries : void 0;
4632
+ }
4633
+ var init_build_app_context = __esm({
4634
+ "src/core/build-app-context.ts"() {
4635
+ "use strict";
4636
+ }
4637
+ });
4638
+
3920
4639
  // src/build/index.ts
3921
4640
  var build_exports = {};
3922
4641
  __export(build_exports, {
3923
4642
  build: () => build,
3924
4643
  getRolldownOptions: () => getRolldownOptions,
4644
+ replaceEntryScript: () => replaceEntryScript,
3925
4645
  resolveClientEntries: () => resolveClientEntries,
3926
4646
  toRolldownPlugins: () => toRolldownPlugins
3927
4647
  });
3928
- import path12 from "path";
3929
- import fs9 from "fs";
4648
+ import path13 from "path";
4649
+ import fs10 from "fs";
3930
4650
  import { builtinModules as builtinModules2 } from "module";
3931
4651
  import { rolldown } from "rolldown";
3932
4652
  import pc6 from "picocolors";
@@ -3934,9 +4654,14 @@ function getRolldownOptions(environment, entryPoints, rolldownPlugins) {
3934
4654
  const config = environment.config;
3935
4655
  const envOptions = environment.options;
3936
4656
  const isServer = environment.consumer === "server";
3937
- const outDir = path12.resolve(config.root, envOptions.build.outDir);
4657
+ const outDir = path13.resolve(config.root, envOptions.build.outDir);
3938
4658
  const assetsDir = envOptions.build.assetsDir;
3939
- const { output: userOutput, transform: userTransform, ...restInputOptions } = envOptions.build.rolldownOptions;
4659
+ const {
4660
+ output: userOutput,
4661
+ transform: userTransform,
4662
+ resolve: userResolve,
4663
+ ...restInputOptions
4664
+ } = envOptions.build.rolldownOptions;
3940
4665
  const vueDefine = config.framework === "vue" ? {
3941
4666
  __VUE_OPTIONS_API__: "true",
3942
4667
  __VUE_PROD_DEVTOOLS__: "false",
@@ -3950,19 +4675,22 @@ function getRolldownOptions(environment, entryPoints, rolldownPlugins) {
3950
4675
  input: entryPoints,
3951
4676
  transform: { ...userTransform, define: mergedDefine },
3952
4677
  plugins: rolldownPlugins,
4678
+ // client/server consumer 都必须使用当前环境的 conditions/mainFields;Lynx
4679
+ // BG/MT 通常同为 client consumer,但仍可能声明不同的运行时条件。
4680
+ resolve: {
4681
+ ...userResolve ?? {},
4682
+ // Environment API 是条件解析的唯一高层入口,优先于继承来的底层选项。
4683
+ conditionNames: envOptions.resolve.conditions,
4684
+ mainFields: envOptions.resolve.mainFields
4685
+ },
3953
4686
  ...isServer ? {
3954
4687
  platform: restInputOptions.platform ?? "node",
3955
- resolve: {
3956
- conditionNames: envOptions.resolve.conditions,
3957
- mainFields: envOptions.resolve.mainFields,
3958
- ...restInputOptions.resolve
3959
- },
3960
4688
  // server 产物:node 内建恒外部化;bare specifier 默认外部化
3961
4689
  //(同 Vite ssr.external 默认 —— 依赖由 node_modules 运行时解析),
3962
4690
  // 相对/绝对/虚拟模块照常打包。需要内联依赖时经 rolldownOptions.external 覆盖。
3963
4691
  external: restInputOptions.external ?? ((id) => {
3964
4692
  if (NODE_BUILTINS2.has(id)) return true;
3965
- return !id.startsWith(".") && !path12.isAbsolute(id) && !id.startsWith("\0");
4693
+ return !id.startsWith(".") && !path13.isAbsolute(id) && !id.startsWith("\0");
3966
4694
  })
3967
4695
  } : {}
3968
4696
  };
@@ -3989,37 +4717,139 @@ function getRolldownOptions(environment, entryPoints, rolldownPlugins) {
3989
4717
  };
3990
4718
  return { inputOptions, outputOptions, outDir };
3991
4719
  }
3992
- function toRolldownPlugins(plugins) {
4720
+ function toRolldownPlugins(plugins, environment) {
4721
+ const wrap = (hook) => {
4722
+ if (!hook) return hook;
4723
+ return function(...args) {
4724
+ return hook.apply(attachEnvironment(this, environment), args);
4725
+ };
4726
+ };
3993
4727
  return plugins.map((p) => ({
3994
4728
  name: p.name,
3995
- resolveId: p.resolveId,
3996
- load: p.load,
3997
- transform: p.transform,
3998
- buildStart: p.buildStart,
3999
- buildEnd: p.buildEnd,
4729
+ resolveId: wrap(p.resolveId),
4730
+ load: wrap(p.load),
4731
+ transform: wrap(p.transform),
4732
+ buildStart: wrap(p.buildStart),
4733
+ buildEnd: wrap(p.buildEnd),
4000
4734
  // closeBundle 在 bundle.close() 时触发 —— PWA manifest/SW 等终态产物依赖
4001
- closeBundle: p.closeBundle,
4002
- renderChunk: p.renderChunk,
4003
- augmentChunkHash: p.augmentChunkHash,
4004
- generateBundle: p.generateBundle
4735
+ closeBundle: wrap(p.closeBundle),
4736
+ renderChunk: wrap(p.renderChunk),
4737
+ augmentChunkHash: wrap(p.augmentChunkHash),
4738
+ generateBundle: wrap(p.generateBundle)
4005
4739
  }));
4006
4740
  }
4741
+ function attachEnvironment(context, environment) {
4742
+ if (context?.environment === environment) return context;
4743
+ try {
4744
+ Object.defineProperty(context, "environment", {
4745
+ configurable: true,
4746
+ enumerable: false,
4747
+ writable: false,
4748
+ value: environment
4749
+ });
4750
+ return context;
4751
+ } catch {
4752
+ return new Proxy(context, {
4753
+ get(target, property) {
4754
+ if (property === "environment") return environment;
4755
+ const value = Reflect.get(target, property, target);
4756
+ return typeof value === "function" ? value.bind(target) : value;
4757
+ },
4758
+ set(target, property, value) {
4759
+ return Reflect.set(target, property, value, target);
4760
+ }
4761
+ });
4762
+ }
4763
+ }
4764
+ function finalizeEnvironmentResult(environment, result) {
4765
+ const metadata = environment.getBuildMetadata();
4766
+ const inferredEntries = inferEnvironmentEntries(result.output);
4767
+ const entries = {
4768
+ ...inferredEntries,
4769
+ ...metadata.entries,
4770
+ ...result.entries
4771
+ };
4772
+ const normalizedEntries = Object.fromEntries(
4773
+ Object.entries(entries).map(([name, fileName]) => {
4774
+ const normalized = normalizeEnvironmentFileName(fileName);
4775
+ if (isInvalidEnvironmentFileName(normalized)) {
4776
+ throw new Error(
4777
+ `[nasti] environment "${environment.name}" returned invalid entry "${name}": ${fileName}`
4778
+ );
4779
+ }
4780
+ return [name, normalized];
4781
+ })
4782
+ );
4783
+ return {
4784
+ ...metadata,
4785
+ ...result,
4786
+ output: result.output,
4787
+ ...Object.keys(normalizedEntries).length > 0 ? { entries: normalizedEntries } : {}
4788
+ };
4789
+ }
4790
+ function prepareBuildOutputDirectories(config, buildableNames) {
4791
+ const directories = /* @__PURE__ */ new Set();
4792
+ const protectedPaths = /* @__PURE__ */ new Set();
4793
+ const clientIsBuilt = buildableNames.includes("client");
4794
+ if (!clientIsBuilt && config.build.emptyOutDir) {
4795
+ directories.add(path13.resolve(config.root, config.build.outDir));
4796
+ }
4797
+ for (const name of buildableNames) {
4798
+ const environment = config.environments[name];
4799
+ const outDir = path13.resolve(config.root, environment.build.outDir);
4800
+ if (!environment.build.emptyOutDir) {
4801
+ protectedPaths.add(outDir);
4802
+ continue;
4803
+ }
4804
+ if (!environment.driver) directories.add(outDir);
4805
+ }
4806
+ const containsPath = (parent, child) => {
4807
+ const relative = path13.relative(parent, child);
4808
+ return relative === "" || !relative.startsWith("..") && !path13.isAbsolute(relative);
4809
+ };
4810
+ const roots = [...directories].filter(
4811
+ (directory) => ![...protectedPaths].some((protectedPath) => containsPath(directory, protectedPath))
4812
+ ).sort((a, b) => a.length - b.length).filter(
4813
+ (directory, index2, all) => !all.slice(0, index2).some((parent) => containsPath(parent, directory))
4814
+ );
4815
+ for (const directory of roots) {
4816
+ if (fs10.existsSync(directory)) fs10.rmSync(directory, { recursive: true, force: true });
4817
+ }
4818
+ }
4819
+ function assertDriverBuildResult(environment, result) {
4820
+ const output = result != null && typeof result === "object" ? result.output : void 0;
4821
+ const hasValidOutput = Array.isArray(output) && output.every(
4822
+ (artifact) => artifact != null && typeof artifact === "object" && typeof artifact.fileName === "string" && typeof artifact.type === "string"
4823
+ );
4824
+ if (!hasValidOutput) {
4825
+ throw new Error(
4826
+ `[nasti] environment "${environment.name}" driver "${environment.driver?.name}" returned an invalid build result; expected { output: EnvironmentBuildOutput[] }`
4827
+ );
4828
+ }
4829
+ }
4007
4830
  function resolveClientEntries(config, html) {
4831
+ const configuredEntries = config.environments.client?.entry ?? [];
4832
+ if (configuredEntries.length > 0) return configuredEntries;
4008
4833
  const entryPoints = [];
4834
+ const htmlFile = config.environments.client?.html;
4835
+ const htmlDir = htmlFile ? path13.dirname(htmlFile) : config.root;
4009
4836
  if (html) {
4010
4837
  const scriptMatches = html.matchAll(/<script[^>]+src=["']([^"']+)["'][^>]*>/gi);
4011
4838
  for (const match of scriptMatches) {
4012
4839
  const src = match[1];
4013
4840
  if (src && !src.startsWith("http")) {
4014
- entryPoints.push(path12.resolve(config.root, src.replace(/^\//, "")));
4841
+ const cleanSrc = src.split(/[?#]/, 1)[0];
4842
+ entryPoints.push(
4843
+ cleanSrc.startsWith("/") ? path13.resolve(config.root, cleanSrc.replace(/^\//, "")) : path13.resolve(htmlDir, cleanSrc)
4844
+ );
4015
4845
  }
4016
4846
  }
4017
4847
  }
4018
4848
  if (entryPoints.length === 0) {
4019
4849
  const fallbackEntries = ["src/main.ts", "src/main.tsx", "src/main.js", "src/index.ts", "src/index.tsx", "src/index.js"];
4020
4850
  for (const entry of fallbackEntries) {
4021
- const fullPath = path12.resolve(config.root, entry);
4022
- if (fs9.existsSync(fullPath)) {
4851
+ const fullPath = path13.resolve(config.root, entry);
4852
+ if (fs10.existsSync(fullPath)) {
4023
4853
  entryPoints.push(fullPath);
4024
4854
  break;
4025
4855
  }
@@ -4047,130 +4877,229 @@ async function build(inlineConfig = {}) {
4047
4877
  const startTime = performance.now();
4048
4878
  logger.info(
4049
4879
  pc6.cyan(`
4050
- nasti v${"2.2.0"} `) + pc6.green(`building for ${config.mode}...`)
4880
+ nasti v${"2.4.0"} `) + pc6.green(`building for ${config.mode}...`)
4051
4881
  );
4052
4882
  debug5?.(`root: ${config.root}`);
4053
- const buildableNames = Object.keys(config.environments).filter(
4054
- (name) => name === "client" || config.environments[name].entry.length > 0
4055
- );
4883
+ const buildableNames = Object.keys(config.environments).filter((name) => {
4884
+ const environment = config.environments[name];
4885
+ if (!environment.buildEnabled) return false;
4886
+ return name === "client" || environment.entry.length > 0 || !!environment.driver;
4887
+ });
4056
4888
  buildableNames.sort((a, b) => a === "client" ? -1 : b === "client" ? 1 : 0);
4889
+ prepareBuildOutputDirectories(config, buildableNames);
4057
4890
  const environments = {};
4891
+ const environmentResults = {};
4892
+ const initializedEnvironments = [];
4893
+ const buildAppContext = createBuildAppContext(config, environmentResults);
4058
4894
  let clientOutput = [];
4059
- for (const name of buildableNames) {
4060
- const output = name === "client" ? await buildClientEnvironment(config) : await buildServerEnvironment(config, name);
4061
- environments[name] = output;
4062
- if (name === "client") clientOutput = output;
4063
- if (buildableNames.length > 1) {
4064
- debug5?.(`environment "${name}" built (${output.length} files)`);
4895
+ let buildFailed = false;
4896
+ try {
4897
+ for (const name of buildableNames) {
4898
+ const built = name === "client" ? await buildClientEnvironment(config) : await buildServerEnvironment(config, name);
4899
+ initializedEnvironments.push(built.environment);
4900
+ environments[name] = built.result.output;
4901
+ environmentResults[name] = built.result;
4902
+ if (name === "client") clientOutput = built.result.output;
4903
+ if (buildableNames.length > 1) {
4904
+ debug5?.(`environment "${name}" built (${built.result.output.length} files)`);
4905
+ }
4906
+ }
4907
+ const pluginApi = getPluginApi(config);
4908
+ for (const plugin of config.plugins) {
4909
+ await plugin.afterBuildApp?.(environmentResults, pluginApi, buildAppContext);
4910
+ }
4911
+ } catch (error) {
4912
+ buildFailed = true;
4913
+ throw error;
4914
+ } finally {
4915
+ let closeFailed = false;
4916
+ let firstCloseError;
4917
+ for (const environment of [...initializedEnvironments].reverse()) {
4918
+ try {
4919
+ await environment.close();
4920
+ } catch (error) {
4921
+ if (!closeFailed) {
4922
+ closeFailed = true;
4923
+ firstCloseError = error;
4924
+ }
4925
+ const closeError = error instanceof Error ? error : new Error(String(error));
4926
+ logger.error(`[nasti] failed to close environment "${environment.name}"`, {
4927
+ error: closeError
4928
+ });
4929
+ }
4930
+ }
4931
+ if (closeFailed && !buildFailed) {
4932
+ throw firstCloseError;
4065
4933
  }
4066
4934
  }
4067
4935
  const elapsed = ((performance.now() - startTime) / 1e3).toFixed(2);
4068
- const totalSize = Object.values(environments).flat().reduce((sum, chunk) => {
4936
+ const allOutput = [...Object.values(environments).flat(), ...buildAppContext.output];
4937
+ const totalSize = allOutput.reduce((sum, chunk) => {
4069
4938
  const content = chunk.type === "chunk" ? chunk.code : chunk.source;
4070
4939
  if (content == null) return sum;
4071
4940
  return sum + (typeof content === "string" ? Buffer.byteLength(content) : content.byteLength);
4072
4941
  }, 0);
4073
- const fileCount = Object.values(environments).flat().length;
4942
+ const fileCount = allOutput.length;
4074
4943
  const envSuffix = buildableNames.length > 1 ? ` (${buildableNames.join(" + ")})` : "";
4075
4944
  logger.info(pc6.green(`\u2713 built in ${elapsed}s`) + pc6.dim(envSuffix));
4076
4945
  logger.info(pc6.dim(` ${fileCount} files, ${displaySize(totalSize)} total \u2192 ${config.build.outDir}/`));
4077
- return { output: clientOutput, environments };
4946
+ return {
4947
+ output: clientOutput,
4948
+ environments,
4949
+ environmentResults,
4950
+ appOutput: [...buildAppContext.output]
4951
+ };
4078
4952
  }
4079
4953
  async function buildClientEnvironment(config) {
4080
4954
  const logger = config.logger;
4081
- const outDir = path12.resolve(config.root, config.build.outDir);
4082
- if (config.build.emptyOutDir && fs9.existsSync(outDir)) {
4083
- fs9.rmSync(outDir, { recursive: true, force: true });
4084
- }
4085
- fs9.mkdirSync(outDir, { recursive: true });
4086
- const html = await readHtmlFile(config.root);
4087
- const entryPoints = resolveClientEntries(config, html);
4088
- if (entryPoints.length === 0) {
4089
- throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
4090
- }
4955
+ const outDir = path13.resolve(config.root, config.build.outDir);
4091
4956
  const cssEngine = createCssEngine();
4092
- const pluginList = resolvePluginList(config, config.plugins, { cssEngine });
4093
- const clientEnv = new NastiEnvironment("client", { ...config, plugins: pluginList }, {
4957
+ const pluginList = resolvePluginList(config, config.plugins, {
4958
+ cssEngine,
4959
+ environmentName: "client"
4960
+ });
4961
+ const clientEnv = new NastiEnvironment("client", config, {
4094
4962
  mode: "build",
4095
- plugins: pluginList
4963
+ plugins: pluginList,
4964
+ pluginApi: getPluginApi(config)
4096
4965
  });
4097
4966
  await clientEnv.init();
4098
- const allPlugins = clientEnv.plugins;
4099
- const nativeReporter = config.logLevel === "silent" ? null : await tryNativeReporterPlugin(config, logger);
4100
- const rolldownPlugins = [
4101
- createOxcTransformPlugin(config, clientEnv),
4102
- ...toRolldownPlugins(allPlugins),
4103
- ...nativeReporter ? [nativeReporter] : []
4104
- ];
4105
- const { inputOptions, outputOptions } = getRolldownOptions(clientEnv, entryPoints, rolldownPlugins);
4106
- const bundle2 = await rolldown(inputOptions);
4107
- const { output } = await bundle2.write(outputOptions);
4108
- await bundle2.close();
4109
- if (html) {
4110
- let processedHtml = html;
4111
- const htmlPlugins = [...allPlugins.filter((p) => p.transformIndexHtml), htmlPlugin(config)];
4112
- for (const p of htmlPlugins) {
4113
- const result = await p.transformIndexHtml(processedHtml);
4114
- if (typeof result === "string") {
4115
- processedHtml = result;
4116
- } else if (result && "html" in result) {
4117
- processedHtml = processHtml(result.html, result.tags);
4118
- } else if (Array.isArray(result)) {
4119
- processedHtml = processHtml(processedHtml, result);
4120
- }
4121
- }
4122
- processedHtml = injectCssLinks(processedHtml, cssEngine, config);
4123
- for (const chunk of output) {
4124
- if (chunk.type === "chunk" && chunk.isEntry && chunk.facadeModuleId) {
4125
- const originalEntry = path12.relative(config.root, chunk.facadeModuleId);
4126
- processedHtml = processedHtml.replace(
4127
- new RegExp(`(src=["'])/?(${escapeRegExp(originalEntry)})(["'])`, "g"),
4128
- `$1${config.base}${chunk.fileName}$3`
4967
+ try {
4968
+ if (clientEnv.driver) {
4969
+ if (!clientEnv.driver.build) {
4970
+ throw new Error(
4971
+ `[nasti] environment "client" driver "${clientEnv.driver.name}" does not implement build()`
4129
4972
  );
4130
4973
  }
4974
+ const result = await clientEnv.driver.build(clientEnv.getDriverContext());
4975
+ assertDriverBuildResult(clientEnv, result);
4976
+ return { environment: clientEnv, result: finalizeEnvironmentResult(clientEnv, result) };
4131
4977
  }
4132
- fs9.writeFileSync(path12.resolve(outDir, "index.html"), processedHtml);
4133
- }
4134
- if (!nativeReporter && config.logLevel !== "silent") {
4135
- reportBuildOutput(output, config, logger);
4978
+ fs10.mkdirSync(outDir, { recursive: true });
4979
+ const htmlFile = config.environments.client.html ?? path13.resolve(config.root, "index.html");
4980
+ const html = await readHtmlFile(config.root, htmlFile);
4981
+ const entryPoints = resolveClientEntries(config, html);
4982
+ if (entryPoints.length === 0) {
4983
+ throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
4984
+ }
4985
+ const allPlugins = clientEnv.plugins;
4986
+ const nativeReporter = config.logLevel === "silent" ? null : await tryNativeReporterPlugin(config, logger);
4987
+ const rolldownPlugins = [
4988
+ createOxcTransformPlugin(config, clientEnv),
4989
+ ...toRolldownPlugins(allPlugins, clientEnv),
4990
+ ...nativeReporter ? [nativeReporter] : []
4991
+ ];
4992
+ const { inputOptions, outputOptions } = getRolldownOptions(
4993
+ clientEnv,
4994
+ entryPoints,
4995
+ rolldownPlugins
4996
+ );
4997
+ const bundle2 = await rolldown(inputOptions);
4998
+ const { output } = await bundle2.write(outputOptions);
4999
+ await bundle2.close();
5000
+ if (html) {
5001
+ let processedHtml = html;
5002
+ const htmlPlugins = [...allPlugins.filter((p) => p.transformIndexHtml), htmlPlugin(config)];
5003
+ for (const p of htmlPlugins) {
5004
+ const result = await p.transformIndexHtml(processedHtml);
5005
+ if (typeof result === "string") {
5006
+ processedHtml = result;
5007
+ } else if (result && "html" in result) {
5008
+ processedHtml = processHtml(result.html, result.tags);
5009
+ } else if (Array.isArray(result)) {
5010
+ processedHtml = processHtml(processedHtml, result);
5011
+ }
5012
+ }
5013
+ processedHtml = injectCssLinks(processedHtml, cssEngine, config);
5014
+ for (const chunk of output) {
5015
+ if (chunk.type === "chunk" && chunk.isEntry && chunk.facadeModuleId) {
5016
+ processedHtml = replaceEntryScript(
5017
+ processedHtml,
5018
+ chunk.facadeModuleId,
5019
+ chunk.fileName,
5020
+ config,
5021
+ htmlFile,
5022
+ config.base
5023
+ );
5024
+ }
5025
+ }
5026
+ fs10.writeFileSync(path13.resolve(outDir, "index.html"), processedHtml);
5027
+ }
5028
+ if (!nativeReporter && config.logLevel !== "silent") {
5029
+ reportBuildOutput(output, config, logger);
5030
+ }
5031
+ warnLargeChunks(output, config, logger);
5032
+ return {
5033
+ environment: clientEnv,
5034
+ result: finalizeEnvironmentResult(clientEnv, { output })
5035
+ };
5036
+ } catch (error) {
5037
+ try {
5038
+ await clientEnv.close();
5039
+ } catch (closeError) {
5040
+ const normalized = closeError instanceof Error ? closeError : new Error(String(closeError));
5041
+ logger.error("[nasti] failed to close client environment after build failure", {
5042
+ error: normalized
5043
+ });
5044
+ }
5045
+ throw error;
4136
5046
  }
4137
- warnLargeChunks(output, config, logger);
4138
- return output;
4139
5047
  }
4140
5048
  async function buildServerEnvironment(config, name) {
4141
5049
  const envOptions = config.environments[name];
4142
5050
  const logger = config.logger;
5051
+ const pluginList = resolvePluginList(config, config.plugins, {
5052
+ consumer: envOptions.consumer,
5053
+ environmentName: name
5054
+ });
5055
+ const environment = new NastiEnvironment(name, config, {
5056
+ mode: "build",
5057
+ plugins: pluginList,
5058
+ pluginApi: getPluginApi(config)
5059
+ });
5060
+ await environment.init();
5061
+ if (environment.driver) {
5062
+ if (!environment.driver.build) {
5063
+ await environment.close();
5064
+ throw new Error(
5065
+ `[nasti] environment "${name}" driver "${environment.driver.name}" does not implement build()`
5066
+ );
5067
+ }
5068
+ try {
5069
+ const result = await environment.driver.build(environment.getDriverContext());
5070
+ assertDriverBuildResult(environment, result);
5071
+ return { environment, result: finalizeEnvironmentResult(environment, result) };
5072
+ } catch (error) {
5073
+ await environment.close();
5074
+ throw error;
5075
+ }
5076
+ }
4143
5077
  for (const entry of envOptions.entry) {
4144
- if (!fs9.existsSync(entry)) {
5078
+ if (!fs10.existsSync(entry)) {
5079
+ await environment.close();
4145
5080
  throw new Error(`[nasti] environment "${name}" entry not found: ${entry}`);
4146
5081
  }
4147
5082
  }
4148
- const pluginList = resolvePluginList(config, config.plugins, { consumer: envOptions.consumer });
4149
- const environment = new NastiEnvironment(name, { ...config, plugins: pluginList }, {
4150
- mode: "build",
4151
- plugins: pluginList
4152
- });
4153
- await environment.init();
4154
5083
  const rolldownPlugins = [
4155
5084
  createOxcTransformPlugin(config, environment),
4156
- ...toRolldownPlugins(environment.plugins)
5085
+ ...toRolldownPlugins(environment.plugins, environment)
4157
5086
  ];
4158
5087
  const { inputOptions, outputOptions, outDir } = getRolldownOptions(
4159
5088
  environment,
4160
5089
  envOptions.entry,
4161
5090
  rolldownPlugins
4162
5091
  );
4163
- if (envOptions.build.emptyOutDir && fs9.existsSync(outDir)) {
4164
- fs9.rmSync(outDir, { recursive: true, force: true });
4165
- }
4166
- fs9.mkdirSync(outDir, { recursive: true });
5092
+ fs10.mkdirSync(outDir, { recursive: true });
4167
5093
  const bundle2 = await rolldown(inputOptions);
4168
5094
  const { output } = await bundle2.write(outputOptions);
4169
5095
  await bundle2.close();
4170
5096
  logger.info(
4171
- pc6.dim(` [${name}] `) + output.map((o) => path12.join(envOptions.build.outDir, o.fileName)).join(pc6.dim(", "))
5097
+ pc6.dim(` [${name}] `) + output.map((o) => path13.join(envOptions.build.outDir, o.fileName)).join(pc6.dim(", "))
4172
5098
  );
4173
- return output;
5099
+ return {
5100
+ environment,
5101
+ result: finalizeEnvironmentResult(environment, { output })
5102
+ };
4174
5103
  }
4175
5104
  function injectCssLinks(html, cssEngine, config) {
4176
5105
  const cssLinkTags = [];
@@ -4196,6 +5125,25 @@ function injectCssLinks(html, cssEngine, config) {
4196
5125
  function escapeRegExp(string) {
4197
5126
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4198
5127
  }
5128
+ function replaceEntryScript(html, facadeModuleId, fileName, config, htmlFile, urlPrefix) {
5129
+ const rootRelative = path13.relative(config.root, facadeModuleId).split(path13.sep).join("/");
5130
+ const resolvedHtmlFile = path13.resolve(config.root, htmlFile);
5131
+ const htmlRelative = path13.relative(path13.dirname(resolvedHtmlFile), facadeModuleId).split(path13.sep).join("/");
5132
+ const candidates = /* @__PURE__ */ new Set([
5133
+ rootRelative,
5134
+ `/${rootRelative}`,
5135
+ htmlRelative,
5136
+ `./${htmlRelative}`
5137
+ ]);
5138
+ let processed = html;
5139
+ for (const candidate of candidates) {
5140
+ processed = processed.replace(
5141
+ new RegExp(`(src=["'])${escapeRegExp(candidate)}([?#][^"']*)?(["'])`, "g"),
5142
+ `$1${urlPrefix}${fileName}$3`
5143
+ );
5144
+ }
5145
+ return processed;
5146
+ }
4199
5147
  var debug5, NODE_BUILTINS2;
4200
5148
  var init_build = __esm({
4201
5149
  "src/build/index.ts"() {
@@ -4209,6 +5157,8 @@ var init_build = __esm({
4209
5157
  init_env();
4210
5158
  init_reporter();
4211
5159
  init_debug();
5160
+ init_plugin_api();
5161
+ init_build_app_context();
4212
5162
  debug5 = createDebugger("nasti:build");
4213
5163
  NODE_BUILTINS2 = /* @__PURE__ */ new Set([...builtinModules2, ...builtinModules2.map((m) => `node:${m}`)]);
4214
5164
  }
@@ -4219,7 +5169,7 @@ var dev_engine_exports = {};
4219
5169
  __export(dev_engine_exports, {
4220
5170
  createBundledDevServer: () => createBundledDevServer
4221
5171
  });
4222
- import path13 from "path";
5172
+ import path14 from "path";
4223
5173
  import crypto3 from "crypto";
4224
5174
  import { WebSocketServer as WsServer2 } from "ws";
4225
5175
  import pc7 from "picocolors";
@@ -4240,7 +5190,7 @@ async function createBundledDevServer(opts) {
4240
5190
  `[nasti] experimental.bundledDev requires rolldown's experimental dev() API (locked to the installed rc; got: ${err.message}). Remove --bundle / experimental.bundledDev to use the default unbundled dev server.`
4241
5191
  );
4242
5192
  }
4243
- const html = await readHtmlFile(config.root);
5193
+ const html = await readHtmlFile(config.root, config.environments.client?.html);
4244
5194
  const entryPoints = resolveClientEntries(config, html);
4245
5195
  if (entryPoints.length === 0) {
4246
5196
  throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
@@ -4255,7 +5205,7 @@ async function createBundledDevServer(opts) {
4255
5205
  createReactRefreshRuntimePlugin(entryPoints),
4256
5206
  createBundledOxcRefreshPlugin()
4257
5207
  ] : [],
4258
- ...stripCatchAllLoad(toRolldownPlugins(clientEnv.plugins)),
5208
+ ...stripCatchAllLoad(toRolldownPlugins(clientEnv.plugins, clientEnv)),
4259
5209
  ...useReactRefresh ? [
4260
5210
  refreshWrapperFn({
4261
5211
  cwd: config.root,
@@ -4304,7 +5254,7 @@ async function createBundledDevServer(opts) {
4304
5254
  }
4305
5255
  const url = `/${patchPath}`;
4306
5256
  logger.info(
4307
- pc7.green("hmr update ") + pc7.dim(changedFiles.map((f) => path13.relative(config.root, f)).join(", ")),
5257
+ pc7.green("hmr update ") + pc7.dim(changedFiles.map((f) => path14.relative(config.root, f)).join(", ")),
4308
5258
  { timestamp: true }
4309
5259
  );
4310
5260
  sendTo(clientId, { type: "hmr:update", path: url, url });
@@ -4440,13 +5390,13 @@ async function createBundledDevServer(opts) {
4440
5390
  return;
4441
5391
  }
4442
5392
  res.setHeader("ETag", hit.etag);
4443
- res.setHeader("Content-Type", MIME_TYPES[path13.extname(fileName)] ?? "application/octet-stream");
5393
+ res.setHeader("Content-Type", MIME_TYPES[path14.extname(fileName)] ?? "application/octet-stream");
4444
5394
  res.setHeader("Cache-Control", "no-cache");
4445
5395
  res.end(hit.content);
4446
5396
  return;
4447
5397
  }
4448
5398
  if (pathname === "/" || pathname.endsWith(".html")) {
4449
- const rawHtml = await readHtmlFile(config.root);
5399
+ const rawHtml = await readHtmlFile(config.root, config.environments.client?.html);
4450
5400
  if (rawHtml) {
4451
5401
  res.setHeader("Content-Type", "text/html");
4452
5402
  res.setHeader("Cache-Control", "no-store");
@@ -4476,7 +5426,7 @@ function stripCatchAllLoad(plugins) {
4476
5426
  );
4477
5427
  }
4478
5428
  function createReactRefreshRuntimePlugin(entryPoints) {
4479
- const entryIds = new Set(entryPoints.map((p) => path13.resolve(p)));
5429
+ const entryIds = new Set(entryPoints.map((p) => path14.resolve(p)));
4480
5430
  return {
4481
5431
  name: "nasti:bundled-react-refresh",
4482
5432
  resolveId(source) {
@@ -4494,7 +5444,7 @@ function createReactRefreshRuntimePlugin(entryPoints) {
4494
5444
  return null;
4495
5445
  },
4496
5446
  transform(code, id) {
4497
- if (!entryIds.has(path13.resolve(id.split("?")[0]))) return null;
5447
+ if (!entryIds.has(path14.resolve(id.split("?")[0]))) return null;
4498
5448
  return { code: `import ${JSON.stringify(PREAMBLE_SPEC)};
4499
5449
  ${code}`, map: null };
4500
5450
  }
@@ -4530,10 +5480,13 @@ async function renderBundledIndexHtml(html, config, entryFileNames) {
4530
5480
  }
4531
5481
  }
4532
5482
  for (const [facadeModuleId, fileName] of entryFileNames) {
4533
- const originalEntry = path13.relative(config.root, facadeModuleId);
4534
- processed = processed.replace(
4535
- new RegExp(`(src=["'])/?(${originalEntry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})(["'])`, "g"),
4536
- `$1/${fileName}$3`
5483
+ processed = replaceEntryScript(
5484
+ processed,
5485
+ facadeModuleId,
5486
+ fileName,
5487
+ config,
5488
+ config.environments.client?.html ?? "index.html",
5489
+ "/"
4537
5490
  );
4538
5491
  }
4539
5492
  return processed;
@@ -4662,7 +5615,7 @@ __export(server_exports, {
4662
5615
  createServer: () => createServer
4663
5616
  });
4664
5617
  import http from "http";
4665
- import path14 from "path";
5618
+ import path15 from "path";
4666
5619
  import os from "os";
4667
5620
  import connect from "connect";
4668
5621
  import sirv from "sirv";
@@ -4672,27 +5625,38 @@ async function createServer(inlineConfig = {}) {
4672
5625
  const startTime = performance.now();
4673
5626
  const config = await resolveConfig(inlineConfig, "serve");
4674
5627
  const logger = config.logger;
4675
- const allPlugins = resolvePluginList(config, config.plugins);
5628
+ const allPlugins = resolvePluginList(config, config.plugins, {
5629
+ environmentName: "client"
5630
+ });
4676
5631
  const configWithPlugins = { ...config, plugins: allPlugins };
4677
5632
  const app = connect();
4678
5633
  const httpServer = http.createServer(app);
4679
5634
  const ws = createWebSocketServer(httpServer);
4680
- const clientEnv = new NastiEnvironment("client", configWithPlugins, {
5635
+ const pluginApi = getPluginApi(config);
5636
+ const clientEnv = new NastiEnvironment("client", config, {
4681
5637
  hot: createWsHotChannel(ws),
4682
5638
  mode: "dev",
4683
- plugins: allPlugins
5639
+ plugins: allPlugins,
5640
+ pluginApi
4684
5641
  });
4685
5642
  await clientEnv.init();
4686
5643
  const environments = { client: clientEnv };
4687
5644
  for (const name of Object.keys(config.environments)) {
4688
5645
  if (name === "client") continue;
4689
5646
  const consumer = config.environments[name].consumer;
4690
- const envPlugins = resolvePluginList(config, config.plugins, { consumer });
4691
- environments[name] = new NastiEnvironment(name, { ...config, plugins: envPlugins }, {
5647
+ const envPlugins = resolvePluginList(config, config.plugins, {
5648
+ consumer,
5649
+ environmentName: name
5650
+ });
5651
+ environments[name] = new NastiEnvironment(name, config, {
4692
5652
  mode: "dev",
4693
- plugins: envPlugins
5653
+ plugins: envPlugins,
5654
+ pluginApi
4694
5655
  });
4695
5656
  }
5657
+ for (const [name, environment] of Object.entries(environments)) {
5658
+ if (name !== "client" && environment.options.driver) await environment.init();
5659
+ }
4696
5660
  let ssrRunner = null;
4697
5661
  async function getSsrRunner() {
4698
5662
  if (ssrRunner) return ssrRunner;
@@ -4717,23 +5681,15 @@ async function createServer(inlineConfig = {}) {
4717
5681
  });
4718
5682
  app.use(bundledServer.middleware);
4719
5683
  }
4720
- app.use(transformMiddleware({
4721
- config: configWithPlugins,
4722
- pluginContainer,
4723
- moduleGraph
4724
- }));
4725
- const publicDir = path14.resolve(config.root, "public");
4726
- app.use(sirv(publicDir, { dev: true, etag: true }));
4727
- app.use(sirv(config.root, { dev: true, etag: true }));
4728
5684
  const ignoredSegments = /* @__PURE__ */ new Set(["node_modules", ".git", ".nasti"]);
4729
- const outDirAbs = path14.resolve(config.root, config.build.outDir);
5685
+ const outDirAbs = path15.resolve(config.root, config.build.outDir);
4730
5686
  const watcher = watch(config.root, {
4731
5687
  ignored: (filePath) => {
4732
5688
  if (filePath === config.root) return false;
4733
- if (filePath === outDirAbs || filePath.startsWith(outDirAbs + path14.sep)) return true;
4734
- const rel = path14.relative(config.root, filePath);
4735
- if (!rel || rel.startsWith("..") || path14.isAbsolute(rel)) return false;
4736
- for (const seg of rel.split(path14.sep)) {
5689
+ if (filePath === outDirAbs || filePath.startsWith(outDirAbs + path15.sep)) return true;
5690
+ const rel = path15.relative(config.root, filePath);
5691
+ if (!rel || rel.startsWith("..") || path15.isAbsolute(rel)) return false;
5692
+ for (const seg of rel.split(path15.sep)) {
4737
5693
  if (ignoredSegments.has(seg)) return true;
4738
5694
  }
4739
5695
  return false;
@@ -4741,13 +5697,72 @@ async function createServer(inlineConfig = {}) {
4741
5697
  ignoreInitial: true
4742
5698
  });
4743
5699
  let server;
5700
+ const environmentServices = {};
5701
+ let environmentDriversStarted = false;
5702
+ const logCloseError = (target, error) => {
5703
+ const normalized = error instanceof Error ? error : new Error(String(error));
5704
+ logger.error(`[nasti] failed to close ${target}`, { error: normalized });
5705
+ };
5706
+ const startEnvironmentDrivers = async () => {
5707
+ if (environmentDriversStarted) return;
5708
+ environmentDriversStarted = true;
5709
+ const started = [];
5710
+ const attempted = [];
5711
+ try {
5712
+ for (const [name, environment] of Object.entries(environments)) {
5713
+ if (!environment.driver?.serve) continue;
5714
+ attempted.push(environment);
5715
+ const result = await environment.driver.serve({
5716
+ ...environment.getDriverContext(),
5717
+ server
5718
+ });
5719
+ started.push({ name, environment, service: result ?? {} });
5720
+ }
5721
+ for (const { name, service } of started) {
5722
+ environmentServices[name] = service;
5723
+ if (service.middleware) app.use(service.middleware);
5724
+ }
5725
+ } catch (error) {
5726
+ environmentDriversStarted = false;
5727
+ for (const { name } of started) {
5728
+ delete environmentServices[name];
5729
+ }
5730
+ for (const environment of attempted.reverse()) {
5731
+ try {
5732
+ await environment.driver?.close?.(environment.getDriverContext());
5733
+ } catch (closeError) {
5734
+ logCloseError(`environment driver "${environment.driver.name}"`, closeError);
5735
+ }
5736
+ }
5737
+ throw error;
5738
+ }
5739
+ };
5740
+ const notifyEnvironmentDrivers = (file, event) => {
5741
+ for (const environment of Object.values(environments)) {
5742
+ if (!environment.driver?.watchChange) continue;
5743
+ void Promise.resolve(
5744
+ environment.driver.watchChange(file, event, environment.getDriverContext())
5745
+ ).catch((error) => {
5746
+ logger.error(
5747
+ `[nasti] environment driver "${environment.driver.name}" watchChange failed`,
5748
+ { error }
5749
+ );
5750
+ });
5751
+ }
5752
+ };
4744
5753
  watcher.on("change", (file) => {
4745
5754
  ssrRunner?.invalidateFile(file);
4746
5755
  handleFileChange(file, server);
5756
+ notifyEnvironmentDrivers(file, "change");
4747
5757
  });
4748
5758
  watcher.on("add", (file) => {
4749
5759
  ssrRunner?.invalidateFile(file);
4750
5760
  handleFileChange(file, server);
5761
+ notifyEnvironmentDrivers(file, "add");
5762
+ });
5763
+ watcher.on("unlink", (file) => {
5764
+ ssrRunner?.invalidateFile(file);
5765
+ notifyEnvironmentDrivers(file, "unlink");
4751
5766
  });
4752
5767
  server = {
4753
5768
  config: configWithPlugins,
@@ -4756,10 +5771,12 @@ async function createServer(inlineConfig = {}) {
4756
5771
  watcher,
4757
5772
  ws,
4758
5773
  environments,
5774
+ environmentServices,
4759
5775
  async listen(port) {
4760
5776
  const finalPort = port ?? config.server.port;
4761
5777
  const host = config.server.host === true ? "0.0.0.0" : config.server.host;
4762
5778
  await pluginContainer.buildStart();
5779
+ await startEnvironmentDrivers();
4763
5780
  return new Promise((resolve, reject) => {
4764
5781
  let currentPort = finalPort;
4765
5782
  const onListening = () => {
@@ -4767,15 +5784,20 @@ async function createServer(inlineConfig = {}) {
4767
5784
  config.server.port = actualPort;
4768
5785
  const localUrl = `http://localhost:${actualPort}/`;
4769
5786
  const networkUrl = host === "0.0.0.0" ? `http://${getNetworkAddress()}:${actualPort}/` : null;
5787
+ const driverLocalUrls = Object.values(environmentServices).flatMap((service) => service.localUrls ?? []);
5788
+ const driverNetworkUrls = Object.values(environmentServices).flatMap((service) => service.networkUrls ?? []);
4770
5789
  logger.clearScreen("info");
4771
5790
  const readyIn = Math.ceil(performance.now() - startTime);
4772
5791
  logger.info(
4773
5792
  `
4774
- ${pc8.cyan(pc8.bold("NASTI"))} ${pc8.cyan(`v${"2.2.0"}`)} ${pc8.dim("ready in")} ${pc8.bold(readyIn)} ${pc8.dim("ms")}
5793
+ ${pc8.cyan(pc8.bold("NASTI"))} ${pc8.cyan(`v${"2.4.0"}`)} ${pc8.dim("ready in")} ${pc8.bold(readyIn)} ${pc8.dim("ms")}
4775
5794
  `
4776
5795
  );
4777
5796
  printServerUrls(
4778
- { local: [localUrl], network: networkUrl ? [networkUrl] : [] },
5797
+ {
5798
+ local: [localUrl, ...driverLocalUrls],
5799
+ network: [...networkUrl ? [networkUrl] : [], ...driverNetworkUrls]
5800
+ },
4779
5801
  logger.info
4780
5802
  );
4781
5803
  logger.info("");
@@ -4796,7 +5818,12 @@ async function createServer(inlineConfig = {}) {
4796
5818
  },
4797
5819
  async transformRequest(url) {
4798
5820
  const { transformRequest: transformRequest2 } = await Promise.resolve().then(() => (init_middleware(), middleware_exports));
4799
- return transformRequest2(url, { config: configWithPlugins, pluginContainer, moduleGraph });
5821
+ return transformRequest2(url, {
5822
+ config: configWithPlugins,
5823
+ pluginContainer,
5824
+ moduleGraph,
5825
+ onPrune: (paths) => ws.send({ type: "prune", paths })
5826
+ });
4800
5827
  },
4801
5828
  async ssrLoadModule(url) {
4802
5829
  const runner = await getSsrRunner();
@@ -4805,11 +5832,63 @@ async function createServer(inlineConfig = {}) {
4805
5832
  async close() {
4806
5833
  await pluginContainer.buildEnd();
4807
5834
  await bundledServer?.close();
4808
- watcher.close();
5835
+ let environmentCloseFailed = false;
5836
+ let firstEnvironmentCloseError;
5837
+ for (const environment of Object.values(environments).reverse()) {
5838
+ try {
5839
+ await environment.close();
5840
+ } catch (error) {
5841
+ if (!environmentCloseFailed) {
5842
+ environmentCloseFailed = true;
5843
+ firstEnvironmentCloseError = error;
5844
+ }
5845
+ logCloseError(`environment "${environment.name}"`, error);
5846
+ }
5847
+ }
5848
+ await watcher.close();
4809
5849
  ws.close();
4810
5850
  httpServer.close();
5851
+ if (environmentCloseFailed) {
5852
+ throw firstEnvironmentCloseError;
5853
+ }
4811
5854
  }
4812
5855
  };
5856
+ try {
5857
+ await startEnvironmentDrivers();
5858
+ } catch (error) {
5859
+ if (bundledServer) {
5860
+ try {
5861
+ await bundledServer.close();
5862
+ } catch (closeError) {
5863
+ logCloseError("bundled dev server after driver startup failure", closeError);
5864
+ }
5865
+ }
5866
+ try {
5867
+ await watcher.close();
5868
+ } catch (closeError) {
5869
+ logCloseError("file watcher after driver startup failure", closeError);
5870
+ }
5871
+ try {
5872
+ ws.close();
5873
+ } catch (closeError) {
5874
+ logCloseError("WebSocket server after driver startup failure", closeError);
5875
+ }
5876
+ try {
5877
+ httpServer.close();
5878
+ } catch (closeError) {
5879
+ logCloseError("HTTP server after driver startup failure", closeError);
5880
+ }
5881
+ throw error;
5882
+ }
5883
+ app.use(transformMiddleware({
5884
+ config: configWithPlugins,
5885
+ pluginContainer,
5886
+ moduleGraph,
5887
+ onPrune: (paths) => ws.send({ type: "prune", paths })
5888
+ }));
5889
+ const publicDir = path15.resolve(config.root, "public");
5890
+ app.use(sirv(publicDir, { dev: true, etag: true }));
5891
+ app.use(sirv(config.root, { dev: true, etag: true }));
4813
5892
  const postMiddlewares = [];
4814
5893
  for (const plugin of allPlugins) {
4815
5894
  if (plugin.configureServer) {
@@ -4844,6 +5923,7 @@ var init_server = __esm({
4844
5923
  init_middleware();
4845
5924
  init_hmr();
4846
5925
  init_builtins();
5926
+ init_plugin_api();
4847
5927
  }
4848
5928
  });
4849
5929
 
@@ -4890,39 +5970,38 @@ var init_electron = __esm({
4890
5970
  var electron_exports = {};
4891
5971
  __export(electron_exports, {
4892
5972
  buildElectron: () => buildElectron,
5973
+ createElectronRendererConfig: () => createElectronRendererConfig,
4893
5974
  detectInstalledElectron: () => detectInstalledElectron,
4894
5975
  normalizePreload: () => normalizePreload
4895
5976
  });
4896
- import path15 from "path";
4897
- import fs10 from "fs";
5977
+ import path16 from "path";
5978
+ import fs11 from "fs";
4898
5979
  import { rolldown as rolldown2 } from "rolldown";
4899
5980
  import pc9 from "picocolors";
4900
5981
  async function buildElectron(inlineConfig = {}) {
4901
5982
  const config = await resolveConfig({ ...inlineConfig, target: "electron" }, "build");
4902
5983
  const startTime = performance.now();
4903
5984
  assertElectronVersion(config);
4904
- console.log(pc9.cyan("\n\u26A1 nasti build (electron)") + pc9.dim(` v${"2.2.0"}`));
5985
+ console.log(pc9.cyan("\n\u26A1 nasti build (electron)") + pc9.dim(` v${"2.4.0"}`));
4905
5986
  console.log(pc9.dim(` root: ${config.root}`));
4906
5987
  console.log(pc9.dim(` mode: ${config.mode}`));
4907
5988
  console.log(pc9.dim(` target: electron (\u2265 ${config.electron.minVersion})`));
4908
- const outDir = path15.resolve(config.root, config.build.outDir);
4909
- if (config.build.emptyOutDir && fs10.existsSync(outDir)) {
4910
- fs10.rmSync(outDir, { recursive: true, force: true });
5989
+ const outDir = path16.resolve(config.root, config.build.outDir);
5990
+ if (config.build.emptyOutDir && fs11.existsSync(outDir)) {
5991
+ fs11.rmSync(outDir, { recursive: true, force: true });
4911
5992
  }
4912
- fs10.mkdirSync(outDir, { recursive: true });
4913
- const rendererOutDir = path15.join(outDir, "renderer");
5993
+ fs11.mkdirSync(outDir, { recursive: true });
5994
+ const rendererOutDir = path16.join(outDir, "renderer");
4914
5995
  const { build: build2 } = await Promise.resolve().then(() => (init_build(), build_exports));
4915
- await build2({
4916
- ...inlineConfig,
4917
- target: "web",
5996
+ await build2(createElectronRendererConfig(config, inlineConfig, {
4918
5997
  build: {
4919
5998
  ...inlineConfig.build,
4920
5999
  outDir: rendererOutDir,
4921
6000
  emptyOutDir: false
4922
6001
  }
4923
- });
4924
- const mainEntry = path15.resolve(config.root, config.electron.main);
4925
- if (!fs10.existsSync(mainEntry)) {
6002
+ }));
6003
+ const mainEntry = path16.resolve(config.root, config.electron.main);
6004
+ if (!fs11.existsSync(mainEntry)) {
4926
6005
  throw new Error(
4927
6006
  `Electron main entry not found: ${config.electron.main}
4928
6007
  \u5728 nasti.config.ts \u7684 electron.main \u6307\u5B9A\u4E3B\u8FDB\u7A0B\u5165\u53E3\u6587\u4EF6\u3002`
@@ -4936,11 +6015,11 @@ async function buildElectron(inlineConfig = {}) {
4936
6015
  const preloadEntries = normalizePreload(config.electron.preload, config.root);
4937
6016
  const preloadFiles = [];
4938
6017
  for (const entry of preloadEntries) {
4939
- if (!fs10.existsSync(entry)) {
6018
+ if (!fs11.existsSync(entry)) {
4940
6019
  console.warn(pc9.yellow(` \u26A0 preload entry not found, skipped: ${entry}`));
4941
6020
  continue;
4942
6021
  }
4943
- const base = path15.basename(entry).replace(/\.[^.]+$/, "");
6022
+ const base = path16.basename(entry).replace(/\.[^.]+$/, "");
4944
6023
  const out = outFileName(outDir, base, config.electron.preloadFormat);
4945
6024
  await bundleNode(config, entry, {
4946
6025
  outFile: out,
@@ -4952,10 +6031,10 @@ async function buildElectron(inlineConfig = {}) {
4952
6031
  const elapsed = ((performance.now() - startTime) / 1e3).toFixed(2);
4953
6032
  console.log(pc9.green(`
4954
6033
  \u2713 Electron build complete in ${elapsed}s`));
4955
- console.log(pc9.dim(` renderer: ${path15.relative(config.root, rendererOutDir)}/`));
4956
- console.log(pc9.dim(` main: ${path15.relative(config.root, mainFile)}`));
6034
+ console.log(pc9.dim(` renderer: ${path16.relative(config.root, rendererOutDir)}/`));
6035
+ console.log(pc9.dim(` main: ${path16.relative(config.root, mainFile)}`));
4957
6036
  for (const pf of preloadFiles) {
4958
- console.log(pc9.dim(` preload: ${path15.relative(config.root, pf)}`));
6037
+ console.log(pc9.dim(` preload: ${path16.relative(config.root, pf)}`));
4959
6038
  }
4960
6039
  console.log();
4961
6040
  return { rendererOutDir, mainFile, preloadFiles };
@@ -4974,7 +6053,8 @@ async function bundleNode(config, entry, opts) {
4974
6053
  const result = transformCode(id, code, {
4975
6054
  sourcemap: !!config.build.sourcemap,
4976
6055
  jsxRuntime: "automatic",
4977
- jsxImportSource: config.framework === "vue" ? "vue" : "react"
6056
+ jsxImportSource: config.framework === "vue" ? "vue" : "react",
6057
+ target: config.electron.nodeTarget
4978
6058
  });
4979
6059
  return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
4980
6060
  }
@@ -4985,10 +6065,14 @@ async function bundleNode(config, entry, opts) {
4985
6065
  ...restInputOptions,
4986
6066
  input: entry,
4987
6067
  platform: "node",
4988
- transform: { ...userTransform, define: mergedDefine },
6068
+ transform: {
6069
+ ...userTransform,
6070
+ target: config.electron.nodeTarget,
6071
+ define: mergedDefine
6072
+ },
4989
6073
  plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)]
4990
6074
  });
4991
- fs10.mkdirSync(path15.dirname(opts.outFile), { recursive: true });
6075
+ fs11.mkdirSync(path16.dirname(opts.outFile), { recursive: true });
4992
6076
  await bundle2.write({
4993
6077
  sourcemap: !!config.build.sourcemap,
4994
6078
  minify: !!config.build.minify,
@@ -4999,16 +6083,35 @@ async function bundleNode(config, entry, opts) {
4999
6083
  codeSplitting: false
5000
6084
  });
5001
6085
  await bundle2.close();
5002
- console.log(pc9.dim(` \u2713 ${opts.label} \u2192 ${path15.relative(config.root, opts.outFile)}`));
6086
+ console.log(pc9.dim(` \u2713 ${opts.label} \u2192 ${path16.relative(config.root, opts.outFile)}`));
5003
6087
  return opts.outFile;
5004
6088
  }
6089
+ function createElectronRendererConfig(config, inlineConfig = {}, overrides = {}) {
6090
+ const inlineClient = inlineConfig.environments?.client ?? {};
6091
+ return {
6092
+ ...inlineConfig,
6093
+ ...overrides,
6094
+ root: config.root,
6095
+ mode: config.mode,
6096
+ target: "web",
6097
+ framework: config.framework,
6098
+ base: config.base === "/" ? "./" : config.base,
6099
+ environments: {
6100
+ ...inlineConfig.environments ?? {},
6101
+ client: {
6102
+ ...inlineClient,
6103
+ html: config.electron.renderer
6104
+ }
6105
+ }
6106
+ };
6107
+ }
5005
6108
  function outFileName(outDir, base, format) {
5006
6109
  const ext = format === "cjs" ? ".cjs" : ".mjs";
5007
- return path15.join(outDir, base + ext);
6110
+ return path16.join(outDir, base + ext);
5008
6111
  }
5009
6112
  function normalizePreload(preload, root) {
5010
6113
  const list = Array.isArray(preload) ? preload : preload ? [preload] : [];
5011
- return list.map((p) => path15.resolve(root, p));
6114
+ return list.map((p) => path16.resolve(root, p));
5012
6115
  }
5013
6116
  function assertElectronVersion(config) {
5014
6117
  const min = config.electron.minVersion;
@@ -5023,9 +6126,9 @@ function assertElectronVersion(config) {
5023
6126
  }
5024
6127
  function detectInstalledElectron(root) {
5025
6128
  try {
5026
- const pkgPath = path15.resolve(root, "node_modules/electron/package.json");
5027
- if (!fs10.existsSync(pkgPath)) return null;
5028
- const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
6129
+ const pkgPath = path16.resolve(root, "node_modules/electron/package.json");
6130
+ if (!fs11.existsSync(pkgPath)) return null;
6131
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
5029
6132
  const major = parseInt(String(pkg.version).split(".")[0], 10);
5030
6133
  return Number.isFinite(major) ? major : null;
5031
6134
  } catch {
@@ -5046,10 +6149,11 @@ var init_electron2 = __esm({
5046
6149
  // src/server/electron-dev.ts
5047
6150
  var electron_dev_exports = {};
5048
6151
  __export(electron_dev_exports, {
6152
+ electronRendererDevPath: () => electronRendererDevPath,
5049
6153
  startElectronDev: () => startElectronDev
5050
6154
  });
5051
- import path16 from "path";
5052
- import fs11 from "fs";
6155
+ import path17 from "path";
6156
+ import fs12 from "fs";
5053
6157
  import { createRequire as createRequire5 } from "module";
5054
6158
  import { spawn } from "child_process";
5055
6159
  import chokidar from "chokidar";
@@ -5059,17 +6163,21 @@ async function startElectronDev(inlineConfig = {}) {
5059
6163
  const { noSpawn, ...rest } = inlineConfig;
5060
6164
  const config = await resolveConfig({ ...rest, target: "electron" }, "serve");
5061
6165
  warnElectronVersion(config);
5062
- console.log(pc10.cyan("\n\u26A1 nasti electron dev") + pc10.dim(` v${"2.2.0"}`));
6166
+ console.log(pc10.cyan("\n\u26A1 nasti electron dev") + pc10.dim(` v${"2.4.0"}`));
5063
6167
  const { createServer: createServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
5064
- const server = await createServer2({ ...rest, target: "electron" });
6168
+ const server = await createServer2({
6169
+ ...rest,
6170
+ target: "electron",
6171
+ framework: config.framework
6172
+ });
5065
6173
  await server.listen();
5066
- const devUrl = `http://localhost:${server.config.server.port}/`;
6174
+ const devUrl = `http://localhost:${server.config.server.port}` + electronRendererDevPath(config.electron.renderer);
5067
6175
  console.log(pc10.dim(` renderer: ${devUrl}`));
5068
- const stageDir = path16.resolve(config.root, ".nasti");
5069
- fs11.mkdirSync(stageDir, { recursive: true });
5070
- const mainEntry = path16.resolve(config.root, config.electron.main);
6176
+ const stageDir = path17.resolve(config.root, ".nasti");
6177
+ fs12.mkdirSync(stageDir, { recursive: true });
6178
+ const mainEntry = path17.resolve(config.root, config.electron.main);
5071
6179
  const preloadEntries = normalizePreload(config.electron.preload, config.root);
5072
- const builtMainFile = path16.join(stageDir, "main" + extFor(config.electron.mainFormat));
6180
+ const builtMainFile = path17.join(stageDir, "main" + extFor(config.electron.mainFormat));
5073
6181
  const builtPreloadFiles = [];
5074
6182
  const compileAll = async () => {
5075
6183
  await compileNode(config, mainEntry, {
@@ -5079,9 +6187,9 @@ async function startElectronDev(inlineConfig = {}) {
5079
6187
  });
5080
6188
  builtPreloadFiles.length = 0;
5081
6189
  for (const entry of preloadEntries) {
5082
- if (!fs11.existsSync(entry)) continue;
5083
- const base = path16.basename(entry).replace(/\.[^.]+$/, "");
5084
- const out = path16.join(stageDir, base + extFor(config.electron.preloadFormat));
6190
+ if (!fs12.existsSync(entry)) continue;
6191
+ const base = path17.basename(entry).replace(/\.[^.]+$/, "");
6192
+ const out = path17.join(stageDir, base + extFor(config.electron.preloadFormat));
5085
6193
  await compileNode(config, entry, {
5086
6194
  outFile: out,
5087
6195
  format: config.electron.preloadFormat,
@@ -5120,7 +6228,7 @@ async function startElectronDev(inlineConfig = {}) {
5120
6228
  };
5121
6229
  spawnElectron();
5122
6230
  if (config.electron.autoRestart) {
5123
- const watchTargets = [mainEntry, ...preloadEntries].filter(fs11.existsSync);
6231
+ const watchTargets = [mainEntry, ...preloadEntries].filter(fs12.existsSync);
5124
6232
  const watcher = chokidar.watch(watchTargets, { ignoreInitial: true });
5125
6233
  let restarting = null;
5126
6234
  let pending = false;
@@ -5185,18 +6293,22 @@ async function compileNode(config, entry, opts) {
5185
6293
  const result = transformCode(id, code, {
5186
6294
  sourcemap: true,
5187
6295
  jsxRuntime: "automatic",
5188
- jsxImportSource: config.framework === "vue" ? "vue" : "react"
6296
+ jsxImportSource: config.framework === "vue" ? "vue" : "react",
6297
+ target: config.electron.nodeTarget
5189
6298
  });
5190
6299
  return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
5191
6300
  }
5192
6301
  };
5193
6302
  const bundle2 = await rolldown3({
5194
6303
  input: entry,
5195
- transform: { define: envDefine },
6304
+ transform: {
6305
+ target: config.electron.nodeTarget,
6306
+ define: envDefine
6307
+ },
5196
6308
  platform: "node",
5197
6309
  plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)]
5198
6310
  });
5199
- fs11.mkdirSync(path16.dirname(opts.outFile), { recursive: true });
6311
+ fs12.mkdirSync(path17.dirname(opts.outFile), { recursive: true });
5200
6312
  await bundle2.write({
5201
6313
  file: opts.outFile,
5202
6314
  format: opts.format === "cjs" ? "cjs" : "esm",
@@ -5208,15 +6320,19 @@ async function compileNode(config, entry, opts) {
5208
6320
  });
5209
6321
  await bundle2.close();
5210
6322
  }
6323
+ function electronRendererDevPath(renderer) {
6324
+ const normalized = renderer.split(path17.sep).join("/").replace(/^\.?\//, "");
6325
+ return normalized === "index.html" ? "/" : `/${normalized}`;
6326
+ }
5211
6327
  function resolveElectronBinary(config) {
5212
- if (config.electron.electronPath && fs11.existsSync(config.electron.electronPath)) {
6328
+ if (config.electron.electronPath && fs12.existsSync(config.electron.electronPath)) {
5213
6329
  return config.electron.electronPath;
5214
6330
  }
5215
6331
  try {
5216
- const require2 = createRequire5(path16.resolve(config.root, "package.json"));
6332
+ const require2 = createRequire5(path17.resolve(config.root, "package.json"));
5217
6333
  const pathFile = require2.resolve("electron");
5218
6334
  const electronModule = require2(pathFile);
5219
- if (typeof electronModule === "string" && fs11.existsSync(electronModule)) {
6335
+ if (typeof electronModule === "string" && fs12.existsSync(electronModule)) {
5220
6336
  return electronModule;
5221
6337
  }
5222
6338
  } catch {
@@ -5391,20 +6507,20 @@ cli.command("preview [root]", "Preview production build").option("--port <port>"
5391
6507
  const logger = createCliLogger(options);
5392
6508
  try {
5393
6509
  const http2 = await import("http");
5394
- const path17 = await import("path");
6510
+ const path18 = await import("path");
5395
6511
  const os2 = await import("os");
5396
6512
  const sirv2 = (await import("sirv")).default;
5397
6513
  const connect2 = (await import("connect")).default;
5398
6514
  const { printServerUrls: printServerUrls2 } = await Promise.resolve().then(() => (init_logger(), logger_exports));
5399
- const resolvedRoot = path17.resolve(root ?? ".");
5400
- const outDir = path17.resolve(resolvedRoot, options.outDir);
6515
+ const resolvedRoot = path18.resolve(root ?? ".");
6516
+ const outDir = path18.resolve(resolvedRoot, options.outDir);
5401
6517
  const app = connect2();
5402
6518
  app.use(sirv2(outDir, { single: true, etag: true, gzip: true, brotli: true }));
5403
6519
  const port = options.port;
5404
6520
  const host = options.host === true ? "0.0.0.0" : options.host ?? "localhost";
5405
6521
  http2.createServer(app).listen(port, host, () => {
5406
6522
  logger.info(`
5407
- ${pc11.cyan(pc11.bold("NASTI"))} ${pc11.cyan(`v${"2.2.0"}`)} ${pc11.dim("preview")}
6523
+ ${pc11.cyan(pc11.bold("NASTI"))} ${pc11.cyan(`v${"2.4.0"}`)} ${pc11.dim("preview")}
5408
6524
  `);
5409
6525
  printServerUrls2(
5410
6526
  {
@@ -5421,6 +6537,6 @@ cli.command("preview [root]", "Preview production build").option("--port <port>"
5421
6537
  }
5422
6538
  });
5423
6539
  cli.help();
5424
- cli.version("2.2.0");
6540
+ cli.version("2.4.0");
5425
6541
  cli.parse();
5426
6542
  //# sourceMappingURL=cli.js.map