@cordy/electro-cli 1.2.6 → 1.2.8

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.
Files changed (2) hide show
  1. package/dist/index.mjs +298 -138
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -8,6 +8,7 @@ import { build, createLogger, createServer, mergeConfig, version } from "vite";
8
8
  import { existsSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { spawn } from "node:child_process";
11
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
11
12
 
12
13
  //#region ../../node_modules/.bun/cac@6.7.14/node_modules/cac/dist/index.mjs
13
14
  function toArr(any) {
@@ -490,7 +491,7 @@ const cac = (name = "") => new CAC(name);
490
491
 
491
492
  //#endregion
492
493
  //#region package.json
493
- var version$1 = "1.2.6";
494
+ var version$1 = "1.2.8";
494
495
 
495
496
  //#endregion
496
497
  //#region src/dev/logger.ts
@@ -752,7 +753,7 @@ function validateMergedNodeConfig(config, scope) {
752
753
  const output = rolldownOpts.output;
753
754
  if (output && typeof output === "object" && !Array.isArray(output)) {
754
755
  const fmt = output.format;
755
- if (fmt && fmt !== "es") throw new Error(`[electro] ${scope}: invalid output format "${fmt}". Electro is ESM-only (format: "es"). Remove the format override from your config.`);
756
+ if (fmt && fmt !== "es" && fmt !== "cjs") throw new Error(`[electro] ${scope}: invalid output format "${fmt}". Electro supports "es" and "cjs" for Node scopes. Update build.rolldownOptions.output.format in your config.`);
756
757
  }
757
758
  }
758
759
  if (config.ssr === false || config.build?.ssr === false) throw new Error(`[electro] ${scope}: SSR cannot be disabled in Node scopes. SSR mode is required for correct Node.js module resolution.`);
@@ -766,11 +767,6 @@ function enforceMergedNodeConfig(config, _scope) {
766
767
  const build = config.build;
767
768
  if (!build) return;
768
769
  if (build.target !== "esnext") build.target = "esnext";
769
- const rolldownOpts = build.rolldownOptions;
770
- if (rolldownOpts) {
771
- const output = rolldownOpts.output;
772
- if (output && output.format !== "es") output.format = "es";
773
- }
774
770
  if (!build.ssr) build.ssr = true;
775
771
  }
776
772
  /** Validate that installed Vite version is within the supported range. */
@@ -831,134 +827,43 @@ function escapeRegExp(str) {
831
827
  }
832
828
 
833
829
  //#endregion
834
- //#region src/dev/vite-node-config.ts
835
- function resolveSourcemap$1(mode) {
836
- if (!mode || mode === "linked" || mode === "external") return true;
837
- if (mode === "inline") return "inline";
838
- if (mode === "none") return false;
839
- return true;
840
- }
841
- function createNodeConfig(opts) {
842
- const resolveConditions = opts.scope === "preload" ? [
843
- "node",
844
- "import",
845
- "default"
846
- ] : ["node", "import"];
847
- const envPrefix = opts.scope === "main" ? ["MAIN_VITE_", "VITE_"] : ["PRELOAD_VITE_", "VITE_"];
848
- const config = {
849
- configFile: false,
850
- root: opts.root,
851
- plugins: opts.plugins ?? [],
852
- customLogger: opts.customLogger,
853
- envPrefix,
854
- define: {
855
- "process.env": "process.env",
856
- ...opts.define
857
- },
858
- build: {
859
- ssr: opts.entry,
860
- ssrEmitAssets: true,
861
- outDir: opts.outDir,
862
- emptyOutDir: true,
863
- rolldownOptions: {
864
- output: {
865
- format: "es",
866
- entryFileNames: "index.mjs"
867
- },
868
- external: opts.externals
869
- },
870
- target: "esnext",
871
- sourcemap: resolveSourcemap$1(opts.sourcemap),
872
- minify: false,
873
- modulePreload: false,
874
- watch: opts.watch ? {} : null,
875
- reportCompressedSize: !opts.watch
876
- },
877
- ssr: {
878
- target: "node",
879
- noExternal: ["@cordy/electro"]
880
- },
881
- resolve: { conditions: resolveConditions },
882
- logLevel: opts.logLevel ?? "warn",
883
- clearScreen: opts.clearScreen
884
- };
885
- if (opts.scope === "main") {
886
- const resourcesDir = resolve(opts.root, "resources");
887
- if (existsSync(resourcesDir)) config.publicDir = resourcesDir;
888
- }
889
- if (opts.userViteConfig) {
890
- const merged = mergeConfig(config, opts.userViteConfig);
891
- validateMergedNodeConfig(merged, opts.scope);
892
- enforceMergedNodeConfig(merged, opts.scope);
893
- return merged;
894
- }
895
- return config;
896
- }
897
-
898
- //#endregion
899
- //#region src/dev/vite-renderer-config.ts
900
- function resolveSourcemap(mode) {
901
- if (!mode || mode === "linked" || mode === "external") return true;
902
- if (mode === "inline") return "inline";
903
- if (mode === "none") return false;
904
- return true;
905
- }
906
- function createRendererConfig(opts) {
907
- const input = {};
908
- for (const view of opts.views) {
909
- const sourceDir = dirname(view.__source);
910
- input[view.name] = resolve(sourceDir, view.entry);
911
- }
912
- const isBuild = !!opts.outDir;
913
- const config = {
914
- configFile: false,
915
- root: opts.root,
916
- customLogger: opts.customLogger,
917
- envPrefix: ["RENDERER_VITE_", "VITE_"],
918
- ...!isBuild && { server: { strictPort: false } },
919
- ...isBuild && { base: "./" },
920
- build: {
921
- rolldownOptions: { input },
922
- ...isBuild && {
923
- outDir: opts.outDir,
924
- emptyOutDir: true,
925
- minify: opts.minify ?? true,
926
- sourcemap: resolveSourcemap(opts.sourcemap),
927
- reportCompressedSize: true,
928
- modulePreload: { polyfill: false }
929
- }
930
- },
931
- logLevel: opts.logLevel ?? "info",
932
- clearScreen: opts.clearScreen
933
- };
934
- if (opts.userViteConfigs?.length) {
935
- let merged = config;
936
- for (const userConfig of opts.userViteConfigs) merged = mergeConfig(merged, userConfig);
937
- merged.plugins = deduplicatePlugins(merged.plugins);
938
- return merged;
830
+ //#region src/dev/node-format.ts
831
+ /**
832
+ * Mirrors Node package behavior:
833
+ * - package.json "type": "module" => ESM output
834
+ * - otherwise => CJS output
835
+ */
836
+ async function resolveNodeOutputFormat(root) {
837
+ const pkgPath = resolve(root, "package.json");
838
+ try {
839
+ const raw = await readFile(pkgPath, "utf-8");
840
+ return JSON.parse(raw).type === "module" ? "es" : "cjs";
841
+ } catch {
842
+ return "cjs";
939
843
  }
940
- return config;
941
844
  }
845
+ const MAIN_ENTRY_CANDIDATES = [
846
+ "index.mjs",
847
+ "index.cjs",
848
+ "index.js"
849
+ ];
942
850
  /**
943
- * Deduplicate plugins by name keeps the first occurrence of each named plugin.
944
- * This allows multiple views to declare the same plugins (e.g. react()) without
945
- * causing duplicate injection errors when configs are merged.
851
+ * Find built main entry regardless of selected output format.
946
852
  */
947
- function deduplicatePlugins(plugins) {
948
- if (!plugins) return [];
949
- const seen = /* @__PURE__ */ new Set();
950
- const result = [];
951
- for (const plugin of plugins.flat(Infinity)) {
952
- const name = plugin?.name;
953
- if (!name) {
954
- result.push(plugin);
955
- continue;
956
- }
957
- if (seen.has(name)) continue;
958
- seen.add(name);
959
- result.push(plugin);
853
+ async function resolveMainEntryPath(mainOutDir) {
854
+ for (const candidate of MAIN_ENTRY_CANDIDATES) {
855
+ const fullPath = resolve(mainOutDir, candidate);
856
+ if (await pathExists(fullPath)) return fullPath;
857
+ }
858
+ throw new Error(`Main entry not found in ${mainOutDir}. Expected one of: ${MAIN_ENTRY_CANDIDATES.join(", ")}`);
859
+ }
860
+ async function pathExists(filePath) {
861
+ try {
862
+ await access(filePath);
863
+ return true;
864
+ } catch {
865
+ return false;
960
866
  }
961
- return result;
962
867
  }
963
868
 
964
869
  //#endregion
@@ -1973,6 +1878,206 @@ var MagicString = class MagicString {
1973
1878
  }
1974
1879
  };
1975
1880
 
1881
+ //#endregion
1882
+ //#region src/plugins/esm-shim.ts
1883
+ const CJSYNTAX_RE = /__filename|__dirname|require\(|require\.resolve\(/;
1884
+ const CJS_SHIM = `
1885
+ // -- CommonJS Shims --
1886
+ import __cjs_url__ from "node:url";
1887
+ import __cjs_path__ from "node:path";
1888
+ import __cjs_mod__ from "node:module";
1889
+ const __filename = __cjs_url__.fileURLToPath(import.meta.url);
1890
+ const __dirname = __cjs_path__.dirname(__filename);
1891
+ const require = __cjs_mod__.createRequire(import.meta.url);
1892
+ `;
1893
+ const ESM_STATIC_IMPORT_RE = /(?<=\s|^|;)import\s*([\s"']*(?<imports>[\p{L}\p{M}\w\t\n\r $*,/{}@.]+)from\s*)?["']\s*(?<specifier>(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][\s;]*/gmu;
1894
+ function findStaticImports(code) {
1895
+ const matches = [];
1896
+ for (const match of code.matchAll(ESM_STATIC_IMPORT_RE)) matches.push({ end: (match.index || 0) + match[0].length });
1897
+ return matches;
1898
+ }
1899
+ /**
1900
+ * Inject CommonJS shims into ESM output when bundled code contains
1901
+ * require/__dirname/__filename references.
1902
+ */
1903
+ function esmShimPlugin() {
1904
+ return {
1905
+ name: "electro:esm-shim",
1906
+ apply: "build",
1907
+ enforce: "post",
1908
+ renderChunk(code, _chunk, { format, sourcemap }) {
1909
+ if (format !== "es") return null;
1910
+ if (code.includes(CJS_SHIM) || !CJSYNTAX_RE.test(code)) return null;
1911
+ const lastImport = findStaticImports(code).pop();
1912
+ const indexToAppend = lastImport ? lastImport.end : 0;
1913
+ const s = new MagicString(code);
1914
+ s.appendRight(indexToAppend, CJS_SHIM);
1915
+ return {
1916
+ code: s.toString(),
1917
+ map: sourcemap ? s.generateMap({ hires: "boundary" }) : null
1918
+ };
1919
+ }
1920
+ };
1921
+ }
1922
+
1923
+ //#endregion
1924
+ //#region src/plugins/import-meta.ts
1925
+ /**
1926
+ * Rewrites import.meta.* expressions for CommonJS output format.
1927
+ */
1928
+ function importMetaPlugin() {
1929
+ return {
1930
+ name: "electro:import-meta",
1931
+ apply: "build",
1932
+ enforce: "pre",
1933
+ resolveImportMeta(property, { format }) {
1934
+ if (format !== "cjs") return null;
1935
+ if (property === "url") return `require("node:url").pathToFileURL(__filename).href`;
1936
+ if (property === "filename") return `__filename`;
1937
+ if (property === "dirname") return `__dirname`;
1938
+ return null;
1939
+ }
1940
+ };
1941
+ }
1942
+
1943
+ //#endregion
1944
+ //#region src/dev/vite-node-config.ts
1945
+ function resolveSourcemap$1(mode) {
1946
+ if (!mode || mode === "linked" || mode === "external") return true;
1947
+ if (mode === "inline") return "inline";
1948
+ if (mode === "none") return false;
1949
+ return true;
1950
+ }
1951
+ function createNodeConfig(opts) {
1952
+ const format = opts.format ?? "es";
1953
+ const moduleCondition = format === "cjs" ? "require" : "import";
1954
+ const resolveConditions = opts.scope === "preload" ? [
1955
+ "node",
1956
+ moduleCondition,
1957
+ "default"
1958
+ ] : ["node", moduleCondition];
1959
+ const envPrefix = opts.scope === "main" ? ["MAIN_VITE_", "VITE_"] : ["PRELOAD_VITE_", "VITE_"];
1960
+ const entryExt = format === "cjs" ? "cjs" : "mjs";
1961
+ const config = {
1962
+ configFile: false,
1963
+ root: opts.root,
1964
+ plugins: [
1965
+ importMetaPlugin(),
1966
+ ...opts.plugins ?? [],
1967
+ esmShimPlugin()
1968
+ ],
1969
+ customLogger: opts.customLogger,
1970
+ envPrefix,
1971
+ define: {
1972
+ "process.env": "process.env",
1973
+ ...opts.define
1974
+ },
1975
+ build: {
1976
+ ssr: opts.entry,
1977
+ ssrEmitAssets: true,
1978
+ outDir: opts.outDir,
1979
+ emptyOutDir: true,
1980
+ rolldownOptions: {
1981
+ output: {
1982
+ format,
1983
+ entryFileNames: `index.${entryExt}`
1984
+ },
1985
+ external: opts.externals
1986
+ },
1987
+ target: "esnext",
1988
+ sourcemap: resolveSourcemap$1(opts.sourcemap),
1989
+ minify: false,
1990
+ modulePreload: false,
1991
+ watch: opts.watch ? {} : null,
1992
+ reportCompressedSize: !opts.watch
1993
+ },
1994
+ ssr: {
1995
+ target: "node",
1996
+ noExternal: ["@cordy/electro"]
1997
+ },
1998
+ resolve: { conditions: resolveConditions },
1999
+ logLevel: opts.logLevel ?? "warn",
2000
+ clearScreen: opts.clearScreen
2001
+ };
2002
+ if (opts.scope === "main") {
2003
+ const resourcesDir = resolve(opts.root, "resources");
2004
+ if (existsSync(resourcesDir)) config.publicDir = resourcesDir;
2005
+ }
2006
+ if (opts.userViteConfig) {
2007
+ const merged = mergeConfig(config, opts.userViteConfig);
2008
+ validateMergedNodeConfig(merged, opts.scope);
2009
+ enforceMergedNodeConfig(merged, opts.scope);
2010
+ return merged;
2011
+ }
2012
+ return config;
2013
+ }
2014
+
2015
+ //#endregion
2016
+ //#region src/dev/vite-renderer-config.ts
2017
+ function resolveSourcemap(mode) {
2018
+ if (!mode || mode === "linked" || mode === "external") return true;
2019
+ if (mode === "inline") return "inline";
2020
+ if (mode === "none") return false;
2021
+ return true;
2022
+ }
2023
+ function createRendererConfig(opts) {
2024
+ const input = {};
2025
+ for (const view of opts.views) {
2026
+ const sourceDir = dirname(view.__source);
2027
+ input[view.name] = resolve(sourceDir, view.entry);
2028
+ }
2029
+ const isBuild = !!opts.outDir;
2030
+ const config = {
2031
+ configFile: false,
2032
+ root: opts.root,
2033
+ customLogger: opts.customLogger,
2034
+ envPrefix: ["RENDERER_VITE_", "VITE_"],
2035
+ ...!isBuild && { server: { strictPort: false } },
2036
+ ...isBuild && { base: "./" },
2037
+ build: {
2038
+ rolldownOptions: { input },
2039
+ ...isBuild && {
2040
+ outDir: opts.outDir,
2041
+ emptyOutDir: true,
2042
+ minify: opts.minify ?? true,
2043
+ sourcemap: resolveSourcemap(opts.sourcemap),
2044
+ reportCompressedSize: true,
2045
+ modulePreload: { polyfill: false }
2046
+ }
2047
+ },
2048
+ logLevel: opts.logLevel ?? "info",
2049
+ clearScreen: opts.clearScreen
2050
+ };
2051
+ if (opts.userViteConfigs?.length) {
2052
+ let merged = config;
2053
+ for (const userConfig of opts.userViteConfigs) merged = mergeConfig(merged, userConfig);
2054
+ merged.plugins = deduplicatePlugins(merged.plugins);
2055
+ return merged;
2056
+ }
2057
+ return config;
2058
+ }
2059
+ /**
2060
+ * Deduplicate plugins by name — keeps the first occurrence of each named plugin.
2061
+ * This allows multiple views to declare the same plugins (e.g. react()) without
2062
+ * causing duplicate injection errors when configs are merged.
2063
+ */
2064
+ function deduplicatePlugins(plugins) {
2065
+ if (!plugins) return [];
2066
+ const seen = /* @__PURE__ */ new Set();
2067
+ const result = [];
2068
+ for (const plugin of plugins.flat(Infinity)) {
2069
+ const name = plugin?.name;
2070
+ if (!name) {
2071
+ result.push(plugin);
2072
+ continue;
2073
+ }
2074
+ if (seen.has(name)) continue;
2075
+ seen.add(name);
2076
+ result.push(plugin);
2077
+ }
2078
+ return result;
2079
+ }
2080
+
1976
2081
  //#endregion
1977
2082
  //#region src/plugins/utils.ts
1978
2083
  /** Strip query and hash from a URL/path. */
@@ -2658,6 +2763,7 @@ async function build$1(options) {
2658
2763
  const root = loaded.root;
2659
2764
  const outDir = resolve(root, options.outDir);
2660
2765
  const codegenDir = resolve(root, ".electro");
2766
+ const nodeFormat = await resolveNodeOutputFormat(root);
2661
2767
  const views = config.views ?? [];
2662
2768
  const rendererViews = views.filter((v) => v.entry);
2663
2769
  const srcDir = resolve(root, "src");
@@ -2705,7 +2811,8 @@ async function build$1(options) {
2705
2811
  externals,
2706
2812
  sourcemap: options.sourcemap,
2707
2813
  logger,
2708
- bytecode: options.bytecode
2814
+ bytecode: options.bytecode,
2815
+ format: nodeFormat
2709
2816
  });
2710
2817
  } catch (err) {
2711
2818
  stepFail("main", err instanceof Error ? err.message : String(err));
@@ -2721,7 +2828,8 @@ async function build$1(options) {
2721
2828
  externals,
2722
2829
  sourcemap: options.sourcemap,
2723
2830
  logger,
2724
- bytecode: options.bytecode
2831
+ bytecode: options.bytecode,
2832
+ format: nodeFormat
2725
2833
  });
2726
2834
  } catch (err) {
2727
2835
  stepFail("preload", err instanceof Error ? err.message : String(err));
@@ -2773,6 +2881,7 @@ async function buildMain(args) {
2773
2881
  sourcemap: args.sourcemap,
2774
2882
  customLogger: args.logger,
2775
2883
  logLevel: "info",
2884
+ format: args.format,
2776
2885
  define: { __ELECTRO_VIEW_REGISTRY__: JSON.stringify(viewRegistry) }
2777
2886
  }));
2778
2887
  }
@@ -2798,7 +2907,8 @@ async function buildPreload(args) {
2798
2907
  plugins: preloadPlugins,
2799
2908
  sourcemap: args.sourcemap,
2800
2909
  customLogger: args.logger,
2801
- logLevel: "info"
2910
+ logLevel: "info",
2911
+ format: args.format
2802
2912
  });
2803
2913
  if (Object.keys(input).length > 1) {
2804
2914
  const subBuildConfig = createNodeConfig({
@@ -2815,7 +2925,8 @@ async function buildPreload(args) {
2815
2925
  ],
2816
2926
  sourcemap: args.sourcemap,
2817
2927
  customLogger: args.logger,
2818
- logLevel: "info"
2928
+ logLevel: "info",
2929
+ format: args.format
2819
2930
  });
2820
2931
  baseConfig.plugins.push(isolateEntriesPlugin(subBuildConfig));
2821
2932
  }
@@ -2860,6 +2971,8 @@ async function flattenRendererOutput(rendererDir, views, root) {
2860
2971
  //#region src/dev/dev-server.ts
2861
2972
  const MAIN_RESTART_DEBOUNCE_MS = 80;
2862
2973
  const CONFIG_DEBOUNCE_MS = 300;
2974
+ const MAIN_ENTRY_WAIT_TIMEOUT_MS = 1e4;
2975
+ const MAIN_ENTRY_WAIT_INTERVAL_MS = 50;
2863
2976
  var DevServer = class {
2864
2977
  rendererServer = null;
2865
2978
  electronProcess = null;
@@ -2878,6 +2991,9 @@ var DevServer = class {
2878
2991
  mainWatch = null;
2879
2992
  preloadWatch = null;
2880
2993
  outputDir = "";
2994
+ nodeFormat = "es";
2995
+ mainInitialBuildPromise = null;
2996
+ resolveMainInitialBuild = null;
2881
2997
  logLevel;
2882
2998
  clearScreen;
2883
2999
  rendererOnly;
@@ -2905,6 +3021,7 @@ var DevServer = class {
2905
3021
  this.config = loaded.config;
2906
3022
  this.root = loaded.root;
2907
3023
  this.outputDir = this.outDirOverride ? resolve(this.root, this.outDirOverride) : resolve(this.root, ".electro");
3024
+ this.nodeFormat = await resolveNodeOutputFormat(this.root);
2908
3025
  this.configPaths.add(loaded.configPath);
2909
3026
  for (const view of this.config.views ?? []) this.configPaths.add(view.__source);
2910
3027
  const views = this.config.views ?? [];
@@ -2966,6 +3083,7 @@ var DevServer = class {
2966
3083
  }
2967
3084
  const electronTimer = startTimer();
2968
3085
  try {
3086
+ await this.waitForMainInitialBuild();
2969
3087
  await this.attachElectronProcess();
2970
3088
  step("electron", electronTimer());
2971
3089
  } catch (err) {
@@ -2994,6 +3112,8 @@ var DevServer = class {
2994
3112
  this.mainWatch = null;
2995
3113
  this.preloadWatch?.close();
2996
3114
  this.preloadWatch = null;
3115
+ this.resolveMainInitialBuild = null;
3116
+ this.mainInitialBuildPromise = null;
2997
3117
  if (this.mainRestartFlushTimer) {
2998
3118
  clearTimeout(this.mainRestartFlushTimer);
2999
3119
  this.mainRestartFlushTimer = null;
@@ -3062,7 +3182,8 @@ var DevServer = class {
3062
3182
  ],
3063
3183
  logLevel: this.logLevel,
3064
3184
  clearScreen: this.clearScreen,
3065
- sourcemap: this.sourcemap
3185
+ sourcemap: this.sourcemap,
3186
+ format: this.nodeFormat
3066
3187
  });
3067
3188
  if (Object.keys(input).length > 1) {
3068
3189
  const subBuildConfig = createNodeConfig({
@@ -3079,7 +3200,8 @@ var DevServer = class {
3079
3200
  ],
3080
3201
  logLevel: this.logLevel,
3081
3202
  clearScreen: this.clearScreen,
3082
- sourcemap: this.sourcemap
3203
+ sourcemap: this.sourcemap,
3204
+ format: this.nodeFormat
3083
3205
  });
3084
3206
  baseConfig.plugins.push(isolateEntriesPlugin(subBuildConfig));
3085
3207
  }
@@ -3108,6 +3230,12 @@ var DevServer = class {
3108
3230
  async buildMain(externals) {
3109
3231
  const runtimeEntry = this.config.runtime.entry;
3110
3232
  const entry = resolve(dirname(this.config.runtime.__source), runtimeEntry);
3233
+ this.mainInitialBuildPromise = new Promise((resolve) => {
3234
+ this.resolveMainInitialBuild = () => {
3235
+ resolve();
3236
+ this.resolveMainInitialBuild = null;
3237
+ };
3238
+ });
3111
3239
  const viewRegistry = (this.config.views ?? []).map((v) => ({
3112
3240
  id: v.name,
3113
3241
  hasRenderer: !!v.entry,
@@ -3130,6 +3258,7 @@ var DevServer = class {
3130
3258
  clearScreen: this.clearScreen,
3131
3259
  userViteConfig: this.config.runtime.vite,
3132
3260
  sourcemap: this.sourcemap,
3261
+ format: this.nodeFormat,
3133
3262
  define: { __ELECTRO_VIEW_REGISTRY__: JSON.stringify(viewRegistry) }
3134
3263
  });
3135
3264
  const self = this;
@@ -3145,6 +3274,7 @@ var DevServer = class {
3145
3274
  if (firstBuild) {
3146
3275
  firstBuild = false;
3147
3276
  changedFile = null;
3277
+ self.resolveMainInitialBuild?.();
3148
3278
  return;
3149
3279
  }
3150
3280
  const currentChanged = changedFile;
@@ -3160,8 +3290,21 @@ var DevServer = class {
3160
3290
  });
3161
3291
  this.mainWatch = await build(mainConfig);
3162
3292
  }
3293
+ /**
3294
+ * Primary startup synchronization for dev mode:
3295
+ * wait until the initial main watch build has completed.
3296
+ */
3297
+ async waitForMainInitialBuild() {
3298
+ if (!this.mainInitialBuildPromise) return;
3299
+ const timeout = setTimeout$1(MAIN_ENTRY_WAIT_TIMEOUT_MS).then(() => {
3300
+ throw new Error(`Main initial build did not finish in ${MAIN_ENTRY_WAIT_TIMEOUT_MS}ms.`);
3301
+ });
3302
+ const promise = this.mainInitialBuildPromise;
3303
+ this.mainInitialBuildPromise = null;
3304
+ await Promise.race([promise, timeout]);
3305
+ }
3163
3306
  async attachElectronProcess() {
3164
- const mainEntry = resolve(this.outputDir, "main/index.mjs");
3307
+ const mainEntry = await this.waitForMainEntry();
3165
3308
  const env = { ELECTRO_DEV: "true" };
3166
3309
  if (this.rendererServer) {
3167
3310
  const addr = this.rendererServer.httpServer?.address();
@@ -3190,6 +3333,23 @@ var DevServer = class {
3190
3333
  });
3191
3334
  }
3192
3335
  /**
3336
+ * In watch mode, Vite can return the watcher before the first output file
3337
+ * is written. Wait for the built main entry to appear before spawning Electron.
3338
+ */
3339
+ async waitForMainEntry() {
3340
+ const mainOutDir = resolve(this.outputDir, "main");
3341
+ const deadline = Date.now() + MAIN_ENTRY_WAIT_TIMEOUT_MS;
3342
+ let lastError;
3343
+ while (Date.now() < deadline) try {
3344
+ return await resolveMainEntryPath(mainOutDir);
3345
+ } catch (err) {
3346
+ lastError = err;
3347
+ await setTimeout$1(MAIN_ENTRY_WAIT_INTERVAL_MS);
3348
+ }
3349
+ const detail = lastError instanceof Error ? ` Last error: ${lastError.message}` : "";
3350
+ throw new Error(`Main entry was not generated in time (${MAIN_ENTRY_WAIT_TIMEOUT_MS}ms). Checked in: ${mainOutDir}.${detail}`);
3351
+ }
3352
+ /**
3193
3353
  * Restart Electron — handles queued restarts if another rebuild
3194
3354
  * arrives while restart is in flight.
3195
3355
  */
@@ -3343,7 +3503,7 @@ async function preview(options) {
3343
3503
  if (!options.skipBuild) await build$1(options);
3344
3504
  else note("Skipped build (--skip-build)");
3345
3505
  const root = process.cwd();
3346
- const mainEntry = resolve(resolve(root, options.outDir), "main/index.mjs");
3506
+ const mainEntry = await resolveMainEntryPath(resolve(resolve(root, options.outDir), "main"));
3347
3507
  const launchTimer = startTimer();
3348
3508
  try {
3349
3509
  const proc = await launchElectron({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cordy/electro-cli",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "CLI for @cordy/electro — dev server, build, and code generation commands",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,12 +47,12 @@
47
47
  "prepublishOnly": "bun run build"
48
48
  },
49
49
  "peerDependencies": {
50
- "@cordy/electro": "1.2.6",
50
+ "@cordy/electro": "1.2.8",
51
51
  "electron": ">=40.4.1",
52
52
  "vite": ">=8.0.0"
53
53
  },
54
54
  "dependencies": {
55
- "@cordy/electro-generator": "1.2.6"
55
+ "@cordy/electro-generator": "1.2.8"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@cordy/electro": "1.2.6",