@pylonsync/functions 0.22.38 → 0.22.40
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/package.json
CHANGED
|
@@ -335,6 +335,66 @@ describe("ssr-client-bundler (Phase 1.5e)", () => {
|
|
|
335
335
|
// inlines; the build test guards the boundary wiring + not-found.tsx entry.
|
|
336
336
|
// ---------------------------------------------------------------------------
|
|
337
337
|
|
|
338
|
+
describe("imported assets", () => {
|
|
339
|
+
// loro-crdt's browser build imports its .wasm; Bun turns that into a URL
|
|
340
|
+
// string. It must be absolute under the build prefix and name a file the
|
|
341
|
+
// build wrote, or the page fetches it relative to its own path and 404s.
|
|
342
|
+
test("an imported .wasm gets an absolute URL that exists in the build", async () => {
|
|
343
|
+
tempDir = makeFixture(
|
|
344
|
+
{
|
|
345
|
+
"page.tsx": PAGE_BODY("Home"),
|
|
346
|
+
"hello/page.tsx": PAGE_BODY("Hello"),
|
|
347
|
+
"docs/[id]/page.tsx": `
|
|
348
|
+
import React from "react";
|
|
349
|
+
import wasmUrl from "./engine.wasm";
|
|
350
|
+
export default function Doc() {
|
|
351
|
+
return <div data-wasm={wasmUrl}>doc</div>;
|
|
352
|
+
}
|
|
353
|
+
`,
|
|
354
|
+
},
|
|
355
|
+
{ "layout.tsx": LAYOUT_BODY },
|
|
356
|
+
);
|
|
357
|
+
fs.writeFileSync(path.join(tempDir, "app", "docs", "[id]", "engine.wasm"), new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]));
|
|
358
|
+
originalCwd = process.cwd();
|
|
359
|
+
process.chdir(tempDir);
|
|
360
|
+
|
|
361
|
+
const { outdir, manifestPath } = await buildClientBundle();
|
|
362
|
+
|
|
363
|
+
const built = [
|
|
364
|
+
...fs.readdirSync(outdir).filter((n) => n.endsWith(".js")).map((n) => path.join(outdir, n)),
|
|
365
|
+
...fs.readdirSync(path.join(outdir, "chunks")).map((n) => path.join(outdir, "chunks", n)),
|
|
366
|
+
]
|
|
367
|
+
.map((f) => fs.readFileSync(f, "utf8"))
|
|
368
|
+
.join("\n");
|
|
369
|
+
const urls = built.match(/["'](\/_pylon\/build\/assets\/engine-[A-Za-z0-9]+\.wasm)["']/);
|
|
370
|
+
expect(urls).not.toBeNull();
|
|
371
|
+
const rel = urls![1].slice("/_pylon/build/".length);
|
|
372
|
+
expect(fs.existsSync(path.join(outdir, rel))).toBe(true);
|
|
373
|
+
// Every import between built files resolves to a file the build wrote,
|
|
374
|
+
// the way the browser resolves it (relative to the importing file).
|
|
375
|
+
const jsFiles = [
|
|
376
|
+
...fs.readdirSync(outdir).filter((n) => n.endsWith(".js")).map((n) => path.join(outdir, n)),
|
|
377
|
+
...fs.readdirSync(path.join(outdir, "chunks")).filter((n) => n.endsWith(".js")).map((n) => path.join(outdir, "chunks", n)),
|
|
378
|
+
];
|
|
379
|
+
for (const file of jsFiles) {
|
|
380
|
+
const src = fs.readFileSync(file, "utf8");
|
|
381
|
+
for (const m of src.matchAll(/(?:from\s*|import\s*\(?\s*)["']([^"']+\.js)["']/g)) {
|
|
382
|
+
const spec = m[1];
|
|
383
|
+
const target = spec.startsWith("/_pylon/build/")
|
|
384
|
+
? path.join(outdir, spec.slice("/_pylon/build/".length))
|
|
385
|
+
: path.resolve(path.dirname(file), spec);
|
|
386
|
+
expect(`${path.relative(outdir, file)} -> ${spec}: ${fs.existsSync(target)}`).toBe(`${path.relative(outdir, file)} -> ${spec}: true`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as PylonBundleManifest;
|
|
390
|
+
expect(manifest.public_prefix).toBe("/_pylon/build/");
|
|
391
|
+
for (const route of Object.values(manifest.routes)) {
|
|
392
|
+
expect(route.imports.length).toBeGreaterThan(0);
|
|
393
|
+
for (const imp of route.imports) expect(fs.existsSync(path.join(outdir, imp))).toBe(true);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
338
398
|
describe("nearestBoundaryComponent (client boundary resolution)", () => {
|
|
339
399
|
const routes = new Set([
|
|
340
400
|
"web/app/not-found",
|
|
@@ -89,6 +89,18 @@ type BunBuildOutput = {
|
|
|
89
89
|
logs?: Array<{ level: string; message: string }>;
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
/** The part of Bun's plugin builder the client build uses. */
|
|
93
|
+
type BunPluginBuild = {
|
|
94
|
+
onResolve(
|
|
95
|
+
opts: { filter: RegExp; namespace?: string },
|
|
96
|
+
callback: (args: { path: string; importer: string }) => void,
|
|
97
|
+
): void;
|
|
98
|
+
onLoad(
|
|
99
|
+
opts: { filter: RegExp; namespace?: string },
|
|
100
|
+
callback: (args: { path: string }) => { contents: string; loader: "js" },
|
|
101
|
+
): void;
|
|
102
|
+
};
|
|
103
|
+
|
|
92
104
|
declare const Bun: {
|
|
93
105
|
build(opts: {
|
|
94
106
|
entrypoints: string[];
|
|
@@ -111,12 +123,7 @@ declare const Bun: {
|
|
|
111
123
|
root?: string;
|
|
112
124
|
plugins?: Array<{
|
|
113
125
|
name: string;
|
|
114
|
-
setup(build:
|
|
115
|
-
onResolve(
|
|
116
|
-
opts: { filter: RegExp; namespace?: string },
|
|
117
|
-
callback: (args: { path: string; importer: string }) => void,
|
|
118
|
-
): void;
|
|
119
|
-
}): void;
|
|
126
|
+
setup(build: BunPluginBuild): void;
|
|
120
127
|
}>;
|
|
121
128
|
}): Promise<BunBuildOutput>;
|
|
122
129
|
file(path: string): { exists(): Promise<boolean> };
|
|
@@ -1644,12 +1651,15 @@ export async function _doBuildInner(
|
|
|
1644
1651
|
naming: {
|
|
1645
1652
|
entry: "[name]-[hash].js",
|
|
1646
1653
|
chunk: "chunks/[name]-[hash].js",
|
|
1647
|
-
|
|
1654
|
+
// Bun's `[ext]` has no leading dot.
|
|
1655
|
+
asset: "assets/[name]-[hash].[ext]",
|
|
1648
1656
|
},
|
|
1657
|
+
|
|
1649
1658
|
// Refuse to bundle a server-only module into a client-reachable page —
|
|
1650
1659
|
// secrets / server config in a page's import graph would otherwise ship
|
|
1651
1660
|
// to the browser.
|
|
1652
1661
|
plugins: [
|
|
1662
|
+
absoluteAssetPlugin(outdir, fs, path, (await import("node:crypto")).createHash),
|
|
1653
1663
|
{
|
|
1654
1664
|
name: "pylon-server-only",
|
|
1655
1665
|
setup(build) {
|
|
@@ -1811,7 +1821,7 @@ export async function _doBuildInner(
|
|
|
1811
1821
|
const manifest: PylonBundleManifest = {
|
|
1812
1822
|
build_id: makeBuildId(),
|
|
1813
1823
|
outdir: outdirRel,
|
|
1814
|
-
public_prefix:
|
|
1824
|
+
public_prefix: BUILD_PREFIX,
|
|
1815
1825
|
routes: {},
|
|
1816
1826
|
...(polyfills ? { polyfills } : {}),
|
|
1817
1827
|
};
|
|
@@ -2090,6 +2100,45 @@ function makeBuildId(): string {
|
|
|
2090
2100
|
* was pruned, breaking hydration). A trailing slash is enforced so
|
|
2091
2101
|
* `${prefix}${file}` always joins correctly.
|
|
2092
2102
|
*/
|
|
2103
|
+
/** Where the Rust host serves the client build (`/_pylon/build/<path>`). The
|
|
2104
|
+
* manifest's `public_prefix` and the bundler's `publicPath` must agree. */
|
|
2105
|
+
const BUILD_PREFIX = "/_pylon/build/";
|
|
2106
|
+
|
|
2107
|
+
/** Files a page can import as a URL (Bun's `file` loader). */
|
|
2108
|
+
const ASSET_RE = /\.(wasm|png|jpe?g|gif|webp|avif|ico|svg|woff2?|ttf|otf|mp3|mp4|webm|wav|ogg)$/i;
|
|
2109
|
+
|
|
2110
|
+
/**
|
|
2111
|
+
* Imported assets (a `.wasm`, an image) as absolute URLs under the build
|
|
2112
|
+
* prefix. Bun's own `file` loader exports a RELATIVE path ("assets/x.wasm"),
|
|
2113
|
+
* and code that fetches it resolves it against the page: loro-crdt's browser
|
|
2114
|
+
* build fetched /dashboard/orgs/acme/assets/loro_wasm_bg-….wasm and 404'd.
|
|
2115
|
+
* Bun's `publicPath` would make it absolute, but it also rewrites imports
|
|
2116
|
+
* between split chunks and drops their `chunks/` directory (Bun 1.3.14), so
|
|
2117
|
+
* this plugin writes the file itself, named by its content hash, and exports
|
|
2118
|
+
* the absolute URL.
|
|
2119
|
+
*/
|
|
2120
|
+
function absoluteAssetPlugin(
|
|
2121
|
+
outdir: string,
|
|
2122
|
+
fs: typeof import("node:fs"),
|
|
2123
|
+
path: typeof import("node:path"),
|
|
2124
|
+
createHash: typeof import("node:crypto").createHash,
|
|
2125
|
+
): { name: string; setup(build: BunPluginBuild): void } {
|
|
2126
|
+
return {
|
|
2127
|
+
name: "pylon-absolute-assets",
|
|
2128
|
+
setup(build) {
|
|
2129
|
+
build.onLoad({ filter: ASSET_RE }, (args) => {
|
|
2130
|
+
const bytes = fs.readFileSync(args.path);
|
|
2131
|
+
const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 12);
|
|
2132
|
+
const ext = path.extname(args.path);
|
|
2133
|
+
const rel = `assets/${path.basename(args.path, ext)}-${hash}${ext}`;
|
|
2134
|
+
fs.mkdirSync(path.join(outdir, "assets"), { recursive: true });
|
|
2135
|
+
fs.writeFileSync(path.join(outdir, rel), bytes);
|
|
2136
|
+
return { contents: `export default ${JSON.stringify(BUILD_PREFIX + rel)};`, loader: "js" };
|
|
2137
|
+
});
|
|
2138
|
+
},
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2093
2142
|
function resolvePublicPrefix(): string {
|
|
2094
2143
|
const raw = (process.env.PYLON_PUBLIC_PREFIX ?? "").trim();
|
|
2095
2144
|
if (!raw) return "/_pylon/build/";
|