@pylonsync/functions 0.11.6 → 0.13.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 +31 -3
- 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 +75 -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 +3 -0
- package/src/production-build.test.ts +269 -0
- package/src/production-build.ts +590 -0
- package/src/runtime-shard-ticket.test.ts +137 -0
- package/src/runtime.ts +83 -82
- package/src/server-bundle.test.ts +89 -0
- package/src/server-bundle.ts +63 -0
- package/src/ssr-client-bundler.ts +125 -13
- 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 +81 -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;
|
|
@@ -1149,8 +1157,12 @@ function slugForComponent(component: string): string {
|
|
|
1149
1157
|
export interface PylonBundleManifest {
|
|
1150
1158
|
/** Build identity — bumps every successful build. */
|
|
1151
1159
|
build_id: string;
|
|
1152
|
-
/** Output root, relative to cwd
|
|
1160
|
+
/** Output root, relative to cwd: `.pylon/client-build`, or `client` in a
|
|
1161
|
+
* `pylon build` artifact. */
|
|
1153
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;
|
|
1154
1166
|
/** Public URL prefix the Rust host serves chunks under. */
|
|
1155
1167
|
public_prefix: string;
|
|
1156
1168
|
/** routeComponentPath → file + imports for that route. */
|
|
@@ -1208,7 +1220,7 @@ async function _prebuiltBundle(): Promise<BuildOutput | null> {
|
|
|
1208
1220
|
const pathMod: any = await import("node:path");
|
|
1209
1221
|
const fs = fsMod.default ?? fsMod;
|
|
1210
1222
|
const path = pathMod.default ?? pathMod;
|
|
1211
|
-
const outdir = path.join(process.cwd(),
|
|
1223
|
+
const outdir = path.join(process.cwd(), clientBuildDir());
|
|
1212
1224
|
const manifestPath = path.join(outdir, "manifest.json");
|
|
1213
1225
|
if (!fs.existsSync(manifestPath)) return null;
|
|
1214
1226
|
|
|
@@ -1269,6 +1281,48 @@ export async function buildClientBundle(
|
|
|
1269
1281
|
return _inflightBuild;
|
|
1270
1282
|
}
|
|
1271
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
|
+
|
|
1272
1326
|
async function _doBuild(appDirRel: string): Promise<BuildOutput> {
|
|
1273
1327
|
// node:* are available in Bun, but `globalThis.require` is
|
|
1274
1328
|
// not defined in ESM. Use dynamic import; Bun fast-paths these.
|
|
@@ -1300,6 +1354,7 @@ export async function buildTailwind(
|
|
|
1300
1354
|
cwd: string,
|
|
1301
1355
|
outdir: string,
|
|
1302
1356
|
appDirRel: string,
|
|
1357
|
+
cssTargets?: string[],
|
|
1303
1358
|
): Promise<string | null> {
|
|
1304
1359
|
const globalsPath = path.join(cwd, appDirRel, "globals.css");
|
|
1305
1360
|
if (!fs.existsSync(globalsPath)) return null;
|
|
@@ -1360,7 +1415,13 @@ export async function buildTailwind(
|
|
|
1360
1415
|
throw new Error(`tailwindcss build failed (exit ${exitCode}): ${err}`);
|
|
1361
1416
|
}
|
|
1362
1417
|
|
|
1363
|
-
|
|
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
|
+
}
|
|
1364
1425
|
let hash = 0;
|
|
1365
1426
|
for (let i = 0; i < out.length; i++) {
|
|
1366
1427
|
hash = (hash * 31 + out.charCodeAt(i)) >>> 0;
|
|
@@ -1400,7 +1461,20 @@ export async function _doBuildInner(
|
|
|
1400
1461
|
path: any,
|
|
1401
1462
|
cwd: string,
|
|
1402
1463
|
appDirRel: string,
|
|
1464
|
+
opts: ClientBuildOptions = {},
|
|
1403
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;
|
|
1404
1478
|
const routes = discoverRoutes(fs, path, cwd, appDirRel);
|
|
1405
1479
|
if (routes.length === 0) {
|
|
1406
1480
|
throw new Error(
|
|
@@ -1423,7 +1497,7 @@ export async function _doBuildInner(
|
|
|
1423
1497
|
}
|
|
1424
1498
|
}
|
|
1425
1499
|
}
|
|
1426
|
-
const outdir = path.join(
|
|
1500
|
+
const outdir = opts.outdir ?? path.join(cwd, CLIENT_BUILD_DIR);
|
|
1427
1501
|
try {
|
|
1428
1502
|
fs.rmSync(outdir, { recursive: true, force: true });
|
|
1429
1503
|
} catch {
|
|
@@ -1522,7 +1596,7 @@ export async function _doBuildInner(
|
|
|
1522
1596
|
target: "browser",
|
|
1523
1597
|
format: "esm",
|
|
1524
1598
|
minify: true,
|
|
1525
|
-
sourcemap: "none",
|
|
1599
|
+
sourcemap: sourcemap ? "linked" : "none",
|
|
1526
1600
|
splitting: true,
|
|
1527
1601
|
// The browser has no `process`. React, next-themes, sonner, and most
|
|
1528
1602
|
// npm UI deps reference `process.env.NODE_ENV` (and migrated Next code
|
|
@@ -1574,7 +1648,35 @@ export async function _doBuildInner(
|
|
|
1574
1648
|
// Manifest paths are URL paths under `public_prefix` ("/"-separated). The
|
|
1575
1649
|
// runtime's /_pylon/build/ handler rejects any request with a backslash.
|
|
1576
1650
|
const toUrlPath = (p: string) => p.replace(/\\/g, "/");
|
|
1577
|
-
const outdirRel =
|
|
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
|
+
}
|
|
1578
1680
|
const entriesByStem = new Map<
|
|
1579
1681
|
string,
|
|
1580
1682
|
{ absPath: string; relPath: string }
|
|
@@ -1583,7 +1685,7 @@ export async function _doBuildInner(
|
|
|
1583
1685
|
string,
|
|
1584
1686
|
{ absPath: string; relPath: string }
|
|
1585
1687
|
>();
|
|
1586
|
-
for (const o of
|
|
1688
|
+
for (const o of outputs) {
|
|
1587
1689
|
const absPath: string = o.path;
|
|
1588
1690
|
const relPath = toUrlPath(path.relative(outdir, absPath));
|
|
1589
1691
|
const base = path.basename(absPath);
|
|
@@ -1608,7 +1710,7 @@ export async function _doBuildInner(
|
|
|
1608
1710
|
// the binary next to the entries AND into chunks/ so the runtime URL
|
|
1609
1711
|
// resolves from either an entry or a split chunk.
|
|
1610
1712
|
try {
|
|
1611
|
-
const referencesLoroWasm =
|
|
1713
|
+
const referencesLoroWasm = outputs.some((o) => {
|
|
1612
1714
|
if (!o.path.endsWith(".js")) return false;
|
|
1613
1715
|
try {
|
|
1614
1716
|
return fs.readFileSync(o.path, "utf8").includes("loro_wasm_bg.wasm");
|
|
@@ -1682,6 +1784,7 @@ export async function _doBuildInner(
|
|
|
1682
1784
|
outdir: outdirRel,
|
|
1683
1785
|
public_prefix: "/_pylon/build/",
|
|
1684
1786
|
routes: {},
|
|
1787
|
+
...(polyfills ? { polyfills } : {}),
|
|
1685
1788
|
};
|
|
1686
1789
|
for (const r of routes) {
|
|
1687
1790
|
const slug = slugForComponent(r.component);
|
|
@@ -1735,7 +1838,7 @@ export async function _doBuildInner(
|
|
|
1735
1838
|
// pylon.manifest.json from cwd. It's generated output — commonly
|
|
1736
1839
|
// gitignored — so when it's absent the bundle still builds but
|
|
1737
1840
|
// silently loses those features. Say so, loudly, instead.
|
|
1738
|
-
if (!fs.existsSync(
|
|
1841
|
+
if (!fs.existsSync(manifestFile)) {
|
|
1739
1842
|
// eslint-disable-next-line no-console
|
|
1740
1843
|
console.warn(
|
|
1741
1844
|
"[pylon ssr] pylon.manifest.json not found — building the client " +
|
|
@@ -1750,7 +1853,14 @@ export async function _doBuildInner(
|
|
|
1750
1853
|
// array so SSR head injection emits `<link rel="stylesheet">`.
|
|
1751
1854
|
let stylesRel: string | null = null;
|
|
1752
1855
|
try {
|
|
1753
|
-
const styles = await buildTailwind(
|
|
1856
|
+
const styles = await buildTailwind(
|
|
1857
|
+
fs,
|
|
1858
|
+
path,
|
|
1859
|
+
cwd,
|
|
1860
|
+
outdir,
|
|
1861
|
+
appDirRel,
|
|
1862
|
+
compat?.cssTargets,
|
|
1863
|
+
);
|
|
1754
1864
|
if (styles) {
|
|
1755
1865
|
stylesRel = styles;
|
|
1756
1866
|
for (const r of Object.values(manifest.routes)) {
|
|
@@ -1769,6 +1879,7 @@ export async function _doBuildInner(
|
|
|
1769
1879
|
/\u001b\[[0-9;]*m/g,
|
|
1770
1880
|
"",
|
|
1771
1881
|
);
|
|
1882
|
+
if (opts.strict) throw new Error(`CSS build failed: ${cssMsg}`);
|
|
1772
1883
|
manifest.css_error = cssMsg;
|
|
1773
1884
|
// eslint-disable-next-line no-console
|
|
1774
1885
|
console.warn(`[pylon ssr] tailwind compile failed: ${cssMsg}`);
|
|
@@ -1782,12 +1893,13 @@ export async function _doBuildInner(
|
|
|
1782
1893
|
// the prebuilt artifact. A fetch/parse failure degrades to a variable-only
|
|
1783
1894
|
// entry — it never kills the build.
|
|
1784
1895
|
try {
|
|
1785
|
-
const declaredFonts = readManifestFonts(fs, path, cwd);
|
|
1896
|
+
const declaredFonts = readManifestFonts(fs, path, cwd, manifestFile);
|
|
1786
1897
|
if (declaredFonts.length > 0) {
|
|
1787
1898
|
const builtFonts = await buildFonts(fs, path, cwd, outdir, declaredFonts);
|
|
1788
1899
|
if (builtFonts) manifest.fonts = builtFonts;
|
|
1789
1900
|
}
|
|
1790
1901
|
} catch (fErr: any) {
|
|
1902
|
+
if (opts.strict) throw new Error(`font build failed: ${fErr?.message ?? fErr}`);
|
|
1791
1903
|
// eslint-disable-next-line no-console
|
|
1792
1904
|
console.warn(`[pylon ssr] font build failed: ${fErr?.message ?? fErr}`);
|
|
1793
1905
|
}
|
|
@@ -1863,7 +1975,7 @@ export async function getManifest(): Promise<PylonBundleManifest> {
|
|
|
1863
1975
|
const fs = fsMod.default ?? fsMod;
|
|
1864
1976
|
const path = pathMod.default ?? pathMod;
|
|
1865
1977
|
const cwd = process.cwd();
|
|
1866
|
-
const manifestPath = path.join(cwd,
|
|
1978
|
+
const manifestPath = path.join(cwd, clientBuildDir(), "manifest.json");
|
|
1867
1979
|
|
|
1868
1980
|
if (fs.existsSync(manifestPath)) {
|
|
1869
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"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Keeps stdout for the host protocol. See `fenceStdout`.
|
|
2
|
+
|
|
3
|
+
// Bun global used here. Declared locally so apps that type-check this source
|
|
4
|
+
// without bun-types still pass `tsc` (same pattern as runtime.ts).
|
|
5
|
+
declare const Bun: {
|
|
6
|
+
write(dest: unknown, data: string): unknown;
|
|
7
|
+
stderr: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Redirect console.* from user code to stderr so handlers can't accidentally
|
|
12
|
+
* emit a line that looks like a protocol frame and confuse the Rust reader.
|
|
13
|
+
*
|
|
14
|
+
* Before this guard, a handler calling `console.log('{"type":"return",...}')`
|
|
15
|
+
* — either intentionally or by logging an object shaped that way — would be
|
|
16
|
+
* parsed by the host as a real protocol message. Moving all console output
|
|
17
|
+
* to stderr keeps stdout reserved for NDJSON protocol frames only.
|
|
18
|
+
*
|
|
19
|
+
* The original console methods are saved on the console object as
|
|
20
|
+
* `__stdoutLog` etc. in case the runtime itself needs to write diagnostics
|
|
21
|
+
* to stdout for some reason (it currently doesn't).
|
|
22
|
+
*
|
|
23
|
+
* Safe to call more than once. A production server bundle calls it from its
|
|
24
|
+
* entry, before the app modules load, and the runtime's `main()` calls it
|
|
25
|
+
* again.
|
|
26
|
+
*/
|
|
27
|
+
export function fenceStdout(): void {
|
|
28
|
+
const c = globalThis.console as unknown as Record<string, unknown>;
|
|
29
|
+
if (c.__pylonFenced) return;
|
|
30
|
+
c.__pylonFenced = true;
|
|
31
|
+
const toStderr = (prefix: string) => (...args: unknown[]) => {
|
|
32
|
+
const line = args
|
|
33
|
+
.map((a) => {
|
|
34
|
+
if (typeof a === "string") return a;
|
|
35
|
+
// Error: JSON.stringify yields `{}` because message/stack are
|
|
36
|
+
// non-enumerable. That made `console.error("x:", err)` log as `x: {}`,
|
|
37
|
+
// hiding the real failure from operators. Unwrap by hand.
|
|
38
|
+
if (a instanceof Error) {
|
|
39
|
+
const parts = [a.stack || `${a.name}: ${a.message}`];
|
|
40
|
+
const code = (a as { code?: unknown }).code;
|
|
41
|
+
if (code !== undefined) parts.push(`code=${String(code)}`);
|
|
42
|
+
const cause = (a as { cause?: unknown }).cause;
|
|
43
|
+
if (cause !== undefined) {
|
|
44
|
+
try {
|
|
45
|
+
parts.push(`cause=${cause instanceof Error ? cause.stack || cause.message : JSON.stringify(cause)}`);
|
|
46
|
+
} catch {
|
|
47
|
+
parts.push(`cause=${String(cause)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return parts.join(" ");
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return JSON.stringify(a);
|
|
54
|
+
} catch {
|
|
55
|
+
return String(a);
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
.join(" ");
|
|
59
|
+
Bun.write(Bun.stderr, `${prefix}${line}\n`);
|
|
60
|
+
};
|
|
61
|
+
// Intentional: we want console.* for user handlers to go to stderr.
|
|
62
|
+
// Overwrite the globals before any user code is loaded.
|
|
63
|
+
c.__stdoutLog = c.log;
|
|
64
|
+
c.log = toStderr("");
|
|
65
|
+
c.info = toStderr("");
|
|
66
|
+
c.warn = toStderr("[warn] ");
|
|
67
|
+
c.error = toStderr("[error] ");
|
|
68
|
+
c.debug = toStderr("[debug] ");
|
|
69
|
+
}
|