@nasti-toolchain/nasti 2.2.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.2.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.2.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) {
@@ -4237,7 +4511,7 @@ async function createBundledDevServer(opts) {
4237
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.`
4238
4512
  );
4239
4513
  }
4240
- const html = await readHtmlFile(config.root);
4514
+ const html = await readHtmlFile(config.root, config.environments.client?.html);
4241
4515
  const entryPoints = resolveClientEntries(config, html);
4242
4516
  if (entryPoints.length === 0) {
4243
4517
  throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
@@ -4443,7 +4717,7 @@ async function createBundledDevServer(opts) {
4443
4717
  return;
4444
4718
  }
4445
4719
  if (pathname === "/" || pathname.endsWith(".html")) {
4446
- const rawHtml = await readHtmlFile(config.root);
4720
+ const rawHtml = await readHtmlFile(config.root, config.environments.client?.html);
4447
4721
  if (rawHtml) {
4448
4722
  res.setHeader("Content-Type", "text/html");
4449
4723
  res.setHeader("Cache-Control", "no-store");
@@ -4527,10 +4801,13 @@ async function renderBundledIndexHtml(html, config, entryFileNames) {
4527
4801
  }
4528
4802
  }
4529
4803
  for (const [facadeModuleId, fileName] of entryFileNames) {
4530
- const originalEntry = path14.relative(config.root, facadeModuleId);
4531
- processed = processed.replace(
4532
- new RegExp(`(src=["'])/?(${originalEntry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})(["'])`, "g"),
4533
- `$1/${fileName}$3`
4804
+ processed = replaceEntryScript(
4805
+ processed,
4806
+ facadeModuleId,
4807
+ fileName,
4808
+ config,
4809
+ config.environments.client?.html ?? "index.html",
4810
+ "/"
4534
4811
  );
4535
4812
  }
4536
4813
  return processed;
@@ -4674,10 +4951,12 @@ async function createServer(inlineConfig = {}) {
4674
4951
  const app = connect();
4675
4952
  const httpServer = http.createServer(app);
4676
4953
  const ws = createWebSocketServer(httpServer);
4677
- const clientEnv = new NastiEnvironment("client", configWithPlugins, {
4954
+ const pluginApi = getPluginApi(config);
4955
+ const clientEnv = new NastiEnvironment("client", config, {
4678
4956
  hot: createWsHotChannel(ws),
4679
4957
  mode: "dev",
4680
- plugins: allPlugins
4958
+ plugins: allPlugins,
4959
+ pluginApi
4681
4960
  });
4682
4961
  await clientEnv.init();
4683
4962
  const environments = { client: clientEnv };
@@ -4685,11 +4964,15 @@ async function createServer(inlineConfig = {}) {
4685
4964
  if (name === "client") continue;
4686
4965
  const consumer = config.environments[name].consumer;
4687
4966
  const envPlugins = resolvePluginList(config, config.plugins, { consumer });
4688
- environments[name] = new NastiEnvironment(name, { ...config, plugins: envPlugins }, {
4967
+ environments[name] = new NastiEnvironment(name, config, {
4689
4968
  mode: "dev",
4690
- plugins: envPlugins
4969
+ plugins: envPlugins,
4970
+ pluginApi
4691
4971
  });
4692
4972
  }
4973
+ for (const [name, environment] of Object.entries(environments)) {
4974
+ if (name !== "client" && environment.options.driver) await environment.init();
4975
+ }
4693
4976
  let ssrRunner = null;
4694
4977
  async function getSsrRunner() {
4695
4978
  if (ssrRunner) return ssrRunner;
@@ -4714,14 +4997,6 @@ async function createServer(inlineConfig = {}) {
4714
4997
  });
4715
4998
  app.use(bundledServer.middleware);
4716
4999
  }
4717
- app.use(transformMiddleware({
4718
- config: configWithPlugins,
4719
- pluginContainer,
4720
- moduleGraph
4721
- }));
4722
- const publicDir = path15.resolve(config.root, "public");
4723
- app.use(sirv(publicDir, { dev: true, etag: true }));
4724
- app.use(sirv(config.root, { dev: true, etag: true }));
4725
5000
  const ignoredSegments = /* @__PURE__ */ new Set(["node_modules", ".git", ".nasti"]);
4726
5001
  const outDirAbs = path15.resolve(config.root, config.build.outDir);
4727
5002
  const watcher = watch(config.root, {
@@ -4738,13 +5013,72 @@ async function createServer(inlineConfig = {}) {
4738
5013
  ignoreInitial: true
4739
5014
  });
4740
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
+ };
4741
5069
  watcher.on("change", (file) => {
4742
5070
  ssrRunner?.invalidateFile(file);
4743
5071
  handleFileChange(file, server);
5072
+ notifyEnvironmentDrivers(file, "change");
4744
5073
  });
4745
5074
  watcher.on("add", (file) => {
4746
5075
  ssrRunner?.invalidateFile(file);
4747
5076
  handleFileChange(file, server);
5077
+ notifyEnvironmentDrivers(file, "add");
5078
+ });
5079
+ watcher.on("unlink", (file) => {
5080
+ ssrRunner?.invalidateFile(file);
5081
+ notifyEnvironmentDrivers(file, "unlink");
4748
5082
  });
4749
5083
  server = {
4750
5084
  config: configWithPlugins,
@@ -4753,10 +5087,12 @@ async function createServer(inlineConfig = {}) {
4753
5087
  watcher,
4754
5088
  ws,
4755
5089
  environments,
5090
+ environmentServices,
4756
5091
  async listen(port) {
4757
5092
  const finalPort = port ?? config.server.port;
4758
5093
  const host = config.server.host === true ? "0.0.0.0" : config.server.host;
4759
5094
  await pluginContainer.buildStart();
5095
+ await startEnvironmentDrivers();
4760
5096
  return new Promise((resolve, reject) => {
4761
5097
  let currentPort = finalPort;
4762
5098
  const onListening = () => {
@@ -4764,15 +5100,20 @@ async function createServer(inlineConfig = {}) {
4764
5100
  config.server.port = actualPort;
4765
5101
  const localUrl = `http://localhost:${actualPort}/`;
4766
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 ?? []);
4767
5105
  logger.clearScreen("info");
4768
5106
  const readyIn = Math.ceil(performance.now() - startTime);
4769
5107
  logger.info(
4770
5108
  `
4771
- ${pc9.cyan(pc9.bold("NASTI"))} ${pc9.cyan(`v${"2.2.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")}
4772
5110
  `
4773
5111
  );
4774
5112
  printServerUrls(
4775
- { local: [localUrl], network: networkUrl ? [networkUrl] : [] },
5113
+ {
5114
+ local: [localUrl, ...driverLocalUrls],
5115
+ network: [...networkUrl ? [networkUrl] : [], ...driverNetworkUrls]
5116
+ },
4776
5117
  logger.info
4777
5118
  );
4778
5119
  logger.info("");
@@ -4802,11 +5143,62 @@ async function createServer(inlineConfig = {}) {
4802
5143
  async close() {
4803
5144
  await pluginContainer.buildEnd();
4804
5145
  await bundledServer?.close();
4805
- 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();
4806
5160
  ws.close();
4807
5161
  httpServer.close();
5162
+ if (environmentCloseFailed) {
5163
+ throw firstEnvironmentCloseError;
5164
+ }
4808
5165
  }
4809
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 }));
4810
5202
  const postMiddlewares = [];
4811
5203
  for (const plugin of allPlugins) {
4812
5204
  if (plugin.configureServer) {
@@ -4841,6 +5233,7 @@ var init_server = __esm({
4841
5233
  init_middleware();
4842
5234
  init_hmr();
4843
5235
  init_builtins();
5236
+ init_plugin_api();
4844
5237
  }
4845
5238
  });
4846
5239
 
@@ -4896,7 +5289,7 @@ async function buildElectron(inlineConfig = {}) {
4896
5289
  const config = await resolveConfig({ ...inlineConfig, target: "electron" }, "build");
4897
5290
  const startTime = performance.now();
4898
5291
  assertElectronVersion(config);
4899
- console.log(pc5.cyan("\n\u26A1 nasti build (electron)") + pc5.dim(` v${"2.2.0"}`));
5292
+ console.log(pc5.cyan("\n\u26A1 nasti build (electron)") + pc5.dim(` v${"2.3.1"}`));
4900
5293
  console.log(pc5.dim(` root: ${config.root}`));
4901
5294
  console.log(pc5.dim(` mode: ${config.mode}`));
4902
5295
  console.log(pc5.dim(` target: electron (\u2265 ${config.electron.minVersion})`));
@@ -4907,15 +5300,13 @@ async function buildElectron(inlineConfig = {}) {
4907
5300
  fs7.mkdirSync(outDir, { recursive: true });
4908
5301
  const rendererOutDir = path10.join(outDir, "renderer");
4909
5302
  const { build: build2 } = await Promise.resolve().then(() => (init_build(), build_exports));
4910
- await build2({
4911
- ...inlineConfig,
4912
- target: "web",
5303
+ await build2(createElectronRendererConfig(config, inlineConfig, {
4913
5304
  build: {
4914
5305
  ...inlineConfig.build,
4915
5306
  outDir: rendererOutDir,
4916
5307
  emptyOutDir: false
4917
5308
  }
4918
- });
5309
+ }));
4919
5310
  const mainEntry = path10.resolve(config.root, config.electron.main);
4920
5311
  if (!fs7.existsSync(mainEntry)) {
4921
5312
  throw new Error(
@@ -4969,7 +5360,8 @@ async function bundleNode(config, entry, opts) {
4969
5360
  const result = transformCode(id, code, {
4970
5361
  sourcemap: !!config.build.sourcemap,
4971
5362
  jsxRuntime: "automatic",
4972
- jsxImportSource: config.framework === "vue" ? "vue" : "react"
5363
+ jsxImportSource: config.framework === "vue" ? "vue" : "react",
5364
+ target: config.electron.nodeTarget
4973
5365
  });
4974
5366
  return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
4975
5367
  }
@@ -4980,7 +5372,11 @@ async function bundleNode(config, entry, opts) {
4980
5372
  ...restInputOptions,
4981
5373
  input: entry,
4982
5374
  platform: "node",
4983
- transform: { ...userTransform, define: mergedDefine },
5375
+ transform: {
5376
+ ...userTransform,
5377
+ target: config.electron.nodeTarget,
5378
+ define: mergedDefine
5379
+ },
4984
5380
  plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)]
4985
5381
  });
4986
5382
  fs7.mkdirSync(path10.dirname(opts.outFile), { recursive: true });
@@ -4997,6 +5393,25 @@ async function bundleNode(config, entry, opts) {
4997
5393
  console.log(pc5.dim(` \u2713 ${opts.label} \u2192 ${path10.relative(config.root, opts.outFile)}`));
4998
5394
  return opts.outFile;
4999
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
+ }
5000
5415
  function outFileName(outDir, base, format) {
5001
5416
  const ext = format === "cjs" ? ".cjs" : ".mjs";
5002
5417
  return path10.join(outDir, base + ext);
@@ -5047,11 +5462,15 @@ async function startElectronDev(inlineConfig = {}) {
5047
5462
  const { noSpawn, ...rest } = inlineConfig;
5048
5463
  const config = await resolveConfig({ ...rest, target: "electron" }, "serve");
5049
5464
  warnElectronVersion(config);
5050
- console.log(pc10.cyan("\n\u26A1 nasti electron dev") + pc10.dim(` v${"2.2.0"}`));
5465
+ console.log(pc10.cyan("\n\u26A1 nasti electron dev") + pc10.dim(` v${"2.3.1"}`));
5051
5466
  const { createServer: createServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
5052
- const server = await createServer2({ ...rest, target: "electron" });
5467
+ const server = await createServer2({
5468
+ ...rest,
5469
+ target: "electron",
5470
+ framework: config.framework
5471
+ });
5053
5472
  await server.listen();
5054
- const devUrl = `http://localhost:${server.config.server.port}/`;
5473
+ const devUrl = `http://localhost:${server.config.server.port}` + electronRendererDevPath(config.electron.renderer);
5055
5474
  console.log(pc10.dim(` renderer: ${devUrl}`));
5056
5475
  const stageDir = path16.resolve(config.root, ".nasti");
5057
5476
  fs11.mkdirSync(stageDir, { recursive: true });
@@ -5173,14 +5592,18 @@ async function compileNode(config, entry, opts) {
5173
5592
  const result = transformCode(id, code, {
5174
5593
  sourcemap: true,
5175
5594
  jsxRuntime: "automatic",
5176
- jsxImportSource: config.framework === "vue" ? "vue" : "react"
5595
+ jsxImportSource: config.framework === "vue" ? "vue" : "react",
5596
+ target: config.electron.nodeTarget
5177
5597
  });
5178
5598
  return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
5179
5599
  }
5180
5600
  };
5181
5601
  const bundle2 = await rolldown3({
5182
5602
  input: entry,
5183
- transform: { define: envDefine },
5603
+ transform: {
5604
+ target: config.electron.nodeTarget,
5605
+ define: envDefine
5606
+ },
5184
5607
  platform: "node",
5185
5608
  plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)]
5186
5609
  });
@@ -5196,6 +5619,10 @@ async function compileNode(config, entry, opts) {
5196
5619
  });
5197
5620
  await bundle2.close();
5198
5621
  }
5622
+ function electronRendererDevPath(renderer) {
5623
+ const normalized = renderer.split(path16.sep).join("/").replace(/^\.?\//, "");
5624
+ return normalized === "index.html" ? "/" : `/${normalized}`;
5625
+ }
5199
5626
  function resolveElectronBinary(config) {
5200
5627
  if (config.electron.electronPath && fs11.existsSync(config.electron.electronPath)) {
5201
5628
  return config.electron.electronPath;
@@ -5452,12 +5879,15 @@ export {
5452
5879
  buildElectron,
5453
5880
  buildEnvDefine,
5454
5881
  createDebugger,
5882
+ createElectronRendererConfig,
5455
5883
  createLogger,
5456
5884
  createNoopHotChannel,
5457
5885
  createServer,
5458
5886
  createWsHotChannel,
5459
5887
  defineConfig,
5888
+ detectFramework,
5460
5889
  electronPlugin,
5890
+ electronRendererDevPath,
5461
5891
  loadEnv,
5462
5892
  monacoEditorPlugin,
5463
5893
  printServerUrls,