@nasti-toolchain/nasti 2.1.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -225,6 +225,92 @@ var init_logger = __esm({
225
225
  }
226
226
  });
227
227
 
228
+ // src/core/plugin-api.ts
229
+ function orderPlugins(plugins) {
230
+ 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);
231
+ const indexesByName = /* @__PURE__ */ new Map();
232
+ baseline.forEach((plugin, index2) => {
233
+ const indexes = indexesByName.get(plugin.name) ?? [];
234
+ indexes.push(index2);
235
+ indexesByName.set(plugin.name, indexes);
236
+ });
237
+ const edges = baseline.map(() => /* @__PURE__ */ new Set());
238
+ const indegree = baseline.map(() => 0);
239
+ const addEdge = (from, to) => {
240
+ if (from === to || edges[from].has(to)) return;
241
+ edges[from].add(to);
242
+ indegree[to]++;
243
+ };
244
+ baseline.forEach((plugin, current) => {
245
+ for (const dependency of plugin.pre ?? []) {
246
+ for (const before of indexesByName.get(dependency) ?? []) addEdge(before, current);
247
+ }
248
+ for (const dependency of plugin.post ?? []) {
249
+ for (const after of indexesByName.get(dependency) ?? []) addEdge(current, after);
250
+ }
251
+ });
252
+ const ready = indegree.map((degree, index2) => ({ degree, index: index2 })).filter(({ degree }) => degree === 0).map(({ index: index2 }) => index2);
253
+ const ordered = [];
254
+ while (ready.length > 0) {
255
+ ready.sort((a, b) => a - b);
256
+ const current = ready.shift();
257
+ ordered.push(baseline[current]);
258
+ for (const next of edges[current]) {
259
+ indegree[next]--;
260
+ if (indegree[next] === 0) ready.push(next);
261
+ }
262
+ }
263
+ if (ordered.length !== baseline.length) {
264
+ const cyclic = baseline.filter((_, index2) => indegree[index2] > 0).map((plugin) => plugin.name);
265
+ throw new Error(
266
+ `[nasti] circular plugin setup dependency: ${[...new Set(cyclic)].join(", ")}`
267
+ );
268
+ }
269
+ return ordered;
270
+ }
271
+ async function setupPluginApi(config, plugins) {
272
+ const exposed = /* @__PURE__ */ new Map();
273
+ const api = {
274
+ config,
275
+ logger: config.logger,
276
+ expose(key, value) {
277
+ if (exposed.has(key) && exposed.get(key) !== value) {
278
+ throw new Error(`[nasti] plugin API key already exposed: ${String(key)}`);
279
+ }
280
+ exposed.set(key, value);
281
+ },
282
+ useExposed(key) {
283
+ return exposed.get(key);
284
+ }
285
+ };
286
+ apiByConfig.set(config, api);
287
+ for (const plugin of plugins) {
288
+ await plugin.setup?.(api);
289
+ }
290
+ return api;
291
+ }
292
+ function getPluginApi(config) {
293
+ const api = apiByConfig.get(config);
294
+ if (!api) {
295
+ throw new Error(
296
+ "[nasti] internal: plugin API requested before setup; getPluginApi requires the original ResolvedConfig reference registered by setupPluginApi, not a shallow copy"
297
+ );
298
+ }
299
+ return api;
300
+ }
301
+ function enforceRank(plugin) {
302
+ if (plugin.enforce === "pre") return 0;
303
+ if (plugin.enforce === "post") return 2;
304
+ return 1;
305
+ }
306
+ var apiByConfig;
307
+ var init_plugin_api = __esm({
308
+ "src/core/plugin-api.ts"() {
309
+ "use strict";
310
+ apiByConfig = /* @__PURE__ */ new WeakMap();
311
+ }
312
+ });
313
+
228
314
  // src/config/index.ts
229
315
  import { pathToFileURL } from "url";
230
316
  import path from "path";
@@ -265,6 +351,43 @@ async function loadConfigFromFile(root) {
265
351
  }
266
352
  return {};
267
353
  }
354
+ function detectFramework(root) {
355
+ const sourceRoot = path.resolve(root, "src");
356
+ if (containsVueFile(sourceRoot)) return "vue";
357
+ const packagePath = path.resolve(root, "package.json");
358
+ if (fs.existsSync(packagePath)) {
359
+ try {
360
+ const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
361
+ const dependencies = {
362
+ ...pkg.dependencies ?? {},
363
+ ...pkg.devDependencies ?? {},
364
+ ...pkg.peerDependencies ?? {},
365
+ ...pkg.optionalDependencies ?? {}
366
+ };
367
+ const hasVue = "vue" in dependencies || "@vue/runtime-dom" in dependencies;
368
+ const hasReact = "react" in dependencies || "react-dom" in dependencies;
369
+ if (hasVue && !hasReact) return "vue";
370
+ if (hasReact) return "react";
371
+ if (hasVue) return "vue";
372
+ } catch {
373
+ }
374
+ }
375
+ return "react";
376
+ }
377
+ function containsVueFile(dir, depth = 0) {
378
+ if (depth > 5 || !fs.existsSync(dir)) return false;
379
+ try {
380
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
381
+ if (entry.isFile() && entry.name.endsWith(".vue")) return true;
382
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && containsVueFile(path.join(dir, entry.name), depth + 1)) {
383
+ return true;
384
+ }
385
+ }
386
+ } catch {
387
+ return false;
388
+ }
389
+ return false;
390
+ }
268
391
  async function loadTsConfig(filePath) {
269
392
  const { transformSync: transformSync2 } = await import("oxc-transform");
270
393
  const code = fs.readFileSync(filePath, "utf-8");
@@ -311,7 +434,7 @@ async function resolveConfig(inlineConfig = {}, command) {
311
434
  base: merged.base ?? defaults.base,
312
435
  mode,
313
436
  target: merged.target ?? defaults.target,
314
- framework: merged.framework ?? defaults.framework,
437
+ framework: (merged.framework ?? defaults.framework) === "auto" ? detectFramework(root) : merged.framework,
315
438
  command,
316
439
  resolve: {
317
440
  // tsconfig paths 优先级最低:tsconfig < defaults < user config
@@ -358,7 +481,12 @@ async function resolveConfig(inlineConfig = {}, command) {
358
481
  if (envOptions.build) Object.assign(resolved.build, envOptions.build);
359
482
  resolved.environments.client = {
360
483
  consumer,
361
- entry: [],
484
+ entry: normalizeEnvironmentEntries(envOptions.entry, root),
485
+ html: path.resolve(
486
+ root,
487
+ envOptions.html ?? (resolved.target === "electron" ? resolved.electron.renderer : "index.html")
488
+ ),
489
+ driver: envOptions.driver,
362
490
  // 同引用 —— 精确镜像(assertClientEnvironmentMirror 校验)
363
491
  resolve: resolved.resolve,
364
492
  build: resolved.build
@@ -367,7 +495,9 @@ async function resolveConfig(inlineConfig = {}, command) {
367
495
  }
368
496
  resolved.environments[name] = {
369
497
  consumer,
370
- entry: (Array.isArray(envOptions.entry) ? envOptions.entry : envOptions.entry ? [envOptions.entry] : []).map((e) => path.resolve(root, e)),
498
+ entry: normalizeEnvironmentEntries(envOptions.entry, root),
499
+ html: envOptions.consumer === "client" && envOptions.html ? path.resolve(root, envOptions.html) : void 0,
500
+ driver: envOptions.driver,
371
501
  resolve: {
372
502
  alias: { ...resolved.resolve.alias, ...envOptions.resolve?.alias },
373
503
  extensions: envOptions.resolve?.extensions ?? [...resolved.resolve.extensions],
@@ -386,12 +516,13 @@ async function resolveConfig(inlineConfig = {}, command) {
386
516
  };
387
517
  }
388
518
  assertClientEnvironmentMirror(resolved);
389
- const filteredPlugins = rawPlugins.filter((p) => {
519
+ const filteredPlugins = orderPlugins(rawPlugins.filter((p) => {
390
520
  if (!p.apply) return true;
391
521
  if (typeof p.apply === "function") return p.apply(resolved, env);
392
522
  return p.apply === command;
393
- });
523
+ }));
394
524
  resolved.plugins = filteredPlugins;
525
+ await setupPluginApi(resolved, filteredPlugins);
395
526
  if (resolved.target === "electron") {
396
527
  const autoExternal = detectNativeDeps(root);
397
528
  if (autoExternal.length > 0) {
@@ -407,6 +538,10 @@ async function resolveConfig(inlineConfig = {}, command) {
407
538
  }
408
539
  return resolved;
409
540
  }
541
+ function normalizeEnvironmentEntries(entry, root) {
542
+ const entries = Array.isArray(entry) ? entry : entry ? [entry] : [];
543
+ return entries.map((item) => path.resolve(root, item));
544
+ }
410
545
  function detectNativeDeps(root) {
411
546
  const result = /* @__PURE__ */ new Set();
412
547
  const pkgJsonPath = path.resolve(root, "package.json");
@@ -524,6 +659,7 @@ var init_config = __esm({
524
659
  "use strict";
525
660
  init_defaults();
526
661
  init_logger();
662
+ init_plugin_api();
527
663
  CONFIG_FILES = [
528
664
  "nasti.config.ts",
529
665
  "nasti.config.js",
@@ -1918,7 +2054,8 @@ function transformCode(filename, code, options = {}) {
1918
2054
  importSource: options.jsxImportSource ?? "react",
1919
2055
  refresh: options.reactRefresh ?? false
1920
2056
  } : void 0,
1921
- sourcemap: options.sourcemap ?? true
2057
+ sourcemap: options.sourcemap ?? true,
2058
+ target: options.target
1922
2059
  });
1923
2060
  if (result.errors && result.errors.length > 0) {
1924
2061
  const msg = result.errors.map((e) => e.message ?? String(e)).join("\n");
@@ -2122,7 +2259,7 @@ function htmlPlugin(config) {
2122
2259
  transformIndexHtml(html) {
2123
2260
  const tags = [];
2124
2261
  if (config.command === "serve") {
2125
- const isReactLike = config.framework === "react" || config.framework === "auto";
2262
+ const isReactLike = config.framework === "react";
2126
2263
  if (isReactLike) {
2127
2264
  tags.push({
2128
2265
  tag: "script",
@@ -2176,8 +2313,8 @@ function serializeTag(tag) {
2176
2313
  }
2177
2314
  return ` <${tag.tag}${attrs}>${children}</${tag.tag}>`;
2178
2315
  }
2179
- async function readHtmlFile(root) {
2180
- const htmlPath = path6.resolve(root, "index.html");
2316
+ async function readHtmlFile(root, htmlFile = "index.html") {
2317
+ const htmlPath = path6.isAbsolute(htmlFile) ? htmlFile : path6.resolve(root, htmlFile);
2181
2318
  if (!fs4.existsSync(htmlPath)) return null;
2182
2319
  return fs4.readFileSync(htmlPath, "utf-8");
2183
2320
  }
@@ -2223,21 +2360,11 @@ var init_builtins = __esm({
2223
2360
  });
2224
2361
 
2225
2362
  // src/core/plugin-container.ts
2226
- function sortPlugins(plugins) {
2227
- const pre = [];
2228
- const normal = [];
2229
- const post = [];
2230
- for (const plugin of plugins) {
2231
- if (plugin.enforce === "pre") pre.push(plugin);
2232
- else if (plugin.enforce === "post") post.push(plugin);
2233
- else normal.push(plugin);
2234
- }
2235
- return [...pre, ...normal, ...post];
2236
- }
2237
2363
  var PluginContainer;
2238
2364
  var init_plugin_container = __esm({
2239
2365
  "src/core/plugin-container.ts"() {
2240
2366
  "use strict";
2367
+ init_plugin_api();
2241
2368
  PluginContainer = class {
2242
2369
  plugins;
2243
2370
  config;
@@ -2248,7 +2375,7 @@ var init_plugin_container = __esm({
2248
2375
  constructor(config, environment) {
2249
2376
  this.config = config;
2250
2377
  this.environment = environment;
2251
- this.plugins = sortPlugins(config.plugins);
2378
+ this.plugins = orderPlugins(config.plugins);
2252
2379
  this.ctx = this.createContext();
2253
2380
  }
2254
2381
  createContext() {
@@ -2544,6 +2671,7 @@ var init_environment = __esm({
2544
2671
  init_module_graph();
2545
2672
  init_hot_channel();
2546
2673
  init_debug();
2674
+ init_plugin_api();
2547
2675
  debug2 = createDebugger("nasti:environment");
2548
2676
  NastiEnvironment = class {
2549
2677
  name;
@@ -2552,6 +2680,7 @@ var init_environment = __esm({
2552
2680
  config;
2553
2681
  options;
2554
2682
  hot;
2683
+ driver;
2555
2684
  /** applyToEnvironment 过滤后的插件(init() 后可用) */
2556
2685
  plugins = [];
2557
2686
  /** per-env 插件容器(init() 后可用;dev 管线使用) */
@@ -2559,6 +2688,7 @@ var init_environment = __esm({
2559
2688
  /** per-env 模块图(dev 管线使用) */
2560
2689
  moduleGraph;
2561
2690
  candidatePlugins;
2691
+ pluginApi;
2562
2692
  initialized = false;
2563
2693
  constructor(name, config, init = {}) {
2564
2694
  const options = config.environments[name];
@@ -2575,6 +2705,7 @@ var init_environment = __esm({
2575
2705
  this.hot = init.hot ?? createNoopHotChannel();
2576
2706
  this.moduleGraph = new ModuleGraph();
2577
2707
  this.candidatePlugins = init.plugins ?? config.plugins;
2708
+ this.pluginApi = init.pluginApi ?? getPluginApi(config);
2578
2709
  }
2579
2710
  /** 过滤插件并建 per-env PluginContainer */
2580
2711
  async init() {
@@ -2585,10 +2716,41 @@ var init_environment = __esm({
2585
2716
  { ...this.config, plugins: this.plugins },
2586
2717
  this
2587
2718
  );
2719
+ if (this.options.driver) {
2720
+ const claimed = [];
2721
+ for (const plugin of this.plugins) {
2722
+ const driver = await plugin.createEnvironmentDriver?.(this, this.pluginApi);
2723
+ if (driver) claimed.push({ plugin, driver });
2724
+ }
2725
+ if (claimed.length === 0) {
2726
+ throw new Error(
2727
+ `[nasti] environment "${this.name}" requested driver "${this.options.driver}", but no plugin provided it`
2728
+ );
2729
+ }
2730
+ if (claimed.length > 1) {
2731
+ throw new Error(
2732
+ `[nasti] environment "${this.name}" was claimed by multiple drivers: ` + claimed.map(({ plugin, driver }) => `${plugin.name} (${driver.name})`).join(", ")
2733
+ );
2734
+ }
2735
+ this.driver = claimed[0].driver;
2736
+ debug2?.(`env "${this.name}" uses driver "${this.driver.name}"`);
2737
+ }
2588
2738
  debug2?.(`env "${this.name}" initialized (${this.plugins.length} plugins)`);
2589
2739
  }
2740
+ getDriverContext() {
2741
+ return {
2742
+ environment: this,
2743
+ config: this.config,
2744
+ api: this.pluginApi,
2745
+ logger: this.config.logger
2746
+ };
2747
+ }
2590
2748
  async close() {
2591
- await this.hot.close?.();
2749
+ try {
2750
+ await this.driver?.close?.(this.getDriverContext());
2751
+ } finally {
2752
+ await this.hot.close?.();
2753
+ }
2592
2754
  }
2593
2755
  };
2594
2756
  }
@@ -2752,6 +2914,7 @@ var build_exports = {};
2752
2914
  __export(build_exports, {
2753
2915
  build: () => build,
2754
2916
  getRolldownOptions: () => getRolldownOptions,
2917
+ replaceEntryScript: () => replaceEntryScript,
2755
2918
  resolveClientEntries: () => resolveClientEntries,
2756
2919
  toRolldownPlugins: () => toRolldownPlugins
2757
2920
  });
@@ -2835,13 +2998,20 @@ function toRolldownPlugins(plugins) {
2835
2998
  }));
2836
2999
  }
2837
3000
  function resolveClientEntries(config, html) {
3001
+ const configuredEntries = config.environments.client?.entry ?? [];
3002
+ if (configuredEntries.length > 0) return configuredEntries;
2838
3003
  const entryPoints = [];
3004
+ const htmlFile = config.environments.client?.html;
3005
+ const htmlDir = htmlFile ? path9.dirname(htmlFile) : config.root;
2839
3006
  if (html) {
2840
3007
  const scriptMatches = html.matchAll(/<script[^>]+src=["']([^"']+)["'][^>]*>/gi);
2841
3008
  for (const match of scriptMatches) {
2842
3009
  const src = match[1];
2843
3010
  if (src && !src.startsWith("http")) {
2844
- entryPoints.push(path9.resolve(config.root, src.replace(/^\//, "")));
3011
+ const cleanSrc = src.split(/[?#]/, 1)[0];
3012
+ entryPoints.push(
3013
+ cleanSrc.startsWith("/") ? path9.resolve(config.root, cleanSrc.replace(/^\//, "")) : path9.resolve(htmlDir, cleanSrc)
3014
+ );
2845
3015
  }
2846
3016
  }
2847
3017
  }
@@ -2877,21 +3047,55 @@ async function build(inlineConfig = {}) {
2877
3047
  const startTime = performance.now();
2878
3048
  logger.info(
2879
3049
  pc4.cyan(`
2880
- nasti v${"2.1.0"} `) + pc4.green(`building for ${config.mode}...`)
3050
+ nasti v${"2.3.1"} `) + pc4.green(`building for ${config.mode}...`)
2881
3051
  );
2882
3052
  debug4?.(`root: ${config.root}`);
2883
3053
  const buildableNames = Object.keys(config.environments).filter(
2884
- (name) => name === "client" || config.environments[name].entry.length > 0
3054
+ (name) => name === "client" || config.environments[name].entry.length > 0 || !!config.environments[name].driver
2885
3055
  );
2886
3056
  buildableNames.sort((a, b) => a === "client" ? -1 : b === "client" ? 1 : 0);
2887
3057
  const environments = {};
3058
+ const environmentResults = {};
3059
+ const initializedEnvironments = [];
2888
3060
  let clientOutput = [];
2889
- for (const name of buildableNames) {
2890
- const output = name === "client" ? await buildClientEnvironment(config) : await buildServerEnvironment(config, name);
2891
- environments[name] = output;
2892
- if (name === "client") clientOutput = output;
2893
- if (buildableNames.length > 1) {
2894
- debug4?.(`environment "${name}" built (${output.length} files)`);
3061
+ let buildFailed = false;
3062
+ try {
3063
+ for (const name of buildableNames) {
3064
+ const built = name === "client" ? await buildClientEnvironment(config) : await buildServerEnvironment(config, name);
3065
+ initializedEnvironments.push(built.environment);
3066
+ environments[name] = built.result.output;
3067
+ environmentResults[name] = built.result;
3068
+ if (name === "client") clientOutput = built.result.output;
3069
+ if (buildableNames.length > 1) {
3070
+ debug4?.(`environment "${name}" built (${built.result.output.length} files)`);
3071
+ }
3072
+ }
3073
+ const pluginApi = getPluginApi(config);
3074
+ for (const plugin of config.plugins) {
3075
+ await plugin.afterBuildApp?.(environmentResults, pluginApi);
3076
+ }
3077
+ } catch (error) {
3078
+ buildFailed = true;
3079
+ throw error;
3080
+ } finally {
3081
+ let closeFailed = false;
3082
+ let firstCloseError;
3083
+ for (const environment of [...initializedEnvironments].reverse()) {
3084
+ try {
3085
+ await environment.close();
3086
+ } catch (error) {
3087
+ if (!closeFailed) {
3088
+ closeFailed = true;
3089
+ firstCloseError = error;
3090
+ }
3091
+ const closeError = error instanceof Error ? error : new Error(String(error));
3092
+ logger.error(`[nasti] failed to close environment "${environment.name}"`, {
3093
+ error: closeError
3094
+ });
3095
+ }
3096
+ }
3097
+ if (closeFailed && !buildFailed) {
3098
+ throw firstCloseError;
2895
3099
  }
2896
3100
  }
2897
3101
  const elapsed = ((performance.now() - startTime) / 1e3).toFixed(2);
@@ -2904,83 +3108,130 @@ nasti v${"2.1.0"} `) + pc4.green(`building for ${config.mode}...`)
2904
3108
  const envSuffix = buildableNames.length > 1 ? ` (${buildableNames.join(" + ")})` : "";
2905
3109
  logger.info(pc4.green(`\u2713 built in ${elapsed}s`) + pc4.dim(envSuffix));
2906
3110
  logger.info(pc4.dim(` ${fileCount} files, ${displaySize(totalSize)} total \u2192 ${config.build.outDir}/`));
2907
- return { output: clientOutput, environments };
3111
+ return { output: clientOutput, environments, environmentResults };
2908
3112
  }
2909
3113
  async function buildClientEnvironment(config) {
2910
3114
  const logger = config.logger;
2911
3115
  const outDir = path9.resolve(config.root, config.build.outDir);
2912
- if (config.build.emptyOutDir && fs6.existsSync(outDir)) {
2913
- fs6.rmSync(outDir, { recursive: true, force: true });
2914
- }
2915
- fs6.mkdirSync(outDir, { recursive: true });
2916
- const html = await readHtmlFile(config.root);
2917
- const entryPoints = resolveClientEntries(config, html);
2918
- if (entryPoints.length === 0) {
2919
- throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
2920
- }
2921
3116
  const cssEngine = createCssEngine();
2922
3117
  const pluginList = resolvePluginList(config, config.plugins, { cssEngine });
2923
- const clientEnv = new NastiEnvironment("client", { ...config, plugins: pluginList }, {
3118
+ const clientEnv = new NastiEnvironment("client", config, {
2924
3119
  mode: "build",
2925
- plugins: pluginList
3120
+ plugins: pluginList,
3121
+ pluginApi: getPluginApi(config)
2926
3122
  });
2927
3123
  await clientEnv.init();
2928
- const allPlugins = clientEnv.plugins;
2929
- const nativeReporter = config.logLevel === "silent" ? null : await tryNativeReporterPlugin(config, logger);
2930
- const rolldownPlugins = [
2931
- createOxcTransformPlugin(config, clientEnv),
2932
- ...toRolldownPlugins(allPlugins),
2933
- ...nativeReporter ? [nativeReporter] : []
2934
- ];
2935
- const { inputOptions, outputOptions } = getRolldownOptions(clientEnv, entryPoints, rolldownPlugins);
2936
- const bundle2 = await rolldown(inputOptions);
2937
- const { output } = await bundle2.write(outputOptions);
2938
- await bundle2.close();
2939
- if (html) {
2940
- let processedHtml = html;
2941
- const htmlPlugins = [...allPlugins.filter((p) => p.transformIndexHtml), htmlPlugin(config)];
2942
- for (const p of htmlPlugins) {
2943
- const result = await p.transformIndexHtml(processedHtml);
2944
- if (typeof result === "string") {
2945
- processedHtml = result;
2946
- } else if (result && "html" in result) {
2947
- processedHtml = processHtml(result.html, result.tags);
2948
- } else if (Array.isArray(result)) {
2949
- processedHtml = processHtml(processedHtml, result);
2950
- }
2951
- }
2952
- processedHtml = injectCssLinks(processedHtml, cssEngine, config);
2953
- for (const chunk of output) {
2954
- if (chunk.type === "chunk" && chunk.isEntry && chunk.facadeModuleId) {
2955
- const originalEntry = path9.relative(config.root, chunk.facadeModuleId);
2956
- processedHtml = processedHtml.replace(
2957
- new RegExp(`(src=["'])/?(${escapeRegExp(originalEntry)})(["'])`, "g"),
2958
- `$1${config.base}${chunk.fileName}$3`
3124
+ try {
3125
+ if (clientEnv.driver) {
3126
+ if (!clientEnv.driver.build) {
3127
+ throw new Error(
3128
+ `[nasti] environment "client" driver "${clientEnv.driver.name}" does not implement build()`
2959
3129
  );
2960
3130
  }
3131
+ const result = await clientEnv.driver.build(clientEnv.getDriverContext());
3132
+ return { environment: clientEnv, result };
2961
3133
  }
2962
- fs6.writeFileSync(path9.resolve(outDir, "index.html"), processedHtml);
2963
- }
2964
- if (!nativeReporter && config.logLevel !== "silent") {
2965
- reportBuildOutput(output, config, logger);
3134
+ if (config.build.emptyOutDir && fs6.existsSync(outDir)) {
3135
+ fs6.rmSync(outDir, { recursive: true, force: true });
3136
+ }
3137
+ fs6.mkdirSync(outDir, { recursive: true });
3138
+ const htmlFile = config.environments.client.html ?? path9.resolve(config.root, "index.html");
3139
+ const html = await readHtmlFile(config.root, htmlFile);
3140
+ const entryPoints = resolveClientEntries(config, html);
3141
+ if (entryPoints.length === 0) {
3142
+ throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
3143
+ }
3144
+ const allPlugins = clientEnv.plugins;
3145
+ const nativeReporter = config.logLevel === "silent" ? null : await tryNativeReporterPlugin(config, logger);
3146
+ const rolldownPlugins = [
3147
+ createOxcTransformPlugin(config, clientEnv),
3148
+ ...toRolldownPlugins(allPlugins),
3149
+ ...nativeReporter ? [nativeReporter] : []
3150
+ ];
3151
+ const { inputOptions, outputOptions } = getRolldownOptions(
3152
+ clientEnv,
3153
+ entryPoints,
3154
+ rolldownPlugins
3155
+ );
3156
+ const bundle2 = await rolldown(inputOptions);
3157
+ const { output } = await bundle2.write(outputOptions);
3158
+ await bundle2.close();
3159
+ if (html) {
3160
+ let processedHtml = html;
3161
+ const htmlPlugins = [...allPlugins.filter((p) => p.transformIndexHtml), htmlPlugin(config)];
3162
+ for (const p of htmlPlugins) {
3163
+ const result = await p.transformIndexHtml(processedHtml);
3164
+ if (typeof result === "string") {
3165
+ processedHtml = result;
3166
+ } else if (result && "html" in result) {
3167
+ processedHtml = processHtml(result.html, result.tags);
3168
+ } else if (Array.isArray(result)) {
3169
+ processedHtml = processHtml(processedHtml, result);
3170
+ }
3171
+ }
3172
+ processedHtml = injectCssLinks(processedHtml, cssEngine, config);
3173
+ for (const chunk of output) {
3174
+ if (chunk.type === "chunk" && chunk.isEntry && chunk.facadeModuleId) {
3175
+ processedHtml = replaceEntryScript(
3176
+ processedHtml,
3177
+ chunk.facadeModuleId,
3178
+ chunk.fileName,
3179
+ config,
3180
+ htmlFile,
3181
+ config.base
3182
+ );
3183
+ }
3184
+ }
3185
+ fs6.writeFileSync(path9.resolve(outDir, "index.html"), processedHtml);
3186
+ }
3187
+ if (!nativeReporter && config.logLevel !== "silent") {
3188
+ reportBuildOutput(output, config, logger);
3189
+ }
3190
+ warnLargeChunks(output, config, logger);
3191
+ return { environment: clientEnv, result: { output } };
3192
+ } catch (error) {
3193
+ try {
3194
+ await clientEnv.close();
3195
+ } catch (closeError) {
3196
+ const normalized = closeError instanceof Error ? closeError : new Error(String(closeError));
3197
+ logger.error("[nasti] failed to close client environment after build failure", {
3198
+ error: normalized
3199
+ });
3200
+ }
3201
+ throw error;
2966
3202
  }
2967
- warnLargeChunks(output, config, logger);
2968
- return output;
2969
3203
  }
2970
3204
  async function buildServerEnvironment(config, name) {
2971
3205
  const envOptions = config.environments[name];
2972
3206
  const logger = config.logger;
3207
+ const pluginList = resolvePluginList(config, config.plugins, { consumer: envOptions.consumer });
3208
+ const environment = new NastiEnvironment(name, config, {
3209
+ mode: "build",
3210
+ plugins: pluginList,
3211
+ pluginApi: getPluginApi(config)
3212
+ });
3213
+ await environment.init();
3214
+ if (environment.driver) {
3215
+ if (!environment.driver.build) {
3216
+ await environment.close();
3217
+ throw new Error(
3218
+ `[nasti] environment "${name}" driver "${environment.driver.name}" does not implement build()`
3219
+ );
3220
+ }
3221
+ try {
3222
+ const result = await environment.driver.build(environment.getDriverContext());
3223
+ return { environment, result };
3224
+ } catch (error) {
3225
+ await environment.close();
3226
+ throw error;
3227
+ }
3228
+ }
2973
3229
  for (const entry of envOptions.entry) {
2974
3230
  if (!fs6.existsSync(entry)) {
3231
+ await environment.close();
2975
3232
  throw new Error(`[nasti] environment "${name}" entry not found: ${entry}`);
2976
3233
  }
2977
3234
  }
2978
- const pluginList = resolvePluginList(config, config.plugins, { consumer: envOptions.consumer });
2979
- const environment = new NastiEnvironment(name, { ...config, plugins: pluginList }, {
2980
- mode: "build",
2981
- plugins: pluginList
2982
- });
2983
- await environment.init();
2984
3235
  const rolldownPlugins = [
2985
3236
  createOxcTransformPlugin(config, environment),
2986
3237
  ...toRolldownPlugins(environment.plugins)
@@ -3000,7 +3251,7 @@ async function buildServerEnvironment(config, name) {
3000
3251
  logger.info(
3001
3252
  pc4.dim(` [${name}] `) + output.map((o) => path9.join(envOptions.build.outDir, o.fileName)).join(pc4.dim(", "))
3002
3253
  );
3003
- return output;
3254
+ return { environment, result: { output } };
3004
3255
  }
3005
3256
  function injectCssLinks(html, cssEngine, config) {
3006
3257
  const cssLinkTags = [];
@@ -3026,6 +3277,25 @@ function injectCssLinks(html, cssEngine, config) {
3026
3277
  function escapeRegExp(string) {
3027
3278
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3028
3279
  }
3280
+ function replaceEntryScript(html, facadeModuleId, fileName, config, htmlFile, urlPrefix) {
3281
+ const rootRelative = path9.relative(config.root, facadeModuleId).split(path9.sep).join("/");
3282
+ const resolvedHtmlFile = path9.resolve(config.root, htmlFile);
3283
+ const htmlRelative = path9.relative(path9.dirname(resolvedHtmlFile), facadeModuleId).split(path9.sep).join("/");
3284
+ const candidates = /* @__PURE__ */ new Set([
3285
+ rootRelative,
3286
+ `/${rootRelative}`,
3287
+ htmlRelative,
3288
+ `./${htmlRelative}`
3289
+ ]);
3290
+ let processed = html;
3291
+ for (const candidate of candidates) {
3292
+ processed = processed.replace(
3293
+ new RegExp(`(src=["'])${escapeRegExp(candidate)}([?#][^"']*)?(["'])`, "g"),
3294
+ `$1${urlPrefix}${fileName}$3`
3295
+ );
3296
+ }
3297
+ return processed;
3298
+ }
3029
3299
  var debug4, NODE_BUILTINS;
3030
3300
  var init_build = __esm({
3031
3301
  "src/build/index.ts"() {
@@ -3039,6 +3309,7 @@ var init_build = __esm({
3039
3309
  init_env();
3040
3310
  init_reporter();
3041
3311
  init_debug();
3312
+ init_plugin_api();
3042
3313
  debug4 = createDebugger("nasti:build");
3043
3314
  NODE_BUILTINS = /* @__PURE__ */ new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]);
3044
3315
  }
@@ -3205,7 +3476,10 @@ function transformMiddleware(ctx) {
3205
3476
  return;
3206
3477
  }
3207
3478
  if (url === "/" || url.endsWith(".html")) {
3208
- const html = await readHtmlFile(ctx.config.root);
3479
+ const html = await readHtmlFile(
3480
+ ctx.config.root,
3481
+ ctx.config.environments.client?.html
3482
+ );
3209
3483
  if (html) {
3210
3484
  let processedHtml = html;
3211
3485
  for (const plugin of ctx.config.plugins) {
@@ -3776,7 +4050,7 @@ const hotModulesMap = new Map();
3776
4050
  const disposeMap = new Map();
3777
4051
  const pruneMap = new Map();
3778
4052
 
3779
- socket.addEventListener('message', ({ data }) => {
4053
+ socket.addEventListener('message', async ({ data }) => {
3780
4054
  const payload = JSON.parse(data);
3781
4055
  switch (payload.type) {
3782
4056
  case 'connected':
@@ -3784,14 +4058,21 @@ socket.addEventListener('message', ({ data }) => {
3784
4058
  clearErrorOverlay();
3785
4059
  break;
3786
4060
  case 'update':
3787
- payload.updates.forEach((update) => {
3788
- if (update.type === 'js-update') {
3789
- fetchUpdate(update);
3790
- } else if (update.type === 'css-update') {
3791
- updateCss(update.path);
3792
- }
3793
- });
3794
- clearErrorOverlay();
4061
+ try {
4062
+ await Promise.all(payload.updates.map((update) => {
4063
+ if (update.type === 'js-update') {
4064
+ return fetchUpdate(update);
4065
+ } else if (update.type === 'css-update') {
4066
+ return updateCss(update.path);
4067
+ }
4068
+ }));
4069
+ clearErrorOverlay();
4070
+ console.log('[nasti] HMR update complete, reloading page');
4071
+ location.reload();
4072
+ } catch (err) {
4073
+ console.error('[nasti] HMR update failed:', err);
4074
+ showErrorOverlay(err);
4075
+ }
3795
4076
  break;
3796
4077
  case 'full-reload':
3797
4078
  console.log('[nasti] full reload');
@@ -3833,7 +4114,7 @@ async function fetchUpdate(update) {
3833
4114
  function updateCss(path) {
3834
4115
  const el = document.querySelector(\`style[data-nasti-css="\${path}"]\`);
3835
4116
  if (el) {
3836
- fetch(path + '?t=' + Date.now())
4117
+ return fetch(path + '?t=' + Date.now())
3837
4118
  .then(r => r.text())
3838
4119
  .then(css => { el.textContent = css; });
3839
4120
  }
@@ -4230,7 +4511,7 @@ async function createBundledDevServer(opts) {
4230
4511
  `[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.`
4231
4512
  );
4232
4513
  }
4233
- const html = await readHtmlFile(config.root);
4514
+ const html = await readHtmlFile(config.root, config.environments.client?.html);
4234
4515
  const entryPoints = resolveClientEntries(config, html);
4235
4516
  if (entryPoints.length === 0) {
4236
4517
  throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
@@ -4285,7 +4566,10 @@ async function createBundledDevServer(opts) {
4285
4566
  continue;
4286
4567
  }
4287
4568
  const patchPath = `__nasti_patch/${update.filename}`;
4288
- patches.set(patchPath, update.code + "\n;export {}");
4569
+ patches.set(
4570
+ patchPath,
4571
+ update.code + "\n;globalThis.location?.reload();\n;export {}"
4572
+ );
4289
4573
  if (update.sourcemap && update.sourcemapFilename) {
4290
4574
  patches.set(`__nasti_patch/${update.sourcemapFilename}`, update.sourcemap);
4291
4575
  }
@@ -4433,7 +4717,7 @@ async function createBundledDevServer(opts) {
4433
4717
  return;
4434
4718
  }
4435
4719
  if (pathname === "/" || pathname.endsWith(".html")) {
4436
- const rawHtml = await readHtmlFile(config.root);
4720
+ const rawHtml = await readHtmlFile(config.root, config.environments.client?.html);
4437
4721
  if (rawHtml) {
4438
4722
  res.setHeader("Content-Type", "text/html");
4439
4723
  res.setHeader("Cache-Control", "no-store");
@@ -4517,10 +4801,13 @@ async function renderBundledIndexHtml(html, config, entryFileNames) {
4517
4801
  }
4518
4802
  }
4519
4803
  for (const [facadeModuleId, fileName] of entryFileNames) {
4520
- const originalEntry = path14.relative(config.root, facadeModuleId);
4521
- processed = processed.replace(
4522
- new RegExp(`(src=["'])/?(${originalEntry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})(["'])`, "g"),
4523
- `$1/${fileName}$3`
4804
+ processed = replaceEntryScript(
4805
+ processed,
4806
+ facadeModuleId,
4807
+ fileName,
4808
+ config,
4809
+ config.environments.client?.html ?? "index.html",
4810
+ "/"
4524
4811
  );
4525
4812
  }
4526
4813
  return processed;
@@ -4664,10 +4951,12 @@ async function createServer(inlineConfig = {}) {
4664
4951
  const app = connect();
4665
4952
  const httpServer = http.createServer(app);
4666
4953
  const ws = createWebSocketServer(httpServer);
4667
- const clientEnv = new NastiEnvironment("client", configWithPlugins, {
4954
+ const pluginApi = getPluginApi(config);
4955
+ const clientEnv = new NastiEnvironment("client", config, {
4668
4956
  hot: createWsHotChannel(ws),
4669
4957
  mode: "dev",
4670
- plugins: allPlugins
4958
+ plugins: allPlugins,
4959
+ pluginApi
4671
4960
  });
4672
4961
  await clientEnv.init();
4673
4962
  const environments = { client: clientEnv };
@@ -4675,11 +4964,15 @@ async function createServer(inlineConfig = {}) {
4675
4964
  if (name === "client") continue;
4676
4965
  const consumer = config.environments[name].consumer;
4677
4966
  const envPlugins = resolvePluginList(config, config.plugins, { consumer });
4678
- environments[name] = new NastiEnvironment(name, { ...config, plugins: envPlugins }, {
4967
+ environments[name] = new NastiEnvironment(name, config, {
4679
4968
  mode: "dev",
4680
- plugins: envPlugins
4969
+ plugins: envPlugins,
4970
+ pluginApi
4681
4971
  });
4682
4972
  }
4973
+ for (const [name, environment] of Object.entries(environments)) {
4974
+ if (name !== "client" && environment.options.driver) await environment.init();
4975
+ }
4683
4976
  let ssrRunner = null;
4684
4977
  async function getSsrRunner() {
4685
4978
  if (ssrRunner) return ssrRunner;
@@ -4704,14 +4997,6 @@ async function createServer(inlineConfig = {}) {
4704
4997
  });
4705
4998
  app.use(bundledServer.middleware);
4706
4999
  }
4707
- app.use(transformMiddleware({
4708
- config: configWithPlugins,
4709
- pluginContainer,
4710
- moduleGraph
4711
- }));
4712
- const publicDir = path15.resolve(config.root, "public");
4713
- app.use(sirv(publicDir, { dev: true, etag: true }));
4714
- app.use(sirv(config.root, { dev: true, etag: true }));
4715
5000
  const ignoredSegments = /* @__PURE__ */ new Set(["node_modules", ".git", ".nasti"]);
4716
5001
  const outDirAbs = path15.resolve(config.root, config.build.outDir);
4717
5002
  const watcher = watch(config.root, {
@@ -4728,13 +5013,72 @@ async function createServer(inlineConfig = {}) {
4728
5013
  ignoreInitial: true
4729
5014
  });
4730
5015
  let server;
5016
+ const environmentServices = {};
5017
+ let environmentDriversStarted = false;
5018
+ const logCloseError = (target, error) => {
5019
+ const normalized = error instanceof Error ? error : new Error(String(error));
5020
+ logger.error(`[nasti] failed to close ${target}`, { error: normalized });
5021
+ };
5022
+ const startEnvironmentDrivers = async () => {
5023
+ if (environmentDriversStarted) return;
5024
+ environmentDriversStarted = true;
5025
+ const started = [];
5026
+ const attempted = [];
5027
+ try {
5028
+ for (const [name, environment] of Object.entries(environments)) {
5029
+ if (!environment.driver?.serve) continue;
5030
+ attempted.push(environment);
5031
+ const result = await environment.driver.serve({
5032
+ ...environment.getDriverContext(),
5033
+ server
5034
+ });
5035
+ started.push({ name, environment, service: result ?? {} });
5036
+ }
5037
+ for (const { name, service } of started) {
5038
+ environmentServices[name] = service;
5039
+ if (service.middleware) app.use(service.middleware);
5040
+ }
5041
+ } catch (error) {
5042
+ environmentDriversStarted = false;
5043
+ for (const { name } of started) {
5044
+ delete environmentServices[name];
5045
+ }
5046
+ for (const environment of attempted.reverse()) {
5047
+ try {
5048
+ await environment.driver?.close?.(environment.getDriverContext());
5049
+ } catch (closeError) {
5050
+ logCloseError(`environment driver "${environment.driver.name}"`, closeError);
5051
+ }
5052
+ }
5053
+ throw error;
5054
+ }
5055
+ };
5056
+ const notifyEnvironmentDrivers = (file, event) => {
5057
+ for (const environment of Object.values(environments)) {
5058
+ if (!environment.driver?.watchChange) continue;
5059
+ void Promise.resolve(
5060
+ environment.driver.watchChange(file, event, environment.getDriverContext())
5061
+ ).catch((error) => {
5062
+ logger.error(
5063
+ `[nasti] environment driver "${environment.driver.name}" watchChange failed`,
5064
+ { error }
5065
+ );
5066
+ });
5067
+ }
5068
+ };
4731
5069
  watcher.on("change", (file) => {
4732
5070
  ssrRunner?.invalidateFile(file);
4733
5071
  handleFileChange(file, server);
5072
+ notifyEnvironmentDrivers(file, "change");
4734
5073
  });
4735
5074
  watcher.on("add", (file) => {
4736
5075
  ssrRunner?.invalidateFile(file);
4737
5076
  handleFileChange(file, server);
5077
+ notifyEnvironmentDrivers(file, "add");
5078
+ });
5079
+ watcher.on("unlink", (file) => {
5080
+ ssrRunner?.invalidateFile(file);
5081
+ notifyEnvironmentDrivers(file, "unlink");
4738
5082
  });
4739
5083
  server = {
4740
5084
  config: configWithPlugins,
@@ -4743,10 +5087,12 @@ async function createServer(inlineConfig = {}) {
4743
5087
  watcher,
4744
5088
  ws,
4745
5089
  environments,
5090
+ environmentServices,
4746
5091
  async listen(port) {
4747
5092
  const finalPort = port ?? config.server.port;
4748
5093
  const host = config.server.host === true ? "0.0.0.0" : config.server.host;
4749
5094
  await pluginContainer.buildStart();
5095
+ await startEnvironmentDrivers();
4750
5096
  return new Promise((resolve, reject) => {
4751
5097
  let currentPort = finalPort;
4752
5098
  const onListening = () => {
@@ -4754,15 +5100,20 @@ async function createServer(inlineConfig = {}) {
4754
5100
  config.server.port = actualPort;
4755
5101
  const localUrl = `http://localhost:${actualPort}/`;
4756
5102
  const networkUrl = host === "0.0.0.0" ? `http://${getNetworkAddress()}:${actualPort}/` : null;
5103
+ const driverLocalUrls = Object.values(environmentServices).flatMap((service) => service.localUrls ?? []);
5104
+ const driverNetworkUrls = Object.values(environmentServices).flatMap((service) => service.networkUrls ?? []);
4757
5105
  logger.clearScreen("info");
4758
5106
  const readyIn = Math.ceil(performance.now() - startTime);
4759
5107
  logger.info(
4760
5108
  `
4761
- ${pc9.cyan(pc9.bold("NASTI"))} ${pc9.cyan(`v${"2.1.0"}`)} ${pc9.dim("ready in")} ${pc9.bold(readyIn)} ${pc9.dim("ms")}
5109
+ ${pc9.cyan(pc9.bold("NASTI"))} ${pc9.cyan(`v${"2.3.1"}`)} ${pc9.dim("ready in")} ${pc9.bold(readyIn)} ${pc9.dim("ms")}
4762
5110
  `
4763
5111
  );
4764
5112
  printServerUrls(
4765
- { local: [localUrl], network: networkUrl ? [networkUrl] : [] },
5113
+ {
5114
+ local: [localUrl, ...driverLocalUrls],
5115
+ network: [...networkUrl ? [networkUrl] : [], ...driverNetworkUrls]
5116
+ },
4766
5117
  logger.info
4767
5118
  );
4768
5119
  logger.info("");
@@ -4792,11 +5143,62 @@ async function createServer(inlineConfig = {}) {
4792
5143
  async close() {
4793
5144
  await pluginContainer.buildEnd();
4794
5145
  await bundledServer?.close();
4795
- watcher.close();
5146
+ let environmentCloseFailed = false;
5147
+ let firstEnvironmentCloseError;
5148
+ for (const environment of Object.values(environments).reverse()) {
5149
+ try {
5150
+ await environment.close();
5151
+ } catch (error) {
5152
+ if (!environmentCloseFailed) {
5153
+ environmentCloseFailed = true;
5154
+ firstEnvironmentCloseError = error;
5155
+ }
5156
+ logCloseError(`environment "${environment.name}"`, error);
5157
+ }
5158
+ }
5159
+ await watcher.close();
4796
5160
  ws.close();
4797
5161
  httpServer.close();
5162
+ if (environmentCloseFailed) {
5163
+ throw firstEnvironmentCloseError;
5164
+ }
4798
5165
  }
4799
5166
  };
5167
+ try {
5168
+ await startEnvironmentDrivers();
5169
+ } catch (error) {
5170
+ if (bundledServer) {
5171
+ try {
5172
+ await bundledServer.close();
5173
+ } catch (closeError) {
5174
+ logCloseError("bundled dev server after driver startup failure", closeError);
5175
+ }
5176
+ }
5177
+ try {
5178
+ await watcher.close();
5179
+ } catch (closeError) {
5180
+ logCloseError("file watcher after driver startup failure", closeError);
5181
+ }
5182
+ try {
5183
+ ws.close();
5184
+ } catch (closeError) {
5185
+ logCloseError("WebSocket server after driver startup failure", closeError);
5186
+ }
5187
+ try {
5188
+ httpServer.close();
5189
+ } catch (closeError) {
5190
+ logCloseError("HTTP server after driver startup failure", closeError);
5191
+ }
5192
+ throw error;
5193
+ }
5194
+ app.use(transformMiddleware({
5195
+ config: configWithPlugins,
5196
+ pluginContainer,
5197
+ moduleGraph
5198
+ }));
5199
+ const publicDir = path15.resolve(config.root, "public");
5200
+ app.use(sirv(publicDir, { dev: true, etag: true }));
5201
+ app.use(sirv(config.root, { dev: true, etag: true }));
4800
5202
  const postMiddlewares = [];
4801
5203
  for (const plugin of allPlugins) {
4802
5204
  if (plugin.configureServer) {
@@ -4831,6 +5233,7 @@ var init_server = __esm({
4831
5233
  init_middleware();
4832
5234
  init_hmr();
4833
5235
  init_builtins();
5236
+ init_plugin_api();
4834
5237
  }
4835
5238
  });
4836
5239
 
@@ -4886,7 +5289,7 @@ async function buildElectron(inlineConfig = {}) {
4886
5289
  const config = await resolveConfig({ ...inlineConfig, target: "electron" }, "build");
4887
5290
  const startTime = performance.now();
4888
5291
  assertElectronVersion(config);
4889
- console.log(pc5.cyan("\n\u26A1 nasti build (electron)") + pc5.dim(` v${"2.1.0"}`));
5292
+ console.log(pc5.cyan("\n\u26A1 nasti build (electron)") + pc5.dim(` v${"2.3.1"}`));
4890
5293
  console.log(pc5.dim(` root: ${config.root}`));
4891
5294
  console.log(pc5.dim(` mode: ${config.mode}`));
4892
5295
  console.log(pc5.dim(` target: electron (\u2265 ${config.electron.minVersion})`));
@@ -4897,15 +5300,13 @@ async function buildElectron(inlineConfig = {}) {
4897
5300
  fs7.mkdirSync(outDir, { recursive: true });
4898
5301
  const rendererOutDir = path10.join(outDir, "renderer");
4899
5302
  const { build: build2 } = await Promise.resolve().then(() => (init_build(), build_exports));
4900
- await build2({
4901
- ...inlineConfig,
4902
- target: "web",
5303
+ await build2(createElectronRendererConfig(config, inlineConfig, {
4903
5304
  build: {
4904
5305
  ...inlineConfig.build,
4905
5306
  outDir: rendererOutDir,
4906
5307
  emptyOutDir: false
4907
5308
  }
4908
- });
5309
+ }));
4909
5310
  const mainEntry = path10.resolve(config.root, config.electron.main);
4910
5311
  if (!fs7.existsSync(mainEntry)) {
4911
5312
  throw new Error(
@@ -4959,7 +5360,8 @@ async function bundleNode(config, entry, opts) {
4959
5360
  const result = transformCode(id, code, {
4960
5361
  sourcemap: !!config.build.sourcemap,
4961
5362
  jsxRuntime: "automatic",
4962
- jsxImportSource: config.framework === "vue" ? "vue" : "react"
5363
+ jsxImportSource: config.framework === "vue" ? "vue" : "react",
5364
+ target: config.electron.nodeTarget
4963
5365
  });
4964
5366
  return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
4965
5367
  }
@@ -4970,7 +5372,11 @@ async function bundleNode(config, entry, opts) {
4970
5372
  ...restInputOptions,
4971
5373
  input: entry,
4972
5374
  platform: "node",
4973
- transform: { ...userTransform, define: mergedDefine },
5375
+ transform: {
5376
+ ...userTransform,
5377
+ target: config.electron.nodeTarget,
5378
+ define: mergedDefine
5379
+ },
4974
5380
  plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)]
4975
5381
  });
4976
5382
  fs7.mkdirSync(path10.dirname(opts.outFile), { recursive: true });
@@ -4987,6 +5393,25 @@ async function bundleNode(config, entry, opts) {
4987
5393
  console.log(pc5.dim(` \u2713 ${opts.label} \u2192 ${path10.relative(config.root, opts.outFile)}`));
4988
5394
  return opts.outFile;
4989
5395
  }
5396
+ function createElectronRendererConfig(config, inlineConfig = {}, overrides = {}) {
5397
+ const inlineClient = inlineConfig.environments?.client ?? {};
5398
+ return {
5399
+ ...inlineConfig,
5400
+ ...overrides,
5401
+ root: config.root,
5402
+ mode: config.mode,
5403
+ target: "web",
5404
+ framework: config.framework,
5405
+ base: config.base === "/" ? "./" : config.base,
5406
+ environments: {
5407
+ ...inlineConfig.environments ?? {},
5408
+ client: {
5409
+ ...inlineClient,
5410
+ html: config.electron.renderer
5411
+ }
5412
+ }
5413
+ };
5414
+ }
4990
5415
  function outFileName(outDir, base, format) {
4991
5416
  const ext = format === "cjs" ? ".cjs" : ".mjs";
4992
5417
  return path10.join(outDir, base + ext);
@@ -5037,11 +5462,15 @@ async function startElectronDev(inlineConfig = {}) {
5037
5462
  const { noSpawn, ...rest } = inlineConfig;
5038
5463
  const config = await resolveConfig({ ...rest, target: "electron" }, "serve");
5039
5464
  warnElectronVersion(config);
5040
- console.log(pc10.cyan("\n\u26A1 nasti electron dev") + pc10.dim(` v${"2.1.0"}`));
5465
+ console.log(pc10.cyan("\n\u26A1 nasti electron dev") + pc10.dim(` v${"2.3.1"}`));
5041
5466
  const { createServer: createServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
5042
- const server = await createServer2({ ...rest, target: "electron" });
5467
+ const server = await createServer2({
5468
+ ...rest,
5469
+ target: "electron",
5470
+ framework: config.framework
5471
+ });
5043
5472
  await server.listen();
5044
- const devUrl = `http://localhost:${server.config.server.port}/`;
5473
+ const devUrl = `http://localhost:${server.config.server.port}` + electronRendererDevPath(config.electron.renderer);
5045
5474
  console.log(pc10.dim(` renderer: ${devUrl}`));
5046
5475
  const stageDir = path16.resolve(config.root, ".nasti");
5047
5476
  fs11.mkdirSync(stageDir, { recursive: true });
@@ -5163,14 +5592,18 @@ async function compileNode(config, entry, opts) {
5163
5592
  const result = transformCode(id, code, {
5164
5593
  sourcemap: true,
5165
5594
  jsxRuntime: "automatic",
5166
- jsxImportSource: config.framework === "vue" ? "vue" : "react"
5595
+ jsxImportSource: config.framework === "vue" ? "vue" : "react",
5596
+ target: config.electron.nodeTarget
5167
5597
  });
5168
5598
  return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
5169
5599
  }
5170
5600
  };
5171
5601
  const bundle2 = await rolldown3({
5172
5602
  input: entry,
5173
- transform: { define: envDefine },
5603
+ transform: {
5604
+ target: config.electron.nodeTarget,
5605
+ define: envDefine
5606
+ },
5174
5607
  platform: "node",
5175
5608
  plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)]
5176
5609
  });
@@ -5186,6 +5619,10 @@ async function compileNode(config, entry, opts) {
5186
5619
  });
5187
5620
  await bundle2.close();
5188
5621
  }
5622
+ function electronRendererDevPath(renderer) {
5623
+ const normalized = renderer.split(path16.sep).join("/").replace(/^\.?\//, "");
5624
+ return normalized === "index.html" ? "/" : `/${normalized}`;
5625
+ }
5189
5626
  function resolveElectronBinary(config) {
5190
5627
  if (config.electron.electronPath && fs11.existsSync(config.electron.electronPath)) {
5191
5628
  return config.electron.electronPath;
@@ -5442,12 +5879,15 @@ export {
5442
5879
  buildElectron,
5443
5880
  buildEnvDefine,
5444
5881
  createDebugger,
5882
+ createElectronRendererConfig,
5445
5883
  createLogger,
5446
5884
  createNoopHotChannel,
5447
5885
  createServer,
5448
5886
  createWsHotChannel,
5449
5887
  defineConfig,
5888
+ detectFramework,
5450
5889
  electronPlugin,
5890
+ electronRendererDevPath,
5451
5891
  loadEnv,
5452
5892
  monacoEditorPlugin,
5453
5893
  printServerUrls,