Skip to content

Commit

Permalink
preload modules
Browse files Browse the repository at this point in the history
  • Loading branch information
codehz committed Aug 17, 2024
1 parent 9d17397 commit 8721d05
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 16 deletions.
14 changes: 11 additions & 3 deletions build.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Glob, fileURLToPath, pathToFileURL } from "bun";
import { Glob, Transpiler, fileURLToPath, pathToFileURL } from "bun";
import { basename, join, relative } from "node:path";

function escapeRegExp(string: string) {
Expand Down Expand Up @@ -98,16 +98,24 @@ export async function build({
],
});
if (result.success) {
const transpiler = new Transpiler({ loader: "js" });
const hashed: Record<string, string> = {};
const dependencies: Record<string, string[]> = {};
for (const output of result.outputs) {
const path = relative(outdir, output.path);
if (output.kind === "entry-point" && output.hash) {
const path = relative(outdir, output.path);
hashed[`/${path}`] = output.hash;
}
if (output.kind === "entry-point" || output.kind === "chunk") {
const imports = transpiler.scanImports(await output.text());
dependencies[`/${path}`] = imports
.filter((x) => x.kind === "import-statement")
.map((x) => "/" + join(path, "..", x.path));
}
}
Bun.write(
join(outdir, ".meta.json"),
JSON.stringify({ version: 1, hashed })
JSON.stringify({ version: 2, hashed, dependencies })
);
}
return result;
Expand Down
Binary file modified bun.lockb
Binary file not shown.
26 changes: 26 additions & 0 deletions example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,

"types": ["react/canary", "react-dom/canary"],

// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,

// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,

// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,

"paths": {
"bun-react-ssr": [".."],
"bun-react-ssr/*": ["../*"]
Expand Down
63 changes: 52 additions & 11 deletions index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { FileSystemRouter } from "bun";
import { NJSON } from "next-json";
import { statSync } from "node:fs";
import { join, relative } from "node:path";
import { preloadModule } from "react-dom";
import { renderToReadableStream } from "react-dom/server";
import { ClientOnlyError } from "./client";
import { MetaContext, PreloadModule } from "./preload";

export class StaticRouters {
readonly server: FileSystemRouter;
readonly client: FileSystemRouter;
readonly #routes: Map<string, string>;
readonly #routes_dump: string;
readonly #dependencies: Record<string, string[]>;
readonly #hashed: Record<string, string>;

constructor(
Expand All @@ -24,17 +28,19 @@ export class StaticRouters {
dir: join(baseDir, buildDir, pageDir),
style: "nextjs",
});
this.#hashed = require(join(baseDir, buildDir, ".meta.json")).hashed;
this.#routes_dump = NJSON.stringify(
Object.fromEntries(
Object.entries(this.client.routes).map(([path, filePath]) => {
let target = "/" + relative(join(baseDir, buildDir), filePath);
if (this.#hashed[target]) target += `?${this.#hashed[target]}`;
return [path, target];
})
),
{ omitStack: true }
const parsed = require(join(baseDir, buildDir, ".meta.json"));
this.#hashed = parsed.hashed;
this.#dependencies = parsed.dependencies;
this.#routes = new Map(
Object.entries(this.client.routes).map(([path, filePath]) => {
let target = "/" + relative(join(baseDir, buildDir), filePath);
if (this.#hashed[target]) target += `?${this.#hashed[target]}`;
return [path, target];
})
);
this.#routes_dump = NJSON.stringify(Object.fromEntries(this.#routes), {
omitStack: true,
});
}

async serve<T = void>(
Expand Down Expand Up @@ -101,7 +107,14 @@ export class StaticRouters {
}
const stream = await renderToReadableStream(
<Shell route={serverSide.pathname + search} {...staticProps} {...result}>
<module.default {...result?.props} />
<MetaContext.Provider
value={{ hash: this.#hashed, dependencies: this.#dependencies }}
>
<PreloadModule
module={this.#routes.get(serverSide.pathname)!.split("?")[0]}
/>
<module.default {...result?.props} />
</MetaContext.Provider>
</Shell>,
{
signal: request.signal,
Expand Down Expand Up @@ -140,6 +153,34 @@ export class StaticRouters {
}
}

function DirectPreloadModule({
target,
dependencies,
}: {
target: string;
dependencies: Record<string, string[]>;
}) {
preloadModule(target, { as: "script" });
preloadModule(target, { as: "script" });
for (const dep of walkDependencies(target, dependencies)) {
preloadModule(dep, { as: "script" });
preloadModule(dep, { as: "script" });
}
return null;
}

function* walkDependencies(
target: string,
dependencies: Record<string, string[]>
): Generator<string> {
if (dependencies[target]) {
for (const dep of dependencies[target]) {
yield dep;
yield* walkDependencies(dep, dependencies);
}
}
}

export async function serveFromDir(config: {
directory: string;
path: string;
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
},
"peerDependencies": {
"typescript": "^5.2.2",
"react": "^19.0.0-rc-cc1ec60d0d-20240607",
"react-dom": "^19.0.0-rc-cc1ec60d0d-20240607"
"react": "19.0.0-rc-1eaccd82-20240816",
"react-dom": "19.0.0-rc-1eaccd82-20240816"
},
"dependencies": {
"next-json": "^0.2.3"
Expand Down
43 changes: 43 additions & 0 deletions preload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createContext, use } from "react";
import { preloadModule, type PreloadModuleOptions } from "react-dom";

// @ignore
export const MetaContext = createContext<{
hash: Record<string, string>;
dependencies: Record<string, string[]>;
}>({ hash: {}, dependencies: {} });

function* walkDependencies(
target: string,
dependencies: Record<string, string[]>
): Generator<string> {
if (dependencies[target]) {
for (const dep of dependencies[target]) {
yield dep;
yield* walkDependencies(dep, dependencies);
}
}
}

function generateHashedName(name: string, hash: Record<string, string>) {
return hash[name] ? `${name}?${hash[name]}` : name;
}

export function PreloadModule({
module,
...options
}: { module: string } & Partial<PreloadModuleOptions>) {
if (typeof window === "undefined") {
try {
const meta = use(MetaContext);
preloadModule(generateHashedName(module, meta.hash), {
as: "script",
...options,
});
for (const dep of walkDependencies(module, meta.dependencies)) {
preloadModule(dep, { as: "script", ...options });
}
} catch {}
}
return null;
}
2 changes: 2 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"jsx": "react-jsx",
"allowJs": true,

"types": ["react/canary", "react-dom/canary"],

// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
Expand Down

0 comments on commit 8721d05

Please sign in to comment.