@pyreon/zero 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as render404Page, a as detectLocaleFromHeader, c as vercelAdapter, d as netlifyAdapter, f as cloudflareAdapter, g as createServer, h as resolveConfig, i as createLocaleContext, l as staticAdapter, m as defineConfig, o as i18nRouting, p as bunAdapter, r as zeroPlugin, s as resolveAdapter, t as getZeroPluginConfig, u as nodeAdapter, v as createApp } from "./vite-plugin-xjWZwudX.js";
1
+ import { _ as render404Page, a as detectLocaleFromHeader, c as vercelAdapter, d as netlifyAdapter, f as cloudflareAdapter, g as createServer, h as resolveConfig, i as createLocaleContext, l as staticAdapter, m as defineConfig, o as i18nRouting, p as bunAdapter, r as zeroPlugin, s as resolveAdapter, t as getZeroPluginConfig, u as nodeAdapter, v as createApp } from "./vite-plugin-y0NmCLJA.js";
2
2
  import { i as generateRouteModule, o as parseFileRoutes, r as generateMiddlewareModule, s as scanRouteFiles, t as filePathToUrlPath } from "./fs-router-MewHc5SB.js";
3
3
  import { existsSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
@@ -210,6 +210,38 @@ interface ZeroConfig {
210
210
  currentPath: string;
211
211
  elapsed: number;
212
212
  }) => void | Promise<void>;
213
+ /**
214
+ * Route-level code splitting in SSG mode. Default `true`.
215
+ *
216
+ * When `true` (default), each route file becomes its own dynamic-import
217
+ * chunk via `lazy(() => import("..."))` — only the route the user
218
+ * lands on plus its dependencies ship in the initial bundle, the
219
+ * rest fetch on navigation. Matches the SSR/SPA-mode behaviour zero
220
+ * has always had; brings parity to SSG.
221
+ *
222
+ * When `false`, every route is bundled statically into the main
223
+ * client chunk (the pre-2026-Q3 SSG behaviour). Useful for tiny
224
+ * sites (2-5 pages) where the single-chunk-then-instant-nav trade
225
+ * is preferable — the chunk-fetch cost on navigation is gone, and
226
+ * the marginal bytes are negligible.
227
+ *
228
+ * Crossover point: ~5-8 routes. Below that, single-chunk is fine.
229
+ * Above that, lazy() shrinks the initial bundle by a meaningful
230
+ * amount (a 50-route docs site might drop from 200 KB to 80 KB on
231
+ * first paint).
232
+ *
233
+ * Underlying mechanism is the same 3-tier generator zero already
234
+ * uses for SSR/SPA mode (`fs-router.ts:generateRouteEntry`): lazy
235
+ * component + inlined metadata when possible, lazy + lazy-thunked
236
+ * function exports when not, namespace-import fallback for cases
237
+ * the literal-extractor can't reach.
238
+ *
239
+ * @example
240
+ * ssg: {
241
+ * splitChunks: false, // bundle-everything for a 3-page marketing site
242
+ * }
243
+ */
244
+ splitChunks?: boolean;
213
245
  };
214
246
  /** ISR config — only used when mode is "isr". */
215
247
  isr?: ISRConfig;
@@ -628,6 +628,38 @@ interface ZeroConfig {
628
628
  currentPath: string;
629
629
  elapsed: number;
630
630
  }) => void | Promise<void>;
631
+ /**
632
+ * Route-level code splitting in SSG mode. Default `true`.
633
+ *
634
+ * When `true` (default), each route file becomes its own dynamic-import
635
+ * chunk via `lazy(() => import("..."))` — only the route the user
636
+ * lands on plus its dependencies ship in the initial bundle, the
637
+ * rest fetch on navigation. Matches the SSR/SPA-mode behaviour zero
638
+ * has always had; brings parity to SSG.
639
+ *
640
+ * When `false`, every route is bundled statically into the main
641
+ * client chunk (the pre-2026-Q3 SSG behaviour). Useful for tiny
642
+ * sites (2-5 pages) where the single-chunk-then-instant-nav trade
643
+ * is preferable — the chunk-fetch cost on navigation is gone, and
644
+ * the marginal bytes are negligible.
645
+ *
646
+ * Crossover point: ~5-8 routes. Below that, single-chunk is fine.
647
+ * Above that, lazy() shrinks the initial bundle by a meaningful
648
+ * amount (a 50-route docs site might drop from 200 KB to 80 KB on
649
+ * first paint).
650
+ *
651
+ * Underlying mechanism is the same 3-tier generator zero already
652
+ * uses for SSR/SPA mode (`fs-router.ts:generateRouteEntry`): lazy
653
+ * component + inlined metadata when possible, lazy + lazy-thunked
654
+ * function exports when not, namespace-import fallback for cases
655
+ * the literal-extractor can't reach.
656
+ *
657
+ * @example
658
+ * ssg: {
659
+ * splitChunks: false, // bundle-everything for a 3-page marketing site
660
+ * }
661
+ */
662
+ splitChunks?: boolean;
631
663
  };
632
664
  /** ISR config — only used when mode is "isr". */
633
665
  isr?: ISRConfig;
@@ -331,6 +331,38 @@ interface ZeroConfig {
331
331
  currentPath: string;
332
332
  elapsed: number;
333
333
  }) => void | Promise<void>;
334
+ /**
335
+ * Route-level code splitting in SSG mode. Default `true`.
336
+ *
337
+ * When `true` (default), each route file becomes its own dynamic-import
338
+ * chunk via `lazy(() => import("..."))` — only the route the user
339
+ * lands on plus its dependencies ship in the initial bundle, the
340
+ * rest fetch on navigation. Matches the SSR/SPA-mode behaviour zero
341
+ * has always had; brings parity to SSG.
342
+ *
343
+ * When `false`, every route is bundled statically into the main
344
+ * client chunk (the pre-2026-Q3 SSG behaviour). Useful for tiny
345
+ * sites (2-5 pages) where the single-chunk-then-instant-nav trade
346
+ * is preferable — the chunk-fetch cost on navigation is gone, and
347
+ * the marginal bytes are negligible.
348
+ *
349
+ * Crossover point: ~5-8 routes. Below that, single-chunk is fine.
350
+ * Above that, lazy() shrinks the initial bundle by a meaningful
351
+ * amount (a 50-route docs site might drop from 200 KB to 80 KB on
352
+ * first paint).
353
+ *
354
+ * Underlying mechanism is the same 3-tier generator zero already
355
+ * uses for SSR/SPA mode (`fs-router.ts:generateRouteEntry`): lazy
356
+ * component + inlined metadata when possible, lazy + lazy-thunked
357
+ * function exports when not, namespace-import fallback for cases
358
+ * the literal-extractor can't reach.
359
+ *
360
+ * @example
361
+ * ssg: {
362
+ * splitChunks: false, // bundle-everything for a 3-page marketing site
363
+ * }
364
+ */
365
+ splitChunks?: boolean;
334
366
  };
335
367
  /** ISR config — only used when mode is "isr". */
336
368
  isr?: ISRConfig;
@@ -2073,6 +2073,7 @@ function ssgPlugin(userConfig = {}) {
2073
2073
  //#endregion
2074
2074
  //#region src/vite-plugin.ts
2075
2075
  var vite_plugin_exports = /* @__PURE__ */ __exportAll({
2076
+ argvHasPortFlag: () => argvHasPortFlag,
2076
2077
  getZeroPluginConfig: () => getZeroPluginConfig,
2077
2078
  zeroPlugin: () => zeroPlugin
2078
2079
  });
@@ -2129,6 +2130,25 @@ function getZeroPluginConfig(plugin) {
2129
2130
  return zeroPluginConfigMap.get(plugin);
2130
2131
  }
2131
2132
  /**
2133
+ * Detects `--port` / `--port=N` / `-p N` / `-p=N` in `process.argv`.
2134
+ * Used by the plugin's `config()` hook to decide whether to apply the
2135
+ * default port — when the CLI was invoked with `--port`, the plugin
2136
+ * must skip its default so the CLI flag wins (see the comment at the
2137
+ * port-handling block in `zeroPlugin()` for the full precedence model).
2138
+ *
2139
+ * Exported for testing only (the plugin uses it internally).
2140
+ *
2141
+ * @internal
2142
+ */
2143
+ function argvHasPortFlag(argv = process.argv) {
2144
+ for (let i = 0; i < argv.length; i++) {
2145
+ const a = argv[i];
2146
+ if (a === "--port" || a === "-p") return true;
2147
+ if (a !== void 0 && (a.startsWith("--port=") || a.startsWith("-p="))) return true;
2148
+ }
2149
+ return false;
2150
+ }
2151
+ /**
2132
2152
  * Zero Vite plugin — adds file-based routing and zero-config conventions
2133
2153
  * on top of @pyreon/vite-plugin.
2134
2154
  *
@@ -2160,7 +2180,9 @@ function zeroPlugin(userConfig = {}) {
2160
2180
  async load(id) {
2161
2181
  if (id === RESOLVED_VIRTUAL_ROUTES_ID) try {
2162
2182
  const baseRoutes = await scanRouteFilesWithExports(routesDir, config.mode);
2163
- return generateRouteModuleFromRoutes(config.i18n ? expandRoutesForLocales(baseRoutes, config.i18n) : baseRoutes, routesDir, { staticImports: config.mode === "ssg" });
2183
+ const routes = config.i18n ? expandRoutesForLocales(baseRoutes, config.i18n) : baseRoutes;
2184
+ const ssgSplitDisabled = config.mode === "ssg" && config.ssg?.splitChunks === false;
2185
+ return generateRouteModuleFromRoutes(routes, routesDir, { staticImports: ssgSplitDisabled });
2164
2186
  } catch (_err) {
2165
2187
  return `export const routes = []`;
2166
2188
  }
@@ -2289,7 +2311,7 @@ function zeroPlugin(userConfig = {}) {
2289
2311
  ...runtimeServerAlias ? { alias: { "@pyreon/runtime-server": runtimeServerAlias } } : {}
2290
2312
  } },
2291
2313
  optimizeDeps: { exclude: pyreonExclude },
2292
- ...userConfig.port !== void 0 ? { server: { port: config.port } } : {},
2314
+ ...userConfig.port === void 0 && argvHasPortFlag() ? {} : { server: { port: config.port } },
2293
2315
  base: config.base,
2294
2316
  define: {
2295
2317
  __ZERO_MODE__: JSON.stringify(config.mode),
@@ -2451,4 +2473,4 @@ function flattenRoutePatterns(routes) {
2451
2473
 
2452
2474
  //#endregion
2453
2475
  export { render404Page as _, detectLocaleFromHeader as a, vercelAdapter as c, netlifyAdapter as d, cloudflareAdapter as f, createServer as g, resolveConfig as h, createLocaleContext as i, staticAdapter as l, defineConfig as m, vite_plugin_exports as n, i18nRouting as o, bunAdapter as p, zeroPlugin as r, resolveAdapter as s, getZeroPluginConfig as t, nodeAdapter as u, createApp as v };
2454
- //# sourceMappingURL=vite-plugin-xjWZwudX.js.map
2476
+ //# sourceMappingURL=vite-plugin-y0NmCLJA.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/zero",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
5
5
  "license": "MIT",
6
6
  "author": "Vit Bokisch",
@@ -168,15 +168,15 @@
168
168
  "lint": "oxlint ."
169
169
  },
170
170
  "dependencies": {
171
- "@pyreon/core": "^0.16.0",
172
- "@pyreon/head": "^0.16.0",
173
- "@pyreon/meta": "^0.16.0",
174
- "@pyreon/reactivity": "^0.16.0",
175
- "@pyreon/router": "^0.16.0",
176
- "@pyreon/runtime-dom": "^0.16.0",
177
- "@pyreon/runtime-server": "^0.16.0",
178
- "@pyreon/server": "^0.16.0",
179
- "@pyreon/vite-plugin": "^0.16.0",
171
+ "@pyreon/core": "^0.18.0",
172
+ "@pyreon/head": "^0.18.0",
173
+ "@pyreon/meta": "^0.18.0",
174
+ "@pyreon/reactivity": "^0.18.0",
175
+ "@pyreon/router": "^0.18.0",
176
+ "@pyreon/runtime-dom": "^0.18.0",
177
+ "@pyreon/runtime-server": "^0.18.0",
178
+ "@pyreon/server": "^0.18.0",
179
+ "@pyreon/vite-plugin": "^0.18.0",
180
180
  "vite": "^8.0.0"
181
181
  },
182
182
  "devDependencies": {
package/src/types.ts CHANGED
@@ -254,6 +254,38 @@ export interface ZeroConfig {
254
254
  currentPath: string
255
255
  elapsed: number
256
256
  }) => void | Promise<void>
257
+ /**
258
+ * Route-level code splitting in SSG mode. Default `true`.
259
+ *
260
+ * When `true` (default), each route file becomes its own dynamic-import
261
+ * chunk via `lazy(() => import("..."))` — only the route the user
262
+ * lands on plus its dependencies ship in the initial bundle, the
263
+ * rest fetch on navigation. Matches the SSR/SPA-mode behaviour zero
264
+ * has always had; brings parity to SSG.
265
+ *
266
+ * When `false`, every route is bundled statically into the main
267
+ * client chunk (the pre-2026-Q3 SSG behaviour). Useful for tiny
268
+ * sites (2-5 pages) where the single-chunk-then-instant-nav trade
269
+ * is preferable — the chunk-fetch cost on navigation is gone, and
270
+ * the marginal bytes are negligible.
271
+ *
272
+ * Crossover point: ~5-8 routes. Below that, single-chunk is fine.
273
+ * Above that, lazy() shrinks the initial bundle by a meaningful
274
+ * amount (a 50-route docs site might drop from 200 KB to 80 KB on
275
+ * first paint).
276
+ *
277
+ * Underlying mechanism is the same 3-tier generator zero already
278
+ * uses for SSR/SPA mode (`fs-router.ts:generateRouteEntry`): lazy
279
+ * component + inlined metadata when possible, lazy + lazy-thunked
280
+ * function exports when not, namespace-import fallback for cases
281
+ * the literal-extractor can't reach.
282
+ *
283
+ * @example
284
+ * ssg: {
285
+ * splitChunks: false, // bundle-everything for a 3-page marketing site
286
+ * }
287
+ */
288
+ splitChunks?: boolean
257
289
  }
258
290
 
259
291
  /** ISR config — only used when mode is "isr". */
@@ -90,6 +90,27 @@ export function getZeroPluginConfig(plugin: Plugin): ZeroConfig | undefined {
90
90
  return zeroPluginConfigMap.get(plugin);
91
91
  }
92
92
 
93
+ /**
94
+ * Detects `--port` / `--port=N` / `-p N` / `-p=N` in `process.argv`.
95
+ * Used by the plugin's `config()` hook to decide whether to apply the
96
+ * default port — when the CLI was invoked with `--port`, the plugin
97
+ * must skip its default so the CLI flag wins (see the comment at the
98
+ * port-handling block in `zeroPlugin()` for the full precedence model).
99
+ *
100
+ * Exported for testing only (the plugin uses it internally).
101
+ *
102
+ * @internal
103
+ */
104
+ export function argvHasPortFlag(argv: readonly string[] = process.argv): boolean {
105
+ for (let i = 0; i < argv.length; i++) {
106
+ const a = argv[i];
107
+ if (a === "--port" || a === "-p") return true;
108
+ if (a !== undefined && (a.startsWith("--port=") || a.startsWith("-p=")))
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+
93
114
  /**
94
115
  * Zero Vite plugin — adds file-based routing and zero-config conventions
95
116
  * on top of @pyreon/vite-plugin.
@@ -138,8 +159,23 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
138
159
  const routes = config.i18n
139
160
  ? expandRoutesForLocales(baseRoutes, config.i18n)
140
161
  : baseRoutes;
162
+ // SSG mode: lazy() route splitting by default (parity with
163
+ // SSR/SPA). Opt-out via `ssg.splitChunks: false` for tiny
164
+ // sites that prefer single-chunk + instant navigation.
165
+ //
166
+ // Pre-2026-Q3: SSG was hardcoded to `staticImports: true`
167
+ // (bundle everything). Trade-off was instant post-hydration
168
+ // nav, but the initial bundle grew linearly with route
169
+ // count — a 50-route docs site shipped all 50 route
170
+ // components on first paint. Lazy splitting (now the
171
+ // default for SSG) fixes that: only the landing route +
172
+ // deps load up front, the rest fetch on navigation. See
173
+ // `ssg.splitChunks` JSDoc in types.ts for the crossover-
174
+ // point rationale.
175
+ const ssgSplitDisabled =
176
+ config.mode === "ssg" && config.ssg?.splitChunks === false;
141
177
  return generateRouteModuleFromRoutes(routes, routesDir, {
142
- staticImports: config.mode === 'ssg',
178
+ staticImports: ssgSplitDisabled,
143
179
  });
144
180
  } catch (_err) {
145
181
  return `export const routes = []`;
@@ -389,16 +425,32 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
389
425
  optimizeDeps: {
390
426
  exclude: pyreonExclude,
391
427
  },
392
- // Only set the port when the user explicitly provided one in
393
- // `zero({ port: N })`. Without this guard, the plugin always
394
- // returned `server: { port: 3000 }` which overrode Vite's CLI
395
- // `--port` flag and made multi-example dev impossible — every
396
- // example tried to bind 3000 even when launched with
397
- // `vite --port 5173`. Surfaced when wiring up the playwright
398
- // e2e suite.
399
- ...(userConfig.port !== undefined
400
- ? { server: { port: config.port } }
401
- : {}),
428
+ // Port handling the zero-canonical default is 3000 (matches
429
+ // `zero dev` / `zero preview` / the runtime adapter, and
430
+ // matches Next.js / Remix / Astro convention).
431
+ //
432
+ // Apply the default UNLESS Vite's CLI was invoked with
433
+ // `--port`/`-p` (in which case the CLI flag must win — see
434
+ // memory: vite cli port doesnt override plugin). PR #579
435
+ // proved this empirically: returning `server: { port: 3000 }`
436
+ // unconditionally clobbered `vite --port 517N --strictPort`
437
+ // in the e2e playwright config and every webServer timed
438
+ // out. argv detection here lets the CLI win at the source.
439
+ //
440
+ // Precedence (CLI > user vite.config > zero({port}) > 3000):
441
+ // 1. `vite --port N` → argvHasPortFlag() === true → plugin
442
+ // omits `server.port` entirely → CLI value wins
443
+ // 2. User `vite.config.ts server: { port: N }` → user
444
+ // config beats plugin in Vite's merge order
445
+ // 3. `zero({ port: N })` → resolved into `config.port`
446
+ // 4. Default 3000 — when no other source set a port
447
+ //
448
+ // `process.argv` is populated by the time Vite invokes the
449
+ // plugin's config() hook (Vite calls plugins synchronously
450
+ // during CLI bootstrap before applying inline overrides).
451
+ ...(userConfig.port === undefined && argvHasPortFlag()
452
+ ? {}
453
+ : { server: { port: config.port } }),
402
454
  // Propagate `zero({ base })` to Vite's `base` config — that's
403
455
  // what controls asset URL rewriting in the built HTML/JS
404
456
  // (`<script src="/blog/assets/…">`). Pre-fix this was a