@pyreon/zero 0.15.0 → 0.16.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/{api-routes-DANluJic.js → api-routes-Ci0kVmM4.js} +2 -2
- package/lib/client.js +4 -1
- package/lib/env.js +6 -6
- package/lib/font.js +3 -3
- package/lib/{fs-router-ZebyutPa.js → fs-router-MewHc5SB.js} +25 -30
- package/lib/i18n-routing.js +112 -1
- package/lib/image.js +140 -58
- package/lib/index.js +252 -82
- package/lib/og-image.js +5 -5
- package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
- package/lib/script.js +114 -25
- package/lib/seo.js +186 -15
- package/lib/server.js +274 -564
- package/lib/types/config.d.ts +275 -3
- package/lib/types/env.d.ts +2 -2
- package/lib/types/i18n-routing.d.ts +193 -2
- package/lib/types/image.d.ts +105 -5
- package/lib/types/index.d.ts +634 -182
- package/lib/types/script.d.ts +78 -6
- package/lib/types/seo.d.ts +128 -4
- package/lib/types/server.d.ts +575 -72
- package/lib/vite-plugin-xjWZwudX.js +2454 -0
- package/package.json +11 -10
- package/src/adapters/bun.ts +20 -1
- package/src/adapters/cloudflare.ts +78 -1
- package/src/adapters/index.ts +25 -3
- package/src/adapters/netlify.ts +63 -1
- package/src/adapters/node.ts +25 -1
- package/src/adapters/static.ts +26 -1
- package/src/adapters/validate.ts +8 -1
- package/src/adapters/vercel.ts +76 -1
- package/src/adapters/warn-missing-env.ts +49 -0
- package/src/app.ts +14 -0
- package/src/client.ts +18 -0
- package/src/entry-server.ts +55 -5
- package/src/env.ts +7 -7
- package/src/font.ts +3 -3
- package/src/fs-router.ts +72 -3
- package/src/i18n-routing.ts +246 -12
- package/src/image.tsx +242 -91
- package/src/index.ts +4 -4
- package/src/isr.ts +24 -6
- package/src/manifest.ts +675 -0
- package/src/og-image.ts +5 -5
- package/src/script.tsx +159 -36
- package/src/seo.ts +346 -15
- package/src/server.ts +10 -2
- package/src/ssg-plugin.ts +1211 -54
- package/src/types.ts +301 -10
- package/src/vercel-revalidate-handler.ts +204 -0
- package/src/vite-plugin.ts +108 -30
- package/lib/vite-plugin-E4BHYvYW.js +0 -855
package/src/vite-plugin.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { resolveConfig } from './config'
|
|
|
14
14
|
// Used in the dev-mode SSR catch handler to convert loader-thrown
|
|
15
15
|
// `redirect()` errors into real HTTP redirects (302/307/308).
|
|
16
16
|
import { getRedirectInfo } from '@pyreon/router'
|
|
17
|
+
import { matchPattern } from './entry-server'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Scan node_modules/@pyreon/ to discover all installed Pyreon packages.
|
|
@@ -46,7 +47,6 @@ function resolveNestedPackage(root: string, name: string): string | undefined {
|
|
|
46
47
|
if (existsSync(nested)) return nested
|
|
47
48
|
return undefined
|
|
48
49
|
}
|
|
49
|
-
import { matchPattern } from "./entry-server";
|
|
50
50
|
import { renderErrorOverlay } from "./error-overlay";
|
|
51
51
|
import {
|
|
52
52
|
generateMiddlewareModule,
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
scanRouteFiles,
|
|
55
55
|
scanRouteFilesWithExports,
|
|
56
56
|
} from "./fs-router";
|
|
57
|
+
import { expandRoutesForLocales } from "./i18n-routing";
|
|
57
58
|
import { render404Page } from "./not-found";
|
|
58
59
|
import { ssgPlugin } from "./ssg-plugin";
|
|
59
60
|
import type { ZeroConfig } from "./types";
|
|
@@ -67,6 +68,28 @@ const RESOLVED_VIRTUAL_MIDDLEWARE_ID = `\0${VIRTUAL_MIDDLEWARE_ID}`;
|
|
|
67
68
|
const VIRTUAL_API_ROUTES_ID = "virtual:zero/api-routes";
|
|
68
69
|
const RESOLVED_VIRTUAL_API_ROUTES_ID = `\0${VIRTUAL_API_ROUTES_ID}`;
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Per-plugin-instance storage for the user-supplied ZeroConfig. Lets
|
|
73
|
+
* downstream consumers (e.g. `@pyreon/zero-cli`'s `build` command, which
|
|
74
|
+
* loads the user's `vite.config.ts` and inspects its plugin list)
|
|
75
|
+
* recover the original config without us attaching internal state to
|
|
76
|
+
* the public Plugin object via an underscore-prefixed property.
|
|
77
|
+
*
|
|
78
|
+
* Exported via `getZeroPluginConfig(plugin)` so the WeakMap itself
|
|
79
|
+
* stays an implementation detail — callers can't enumerate or mutate
|
|
80
|
+
* the table, only read by Plugin identity.
|
|
81
|
+
*/
|
|
82
|
+
const zeroPluginConfigMap = new WeakMap<Plugin, ZeroConfig>();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retrieve the `ZeroConfig` that was passed to `zeroPlugin(userConfig)`
|
|
86
|
+
* when the plugin was created. Returns `undefined` if the argument
|
|
87
|
+
* isn't a recognized pyreon-zero main plugin instance.
|
|
88
|
+
*/
|
|
89
|
+
export function getZeroPluginConfig(plugin: Plugin): ZeroConfig | undefined {
|
|
90
|
+
return zeroPluginConfigMap.get(plugin);
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
/**
|
|
71
94
|
* Zero Vite plugin — adds file-based routing and zero-config conventions
|
|
72
95
|
* on top of @pyreon/vite-plugin.
|
|
@@ -85,10 +108,9 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
85
108
|
let routesDir: string;
|
|
86
109
|
let root: string;
|
|
87
110
|
|
|
88
|
-
const mainPlugin: Plugin
|
|
111
|
+
const mainPlugin: Plugin = {
|
|
89
112
|
name: "pyreon-zero",
|
|
90
113
|
enforce: "pre",
|
|
91
|
-
_zeroConfig: userConfig,
|
|
92
114
|
|
|
93
115
|
configResolved(resolvedConfig) {
|
|
94
116
|
root = resolvedConfig.root;
|
|
@@ -109,7 +131,13 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
109
131
|
// • lazy() for routes that only export `default` (best code splitting)
|
|
110
132
|
// • Direct mod.loader/.guard/.meta access for routes with metadata
|
|
111
133
|
// • No spurious IMPORT_IS_UNDEFINED warnings from Rolldown
|
|
112
|
-
const
|
|
134
|
+
const baseRoutes = await scanRouteFilesWithExports(routesDir, config.mode);
|
|
135
|
+
// PR H — fan routes into per-locale variants when `i18n` is
|
|
136
|
+
// configured. No-op when unset; identity-returns the input
|
|
137
|
+
// otherwise so existing apps see byte-identical output.
|
|
138
|
+
const routes = config.i18n
|
|
139
|
+
? expandRoutesForLocales(baseRoutes, config.i18n)
|
|
140
|
+
: baseRoutes;
|
|
113
141
|
return generateRouteModuleFromRoutes(routes, routesDir, {
|
|
114
142
|
staticImports: config.mode === 'ssg',
|
|
115
143
|
});
|
|
@@ -203,10 +231,10 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
203
231
|
renderSsr(server, root, req.originalUrl ?? pathname, pathname, webReq).then(
|
|
204
232
|
(result) => {
|
|
205
233
|
if (result === null) return next();
|
|
206
|
-
res.statusCode =
|
|
234
|
+
res.statusCode = result.status;
|
|
207
235
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
208
|
-
res.setHeader("Content-Length", Buffer.byteLength(result));
|
|
209
|
-
res.end(result);
|
|
236
|
+
res.setHeader("Content-Length", Buffer.byteLength(result.html));
|
|
237
|
+
res.end(result.html);
|
|
210
238
|
},
|
|
211
239
|
(err: unknown) => {
|
|
212
240
|
// Loader-thrown `redirect()` — convert to a real HTTP redirect
|
|
@@ -327,15 +355,15 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
327
355
|
// Discover all @pyreon/* packages installed in node_modules.
|
|
328
356
|
// The "bun" export condition points to TS source — esbuild's
|
|
329
357
|
// dep optimizer would compile them with the wrong JSX runtime.
|
|
330
|
-
const
|
|
331
|
-
const pyreonExclude = scanPyreonPackages(
|
|
358
|
+
const cwd = viteUserConfig.root ?? process.cwd()
|
|
359
|
+
const pyreonExclude = scanPyreonPackages(cwd)
|
|
332
360
|
|
|
333
361
|
// `@pyreon/runtime-server` is only imported by zero's dev SSR
|
|
334
362
|
// middleware and the production server entry — apps rarely list it
|
|
335
363
|
// as a direct dep. Resolve it to the copy nested under zero so
|
|
336
364
|
// `ssrLoadModule("@pyreon/runtime-server")` works uniformly.
|
|
337
365
|
const runtimeServerAlias = resolveNestedPackage(
|
|
338
|
-
|
|
366
|
+
cwd,
|
|
339
367
|
"@pyreon/runtime-server",
|
|
340
368
|
)
|
|
341
369
|
|
|
@@ -371,6 +399,25 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
371
399
|
...(userConfig.port !== undefined
|
|
372
400
|
? { server: { port: config.port } }
|
|
373
401
|
: {}),
|
|
402
|
+
// Propagate `zero({ base })` to Vite's `base` config — that's
|
|
403
|
+
// what controls asset URL rewriting in the built HTML/JS
|
|
404
|
+
// (`<script src="/blog/assets/…">`). Pre-fix this was a
|
|
405
|
+
// typed-but-unimplemented field: `__ZERO_BASE__` was defined
|
|
406
|
+
// as a Vite global but no consumer existed, AND Vite's own
|
|
407
|
+
// `base` had to be set manually in vite.config.ts. Setting
|
|
408
|
+
// it here makes `zero({ base: '/blog/' })` the canonical
|
|
409
|
+
// single-source-of-truth surface; the value flows through
|
|
410
|
+
// to (a) Vite's HTML/asset URL rewriter, (b) `createRouter`
|
|
411
|
+
// via `__ZERO_BASE__` in `startClient` / `createApp`, (c)
|
|
412
|
+
// the SSG entry's `createApp({ base })` call.
|
|
413
|
+
//
|
|
414
|
+
// Vite's config-merge semantics: plugin-returned config is
|
|
415
|
+
// the BASE; user's `vite.config.ts` top-level overrides.
|
|
416
|
+
// So a user who sets BOTH `zero({ base: '/blog/' })` AND
|
|
417
|
+
// `vite.config.base: '/foo/'` gets the latter — the user's
|
|
418
|
+
// explicit override wins. The default `/` is a no-op
|
|
419
|
+
// (matches Vite's default), so always-setting is safe.
|
|
420
|
+
base: config.base,
|
|
374
421
|
define: {
|
|
375
422
|
__ZERO_MODE__: JSON.stringify(config.mode),
|
|
376
423
|
__ZERO_BASE__: JSON.stringify(config.base),
|
|
@@ -379,6 +426,12 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
379
426
|
},
|
|
380
427
|
};
|
|
381
428
|
|
|
429
|
+
// Stash the original user config keyed by plugin identity so the CLI
|
|
430
|
+
// (which loads vite.config.ts and inspects the plugin list) can
|
|
431
|
+
// recover it via `getZeroPluginConfig(plugin)` without us hanging a
|
|
432
|
+
// `_`-prefixed property off the public Plugin object.
|
|
433
|
+
zeroPluginConfigMap.set(mainPlugin, userConfig);
|
|
434
|
+
|
|
382
435
|
// SSG mode auto-wires the static-site generation hook. Other modes get
|
|
383
436
|
// just the main plugin. The SSG plugin internally no-ops when
|
|
384
437
|
// `mode !== 'ssg'`, but skipping it entirely keeps the plugin chain
|
|
@@ -488,13 +541,24 @@ async function dispatchApiRoute(
|
|
|
488
541
|
}
|
|
489
542
|
|
|
490
543
|
/**
|
|
491
|
-
*
|
|
492
|
-
* Returns true if the 404 was handled (response sent), false otherwise.
|
|
544
|
+
* Static-page 404 fallback for apps WITHOUT `_404.tsx` in the routes tree.
|
|
493
545
|
*
|
|
494
|
-
*
|
|
495
|
-
* the
|
|
496
|
-
*
|
|
497
|
-
*
|
|
546
|
+
* For `mode: 'ssr'` apps with `_404.tsx`, the SSR middleware's `renderSsr`
|
|
547
|
+
* routes unmatched URLs through the router-driven path (PR L5 + M1.2) — that
|
|
548
|
+
* produces a layout-wrapped 404 with HTTP status 404, never reaching here.
|
|
549
|
+
* This function is the LEGACY fallback that fires only when:
|
|
550
|
+
* - The app is in `mode: 'spa'` / `mode: 'ssg'` (no dev SSR middleware), OR
|
|
551
|
+
* - The app has no reachable `notFoundComponent` in its routes tree (so the
|
|
552
|
+
* SSR middleware's `resolveRoute` returns matched: [] and falls through).
|
|
553
|
+
*
|
|
554
|
+
* Returns true if the 404 was handled (response sent), false if the path
|
|
555
|
+
* actually matches a route (caller continues to next middleware).
|
|
556
|
+
*
|
|
557
|
+
* Pre-M1.2 a stale comment claimed `_404.tsx` "cannot be SSR-rendered because
|
|
558
|
+
* the compiler emits _tpl() calls that require document". That was wrong — the
|
|
559
|
+
* SSR runtime renders compiler-emitted components fine via `renderToString`
|
|
560
|
+
* (no document needed). The static fallback exists for backward compat with
|
|
561
|
+
* apps that don't ship `_404.tsx`, not because SSR-rendering it is impossible.
|
|
498
562
|
*/
|
|
499
563
|
async function handle404(
|
|
500
564
|
server: import("vite").ViteDevServer,
|
|
@@ -510,10 +574,11 @@ async function handle404(
|
|
|
510
574
|
return false; // Route matches — not a 404
|
|
511
575
|
}
|
|
512
576
|
|
|
513
|
-
// No route matched —
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
577
|
+
// No route matched + no `_404.tsx` reachable — emit a minimal static page
|
|
578
|
+
// so the user gets SOMETHING. Apps that want branded 404s should add
|
|
579
|
+
// `_404.tsx` to their routes tree (canonical pattern); the SSR middleware
|
|
580
|
+
// then routes through the router-driven path with layout chrome instead
|
|
581
|
+
// of landing here.
|
|
517
582
|
const html = await render404Page(undefined);
|
|
518
583
|
|
|
519
584
|
res.statusCode = 404;
|
|
@@ -541,18 +606,12 @@ async function renderSsr(
|
|
|
541
606
|
originalUrl: string,
|
|
542
607
|
pathname: string,
|
|
543
608
|
req?: Request,
|
|
544
|
-
): Promise<string | null> {
|
|
545
|
-
// Pattern check FIRST — otherwise SSR would try (and likely crash) on
|
|
546
|
-
// asset paths that happened to accept text/html (e.g. curl-style).
|
|
609
|
+
): Promise<{ html: string; status: number } | null> {
|
|
547
610
|
const routesMod = await server.ssrLoadModule(VIRTUAL_ROUTES_ID);
|
|
548
611
|
const routes = routesMod.routes as Array<{
|
|
549
612
|
path?: string;
|
|
550
613
|
children?: unknown[];
|
|
551
614
|
}>;
|
|
552
|
-
const patterns = flattenRoutePatterns(routes);
|
|
553
|
-
if (!patterns.some((pattern) => matchPattern(pattern, pathname))) {
|
|
554
|
-
return null;
|
|
555
|
-
}
|
|
556
615
|
|
|
557
616
|
// Read + transform index.html (Vite injects the HMR client / JSX prelude).
|
|
558
617
|
let template = await readFile(join(root, "index.html"), "utf-8");
|
|
@@ -565,7 +624,7 @@ async function renderSsr(
|
|
|
565
624
|
// `@pyreon/runtime-server` isn't a direct dep of most apps, so zero's
|
|
566
625
|
// `config()` hook registers an alias that points it at the copy under
|
|
567
626
|
// zero's own `node_modules` — same path → same Vite module → same instance.
|
|
568
|
-
const [core,
|
|
627
|
+
const [core, _headPkg, headSsr, routerPkg, runtimeServer] = await Promise.all(
|
|
569
628
|
[
|
|
570
629
|
server.ssrLoadModule("@pyreon/core") as Promise<
|
|
571
630
|
typeof import("@pyreon/core")
|
|
@@ -620,8 +679,25 @@ async function renderSsr(
|
|
|
620
679
|
// `preload` loads lazy route components AND runs loaders for `pathname` so
|
|
621
680
|
// the synchronous render pass produces final HTML — no loading fallbacks,
|
|
622
681
|
// no `useLoaderData() === undefined`.
|
|
682
|
+
//
|
|
683
|
+
// M1.2 — Unmatched URLs no longer bail to a static 404 page. The router's
|
|
684
|
+
// `resolveRoute` (PR L5) walks the route tree and, if a parent layout has
|
|
685
|
+
// `notFoundComponent` AND the URL is under that layout's prefix, builds a
|
|
686
|
+
// synthetic chain `[...ancestorLayouts, syntheticLeaf]` with
|
|
687
|
+
// `isNotFound: true`. The render then produces 404 HTML INSIDE the
|
|
688
|
+
// layout's chrome. If the routes tree has no reachable `notFoundComponent`,
|
|
689
|
+
// `matched` stays empty — fall through to `handle404` for the static
|
|
690
|
+
// fallback (preserves backward compat for apps without `_404.tsx`).
|
|
623
691
|
await routerInst.preload(pathname, req);
|
|
624
692
|
|
|
693
|
+
const resolved = routerInst.currentRoute() as
|
|
694
|
+
| { matched?: unknown[]; isNotFound?: boolean }
|
|
695
|
+
| undefined;
|
|
696
|
+
if (!resolved?.matched || resolved.matched.length === 0) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
const status = resolved.isNotFound === true ? 404 : 200;
|
|
700
|
+
|
|
625
701
|
return runtimeServer.runWithRequestContext(async () => {
|
|
626
702
|
const app = core.h(App as Parameters<typeof core.h>[0], null);
|
|
627
703
|
|
|
@@ -630,14 +706,16 @@ async function renderSsr(
|
|
|
630
706
|
routerInst as Parameters<typeof routerPkg.serializeLoaderData>[0],
|
|
631
707
|
);
|
|
632
708
|
const hasData = loaderData && Object.keys(loaderData).length > 0;
|
|
709
|
+
// M2.2 — safe serializer (parity with production handler / SSG entry).
|
|
633
710
|
const loaderScript = hasData
|
|
634
|
-
? `<script>window.__PYREON_LOADER_DATA__=${
|
|
711
|
+
? `<script>window.__PYREON_LOADER_DATA__=${routerPkg.stringifyLoaderData(loaderData)}</script>`
|
|
635
712
|
: "";
|
|
636
713
|
|
|
637
|
-
|
|
714
|
+
const html = template
|
|
638
715
|
.replace("<!--pyreon-head-->", head)
|
|
639
716
|
.replace("<!--pyreon-app-->", appHtml)
|
|
640
717
|
.replace("<!--pyreon-scripts-->", loaderScript);
|
|
718
|
+
return { html, status };
|
|
641
719
|
});
|
|
642
720
|
}
|
|
643
721
|
|