@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/cli.cjs CHANGED
@@ -226,6 +226,92 @@ var init_defaults = __esm({
226
226
  }
227
227
  });
228
228
 
229
+ // src/core/plugin-api.ts
230
+ function orderPlugins(plugins) {
231
+ 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);
232
+ const indexesByName = /* @__PURE__ */ new Map();
233
+ baseline.forEach((plugin, index2) => {
234
+ const indexes = indexesByName.get(plugin.name) ?? [];
235
+ indexes.push(index2);
236
+ indexesByName.set(plugin.name, indexes);
237
+ });
238
+ const edges = baseline.map(() => /* @__PURE__ */ new Set());
239
+ const indegree = baseline.map(() => 0);
240
+ const addEdge = (from, to) => {
241
+ if (from === to || edges[from].has(to)) return;
242
+ edges[from].add(to);
243
+ indegree[to]++;
244
+ };
245
+ baseline.forEach((plugin, current) => {
246
+ for (const dependency of plugin.pre ?? []) {
247
+ for (const before of indexesByName.get(dependency) ?? []) addEdge(before, current);
248
+ }
249
+ for (const dependency of plugin.post ?? []) {
250
+ for (const after of indexesByName.get(dependency) ?? []) addEdge(current, after);
251
+ }
252
+ });
253
+ const ready = indegree.map((degree, index2) => ({ degree, index: index2 })).filter(({ degree }) => degree === 0).map(({ index: index2 }) => index2);
254
+ const ordered = [];
255
+ while (ready.length > 0) {
256
+ ready.sort((a, b) => a - b);
257
+ const current = ready.shift();
258
+ ordered.push(baseline[current]);
259
+ for (const next of edges[current]) {
260
+ indegree[next]--;
261
+ if (indegree[next] === 0) ready.push(next);
262
+ }
263
+ }
264
+ if (ordered.length !== baseline.length) {
265
+ const cyclic = baseline.filter((_, index2) => indegree[index2] > 0).map((plugin) => plugin.name);
266
+ throw new Error(
267
+ `[nasti] circular plugin setup dependency: ${[...new Set(cyclic)].join(", ")}`
268
+ );
269
+ }
270
+ return ordered;
271
+ }
272
+ async function setupPluginApi(config, plugins) {
273
+ const exposed = /* @__PURE__ */ new Map();
274
+ const api = {
275
+ config,
276
+ logger: config.logger,
277
+ expose(key, value) {
278
+ if (exposed.has(key) && exposed.get(key) !== value) {
279
+ throw new Error(`[nasti] plugin API key already exposed: ${String(key)}`);
280
+ }
281
+ exposed.set(key, value);
282
+ },
283
+ useExposed(key) {
284
+ return exposed.get(key);
285
+ }
286
+ };
287
+ apiByConfig.set(config, api);
288
+ for (const plugin of plugins) {
289
+ await plugin.setup?.(api);
290
+ }
291
+ return api;
292
+ }
293
+ function getPluginApi(config) {
294
+ const api = apiByConfig.get(config);
295
+ if (!api) {
296
+ throw new Error(
297
+ "[nasti] internal: plugin API requested before setup; getPluginApi requires the original ResolvedConfig reference registered by setupPluginApi, not a shallow copy"
298
+ );
299
+ }
300
+ return api;
301
+ }
302
+ function enforceRank(plugin) {
303
+ if (plugin.enforce === "pre") return 0;
304
+ if (plugin.enforce === "post") return 2;
305
+ return 1;
306
+ }
307
+ var apiByConfig;
308
+ var init_plugin_api = __esm({
309
+ "src/core/plugin-api.ts"() {
310
+ "use strict";
311
+ apiByConfig = /* @__PURE__ */ new WeakMap();
312
+ }
313
+ });
314
+
229
315
  // src/config/index.ts
230
316
  function loadTsconfigPaths(root) {
231
317
  const tsconfigPath = import_node_path.default.resolve(root, "tsconfig.json");
@@ -260,6 +346,43 @@ async function loadConfigFromFile(root) {
260
346
  }
261
347
  return {};
262
348
  }
349
+ function detectFramework(root) {
350
+ const sourceRoot = import_node_path.default.resolve(root, "src");
351
+ if (containsVueFile(sourceRoot)) return "vue";
352
+ const packagePath = import_node_path.default.resolve(root, "package.json");
353
+ if (import_node_fs.default.existsSync(packagePath)) {
354
+ try {
355
+ const pkg = JSON.parse(import_node_fs.default.readFileSync(packagePath, "utf-8"));
356
+ const dependencies = {
357
+ ...pkg.dependencies ?? {},
358
+ ...pkg.devDependencies ?? {},
359
+ ...pkg.peerDependencies ?? {},
360
+ ...pkg.optionalDependencies ?? {}
361
+ };
362
+ const hasVue = "vue" in dependencies || "@vue/runtime-dom" in dependencies;
363
+ const hasReact = "react" in dependencies || "react-dom" in dependencies;
364
+ if (hasVue && !hasReact) return "vue";
365
+ if (hasReact) return "react";
366
+ if (hasVue) return "vue";
367
+ } catch {
368
+ }
369
+ }
370
+ return "react";
371
+ }
372
+ function containsVueFile(dir, depth = 0) {
373
+ if (depth > 5 || !import_node_fs.default.existsSync(dir)) return false;
374
+ try {
375
+ for (const entry of import_node_fs.default.readdirSync(dir, { withFileTypes: true })) {
376
+ if (entry.isFile() && entry.name.endsWith(".vue")) return true;
377
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && containsVueFile(import_node_path.default.join(dir, entry.name), depth + 1)) {
378
+ return true;
379
+ }
380
+ }
381
+ } catch {
382
+ return false;
383
+ }
384
+ return false;
385
+ }
263
386
  async function loadTsConfig(filePath) {
264
387
  const { transformSync: transformSync2 } = await import("oxc-transform");
265
388
  const code = import_node_fs.default.readFileSync(filePath, "utf-8");
@@ -306,7 +429,7 @@ async function resolveConfig(inlineConfig = {}, command) {
306
429
  base: merged.base ?? defaults.base,
307
430
  mode,
308
431
  target: merged.target ?? defaults.target,
309
- framework: merged.framework ?? defaults.framework,
432
+ framework: (merged.framework ?? defaults.framework) === "auto" ? detectFramework(root) : merged.framework,
310
433
  command,
311
434
  resolve: {
312
435
  // tsconfig paths 优先级最低:tsconfig < defaults < user config
@@ -353,7 +476,12 @@ async function resolveConfig(inlineConfig = {}, command) {
353
476
  if (envOptions.build) Object.assign(resolved.build, envOptions.build);
354
477
  resolved.environments.client = {
355
478
  consumer,
356
- entry: [],
479
+ entry: normalizeEnvironmentEntries(envOptions.entry, root),
480
+ html: import_node_path.default.resolve(
481
+ root,
482
+ envOptions.html ?? (resolved.target === "electron" ? resolved.electron.renderer : "index.html")
483
+ ),
484
+ driver: envOptions.driver,
357
485
  // 同引用 —— 精确镜像(assertClientEnvironmentMirror 校验)
358
486
  resolve: resolved.resolve,
359
487
  build: resolved.build
@@ -362,7 +490,9 @@ async function resolveConfig(inlineConfig = {}, command) {
362
490
  }
363
491
  resolved.environments[name] = {
364
492
  consumer,
365
- entry: (Array.isArray(envOptions.entry) ? envOptions.entry : envOptions.entry ? [envOptions.entry] : []).map((e) => import_node_path.default.resolve(root, e)),
493
+ entry: normalizeEnvironmentEntries(envOptions.entry, root),
494
+ html: envOptions.consumer === "client" && envOptions.html ? import_node_path.default.resolve(root, envOptions.html) : void 0,
495
+ driver: envOptions.driver,
366
496
  resolve: {
367
497
  alias: { ...resolved.resolve.alias, ...envOptions.resolve?.alias },
368
498
  extensions: envOptions.resolve?.extensions ?? [...resolved.resolve.extensions],
@@ -381,12 +511,13 @@ async function resolveConfig(inlineConfig = {}, command) {
381
511
  };
382
512
  }
383
513
  assertClientEnvironmentMirror(resolved);
384
- const filteredPlugins = rawPlugins.filter((p) => {
514
+ const filteredPlugins = orderPlugins(rawPlugins.filter((p) => {
385
515
  if (!p.apply) return true;
386
516
  if (typeof p.apply === "function") return p.apply(resolved, env);
387
517
  return p.apply === command;
388
- });
518
+ }));
389
519
  resolved.plugins = filteredPlugins;
520
+ await setupPluginApi(resolved, filteredPlugins);
390
521
  if (resolved.target === "electron") {
391
522
  const autoExternal = detectNativeDeps(root);
392
523
  if (autoExternal.length > 0) {
@@ -402,6 +533,10 @@ async function resolveConfig(inlineConfig = {}, command) {
402
533
  }
403
534
  return resolved;
404
535
  }
536
+ function normalizeEnvironmentEntries(entry, root) {
537
+ const entries = Array.isArray(entry) ? entry : entry ? [entry] : [];
538
+ return entries.map((item) => import_node_path.default.resolve(root, item));
539
+ }
405
540
  function detectNativeDeps(root) {
406
541
  const result = /* @__PURE__ */ new Set();
407
542
  const pkgJsonPath = import_node_path.default.resolve(root, "package.json");
@@ -522,6 +657,7 @@ var init_config = __esm({
522
657
  import_node_fs = __toESM(require("fs"), 1);
523
658
  init_defaults();
524
659
  init_logger();
660
+ init_plugin_api();
525
661
  CONFIG_FILES = [
526
662
  "nasti.config.ts",
527
663
  "nasti.config.js",
@@ -532,21 +668,11 @@ var init_config = __esm({
532
668
  });
533
669
 
534
670
  // src/core/plugin-container.ts
535
- function sortPlugins(plugins) {
536
- const pre = [];
537
- const normal = [];
538
- const post = [];
539
- for (const plugin of plugins) {
540
- if (plugin.enforce === "pre") pre.push(plugin);
541
- else if (plugin.enforce === "post") post.push(plugin);
542
- else normal.push(plugin);
543
- }
544
- return [...pre, ...normal, ...post];
545
- }
546
671
  var PluginContainer;
547
672
  var init_plugin_container = __esm({
548
673
  "src/core/plugin-container.ts"() {
549
674
  "use strict";
675
+ init_plugin_api();
550
676
  PluginContainer = class {
551
677
  plugins;
552
678
  config;
@@ -557,7 +683,7 @@ var init_plugin_container = __esm({
557
683
  constructor(config, environment) {
558
684
  this.config = config;
559
685
  this.environment = environment;
560
- this.plugins = sortPlugins(config.plugins);
686
+ this.plugins = orderPlugins(config.plugins);
561
687
  this.ctx = this.createContext();
562
688
  }
563
689
  createContext() {
@@ -896,6 +1022,7 @@ var init_environment = __esm({
896
1022
  init_module_graph();
897
1023
  init_hot_channel();
898
1024
  init_debug();
1025
+ init_plugin_api();
899
1026
  debug = createDebugger("nasti:environment");
900
1027
  NastiEnvironment = class {
901
1028
  name;
@@ -904,6 +1031,7 @@ var init_environment = __esm({
904
1031
  config;
905
1032
  options;
906
1033
  hot;
1034
+ driver;
907
1035
  /** applyToEnvironment 过滤后的插件(init() 后可用) */
908
1036
  plugins = [];
909
1037
  /** per-env 插件容器(init() 后可用;dev 管线使用) */
@@ -911,6 +1039,7 @@ var init_environment = __esm({
911
1039
  /** per-env 模块图(dev 管线使用) */
912
1040
  moduleGraph;
913
1041
  candidatePlugins;
1042
+ pluginApi;
914
1043
  initialized = false;
915
1044
  constructor(name, config, init = {}) {
916
1045
  const options = config.environments[name];
@@ -927,6 +1056,7 @@ var init_environment = __esm({
927
1056
  this.hot = init.hot ?? createNoopHotChannel();
928
1057
  this.moduleGraph = new ModuleGraph();
929
1058
  this.candidatePlugins = init.plugins ?? config.plugins;
1059
+ this.pluginApi = init.pluginApi ?? getPluginApi(config);
930
1060
  }
931
1061
  /** 过滤插件并建 per-env PluginContainer */
932
1062
  async init() {
@@ -937,10 +1067,41 @@ var init_environment = __esm({
937
1067
  { ...this.config, plugins: this.plugins },
938
1068
  this
939
1069
  );
1070
+ if (this.options.driver) {
1071
+ const claimed = [];
1072
+ for (const plugin of this.plugins) {
1073
+ const driver = await plugin.createEnvironmentDriver?.(this, this.pluginApi);
1074
+ if (driver) claimed.push({ plugin, driver });
1075
+ }
1076
+ if (claimed.length === 0) {
1077
+ throw new Error(
1078
+ `[nasti] environment "${this.name}" requested driver "${this.options.driver}", but no plugin provided it`
1079
+ );
1080
+ }
1081
+ if (claimed.length > 1) {
1082
+ throw new Error(
1083
+ `[nasti] environment "${this.name}" was claimed by multiple drivers: ` + claimed.map(({ plugin, driver }) => `${plugin.name} (${driver.name})`).join(", ")
1084
+ );
1085
+ }
1086
+ this.driver = claimed[0].driver;
1087
+ debug?.(`env "${this.name}" uses driver "${this.driver.name}"`);
1088
+ }
940
1089
  debug?.(`env "${this.name}" initialized (${this.plugins.length} plugins)`);
941
1090
  }
1091
+ getDriverContext() {
1092
+ return {
1093
+ environment: this,
1094
+ config: this.config,
1095
+ api: this.pluginApi,
1096
+ logger: this.config.logger
1097
+ };
1098
+ }
942
1099
  async close() {
943
- await this.hot.close?.();
1100
+ try {
1101
+ await this.driver?.close?.(this.getDriverContext());
1102
+ } finally {
1103
+ await this.hot.close?.();
1104
+ }
944
1105
  }
945
1106
  };
946
1107
  }
@@ -1005,7 +1166,8 @@ function transformCode(filename, code, options = {}) {
1005
1166
  importSource: options.jsxImportSource ?? "react",
1006
1167
  refresh: options.reactRefresh ?? false
1007
1168
  } : void 0,
1008
- sourcemap: options.sourcemap ?? true
1169
+ sourcemap: options.sourcemap ?? true,
1170
+ target: options.target
1009
1171
  });
1010
1172
  if (result.errors && result.errors.length > 0) {
1011
1173
  const msg = result.errors.map((e) => e.message ?? String(e)).join("\n");
@@ -1036,7 +1198,7 @@ function htmlPlugin(config) {
1036
1198
  transformIndexHtml(html) {
1037
1199
  const tags = [];
1038
1200
  if (config.command === "serve") {
1039
- const isReactLike = config.framework === "react" || config.framework === "auto";
1201
+ const isReactLike = config.framework === "react";
1040
1202
  if (isReactLike) {
1041
1203
  tags.push({
1042
1204
  tag: "script",
@@ -1090,8 +1252,8 @@ function serializeTag(tag) {
1090
1252
  }
1091
1253
  return ` <${tag.tag}${attrs}>${children}</${tag.tag}>`;
1092
1254
  }
1093
- async function readHtmlFile(root) {
1094
- const htmlPath = import_node_path2.default.resolve(root, "index.html");
1255
+ async function readHtmlFile(root, htmlFile = "index.html") {
1256
+ const htmlPath = import_node_path2.default.isAbsolute(htmlFile) ? htmlFile : import_node_path2.default.resolve(root, htmlFile);
1095
1257
  if (!import_node_fs2.default.existsSync(htmlPath)) return null;
1096
1258
  return import_node_fs2.default.readFileSync(htmlPath, "utf-8");
1097
1259
  }
@@ -1288,7 +1450,10 @@ function transformMiddleware(ctx) {
1288
1450
  return;
1289
1451
  }
1290
1452
  if (url === "/" || url.endsWith(".html")) {
1291
- const html = await readHtmlFile(ctx.config.root);
1453
+ const html = await readHtmlFile(
1454
+ ctx.config.root,
1455
+ ctx.config.environments.client?.html
1456
+ );
1292
1457
  if (html) {
1293
1458
  let processedHtml = html;
1294
1459
  for (const plugin of ctx.config.plugins) {
@@ -3923,6 +4088,7 @@ var build_exports = {};
3923
4088
  __export(build_exports, {
3924
4089
  build: () => build,
3925
4090
  getRolldownOptions: () => getRolldownOptions,
4091
+ replaceEntryScript: () => replaceEntryScript,
3926
4092
  resolveClientEntries: () => resolveClientEntries,
3927
4093
  toRolldownPlugins: () => toRolldownPlugins
3928
4094
  });
@@ -4001,13 +4167,20 @@ function toRolldownPlugins(plugins) {
4001
4167
  }));
4002
4168
  }
4003
4169
  function resolveClientEntries(config, html) {
4170
+ const configuredEntries = config.environments.client?.entry ?? [];
4171
+ if (configuredEntries.length > 0) return configuredEntries;
4004
4172
  const entryPoints = [];
4173
+ const htmlFile = config.environments.client?.html;
4174
+ const htmlDir = htmlFile ? import_node_path12.default.dirname(htmlFile) : config.root;
4005
4175
  if (html) {
4006
4176
  const scriptMatches = html.matchAll(/<script[^>]+src=["']([^"']+)["'][^>]*>/gi);
4007
4177
  for (const match of scriptMatches) {
4008
4178
  const src = match[1];
4009
4179
  if (src && !src.startsWith("http")) {
4010
- entryPoints.push(import_node_path12.default.resolve(config.root, src.replace(/^\//, "")));
4180
+ const cleanSrc = src.split(/[?#]/, 1)[0];
4181
+ entryPoints.push(
4182
+ cleanSrc.startsWith("/") ? import_node_path12.default.resolve(config.root, cleanSrc.replace(/^\//, "")) : import_node_path12.default.resolve(htmlDir, cleanSrc)
4183
+ );
4011
4184
  }
4012
4185
  }
4013
4186
  }
@@ -4043,21 +4216,55 @@ async function build(inlineConfig = {}) {
4043
4216
  const startTime = performance.now();
4044
4217
  logger.info(
4045
4218
  import_picocolors6.default.cyan(`
4046
- nasti v${"2.2.0"} `) + import_picocolors6.default.green(`building for ${config.mode}...`)
4219
+ nasti v${"2.3.1"} `) + import_picocolors6.default.green(`building for ${config.mode}...`)
4047
4220
  );
4048
4221
  debug5?.(`root: ${config.root}`);
4049
4222
  const buildableNames = Object.keys(config.environments).filter(
4050
- (name) => name === "client" || config.environments[name].entry.length > 0
4223
+ (name) => name === "client" || config.environments[name].entry.length > 0 || !!config.environments[name].driver
4051
4224
  );
4052
4225
  buildableNames.sort((a, b) => a === "client" ? -1 : b === "client" ? 1 : 0);
4053
4226
  const environments = {};
4227
+ const environmentResults = {};
4228
+ const initializedEnvironments = [];
4054
4229
  let clientOutput = [];
4055
- for (const name of buildableNames) {
4056
- const output = name === "client" ? await buildClientEnvironment(config) : await buildServerEnvironment(config, name);
4057
- environments[name] = output;
4058
- if (name === "client") clientOutput = output;
4059
- if (buildableNames.length > 1) {
4060
- debug5?.(`environment "${name}" built (${output.length} files)`);
4230
+ let buildFailed = false;
4231
+ try {
4232
+ for (const name of buildableNames) {
4233
+ const built = name === "client" ? await buildClientEnvironment(config) : await buildServerEnvironment(config, name);
4234
+ initializedEnvironments.push(built.environment);
4235
+ environments[name] = built.result.output;
4236
+ environmentResults[name] = built.result;
4237
+ if (name === "client") clientOutput = built.result.output;
4238
+ if (buildableNames.length > 1) {
4239
+ debug5?.(`environment "${name}" built (${built.result.output.length} files)`);
4240
+ }
4241
+ }
4242
+ const pluginApi = getPluginApi(config);
4243
+ for (const plugin of config.plugins) {
4244
+ await plugin.afterBuildApp?.(environmentResults, pluginApi);
4245
+ }
4246
+ } catch (error) {
4247
+ buildFailed = true;
4248
+ throw error;
4249
+ } finally {
4250
+ let closeFailed = false;
4251
+ let firstCloseError;
4252
+ for (const environment of [...initializedEnvironments].reverse()) {
4253
+ try {
4254
+ await environment.close();
4255
+ } catch (error) {
4256
+ if (!closeFailed) {
4257
+ closeFailed = true;
4258
+ firstCloseError = error;
4259
+ }
4260
+ const closeError = error instanceof Error ? error : new Error(String(error));
4261
+ logger.error(`[nasti] failed to close environment "${environment.name}"`, {
4262
+ error: closeError
4263
+ });
4264
+ }
4265
+ }
4266
+ if (closeFailed && !buildFailed) {
4267
+ throw firstCloseError;
4061
4268
  }
4062
4269
  }
4063
4270
  const elapsed = ((performance.now() - startTime) / 1e3).toFixed(2);
@@ -4070,83 +4277,130 @@ nasti v${"2.2.0"} `) + import_picocolors6.default.green(`building for ${config.m
4070
4277
  const envSuffix = buildableNames.length > 1 ? ` (${buildableNames.join(" + ")})` : "";
4071
4278
  logger.info(import_picocolors6.default.green(`\u2713 built in ${elapsed}s`) + import_picocolors6.default.dim(envSuffix));
4072
4279
  logger.info(import_picocolors6.default.dim(` ${fileCount} files, ${displaySize(totalSize)} total \u2192 ${config.build.outDir}/`));
4073
- return { output: clientOutput, environments };
4280
+ return { output: clientOutput, environments, environmentResults };
4074
4281
  }
4075
4282
  async function buildClientEnvironment(config) {
4076
4283
  const logger = config.logger;
4077
4284
  const outDir = import_node_path12.default.resolve(config.root, config.build.outDir);
4078
- if (config.build.emptyOutDir && import_node_fs9.default.existsSync(outDir)) {
4079
- import_node_fs9.default.rmSync(outDir, { recursive: true, force: true });
4080
- }
4081
- import_node_fs9.default.mkdirSync(outDir, { recursive: true });
4082
- const html = await readHtmlFile(config.root);
4083
- const entryPoints = resolveClientEntries(config, html);
4084
- if (entryPoints.length === 0) {
4085
- throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
4086
- }
4087
4285
  const cssEngine = createCssEngine();
4088
4286
  const pluginList = resolvePluginList(config, config.plugins, { cssEngine });
4089
- const clientEnv = new NastiEnvironment("client", { ...config, plugins: pluginList }, {
4287
+ const clientEnv = new NastiEnvironment("client", config, {
4090
4288
  mode: "build",
4091
- plugins: pluginList
4289
+ plugins: pluginList,
4290
+ pluginApi: getPluginApi(config)
4092
4291
  });
4093
4292
  await clientEnv.init();
4094
- const allPlugins = clientEnv.plugins;
4095
- const nativeReporter = config.logLevel === "silent" ? null : await tryNativeReporterPlugin(config, logger);
4096
- const rolldownPlugins = [
4097
- createOxcTransformPlugin(config, clientEnv),
4098
- ...toRolldownPlugins(allPlugins),
4099
- ...nativeReporter ? [nativeReporter] : []
4100
- ];
4101
- const { inputOptions, outputOptions } = getRolldownOptions(clientEnv, entryPoints, rolldownPlugins);
4102
- const bundle2 = await (0, import_rolldown.rolldown)(inputOptions);
4103
- const { output } = await bundle2.write(outputOptions);
4104
- await bundle2.close();
4105
- if (html) {
4106
- let processedHtml = html;
4107
- const htmlPlugins = [...allPlugins.filter((p) => p.transformIndexHtml), htmlPlugin(config)];
4108
- for (const p of htmlPlugins) {
4109
- const result = await p.transformIndexHtml(processedHtml);
4110
- if (typeof result === "string") {
4111
- processedHtml = result;
4112
- } else if (result && "html" in result) {
4113
- processedHtml = processHtml(result.html, result.tags);
4114
- } else if (Array.isArray(result)) {
4115
- processedHtml = processHtml(processedHtml, result);
4116
- }
4117
- }
4118
- processedHtml = injectCssLinks(processedHtml, cssEngine, config);
4119
- for (const chunk of output) {
4120
- if (chunk.type === "chunk" && chunk.isEntry && chunk.facadeModuleId) {
4121
- const originalEntry = import_node_path12.default.relative(config.root, chunk.facadeModuleId);
4122
- processedHtml = processedHtml.replace(
4123
- new RegExp(`(src=["'])/?(${escapeRegExp(originalEntry)})(["'])`, "g"),
4124
- `$1${config.base}${chunk.fileName}$3`
4293
+ try {
4294
+ if (clientEnv.driver) {
4295
+ if (!clientEnv.driver.build) {
4296
+ throw new Error(
4297
+ `[nasti] environment "client" driver "${clientEnv.driver.name}" does not implement build()`
4125
4298
  );
4126
4299
  }
4300
+ const result = await clientEnv.driver.build(clientEnv.getDriverContext());
4301
+ return { environment: clientEnv, result };
4127
4302
  }
4128
- import_node_fs9.default.writeFileSync(import_node_path12.default.resolve(outDir, "index.html"), processedHtml);
4129
- }
4130
- if (!nativeReporter && config.logLevel !== "silent") {
4131
- reportBuildOutput(output, config, logger);
4303
+ if (config.build.emptyOutDir && import_node_fs9.default.existsSync(outDir)) {
4304
+ import_node_fs9.default.rmSync(outDir, { recursive: true, force: true });
4305
+ }
4306
+ import_node_fs9.default.mkdirSync(outDir, { recursive: true });
4307
+ const htmlFile = config.environments.client.html ?? import_node_path12.default.resolve(config.root, "index.html");
4308
+ const html = await readHtmlFile(config.root, htmlFile);
4309
+ const entryPoints = resolveClientEntries(config, html);
4310
+ if (entryPoints.length === 0) {
4311
+ throw new Error("No entry point found. Add a <script> tag to index.html or create src/main.ts");
4312
+ }
4313
+ const allPlugins = clientEnv.plugins;
4314
+ const nativeReporter = config.logLevel === "silent" ? null : await tryNativeReporterPlugin(config, logger);
4315
+ const rolldownPlugins = [
4316
+ createOxcTransformPlugin(config, clientEnv),
4317
+ ...toRolldownPlugins(allPlugins),
4318
+ ...nativeReporter ? [nativeReporter] : []
4319
+ ];
4320
+ const { inputOptions, outputOptions } = getRolldownOptions(
4321
+ clientEnv,
4322
+ entryPoints,
4323
+ rolldownPlugins
4324
+ );
4325
+ const bundle2 = await (0, import_rolldown.rolldown)(inputOptions);
4326
+ const { output } = await bundle2.write(outputOptions);
4327
+ await bundle2.close();
4328
+ if (html) {
4329
+ let processedHtml = html;
4330
+ const htmlPlugins = [...allPlugins.filter((p) => p.transformIndexHtml), htmlPlugin(config)];
4331
+ for (const p of htmlPlugins) {
4332
+ const result = await p.transformIndexHtml(processedHtml);
4333
+ if (typeof result === "string") {
4334
+ processedHtml = result;
4335
+ } else if (result && "html" in result) {
4336
+ processedHtml = processHtml(result.html, result.tags);
4337
+ } else if (Array.isArray(result)) {
4338
+ processedHtml = processHtml(processedHtml, result);
4339
+ }
4340
+ }
4341
+ processedHtml = injectCssLinks(processedHtml, cssEngine, config);
4342
+ for (const chunk of output) {
4343
+ if (chunk.type === "chunk" && chunk.isEntry && chunk.facadeModuleId) {
4344
+ processedHtml = replaceEntryScript(
4345
+ processedHtml,
4346
+ chunk.facadeModuleId,
4347
+ chunk.fileName,
4348
+ config,
4349
+ htmlFile,
4350
+ config.base
4351
+ );
4352
+ }
4353
+ }
4354
+ import_node_fs9.default.writeFileSync(import_node_path12.default.resolve(outDir, "index.html"), processedHtml);
4355
+ }
4356
+ if (!nativeReporter && config.logLevel !== "silent") {
4357
+ reportBuildOutput(output, config, logger);
4358
+ }
4359
+ warnLargeChunks(output, config, logger);
4360
+ return { environment: clientEnv, result: { output } };
4361
+ } catch (error) {
4362
+ try {
4363
+ await clientEnv.close();
4364
+ } catch (closeError) {
4365
+ const normalized = closeError instanceof Error ? closeError : new Error(String(closeError));
4366
+ logger.error("[nasti] failed to close client environment after build failure", {
4367
+ error: normalized
4368
+ });
4369
+ }
4370
+ throw error;
4132
4371
  }
4133
- warnLargeChunks(output, config, logger);
4134
- return output;
4135
4372
  }
4136
4373
  async function buildServerEnvironment(config, name) {
4137
4374
  const envOptions = config.environments[name];
4138
4375
  const logger = config.logger;
4376
+ const pluginList = resolvePluginList(config, config.plugins, { consumer: envOptions.consumer });
4377
+ const environment = new NastiEnvironment(name, config, {
4378
+ mode: "build",
4379
+ plugins: pluginList,
4380
+ pluginApi: getPluginApi(config)
4381
+ });
4382
+ await environment.init();
4383
+ if (environment.driver) {
4384
+ if (!environment.driver.build) {
4385
+ await environment.close();
4386
+ throw new Error(
4387
+ `[nasti] environment "${name}" driver "${environment.driver.name}" does not implement build()`
4388
+ );
4389
+ }
4390
+ try {
4391
+ const result = await environment.driver.build(environment.getDriverContext());
4392
+ return { environment, result };
4393
+ } catch (error) {
4394
+ await environment.close();
4395
+ throw error;
4396
+ }
4397
+ }
4139
4398
  for (const entry of envOptions.entry) {
4140
4399
  if (!import_node_fs9.default.existsSync(entry)) {
4400
+ await environment.close();
4141
4401
  throw new Error(`[nasti] environment "${name}" entry not found: ${entry}`);
4142
4402
  }
4143
4403
  }
4144
- const pluginList = resolvePluginList(config, config.plugins, { consumer: envOptions.consumer });
4145
- const environment = new NastiEnvironment(name, { ...config, plugins: pluginList }, {
4146
- mode: "build",
4147
- plugins: pluginList
4148
- });
4149
- await environment.init();
4150
4404
  const rolldownPlugins = [
4151
4405
  createOxcTransformPlugin(config, environment),
4152
4406
  ...toRolldownPlugins(environment.plugins)
@@ -4166,7 +4420,7 @@ async function buildServerEnvironment(config, name) {
4166
4420
  logger.info(
4167
4421
  import_picocolors6.default.dim(` [${name}] `) + output.map((o) => import_node_path12.default.join(envOptions.build.outDir, o.fileName)).join(import_picocolors6.default.dim(", "))
4168
4422
  );
4169
- return output;
4423
+ return { environment, result: { output } };
4170
4424
  }
4171
4425
  function injectCssLinks(html, cssEngine, config) {
4172
4426
  const cssLinkTags = [];
@@ -4192,6 +4446,25 @@ function injectCssLinks(html, cssEngine, config) {
4192
4446
  function escapeRegExp(string) {
4193
4447
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4194
4448
  }
4449
+ function replaceEntryScript(html, facadeModuleId, fileName, config, htmlFile, urlPrefix) {
4450
+ const rootRelative = import_node_path12.default.relative(config.root, facadeModuleId).split(import_node_path12.default.sep).join("/");
4451
+ const resolvedHtmlFile = import_node_path12.default.resolve(config.root, htmlFile);
4452
+ const htmlRelative = import_node_path12.default.relative(import_node_path12.default.dirname(resolvedHtmlFile), facadeModuleId).split(import_node_path12.default.sep).join("/");
4453
+ const candidates = /* @__PURE__ */ new Set([
4454
+ rootRelative,
4455
+ `/${rootRelative}`,
4456
+ htmlRelative,
4457
+ `./${htmlRelative}`
4458
+ ]);
4459
+ let processed = html;
4460
+ for (const candidate of candidates) {
4461
+ processed = processed.replace(
4462
+ new RegExp(`(src=["'])${escapeRegExp(candidate)}([?#][^"']*)?(["'])`, "g"),
4463
+ `$1${urlPrefix}${fileName}$3`
4464
+ );
4465
+ }
4466
+ return processed;
4467
+ }
4195
4468
  var import_node_path12, import_node_fs9, import_node_module5, import_rolldown, import_picocolors6, debug5, NODE_BUILTINS2;
4196
4469
  var init_build = __esm({
4197
4470
  "src/build/index.ts"() {
@@ -4209,6 +4482,7 @@ var init_build = __esm({
4209
4482
  init_env();
4210
4483
  init_reporter();
4211
4484
  init_debug();
4485
+ init_plugin_api();
4212
4486
  import_picocolors6 = __toESM(require("picocolors"), 1);
4213
4487
  debug5 = createDebugger("nasti:build");
4214
4488
  NODE_BUILTINS2 = /* @__PURE__ */ new Set([...import_node_module5.builtinModules, ...import_node_module5.builtinModules.map((m) => `node:${m}`)]);
@@ -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 = import_node_path13.default.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;
@@ -4671,10 +4948,12 @@ async function createServer(inlineConfig = {}) {
4671
4948
  const app = (0, import_connect.default)();
4672
4949
  const httpServer = import_node_http.default.createServer(app);
4673
4950
  const ws = createWebSocketServer(httpServer);
4674
- const clientEnv = new NastiEnvironment("client", configWithPlugins, {
4951
+ const pluginApi = getPluginApi(config);
4952
+ const clientEnv = new NastiEnvironment("client", config, {
4675
4953
  hot: createWsHotChannel(ws),
4676
4954
  mode: "dev",
4677
- plugins: allPlugins
4955
+ plugins: allPlugins,
4956
+ pluginApi
4678
4957
  });
4679
4958
  await clientEnv.init();
4680
4959
  const environments = { client: clientEnv };
@@ -4682,11 +4961,15 @@ async function createServer(inlineConfig = {}) {
4682
4961
  if (name === "client") continue;
4683
4962
  const consumer = config.environments[name].consumer;
4684
4963
  const envPlugins = resolvePluginList(config, config.plugins, { consumer });
4685
- environments[name] = new NastiEnvironment(name, { ...config, plugins: envPlugins }, {
4964
+ environments[name] = new NastiEnvironment(name, config, {
4686
4965
  mode: "dev",
4687
- plugins: envPlugins
4966
+ plugins: envPlugins,
4967
+ pluginApi
4688
4968
  });
4689
4969
  }
4970
+ for (const [name, environment] of Object.entries(environments)) {
4971
+ if (name !== "client" && environment.options.driver) await environment.init();
4972
+ }
4690
4973
  let ssrRunner = null;
4691
4974
  async function getSsrRunner() {
4692
4975
  if (ssrRunner) return ssrRunner;
@@ -4711,14 +4994,6 @@ async function createServer(inlineConfig = {}) {
4711
4994
  });
4712
4995
  app.use(bundledServer.middleware);
4713
4996
  }
4714
- app.use(transformMiddleware({
4715
- config: configWithPlugins,
4716
- pluginContainer,
4717
- moduleGraph
4718
- }));
4719
- const publicDir = import_node_path14.default.resolve(config.root, "public");
4720
- app.use((0, import_sirv.default)(publicDir, { dev: true, etag: true }));
4721
- app.use((0, import_sirv.default)(config.root, { dev: true, etag: true }));
4722
4997
  const ignoredSegments = /* @__PURE__ */ new Set(["node_modules", ".git", ".nasti"]);
4723
4998
  const outDirAbs = import_node_path14.default.resolve(config.root, config.build.outDir);
4724
4999
  const watcher = (0, import_chokidar.watch)(config.root, {
@@ -4735,13 +5010,72 @@ async function createServer(inlineConfig = {}) {
4735
5010
  ignoreInitial: true
4736
5011
  });
4737
5012
  let server;
5013
+ const environmentServices = {};
5014
+ let environmentDriversStarted = false;
5015
+ const logCloseError = (target, error) => {
5016
+ const normalized = error instanceof Error ? error : new Error(String(error));
5017
+ logger.error(`[nasti] failed to close ${target}`, { error: normalized });
5018
+ };
5019
+ const startEnvironmentDrivers = async () => {
5020
+ if (environmentDriversStarted) return;
5021
+ environmentDriversStarted = true;
5022
+ const started = [];
5023
+ const attempted = [];
5024
+ try {
5025
+ for (const [name, environment] of Object.entries(environments)) {
5026
+ if (!environment.driver?.serve) continue;
5027
+ attempted.push(environment);
5028
+ const result = await environment.driver.serve({
5029
+ ...environment.getDriverContext(),
5030
+ server
5031
+ });
5032
+ started.push({ name, environment, service: result ?? {} });
5033
+ }
5034
+ for (const { name, service } of started) {
5035
+ environmentServices[name] = service;
5036
+ if (service.middleware) app.use(service.middleware);
5037
+ }
5038
+ } catch (error) {
5039
+ environmentDriversStarted = false;
5040
+ for (const { name } of started) {
5041
+ delete environmentServices[name];
5042
+ }
5043
+ for (const environment of attempted.reverse()) {
5044
+ try {
5045
+ await environment.driver?.close?.(environment.getDriverContext());
5046
+ } catch (closeError) {
5047
+ logCloseError(`environment driver "${environment.driver.name}"`, closeError);
5048
+ }
5049
+ }
5050
+ throw error;
5051
+ }
5052
+ };
5053
+ const notifyEnvironmentDrivers = (file, event) => {
5054
+ for (const environment of Object.values(environments)) {
5055
+ if (!environment.driver?.watchChange) continue;
5056
+ void Promise.resolve(
5057
+ environment.driver.watchChange(file, event, environment.getDriverContext())
5058
+ ).catch((error) => {
5059
+ logger.error(
5060
+ `[nasti] environment driver "${environment.driver.name}" watchChange failed`,
5061
+ { error }
5062
+ );
5063
+ });
5064
+ }
5065
+ };
4738
5066
  watcher.on("change", (file) => {
4739
5067
  ssrRunner?.invalidateFile(file);
4740
5068
  handleFileChange(file, server);
5069
+ notifyEnvironmentDrivers(file, "change");
4741
5070
  });
4742
5071
  watcher.on("add", (file) => {
4743
5072
  ssrRunner?.invalidateFile(file);
4744
5073
  handleFileChange(file, server);
5074
+ notifyEnvironmentDrivers(file, "add");
5075
+ });
5076
+ watcher.on("unlink", (file) => {
5077
+ ssrRunner?.invalidateFile(file);
5078
+ notifyEnvironmentDrivers(file, "unlink");
4745
5079
  });
4746
5080
  server = {
4747
5081
  config: configWithPlugins,
@@ -4750,10 +5084,12 @@ async function createServer(inlineConfig = {}) {
4750
5084
  watcher,
4751
5085
  ws,
4752
5086
  environments,
5087
+ environmentServices,
4753
5088
  async listen(port) {
4754
5089
  const finalPort = port ?? config.server.port;
4755
5090
  const host = config.server.host === true ? "0.0.0.0" : config.server.host;
4756
5091
  await pluginContainer.buildStart();
5092
+ await startEnvironmentDrivers();
4757
5093
  return new Promise((resolve, reject) => {
4758
5094
  let currentPort = finalPort;
4759
5095
  const onListening = () => {
@@ -4761,15 +5097,20 @@ async function createServer(inlineConfig = {}) {
4761
5097
  config.server.port = actualPort;
4762
5098
  const localUrl = `http://localhost:${actualPort}/`;
4763
5099
  const networkUrl = host === "0.0.0.0" ? `http://${getNetworkAddress()}:${actualPort}/` : null;
5100
+ const driverLocalUrls = Object.values(environmentServices).flatMap((service) => service.localUrls ?? []);
5101
+ const driverNetworkUrls = Object.values(environmentServices).flatMap((service) => service.networkUrls ?? []);
4764
5102
  logger.clearScreen("info");
4765
5103
  const readyIn = Math.ceil(performance.now() - startTime);
4766
5104
  logger.info(
4767
5105
  `
4768
- ${import_picocolors8.default.cyan(import_picocolors8.default.bold("NASTI"))} ${import_picocolors8.default.cyan(`v${"2.2.0"}`)} ${import_picocolors8.default.dim("ready in")} ${import_picocolors8.default.bold(readyIn)} ${import_picocolors8.default.dim("ms")}
5106
+ ${import_picocolors8.default.cyan(import_picocolors8.default.bold("NASTI"))} ${import_picocolors8.default.cyan(`v${"2.3.1"}`)} ${import_picocolors8.default.dim("ready in")} ${import_picocolors8.default.bold(readyIn)} ${import_picocolors8.default.dim("ms")}
4769
5107
  `
4770
5108
  );
4771
5109
  printServerUrls(
4772
- { local: [localUrl], network: networkUrl ? [networkUrl] : [] },
5110
+ {
5111
+ local: [localUrl, ...driverLocalUrls],
5112
+ network: [...networkUrl ? [networkUrl] : [], ...driverNetworkUrls]
5113
+ },
4773
5114
  logger.info
4774
5115
  );
4775
5116
  logger.info("");
@@ -4799,11 +5140,62 @@ async function createServer(inlineConfig = {}) {
4799
5140
  async close() {
4800
5141
  await pluginContainer.buildEnd();
4801
5142
  await bundledServer?.close();
4802
- watcher.close();
5143
+ let environmentCloseFailed = false;
5144
+ let firstEnvironmentCloseError;
5145
+ for (const environment of Object.values(environments).reverse()) {
5146
+ try {
5147
+ await environment.close();
5148
+ } catch (error) {
5149
+ if (!environmentCloseFailed) {
5150
+ environmentCloseFailed = true;
5151
+ firstEnvironmentCloseError = error;
5152
+ }
5153
+ logCloseError(`environment "${environment.name}"`, error);
5154
+ }
5155
+ }
5156
+ await watcher.close();
4803
5157
  ws.close();
4804
5158
  httpServer.close();
5159
+ if (environmentCloseFailed) {
5160
+ throw firstEnvironmentCloseError;
5161
+ }
4805
5162
  }
4806
5163
  };
5164
+ try {
5165
+ await startEnvironmentDrivers();
5166
+ } catch (error) {
5167
+ if (bundledServer) {
5168
+ try {
5169
+ await bundledServer.close();
5170
+ } catch (closeError) {
5171
+ logCloseError("bundled dev server after driver startup failure", closeError);
5172
+ }
5173
+ }
5174
+ try {
5175
+ await watcher.close();
5176
+ } catch (closeError) {
5177
+ logCloseError("file watcher after driver startup failure", closeError);
5178
+ }
5179
+ try {
5180
+ ws.close();
5181
+ } catch (closeError) {
5182
+ logCloseError("WebSocket server after driver startup failure", closeError);
5183
+ }
5184
+ try {
5185
+ httpServer.close();
5186
+ } catch (closeError) {
5187
+ logCloseError("HTTP server after driver startup failure", closeError);
5188
+ }
5189
+ throw error;
5190
+ }
5191
+ app.use(transformMiddleware({
5192
+ config: configWithPlugins,
5193
+ pluginContainer,
5194
+ moduleGraph
5195
+ }));
5196
+ const publicDir = import_node_path14.default.resolve(config.root, "public");
5197
+ app.use((0, import_sirv.default)(publicDir, { dev: true, etag: true }));
5198
+ app.use((0, import_sirv.default)(config.root, { dev: true, etag: true }));
4807
5199
  const postMiddlewares = [];
4808
5200
  for (const plugin of allPlugins) {
4809
5201
  if (plugin.configureServer) {
@@ -4846,6 +5238,7 @@ var init_server = __esm({
4846
5238
  init_middleware();
4847
5239
  init_hmr();
4848
5240
  init_builtins();
5241
+ init_plugin_api();
4849
5242
  }
4850
5243
  });
4851
5244
 
@@ -4892,6 +5285,7 @@ var init_electron = __esm({
4892
5285
  var electron_exports = {};
4893
5286
  __export(electron_exports, {
4894
5287
  buildElectron: () => buildElectron,
5288
+ createElectronRendererConfig: () => createElectronRendererConfig,
4895
5289
  detectInstalledElectron: () => detectInstalledElectron,
4896
5290
  normalizePreload: () => normalizePreload
4897
5291
  });
@@ -4899,7 +5293,7 @@ async function buildElectron(inlineConfig = {}) {
4899
5293
  const config = await resolveConfig({ ...inlineConfig, target: "electron" }, "build");
4900
5294
  const startTime = performance.now();
4901
5295
  assertElectronVersion(config);
4902
- console.log(import_picocolors9.default.cyan("\n\u26A1 nasti build (electron)") + import_picocolors9.default.dim(` v${"2.2.0"}`));
5296
+ console.log(import_picocolors9.default.cyan("\n\u26A1 nasti build (electron)") + import_picocolors9.default.dim(` v${"2.3.1"}`));
4903
5297
  console.log(import_picocolors9.default.dim(` root: ${config.root}`));
4904
5298
  console.log(import_picocolors9.default.dim(` mode: ${config.mode}`));
4905
5299
  console.log(import_picocolors9.default.dim(` target: electron (\u2265 ${config.electron.minVersion})`));
@@ -4910,15 +5304,13 @@ async function buildElectron(inlineConfig = {}) {
4910
5304
  import_node_fs10.default.mkdirSync(outDir, { recursive: true });
4911
5305
  const rendererOutDir = import_node_path15.default.join(outDir, "renderer");
4912
5306
  const { build: build2 } = await Promise.resolve().then(() => (init_build(), build_exports));
4913
- await build2({
4914
- ...inlineConfig,
4915
- target: "web",
5307
+ await build2(createElectronRendererConfig(config, inlineConfig, {
4916
5308
  build: {
4917
5309
  ...inlineConfig.build,
4918
5310
  outDir: rendererOutDir,
4919
5311
  emptyOutDir: false
4920
5312
  }
4921
- });
5313
+ }));
4922
5314
  const mainEntry = import_node_path15.default.resolve(config.root, config.electron.main);
4923
5315
  if (!import_node_fs10.default.existsSync(mainEntry)) {
4924
5316
  throw new Error(
@@ -4972,7 +5364,8 @@ async function bundleNode(config, entry, opts) {
4972
5364
  const result = transformCode(id, code, {
4973
5365
  sourcemap: !!config.build.sourcemap,
4974
5366
  jsxRuntime: "automatic",
4975
- jsxImportSource: config.framework === "vue" ? "vue" : "react"
5367
+ jsxImportSource: config.framework === "vue" ? "vue" : "react",
5368
+ target: config.electron.nodeTarget
4976
5369
  });
4977
5370
  return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
4978
5371
  }
@@ -4983,7 +5376,11 @@ async function bundleNode(config, entry, opts) {
4983
5376
  ...restInputOptions,
4984
5377
  input: entry,
4985
5378
  platform: "node",
4986
- transform: { ...userTransform, define: mergedDefine },
5379
+ transform: {
5380
+ ...userTransform,
5381
+ target: config.electron.nodeTarget,
5382
+ define: mergedDefine
5383
+ },
4987
5384
  plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)]
4988
5385
  });
4989
5386
  import_node_fs10.default.mkdirSync(import_node_path15.default.dirname(opts.outFile), { recursive: true });
@@ -5000,6 +5397,25 @@ async function bundleNode(config, entry, opts) {
5000
5397
  console.log(import_picocolors9.default.dim(` \u2713 ${opts.label} \u2192 ${import_node_path15.default.relative(config.root, opts.outFile)}`));
5001
5398
  return opts.outFile;
5002
5399
  }
5400
+ function createElectronRendererConfig(config, inlineConfig = {}, overrides = {}) {
5401
+ const inlineClient = inlineConfig.environments?.client ?? {};
5402
+ return {
5403
+ ...inlineConfig,
5404
+ ...overrides,
5405
+ root: config.root,
5406
+ mode: config.mode,
5407
+ target: "web",
5408
+ framework: config.framework,
5409
+ base: config.base === "/" ? "./" : config.base,
5410
+ environments: {
5411
+ ...inlineConfig.environments ?? {},
5412
+ client: {
5413
+ ...inlineClient,
5414
+ html: config.electron.renderer
5415
+ }
5416
+ }
5417
+ };
5418
+ }
5003
5419
  function outFileName(outDir, base, format) {
5004
5420
  const ext = format === "cjs" ? ".cjs" : ".mjs";
5005
5421
  return import_node_path15.default.join(outDir, base + ext);
@@ -5049,17 +5465,22 @@ var init_electron2 = __esm({
5049
5465
  // src/server/electron-dev.ts
5050
5466
  var electron_dev_exports = {};
5051
5467
  __export(electron_dev_exports, {
5468
+ electronRendererDevPath: () => electronRendererDevPath,
5052
5469
  startElectronDev: () => startElectronDev
5053
5470
  });
5054
5471
  async function startElectronDev(inlineConfig = {}) {
5055
5472
  const { noSpawn, ...rest } = inlineConfig;
5056
5473
  const config = await resolveConfig({ ...rest, target: "electron" }, "serve");
5057
5474
  warnElectronVersion(config);
5058
- console.log(import_picocolors10.default.cyan("\n\u26A1 nasti electron dev") + import_picocolors10.default.dim(` v${"2.2.0"}`));
5475
+ console.log(import_picocolors10.default.cyan("\n\u26A1 nasti electron dev") + import_picocolors10.default.dim(` v${"2.3.1"}`));
5059
5476
  const { createServer: createServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
5060
- const server = await createServer2({ ...rest, target: "electron" });
5477
+ const server = await createServer2({
5478
+ ...rest,
5479
+ target: "electron",
5480
+ framework: config.framework
5481
+ });
5061
5482
  await server.listen();
5062
- const devUrl = `http://localhost:${server.config.server.port}/`;
5483
+ const devUrl = `http://localhost:${server.config.server.port}` + electronRendererDevPath(config.electron.renderer);
5063
5484
  console.log(import_picocolors10.default.dim(` renderer: ${devUrl}`));
5064
5485
  const stageDir = import_node_path16.default.resolve(config.root, ".nasti");
5065
5486
  import_node_fs11.default.mkdirSync(stageDir, { recursive: true });
@@ -5181,14 +5602,18 @@ async function compileNode(config, entry, opts) {
5181
5602
  const result = transformCode(id, code, {
5182
5603
  sourcemap: true,
5183
5604
  jsxRuntime: "automatic",
5184
- jsxImportSource: config.framework === "vue" ? "vue" : "react"
5605
+ jsxImportSource: config.framework === "vue" ? "vue" : "react",
5606
+ target: config.electron.nodeTarget
5185
5607
  });
5186
5608
  return { code: result.code, map: result.map ? JSON.parse(result.map) : void 0 };
5187
5609
  }
5188
5610
  };
5189
5611
  const bundle2 = await (0, import_rolldown3.rolldown)({
5190
5612
  input: entry,
5191
- transform: { define: envDefine },
5613
+ transform: {
5614
+ target: config.electron.nodeTarget,
5615
+ define: envDefine
5616
+ },
5192
5617
  platform: "node",
5193
5618
  plugins: [oxcTransformPlugin, electronPlugin(config), resolvePlugin(config)]
5194
5619
  });
@@ -5204,6 +5629,10 @@ async function compileNode(config, entry, opts) {
5204
5629
  });
5205
5630
  await bundle2.close();
5206
5631
  }
5632
+ function electronRendererDevPath(renderer) {
5633
+ const normalized = renderer.split(import_node_path16.default.sep).join("/").replace(/^\.?\//, "");
5634
+ return normalized === "index.html" ? "/" : `/${normalized}`;
5635
+ }
5207
5636
  function resolveElectronBinary(config) {
5208
5637
  if (config.electron.electronPath && import_node_fs11.default.existsSync(config.electron.electronPath)) {
5209
5638
  return config.electron.electronPath;
@@ -5408,7 +5837,7 @@ cli.command("preview [root]", "Preview production build").option("--port <port>"
5408
5837
  const host = options.host === true ? "0.0.0.0" : options.host ?? "localhost";
5409
5838
  http2.createServer(app).listen(port, host, () => {
5410
5839
  logger.info(`
5411
- ${import_picocolors11.default.cyan(import_picocolors11.default.bold("NASTI"))} ${import_picocolors11.default.cyan(`v${"2.2.0"}`)} ${import_picocolors11.default.dim("preview")}
5840
+ ${import_picocolors11.default.cyan(import_picocolors11.default.bold("NASTI"))} ${import_picocolors11.default.cyan(`v${"2.3.1"}`)} ${import_picocolors11.default.dim("preview")}
5412
5841
  `);
5413
5842
  printServerUrls2(
5414
5843
  {
@@ -5425,6 +5854,6 @@ cli.command("preview [root]", "Preview production build").option("--port <port>"
5425
5854
  }
5426
5855
  });
5427
5856
  cli.help();
5428
- cli.version("2.2.0");
5857
+ cli.version("2.3.1");
5429
5858
  cli.parse();
5430
5859
  //# sourceMappingURL=cli.cjs.map