@pylonsync/functions 0.11.5 → 0.12.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/dist/build-cli.d.ts +1 -0
- package/dist/build-compat.d.ts +81 -0
- package/dist/index.d.ts +1 -1
- package/dist/production-build.d.ts +47 -0
- package/dist/server-bundle.d.ts +31 -0
- package/dist/ssr-client-bundler.d.ts +39 -2
- package/dist/ssr-fonts.d.ts +1 -1
- package/dist/ssr-runtime.d.ts +13 -1
- package/dist/stdout-fence.d.ts +18 -0
- package/dist/types.d.ts +42 -0
- package/package.json +8 -4
- package/src/build-cli.ts +36 -0
- package/src/build-compat.test.ts +173 -0
- package/src/build-compat.ts +430 -0
- package/src/index.ts +1 -0
- package/src/production-build.test.ts +269 -0
- package/src/production-build.ts +577 -0
- package/src/runtime-shard-ticket.test.ts +87 -0
- package/src/runtime.ts +64 -82
- package/src/server-bundle.test.ts +89 -0
- package/src/server-bundle.ts +63 -0
- package/src/ssr-client-bundler.test.ts +51 -0
- package/src/ssr-client-bundler.ts +150 -21
- package/src/ssr-fonts.ts +1 -1
- package/src/ssr-og-runtime.ts +17 -5
- package/src/ssr-runtime.ts +112 -47
- package/src/stdout-fence.ts +69 -0
- package/src/types.ts +42 -0
|
@@ -34,6 +34,14 @@
|
|
|
34
34
|
// - CSS chunking. No CSS support in SSR yet.
|
|
35
35
|
|
|
36
36
|
import { buildFonts, readManifestFonts, type ManifestFonts } from "./ssr-fonts";
|
|
37
|
+
import {
|
|
38
|
+
applyClientCompat,
|
|
39
|
+
resolveClientCompat,
|
|
40
|
+
transformCss,
|
|
41
|
+
type ClientCompat,
|
|
42
|
+
type ManifestBuildConfig,
|
|
43
|
+
} from "./build-compat";
|
|
44
|
+
import { serverBundle } from "./server-bundle";
|
|
37
45
|
|
|
38
46
|
type Send = (msg: Record<string, unknown>) => void;
|
|
39
47
|
|
|
@@ -88,7 +96,7 @@ declare const Bun: {
|
|
|
88
96
|
target?: "browser" | "bun" | "node";
|
|
89
97
|
format?: "esm" | "iife";
|
|
90
98
|
minify?: boolean;
|
|
91
|
-
sourcemap?: "none" | "inline" | "external";
|
|
99
|
+
sourcemap?: "none" | "inline" | "external" | "linked";
|
|
92
100
|
define?: Record<string, string>;
|
|
93
101
|
external?: string[];
|
|
94
102
|
splitting?: boolean;
|
|
@@ -141,6 +149,20 @@ export function assertNotServerOnly(specifier: string, importer: string): void {
|
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
|
|
152
|
+
/**
|
|
153
|
+
* A module file → its project-relative, extension-less component path
|
|
154
|
+
* (`<cwd>/app/blog/page.tsx` → `app/blog/page`). Always "/"-separated, on
|
|
155
|
+
* Windows too: the result is a manifest key, the input to the SSR boundary
|
|
156
|
+
* walk, and is spliced into generated `import` specifiers, where a backslash
|
|
157
|
+
* is a string escape (`"../app\page"` resolves `../apppage`).
|
|
158
|
+
*/
|
|
159
|
+
export function componentPathFor(path: any, cwd: string, file: string): string {
|
|
160
|
+
return path
|
|
161
|
+
.relative(cwd, file)
|
|
162
|
+
.replace(/\.(tsx?|jsx?)$/, "")
|
|
163
|
+
.replace(/\\/g, "/");
|
|
164
|
+
}
|
|
165
|
+
|
|
144
166
|
/**
|
|
145
167
|
* Synchronously walk the route dir (`<appDirRel>` under cwd, e.g.
|
|
146
168
|
* `app` or `web/app`) and return one entry per discovered page, each
|
|
@@ -182,7 +204,7 @@ function discoverRoutes(
|
|
|
182
204
|
.map((n: string) => path.join(dir, n))
|
|
183
205
|
.find((p: string) => fs.existsSync(p));
|
|
184
206
|
const nextLayouts = layoutHere
|
|
185
|
-
? [...layouts, path
|
|
207
|
+
? [...layouts, componentPathFor(path, cwd, layoutHere)]
|
|
186
208
|
: layouts;
|
|
187
209
|
const pageHere = ["page.tsx", "page.ts", "page.jsx", "page.js"]
|
|
188
210
|
.map((n: string) => path.join(dir, n))
|
|
@@ -190,7 +212,7 @@ function discoverRoutes(
|
|
|
190
212
|
if (pageHere) {
|
|
191
213
|
pages.push({
|
|
192
214
|
segments: [...segments],
|
|
193
|
-
component: path
|
|
215
|
+
component: componentPathFor(path, cwd, pageHere),
|
|
194
216
|
layouts: nextLayouts,
|
|
195
217
|
// e.g. [] → "/", ["p","[slug]"] → "/p/[slug]".
|
|
196
218
|
pattern: "/" + segments.join("/"),
|
|
@@ -207,7 +229,7 @@ function discoverRoutes(
|
|
|
207
229
|
if (bHere) {
|
|
208
230
|
pages.push({
|
|
209
231
|
segments: [...segments],
|
|
210
|
-
component: path
|
|
232
|
+
component: componentPathFor(path, cwd, bHere),
|
|
211
233
|
layouts: nextLayouts,
|
|
212
234
|
});
|
|
213
235
|
}
|
|
@@ -255,9 +277,7 @@ export function discoverLoadingModules(
|
|
|
255
277
|
.map((n: string) => path.join(dir, n))
|
|
256
278
|
.find((p: string) => fs.existsSync(p));
|
|
257
279
|
if (here) {
|
|
258
|
-
found.push(
|
|
259
|
-
path.relative(cwd, here).replace(/\.(tsx?|jsx?)$/, "").replace(/\\/g, "/"),
|
|
260
|
-
);
|
|
280
|
+
found.push(componentPathFor(path, cwd, here));
|
|
261
281
|
}
|
|
262
282
|
for (const e of entries) {
|
|
263
283
|
if (!e.isDirectory()) continue;
|
|
@@ -1137,8 +1157,12 @@ function slugForComponent(component: string): string {
|
|
|
1137
1157
|
export interface PylonBundleManifest {
|
|
1138
1158
|
/** Build identity — bumps every successful build. */
|
|
1139
1159
|
build_id: string;
|
|
1140
|
-
/** Output root, relative to cwd
|
|
1160
|
+
/** Output root, relative to cwd: `.pylon/client-build`, or `client` in a
|
|
1161
|
+
* `pylon build` artifact. */
|
|
1141
1162
|
outdir: string;
|
|
1163
|
+
/** core-js polyfill bundle (relative to outdir), loaded before any route
|
|
1164
|
+
* entry. Present when `build.polyfill` is set and the targets need one. */
|
|
1165
|
+
polyfills?: string;
|
|
1142
1166
|
/** Public URL prefix the Rust host serves chunks under. */
|
|
1143
1167
|
public_prefix: string;
|
|
1144
1168
|
/** routeComponentPath → file + imports for that route. */
|
|
@@ -1196,7 +1220,7 @@ async function _prebuiltBundle(): Promise<BuildOutput | null> {
|
|
|
1196
1220
|
const pathMod: any = await import("node:path");
|
|
1197
1221
|
const fs = fsMod.default ?? fsMod;
|
|
1198
1222
|
const path = pathMod.default ?? pathMod;
|
|
1199
|
-
const outdir = path.join(process.cwd(),
|
|
1223
|
+
const outdir = path.join(process.cwd(), clientBuildDir());
|
|
1200
1224
|
const manifestPath = path.join(outdir, "manifest.json");
|
|
1201
1225
|
if (!fs.existsSync(manifestPath)) return null;
|
|
1202
1226
|
|
|
@@ -1257,6 +1281,48 @@ export async function buildClientBundle(
|
|
|
1257
1281
|
return _inflightBuild;
|
|
1258
1282
|
}
|
|
1259
1283
|
|
|
1284
|
+
/** Default client bundle directory, relative to the project root. */
|
|
1285
|
+
export const CLIENT_BUILD_DIR = ".pylon/client-build";
|
|
1286
|
+
|
|
1287
|
+
/** The client bundle directory, relative to cwd. A production server bundle
|
|
1288
|
+
* names its own; source mode uses `CLIENT_BUILD_DIR`. */
|
|
1289
|
+
export function clientBuildDir(): string {
|
|
1290
|
+
return serverBundle()?.clientDir ?? CLIENT_BUILD_DIR;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
export interface ClientBuildOptions {
|
|
1294
|
+
/** Absolute output directory. Default: `<cwd>/.pylon/client-build`. */
|
|
1295
|
+
outdir?: string;
|
|
1296
|
+
/** The manifest's `outdir` value. Default: `outdir` relative to cwd. A
|
|
1297
|
+
* `pylon build` artifact sets `client`, its path at run time. */
|
|
1298
|
+
manifestOutdir?: string;
|
|
1299
|
+
/** The `build` block to apply. Default: read from `pylon.manifest.json`,
|
|
1300
|
+
* except in dev (`NODE_ENV=development`), which applies none. `null`
|
|
1301
|
+
* applies none. */
|
|
1302
|
+
buildConfig?: ManifestBuildConfig | null;
|
|
1303
|
+
/** Fail on a CSS or font build error instead of shipping without it. */
|
|
1304
|
+
strict?: boolean;
|
|
1305
|
+
/** The app manifest (fonts, build settings). Default
|
|
1306
|
+
* `<cwd>/pylon.manifest.json`. */
|
|
1307
|
+
manifestPath?: string;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/** The `build` block of `<cwd>/pylon.manifest.json`, if any. */
|
|
1311
|
+
export function readManifestBuildConfig(
|
|
1312
|
+
fs: any,
|
|
1313
|
+
path: any,
|
|
1314
|
+
cwd: string,
|
|
1315
|
+
): ManifestBuildConfig | undefined {
|
|
1316
|
+
const manifestPath = path.join(cwd, "pylon.manifest.json");
|
|
1317
|
+
if (!fs.existsSync(manifestPath)) return undefined;
|
|
1318
|
+
try {
|
|
1319
|
+
const m = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
1320
|
+
return m && typeof m.build === "object" && m.build !== null ? m.build : undefined;
|
|
1321
|
+
} catch {
|
|
1322
|
+
return undefined;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1260
1326
|
async function _doBuild(appDirRel: string): Promise<BuildOutput> {
|
|
1261
1327
|
// node:* are available in Bun, but `globalThis.require` is
|
|
1262
1328
|
// not defined in ESM. Use dynamic import; Bun fast-paths these.
|
|
@@ -1288,6 +1354,7 @@ export async function buildTailwind(
|
|
|
1288
1354
|
cwd: string,
|
|
1289
1355
|
outdir: string,
|
|
1290
1356
|
appDirRel: string,
|
|
1357
|
+
cssTargets?: string[],
|
|
1291
1358
|
): Promise<string | null> {
|
|
1292
1359
|
const globalsPath = path.join(cwd, appDirRel, "globals.css");
|
|
1293
1360
|
if (!fs.existsSync(globalsPath)) return null;
|
|
@@ -1348,7 +1415,13 @@ export async function buildTailwind(
|
|
|
1348
1415
|
throw new Error(`tailwindcss build failed (exit ${exitCode}): ${err}`);
|
|
1349
1416
|
}
|
|
1350
1417
|
|
|
1351
|
-
|
|
1418
|
+
let out = fs.readFileSync(tmpPath, "utf8");
|
|
1419
|
+
if (cssTargets && cssTargets.length > 0) {
|
|
1420
|
+
// Hash the TRANSFORMED output (below), so a CSS target change changes
|
|
1421
|
+
// the file name.
|
|
1422
|
+
out = await transformCss(cwd, out, "globals.css", cssTargets);
|
|
1423
|
+
fs.writeFileSync(tmpPath, out, "utf8");
|
|
1424
|
+
}
|
|
1352
1425
|
let hash = 0;
|
|
1353
1426
|
for (let i = 0; i < out.length; i++) {
|
|
1354
1427
|
hash = (hash * 31 + out.charCodeAt(i)) >>> 0;
|
|
@@ -1381,12 +1454,27 @@ export async function buildTailwind(
|
|
|
1381
1454
|
return stylesName;
|
|
1382
1455
|
}
|
|
1383
1456
|
|
|
1384
|
-
|
|
1457
|
+
// Exported for the Windows-separator regression test (ssr-client-bundler.test.ts),
|
|
1458
|
+
// which injects a `path` whose `relative` returns "\"-separated paths.
|
|
1459
|
+
export async function _doBuildInner(
|
|
1385
1460
|
fs: any,
|
|
1386
1461
|
path: any,
|
|
1387
1462
|
cwd: string,
|
|
1388
1463
|
appDirRel: string,
|
|
1464
|
+
opts: ClientBuildOptions = {},
|
|
1389
1465
|
): Promise<BuildOutput> {
|
|
1466
|
+
// Production settings from the manifest's `build` block. Dev builds skip
|
|
1467
|
+
// them: lowering and polyfills cost build time and change nothing a modern
|
|
1468
|
+
// dev browser needs.
|
|
1469
|
+
const buildConfig =
|
|
1470
|
+
opts.buildConfig !== undefined
|
|
1471
|
+
? opts.buildConfig
|
|
1472
|
+
: process.env.NODE_ENV === "development"
|
|
1473
|
+
? undefined
|
|
1474
|
+
: readManifestBuildConfig(fs, path, cwd);
|
|
1475
|
+
const compat = resolveClientCompat(buildConfig ?? undefined);
|
|
1476
|
+
const manifestFile = opts.manifestPath ?? path.join(cwd, "pylon.manifest.json");
|
|
1477
|
+
const sourcemap = buildConfig?.sourcemap === true;
|
|
1390
1478
|
const routes = discoverRoutes(fs, path, cwd, appDirRel);
|
|
1391
1479
|
if (routes.length === 0) {
|
|
1392
1480
|
throw new Error(
|
|
@@ -1409,7 +1497,7 @@ async function _doBuildInner(
|
|
|
1409
1497
|
}
|
|
1410
1498
|
}
|
|
1411
1499
|
}
|
|
1412
|
-
const outdir = path.join(
|
|
1500
|
+
const outdir = opts.outdir ?? path.join(cwd, CLIENT_BUILD_DIR);
|
|
1413
1501
|
try {
|
|
1414
1502
|
fs.rmSync(outdir, { recursive: true, force: true });
|
|
1415
1503
|
} catch {
|
|
@@ -1508,7 +1596,7 @@ async function _doBuildInner(
|
|
|
1508
1596
|
target: "browser",
|
|
1509
1597
|
format: "esm",
|
|
1510
1598
|
minify: true,
|
|
1511
|
-
sourcemap: "none",
|
|
1599
|
+
sourcemap: sourcemap ? "linked" : "none",
|
|
1512
1600
|
splitting: true,
|
|
1513
1601
|
// The browser has no `process`. React, next-themes, sonner, and most
|
|
1514
1602
|
// npm UI deps reference `process.env.NODE_ENV` (and migrated Next code
|
|
@@ -1557,7 +1645,38 @@ async function _doBuildInner(
|
|
|
1557
1645
|
// - chunks (kind === "chunk") — looked up by basename when
|
|
1558
1646
|
// scanning entry files for static `import "./chunks/..."`
|
|
1559
1647
|
// specifiers.
|
|
1560
|
-
|
|
1648
|
+
// Manifest paths are URL paths under `public_prefix` ("/"-separated). The
|
|
1649
|
+
// runtime's /_pylon/build/ handler rejects any request with a backslash.
|
|
1650
|
+
const toUrlPath = (p: string) => p.replace(/\\/g, "/");
|
|
1651
|
+
const outdirRel =
|
|
1652
|
+
opts.manifestOutdir ?? toUrlPath(path.relative(cwd, outdir));
|
|
1653
|
+
|
|
1654
|
+
// Browser compatibility pass (build.target): lowers syntax, collects
|
|
1655
|
+
// polyfills, and renames the .js outputs. Later steps read the renamed
|
|
1656
|
+
// paths.
|
|
1657
|
+
let outputs: Array<{ path: string; kind: string }> = result.outputs.map(
|
|
1658
|
+
(o) => ({ path: o.path, kind: o.kind }),
|
|
1659
|
+
);
|
|
1660
|
+
let polyfills: string | undefined;
|
|
1661
|
+
if (compat && compat.jsTargets.length > 0) {
|
|
1662
|
+
const jsFiles = outputs
|
|
1663
|
+
.filter((o) => o.path.endsWith(".js"))
|
|
1664
|
+
.map((o) => toUrlPath(path.relative(outdir, o.path)));
|
|
1665
|
+
const res = await applyClientCompat({ fs, path, cwd, outdir, jsFiles, compat });
|
|
1666
|
+
if (res.unsupported.length > 0) {
|
|
1667
|
+
// eslint-disable-next-line no-console
|
|
1668
|
+
console.warn(
|
|
1669
|
+
`[pylon build] build.target includes browsers that cannot run the Pylon client runtime ` +
|
|
1670
|
+
`(it needs ES modules and dynamic import()): ${res.unsupported.join(", ")}`,
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
outputs = outputs.map((o) => {
|
|
1674
|
+
const rel = toUrlPath(path.relative(outdir, o.path));
|
|
1675
|
+
const next = res.renamed.get(rel);
|
|
1676
|
+
return next ? { ...o, path: path.join(outdir, next) } : o;
|
|
1677
|
+
});
|
|
1678
|
+
polyfills = res.polyfills;
|
|
1679
|
+
}
|
|
1561
1680
|
const entriesByStem = new Map<
|
|
1562
1681
|
string,
|
|
1563
1682
|
{ absPath: string; relPath: string }
|
|
@@ -1566,9 +1685,9 @@ async function _doBuildInner(
|
|
|
1566
1685
|
string,
|
|
1567
1686
|
{ absPath: string; relPath: string }
|
|
1568
1687
|
>();
|
|
1569
|
-
for (const o of
|
|
1688
|
+
for (const o of outputs) {
|
|
1570
1689
|
const absPath: string = o.path;
|
|
1571
|
-
const relPath = path.relative(outdir, absPath);
|
|
1690
|
+
const relPath = toUrlPath(path.relative(outdir, absPath));
|
|
1572
1691
|
const base = path.basename(absPath);
|
|
1573
1692
|
// Strip `-<hash>.js` to recover the entry source's stem
|
|
1574
1693
|
// (e.g. `client-entry-app__hello__page`). The hash is
|
|
@@ -1591,7 +1710,7 @@ async function _doBuildInner(
|
|
|
1591
1710
|
// the binary next to the entries AND into chunks/ so the runtime URL
|
|
1592
1711
|
// resolves from either an entry or a split chunk.
|
|
1593
1712
|
try {
|
|
1594
|
-
const referencesLoroWasm =
|
|
1713
|
+
const referencesLoroWasm = outputs.some((o) => {
|
|
1595
1714
|
if (!o.path.endsWith(".js")) return false;
|
|
1596
1715
|
try {
|
|
1597
1716
|
return fs.readFileSync(o.path, "utf8").includes("loro_wasm_bg.wasm");
|
|
@@ -1665,6 +1784,7 @@ async function _doBuildInner(
|
|
|
1665
1784
|
outdir: outdirRel,
|
|
1666
1785
|
public_prefix: "/_pylon/build/",
|
|
1667
1786
|
routes: {},
|
|
1787
|
+
...(polyfills ? { polyfills } : {}),
|
|
1668
1788
|
};
|
|
1669
1789
|
for (const r of routes) {
|
|
1670
1790
|
const slug = slugForComponent(r.component);
|
|
@@ -1718,7 +1838,7 @@ async function _doBuildInner(
|
|
|
1718
1838
|
// pylon.manifest.json from cwd. It's generated output — commonly
|
|
1719
1839
|
// gitignored — so when it's absent the bundle still builds but
|
|
1720
1840
|
// silently loses those features. Say so, loudly, instead.
|
|
1721
|
-
if (!fs.existsSync(
|
|
1841
|
+
if (!fs.existsSync(manifestFile)) {
|
|
1722
1842
|
// eslint-disable-next-line no-console
|
|
1723
1843
|
console.warn(
|
|
1724
1844
|
"[pylon ssr] pylon.manifest.json not found — building the client " +
|
|
@@ -1733,7 +1853,14 @@ async function _doBuildInner(
|
|
|
1733
1853
|
// array so SSR head injection emits `<link rel="stylesheet">`.
|
|
1734
1854
|
let stylesRel: string | null = null;
|
|
1735
1855
|
try {
|
|
1736
|
-
const styles = await buildTailwind(
|
|
1856
|
+
const styles = await buildTailwind(
|
|
1857
|
+
fs,
|
|
1858
|
+
path,
|
|
1859
|
+
cwd,
|
|
1860
|
+
outdir,
|
|
1861
|
+
appDirRel,
|
|
1862
|
+
compat?.cssTargets,
|
|
1863
|
+
);
|
|
1737
1864
|
if (styles) {
|
|
1738
1865
|
stylesRel = styles;
|
|
1739
1866
|
for (const r of Object.values(manifest.routes)) {
|
|
@@ -1752,6 +1879,7 @@ async function _doBuildInner(
|
|
|
1752
1879
|
/\u001b\[[0-9;]*m/g,
|
|
1753
1880
|
"",
|
|
1754
1881
|
);
|
|
1882
|
+
if (opts.strict) throw new Error(`CSS build failed: ${cssMsg}`);
|
|
1755
1883
|
manifest.css_error = cssMsg;
|
|
1756
1884
|
// eslint-disable-next-line no-console
|
|
1757
1885
|
console.warn(`[pylon ssr] tailwind compile failed: ${cssMsg}`);
|
|
@@ -1765,12 +1893,13 @@ async function _doBuildInner(
|
|
|
1765
1893
|
// the prebuilt artifact. A fetch/parse failure degrades to a variable-only
|
|
1766
1894
|
// entry — it never kills the build.
|
|
1767
1895
|
try {
|
|
1768
|
-
const declaredFonts = readManifestFonts(fs, path, cwd);
|
|
1896
|
+
const declaredFonts = readManifestFonts(fs, path, cwd, manifestFile);
|
|
1769
1897
|
if (declaredFonts.length > 0) {
|
|
1770
1898
|
const builtFonts = await buildFonts(fs, path, cwd, outdir, declaredFonts);
|
|
1771
1899
|
if (builtFonts) manifest.fonts = builtFonts;
|
|
1772
1900
|
}
|
|
1773
1901
|
} catch (fErr: any) {
|
|
1902
|
+
if (opts.strict) throw new Error(`font build failed: ${fErr?.message ?? fErr}`);
|
|
1774
1903
|
// eslint-disable-next-line no-console
|
|
1775
1904
|
console.warn(`[pylon ssr] font build failed: ${fErr?.message ?? fErr}`);
|
|
1776
1905
|
}
|
|
@@ -1846,7 +1975,7 @@ export async function getManifest(): Promise<PylonBundleManifest> {
|
|
|
1846
1975
|
const fs = fsMod.default ?? fsMod;
|
|
1847
1976
|
const path = pathMod.default ?? pathMod;
|
|
1848
1977
|
const cwd = process.cwd();
|
|
1849
|
-
const manifestPath = path.join(cwd,
|
|
1978
|
+
const manifestPath = path.join(cwd, clientBuildDir(), "manifest.json");
|
|
1850
1979
|
|
|
1851
1980
|
if (fs.existsSync(manifestPath)) {
|
|
1852
1981
|
const stat = fs.statSync(manifestPath);
|
package/src/ssr-fonts.ts
CHANGED
|
@@ -621,8 +621,8 @@ export function readManifestFonts(
|
|
|
621
621
|
fs: any,
|
|
622
622
|
path: any,
|
|
623
623
|
cwd: string,
|
|
624
|
+
manifestPath: string = path.join(cwd, "pylon.manifest.json"),
|
|
624
625
|
): ManifestFontInput[] {
|
|
625
|
-
const manifestPath = path.join(cwd, "pylon.manifest.json");
|
|
626
626
|
if (!fs.existsSync(manifestPath)) return [];
|
|
627
627
|
try {
|
|
628
628
|
const m = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
package/src/ssr-og-runtime.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { readFileSync } from "node:fs";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { serverBundle } from "./server-bundle";
|
|
28
29
|
|
|
29
30
|
// Satori's font descriptor.
|
|
30
31
|
export interface OgFont {
|
|
@@ -68,9 +69,15 @@ function loadResvg() {
|
|
|
68
69
|
if (!resvgReady) {
|
|
69
70
|
resvgReady = (async () => {
|
|
70
71
|
const mod = await import("@resvg/resvg-wasm");
|
|
71
|
-
// Locate the wasm binary next to the package entry
|
|
72
|
-
|
|
73
|
-
const
|
|
72
|
+
// Locate the wasm binary next to the package entry (or in the
|
|
73
|
+
// production server bundle) and init once.
|
|
74
|
+
const bundled = serverBundle()?.ogAssets.resvgWasm;
|
|
75
|
+
const wasmPath =
|
|
76
|
+
bundled ??
|
|
77
|
+
fileURLToPath(import.meta.resolve("@resvg/resvg-wasm")).replace(
|
|
78
|
+
/index\.[^/]+$/,
|
|
79
|
+
"index_bg.wasm",
|
|
80
|
+
);
|
|
74
81
|
await mod.initWasm(readFileSync(wasmPath));
|
|
75
82
|
return mod;
|
|
76
83
|
})();
|
|
@@ -83,18 +90,23 @@ function loadResvg() {
|
|
|
83
90
|
let defaultFonts: OgFont[] | null = null;
|
|
84
91
|
function loadDefaultFonts(): OgFont[] {
|
|
85
92
|
if (defaultFonts) return defaultFonts;
|
|
93
|
+
const bundled = serverBundle()?.ogAssets;
|
|
86
94
|
const read = (rel: string) =>
|
|
87
95
|
readFileSync(fileURLToPath(new URL(rel, import.meta.url)));
|
|
88
96
|
defaultFonts = [
|
|
89
97
|
{
|
|
90
98
|
name: "Inter",
|
|
91
|
-
data:
|
|
99
|
+
data: bundled
|
|
100
|
+
? readFileSync(bundled.interRegular)
|
|
101
|
+
: read("../assets/fonts/Inter-Regular.ttf"),
|
|
92
102
|
weight: 400,
|
|
93
103
|
style: "normal",
|
|
94
104
|
},
|
|
95
105
|
{
|
|
96
106
|
name: "Inter",
|
|
97
|
-
data:
|
|
107
|
+
data: bundled
|
|
108
|
+
? readFileSync(bundled.interSemiBold)
|
|
109
|
+
: read("../assets/fonts/Inter-SemiBold.ttf"),
|
|
98
110
|
weight: 600,
|
|
99
111
|
style: "normal",
|
|
100
112
|
},
|
package/src/ssr-runtime.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// the dispatch arm) so projects without SSR routes pay nothing — no
|
|
8
8
|
// react-dom dependency requirement, no startup cost.
|
|
9
9
|
|
|
10
|
+
import { moduleKey, serverBundle } from "./server-bundle";
|
|
11
|
+
|
|
10
12
|
// Bun runtime global. This module runs under Bun but is type-checked by
|
|
11
13
|
// consuming apps under node/DOM (no `Bun` global) — declare the surface used.
|
|
12
14
|
declare const Bun: {
|
|
@@ -716,8 +718,19 @@ export function renderMetadata(React: any, m: SsrMetadata | undefined): any {
|
|
|
716
718
|
|
|
717
719
|
const MODULE_EXTS = [".tsx", ".ts", ".jsx", ".js"];
|
|
718
720
|
|
|
719
|
-
/**
|
|
721
|
+
/**
|
|
722
|
+
* Import a project-relative module, trying each common extension. A
|
|
723
|
+
* production server bundle serves it from the bundle's module registry.
|
|
724
|
+
*/
|
|
720
725
|
export async function importModule(cwd: string, relPath: string): Promise<any> {
|
|
726
|
+
const bundle = serverBundle();
|
|
727
|
+
if (bundle) {
|
|
728
|
+
const load = bundle.modules[moduleKey(relPath)];
|
|
729
|
+
if (load === undefined) {
|
|
730
|
+
throw new Error(`module "${relPath}" is not in the server bundle`);
|
|
731
|
+
}
|
|
732
|
+
return load();
|
|
733
|
+
}
|
|
721
734
|
const base = `${cwd}/${relPath}`;
|
|
722
735
|
let lastErr: unknown = null;
|
|
723
736
|
for (const ext of MODULE_EXTS) {
|
|
@@ -730,6 +743,58 @@ export async function importModule(cwd: string, relPath: string): Promise<any> {
|
|
|
730
743
|
throw lastErr ?? new Error(`could not import module "${relPath}"`);
|
|
731
744
|
}
|
|
732
745
|
|
|
746
|
+
/**
|
|
747
|
+
* True when the project has a module at `<dir>/<name>` with any code
|
|
748
|
+
* extension. `dir` is project-relative and "/"-separated. A production server
|
|
749
|
+
* bundle answers from its module registry; source mode checks the disk.
|
|
750
|
+
*/
|
|
751
|
+
export function moduleExistsIn(
|
|
752
|
+
fs: any,
|
|
753
|
+
path: any,
|
|
754
|
+
cwd: string,
|
|
755
|
+
dir: string,
|
|
756
|
+
name: string,
|
|
757
|
+
): boolean {
|
|
758
|
+
const bundle = serverBundle();
|
|
759
|
+
if (bundle) return moduleKey(`${dir}/${name}`) in bundle.modules;
|
|
760
|
+
return MODULE_EXTS.some((ext) =>
|
|
761
|
+
fs.existsSync(path.join(cwd, dir, `${name}${ext}`)),
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* The route-group directories (`(name)`) directly under the project-relative
|
|
767
|
+
* `dir`, sorted by name. A production server bundle derives them from its
|
|
768
|
+
* module registry keys; source mode reads the directory.
|
|
769
|
+
*/
|
|
770
|
+
function routeGroupDirsIn(fs: any, path: any, cwd: string, dir: string): string[] {
|
|
771
|
+
const isGroup = (n: string) => n.startsWith("(") && n.endsWith(")");
|
|
772
|
+
const bundle = serverBundle();
|
|
773
|
+
if (bundle) {
|
|
774
|
+
const prefix = `${dir}/`;
|
|
775
|
+
const found = new Set<string>();
|
|
776
|
+
for (const key of Object.keys(bundle.modules)) {
|
|
777
|
+
if (!key.startsWith(prefix)) continue;
|
|
778
|
+
const child = key.slice(prefix.length).split("/")[0];
|
|
779
|
+
// A key with no "/" after the prefix is a module file, not a directory.
|
|
780
|
+
if (key.slice(prefix.length).includes("/") && isGroup(child)) {
|
|
781
|
+
found.add(child);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return [...found].sort();
|
|
785
|
+
}
|
|
786
|
+
let entries: any[];
|
|
787
|
+
try {
|
|
788
|
+
entries = fs.readdirSync(path.join(cwd, dir), { withFileTypes: true });
|
|
789
|
+
} catch {
|
|
790
|
+
return [];
|
|
791
|
+
}
|
|
792
|
+
return entries
|
|
793
|
+
.filter((e: any) => e.isDirectory() && isGroup(e.name))
|
|
794
|
+
.map((e: any) => e.name as string)
|
|
795
|
+
.sort();
|
|
796
|
+
}
|
|
797
|
+
|
|
733
798
|
/**
|
|
734
799
|
* Wrap a leaf element in its layout chain (leaf → root). Resolves ALL
|
|
735
800
|
* layouts first so a missing one fails before any chunk is emitted. Reused
|
|
@@ -829,25 +894,10 @@ function boundaryInDirOrGroups(
|
|
|
829
894
|
dir: string,
|
|
830
895
|
fileName: string,
|
|
831
896
|
): string | null {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
return `${dir}/${fileName}`;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
let entries: any[];
|
|
838
|
-
try {
|
|
839
|
-
entries = fs.readdirSync(path.join(cwd, dir), { withFileTypes: true });
|
|
840
|
-
} catch {
|
|
841
|
-
return null;
|
|
897
|
+
if (moduleExistsIn(fs, path, cwd, dir, fileName)) {
|
|
898
|
+
return `${dir}/${fileName}`;
|
|
842
899
|
}
|
|
843
|
-
const
|
|
844
|
-
.filter(
|
|
845
|
-
(e: any) =>
|
|
846
|
-
e.isDirectory() && e.name.startsWith("(") && e.name.endsWith(")"),
|
|
847
|
-
)
|
|
848
|
-
.map((e: any) => e.name as string)
|
|
849
|
-
.sort();
|
|
850
|
-
for (const g of groups) {
|
|
900
|
+
for (const g of routeGroupDirsIn(fs, path, cwd, dir)) {
|
|
851
901
|
const hit = boundaryInDirOrGroups(fs, path, cwd, `${dir}/${g}`, fileName);
|
|
852
902
|
if (hit) return hit;
|
|
853
903
|
}
|
|
@@ -892,8 +942,6 @@ function findColocatedImage(
|
|
|
892
942
|
return null;
|
|
893
943
|
}
|
|
894
944
|
|
|
895
|
-
const OG_IMAGE_CODE_EXTS = [".tsx", ".ts", ".jsx", ".js"];
|
|
896
|
-
|
|
897
945
|
/**
|
|
898
946
|
* Walk up from a page's directory to the nearest colocated dynamic OG
|
|
899
947
|
* module (`opengraph-image.{tsx,ts,jsx,js}`) and return the CONCRETE
|
|
@@ -938,14 +986,12 @@ function findColocatedOgImageRoute(
|
|
|
938
986
|
return below.filter((s) => !(s.startsWith("(") && s.endsWith(")"))).length;
|
|
939
987
|
};
|
|
940
988
|
while (dir && dir !== "." && dir !== "/") {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
return "/" + [...prefix, "opengraph-image"].join("/");
|
|
948
|
-
}
|
|
989
|
+
if (moduleExistsIn(fs, path, cwd, dir, "opengraph-image")) {
|
|
990
|
+
const prefix =
|
|
991
|
+
dir === pageDir && !isBoundary
|
|
992
|
+
? urlSegs
|
|
993
|
+
: urlSegs.slice(0, nonGroupDepth(dir));
|
|
994
|
+
return "/" + [...prefix, "opengraph-image"].join("/");
|
|
949
995
|
}
|
|
950
996
|
const slash = dir.lastIndexOf("/");
|
|
951
997
|
dir = slash >= 0 ? dir.slice(0, slash) : "";
|
|
@@ -2098,6 +2144,9 @@ export function buildHydrationTail(args: {
|
|
|
2098
2144
|
manifestRoute: { file: string; imports: string[]; css: string[] } | null;
|
|
2099
2145
|
publicPrefix: string;
|
|
2100
2146
|
manifestErr: string | null;
|
|
2147
|
+
/** core-js polyfill bundle (relative to `publicPrefix`), from the bundle
|
|
2148
|
+
* manifest. Loaded before the route entry. */
|
|
2149
|
+
polyfills?: string;
|
|
2101
2150
|
kind?: "error" | "not-found";
|
|
2102
2151
|
errorForClient?: { message: string; digest?: string };
|
|
2103
2152
|
// PPR Phase 0 (auth-bucketed caching): when set, this render is being stored
|
|
@@ -2181,6 +2230,11 @@ export function buildHydrationTail(args: {
|
|
|
2181
2230
|
// reusable (no double fetch) and surfaces load errors. No-op for the
|
|
2182
2231
|
// same-origin `/_pylon/build/` default.
|
|
2183
2232
|
const co = /^https?:\/\//i.test(args.publicPrefix) ? " crossorigin" : "";
|
|
2233
|
+
// Module scripts run in document order, so the polyfills are in place
|
|
2234
|
+
// before the entry (and the shared chunk it imports) runs.
|
|
2235
|
+
if (args.polyfills) {
|
|
2236
|
+
tail += `<script type="module"${co} src="${args.publicPrefix}${args.polyfills}"></script>`;
|
|
2237
|
+
}
|
|
2184
2238
|
tail += `<script type="module"${co} src="${args.publicPrefix}${args.manifestRoute.file}"></script>`;
|
|
2185
2239
|
} else {
|
|
2186
2240
|
// Executable script → the serialized message MUST be script-escaped
|
|
@@ -2258,11 +2312,8 @@ function resolveLayoutChain(componentRelPath: string, cwd: string): string[] {
|
|
|
2258
2312
|
let acc = "";
|
|
2259
2313
|
for (const part of parts) {
|
|
2260
2314
|
acc = acc ? `${acc}/${part}` : part;
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
layouts.push(`${acc}/layout`);
|
|
2264
|
-
break;
|
|
2265
|
-
}
|
|
2315
|
+
if (moduleExistsIn(fs, path, cwd, acc, "layout")) {
|
|
2316
|
+
layouts.push(`${acc}/layout`);
|
|
2266
2317
|
}
|
|
2267
2318
|
}
|
|
2268
2319
|
return layouts;
|
|
@@ -2321,6 +2372,7 @@ async function renderBoundaryToClient(
|
|
|
2321
2372
|
| { file: string; imports: string[]; css: string[] }
|
|
2322
2373
|
| null = null;
|
|
2323
2374
|
let publicPrefix = "/_pylon/build/";
|
|
2375
|
+
let polyfills: string | undefined;
|
|
2324
2376
|
let headBlob = "";
|
|
2325
2377
|
if (tail) {
|
|
2326
2378
|
try {
|
|
@@ -2328,6 +2380,7 @@ async function renderBoundaryToClient(
|
|
|
2328
2380
|
const manifest = await getManifest();
|
|
2329
2381
|
publicPrefix = manifest.public_prefix || publicPrefix;
|
|
2330
2382
|
manifestRoute = manifest.routes[tail.component] ?? null;
|
|
2383
|
+
polyfills = manifest.polyfills;
|
|
2331
2384
|
} catch {
|
|
2332
2385
|
manifestRoute = null;
|
|
2333
2386
|
}
|
|
@@ -2374,6 +2427,7 @@ async function renderBoundaryToClient(
|
|
|
2374
2427
|
manifestRoute,
|
|
2375
2428
|
publicPrefix,
|
|
2376
2429
|
manifestErr: null,
|
|
2430
|
+
polyfills,
|
|
2377
2431
|
kind: tail.kind,
|
|
2378
2432
|
errorForClient: tail.errorForClient,
|
|
2379
2433
|
});
|
|
@@ -3201,22 +3255,30 @@ export async function handleRenderRoute(
|
|
|
3201
3255
|
// `react-dom/server` (which is Node-stream-style). Try browser
|
|
3202
3256
|
// first, fall back to the default entry for environments that
|
|
3203
3257
|
// re-route it (Next runs a custom dist).
|
|
3258
|
+
//
|
|
3259
|
+
// A production server bundle carries the app's React, bundled with the
|
|
3260
|
+
// page modules, so there is nothing to resolve.
|
|
3261
|
+
const bundle = serverBundle();
|
|
3204
3262
|
let reactDomServerImport: any;
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
reactDomServerImport =
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3263
|
+
let reactImport: any;
|
|
3264
|
+
if (bundle) {
|
|
3265
|
+
reactDomServerImport = bundle.reactDomServer;
|
|
3266
|
+
reactImport = bundle.react;
|
|
3267
|
+
} else {
|
|
3268
|
+
try {
|
|
3269
|
+
// @ts-ignore — user-dep, resolved at runtime
|
|
3270
|
+
reactDomServerImport = await import(
|
|
3271
|
+
/* @vite-ignore */ resolveFromUser("react-dom/server.browser")
|
|
3272
|
+
);
|
|
3273
|
+
} catch {
|
|
3274
|
+
// @ts-ignore — user-dep, resolved at runtime
|
|
3275
|
+
reactDomServerImport = await import(
|
|
3276
|
+
/* @vite-ignore */ resolveFromUser("react-dom/server")
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3211
3279
|
// @ts-ignore — user-dep, resolved at runtime
|
|
3212
|
-
|
|
3213
|
-
/* @vite-ignore */ resolveFromUser("react-dom/server")
|
|
3214
|
-
);
|
|
3280
|
+
reactImport = await import(/* @vite-ignore */ resolveFromUser("react"));
|
|
3215
3281
|
}
|
|
3216
|
-
// @ts-ignore — user-dep, resolved at runtime
|
|
3217
|
-
const reactImport = await import(
|
|
3218
|
-
/* @vite-ignore */ resolveFromUser("react")
|
|
3219
|
-
);
|
|
3220
3282
|
React = reactImport.default ?? reactImport;
|
|
3221
3283
|
renderToReadableStream =
|
|
3222
3284
|
reactDomServerImport.renderToReadableStream ??
|
|
@@ -3700,11 +3762,13 @@ export async function handleRenderRoute(
|
|
|
3700
3762
|
| null = null;
|
|
3701
3763
|
let preloadManifestErr: string | null = null;
|
|
3702
3764
|
let preloadPublicPrefix = "/_pylon/build/";
|
|
3765
|
+
let preloadPolyfills: string | undefined;
|
|
3703
3766
|
try {
|
|
3704
3767
|
const { getManifest } = await import("./ssr-client-bundler");
|
|
3705
3768
|
const manifest = await getManifest();
|
|
3706
3769
|
preloadPublicPrefix = manifest.public_prefix || preloadPublicPrefix;
|
|
3707
3770
|
preloadManifestRoute = manifest.routes[msg.component] ?? null;
|
|
3771
|
+
preloadPolyfills = manifest.polyfills;
|
|
3708
3772
|
if (!preloadManifestRoute) {
|
|
3709
3773
|
preloadManifestErr = `manifest has no entry for "${msg.component}"`;
|
|
3710
3774
|
}
|
|
@@ -3866,6 +3930,7 @@ export async function handleRenderRoute(
|
|
|
3866
3930
|
manifestRoute: preloadManifestRoute,
|
|
3867
3931
|
publicPrefix: preloadPublicPrefix,
|
|
3868
3932
|
manifestErr: preloadManifestErr,
|
|
3933
|
+
polyfills: preloadPolyfills,
|
|
3869
3934
|
kind: isBoundaryComponent
|
|
3870
3935
|
? /(^|\/)error$/.test(msg.component)
|
|
3871
3936
|
? "error"
|