From 19ba08ddd29bcdf470489f6a1720c5b0a4b1ef63 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 11 May 2023 23:04:34 +0900 Subject: [PATCH 01/37] wip: server bundling --- .swcrc | 3 +- cli.js | 11 +-- package.json | 1 - src/middleware/lib/rsc-renderer-worker.ts | 103 +++++++++++++++++----- src/middleware/lib/rsc-renderer.ts | 17 +--- src/middleware/rscDev.ts | 1 + src/node-loader.ts | 39 -------- src/router/server.ts | 5 +- src/types.d.ts | 2 +- website/tsconfig.json | 13 +++ 10 files changed, 103 insertions(+), 92 deletions(-) delete mode 100644 src/node-loader.ts create mode 100644 website/tsconfig.json diff --git a/.swcrc b/.swcrc index 889d091c2..60e144e72 100644 --- a/.swcrc +++ b/.swcrc @@ -4,6 +4,5 @@ "syntax": "typescript" }, "target": "esnext" - }, - "minify": true + } } diff --git a/cli.js b/cli.js index 727db1502..c592778cd 100755 --- a/cli.js +++ b/cli.js @@ -3,12 +3,5 @@ import { Worker } from "node:worker_threads"; const cmd = process.argv[2]; -process.env.WAKUWORK_CMD = cmd; -const execArgv = [ - ...(cmd === "dev" ? ["--experimental-loader", "tsx"] : []), - "--experimental-loader", - "wakuwork/node-loader", - "--experimental-loader", - "react-server-dom-webpack/node-loader", -]; -new Worker(new URL(`dist/cli-${cmd}.js`, import.meta.url), { execArgv }); +process.env.WAKUWORK_CMD = cmd; // TODO TEMP temporary solution +import(`./dist/cli-${cmd}.js`); diff --git a/package.json b/package.json index 57217cfdb..a0f74a704 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ }, "exports": { "./package.json": "./package.json", - "./node-loader": "./dist/node-loader.js", ".": { "types": "./dist/main.d.ts", "default": "./dist/main.js" diff --git a/src/middleware/lib/rsc-renderer-worker.ts b/src/middleware/lib/rsc-renderer-worker.ts index d3d4129e9..b15e48e49 100644 --- a/src/middleware/lib/rsc-renderer-worker.ts +++ b/src/middleware/lib/rsc-renderer-worker.ts @@ -1,10 +1,12 @@ import path from "node:path"; -import url from "node:url"; import { parentPort } from "node:worker_threads"; import { Writable } from "node:stream"; +import { createServer } from "vite"; +import type { Plugin } from "vite"; import { createElement } from "react"; import RSDWServer from "react-server-dom-webpack/server"; +import * as RSDWNodeLoader from "react-server-dom-webpack/node-loader"; import { transformRsfId } from "./rsc-utils.js"; import type { Input, MessageReq, MessageRes } from "./rsc-renderer.js"; @@ -12,6 +14,55 @@ import type { Config } from "../../config.js"; const { renderToPipeableStream } = RSDWServer; +const rscPlugin = (): Plugin => { + return { + name: "rsc-plugin", + async resolveId(id, importer, options) { + if (!id.endsWith(".js")) { + return id; + } + for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { + const resolved = await this.resolve(id.slice(0, -3) + ext, importer, { + ...options, + skipSelf: true, + }); + if (resolved) { + return resolved; + } + } + }, + async transform(code, id) { + const resolve = async ( + specifier: string, + { parentURL }: { parentURL: string } + ) => { + if (!specifier) { + return { url: "" }; + } + const url = (await this.resolve(specifier, parentURL, { + skipSelf: true, + }))!.id; + return { url }; + }; + const load = async (url: string) => { + let source = url === id ? code : (await this.load({ id: url })).code; + // HACK move directives before import statements. + source = source!.replace( + /^(import {.*?} from ".*?";)\s*"use (client|server)";/, + '"use $2";$1' + ); + return { format: "module", source }; + }; + RSDWNodeLoader.resolve( + "", + { conditions: ["react-server"], parentURL: "" }, + resolve + ); + return (await RSDWNodeLoader.load(id, null, load)).source; + }, + }; +}; + type PipeableStream = { pipe(destination: T): T; }; @@ -29,18 +80,30 @@ const dirFromConfig = { const dir = path.resolve(dirFromConfig || "."); const basePath = config.build?.basePath || "/"; // FIXME it's not build only const distPath = config.files?.dist || "dist"; -const entriesFile = url - .pathToFileURL( - path.join( - dir, - process.env.WAKUWORK_CMD === "build" ? distPath : "", - config.files?.entriesJs || "entries.js" - ) - ) - .toString(); +const entriesFile = path.join( + dir, + process.env.WAKUWORK_CMD === "build" ? distPath : "", + config.files?.entriesJs || "entries.js" +); + +const vitePromise = createServer({ + root: dir, + plugins: [rscPlugin()], + appType: "custom", +}); + +const loadEntries = async () => { + const vite = await vitePromise; + return vite.ssrLoadModule(entriesFile); +}; + +const loadServerFile = async (fname: string) => { + const vite = await vitePromise; + return vite.ssrLoadModule(fname); +}; const getFunctionComponent = async (rscId: string) => { - const { getEntry } = await import(entriesFile); + const { getEntry } = await loadEntries(); const mod = await getEntry(rscId); if (typeof mod === "function") { return mod; @@ -117,13 +180,13 @@ async function renderRSC( let clientEntries: Record | undefined; let serverEntries: Record | undefined; if (options.loadClientEntries) { - ({ clientEntries } = await import(entriesFile)); + ({ clientEntries } = await loadEntries()); if (!clientEntries) { throw new Error("Failed to load clientEntries"); } } if (options.loadServerEntries) { - ({ serverEntries } = await import(entriesFile)); + ({ serverEntries } = await loadEntries()); if (!serverEntries) { throw new Error("Failed to load serverEntries"); } @@ -150,13 +213,10 @@ async function renderRSC( let [id, name] = encodedId.split("#") as [string, string]; if (!id.startsWith("wakuwork/")) { id = path.relative( - "file://" + - encodeURI( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : "") - ), + path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : ""), id ); - id = basePath + getClientEntry(decodeURI(id)); + id = basePath + getClientEntry(id); } return [id, name]; }; @@ -200,7 +260,7 @@ async function renderRSC( if (input.rsfId && input.args) { const [fileId, name] = getServerEntry(input.rsfId).split("#"); const fname = path.join(dir, fileId!); - const mod = await import(fname); + const mod = await loadServerFile(fname); const data = await (mod[name!] || mod)(...input.args); if (!input.rscId) { return renderToPipeableStream(data, bundlerConfig); @@ -214,10 +274,7 @@ async function renderRSC( bundlerConfig ).pipe( transformRsfId( - "file://" + - encodeURI( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : "") - ), + path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : ""), registerServerEntry ) ); diff --git a/src/middleware/lib/rsc-renderer.ts b/src/middleware/lib/rsc-renderer.ts index 7063bbfdd..5733eeef6 100644 --- a/src/middleware/lib/rsc-renderer.ts +++ b/src/middleware/lib/rsc-renderer.ts @@ -15,21 +15,8 @@ type Options = { serverEntryCallback?: (rsfId: string, fileId: string) => void; }; -const execArgv = [ - "--conditions", - "react-server", - // TODO the use of process.env is temporary - ...(process.env.WAKUWORK_CMD === "dev" - ? ["--experimental-loader", "tsx"] - : []), - "--experimental-loader", - "wakuwork/node-loader", - "--experimental-loader", - "react-server-dom-webpack/node-loader", -]; - const worker = new Worker(new URL("rsc-renderer-worker.js", import.meta.url), { - execArgv, + execArgv: ["--conditions", "react-server"], }); export type MessageReq = { @@ -79,7 +66,7 @@ export function renderRSC(input: Input, options?: Options): Readable { input, loadClientEntries: options?.loadClientEntries, loadServerEntries: options?.loadServerEntries, - notifyServerEntry: !!options?.serverEntryCallback + notifyServerEntry: !!options?.serverEntryCallback, }; worker.postMessage(mesg); return passthrough; diff --git a/src/middleware/rscDev.ts b/src/middleware/rscDev.ts index 25a84b195..7b1d2c70a 100644 --- a/src/middleware/rscDev.ts +++ b/src/middleware/rscDev.ts @@ -20,6 +20,7 @@ const rscDev: MiddlewareCreator = (config, shared) => { (process.platform === "win32" ? "file://" : "") + path.join(dir, config.files?.entriesJs || "entries.js"); const prefetcher: Prefetcher = async (pathItem) => { + return {}; // TODO TEMP experimenting without prefetcher const mod = await import(entriesFile); return mod?.prefetcher(pathItem) ?? {}; }; diff --git a/src/node-loader.ts b/src/node-loader.ts deleted file mode 100644 index d93e8227f..000000000 --- a/src/node-loader.ts +++ /dev/null @@ -1,39 +0,0 @@ -export async function resolve( - specifier: string, - context: any, - nextResolve: any -) { - if (specifier.endsWith(".js")) { - // Hoped tsx handles it, but doesn't seem so. - for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { - try { - return await nextResolve( - specifier.slice(0, -3) + ext, - context, - nextResolve - ); - } catch (e) { - // ignored - } - } - } - return await nextResolve(specifier, context, nextResolve); -} - -export async function load(url: string, context: any, nextLoad: any) { - const result = await nextLoad(url, context, nextLoad); - if (result.format === "module") { - let { source } = result; - if (typeof source !== "string") { - source = source.toString(); - } - // HACK pull directive to the root - // Hope we can configure tsx to avoid this - const p = source.match(/(?:^|\n|;)("use (client|server)";)/); - if (p) { - source = p[1] + source; - } - return { ...result, source }; - } - return result; -} diff --git a/src/router/server.ts b/src/router/server.ts index a2389003d..7bc6e7e65 100644 --- a/src/router/server.ts +++ b/src/router/server.ts @@ -60,7 +60,7 @@ export function fileRouter(base: string) { return ( await Promise.all( modules.map(async ([fname, exportNames]) => { - const m = await import(fname); + const m = await import(/* @vite-ignore */ fname); return exportNames.flatMap((name) => { if (m[name]?.["$$typeof"] === CLIENT_REFERENCE) { return [m[name]]; @@ -91,7 +91,8 @@ export function fileRouter(base: string) { const getEntry: GetEntry = async (id) => { // This can be too unsecure? FIXME - const component = (await import(`${base}/${id}.js`)).default; + const component = (await import(/* @vite-ignore */ `${base}/${id}.js`)) + .default; const RouteComponent: any = (props: RouteProps) => { const componentProps: Record = {}; for (const [key, value] of new URLSearchParams(props.search)) { diff --git a/src/types.d.ts b/src/types.d.ts index a2d043aab..83441e7ee 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,4 +1,4 @@ -declare module "react-server-dom-webpack/node-register"; +declare module "react-server-dom-webpack/node-loader"; declare module "react-server-dom-webpack/server"; declare module "react-server-dom-webpack/server.node.unbundled"; declare module "react-server-dom-webpack/client"; diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 000000000..7781cf1ae --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "target": "esnext", + "downlevelIteration": true, + "esModuleInterop": true, + "module": "nodenext", + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "jsx": "react-jsx" + } +} From 08a4ef75e13641988bd0ecadad36918419a170da Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 14 May 2023 13:06:03 +0900 Subject: [PATCH 02/37] rename rsc-renderer to rsc-handler --- src/builder.ts | 4 ++-- ...{rsc-renderer-worker.ts => rsc-handler-worker.ts} | 2 +- .../lib/{rsc-renderer.ts => rsc-handler.ts} | 12 ++++++------ src/middleware/rscDev.ts | 4 ++-- src/middleware/rscPrd.ts | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) rename src/middleware/lib/{rsc-renderer-worker.ts => rsc-handler-worker.ts} (99%) rename src/middleware/lib/{rsc-renderer.ts => rsc-handler.ts} (86%) diff --git a/src/builder.ts b/src/builder.ts index 0f854eb88..3f2d734ba 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -10,11 +10,11 @@ import * as swc from "@swc/core"; import type { Config } from "./config.js"; import type { GetEntry, Prefetcher, Prerenderer } from "./server.js"; import { generatePrefetchCode } from "./middleware/lib/rsc-utils.js"; -import { renderRSC } from "./middleware/lib/rsc-renderer.js"; +import { renderRSC } from "./middleware/lib/rsc-handler.js"; const CLIENT_REFERENCE = Symbol.for("react.client.reference"); -// TODO we have duplicate code here and rscPrd.ts and rsc-renderers*.ts +// TODO we have duplicate code here and rscPrd.ts and rsc-handler*.ts const rscPlugin = (): Plugin => { const code = ` diff --git a/src/middleware/lib/rsc-renderer-worker.ts b/src/middleware/lib/rsc-handler-worker.ts similarity index 99% rename from src/middleware/lib/rsc-renderer-worker.ts rename to src/middleware/lib/rsc-handler-worker.ts index b15e48e49..d36b8b5eb 100644 --- a/src/middleware/lib/rsc-renderer-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -9,7 +9,7 @@ import RSDWServer from "react-server-dom-webpack/server"; import * as RSDWNodeLoader from "react-server-dom-webpack/node-loader"; import { transformRsfId } from "./rsc-utils.js"; -import type { Input, MessageReq, MessageRes } from "./rsc-renderer.js"; +import type { Input, MessageReq, MessageRes } from "./rsc-handler.js"; import type { Config } from "../../config.js"; const { renderToPipeableStream } = RSDWServer; diff --git a/src/middleware/lib/rsc-renderer.ts b/src/middleware/lib/rsc-handler.ts similarity index 86% rename from src/middleware/lib/rsc-renderer.ts rename to src/middleware/lib/rsc-handler.ts index 5733eeef6..b8a76eab5 100644 --- a/src/middleware/lib/rsc-renderer.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -15,7 +15,7 @@ type Options = { serverEntryCallback?: (rsfId: string, fileId: string) => void; }; -const worker = new Worker(new URL("rsc-renderer-worker.js", import.meta.url), { +const worker = new Worker(new URL("rsc-handler-worker.js", import.meta.url), { execArgv: ["--conditions", "react-server"], }); @@ -34,10 +34,10 @@ export type MessageRes = | { id: number; type: "err"; err: unknown } | { id: number; type: "serverEntry"; rsfId: string; fileId: string }; -const handlers = new Map void>(); +const messageCallbacks = new Map void>(); worker.on("message", (mesg: MessageRes) => { - handlers.get(mesg.id)?.(mesg); + messageCallbacks.get(mesg.id)?.(mesg); }); let nextId = 1; @@ -45,17 +45,17 @@ let nextId = 1; export function renderRSC(input: Input, options?: Options): Readable { const id = nextId++; const passthrough = new PassThrough(); - handlers.set(id, (mesg) => { + messageCallbacks.set(id, (mesg) => { if (mesg.type === "buf") { passthrough.write(Buffer.from(mesg.buf, mesg.offset, mesg.len)); } else if (mesg.type === "end") { passthrough.end(); - handlers.delete(id); + messageCallbacks.delete(id); } else if (mesg.type === "err") { passthrough.destroy( mesg.err instanceof Error ? mesg.err : new Error(String(mesg.err)) ); - handlers.delete(id); + messageCallbacks.delete(id); } else if (mesg.type === "serverEntry" && options?.serverEntryCallback) { options.serverEntryCallback(mesg.rsfId, mesg.fileId); } diff --git a/src/middleware/rscDev.ts b/src/middleware/rscDev.ts index 7b1d2c70a..b78077041 100644 --- a/src/middleware/rscDev.ts +++ b/src/middleware/rscDev.ts @@ -7,7 +7,7 @@ import type { MiddlewareCreator } from "./lib/common.js"; import type { Prefetcher } from "../server.js"; import { generatePrefetchCode } from "./lib/rsc-utils.js"; -import { renderRSC } from "./lib/rsc-renderer.js"; +import { renderRSC } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; @@ -49,7 +49,7 @@ globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.ge const [id] = decodeId(m["$$id"]); moduleIds.push(id); } - code += generatePrefetchCode?.(entryItems, moduleIds) || ""; + code += generatePrefetchCode(entryItems, moduleIds); return code; }; diff --git a/src/middleware/rscPrd.ts b/src/middleware/rscPrd.ts index 095da3da1..0dfbe64dc 100644 --- a/src/middleware/rscPrd.ts +++ b/src/middleware/rscPrd.ts @@ -6,13 +6,13 @@ import busboy from "busboy"; import type { MiddlewareCreator } from "./lib/common.js"; import type { Prefetcher } from "../server.js"; import { generatePrefetchCode } from "./lib/rsc-utils.js"; -import { renderRSC } from "./lib/rsc-renderer.js"; +import { renderRSC } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; const CLIENT_REFERENCE = Symbol.for("react.client.reference"); -// TODO we have duplicate code here and rsc-renderer-worker.ts +// TODO we have duplicate code here and rsc-handler-worker.ts const rscPrd: MiddlewareCreator = (config, shared) => { const dir = path.resolve(config.prdServer?.dir || "."); From fc0f8fe04bba6b990b0213960b29ce032736d640 Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 14 May 2023 13:34:00 +0900 Subject: [PATCH 03/37] refactor rsc-plugin --- src/middleware/lib/rsc-handler-worker.ts | 165 ++++++++--------------- src/middleware/lib/rsc-handler.ts | 23 ++-- src/middleware/lib/vite-rsc-plugin.ts | 51 +++++++ 3 files changed, 122 insertions(+), 117 deletions(-) create mode 100644 src/middleware/lib/vite-rsc-plugin.ts diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index d36b8b5eb..b6f02b327 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -3,118 +3,17 @@ import { parentPort } from "node:worker_threads"; import { Writable } from "node:stream"; import { createServer } from "vite"; -import type { Plugin } from "vite"; import { createElement } from "react"; import RSDWServer from "react-server-dom-webpack/server"; -import * as RSDWNodeLoader from "react-server-dom-webpack/node-loader"; import { transformRsfId } from "./rsc-utils.js"; -import type { Input, MessageReq, MessageRes } from "./rsc-handler.js"; +import type { RenderInput, MessageReq, MessageRes } from "./rsc-handler.js"; import type { Config } from "../../config.js"; +import { rscPlugin } from "./vite-rsc-plugin.js"; const { renderToPipeableStream } = RSDWServer; -const rscPlugin = (): Plugin => { - return { - name: "rsc-plugin", - async resolveId(id, importer, options) { - if (!id.endsWith(".js")) { - return id; - } - for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { - const resolved = await this.resolve(id.slice(0, -3) + ext, importer, { - ...options, - skipSelf: true, - }); - if (resolved) { - return resolved; - } - } - }, - async transform(code, id) { - const resolve = async ( - specifier: string, - { parentURL }: { parentURL: string } - ) => { - if (!specifier) { - return { url: "" }; - } - const url = (await this.resolve(specifier, parentURL, { - skipSelf: true, - }))!.id; - return { url }; - }; - const load = async (url: string) => { - let source = url === id ? code : (await this.load({ id: url })).code; - // HACK move directives before import statements. - source = source!.replace( - /^(import {.*?} from ".*?";)\s*"use (client|server)";/, - '"use $2";$1' - ); - return { format: "module", source }; - }; - RSDWNodeLoader.resolve( - "", - { conditions: ["react-server"], parentURL: "" }, - resolve - ); - return (await RSDWNodeLoader.load(id, null, load)).source; - }, - }; -}; - -type PipeableStream = { - pipe(destination: T): T; -}; - -// TODO use of process.env is all temporary -// TODO these are temporary -const config: Config = - (process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG)) || - {}; -const dirFromConfig = { - dev: config.devServer?.dir, - build: config.build?.dir, - start: config.prdServer?.dir, -}[String(process.env.WAKUWORK_CMD)]; -const dir = path.resolve(dirFromConfig || "."); -const basePath = config.build?.basePath || "/"; // FIXME it's not build only -const distPath = config.files?.dist || "dist"; -const entriesFile = path.join( - dir, - process.env.WAKUWORK_CMD === "build" ? distPath : "", - config.files?.entriesJs || "entries.js" -); - -const vitePromise = createServer({ - root: dir, - plugins: [rscPlugin()], - appType: "custom", -}); - -const loadEntries = async () => { - const vite = await vitePromise; - return vite.ssrLoadModule(entriesFile); -}; - -const loadServerFile = async (fname: string) => { - const vite = await vitePromise; - return vite.ssrLoadModule(fname); -}; - -const getFunctionComponent = async (rscId: string) => { - const { getEntry } = await loadEntries(); - const mod = await getEntry(rscId); - if (typeof mod === "function") { - return mod; - } - if (typeof mod.default === "function") { - return mod.default; - } - throw new Error("No function component found"); -}; - -parentPort!.on("message", async (mesg: MessageReq) => { +const handleRender = async (mesg: MessageReq & { type: "render" }) => { const { id, input, loadClientEntries, loadServerEntries, notifyServerEntry } = mesg; try { @@ -167,10 +66,62 @@ parentPort!.on("message", async (mesg: MessageReq) => { }; parentPort!.postMessage(mesg); } +}; + +parentPort!.on("message", (mesg: MessageReq) => { + if ((mesg.type = "render")) { + handleRender(mesg); + } +}); + +type PipeableStream = { + pipe(destination: T): T; +}; + +// TODO use of process.env is all temporary +// TODO these are temporary +const config: Config = + (process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG)) || + {}; +const dirFromConfig = { + dev: config.devServer?.dir, + build: config.build?.dir, + start: config.prdServer?.dir, +}[String(process.env.WAKUWORK_CMD)]; +const dir = path.resolve(dirFromConfig || "."); +const basePath = config.build?.basePath || "/"; // FIXME it's not build only +const distPath = config.files?.dist || "dist"; +const entriesFile = path.join( + dir, + process.env.WAKUWORK_CMD === "build" ? distPath : "", + config.files?.entriesJs || "entries.js" +); + +const vitePromise = createServer({ + root: dir, + plugins: [rscPlugin()], + appType: "custom", }); +const loadServerFile = async (fname: string) => { + const vite = await vitePromise; + return vite.ssrLoadModule(fname); +}; + +const getFunctionComponent = async (rscId: string) => { + const { getEntry } = await loadServerFile(entriesFile); + const mod = await getEntry(rscId); + if (typeof mod === "function") { + return mod; + } + if (typeof mod.default === "function") { + return mod.default; + } + throw new Error("No function component found"); +}; + async function renderRSC( - input: Input, + input: RenderInput, options: { loadClientEntries: boolean | undefined; loadServerEntries: boolean | undefined; @@ -180,13 +131,13 @@ async function renderRSC( let clientEntries: Record | undefined; let serverEntries: Record | undefined; if (options.loadClientEntries) { - ({ clientEntries } = await loadEntries()); + ({ clientEntries } = await loadServerFile(entriesFile)); if (!clientEntries) { throw new Error("Failed to load clientEntries"); } } if (options.loadServerEntries) { - ({ serverEntries } = await loadEntries()); + ({ serverEntries } = await loadServerFile(entriesFile)); if (!serverEntries) { throw new Error("Failed to load serverEntries"); } diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index b8a76eab5..b41d5962e 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -2,27 +2,27 @@ import { PassThrough } from "node:stream"; import type { Readable } from "node:stream"; import { Worker } from "node:worker_threads"; -export type Input = { +const worker = new Worker(new URL("rsc-handler-worker.js", import.meta.url), { + execArgv: ["--conditions", "react-server"], +}); + +export type RenderInput = { rscId?: string | undefined; props?: Props | undefined; rsfId?: string | undefined; args?: unknown[] | undefined; }; -type Options = { +type RenderOptions = { loadClientEntries?: boolean; loadServerEntries?: boolean; serverEntryCallback?: (rsfId: string, fileId: string) => void; }; -const worker = new Worker(new URL("rsc-handler-worker.js", import.meta.url), { - execArgv: ["--conditions", "react-server"], -}); - export type MessageReq = { id: number; - type: "start"; - input: Input; + type: "render"; + input: RenderInput; loadClientEntries: boolean | undefined; loadServerEntries: boolean | undefined; notifyServerEntry: boolean; @@ -42,7 +42,10 @@ worker.on("message", (mesg: MessageRes) => { let nextId = 1; -export function renderRSC(input: Input, options?: Options): Readable { +export function renderRSC( + input: RenderInput, + options?: RenderOptions +): Readable { const id = nextId++; const passthrough = new PassThrough(); messageCallbacks.set(id, (mesg) => { @@ -62,7 +65,7 @@ export function renderRSC(input: Input, options?: Options): Readable { }); const mesg: MessageReq = { id, - type: "start", + type: "render", input, loadClientEntries: options?.loadClientEntries, loadServerEntries: options?.loadServerEntries, diff --git a/src/middleware/lib/vite-rsc-plugin.ts b/src/middleware/lib/vite-rsc-plugin.ts new file mode 100644 index 000000000..f6c7044fb --- /dev/null +++ b/src/middleware/lib/vite-rsc-plugin.ts @@ -0,0 +1,51 @@ +import type { Plugin } from "vite"; +import * as RSDWNodeLoader from "react-server-dom-webpack/node-loader"; + +export const rscPlugin = (): Plugin => { + return { + name: "rsc-plugin", + async resolveId(id, importer, options) { + if (!id.endsWith(".js")) { + return id; + } + for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { + const resolved = await this.resolve(id.slice(0, -3) + ext, importer, { + ...options, + skipSelf: true, + }); + if (resolved) { + return resolved; + } + } + }, + async transform(code, id) { + const resolve = async ( + specifier: string, + { parentURL }: { parentURL: string } + ) => { + if (!specifier) { + return { url: "" }; + } + const url = (await this.resolve(specifier, parentURL, { + skipSelf: true, + }))!.id; + return { url }; + }; + const load = async (url: string) => { + let source = url === id ? code : (await this.load({ id: url })).code; + // HACK move directives before import statements. + source = source!.replace( + /^(import {.*?} from ".*?";)\s*"use (client|server)";/, + '"use $2";$1' + ); + return { format: "module", source }; + }; + RSDWNodeLoader.resolve( + "", + { conditions: ["react-server"], parentURL: "" }, + resolve + ); + return (await RSDWNodeLoader.load(id, null, load)).source; + }, + }; +}; From 53aab622ed268889f0726f17b82c4427fb05a09a Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 14 May 2023 14:23:52 +0900 Subject: [PATCH 04/37] prefetcher in dev --- src/middleware/lib/rsc-handler-worker.ts | 87 +++++++++++++++++++++++- src/middleware/lib/rsc-handler.ts | 51 +++++++++++--- src/middleware/rscDev.ts | 51 +++----------- 3 files changed, 135 insertions(+), 54 deletions(-) diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index b6f02b327..7ff2388e1 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -6,12 +6,13 @@ import { createServer } from "vite"; import { createElement } from "react"; import RSDWServer from "react-server-dom-webpack/server"; -import { transformRsfId } from "./rsc-utils.js"; +import { transformRsfId, generatePrefetchCode } from "./rsc-utils.js"; import type { RenderInput, MessageReq, MessageRes } from "./rsc-handler.js"; import type { Config } from "../../config.js"; import { rscPlugin } from "./vite-rsc-plugin.js"; const { renderToPipeableStream } = RSDWServer; +const CLIENT_REFERENCE = Symbol.for("react.client.reference"); const handleRender = async (mesg: MessageReq & { type: "render" }) => { const { id, input, loadClientEntries, loadServerEntries, notifyServerEntry } = @@ -68,9 +69,33 @@ const handleRender = async (mesg: MessageReq & { type: "render" }) => { } }; +const handlePrefetcher = async (mesg: MessageReq & { type: "prefetcher" }) => { + const { id, pathItem, loadClientEntries } = mesg; + try { + const code = await prefetcherRSC(pathItem, { + loadClientEntries, + }); + const mesg: MessageRes = { + id, + type: "prefetcher", + code, + }; + parentPort!.postMessage(mesg); + } catch (err) { + const mesg: MessageRes = { + id, + type: "err", + err, + }; + parentPort!.postMessage(mesg); + } +}; + parentPort!.on("message", (mesg: MessageReq) => { - if ((mesg.type = "render")) { + if (mesg.type === "render") { handleRender(mesg); + } else if (mesg.type === "prefetcher") { + handlePrefetcher(mesg); } }); @@ -232,3 +257,61 @@ async function renderRSC( } throw new Error("Unexpected input"); } + +async function prefetcherRSC( + pathItem: string, + options: { + loadClientEntries: boolean | undefined; + } +): Promise { + let code = ""; + + // TOOD duplicated code with renderRSC + let clientEntries: Record | undefined; + if (options.loadClientEntries) { + ({ clientEntries } = await loadServerFile(entriesFile)); + if (!clientEntries) { + throw new Error("Failed to load clientEntries"); + } + } + const getClientEntry = (id: string) => { + if (!clientEntries) { + return id; + } + const clientEntry = + clientEntries[id] || + clientEntries[id.replace(/\.js$/, ".ts")] || + clientEntries[id.replace(/\.js$/, ".tsx")] || + clientEntries[id.replace(/\.js$/, ".jsx")]; + if (!clientEntry) { + throw new Error("No client entry found"); + } + return clientEntry; + }; + const decodeId = (encodedId: string): [id: string, name: string] => { + let [id, name] = encodedId.split("#") as [string, string]; + if (!id.startsWith("wakuwork/")) { + id = path.relative( + path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : ""), + id + ); + id = basePath + getClientEntry(id); + } + return [id, name]; + }; + + const { prefetcher } = await loadServerFile(entriesFile); + const { entryItems = [], clientModules = [] } = prefetcher + ? await prefetcher(pathItem) + : {}; + const moduleIds: string[] = []; + for (const m of clientModules as any[]) { + if (m["$$typeof"] !== CLIENT_REFERENCE) { + throw new Error("clientModules must be client references"); + } + const [id] = decodeId(m["$$id"]); + moduleIds.push(id); + } + code += generatePrefetchCode(entryItems, moduleIds); + return code; +} diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index b41d5962e..6e0bcb66a 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -19,20 +19,28 @@ type RenderOptions = { serverEntryCallback?: (rsfId: string, fileId: string) => void; }; -export type MessageReq = { - id: number; - type: "render"; - input: RenderInput; - loadClientEntries: boolean | undefined; - loadServerEntries: boolean | undefined; - notifyServerEntry: boolean; -}; +export type MessageReq = + | { + id: number; + type: "render"; + input: RenderInput; + loadClientEntries: boolean | undefined; + loadServerEntries: boolean | undefined; + notifyServerEntry: boolean; + } + | { + id: number; + type: "prefetcher"; + pathItem: string; + loadClientEntries: boolean | undefined; + }; export type MessageRes = | { id: number; type: "buf"; buf: ArrayBuffer; offset: number; len: number } | { id: number; type: "end" } | { id: number; type: "err"; err: unknown } - | { id: number; type: "serverEntry"; rsfId: string; fileId: string }; + | { id: number; type: "serverEntry"; rsfId: string; fileId: string } + | { id: number; type: "prefetcher"; code: string }; const messageCallbacks = new Map void>(); @@ -74,3 +82,28 @@ export function renderRSC( worker.postMessage(mesg); return passthrough; } + +export function prefetcherRSC( + pathItem: string, + loadClientEntries?: boolean +): Promise { + return new Promise((resolve, reject) => { + const id = nextId++; + messageCallbacks.set(id, (mesg) => { + if (mesg.type === "prefetcher") { + resolve(mesg.code); + messageCallbacks.delete(id); + } else if (mesg.type === "err") { + reject(mesg.err); + messageCallbacks.delete(id); + } + }); + const mesg: MessageReq = { + id, + type: "prefetcher", + pathItem, + loadClientEntries, + }; + worker.postMessage(mesg); + }); +} diff --git a/src/middleware/rscDev.ts b/src/middleware/rscDev.ts index b78077041..e145d1b68 100644 --- a/src/middleware/rscDev.ts +++ b/src/middleware/rscDev.ts @@ -1,55 +1,20 @@ -import path from "node:path"; - import RSDWServer from "react-server-dom-webpack/server.node.unbundled"; import busboy from "busboy"; import type { MiddlewareCreator } from "./lib/common.js"; -import type { Prefetcher } from "../server.js"; -import { generatePrefetchCode } from "./lib/rsc-utils.js"; -import { renderRSC } from "./lib/rsc-handler.js"; +import { renderRSC, prefetcherRSC } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; -const CLIENT_REFERENCE = Symbol.for("react.client.reference"); - -const rscDev: MiddlewareCreator = (config, shared) => { - const dir = path.resolve(config.devServer?.dir || "."); - - const entriesFile = - (process.platform === "win32" ? "file://" : "") + - path.join(dir, config.files?.entriesJs || "entries.js"); - const prefetcher: Prefetcher = async (pathItem) => { - return {}; // TODO TEMP experimenting without prefetcher - const mod = await import(entriesFile); - return mod?.prefetcher(pathItem) ?? {}; - }; - - const decodeId = (encodedId: string): [id: string, name: string] => { - let [id, name] = encodedId.split("#") as [string, string]; - if (!id.startsWith("wakuwork/")) { - id = path.relative("file://" + encodeURI(dir), id); - id = "/" + decodeURI(id); - } - return [id, name]; - }; - - shared.devScriptToInject = async (path: string) => { - let code = ` +const rscDev: MiddlewareCreator = (_config, shared) => { + shared.devScriptToInject = async (pathItem: string) => { + const code = + ` globalThis.__wakuwork_module_cache__ = new Map(); globalThis.__webpack_chunk_load__ = async (id) => id.startsWith("wakuwork/") || import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); -globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id); -`; - const { entryItems = [], clientModules = [] } = await prefetcher(path); - const moduleIds: string[] = []; - for (const m of clientModules as any[]) { - if (m["$$typeof"] !== CLIENT_REFERENCE) { - throw new Error("clientModules must be client references"); - } - const [id] = decodeId(m["$$id"]); - moduleIds.push(id); - } - code += generatePrefetchCode(entryItems, moduleIds); +globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id);` + + (await prefetcherRSC(pathItem)); return code; }; @@ -57,7 +22,7 @@ globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.ge const rscId = req.headers["x-react-server-component-id"]; const rsfId = req.headers["x-react-server-function-id"]; if (Array.isArray(rscId) || Array.isArray(rsfId)) { - throw new Error('rscId and rsfId should not be array') + throw new Error("rscId and rsfId should not be array"); } let props = {}; if (rscId) { From 1d56a46c4e5e5523e0e412fabcd00e246508e499 Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 14 May 2023 14:51:56 +0900 Subject: [PATCH 05/37] wip prefetcherRSC in builder --- src/builder.ts | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index 3f2d734ba..d1b5b394a 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -8,14 +8,12 @@ import react from "@vitejs/plugin-react"; import * as swc from "@swc/core"; import type { Config } from "./config.js"; -import type { GetEntry, Prefetcher, Prerenderer } from "./server.js"; -import { generatePrefetchCode } from "./middleware/lib/rsc-utils.js"; -import { renderRSC } from "./middleware/lib/rsc-handler.js"; - -const CLIENT_REFERENCE = Symbol.for("react.client.reference"); +import type { GetEntry, Prerenderer } from "./server.js"; +import { renderRSC, prefetcherRSC } from "./middleware/lib/rsc-handler.js"; // TODO we have duplicate code here and rscPrd.ts and rsc-handler*.ts +// TODO we could do this without plugin anyway const rscPlugin = (): Plugin => { const code = ` globalThis.__wakuwork_module_cache__ = new Map(); @@ -114,11 +112,8 @@ const prerender = async ( ): Promise> => { const serverEntries: Record = {}; - const { prefetcher, prerenderer, clientEntries } = await (import( - entriesFile - ) as Promise<{ + const { prerenderer, clientEntries } = await (import(entriesFile) as Promise<{ getEntry: GetEntry; - prefetcher?: Prefetcher; prerenderer?: Prerenderer; clientEntries?: Record; }>); @@ -189,21 +184,7 @@ const prerender = async ( encoding: "utf8", }); for (const pathItem of paths) { - let code = ""; - if (prefetcher) { - const { entryItems = [], clientModules = [] } = await prefetcher( - pathItem - ); - const moduleIds: string[] = []; - for (const m of clientModules as any[]) { - if (m["$$typeof"] !== CLIENT_REFERENCE) { - throw new Error("clientModules must be client references"); - } - const [id] = decodeId(m["$$id"]); - moduleIds.push(id); - } - code += generatePrefetchCode?.(entryItems, moduleIds) || ""; - } + const code = await prefetcherRSC(pathItem, true); const destFile = path.join( dir, publicPath, From 9a29c23f669dded4302106fcd85082b5c18894c5 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 15 May 2023 00:08:01 +0900 Subject: [PATCH 06/37] wip: naive build --- cli.js | 2 - package.json | 3 +- src/builder.ts | 161 +++++++++++++++--------------- src/middleware/lib/rsc-handler.ts | 4 +- src/middleware/lib/rsc-utils.ts | 2 +- src/middleware/rscPrd.ts | 67 +------------ 6 files changed, 88 insertions(+), 151 deletions(-) diff --git a/cli.js b/cli.js index c592778cd..b19709ef5 100755 --- a/cli.js +++ b/cli.js @@ -1,7 +1,5 @@ #!/usr/bin/env node -import { Worker } from "node:worker_threads"; - const cmd = process.argv[2]; process.env.WAKUWORK_CMD = cmd; // TODO TEMP temporary solution import(`./dist/cli-${cmd}.js`); diff --git a/package.json b/package.json index a0f74a704..816ab254b 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "examples:dev:05_mutation": "NAME=05_mutation npm run examples:dev", "examples:dev:06_nesting": "NAME=06_nesting npm run examples:dev", "examples:dev:07_router": "NAME=07_router npm run examples:dev", - "examples:prd": "npm run compile:code && WAKUWORK_CONFIG=\"{\\\"build\\\":{\\\"dir\\\":\\\"./examples/${NAME}\\\"}}\" ./cli.js build && WAKUWORK_CONFIG=\"{\\\"prdServer\\\":{\\\"dir\\\":\\\"./examples/${NAME}/dist\\\"}}\" ./cli.js start", + "examples:build": "npm run compile:code && WAKUWORK_CONFIG=\"{\\\"build\\\":{\\\"dir\\\":\\\"./examples/${NAME}\\\"}}\" ./cli.js build", + "examples:prd": "npm run examples:build && WAKUWORK_CONFIG=\"{\\\"prdServer\\\":{\\\"dir\\\":\\\"./examples/${NAME}/dist\\\"}}\" ./cli.js start", "examples:prd:01_counter": "NAME=01_counter npm run examples:prd", "examples:prd:02_async": "NAME=02_async npm run examples:prd", "examples:prd:03_promise": "NAME=03_promise npm run examples:prd", diff --git a/src/builder.ts b/src/builder.ts index d1b5b394a..800cc91a9 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -13,15 +13,15 @@ import { renderRSC, prefetcherRSC } from "./middleware/lib/rsc-handler.js"; // TODO we have duplicate code here and rscPrd.ts and rsc-handler*.ts -// TODO we could do this without plugin anyway -const rscPlugin = (): Plugin => { +// FIXME we could do this without plugin anyway +const rscIndexPlugin = (): Plugin => { const code = ` globalThis.__wakuwork_module_cache__ = new Map(); globalThis.__webpack_chunk_load__ = async (id) => id.startsWith("wakuwork/") || import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id); `; return { - name: "rscPlugin", + name: "rsc-index-plugin", async transformIndexHtml() { return [ { @@ -34,85 +34,47 @@ globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.ge }; }; -const walkDirSync = (dir: string, callback: (filePath: string) => void) => { - fs.readdirSync(dir, { withFileTypes: true }).forEach((dirent) => { - const filePath = path.join(dir, dirent.name); - if (dirent.isDirectory()) { - if (dirent.name !== "node_modules") { - walkDirSync(filePath, callback); - } - } else { - callback(filePath); - } - }); -}; - -const getClientEntryFiles = (dir: string) => { - const files: string[] = []; - walkDirSync(dir, (fname) => { - if (fname.endsWith(".ts") || fname.endsWith(".tsx")) { - const mod = swc.parseFileSync(fname, { - syntax: "typescript", - tsx: fname.endsWith(".tsx"), - }); - for (const item of mod.body) { - if ( - item.type === "ExpressionStatement" && - item.expression.type === "StringLiteral" && - item.expression.value === "use client" - ) { - files.push(fname); +const rscBundlePlugin = ( + clientEntryCallback: (id: string) => void +): Plugin => { + return { + name: "rsc-bundle-plugin", + transform(code, id) { + const ext = path.extname(id); + if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) { + const mod = swc.parseSync(code, { + syntax: ext === ".ts" || ext === ".tsx" ? "typescript" : "ecmascript", + tsx: ext === ".tsx", + }); + for (const item of mod.body) { + if ( + item.type === "ExpressionStatement" && + item.expression.type === "StringLiteral" && + item.expression.value === "use client" + ) { + clientEntryCallback(id); + break; + } } } - } - // TODO transpile ".jsx" - }); - return files; -}; - -const compileFiles = (dir: string, distPath: string) => { - walkDirSync(dir, (fname) => { - const relativePath = path.relative(dir, fname); - if (relativePath.startsWith(distPath)) { - return; - } - if (fname.endsWith(".ts") || fname.endsWith(".tsx")) { - const { code } = swc.transformFileSync(fname, { - jsc: { - parser: { - syntax: "typescript", - tsx: fname.endsWith(".tsx"), - }, - transform: { - react: { - runtime: "automatic", - }, - }, - }, - }); - const destFile = path.join( - dir, - distPath, - relativePath.replace(/\.tsx?$/, ".js") - ); - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - fs.writeFileSync(destFile, code); - } - // TODO transpile ".jsx" - }); + return code; + }, + }; }; const prerender = async ( dir: string, distPath: string, publicPath: string, - entriesFile: string, + distEntriesFile: string, basePath: string, publicIndexHtmlFile: string ): Promise> => { const serverEntries: Record = {}; - const { prerenderer, clientEntries } = await (import(entriesFile) as Promise<{ + const { prerenderer, clientEntries } = await (import( + distEntriesFile + ) as Promise<{ getEntry: GetEntry; prerenderer?: Prerenderer; clientEntries?: Record; @@ -224,23 +186,49 @@ export async function runBuild(config: Config = {}) { publicPath, config.files?.indexHtml || "index.html" ); - const entriesFile = path.join( + const distEntriesFile = path.join( dir, distPath, config.files?.entriesJs || "entries.js" ); const require = createRequire(import.meta.url); + const clientEntryFileSet = new Set(); + // TODO "use server" separation doesn't work yet + const serverBuildOutput = await build({ + root: dir, + base: basePath, + plugins: [rscBundlePlugin((id) => clientEntryFileSet.add(id))], + build: { + outDir: distPath, + ssr: "entries", + rollupOptions: { + output: { + banner: (chunk) => { + // HACK to pull directives front + if (chunk.moduleIds.some((id) => clientEntryFileSet.has(id))) { + return '"use client";'; + } + return ""; + }, + }, + }, + }, + }); + if (!("output" in serverBuildOutput)) { + throw new Error("Unexpected vite server build output"); + } + const clientEntryFiles = Object.fromEntries( - getClientEntryFiles(dir).map((fname, i) => [`rsc${i}`, fname]) + Array.from(clientEntryFileSet).map((fname, i) => [`rsc${i}`, fname]) ); - const output = await build({ + const clientBuildOutput = await build({ root: dir, base: basePath, plugins: [ // @ts-ignore react(), - rscPlugin(), + rscIndexPlugin(), ], build: { outDir: publicPath, @@ -253,37 +241,44 @@ export async function runBuild(config: Config = {}) { }, }, }); - const clientEntries: Record = {}; - if (!("output" in output)) { - throw new Error("Unexpected vite build output"); + if (!("output" in clientBuildOutput)) { + throw new Error("Unexpected vite client build output"); } - for (const item of output.output) { + const clientEntries: Record = {}; + for (const item of clientBuildOutput.output) { const { name, fileName } = item; - const entryFile = name && clientEntryFiles[name]; + const entryFile = + name && + serverBuildOutput.output.find( + (item) => + "moduleIds" in item && + item.moduleIds.includes(clientEntryFiles[name] as string) + )?.fileName; if (entryFile) { - clientEntries[path.relative(dir, entryFile)] = fileName; + clientEntries[entryFile] = fileName; } } console.log("clientEntries", clientEntries); - compileFiles(dir, distPath); fs.appendFileSync( - entriesFile, + distEntriesFile, `export const clientEntries=${JSON.stringify(clientEntries)};` ); + /* const serverEntries = await prerender( dir, distPath, publicPath, - entriesFile, + distEntriesFile, basePath, publicIndexHtmlFile ); console.log("serverEntries", serverEntries); fs.appendFileSync( - entriesFile, + distEntriesFile, `export const serverEntries=${JSON.stringify(serverEntries)};` ); + */ const origPackageJson = require(path.join(dir, "package.json")); const packageJson = { diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index 6e0bcb66a..6a59a9482 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -15,8 +15,8 @@ export type RenderInput = { type RenderOptions = { loadClientEntries?: boolean; - loadServerEntries?: boolean; - serverEntryCallback?: (rsfId: string, fileId: string) => void; + loadServerEntries?: boolean; // TODO remove + serverEntryCallback?: (rsfId: string, fileId: string) => void; // TODO remove }; export type MessageReq = diff --git a/src/middleware/lib/rsc-utils.ts b/src/middleware/lib/rsc-utils.ts index ecbd46729..a1e490f43 100644 --- a/src/middleware/lib/rsc-utils.ts +++ b/src/middleware/lib/rsc-utils.ts @@ -10,7 +10,7 @@ export const generatePrefetchCode = ( const entryItems = Array.from(entryItemsIterable); let code = ""; if (entryItems.length) { - const rscIds = [...new Set(entryItems.map(([rscId]) => rscId))]; + const rscIds = Array.from(new Set(entryItems.map(([rscId]) => rscId))); code += ` globalThis.__WAKUWORK_PREFETCHED__ = { ${rscIds diff --git a/src/middleware/rscPrd.ts b/src/middleware/rscPrd.ts index 0dfbe64dc..874589efb 100644 --- a/src/middleware/rscPrd.ts +++ b/src/middleware/rscPrd.ts @@ -1,73 +1,16 @@ -import path from "node:path"; - import RSDWServer from "react-server-dom-webpack/server.node.unbundled"; import busboy from "busboy"; import type { MiddlewareCreator } from "./lib/common.js"; -import type { Prefetcher } from "../server.js"; -import { generatePrefetchCode } from "./lib/rsc-utils.js"; -import { renderRSC } from "./lib/rsc-handler.js"; +import { renderRSC, prefetcherRSC } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; -const CLIENT_REFERENCE = Symbol.for("react.client.reference"); - // TODO we have duplicate code here and rsc-handler-worker.ts -const rscPrd: MiddlewareCreator = (config, shared) => { - const dir = path.resolve(config.prdServer?.dir || "."); - const basePath = config.build?.basePath || "/"; // FIXME it's not build only - - const entriesFile = - (process.platform === "win32" ? "file://" : "") + - path.join(dir, config.files?.entriesJs || "entries.js"); - const prefetcher: Prefetcher = async (pathItem) => { - const mod = await import(entriesFile); - return mod?.prefetcher(pathItem) ?? {}; - }; - let clientEntries: Record | undefined; - import(entriesFile).then((mod) => { - clientEntries = mod.clientEntries; - }); - - const getClientEntry = (id: string) => { - if (!clientEntries) { - throw new Error("Missing client entries"); - } - const clientEntry = - clientEntries[id] || - clientEntries[id.replace(/\.js$/, ".ts")] || - clientEntries[id.replace(/\.js$/, ".tsx")] || - clientEntries[id.replace(/\.js$/, ".jsx")]; - if (!clientEntry) { - throw new Error("No client entry found"); - } - return clientEntry; - }; - - const decodeId = (encodedId: string): [id: string, name: string] => { - let [id, name] = encodedId.split("#") as [string, string]; - if (!id.startsWith("wakuwork/")) { - id = path.relative("file://" + encodeURI(dir), id); - id = basePath + getClientEntry(decodeURI(id)); - } - return [id, name]; - }; - - shared.prdScriptToInject = async (path: string) => { - let code = ""; - if (prefetcher) { - const { entryItems = [], clientModules = [] } = await prefetcher(path); - const moduleIds: string[] = []; - for (const m of clientModules as any[]) { - if (m["$$typeof"] !== CLIENT_REFERENCE) { - throw new Error("clientModules must be client references"); - } - const [id] = decodeId(m["$$id"]); - moduleIds.push(id); - } - code += generatePrefetchCode?.(entryItems, moduleIds) || ""; - } +const rscPrd: MiddlewareCreator = (_config, shared) => { + shared.prdScriptToInject = async (pathItem: string) => { + const code = await prefetcherRSC(pathItem); return code; }; @@ -105,7 +48,7 @@ const rscPrd: MiddlewareCreator = (config, shared) => { if (rscId || rsfId) { renderRSC( { rscId, props, rsfId, args }, - { loadClientEntries: true, loadServerEntries: true } + { loadClientEntries: true } ).pipe(res); return; } From 1a634955c3ad6ab3bd182c973ca1f585d12ba80b Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 15 May 2023 00:11:26 +0900 Subject: [PATCH 07/37] still wip comment --- src/builder.ts | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index 800cc91a9..59153c724 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -34,9 +34,7 @@ globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.ge }; }; -const rscBundlePlugin = ( - clientEntryCallback: (id: string) => void -): Plugin => { +const rscBundlePlugin = (clientEntryCallback: (id: string) => void): Plugin => { return { name: "rsc-bundle-plugin", transform(code, id) { @@ -264,21 +262,22 @@ export async function runBuild(config: Config = {}) { distEntriesFile, `export const clientEntries=${JSON.stringify(clientEntries)};` ); - /* - const serverEntries = await prerender( - dir, - distPath, - publicPath, - distEntriesFile, - basePath, - publicIndexHtmlFile - ); - console.log("serverEntries", serverEntries); - fs.appendFileSync( - distEntriesFile, - `export const serverEntries=${JSON.stringify(serverEntries)};` - ); - */ + if (!"STILL WIP") { + // TODO still wip + const serverEntries = await prerender( + dir, + distPath, + publicPath, + distEntriesFile, + basePath, + publicIndexHtmlFile + ); + console.log("serverEntries", serverEntries); + fs.appendFileSync( + distEntriesFile, + `export const serverEntries=${JSON.stringify(serverEntries)};` + ); + } const origPackageJson = require(path.join(dir, "package.json")); const packageJson = { From a231b50676fb3c3b2a0b95f7ac8063745c800524 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 15 May 2023 10:15:31 +0900 Subject: [PATCH 08/37] eliminate server entries and simplify --- src/builder.ts | 20 ++----- src/middleware/lib/rsc-handler-worker.ts | 67 +++--------------------- src/middleware/lib/rsc-handler.ts | 23 ++------ src/middleware/lib/rsc-utils.ts | 7 +-- src/middleware/rscDev.ts | 4 +- src/middleware/rscPrd.ts | 7 +-- 6 files changed, 20 insertions(+), 108 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index 59153c724..448cfc966 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -67,9 +67,7 @@ const prerender = async ( distEntriesFile: string, basePath: string, publicIndexHtmlFile: string -): Promise> => { - const serverEntries: Record = {}; - +) => { const { prerenderer, clientEntries } = await (import( distEntriesFile ) as Promise<{ @@ -129,12 +127,7 @@ const prerender = async ( rscId, props: props as any, }, - { - loadClientEntries: true, - serverEntryCallback: (rsfId, fileId) => { - serverEntries[rsfId] = fileId; - }, - } + true ).pipe(stream); }); }) @@ -169,8 +162,6 @@ const prerender = async ( fs.writeFileSync(destFile, data, { encoding: "utf8" }); } } - - return serverEntries; }; export async function runBuild(config: Config = {}) { @@ -264,7 +255,7 @@ export async function runBuild(config: Config = {}) { ); if (!"STILL WIP") { // TODO still wip - const serverEntries = await prerender( + await prerender( dir, distPath, publicPath, @@ -272,11 +263,6 @@ export async function runBuild(config: Config = {}) { basePath, publicIndexHtmlFile ); - console.log("serverEntries", serverEntries); - fs.appendFileSync( - distEntriesFile, - `export const serverEntries=${JSON.stringify(serverEntries)};` - ); } const origPackageJson = require(path.join(dir, "package.json")); diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 7ff2388e1..d5bf18cbd 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -15,24 +15,9 @@ const { renderToPipeableStream } = RSDWServer; const CLIENT_REFERENCE = Symbol.for("react.client.reference"); const handleRender = async (mesg: MessageReq & { type: "render" }) => { - const { id, input, loadClientEntries, loadServerEntries, notifyServerEntry } = - mesg; + const { id, input, loadClientEntries } = mesg; try { - const pipeable = await renderRSC(input, { - loadClientEntries, - loadServerEntries, - serverEntryCallback: notifyServerEntry - ? (rsfId, fileId) => { - const mesg: MessageRes = { - id, - type: "serverEntry", - rsfId, - fileId, - }; - parentPort!.postMessage(mesg); - } - : undefined, - }); + const pipeable = await renderRSC(input, loadClientEntries); const writable = new Writable({ write(chunk, encoding, callback) { if (encoding !== ("buffer" as any)) { @@ -147,28 +132,15 @@ const getFunctionComponent = async (rscId: string) => { async function renderRSC( input: RenderInput, - options: { - loadClientEntries: boolean | undefined; - loadServerEntries: boolean | undefined; - serverEntryCallback: ((rsfId: string, fileId: string) => void) | undefined; - } + loadClientEntries: boolean ): Promise { let clientEntries: Record | undefined; - let serverEntries: Record | undefined; - if (options.loadClientEntries) { + if (loadClientEntries) { ({ clientEntries } = await loadServerFile(entriesFile)); if (!clientEntries) { throw new Error("Failed to load clientEntries"); } } - if (options.loadServerEntries) { - ({ serverEntries } = await loadServerFile(entriesFile)); - if (!serverEntries) { - throw new Error("Failed to load serverEntries"); - } - } else if (process.env.WAKUWORK_CMD !== "dev") { - serverEntries = {}; - } const getClientEntry = (id: string) => { if (!clientEntries) { @@ -207,34 +179,8 @@ async function renderRSC( } ); - const registerServerEntry = (fileId: string): string => { - if (!serverEntries) { - return fileId; - } - for (const entry of Object.entries(serverEntries)) { - if (entry[1] === fileId) { - return entry[0]; - } - } - const rsfId = `rsf${Object.keys(serverEntries).length}`; - serverEntries[rsfId] = fileId; - options.serverEntryCallback?.(rsfId, fileId); - return rsfId; - }; - - const getServerEntry = (rsfId: string): string => { - if (!serverEntries) { - return rsfId; - } - const fileId = serverEntries[rsfId]; - if (!fileId) { - throw new Error("No server entry found"); - } - return fileId; - }; - if (input.rsfId && input.args) { - const [fileId, name] = getServerEntry(input.rsfId).split("#"); + const [fileId, name] = input.rsfId.split("#"); const fname = path.join(dir, fileId!); const mod = await loadServerFile(fname); const data = await (mod[name!] || mod)(...input.args); @@ -250,8 +196,7 @@ async function renderRSC( bundlerConfig ).pipe( transformRsfId( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : ""), - registerServerEntry + path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : "") ) ); } diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index 6a59a9482..8628badca 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -13,33 +13,24 @@ export type RenderInput = { args?: unknown[] | undefined; }; -type RenderOptions = { - loadClientEntries?: boolean; - loadServerEntries?: boolean; // TODO remove - serverEntryCallback?: (rsfId: string, fileId: string) => void; // TODO remove -}; - export type MessageReq = | { id: number; type: "render"; input: RenderInput; - loadClientEntries: boolean | undefined; - loadServerEntries: boolean | undefined; - notifyServerEntry: boolean; + loadClientEntries: boolean; } | { id: number; type: "prefetcher"; pathItem: string; - loadClientEntries: boolean | undefined; + loadClientEntries: boolean; }; export type MessageRes = | { id: number; type: "buf"; buf: ArrayBuffer; offset: number; len: number } | { id: number; type: "end" } | { id: number; type: "err"; err: unknown } - | { id: number; type: "serverEntry"; rsfId: string; fileId: string } | { id: number; type: "prefetcher"; code: string }; const messageCallbacks = new Map void>(); @@ -52,7 +43,7 @@ let nextId = 1; export function renderRSC( input: RenderInput, - options?: RenderOptions + loadClientEntries: boolean ): Readable { const id = nextId++; const passthrough = new PassThrough(); @@ -67,17 +58,13 @@ export function renderRSC( mesg.err instanceof Error ? mesg.err : new Error(String(mesg.err)) ); messageCallbacks.delete(id); - } else if (mesg.type === "serverEntry" && options?.serverEntryCallback) { - options.serverEntryCallback(mesg.rsfId, mesg.fileId); } }); const mesg: MessageReq = { id, type: "render", input, - loadClientEntries: options?.loadClientEntries, - loadServerEntries: options?.loadServerEntries, - notifyServerEntry: !!options?.serverEntryCallback, + loadClientEntries, }; worker.postMessage(mesg); return passthrough; @@ -85,7 +72,7 @@ export function renderRSC( export function prefetcherRSC( pathItem: string, - loadClientEntries?: boolean + loadClientEntries: boolean ): Promise { return new Promise((resolve, reject) => { const id = nextId++; diff --git a/src/middleware/lib/rsc-utils.ts b/src/middleware/lib/rsc-utils.ts index a1e490f43..d410cfc15 100644 --- a/src/middleware/lib/rsc-utils.ts +++ b/src/middleware/lib/rsc-utils.ts @@ -43,10 +43,7 @@ import('${moduleId}');`; }; // HACK Patching stream is very fragile. -export const transformRsfId = ( - prefixToRemove: string, - convert: (id: string) => string -) => +export const transformRsfId = (prefixToRemove: string) => new Transform({ transform(chunk, encoding, callback) { if (encoding !== ("buffer" as any)) { @@ -60,7 +57,7 @@ export const transformRsfId = ( new RegExp(`^([0-9]+):{"id":"${prefixToRemove}(.*?)"(.*)$`) ); if (match) { - lines[i] = `${match[1]}:{"id":"${convert(match[2])}"${match[3]}`; + lines[i] = `${match[1]}:{"id":"${match[2]}"${match[3]}`; changed = true; } } diff --git a/src/middleware/rscDev.ts b/src/middleware/rscDev.ts index e145d1b68..284f00a98 100644 --- a/src/middleware/rscDev.ts +++ b/src/middleware/rscDev.ts @@ -14,7 +14,7 @@ const rscDev: MiddlewareCreator = (_config, shared) => { globalThis.__wakuwork_module_cache__ = new Map(); globalThis.__webpack_chunk_load__ = async (id) => id.startsWith("wakuwork/") || import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id);` + - (await prefetcherRSC(pathItem)); + (await prefetcherRSC(pathItem, false)); return code; }; @@ -50,7 +50,7 @@ globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.ge } } if (rscId || rsfId) { - renderRSC({ rscId, props, rsfId, args }).pipe(res); + renderRSC({ rscId, props, rsfId, args }, false).pipe(res); return; } await next(); diff --git a/src/middleware/rscPrd.ts b/src/middleware/rscPrd.ts index 874589efb..efb572947 100644 --- a/src/middleware/rscPrd.ts +++ b/src/middleware/rscPrd.ts @@ -10,7 +10,7 @@ const { decodeReply, decodeReplyFromBusboy } = RSDWServer; const rscPrd: MiddlewareCreator = (_config, shared) => { shared.prdScriptToInject = async (pathItem: string) => { - const code = await prefetcherRSC(pathItem); + const code = await prefetcherRSC(pathItem, true); return code; }; @@ -46,10 +46,7 @@ const rscPrd: MiddlewareCreator = (_config, shared) => { } } if (rscId || rsfId) { - renderRSC( - { rscId, props, rsfId, args }, - { loadClientEntries: true } - ).pipe(res); + renderRSC({ rscId, props, rsfId, args }, true).pipe(res); return; } await next(); From 3f8e2e6347fc8aa500af07f17b0811d3d2cd5524 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 15 May 2023 11:14:08 +0900 Subject: [PATCH 09/37] 3-pass build --- src/builder.ts | 79 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index 448cfc966..afbf972ba 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -34,7 +34,10 @@ globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.ge }; }; -const rscBundlePlugin = (clientEntryCallback: (id: string) => void): Plugin => { +const rscAnalyzePlugin = ( + clientEntryCallback: (id: string) => void, + serverEntryCallback: (id: string) => void +): Plugin => { return { name: "rsc-bundle-plugin", transform(code, id) { @@ -47,11 +50,13 @@ const rscBundlePlugin = (clientEntryCallback: (id: string) => void): Plugin => { for (const item of mod.body) { if ( item.type === "ExpressionStatement" && - item.expression.type === "StringLiteral" && - item.expression.value === "use client" + item.expression.type === "StringLiteral" ) { - clientEntryCallback(id); - break; + if (item.expression.value === "use client") { + clientEntryCallback(id); + } else if (item.expression.value === "use server") { + serverEntryCallback(id); + } } } } @@ -180,25 +185,71 @@ export async function runBuild(config: Config = {}) { distPath, config.files?.entriesJs || "entries.js" ); + let entriesFile = path.join(dir, config.files?.entriesJs || "entries.js"); + if (entriesFile.endsWith(".js")) { + for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { + const tmp = entriesFile.slice(0, -3) + ext; + if (fs.existsSync(tmp)) { + entriesFile = tmp; + break; + } + } + } const require = createRequire(import.meta.url); const clientEntryFileSet = new Set(); - // TODO "use server" separation doesn't work yet + const serverEntryFileSet = new Set(); + await build({ + root: dir, + base: basePath, + plugins: [ + rscAnalyzePlugin( + (id) => clientEntryFileSet.add(id), + (id) => serverEntryFileSet.add(id) + ), + ], + build: { + outDir: distPath, + ssr: entriesFile, + write: false, + }, + }); + const clientEntryFiles = Object.fromEntries( + Array.from(clientEntryFileSet).map((fname, i) => [`rsc${i}`, fname]) + ); + const serverEntryFiles = Object.fromEntries( + Array.from(serverEntryFileSet).map((fname, i) => [`rsf${i}`, fname]) + ); + const serverBuildOutput = await build({ root: dir, base: basePath, - plugins: [rscBundlePlugin((id) => clientEntryFileSet.add(id))], build: { outDir: distPath, - ssr: "entries", + ssr: true, rollupOptions: { + input: { + entries: entriesFile, + ...clientEntryFiles, + ...serverEntryFiles, + }, output: { banner: (chunk) => { - // HACK to pull directives front + // HACK to bring directives to the front + let code = ""; if (chunk.moduleIds.some((id) => clientEntryFileSet.has(id))) { - return '"use client";'; + code += '"use client";'; + } + if (chunk.moduleIds.some((id) => serverEntryFileSet.has(id))) { + code += '"use server";'; + } + return code; + }, + entryFileNames: (chunkInfo) => { + if (chunkInfo.name === "entries") { + return "[name].js"; } - return ""; + return "assets/[name].js"; }, }, }, @@ -208,9 +259,6 @@ export async function runBuild(config: Config = {}) { throw new Error("Unexpected vite server build output"); } - const clientEntryFiles = Object.fromEntries( - Array.from(clientEntryFileSet).map((fname, i) => [`rsc${i}`, fname]) - ); const clientBuildOutput = await build({ root: dir, base: basePath, @@ -233,6 +281,7 @@ export async function runBuild(config: Config = {}) { if (!("output" in clientBuildOutput)) { throw new Error("Unexpected vite client build output"); } + const clientEntries: Record = {}; for (const item of clientBuildOutput.output) { const { name, fileName } = item; @@ -248,11 +297,11 @@ export async function runBuild(config: Config = {}) { } } console.log("clientEntries", clientEntries); - fs.appendFileSync( distEntriesFile, `export const clientEntries=${JSON.stringify(clientEntries)};` ); + if (!"STILL WIP") { // TODO still wip await prerender( From bb0f5cf45b43872a28cb5ac67d83c3e8578111af Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 15 May 2023 12:21:43 +0900 Subject: [PATCH 10/37] prerender with bad design... --- src/builder.ts | 124 +------------------ src/middleware/lib/rsc-handler-worker.ts | 151 +++++++++++++++++++++-- src/middleware/lib/rsc-handler.ts | 28 +++++ 3 files changed, 171 insertions(+), 132 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index afbf972ba..231c40228 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -8,8 +8,7 @@ import react from "@vitejs/plugin-react"; import * as swc from "@swc/core"; import type { Config } from "./config.js"; -import type { GetEntry, Prerenderer } from "./server.js"; -import { renderRSC, prefetcherRSC } from "./middleware/lib/rsc-handler.js"; +import { prerenderRSC } from "./middleware/lib/rsc-handler.js"; // TODO we have duplicate code here and rscPrd.ts and rsc-handler*.ts @@ -65,121 +64,12 @@ const rscAnalyzePlugin = ( }; }; -const prerender = async ( - dir: string, - distPath: string, - publicPath: string, - distEntriesFile: string, - basePath: string, - publicIndexHtmlFile: string -) => { - const { prerenderer, clientEntries } = await (import( - distEntriesFile - ) as Promise<{ - getEntry: GetEntry; - prerenderer?: Prerenderer; - clientEntries?: Record; - }>); - - const getClientEntry = (id: string) => { - if (!clientEntries) { - throw new Error("Missing client entries"); - } - const clientEntry = - clientEntries[id] || - clientEntries[id.replace(/\.js$/, ".ts")] || - clientEntries[id.replace(/\.js$/, ".tsx")]; - if (!clientEntry) { - throw new Error("No client entry found"); - } - return clientEntry; - }; - const decodeId = (encodedId: string): [id: string, name: string] => { - let [id, name] = encodedId.split("#") as [string, string]; - if (!id.startsWith("wakuwork/")) { - id = path.relative("file://" + encodeURI(path.join(dir, distPath)), id); - id = basePath + getClientEntry(decodeURI(id)); - } - return [id, name]; - }; - - if (prerenderer) { - const { - entryItems = [], - paths = [], - unstable_customCode = () => "", - } = await prerenderer(); - await Promise.all( - Array.from(entryItems).map(async ([rscId, props]) => { - // FIXME we blindly expect JSON.stringify usage is deterministic - const serializedProps = JSON.stringify(props); - const searchParams = new URLSearchParams(); - searchParams.set("props", serializedProps); - const destFile = path.join( - dir, - publicPath, - "RSC", - decodeURIComponent(rscId), - decodeURIComponent(`${searchParams}`) - ); - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - await new Promise((resolve, reject) => { - const stream = fs.createWriteStream(destFile); - stream.on("finish", resolve); - stream.on("error", reject); - renderRSC( - { - rscId, - props: props as any, - }, - true - ).pipe(stream); - }); - }) - ); - - const publicIndexHtml = fs.readFileSync(publicIndexHtmlFile, { - encoding: "utf8", - }); - for (const pathItem of paths) { - const code = await prefetcherRSC(pathItem, true); - const destFile = path.join( - dir, - publicPath, - pathItem, - pathItem.endsWith("/") ? "index.html" : "" - ); - let data = ""; - if (fs.existsSync(destFile)) { - data = fs.readFileSync(destFile, { encoding: "utf8" }); - } else { - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - data = publicIndexHtml; - } - if (code) { - // HACK is this too naive to inject script code? - data = data.replace(/<\/body>/, ``); - } - const code2 = unstable_customCode(pathItem, decodeId); - if (code2) { - data = data.replace(/<\/body>/, ``); - } - fs.writeFileSync(destFile, data, { encoding: "utf8" }); - } - } -}; - export async function runBuild(config: Config = {}) { const dir = path.resolve(config.build?.dir || "."); const basePath = config.build?.basePath || "/"; const distPath = config.files?.dist || "dist"; const publicPath = path.join(distPath, config.files?.public || "public"); const indexHtmlFile = path.join(dir, config.files?.indexHtml || "index.html"); - const publicIndexHtmlFile = path.join( - dir, - publicPath, - config.files?.indexHtml || "index.html" - ); const distEntriesFile = path.join( dir, distPath, @@ -302,17 +192,7 @@ export async function runBuild(config: Config = {}) { `export const clientEntries=${JSON.stringify(clientEntries)};` ); - if (!"STILL WIP") { - // TODO still wip - await prerender( - dir, - distPath, - publicPath, - distEntriesFile, - basePath, - publicIndexHtmlFile - ); - } + await prerenderRSC(true); const origPackageJson = require(path.join(dir, "package.json")); const packageJson = { diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index d5bf18cbd..491fcc41a 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import fs from "node:fs"; import { parentPort } from "node:worker_threads"; import { Writable } from "node:stream"; @@ -9,6 +10,7 @@ import RSDWServer from "react-server-dom-webpack/server"; import { transformRsfId, generatePrefetchCode } from "./rsc-utils.js"; import type { RenderInput, MessageReq, MessageRes } from "./rsc-handler.js"; import type { Config } from "../../config.js"; +import type { GetEntry, Prefetcher, Prerenderer } from "../../server.js"; import { rscPlugin } from "./vite-rsc-plugin.js"; const { renderToPipeableStream } = RSDWServer; @@ -57,9 +59,7 @@ const handleRender = async (mesg: MessageReq & { type: "render" }) => { const handlePrefetcher = async (mesg: MessageReq & { type: "prefetcher" }) => { const { id, pathItem, loadClientEntries } = mesg; try { - const code = await prefetcherRSC(pathItem, { - loadClientEntries, - }); + const code = await prefetcherRSC(pathItem, loadClientEntries); const mesg: MessageRes = { id, type: "prefetcher", @@ -76,11 +76,32 @@ const handlePrefetcher = async (mesg: MessageReq & { type: "prefetcher" }) => { } }; +const handlePrerender = async (mesg: MessageReq & { type: "prerender" }) => { + const { id, loadClientEntries } = mesg; + try { + await prerenderRSC(loadClientEntries); + const mesg: MessageRes = { + id, + type: "end", + }; + parentPort!.postMessage(mesg); + } catch (err) { + const mesg: MessageRes = { + id, + type: "err", + err, + }; + parentPort!.postMessage(mesg); + } +}; + parentPort!.on("message", (mesg: MessageReq) => { if (mesg.type === "render") { handleRender(mesg); } else if (mesg.type === "prefetcher") { handlePrefetcher(mesg); + } else if (mesg.type === "prerender") { + handlePrerender(mesg); } }); @@ -101,6 +122,12 @@ const dirFromConfig = { const dir = path.resolve(dirFromConfig || "."); const basePath = config.build?.basePath || "/"; // FIXME it's not build only const distPath = config.files?.dist || "dist"; +const publicPath = path.join(distPath, config.files?.public || "public"); +const publicIndexHtmlFile = path.join( + dir, + publicPath, + config.files?.indexHtml || "index.html" +); const entriesFile = path.join( dir, process.env.WAKUWORK_CMD === "build" ? distPath : "", @@ -119,7 +146,9 @@ const loadServerFile = async (fname: string) => { }; const getFunctionComponent = async (rscId: string) => { - const { getEntry } = await loadServerFile(entriesFile); + const { getEntry } = await (loadServerFile(entriesFile) as Promise<{ + getEntry: GetEntry; + }>); const mod = await getEntry(rscId); if (typeof mod === "function") { return mod; @@ -205,15 +234,13 @@ async function renderRSC( async function prefetcherRSC( pathItem: string, - options: { - loadClientEntries: boolean | undefined; - } + loadClientEntries: boolean ): Promise { let code = ""; - // TOOD duplicated code with renderRSC + // FIXME duplicated code with renderRSC let clientEntries: Record | undefined; - if (options.loadClientEntries) { + if (loadClientEntries) { ({ clientEntries } = await loadServerFile(entriesFile)); if (!clientEntries) { throw new Error("Failed to load clientEntries"); @@ -245,7 +272,9 @@ async function prefetcherRSC( return [id, name]; }; - const { prefetcher } = await loadServerFile(entriesFile); + const { prefetcher } = await (loadServerFile(entriesFile) as Promise<{ + prefetcher: Prefetcher; + }>); const { entryItems = [], clientModules = [] } = prefetcher ? await prefetcher(pathItem) : {}; @@ -260,3 +289,105 @@ async function prefetcherRSC( code += generatePrefetchCode(entryItems, moduleIds); return code; } + +// TODO this takes too much responsibility +// FIXME it shouldn't depend on `fs` +async function prerenderRSC(loadClientEntries: boolean): Promise { + const { prerenderer } = await (loadServerFile(entriesFile) as Promise<{ + prerenderer?: Prerenderer; + }>); + + // FIXME duplicated code with renderRSC + let clientEntries: Record | undefined; + if (loadClientEntries) { + ({ clientEntries } = await loadServerFile(entriesFile)); + if (!clientEntries) { + throw new Error("Failed to load clientEntries"); + } + } + const getClientEntry = (id: string) => { + if (!clientEntries) { + return id; + } + const clientEntry = + clientEntries[id] || + clientEntries[id.replace(/\.js$/, ".ts")] || + clientEntries[id.replace(/\.js$/, ".tsx")] || + clientEntries[id.replace(/\.js$/, ".jsx")]; + if (!clientEntry) { + throw new Error("No client entry found"); + } + return clientEntry; + }; + const decodeId = (encodedId: string): [id: string, name: string] => { + let [id, name] = encodedId.split("#") as [string, string]; + if (!id.startsWith("wakuwork/")) { + id = path.relative( + path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : ""), + id + ); + id = basePath + getClientEntry(id); + } + return [id, name]; + }; + + if (prerenderer) { + const { + entryItems = [], + paths = [], + unstable_customCode = () => "", + } = await prerenderer(); + await Promise.all( + Array.from(entryItems).map(async ([rscId, props]) => { + // FIXME we blindly expect JSON.stringify usage is deterministic + const serializedProps = JSON.stringify(props); + const searchParams = new URLSearchParams(); + searchParams.set("props", serializedProps); + const destFile = path.join( + dir, + publicPath, + "RSC", + decodeURIComponent(rscId), + decodeURIComponent(`${searchParams}`) + ); + fs.mkdirSync(path.dirname(destFile), { recursive: true }); + const pipeable = await renderRSC({ rscId, props: props as any }, true); + await new Promise((resolve, reject) => { + const stream = fs.createWriteStream(destFile); + stream.on("finish", resolve); + stream.on("error", reject); + pipeable.pipe(stream); + }); + }) + ); + + const publicIndexHtml = fs.readFileSync(publicIndexHtmlFile, { + encoding: "utf8", + }); + for (const pathItem of paths) { + const code = await prefetcherRSC(pathItem, true); + const destFile = path.join( + dir, + publicPath, + pathItem, + pathItem.endsWith("/") ? "index.html" : "" + ); + let data = ""; + if (fs.existsSync(destFile)) { + data = fs.readFileSync(destFile, { encoding: "utf8" }); + } else { + fs.mkdirSync(path.dirname(destFile), { recursive: true }); + data = publicIndexHtml; + } + if (code) { + // HACK is this too naive to inject script code? + data = data.replace(/<\/body>/, ``); + } + const code2 = unstable_customCode(pathItem, decodeId); + if (code2) { + data = data.replace(/<\/body>/, ``); + } + fs.writeFileSync(destFile, data, { encoding: "utf8" }); + } + } +} diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index 8628badca..19c8ee932 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -25,6 +25,11 @@ export type MessageReq = type: "prefetcher"; pathItem: string; loadClientEntries: boolean; + } + | { + id: number; + type: "prerender"; + loadClientEntries: boolean; }; export type MessageRes = @@ -94,3 +99,26 @@ export function prefetcherRSC( worker.postMessage(mesg); }); } + +export function prerenderRSC( + loadClientEntries: boolean +): Promise { + return new Promise((resolve, reject) => { + const id = nextId++; + messageCallbacks.set(id, (mesg) => { + if (mesg.type === "end") { + resolve(); + messageCallbacks.delete(id); + } else if (mesg.type === "err") { + reject(mesg.err); + messageCallbacks.delete(id); + } + }); + const mesg: MessageReq = { + id, + type: "prerender", + loadClientEntries, + }; + worker.postMessage(mesg); + }); +} From b853d3c5473afb52d1cc8394d67b8659ed30a5fc Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 16 May 2023 11:03:15 +0900 Subject: [PATCH 11/37] wip wip buildRSC --- src/builder.ts | 12 +- src/cli-start.ts | 2 + src/middleware/lib/rsc-handler-worker.ts | 256 ++++++++++++++++------- src/middleware/lib/rsc-handler.ts | 57 ++++- src/middleware/lib/vite-rsc-plugin.ts | 1 + src/router/server.ts | 1 + src/server.ts | 15 ++ 7 files changed, 265 insertions(+), 79 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index 231c40228..656057e9a 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -8,7 +8,7 @@ import react from "@vitejs/plugin-react"; import * as swc from "@swc/core"; import type { Config } from "./config.js"; -import { prerenderRSC } from "./middleware/lib/rsc-handler.js"; +import { getCustomModulesRSC, buildRSC } from "./middleware/lib/rsc-handler.js"; // TODO we have duplicate code here and rscPrd.ts and rsc-handler*.ts @@ -87,8 +87,9 @@ export async function runBuild(config: Config = {}) { } const require = createRequire(import.meta.url); + const customModules = await getCustomModulesRSC(); const clientEntryFileSet = new Set(); - const serverEntryFileSet = new Set(); + const serverEntryFileSet = new Set(customModules); await build({ root: dir, base: basePath, @@ -100,8 +101,11 @@ export async function runBuild(config: Config = {}) { ], build: { outDir: distPath, - ssr: entriesFile, write: false, + ssr: true, + rollupOptions: { + input: [entriesFile, ...customModules], + }, }, }); const clientEntryFiles = Object.fromEntries( @@ -192,7 +196,7 @@ export async function runBuild(config: Config = {}) { `export const clientEntries=${JSON.stringify(clientEntries)};` ); - await prerenderRSC(true); + await buildRSC(); const origPackageJson = require(path.join(dir, "package.json")); const packageJson = { diff --git a/src/cli-start.ts b/src/cli-start.ts index 83d4d0176..aeada996e 100644 --- a/src/cli-start.ts +++ b/src/cli-start.ts @@ -1,5 +1,7 @@ import { startPrdServer } from "./prdServer.js"; +// FIXME maybe we should set NODE_ENV=production + const config = process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG); diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 491fcc41a..78354ebcf 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -10,7 +10,12 @@ import RSDWServer from "react-server-dom-webpack/server"; import { transformRsfId, generatePrefetchCode } from "./rsc-utils.js"; import type { RenderInput, MessageReq, MessageRes } from "./rsc-handler.js"; import type { Config } from "../../config.js"; -import type { GetEntry, Prefetcher, Prerenderer } from "../../server.js"; +import type { + GetEntry, + Prefetcher, + Prerenderer, + GetBuilder, +} from "../../server.js"; import { rscPlugin } from "./vite-rsc-plugin.js"; const { renderToPipeableStream } = RSDWServer; @@ -95,6 +100,47 @@ const handlePrerender = async (mesg: MessageReq & { type: "prerender" }) => { } }; +const handleGetCustomModules = async ( + mesg: MessageReq & { type: "getCustomModules" } +) => { + const { id } = mesg; + try { + const modules = await getCustomModulesRSC(); + const mesg: MessageRes = { + id, + type: "customModules", + modules, + }; + parentPort!.postMessage(mesg); + } catch (err) { + const mesg: MessageRes = { + id, + type: "err", + err, + }; + parentPort!.postMessage(mesg); + } +}; + +const handleBuild = async (mesg: MessageReq & { type: "build" }) => { + const { id } = mesg; + try { + await buildRSC(); + const mesg: MessageRes = { + id, + type: "end", + }; + parentPort!.postMessage(mesg); + } catch (err) { + const mesg: MessageRes = { + id, + type: "err", + err, + }; + parentPort!.postMessage(mesg); + } +}; + parentPort!.on("message", (mesg: MessageReq) => { if (mesg.type === "render") { handleRender(mesg); @@ -102,6 +148,10 @@ parentPort!.on("message", (mesg: MessageReq) => { handlePrefetcher(mesg); } else if (mesg.type === "prerender") { handlePrerender(mesg); + } else if (mesg.type === "getCustomModules") { + handleGetCustomModules(mesg); + } else if (mesg.type === "build") { + handleBuild(mesg); } }); @@ -159,10 +209,8 @@ const getFunctionComponent = async (rscId: string) => { throw new Error("No function component found"); }; -async function renderRSC( - input: RenderInput, - loadClientEntries: boolean -): Promise { +// FIXME better function name? decodeId seems too general +const getDecodeId = async (loadClientEntries: boolean) => { let clientEntries: Record | undefined; if (loadClientEntries) { ({ clientEntries } = await loadServerFile(entriesFile)); @@ -198,6 +246,15 @@ async function renderRSC( return [id, name]; }; + return decodeId; +}; + +async function renderRSC( + input: RenderInput, + loadClientEntries: boolean +): Promise { + const decodeId = await getDecodeId(loadClientEntries); + const bundlerConfig = new Proxy( {}, { @@ -238,39 +295,7 @@ async function prefetcherRSC( ): Promise { let code = ""; - // FIXME duplicated code with renderRSC - let clientEntries: Record | undefined; - if (loadClientEntries) { - ({ clientEntries } = await loadServerFile(entriesFile)); - if (!clientEntries) { - throw new Error("Failed to load clientEntries"); - } - } - const getClientEntry = (id: string) => { - if (!clientEntries) { - return id; - } - const clientEntry = - clientEntries[id] || - clientEntries[id.replace(/\.js$/, ".ts")] || - clientEntries[id.replace(/\.js$/, ".tsx")] || - clientEntries[id.replace(/\.js$/, ".jsx")]; - if (!clientEntry) { - throw new Error("No client entry found"); - } - return clientEntry; - }; - const decodeId = (encodedId: string): [id: string, name: string] => { - let [id, name] = encodedId.split("#") as [string, string]; - if (!id.startsWith("wakuwork/")) { - id = path.relative( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : ""), - id - ); - id = basePath + getClientEntry(id); - } - return [id, name]; - }; + const decodeId = await getDecodeId(loadClientEntries); const { prefetcher } = await (loadServerFile(entriesFile) as Promise<{ prefetcher: Prefetcher; @@ -297,39 +322,7 @@ async function prerenderRSC(loadClientEntries: boolean): Promise { prerenderer?: Prerenderer; }>); - // FIXME duplicated code with renderRSC - let clientEntries: Record | undefined; - if (loadClientEntries) { - ({ clientEntries } = await loadServerFile(entriesFile)); - if (!clientEntries) { - throw new Error("Failed to load clientEntries"); - } - } - const getClientEntry = (id: string) => { - if (!clientEntries) { - return id; - } - const clientEntry = - clientEntries[id] || - clientEntries[id.replace(/\.js$/, ".ts")] || - clientEntries[id.replace(/\.js$/, ".tsx")] || - clientEntries[id.replace(/\.js$/, ".jsx")]; - if (!clientEntry) { - throw new Error("No client entry found"); - } - return clientEntry; - }; - const decodeId = (encodedId: string): [id: string, name: string] => { - let [id, name] = encodedId.split("#") as [string, string]; - if (!id.startsWith("wakuwork/")) { - id = path.relative( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : ""), - id - ); - id = basePath + getClientEntry(id); - } - return [id, name]; - }; + const decodeId = await getDecodeId(loadClientEntries); if (prerenderer) { const { @@ -391,3 +384,124 @@ async function prerenderRSC(loadClientEntries: boolean): Promise { } } } + +async function getCustomModulesRSC(): Promise { + const { getBuilder } = await (loadServerFile(entriesFile) as Promise<{ + getBuilder?: GetBuilder; + }>); + if (!getBuilder) { + return []; + } + const decodeId = await getDecodeId(true); + const pathMap = await getBuilder(decodeId); + return Object.values(pathMap).flatMap((x) => + Array.from(x.customModules || []) + ); +} + +// FIXME this may take too much responsibility +async function buildRSC(): Promise { + const { getBuilder } = await (loadServerFile(entriesFile) as Promise<{ + getBuilder?: GetBuilder; + }>); + if (!getBuilder) { + console.warn( + "getBuilder is undefined. It's recommended for optimization and sometimes required." + ); + return; + } + + const decodeId = await getDecodeId(true); + + const pathMap = await getBuilder(decodeId); + const clientModuleMap = new Map>(); + const addClientModule = (pathStr: string, id: string) => { + let idSet = clientModuleMap.get(pathStr); + if (!idSet) { + idSet = new Set(); + clientModuleMap.set(pathStr, idSet); + } + idSet.add(id); + }; + await Promise.all( + Object.entries(pathMap).map(([pathStr, { elements }]) => + Promise.all( + Array.from(elements || []).map(async ([rscId, props]) => { + // FIXME we blindly expect JSON.stringify usage is deterministic + const serializedProps = JSON.stringify(props); + const searchParams = new URLSearchParams(); + searchParams.set("props", serializedProps); + const destFile = path.join( + dir, + publicPath, + "RSC", + decodeURIComponent(rscId), + decodeURIComponent(`${searchParams}`) + ); + fs.mkdirSync(path.dirname(destFile), { recursive: true }); + const bundlerConfig = new Proxy( + {}, + { + get(_target, encodedId: string) { + const [id, name] = decodeId(encodedId); + addClientModule(pathStr, id); + return { id, chunks: [id], name, async: true }; + }, + } + ); + const component = await getFunctionComponent(rscId); + const pipeable = renderToPipeableStream( + createElement(component, props as any), + bundlerConfig + ).pipe( + transformRsfId( + path.join( + dir, + process.env.WAKUWORK_CMD === "build" ? distPath : "" + ) + ) + ); + await new Promise((resolve, reject) => { + const stream = fs.createWriteStream(destFile); + stream.on("finish", resolve); + stream.on("error", reject); + pipeable.pipe(stream); + }); + }) + ) + ) + ); + + const publicIndexHtml = fs.readFileSync(publicIndexHtmlFile, { + encoding: "utf8", + }); + await Promise.all( + Object.entries(pathMap).map( + async ([pathStr, { elements, unstable_customCode }]) => { + const destFile = path.join( + dir, + publicPath, + pathStr, + pathStr.endsWith("/") ? "index.html" : "" + ); + let data = ""; + if (fs.existsSync(destFile)) { + data = fs.readFileSync(destFile, { encoding: "utf8" }); + } else { + fs.mkdirSync(path.dirname(destFile), { recursive: true }); + data = publicIndexHtml; + } + const code = + generatePrefetchCode( + elements || [], + clientModuleMap.get(pathStr) || [] + ) + unstable_customCode; + if (code) { + // HACK is this too naive to inject script code? + data = data.replace(/<\/body>/, ``); + } + fs.writeFileSync(destFile, data, { encoding: "utf8" }); + } + ) + ); +} diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index 19c8ee932..1d719264f 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -30,13 +30,22 @@ export type MessageReq = id: number; type: "prerender"; loadClientEntries: boolean; + } + | { + id: number; + type: "getCustomModules"; + } + | { + id: number; + type: "build"; }; export type MessageRes = | { id: number; type: "buf"; buf: ArrayBuffer; offset: number; len: number } | { id: number; type: "end" } | { id: number; type: "err"; err: unknown } - | { id: number; type: "prefetcher"; code: string }; + | { id: number; type: "prefetcher"; code: string } + | { id: number; type: "customModules"; modules: string[] }; const messageCallbacks = new Map void>(); @@ -75,6 +84,7 @@ export function renderRSC( return passthrough; } +// TODO remove export function prefetcherRSC( pathItem: string, loadClientEntries: boolean @@ -100,9 +110,8 @@ export function prefetcherRSC( }); } -export function prerenderRSC( - loadClientEntries: boolean -): Promise { +// TODO remove +export function prerenderRSC(loadClientEntries: boolean): Promise { return new Promise((resolve, reject) => { const id = nextId++; messageCallbacks.set(id, (mesg) => { @@ -122,3 +131,43 @@ export function prerenderRSC( worker.postMessage(mesg); }); } + +export function getCustomModulesRSC(): Promise { + return new Promise((resolve, reject) => { + const id = nextId++; + messageCallbacks.set(id, (mesg) => { + if (mesg.type === "customModules") { + resolve(mesg.modules); + messageCallbacks.delete(id); + } else if (mesg.type === "err") { + reject(mesg.err); + messageCallbacks.delete(id); + } + }); + const mesg: MessageReq = { + id, + type: "getCustomModules", + }; + worker.postMessage(mesg); + }); +} + +export function buildRSC(): Promise { + return new Promise((resolve, reject) => { + const id = nextId++; + messageCallbacks.set(id, (mesg) => { + if (mesg.type === "end") { + resolve(); + messageCallbacks.delete(id); + } else if (mesg.type === "err") { + reject(mesg.err); + messageCallbacks.delete(id); + } + }); + const mesg: MessageReq = { + id, + type: "build", + }; + worker.postMessage(mesg); + }); +} diff --git a/src/middleware/lib/vite-rsc-plugin.ts b/src/middleware/lib/vite-rsc-plugin.ts index f6c7044fb..e2a2c6e78 100644 --- a/src/middleware/lib/vite-rsc-plugin.ts +++ b/src/middleware/lib/vite-rsc-plugin.ts @@ -8,6 +8,7 @@ export const rscPlugin = (): Plugin => { if (!id.endsWith(".js")) { return id; } + // FIXME This isn't necessary in production mode for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { const resolved = await this.resolve(id.slice(0, -3) + ext, importer, { ...options, diff --git a/src/router/server.ts b/src/router/server.ts index 7bc6e7e65..cf76784a7 100644 --- a/src/router/server.ts +++ b/src/router/server.ts @@ -171,6 +171,7 @@ globalThis.__WAKUWORK_ROUTER_PREFETCH__ = (pathname, search) => { }; }; + return { getEntry }; // TODO return { getEntry, prefetcher, prerenderer }; } diff --git a/src/server.ts b/src/server.ts index e2ec10215..97ec9e1c0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,16 +1,20 @@ import type { FunctionComponent } from "react"; +// TODO revisit entries API (prefer export default ...?) + export type GetEntry = ( rscId: string ) => Promise; // For run-time optimization (plus, for build-time optimization with `paths`) +// TODO remove export type Prefetcher = (path: string) => Promise<{ entryItems?: Iterable; clientModules?: Iterable; }>; // For build-time optimization +// TODO remove export type Prerenderer = () => Promise<{ entryItems?: Iterable; paths?: Iterable; @@ -19,3 +23,14 @@ export type Prerenderer = () => Promise<{ decodeId: (encodedId: string) => [id: string, name: string] ) => string; }>; + +export type GetBuilder = ( + // FIXME can we somehow avoid leaking internal implementation? + unstable_decodeId: (encodedId: string) => [id: string, name: string] +) => Promise<{ + [pathStr: string]: { + elements?: Iterable; + customModules?: Iterable; // for ignored dynamic imports + unstable_customCode?: string; + }; +}>; From a7d262f6dbcecc193de704f9162933ab8a93463e Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 16 May 2023 14:15:02 +0900 Subject: [PATCH 12/37] use getBuilder in example 01-06 --- examples/01_counter/entries.ts | 21 ++----- examples/02_async/entries.ts | 21 ++----- examples/03_promise/entries.ts | 21 ++----- examples/04_callserver/entries.ts | 21 ++----- examples/05_mutation/entries.ts | 21 ++----- examples/06_nesting/entries.ts | 40 +++++-------- src/middleware/lib/rsc-handler-worker.ts | 72 +++++++++++++----------- src/server.ts | 14 +++-- 8 files changed, 86 insertions(+), 145 deletions(-) diff --git a/examples/01_counter/entries.ts b/examples/01_counter/entries.ts index 2ec5be310..6c728c83d 100644 --- a/examples/01_counter/entries.ts +++ b/examples/01_counter/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -9,21 +9,10 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [["App", { name: "Wakuwork" }]], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [["App", { name: "Wakuwork" }]], - paths: ["/"], + "/": { + elements: [["App", { name: "Wakuwork" }]], + }, }; }; diff --git a/examples/02_async/entries.ts b/examples/02_async/entries.ts index 2ec5be310..6c728c83d 100644 --- a/examples/02_async/entries.ts +++ b/examples/02_async/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -9,21 +9,10 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [["App", { name: "Wakuwork" }]], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [["App", { name: "Wakuwork" }]], - paths: ["/"], + "/": { + elements: [["App", { name: "Wakuwork" }]], + }, }; }; diff --git a/examples/03_promise/entries.ts b/examples/03_promise/entries.ts index 2ec5be310..6c728c83d 100644 --- a/examples/03_promise/entries.ts +++ b/examples/03_promise/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -9,21 +9,10 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [["App", { name: "Wakuwork" }]], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [["App", { name: "Wakuwork" }]], - paths: ["/"], + "/": { + elements: [["App", { name: "Wakuwork" }]], + }, }; }; diff --git a/examples/04_callserver/entries.ts b/examples/04_callserver/entries.ts index 2ec5be310..6c728c83d 100644 --- a/examples/04_callserver/entries.ts +++ b/examples/04_callserver/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -9,21 +9,10 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [["App", { name: "Wakuwork" }]], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [["App", { name: "Wakuwork" }]], - paths: ["/"], + "/": { + elements: [["App", { name: "Wakuwork" }]], + }, }; }; diff --git a/examples/05_mutation/entries.ts b/examples/05_mutation/entries.ts index 2ec5be310..6c728c83d 100644 --- a/examples/05_mutation/entries.ts +++ b/examples/05_mutation/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -9,21 +9,10 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [["App", { name: "Wakuwork" }]], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [["App", { name: "Wakuwork" }]], - paths: ["/"], + "/": { + elements: [["App", { name: "Wakuwork" }]], + }, }; }; diff --git a/examples/06_nesting/entries.ts b/examples/06_nesting/entries.ts index b40e856e8..b6328aa02 100644 --- a/examples/06_nesting/entries.ts +++ b/examples/06_nesting/entries.ts @@ -1,4 +1,4 @@ -import type { GetEntry, Prefetcher, Prerenderer } from "wakuwork/server"; +import type { GetEntry, GetBuilder } from "wakuwork/server"; export const getEntry: GetEntry = async (id) => { switch (id) { @@ -11,32 +11,18 @@ export const getEntry: GetEntry = async (id) => { } }; -export const prefetcher: Prefetcher = async (path) => { - switch (path) { - case "/": - return { - entryItems: [ - ["App", { name: "Wakuwork" }], - ["InnerApp", { count: 0 }], - ], - clientModules: [(await import("./src/Counter.js")).Counter], - }; - default: - return {}; - } -}; - -export const prerenderer: Prerenderer = async () => { +export const getBuilder: GetBuilder = async () => { return { - entryItems: [ - ["App", { name: "Wakuwork" }], - ["InnerApp", { count: 0 }], - ["InnerApp", { count: 1 }], - ["InnerApp", { count: 2 }], - ["InnerApp", { count: 3 }], - ["InnerApp", { count: 4 }], - ["InnerApp", { count: 5 }], - ], - paths: ["/"], + "/": { + elements: [ + ["App", { name: "Wakuwork" }], + ["InnerApp", { count: 0 }], + ["InnerApp", { count: 1 }, true], + ["InnerApp", { count: 2 }, true], + ["InnerApp", { count: 3 }, true], + ["InnerApp", { count: 4 }, true], + ["InnerApp", { count: 5 }, true], + ], + }, }; }; diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 78354ebcf..58f49f7b1 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -15,6 +15,7 @@ import type { Prefetcher, Prerenderer, GetBuilder, + GetCustomModules, } from "../../server.js"; import { rscPlugin } from "./vite-rsc-plugin.js"; @@ -178,6 +179,7 @@ const publicIndexHtmlFile = path.join( publicPath, config.files?.indexHtml || "index.html" ); +const srcEntriesFile = path.join(dir, config.files?.entriesJs || "entries.js"); const entriesFile = path.join( dir, process.env.WAKUWORK_CMD === "build" ? distPath : "", @@ -386,17 +388,16 @@ async function prerenderRSC(loadClientEntries: boolean): Promise { } async function getCustomModulesRSC(): Promise { - const { getBuilder } = await (loadServerFile(entriesFile) as Promise<{ - getBuilder?: GetBuilder; + const { getCustomModules } = await (loadServerFile( + srcEntriesFile + ) as Promise<{ + getCustomModules?: GetCustomModules; }>); - if (!getBuilder) { + if (!getCustomModules) { return []; } - const decodeId = await getDecodeId(true); - const pathMap = await getBuilder(decodeId); - return Object.values(pathMap).flatMap((x) => - Array.from(x.customModules || []) - ); + const modules = await getCustomModules(); + return Array.from(modules); } // FIXME this may take too much responsibility @@ -476,32 +477,35 @@ async function buildRSC(): Promise { encoding: "utf8", }); await Promise.all( - Object.entries(pathMap).map( - async ([pathStr, { elements, unstable_customCode }]) => { - const destFile = path.join( - dir, - publicPath, - pathStr, - pathStr.endsWith("/") ? "index.html" : "" - ); - let data = ""; - if (fs.existsSync(destFile)) { - data = fs.readFileSync(destFile, { encoding: "utf8" }); - } else { - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - data = publicIndexHtml; - } - const code = - generatePrefetchCode( - elements || [], - clientModuleMap.get(pathStr) || [] - ) + unstable_customCode; - if (code) { - // HACK is this too naive to inject script code? - data = data.replace(/<\/body>/, ``); - } - fs.writeFileSync(destFile, data, { encoding: "utf8" }); + Object.entries(pathMap).map(async ([pathStr, { elements, customCode }]) => { + const destFile = path.join( + dir, + publicPath, + pathStr, + pathStr.endsWith("/") ? "index.html" : "" + ); + let data = ""; + if (fs.existsSync(destFile)) { + data = fs.readFileSync(destFile, { encoding: "utf8" }); + } else { + fs.mkdirSync(path.dirname(destFile), { recursive: true }); + data = publicIndexHtml; } - ) + const code = + generatePrefetchCode( + Array.from(elements || []).flatMap(([rscId, props, skipPrefetch]) => { + if (skipPrefetch) { + return []; + } + return [[rscId, props]]; + }), + clientModuleMap.get(pathStr) || [] + ) + (customCode || ""); + if (code) { + // HACK is this too naive to inject script code? + data = data.replace(/<\/body>/, ``); + } + fs.writeFileSync(destFile, data, { encoding: "utf8" }); + }) ); } diff --git a/src/server.ts b/src/server.ts index 97ec9e1c0..84a0da5c0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,8 @@ import type { FunctionComponent } from "react"; -// TODO revisit entries API (prefer export default ...?) +// TODO revisit entries API +// - prefer export default? +// - return null from getEntry for 404, instead of throwing export type GetEntry = ( rscId: string @@ -29,8 +31,12 @@ export type GetBuilder = ( unstable_decodeId: (encodedId: string) => [id: string, name: string] ) => Promise<{ [pathStr: string]: { - elements?: Iterable; - customModules?: Iterable; // for ignored dynamic imports - unstable_customCode?: string; + elements?: Iterable< + readonly [rscId: string, props: unknown, skipPrefetch?: boolean] + >; + customCode?: string; // optional code to inject }; }>; + +// XXX Are there any better ways? +export type GetCustomModules = () => Promise>; // for ignored dynamic imports From e1090d07cb95438a3afdf6527fdd332d2e562702 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 16 May 2023 14:26:52 +0900 Subject: [PATCH 13/37] wip remove prefetcher and prerenderer --- src/builder.ts | 8 +- src/middleware/indexFallback.ts | 15 +-- src/middleware/lib/common.ts | 5 +- src/middleware/lib/rsc-handler-worker.ts | 148 +---------------------- src/middleware/lib/rsc-handler.ts | 60 --------- src/middleware/lib/rsc-utils.ts | 5 + src/middleware/rscDev.ts | 14 +-- src/middleware/rscPrd.ts | 9 +- src/middleware/viteServer.ts | 28 ++--- src/server.ts | 18 --- 10 files changed, 26 insertions(+), 284 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index 656057e9a..b8e591193 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -8,24 +8,20 @@ import react from "@vitejs/plugin-react"; import * as swc from "@swc/core"; import type { Config } from "./config.js"; +import { codeToInject } from "./middleware/lib/rsc-utils.js"; import { getCustomModulesRSC, buildRSC } from "./middleware/lib/rsc-handler.js"; // TODO we have duplicate code here and rscPrd.ts and rsc-handler*.ts // FIXME we could do this without plugin anyway const rscIndexPlugin = (): Plugin => { - const code = ` -globalThis.__wakuwork_module_cache__ = new Map(); -globalThis.__webpack_chunk_load__ = async (id) => id.startsWith("wakuwork/") || import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); -globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id); -`; return { name: "rsc-index-plugin", async transformIndexHtml() { return [ { tag: "script", - children: code, + children: codeToInject, injectTo: "body", }, ]; diff --git a/src/middleware/indexFallback.ts b/src/middleware/indexFallback.ts index dbb79717f..a602a1b9d 100644 --- a/src/middleware/indexFallback.ts +++ b/src/middleware/indexFallback.ts @@ -1,10 +1,9 @@ import path from "node:path"; import fs from "node:fs"; -import fsPromises from "node:fs/promises"; import type { MiddlewareCreator } from "./lib/common.js"; -const staticFile: MiddlewareCreator = (config, shared) => { +const staticFile: MiddlewareCreator = (config) => { const dir = path.resolve(config.prdServer?.dir || "."); const publicPath = config.files?.public || "public"; const indexHtml = config.files?.indexHtml || "index.html"; @@ -17,18 +16,6 @@ const staticFile: MiddlewareCreator = (config, shared) => { const stat = fs.statSync(indexHtmlFile, { throwIfNoEntry: false }); if (stat) { res.setHeader("Content-Type", "text/html; charset=utf-8"); - const code = await shared.prdScriptToInject?.(req.url || ""); - if (code) { - let data = await fsPromises.readFile(indexHtmlFile, { - encoding: "utf-8", - }); - const scriptToInject = ``; - if (!data.includes(scriptToInject)) { - data = data.replace(/<\/body>/, `${scriptToInject}`); - } - res.end(data); - return; - } res.setHeader("Content-Length", stat.size); fs.createReadStream(indexHtmlFile).pipe(res); return; diff --git a/src/middleware/lib/common.ts b/src/middleware/lib/common.ts index 82c3119fe..5e6c8953f 100644 --- a/src/middleware/lib/common.ts +++ b/src/middleware/lib/common.ts @@ -1,9 +1,6 @@ import type { Config, Middleware } from "../../config.js"; -export type Shared = { - devScriptToInject?: (path: string) => Promise; - prdScriptToInject?: (path: string) => Promise; -}; +export type Shared = {}; export type MiddlewareCreator = (config: Config, shared: Shared) => Middleware; diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 58f49f7b1..aef05c00f 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -10,17 +10,10 @@ import RSDWServer from "react-server-dom-webpack/server"; import { transformRsfId, generatePrefetchCode } from "./rsc-utils.js"; import type { RenderInput, MessageReq, MessageRes } from "./rsc-handler.js"; import type { Config } from "../../config.js"; -import type { - GetEntry, - Prefetcher, - Prerenderer, - GetBuilder, - GetCustomModules, -} from "../../server.js"; +import type { GetEntry, GetBuilder, GetCustomModules } from "../../server.js"; import { rscPlugin } from "./vite-rsc-plugin.js"; const { renderToPipeableStream } = RSDWServer; -const CLIENT_REFERENCE = Symbol.for("react.client.reference"); const handleRender = async (mesg: MessageReq & { type: "render" }) => { const { id, input, loadClientEntries } = mesg; @@ -62,45 +55,6 @@ const handleRender = async (mesg: MessageReq & { type: "render" }) => { } }; -const handlePrefetcher = async (mesg: MessageReq & { type: "prefetcher" }) => { - const { id, pathItem, loadClientEntries } = mesg; - try { - const code = await prefetcherRSC(pathItem, loadClientEntries); - const mesg: MessageRes = { - id, - type: "prefetcher", - code, - }; - parentPort!.postMessage(mesg); - } catch (err) { - const mesg: MessageRes = { - id, - type: "err", - err, - }; - parentPort!.postMessage(mesg); - } -}; - -const handlePrerender = async (mesg: MessageReq & { type: "prerender" }) => { - const { id, loadClientEntries } = mesg; - try { - await prerenderRSC(loadClientEntries); - const mesg: MessageRes = { - id, - type: "end", - }; - parentPort!.postMessage(mesg); - } catch (err) { - const mesg: MessageRes = { - id, - type: "err", - err, - }; - parentPort!.postMessage(mesg); - } -}; - const handleGetCustomModules = async ( mesg: MessageReq & { type: "getCustomModules" } ) => { @@ -145,10 +99,6 @@ const handleBuild = async (mesg: MessageReq & { type: "build" }) => { parentPort!.on("message", (mesg: MessageReq) => { if (mesg.type === "render") { handleRender(mesg); - } else if (mesg.type === "prefetcher") { - handlePrefetcher(mesg); - } else if (mesg.type === "prerender") { - handlePrerender(mesg); } else if (mesg.type === "getCustomModules") { handleGetCustomModules(mesg); } else if (mesg.type === "build") { @@ -291,102 +241,6 @@ async function renderRSC( throw new Error("Unexpected input"); } -async function prefetcherRSC( - pathItem: string, - loadClientEntries: boolean -): Promise { - let code = ""; - - const decodeId = await getDecodeId(loadClientEntries); - - const { prefetcher } = await (loadServerFile(entriesFile) as Promise<{ - prefetcher: Prefetcher; - }>); - const { entryItems = [], clientModules = [] } = prefetcher - ? await prefetcher(pathItem) - : {}; - const moduleIds: string[] = []; - for (const m of clientModules as any[]) { - if (m["$$typeof"] !== CLIENT_REFERENCE) { - throw new Error("clientModules must be client references"); - } - const [id] = decodeId(m["$$id"]); - moduleIds.push(id); - } - code += generatePrefetchCode(entryItems, moduleIds); - return code; -} - -// TODO this takes too much responsibility -// FIXME it shouldn't depend on `fs` -async function prerenderRSC(loadClientEntries: boolean): Promise { - const { prerenderer } = await (loadServerFile(entriesFile) as Promise<{ - prerenderer?: Prerenderer; - }>); - - const decodeId = await getDecodeId(loadClientEntries); - - if (prerenderer) { - const { - entryItems = [], - paths = [], - unstable_customCode = () => "", - } = await prerenderer(); - await Promise.all( - Array.from(entryItems).map(async ([rscId, props]) => { - // FIXME we blindly expect JSON.stringify usage is deterministic - const serializedProps = JSON.stringify(props); - const searchParams = new URLSearchParams(); - searchParams.set("props", serializedProps); - const destFile = path.join( - dir, - publicPath, - "RSC", - decodeURIComponent(rscId), - decodeURIComponent(`${searchParams}`) - ); - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - const pipeable = await renderRSC({ rscId, props: props as any }, true); - await new Promise((resolve, reject) => { - const stream = fs.createWriteStream(destFile); - stream.on("finish", resolve); - stream.on("error", reject); - pipeable.pipe(stream); - }); - }) - ); - - const publicIndexHtml = fs.readFileSync(publicIndexHtmlFile, { - encoding: "utf8", - }); - for (const pathItem of paths) { - const code = await prefetcherRSC(pathItem, true); - const destFile = path.join( - dir, - publicPath, - pathItem, - pathItem.endsWith("/") ? "index.html" : "" - ); - let data = ""; - if (fs.existsSync(destFile)) { - data = fs.readFileSync(destFile, { encoding: "utf8" }); - } else { - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - data = publicIndexHtml; - } - if (code) { - // HACK is this too naive to inject script code? - data = data.replace(/<\/body>/, ``); - } - const code2 = unstable_customCode(pathItem, decodeId); - if (code2) { - data = data.replace(/<\/body>/, ``); - } - fs.writeFileSync(destFile, data, { encoding: "utf8" }); - } - } -} - async function getCustomModulesRSC(): Promise { const { getCustomModules } = await (loadServerFile( srcEntriesFile diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index 1d719264f..4a4f154a6 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -20,17 +20,6 @@ export type MessageReq = input: RenderInput; loadClientEntries: boolean; } - | { - id: number; - type: "prefetcher"; - pathItem: string; - loadClientEntries: boolean; - } - | { - id: number; - type: "prerender"; - loadClientEntries: boolean; - } | { id: number; type: "getCustomModules"; @@ -44,7 +33,6 @@ export type MessageRes = | { id: number; type: "buf"; buf: ArrayBuffer; offset: number; len: number } | { id: number; type: "end" } | { id: number; type: "err"; err: unknown } - | { id: number; type: "prefetcher"; code: string } | { id: number; type: "customModules"; modules: string[] }; const messageCallbacks = new Map void>(); @@ -84,54 +72,6 @@ export function renderRSC( return passthrough; } -// TODO remove -export function prefetcherRSC( - pathItem: string, - loadClientEntries: boolean -): Promise { - return new Promise((resolve, reject) => { - const id = nextId++; - messageCallbacks.set(id, (mesg) => { - if (mesg.type === "prefetcher") { - resolve(mesg.code); - messageCallbacks.delete(id); - } else if (mesg.type === "err") { - reject(mesg.err); - messageCallbacks.delete(id); - } - }); - const mesg: MessageReq = { - id, - type: "prefetcher", - pathItem, - loadClientEntries, - }; - worker.postMessage(mesg); - }); -} - -// TODO remove -export function prerenderRSC(loadClientEntries: boolean): Promise { - return new Promise((resolve, reject) => { - const id = nextId++; - messageCallbacks.set(id, (mesg) => { - if (mesg.type === "end") { - resolve(); - messageCallbacks.delete(id); - } else if (mesg.type === "err") { - reject(mesg.err); - messageCallbacks.delete(id); - } - }); - const mesg: MessageReq = { - id, - type: "prerender", - loadClientEntries, - }; - worker.postMessage(mesg); - }); -} - export function getCustomModulesRSC(): Promise { return new Promise((resolve, reject) => { const id = nextId++; diff --git a/src/middleware/lib/rsc-utils.ts b/src/middleware/lib/rsc-utils.ts index d410cfc15..5bff577e6 100644 --- a/src/middleware/lib/rsc-utils.ts +++ b/src/middleware/lib/rsc-utils.ts @@ -3,6 +3,11 @@ import { Buffer } from "node:buffer"; import { Transform } from "node:stream"; +export const codeToInject = ` +globalThis.__wakuwork_module_cache__ = new Map(); +globalThis.__webpack_chunk_load__ = async (id) => id.startsWith("wakuwork/") || import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); +globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id);`; + export const generatePrefetchCode = ( entryItemsIterable: Iterable, moduleIds: Iterable diff --git a/src/middleware/rscDev.ts b/src/middleware/rscDev.ts index 284f00a98..948760fb6 100644 --- a/src/middleware/rscDev.ts +++ b/src/middleware/rscDev.ts @@ -3,21 +3,11 @@ import busboy from "busboy"; import type { MiddlewareCreator } from "./lib/common.js"; -import { renderRSC, prefetcherRSC } from "./lib/rsc-handler.js"; +import { renderRSC } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; -const rscDev: MiddlewareCreator = (_config, shared) => { - shared.devScriptToInject = async (pathItem: string) => { - const code = - ` -globalThis.__wakuwork_module_cache__ = new Map(); -globalThis.__webpack_chunk_load__ = async (id) => id.startsWith("wakuwork/") || import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); -globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id);` + - (await prefetcherRSC(pathItem, false)); - return code; - }; - +const rscDev: MiddlewareCreator = () => { return async (req, res, next) => { const rscId = req.headers["x-react-server-component-id"]; const rsfId = req.headers["x-react-server-function-id"]; diff --git a/src/middleware/rscPrd.ts b/src/middleware/rscPrd.ts index efb572947..e47fce69b 100644 --- a/src/middleware/rscPrd.ts +++ b/src/middleware/rscPrd.ts @@ -2,18 +2,13 @@ import RSDWServer from "react-server-dom-webpack/server.node.unbundled"; import busboy from "busboy"; import type { MiddlewareCreator } from "./lib/common.js"; -import { renderRSC, prefetcherRSC } from "./lib/rsc-handler.js"; +import { renderRSC } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; // TODO we have duplicate code here and rsc-handler-worker.ts -const rscPrd: MiddlewareCreator = (_config, shared) => { - shared.prdScriptToInject = async (pathItem: string) => { - const code = await prefetcherRSC(pathItem, true); - return code; - }; - +const rscPrd: MiddlewareCreator = () => { return async (req, res, next) => { const rscId = req.headers["x-react-server-component-id"]; const rsfId = req.headers["x-react-server-function-id"]; diff --git a/src/middleware/viteServer.ts b/src/middleware/viteServer.ts index 0bda56511..c8377a76f 100644 --- a/src/middleware/viteServer.ts +++ b/src/middleware/viteServer.ts @@ -6,28 +6,24 @@ import type { Plugin } from "vite"; import react from "@vitejs/plugin-react"; import type { MiddlewareCreator } from "./lib/common.js"; +import { codeToInject } from "./lib/rsc-utils.js"; -const rscPlugin = ( - scriptToInject: (path: string) => Promise -): Plugin => { +const rscPlugin = (): Plugin => { return { name: "rscPlugin", - async transformIndexHtml(_html, ctx) { - const code = await scriptToInject(ctx.path); - if (code) { - return [ - { - tag: "script", - children: code, - injectTo: "body", - }, - ]; - } + async transformIndexHtml() { + return [ + { + tag: "script", + children: codeToInject, + injectTo: "body", + }, + ]; }, }; }; -const viteServer: MiddlewareCreator = (config, shared) => { +const viteServer: MiddlewareCreator = (config) => { const dir = path.resolve(config.devServer?.dir || "."); const indexHtml = config.files?.indexHtml || "index.html"; const indexHtmlFile = path.join(dir, indexHtml); @@ -36,7 +32,7 @@ const viteServer: MiddlewareCreator = (config, shared) => { plugins: [ // @ts-ignore react(), - rscPlugin(async (path: string) => shared.devScriptToInject?.(path) || ""), + rscPlugin(), ], server: { middlewareMode: true }, appType: "custom", diff --git a/src/server.ts b/src/server.ts index 84a0da5c0..16b05ea0a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,24 +8,6 @@ export type GetEntry = ( rscId: string ) => Promise; -// For run-time optimization (plus, for build-time optimization with `paths`) -// TODO remove -export type Prefetcher = (path: string) => Promise<{ - entryItems?: Iterable; - clientModules?: Iterable; -}>; - -// For build-time optimization -// TODO remove -export type Prerenderer = () => Promise<{ - entryItems?: Iterable; - paths?: Iterable; - unstable_customCode?: ( - path: string, - decodeId: (encodedId: string) => [id: string, name: string] - ) => string; -}>; - export type GetBuilder = ( // FIXME can we somehow avoid leaking internal implementation? unstable_decodeId: (encodedId: string) => [id: string, name: string] From c9b8468ee77b1d7925e3b5a85cf49d703f8394f0 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 16 May 2023 19:14:46 +0900 Subject: [PATCH 14/37] migrate router for new api, still wip --- examples/07_router/entries.ts | 2 +- src/builder.ts | 10 +- src/middleware/lib/rsc-handler-worker.ts | 6 +- src/middleware/lib/rsc-handler.ts | 10 +- src/middleware/rscDev.ts | 8 +- src/middleware/rscPrd.ts | 8 +- src/router/client.ts | 3 - src/router/server.ts | 148 +++++++++++++---------- src/server.ts | 5 +- website/entries.ts | 2 +- 10 files changed, 122 insertions(+), 80 deletions(-) diff --git a/examples/07_router/entries.ts b/examples/07_router/entries.ts index b749bdb90..7be6261b3 100644 --- a/examples/07_router/entries.ts +++ b/examples/07_router/entries.ts @@ -3,6 +3,6 @@ import url from "node:url"; import { fileRouter } from "wakuwork/router/server"; -export const { getEntry, prefetcher, prerenderer } = fileRouter( +export const { getEntry, getBuilder, getCustomModules } = fileRouter( path.join(path.dirname(url.fileURLToPath(import.meta.url)), "routes") ); diff --git a/src/builder.ts b/src/builder.ts index b8e591193..fce5b2fad 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -85,7 +85,7 @@ export async function runBuild(config: Config = {}) { const customModules = await getCustomModulesRSC(); const clientEntryFileSet = new Set(); - const serverEntryFileSet = new Set(customModules); + const serverEntryFileSet = new Set(); await build({ root: dir, base: basePath, @@ -100,7 +100,10 @@ export async function runBuild(config: Config = {}) { write: false, ssr: true, rollupOptions: { - input: [entriesFile, ...customModules], + input: { + entries: entriesFile, + ...customModules, + }, }, }, }); @@ -122,6 +125,7 @@ export async function runBuild(config: Config = {}) { entries: entriesFile, ...clientEntryFiles, ...serverEntryFiles, + ...customModules, }, output: { banner: (chunk) => { @@ -136,7 +140,7 @@ export async function runBuild(config: Config = {}) { return code; }, entryFileNames: (chunkInfo) => { - if (chunkInfo.name === "entries") { + if (chunkInfo.name === "entries" || customModules[chunkInfo.name]) { return "[name].js"; } return "assets/[name].js"; diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index aef05c00f..735413da8 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -241,17 +241,17 @@ async function renderRSC( throw new Error("Unexpected input"); } -async function getCustomModulesRSC(): Promise { +async function getCustomModulesRSC(): Promise<{ [name: string]: string }> { const { getCustomModules } = await (loadServerFile( srcEntriesFile ) as Promise<{ getCustomModules?: GetCustomModules; }>); if (!getCustomModules) { - return []; + return {}; } const modules = await getCustomModules(); - return Array.from(modules); + return modules; } // FIXME this may take too much responsibility diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index 4a4f154a6..f102bd7b2 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -13,6 +13,10 @@ export type RenderInput = { args?: unknown[] | undefined; }; +type CustomModules = { + [name: string]: string; +}; + export type MessageReq = | { id: number; @@ -33,7 +37,7 @@ export type MessageRes = | { id: number; type: "buf"; buf: ArrayBuffer; offset: number; len: number } | { id: number; type: "end" } | { id: number; type: "err"; err: unknown } - | { id: number; type: "customModules"; modules: string[] }; + | { id: number; type: "customModules"; modules: CustomModules }; const messageCallbacks = new Map void>(); @@ -72,8 +76,8 @@ export function renderRSC( return passthrough; } -export function getCustomModulesRSC(): Promise { - return new Promise((resolve, reject) => { +export function getCustomModulesRSC(): Promise { + return new Promise((resolve, reject) => { const id = nextId++; messageCallbacks.set(id, (mesg) => { if (mesg.type === "customModules") { diff --git a/src/middleware/rscDev.ts b/src/middleware/rscDev.ts index 948760fb6..e5e1e8449 100644 --- a/src/middleware/rscDev.ts +++ b/src/middleware/rscDev.ts @@ -40,7 +40,13 @@ const rscDev: MiddlewareCreator = () => { } } if (rscId || rsfId) { - renderRSC({ rscId, props, rsfId, args }, false).pipe(res); + const pipeable = renderRSC({ rscId, props, rsfId, args }, false); + pipeable.on("error", (err) => { + console.info("Cannot render RSC", err); + res.statusCode = 500; + res.end(String(err)); + }); + pipeable.pipe(res); return; } await next(); diff --git a/src/middleware/rscPrd.ts b/src/middleware/rscPrd.ts index e47fce69b..5e746739f 100644 --- a/src/middleware/rscPrd.ts +++ b/src/middleware/rscPrd.ts @@ -41,7 +41,13 @@ const rscPrd: MiddlewareCreator = () => { } } if (rscId || rsfId) { - renderRSC({ rscId, props, rsfId, args }, true).pipe(res); + const pipeable = renderRSC({ rscId, props, rsfId, args }, true); + pipeable.on("error", (err) => { + console.info("Cannot render RSC", err); + res.statusCode = 500; + res.end(); + }); + pipeable.pipe(res); return; } await next(); diff --git a/src/router/client.ts b/src/router/client.ts index 7a1f037a0..7c2447de9 100644 --- a/src/router/client.ts +++ b/src/router/client.ts @@ -45,9 +45,6 @@ export function useLocation() { // FIXME normalizing `search` before prefetch would be necessary. // FIXME ommitting `search` items would be important for caching. -// TODO prefetching dependent client modules in not supported yet. -// Is it only possible with build step? No runtime solution? - const prefetchRoutes = (pathname: string, search: string) => { const prefetched = ((globalThis as any).__WAKUWORK_PREFETCHED__ ||= {}); const pathItems = pathname.split("/").filter(Boolean); diff --git a/src/router/server.ts b/src/router/server.ts index cf76784a7..ea9cc6baa 100644 --- a/src/router/server.ts +++ b/src/router/server.ts @@ -3,11 +3,39 @@ import fs from "node:fs"; import { createElement } from "react"; import * as swc from "@swc/core"; -import type { GetEntry, Prefetcher, Prerenderer } from "../server.js"; +import type { GetEntry, GetBuilder, GetCustomModules } from "../server.js"; import { childReference, linkReference } from "./common.js"; import type { RouteProps, LinkProps } from "./common.js"; +const getAllFiles = (base: string, parent = ""): string[] => + fs + .readdirSync(path.join(base, parent), { withFileTypes: true }) + .flatMap((dirent) => { + if (dirent.isDirectory()) { + return getAllFiles(base, path.join(parent, dirent.name)); + } + const fname = path.join(parent, dirent.name); + return [fname]; + }); + +const getAllPaths = (base: string, parent = ""): string[] => + fs + .readdirSync(path.join(base, parent), { withFileTypes: true }) + .flatMap((dirent) => { + if (dirent.isDirectory()) { + return getAllPaths(base, path.join(parent, dirent.name)); + } + const fname = path.join(parent, path.parse(dirent.name).name); + const stat = fs.statSync(path.join(base, fname), { + throwIfNoEntry: false, + }); + if (stat?.isDirectory()) { + return [fname + "/"]; + } + return [fname]; + }); + const CLIENT_REFERENCE = Symbol.for("react.client.reference"); const resolveFileName = (fname: string) => { @@ -22,10 +50,10 @@ const resolveFileName = (fname: string) => { const findDependentModules = (fname: string) => { fname = resolveFileName(fname); - // TODO support ".js" and ".jsx" + const ext = path.extname(fname); const mod = swc.parseFileSync(fname, { - syntax: "typescript", - tsx: fname.endsWith(".tsx"), + syntax: ext === ".ts" || ext === ".tsx" ? "typescript" : "ecmascript", + tsx: ext === ".tsx", }); const modules: (readonly [fname: string, exportNames: string[]])[] = []; for (const item of mod.body) { @@ -53,42 +81,25 @@ const findDependentModules = (fname: string) => { return modules; }; -export function fileRouter(base: string) { - const findClientModules = async (id: string) => { - const fname = `${base}/${id}.js`; - const modules = findDependentModules(fname); - return ( - await Promise.all( - modules.map(async ([fname, exportNames]) => { - const m = await import(/* @vite-ignore */ fname); - return exportNames.flatMap((name) => { - if (m[name]?.["$$typeof"] === CLIENT_REFERENCE) { - return [m[name]]; - } - return []; - }); - }) - ) - ).flat(); - }; - - const getAllPaths = (dir = ""): string[] => - fs - .readdirSync(path.join(base, dir), { withFileTypes: true }) - .flatMap((dirent) => { - if (dirent.isDirectory()) { - return getAllPaths(path.join(dir, dirent.name)); - } - const fname = path.join(dir, path.parse(dirent.name).name); - const stat = fs.statSync(path.join(base, fname), { - throwIfNoEntry: false, +const findClientModules = async (base: string, id: string) => { + const fname = `${base}/${id}.js`; + const modules = findDependentModules(fname); + return ( + await Promise.all( + modules.map(async ([fname, exportNames]) => { + const m = await import(/* @vite-ignore */ fname); + return exportNames.flatMap((name) => { + if (m[name]?.["$$typeof"] === CLIENT_REFERENCE) { + return [m[name]]; + } + return []; }); - if (stat?.isDirectory()) { - return [fname + "/"]; - } - return [fname]; - }); + }) + ) + ).flat(); +}; +export function fileRouter(base: string) { const getEntry: GetEntry = async (id) => { // This can be too unsecure? FIXME const component = (await import(/* @vite-ignore */ `${base}/${id}.js`)) @@ -109,14 +120,14 @@ export function fileRouter(base: string) { return RouteComponent; }; - const prefetcher: Prefetcher = async (path) => { - const url = new URL(path || "", "http://localhost"); - const result: (readonly [id: string, props: RouteProps])[] = []; + const prefetcher = async (pathStr: string) => { + const url = new URL(pathStr || "", "http://localhost"); + const elements: (readonly [id: string, props: RouteProps])[] = []; const pathItems = url.pathname.split("/").filter(Boolean); const search = url.search; for (let index = 0; index <= pathItems.length; ++index) { const rscId = pathItems.slice(0, index).join("/") || "index"; - result.push([ + elements.push([ rscId, index < pathItems.length ? { childIndex: index + 1, search } @@ -125,29 +136,27 @@ export function fileRouter(base: string) { } const clientModules = new Set( ( - await Promise.all(result.map(([rscId]) => findClientModules(rscId))) + await Promise.all( + elements.map(([rscId]) => findClientModules(base, rscId)) + ) ).flat() ); - return { - entryItems: result, - clientModules, - }; + return { elements, clientModules }; }; - const prerenderer: Prerenderer = async () => { - const paths = getAllPaths().map((item) => + const getBuilder: GetBuilder = async ( + decodeId: (encodedId: string) => [id: string, name: string] + ) => { + const paths = getAllPaths(base).map((item) => item === "index" ? "/" : `/${item}` ); const prefetcherForPaths = await Promise.all(paths.map(prefetcher)); - const unstable_customCode = ( - _path: string, - decodeId: (encodedId: string) => [id: string, name: string] - ) => ` + const customCode = ` globalThis.__WAKUWORK_ROUTER_PREFETCH__ = (pathname, search) => { const path = search ? pathname + "?" + search : pathname; const path2ids = {${paths.map((pathItem, index) => { const moduleIds: string[] = []; - for (const m of prefetcherForPaths[index]?.clientModules as any[]) { + for (const m of prefetcherForPaths[index]?.clientModules || []) { if (m["$$typeof"] !== CLIENT_REFERENCE) { throw new Error("clientModules must be client references"); } @@ -162,17 +171,30 @@ globalThis.__WAKUWORK_ROUTER_PREFETCH__ = (pathname, search) => { import(id); } };`; - return { - entryItems: Array.from(prefetcherForPaths.values()).flatMap((item) => - Array.from(item.entryItems || []) - ), - paths, - unstable_customCode, - }; + return Object.fromEntries( + paths.map((pathStr, index) => { + return [ + pathStr, + { + elements: prefetcherForPaths[index]?.elements || [], + customCode, + }, + ]; + }) + ); + }; + + const getCustomModules: GetCustomModules = async () => { + return Object.fromEntries( + // TODO TEMP make "routes" configurable + getAllFiles(base).map((file) => [ + `routes/${file.replace(/\.\w+$/, "")}`, + `${base}/${file}`, + ]) + ); }; - return { getEntry }; // TODO - return { getEntry, prefetcher, prerenderer }; + return { getEntry, getBuilder, getCustomModules }; } export function Link(props: LinkProps) { diff --git a/src/server.ts b/src/server.ts index 16b05ea0a..41c7092cd 100644 --- a/src/server.ts +++ b/src/server.ts @@ -20,5 +20,8 @@ export type GetBuilder = ( }; }>; +// This is for ignored dynamic imports // XXX Are there any better ways? -export type GetCustomModules = () => Promise>; // for ignored dynamic imports +export type GetCustomModules = () => Promise<{ + [name: string]: string +}>; diff --git a/website/entries.ts b/website/entries.ts index b749bdb90..7be6261b3 100644 --- a/website/entries.ts +++ b/website/entries.ts @@ -3,6 +3,6 @@ import url from "node:url"; import { fileRouter } from "wakuwork/router/server"; -export const { getEntry, prefetcher, prerenderer } = fileRouter( +export const { getEntry, getBuilder, getCustomModules } = fileRouter( path.join(path.dirname(url.fileURLToPath(import.meta.url)), "routes") ); From d12e8714746733dab7afcd6ca1095fadbfbc91e5 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 16 May 2023 19:34:17 +0900 Subject: [PATCH 15/37] refactor avoid double promise.all --- src/middleware/lib/rsc-handler-worker.ts | 87 +++++++++++------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 735413da8..2c94520bb 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -279,52 +279,47 @@ async function buildRSC(): Promise { idSet.add(id); }; await Promise.all( - Object.entries(pathMap).map(([pathStr, { elements }]) => - Promise.all( - Array.from(elements || []).map(async ([rscId, props]) => { - // FIXME we blindly expect JSON.stringify usage is deterministic - const serializedProps = JSON.stringify(props); - const searchParams = new URLSearchParams(); - searchParams.set("props", serializedProps); - const destFile = path.join( - dir, - publicPath, - "RSC", - decodeURIComponent(rscId), - decodeURIComponent(`${searchParams}`) - ); - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - const bundlerConfig = new Proxy( - {}, - { - get(_target, encodedId: string) { - const [id, name] = decodeId(encodedId); - addClientModule(pathStr, id); - return { id, chunks: [id], name, async: true }; - }, - } - ); - const component = await getFunctionComponent(rscId); - const pipeable = renderToPipeableStream( - createElement(component, props as any), - bundlerConfig - ).pipe( - transformRsfId( - path.join( - dir, - process.env.WAKUWORK_CMD === "build" ? distPath : "" - ) - ) - ); - await new Promise((resolve, reject) => { - const stream = fs.createWriteStream(destFile); - stream.on("finish", resolve); - stream.on("error", reject); - pipeable.pipe(stream); - }); - }) - ) - ) + Object.entries(pathMap).map(async ([pathStr, { elements }]) => { + for (const [rscId, props] of elements || []) { + // FIXME we blindly expect JSON.stringify usage is deterministic + const serializedProps = JSON.stringify(props); + const searchParams = new URLSearchParams(); + searchParams.set("props", serializedProps); + const destFile = path.join( + dir, + publicPath, + "RSC", + decodeURIComponent(rscId), + decodeURIComponent(`${searchParams}`) + ); + fs.mkdirSync(path.dirname(destFile), { recursive: true }); + const bundlerConfig = new Proxy( + {}, + { + get(_target, encodedId: string) { + const [id, name] = decodeId(encodedId); + addClientModule(pathStr, id); + return { id, chunks: [id], name, async: true }; + }, + } + ); + const component = await getFunctionComponent(rscId); + const pipeable = renderToPipeableStream( + createElement(component, props as any), + bundlerConfig + ).pipe( + transformRsfId( + path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : "") + ) + ); + await new Promise((resolve, reject) => { + const stream = fs.createWriteStream(destFile); + stream.on("finish", resolve); + stream.on("error", reject); + pipeable.pipe(stream); + }); + } + }) ); const publicIndexHtml = fs.readFileSync(publicIndexHtmlFile, { From 4fb4ab747698828c2dbe684cd4972638ca687199 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 16 May 2023 21:58:57 +0900 Subject: [PATCH 16/37] wip: graceful shutdown & use client in router client --- src/builder.ts | 4 +++- src/cli-build.ts | 1 - src/middleware/lib/rsc-handler-worker.ts | 19 +++++++++++-------- src/middleware/lib/rsc-handler.ts | 8 ++++++++ src/middleware/lib/rsc-utils.ts | 2 +- src/router/client.ts | 13 ++++--------- src/router/common.ts | 16 ---------------- src/router/server.ts | 12 +++++------- 8 files changed, 32 insertions(+), 43 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index fce5b2fad..faab2c92e 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -9,7 +9,7 @@ import * as swc from "@swc/core"; import type { Config } from "./config.js"; import { codeToInject } from "./middleware/lib/rsc-utils.js"; -import { getCustomModulesRSC, buildRSC } from "./middleware/lib/rsc-handler.js"; +import { shutdown, getCustomModulesRSC, buildRSC } from "./middleware/lib/rsc-handler.js"; // TODO we have duplicate code here and rscPrd.ts and rsc-handler*.ts @@ -213,4 +213,6 @@ export async function runBuild(config: Config = {}) { path.join(dir, distPath, "package.json"), JSON.stringify(packageJson, null, 2) ); + + await shutdown(); } diff --git a/src/cli-build.ts b/src/cli-build.ts index d2e02727d..a7f72e045 100644 --- a/src/cli-build.ts +++ b/src/cli-build.ts @@ -4,4 +4,3 @@ const config = process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG); await runBuild(config); -process.exit(0); diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 2c94520bb..869093d78 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -97,7 +97,12 @@ const handleBuild = async (mesg: MessageReq & { type: "build" }) => { }; parentPort!.on("message", (mesg: MessageReq) => { - if (mesg.type === "render") { + if (mesg.type === "shutdown") { + vitePromise.then(async (vite) => { + await vite.close(); + parentPort!.close(); + }); + } else if (mesg.type === "render") { handleRender(mesg); } else if (mesg.type === "getCustomModules") { handleGetCustomModules(mesg); @@ -188,13 +193,11 @@ const getDecodeId = async (loadClientEntries: boolean) => { const decodeId = (encodedId: string): [id: string, name: string] => { let [id, name] = encodedId.split("#") as [string, string]; - if (!id.startsWith("wakuwork/")) { - id = path.relative( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : ""), - id - ); - id = basePath + getClientEntry(id); - } + id = path.relative( + path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : ""), + id + ); + id = basePath + getClientEntry(id); return [id, name]; }; diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index f102bd7b2..2bd09de43 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -18,6 +18,7 @@ type CustomModules = { }; export type MessageReq = + | { type: "shutdown" } | { id: number; type: "render"; @@ -45,6 +46,13 @@ worker.on("message", (mesg: MessageRes) => { messageCallbacks.get(mesg.id)?.(mesg); }); +export function shutdown() { + return new Promise((resolve) => { + worker.on("close", resolve); + worker.postMessage({ type: "shutdown" }); + }); +} + let nextId = 1; export function renderRSC( diff --git a/src/middleware/lib/rsc-utils.ts b/src/middleware/lib/rsc-utils.ts index 5bff577e6..1afa3edb5 100644 --- a/src/middleware/lib/rsc-utils.ts +++ b/src/middleware/lib/rsc-utils.ts @@ -5,7 +5,7 @@ import { Transform } from "node:stream"; export const codeToInject = ` globalThis.__wakuwork_module_cache__ = new Map(); -globalThis.__webpack_chunk_load__ = async (id) => id.startsWith("wakuwork/") || import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); +globalThis.__webpack_chunk_load__ = (id) => import(id).then((m) => globalThis.__wakuwork_module_cache__.set(id, m)); globalThis.__webpack_require__ = (id) => globalThis.__wakuwork_module_cache__.get(id);`; export const generatePrefetchCode = ( diff --git a/src/router/client.ts b/src/router/client.ts index 7c2447de9..b0f41e649 100644 --- a/src/router/client.ts +++ b/src/router/client.ts @@ -1,5 +1,7 @@ /// +"use client"; + import { cache, createContext, @@ -12,7 +14,6 @@ import { } from "react"; import { serve } from "../client.js"; -import { WAKUWORK_ROUTER } from "./common.js"; import type { RouteProps, ChildProps, LinkProps } from "./common.js"; type ChangeLocation = ( @@ -70,7 +71,7 @@ const prefetchRoutes = (pathname: string, search: string) => { const getRoute = cache((rscId: string) => serve(rscId)); -const Child = ({ index }: ChildProps) => { +export function Child({ index }: ChildProps) { const { pathname, search } = useLocation(); const pathItems = pathname.split("/").filter(Boolean); if (index > pathItems.length) { @@ -81,7 +82,7 @@ const Child = ({ index }: ChildProps) => { getRoute(rscId), index < pathItems.length ? { childIndex: index + 1, search } : { search } ); -}; +} export function Link({ href, @@ -119,12 +120,6 @@ export function Link({ return ele; } -// FIXME Eventually, if we have server module graph, we could omit this hack. -(globalThis as any).__wakuwork_module_cache__.set(WAKUWORK_ROUTER, { - Child, - Link, -}); - const parseLocation = () => { const { pathname, search } = window.location; return { pathname, search }; diff --git a/src/router/common.ts b/src/router/common.ts index 19ab64f92..677f6bd3b 100644 --- a/src/router/common.ts +++ b/src/router/common.ts @@ -16,19 +16,3 @@ export type LinkProps = { notPending?: ReactNode; unstable_prefetchOnEnter?: boolean; }; - -const CLIENT_REFERENCE = Symbol.for("react.client.reference"); - -export const WAKUWORK_ROUTER = "wakuwork/router"; - -export const childReference = Object.defineProperties({} as any, { - $$typeof: { value: CLIENT_REFERENCE }, - $$id: { value: WAKUWORK_ROUTER + "#Child" }, - $$async: { value: false }, -}); - -export const linkReference = Object.defineProperties({} as any, { - $$typeof: { value: CLIENT_REFERENCE }, - $$id: { value: WAKUWORK_ROUTER + "#Link" }, - $$async: { value: false }, -}); diff --git a/src/router/server.ts b/src/router/server.ts index ea9cc6baa..bfef7f93c 100644 --- a/src/router/server.ts +++ b/src/router/server.ts @@ -5,8 +5,8 @@ import * as swc from "@swc/core"; import type { GetEntry, GetBuilder, GetCustomModules } from "../server.js"; -import { childReference, linkReference } from "./common.js"; import type { RouteProps, LinkProps } from "./common.js"; +import { Child as ClientChild, Link as ClientLink } from "./client.js"; const getAllFiles = (base: string, parent = ""): string[] => fs @@ -48,6 +48,7 @@ const resolveFileName = (fname: string) => { throw new Error(`Cannot resolve file ${fname}`); }; +// XXX Can we avoid doing this here? const findDependentModules = (fname: string) => { fname = resolveFileName(fname); const ext = path.extname(fname); @@ -113,7 +114,7 @@ export function fileRouter(base: string) { component, componentProps, props.childIndex - ? createElement(childReference, { index: props.childIndex }) + ? createElement(ClientChild, { index: props.childIndex }) : null ); }; @@ -121,7 +122,7 @@ export function fileRouter(base: string) { }; const prefetcher = async (pathStr: string) => { - const url = new URL(pathStr || "", "http://localhost"); + const url = new URL(pathStr, "http://localhost"); const elements: (readonly [id: string, props: RouteProps])[] = []; const pathItems = url.pathname.split("/").filter(Boolean); const search = url.search; @@ -157,9 +158,6 @@ globalThis.__WAKUWORK_ROUTER_PREFETCH__ = (pathname, search) => { const path2ids = {${paths.map((pathItem, index) => { const moduleIds: string[] = []; for (const m of prefetcherForPaths[index]?.clientModules || []) { - if (m["$$typeof"] !== CLIENT_REFERENCE) { - throw new Error("clientModules must be client references"); - } const [id] = decodeId(m["$$id"]); moduleIds.push(id); } @@ -198,5 +196,5 @@ globalThis.__WAKUWORK_ROUTER_PREFETCH__ = (pathname, search) => { } export function Link(props: LinkProps) { - return createElement(linkReference, props); + return createElement(ClientLink, props); } From 68d5f53635be27e343c2728706251fb7fdcf010f Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 16 May 2023 22:51:27 +0900 Subject: [PATCH 17/37] refactor without CMD --- cli.js | 1 - src/middleware/lib/rsc-handler-worker.ts | 56 ++++++++++-------------- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/cli.js b/cli.js index b19709ef5..61b7aacdc 100755 --- a/cli.js +++ b/cli.js @@ -1,5 +1,4 @@ #!/usr/bin/env node const cmd = process.argv[2]; -process.env.WAKUWORK_CMD = cmd; // TODO TEMP temporary solution import(`./dist/cli-${cmd}.js`); diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 869093d78..6ff79584a 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -120,11 +120,8 @@ type PipeableStream = { const config: Config = (process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG)) || {}; -const dirFromConfig = { - dev: config.devServer?.dir, - build: config.build?.dir, - start: config.prdServer?.dir, -}[String(process.env.WAKUWORK_CMD)]; +const dirFromConfig = + config.prdServer?.dir ?? config.build?.dir ?? config.devServer?.dir; // HACK const dir = path.resolve(dirFromConfig || "."); const basePath = config.build?.basePath || "/"; // FIXME it's not build only const distPath = config.files?.dist || "dist"; @@ -134,10 +131,10 @@ const publicIndexHtmlFile = path.join( publicPath, config.files?.indexHtml || "index.html" ); -const srcEntriesFile = path.join(dir, config.files?.entriesJs || "entries.js"); -const entriesFile = path.join( +const entriesFile = path.join(dir, config.files?.entriesJs || "entries.js"); +const distEntriesFile = path.join( dir, - process.env.WAKUWORK_CMD === "build" ? distPath : "", + distPath, config.files?.entriesJs || "entries.js" ); @@ -152,8 +149,10 @@ const loadServerFile = async (fname: string) => { return vite.ssrLoadModule(fname); }; -const getFunctionComponent = async (rscId: string) => { - const { getEntry } = await (loadServerFile(entriesFile) as Promise<{ +const getFunctionComponent = async (rscId: string, isBuild: boolean) => { + const { getEntry } = await (loadServerFile( + isBuild ? distEntriesFile : entriesFile + ) as Promise<{ getEntry: GetEntry; }>); const mod = await getEntry(rscId); @@ -167,10 +166,12 @@ const getFunctionComponent = async (rscId: string) => { }; // FIXME better function name? decodeId seems too general -const getDecodeId = async (loadClientEntries: boolean) => { +const getDecodeId = async (loadClientEntries: boolean, isBuild: boolean) => { let clientEntries: Record | undefined; if (loadClientEntries) { - ({ clientEntries } = await loadServerFile(entriesFile)); + ({ clientEntries } = await loadServerFile( + isBuild ? distEntriesFile : entriesFile + )); if (!clientEntries) { throw new Error("Failed to load clientEntries"); } @@ -193,10 +194,7 @@ const getDecodeId = async (loadClientEntries: boolean) => { const decodeId = (encodedId: string): [id: string, name: string] => { let [id, name] = encodedId.split("#") as [string, string]; - id = path.relative( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : ""), - id - ); + id = path.relative(path.join(dir, isBuild ? distPath : ""), id); id = basePath + getClientEntry(id); return [id, name]; }; @@ -208,7 +206,7 @@ async function renderRSC( input: RenderInput, loadClientEntries: boolean ): Promise { - const decodeId = await getDecodeId(loadClientEntries); + const decodeId = await getDecodeId(loadClientEntries, false); const bundlerConfig = new Proxy( {}, @@ -231,23 +229,17 @@ async function renderRSC( // continue for mutation mode } if (input.rscId && input.props) { - const component = await getFunctionComponent(input.rscId); + const component = await getFunctionComponent(input.rscId, false); return renderToPipeableStream( createElement(component, input.props), bundlerConfig - ).pipe( - transformRsfId( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : "") - ) - ); + ).pipe(transformRsfId(dir)); } throw new Error("Unexpected input"); } async function getCustomModulesRSC(): Promise<{ [name: string]: string }> { - const { getCustomModules } = await (loadServerFile( - srcEntriesFile - ) as Promise<{ + const { getCustomModules } = await (loadServerFile(entriesFile) as Promise<{ getCustomModules?: GetCustomModules; }>); if (!getCustomModules) { @@ -259,7 +251,7 @@ async function getCustomModulesRSC(): Promise<{ [name: string]: string }> { // FIXME this may take too much responsibility async function buildRSC(): Promise { - const { getBuilder } = await (loadServerFile(entriesFile) as Promise<{ + const { getBuilder } = await (loadServerFile(distEntriesFile) as Promise<{ getBuilder?: GetBuilder; }>); if (!getBuilder) { @@ -269,7 +261,7 @@ async function buildRSC(): Promise { return; } - const decodeId = await getDecodeId(true); + const decodeId = await getDecodeId(true, true); const pathMap = await getBuilder(decodeId); const clientModuleMap = new Map>(); @@ -306,15 +298,11 @@ async function buildRSC(): Promise { }, } ); - const component = await getFunctionComponent(rscId); + const component = await getFunctionComponent(rscId, true); const pipeable = renderToPipeableStream( createElement(component, props as any), bundlerConfig - ).pipe( - transformRsfId( - path.join(dir, process.env.WAKUWORK_CMD === "build" ? distPath : "") - ) - ); + ).pipe(transformRsfId(path.join(dir, distPath))); await new Promise((resolve, reject) => { const stream = fs.createWriteStream(destFile); stream.on("finish", resolve); From 37b55312d9578c89ece15f57b11cbf18196075e1 Mon Sep 17 00:00:00 2001 From: daishi Date: Wed, 17 May 2023 23:57:04 +0900 Subject: [PATCH 18/37] @fs hack --- src/builder.ts | 2 -- src/cli-start.ts | 2 +- src/middleware/lib/rsc-handler-worker.ts | 12 ++++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index faab2c92e..73e1a22f1 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -11,8 +11,6 @@ import type { Config } from "./config.js"; import { codeToInject } from "./middleware/lib/rsc-utils.js"; import { shutdown, getCustomModulesRSC, buildRSC } from "./middleware/lib/rsc-handler.js"; -// TODO we have duplicate code here and rscPrd.ts and rsc-handler*.ts - // FIXME we could do this without plugin anyway const rscIndexPlugin = (): Plugin => { return { diff --git a/src/cli-start.ts b/src/cli-start.ts index aeada996e..c56aac1d4 100644 --- a/src/cli-start.ts +++ b/src/cli-start.ts @@ -1,6 +1,6 @@ import { startPrdServer } from "./prdServer.js"; -// FIXME maybe we should set NODE_ENV=production +process.env.NODE_ENV ||= "production"; const config = process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG); diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 6ff79584a..e3d83b158 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -140,6 +140,7 @@ const distEntriesFile = path.join( const vitePromise = createServer({ root: dir, + ...(process.env.NODE_ENV && { mode: process.env.NODE_ENV }), plugins: [rscPlugin()], appType: "custom", }); @@ -194,8 +195,15 @@ const getDecodeId = async (loadClientEntries: boolean, isBuild: boolean) => { const decodeId = (encodedId: string): [id: string, name: string] => { let [id, name] = encodedId.split("#") as [string, string]; - id = path.relative(path.join(dir, isBuild ? distPath : ""), id); - id = basePath + getClientEntry(id); + const baseDir = isBuild ? path.join(dir, distPath) : dir; + if (id.startsWith(baseDir)) { + id = basePath + getClientEntry(path.relative(baseDir, id)); + } else { + if (isBuild || process.env.NODE_ENV === "production") { + throw new Error("decodeId: no relative path in production"); + } + id = basePath + path.join("@fs", getClientEntry(id)); + } return [id, name]; }; From fdd93b90e95d48c605365047fb745f7b46848f30 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 18 May 2023 12:23:12 +0900 Subject: [PATCH 19/37] uninstall tsx --- examples/01_counter/package.json | 1 - examples/02_async/package.json | 1 - examples/03_promise/package.json | 1 - examples/04_callserver/package.json | 1 - examples/05_mutation/package.json | 1 - examples/06_nesting/package.json | 1 - examples/07_router/package.json | 1 - package.json | 4 +- pnpm-lock.yaml | 469 +++++++++++++++++++++++++--- 9 files changed, 429 insertions(+), 51 deletions(-) diff --git a/examples/01_counter/package.json b/examples/01_counter/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/01_counter/package.json +++ b/examples/01_counter/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/02_async/package.json b/examples/02_async/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/02_async/package.json +++ b/examples/02_async/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/03_promise/package.json b/examples/03_promise/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/03_promise/package.json +++ b/examples/03_promise/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/04_callserver/package.json b/examples/04_callserver/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/04_callserver/package.json +++ b/examples/04_callserver/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/05_mutation/package.json b/examples/05_mutation/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/05_mutation/package.json +++ b/examples/05_mutation/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/06_nesting/package.json b/examples/06_nesting/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/06_nesting/package.json +++ b/examples/06_nesting/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/examples/07_router/package.json b/examples/07_router/package.json index 9c2e902b7..7dec3877d 100644 --- a/examples/07_router/package.json +++ b/examples/07_router/package.json @@ -12,7 +12,6 @@ "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.7", "wakuwork": "~0.9.4" }, "devDependencies": { diff --git a/package.json b/package.json index 816ab254b..e21e1b1c2 100644 --- a/package.json +++ b/package.json @@ -95,13 +95,11 @@ "react-dom": "18.3.0-canary-aef7ce554-20230503", "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", "tailwindcss": "^3.3.2", - "tsx": "^3.12.7", "typescript": "^5.0.4", "wakuwork": "link:." }, "peerDependencies": { "react": "18.3.0-canary-aef7ce554-20230503", - "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503", - "tsx": "^3.12.6" + "react-server-dom-webpack": "18.3.0-canary-aef7ce554-20230503" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0f43e7ce..b55031a86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,13 +59,10 @@ devDependencies: version: 18.3.0-canary-aef7ce554-20230503(react@18.3.0-canary-aef7ce554-20230503) react-server-dom-webpack: specifier: 18.3.0-canary-aef7ce554-20230503 - version: 18.3.0-canary-aef7ce554-20230503(react-dom@18.3.0-canary-aef7ce554-20230503)(react@18.3.0-canary-aef7ce554-20230503) + version: 18.3.0-canary-aef7ce554-20230503(react-dom@18.3.0-canary-aef7ce554-20230503)(react@18.3.0-canary-aef7ce554-20230503)(webpack@5.83.1) tailwindcss: specifier: ^3.3.2 version: 3.3.2 - tsx: - specifier: ^3.12.7 - version: 3.12.7 typescript: specifier: ^5.0.4 version: 5.0.4 @@ -304,33 +301,13 @@ packages: '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 - /@esbuild-kit/cjs-loader@2.4.2: - resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==} - dependencies: - '@esbuild-kit/core-utils': 3.1.0 - get-tsconfig: 4.5.0 - dev: true - - /@esbuild-kit/core-utils@3.1.0: - resolution: {integrity: sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw==} - dependencies: - esbuild: 0.17.18 - source-map-support: 0.5.21 - dev: true - - /@esbuild-kit/esm-loader@2.5.5: - resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==} - dependencies: - '@esbuild-kit/core-utils': 3.1.0 - get-tsconfig: 4.5.0 - dev: true - /@esbuild/android-arm64@0.17.18: resolution: {integrity: sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==} engines: {node: '>=12'} cpu: [arm64] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-arm@0.17.18: @@ -339,6 +316,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-x64@0.17.18: @@ -347,6 +325,7 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/darwin-arm64@0.17.18: @@ -355,6 +334,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: false optional: true /@esbuild/darwin-x64@0.17.18: @@ -363,6 +343,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: false optional: true /@esbuild/freebsd-arm64@0.17.18: @@ -371,6 +352,7 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: false optional: true /@esbuild/freebsd-x64@0.17.18: @@ -379,6 +361,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: false optional: true /@esbuild/linux-arm64@0.17.18: @@ -387,6 +370,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-arm@0.17.18: @@ -395,6 +379,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-ia32@0.17.18: @@ -403,6 +388,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-loong64@0.17.18: @@ -411,6 +397,7 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-mips64el@0.17.18: @@ -419,6 +406,7 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-ppc64@0.17.18: @@ -427,6 +415,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-riscv64@0.17.18: @@ -435,6 +424,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-s390x@0.17.18: @@ -443,6 +433,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-x64@0.17.18: @@ -451,6 +442,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/netbsd-x64@0.17.18: @@ -459,6 +451,7 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: false optional: true /@esbuild/openbsd-x64@0.17.18: @@ -467,6 +460,7 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: false optional: true /@esbuild/sunos-x64@0.17.18: @@ -475,6 +469,7 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: false optional: true /@esbuild/win32-arm64@0.17.18: @@ -483,6 +478,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-ia32@0.17.18: @@ -491,6 +487,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-x64@0.17.18: @@ -499,6 +496,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: false optional: true /@jridgewell/gen-mapping@0.3.3: @@ -517,6 +515,13 @@ packages: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} + /@jridgewell/source-map@0.3.3: + resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 + dev: true + /@jridgewell/sourcemap-codec@1.4.14: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} @@ -756,10 +761,32 @@ packages: '@types/responselike': 1.0.0 dev: true + /@types/eslint-scope@3.7.4: + resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} + dependencies: + '@types/eslint': 8.37.0 + '@types/estree': 1.0.1 + dev: true + + /@types/eslint@8.37.0: + resolution: {integrity: sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==} + dependencies: + '@types/estree': 1.0.1 + '@types/json-schema': 7.0.11 + dev: true + + /@types/estree@1.0.1: + resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} + dev: true + /@types/http-cache-semantics@4.0.1: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} dev: true + /@types/json-schema@7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true + /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: @@ -816,10 +843,132 @@ packages: - supports-color dev: false + /@webassemblyjs/ast@1.11.6: + resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} + dependencies: + '@webassemblyjs/helper-numbers': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + dev: true + + /@webassemblyjs/floating-point-hex-parser@1.11.6: + resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + dev: true + + /@webassemblyjs/helper-api-error@1.11.6: + resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + dev: true + + /@webassemblyjs/helper-buffer@1.11.6: + resolution: {integrity: sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==} + dev: true + + /@webassemblyjs/helper-numbers@1.11.6: + resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/helper-wasm-bytecode@1.11.6: + resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + dev: true + + /@webassemblyjs/helper-wasm-section@1.11.6: + resolution: {integrity: sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + dev: true + + /@webassemblyjs/ieee754@1.11.6: + resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + dependencies: + '@xtuc/ieee754': 1.2.0 + dev: true + + /@webassemblyjs/leb128@1.11.6: + resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + dependencies: + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/utf8@1.11.6: + resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + dev: true + + /@webassemblyjs/wasm-edit@1.11.6: + resolution: {integrity: sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-wasm-section': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-opt': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + '@webassemblyjs/wast-printer': 1.11.6 + dev: true + + /@webassemblyjs/wasm-gen@1.11.6: + resolution: {integrity: sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: true + + /@webassemblyjs/wasm-opt@1.11.6: + resolution: {integrity: sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + dev: true + + /@webassemblyjs/wasm-parser@1.11.6: + resolution: {integrity: sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: true + + /@webassemblyjs/wast-printer@1.11.6: + resolution: {integrity: sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@xtuc/long': 4.2.2 + dev: true + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + dev: true + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + dev: true + /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: true + /acorn-import-assertions@1.9.0(acorn@8.8.2): + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.8.2 + dev: true + /acorn-loose@8.3.0: resolution: {integrity: sha512-75lAs9H19ldmW+fAbyqHdjgdCrz0pWGXKmnqFoh8PyVd1L2RIb4RzYrSjmopeqv3E1G3/Pimu6GgLlrGbrkF7w==} engines: {node: '>=0.4.0'} @@ -833,6 +982,23 @@ packages: hasBin: true dev: true + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -995,6 +1161,11 @@ packages: fsevents: 2.3.2 dev: true + /chrome-trace-event@1.0.3: + resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} + engines: {node: '>=6.0'} + dev: true + /clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} dependencies: @@ -1011,6 +1182,10 @@ packages: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} dev: false + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1116,6 +1291,18 @@ packages: once: 1.4.0 dev: true + /enhanced-resolve@5.14.0: + resolution: {integrity: sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + + /es-module-lexer@1.2.1: + resolution: {integrity: sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==} + dev: true + /esbuild@0.17.18: resolution: {integrity: sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==} engines: {node: '>=12'} @@ -1144,6 +1331,7 @@ packages: '@esbuild/win32-arm64': 0.17.18 '@esbuild/win32-ia32': 0.17.18 '@esbuild/win32-x64': 0.17.18 + dev: false /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -1159,6 +1347,36 @@ packages: engines: {node: '>=12'} dev: true + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + /execa@0.7.0: resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==} engines: {node: '>=4'} @@ -1209,6 +1427,10 @@ packages: sort-keys-length: 1.0.1 dev: true + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -1220,6 +1442,10 @@ packages: micromatch: 4.0.5 dev: true + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -1304,10 +1530,6 @@ packages: engines: {node: '>=10'} dev: true - /get-tsconfig@4.5.0: - resolution: {integrity: sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==} - dev: true - /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1322,6 +1544,10 @@ packages: is-glob: 4.0.3 dev: true + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + /glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} dependencies: @@ -1355,10 +1581,19 @@ packages: responselike: 2.0.1 dev: true + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -1451,6 +1686,15 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.0.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + /jiti@1.18.2: resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} hasBin: true @@ -1469,6 +1713,14 @@ packages: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + /json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1490,6 +1742,11 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + dev: true + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1544,6 +1801,13 @@ packages: engines: {node: '>= 0.6'} dev: true + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + /mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -1824,6 +2088,11 @@ packages: once: 1.4.0 dev: true + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -1833,6 +2102,12 @@ packages: engines: {node: '>=10'} dev: true + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + /react-dom@18.3.0-canary-aef7ce554-20230503(react@18.3.0-canary-aef7ce554-20230503): resolution: {integrity: sha512-XyunYyvQ74LQ70sQefYRte21x3zhBfSvMu3drjd8jONP8LoJmFH0wL5pNLqfE1nH1NeCuatHNhx4MrhOzFKmZA==} peerDependencies: @@ -1848,7 +2123,7 @@ packages: engines: {node: '>=0.10.0'} dev: false - /react-server-dom-webpack@18.3.0-canary-aef7ce554-20230503(react-dom@18.3.0-canary-aef7ce554-20230503)(react@18.3.0-canary-aef7ce554-20230503): + /react-server-dom-webpack@18.3.0-canary-aef7ce554-20230503(react-dom@18.3.0-canary-aef7ce554-20230503)(react@18.3.0-canary-aef7ce554-20230503)(webpack@5.83.1): resolution: {integrity: sha512-pBsyZTIYmzSTsEl9h1ejG6MMSv9fG8bhPjOTMJR0oxLAQEAOu8gmrG0Z/V297Vrj7Q69hr0bpvQBrE/Za1869g==} engines: {node: '>=0.10.0'} peerDependencies: @@ -1861,6 +2136,7 @@ packages: neo-async: 2.6.2 react: 18.3.0-canary-aef7ce554-20230503 react-dom: 18.3.0-canary-aef7ce554-20230503(react@18.3.0-canary-aef7ce554-20230503) + webpack: 5.83.1(@swc/core@1.3.56) dev: true /react@18.3.0-canary-aef7ce554-20230503: @@ -1947,6 +2223,15 @@ packages: loose-envify: 1.4.0 dev: true + /schema-utils@3.1.2: + resolution: {integrity: sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.11 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: true + /semver-regex@4.0.5: resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} engines: {node: '>=12'} @@ -1981,6 +2266,12 @@ packages: lru-cache: 6.0.0 dev: true + /serialize-javascript@6.0.1: + resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} + dependencies: + randombytes: 2.1.0 + dev: true + /shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -2110,6 +2401,13 @@ packages: dependencies: has-flag: 3.0.0 + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -2147,6 +2445,47 @@ packages: - ts-node dev: true + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + + /terser-webpack-plugin@5.3.9(@swc/core@1.3.56)(webpack@5.83.1): + resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.18 + '@swc/core': 1.3.56 + jest-worker: 27.5.1 + schema-utils: 3.1.2 + serialize-javascript: 6.0.1 + terser: 5.17.4 + webpack: 5.83.1(@swc/core@1.3.56) + dev: true + + /terser@5.17.4: + resolution: {integrity: sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.3 + acorn: 8.8.2 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + /thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -2197,17 +2536,6 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true - /tsx@3.12.7: - resolution: {integrity: sha512-C2Ip+jPmqKd1GWVQDvz/Eyc6QJbGfE7NrR3fx5BpEHMZsEHoIxHL1j+lKdGobr8ovEyqeNkPLSKp6SCSOt7gmw==} - hasBin: true - dependencies: - '@esbuild-kit/cjs-loader': 2.4.2 - '@esbuild-kit/core-utils': 3.1.0 - '@esbuild-kit/esm-loader': 2.5.5 - optionalDependencies: - fsevents: 2.3.2 - dev: true - /typescript@5.0.4: resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} engines: {node: '>=12.20'} @@ -2228,6 +2556,12 @@ packages: escalade: 3.1.1 picocolors: 1.0.0 + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true @@ -2265,6 +2599,59 @@ packages: fsevents: 2.3.2 dev: false + /watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: true + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + + /webpack@5.83.1(@swc/core@1.3.56): + resolution: {integrity: sha512-TNsG9jDScbNuB+Lb/3+vYolPplCS3bbEaJf+Bj0Gw4DhP3ioAflBb1flcRt9zsWITyvOhM96wMQNRWlSX52DgA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.4 + '@types/estree': 1.0.1 + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/wasm-edit': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + acorn: 8.8.2 + acorn-import-assertions: 1.9.0(acorn@8.8.2) + browserslist: 4.21.5 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.14.0 + es-module-lexer: 1.2.1 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.1.2 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.9(@swc/core@1.3.56)(webpack@5.83.1) + watchpack: 2.4.0 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true From ce740654528fa3675cf35ee8b7b3483213fc45fd Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 18 May 2023 18:28:16 +0900 Subject: [PATCH 20/37] trying to fix some issues --- src/client.ts | 4 +--- src/middleware/lib/rsc-handler-worker.ts | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/client.ts b/src/client.ts index 346a65e83..9b4d5eeac 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2,9 +2,7 @@ import { cache, use, useEffect, useState } from "react"; import type { ReactElement } from "react"; -import RSDWClient from "react-server-dom-webpack/client"; - -const { createFromFetch, encodeReply } = RSDWClient; +import { createFromFetch, encodeReply } from "react-server-dom-webpack/client"; // FIXME only works with basePath="/" const basePath = "/"; diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index e3d83b158..ef3821250 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -142,6 +142,9 @@ const vitePromise = createServer({ root: dir, ...(process.env.NODE_ENV && { mode: process.env.NODE_ENV }), plugins: [rscPlugin()], + ssr: { + noExternal: ["wakuwork"], // FIXME this doesn't seem ideal? + }, appType: "custom", }); @@ -200,7 +203,7 @@ const getDecodeId = async (loadClientEntries: boolean, isBuild: boolean) => { id = basePath + getClientEntry(path.relative(baseDir, id)); } else { if (isBuild || process.env.NODE_ENV === "production") { - throw new Error("decodeId: no relative path in production"); + throw new Error("decodeId: no relative path in production: " + id); } id = basePath + path.join("@fs", getClientEntry(id)); } From be0bcd60bdaa303bf6ba81a202cfe3e8f875f4d6 Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 20 May 2023 10:25:20 +0900 Subject: [PATCH 21/37] wip --- src/middleware/lib/rsc-handler-worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index ef3821250..688d279f0 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -143,7 +143,7 @@ const vitePromise = createServer({ ...(process.env.NODE_ENV && { mode: process.env.NODE_ENV }), plugins: [rscPlugin()], ssr: { - noExternal: ["wakuwork"], // FIXME this doesn't seem ideal? + noExternal: ["wakuwork", "@swc/core"], // FIXME this doesn't seem ideal? }, appType: "custom", }); From dfa953b489b80091a7af0284b6094a81422f4dfb Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 20 May 2023 10:31:13 +0900 Subject: [PATCH 22/37] Revert "wip" This reverts commit be0bcd60bdaa303bf6ba81a202cfe3e8f875f4d6. --- src/middleware/lib/rsc-handler-worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 688d279f0..ef3821250 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -143,7 +143,7 @@ const vitePromise = createServer({ ...(process.env.NODE_ENV && { mode: process.env.NODE_ENV }), plugins: [rscPlugin()], ssr: { - noExternal: ["wakuwork", "@swc/core"], // FIXME this doesn't seem ideal? + noExternal: ["wakuwork"], // FIXME this doesn't seem ideal? }, appType: "custom", }); From cd9a909c18bc0c1a46fa4b25e364b0d871c9e262 Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 20 May 2023 11:19:56 +0900 Subject: [PATCH 23/37] take care of node_modules client modules --- src/middleware/lib/rsc-handler-worker.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index ef3821250..6c24f3cfe 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -201,6 +201,10 @@ const getDecodeId = async (loadClientEntries: boolean, isBuild: boolean) => { const baseDir = isBuild ? path.join(dir, distPath) : dir; if (id.startsWith(baseDir)) { id = basePath + getClientEntry(path.relative(baseDir, id)); + } else if (id.startsWith(path.join(dir, "node_modules"))) { + id = + basePath + + getClientEntry(path.relative(path.join(dir, "node_modules"), id)); } else { if (isBuild || process.env.NODE_ENV === "production") { throw new Error("decodeId: no relative path in production: " + id); From 7510284f2141aff08d12703539b9197976ccc359 Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 20 May 2023 11:22:28 +0900 Subject: [PATCH 24/37] Revert "take care of node_modules client modules" This reverts commit cd9a909c18bc0c1a46fa4b25e364b0d871c9e262. --- src/middleware/lib/rsc-handler-worker.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 6c24f3cfe..ef3821250 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -201,10 +201,6 @@ const getDecodeId = async (loadClientEntries: boolean, isBuild: boolean) => { const baseDir = isBuild ? path.join(dir, distPath) : dir; if (id.startsWith(baseDir)) { id = basePath + getClientEntry(path.relative(baseDir, id)); - } else if (id.startsWith(path.join(dir, "node_modules"))) { - id = - basePath + - getClientEntry(path.relative(path.join(dir, "node_modules"), id)); } else { if (isBuild || process.env.NODE_ENV === "production") { throw new Error("decodeId: no relative path in production: " + id); From 523528bd5662f3e41720e090804b3f72424c0aef Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 20 May 2023 12:21:06 +0900 Subject: [PATCH 25/37] wip: transpile node_modules for server --- src/builder.ts | 15 ++++++++++++++- src/middleware/rscDev.ts | 1 - src/middleware/rscPrd.ts | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index 73e1a22f1..d46644cfe 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -9,7 +9,11 @@ import * as swc from "@swc/core"; import type { Config } from "./config.js"; import { codeToInject } from "./middleware/lib/rsc-utils.js"; -import { shutdown, getCustomModulesRSC, buildRSC } from "./middleware/lib/rsc-handler.js"; +import { + shutdown, + getCustomModulesRSC, + buildRSC, +} from "./middleware/lib/rsc-handler.js"; // FIXME we could do this without plugin anyway const rscIndexPlugin = (): Plugin => { @@ -93,6 +97,9 @@ export async function runBuild(config: Config = {}) { (id) => serverEntryFileSet.add(id) ), ], + ssr: { + noExternal: ["wakuwork"], // TODO we need to add all possible libs to analyze + }, build: { outDir: distPath, write: false, @@ -115,6 +122,12 @@ export async function runBuild(config: Config = {}) { const serverBuildOutput = await build({ root: dir, base: basePath, + ssr: { + noExternal: Array.from(clientEntryFileSet).map( + (fname) => + path.relative(path.join(dir, "node_modules"), fname).split("/")[0]! + ), + }, build: { outDir: distPath, ssr: true, diff --git a/src/middleware/rscDev.ts b/src/middleware/rscDev.ts index e5e1e8449..211d7d910 100644 --- a/src/middleware/rscDev.ts +++ b/src/middleware/rscDev.ts @@ -2,7 +2,6 @@ import RSDWServer from "react-server-dom-webpack/server.node.unbundled"; import busboy from "busboy"; import type { MiddlewareCreator } from "./lib/common.js"; - import { renderRSC } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; diff --git a/src/middleware/rscPrd.ts b/src/middleware/rscPrd.ts index 5e746739f..45a555544 100644 --- a/src/middleware/rscPrd.ts +++ b/src/middleware/rscPrd.ts @@ -6,7 +6,7 @@ import { renderRSC } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; -// TODO we have duplicate code here and rsc-handler-worker.ts +// FIXME we have duplicate code here and rscDev.ts const rscPrd: MiddlewareCreator = () => { return async (req, res, next) => { From 6d40113dd7e25220a7cec11ed3180d6178dd9b64 Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 20 May 2023 13:26:09 +0900 Subject: [PATCH 26/37] hack for react-server-dom-webpack --- src/middleware/viteServer.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/middleware/viteServer.ts b/src/middleware/viteServer.ts index c8377a76f..fd38b4216 100644 --- a/src/middleware/viteServer.ts +++ b/src/middleware/viteServer.ts @@ -8,9 +8,9 @@ import react from "@vitejs/plugin-react"; import type { MiddlewareCreator } from "./lib/common.js"; import { codeToInject } from "./lib/rsc-utils.js"; -const rscPlugin = (): Plugin => { +const rscIndexPlugin = (): Plugin => { return { - name: "rscPlugin", + name: "rsc-index-plugin", async transformIndexHtml() { return [ { @@ -29,10 +29,13 @@ const viteServer: MiddlewareCreator = (config) => { const indexHtmlFile = path.join(dir, indexHtml); const vitePromise = createServer({ root: dir, + optimizeDeps: { + include: ["react-server-dom-webpack/client"], + }, plugins: [ // @ts-ignore react(), - rscPlugin(), + rscIndexPlugin(), ], server: { middlewareMode: true }, appType: "custom", From 2269bf98741f3f357019d5a1565590e62a1f761c Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 20 May 2023 23:11:00 +0900 Subject: [PATCH 27/37] use vite module graph for client entries in DEV --- src/builder.ts | 9 ++ src/middleware/lib/rsc-handler-worker.ts | 111 +++++++++++++---------- src/middleware/lib/rsc-handler.ts | 35 +++++-- src/middleware/rscDev.ts | 2 +- src/middleware/rscPrd.ts | 6 +- src/middleware/viteServer.ts | 9 ++ 6 files changed, 114 insertions(+), 58 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index d46644cfe..d9790877b 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -11,6 +11,7 @@ import type { Config } from "./config.js"; import { codeToInject } from "./middleware/lib/rsc-utils.js"; import { shutdown, + setClientEntries, getCustomModulesRSC, buildRSC, } from "./middleware/lib/rsc-handler.js"; @@ -207,6 +208,14 @@ export async function runBuild(config: Config = {}) { `export const clientEntries=${JSON.stringify(clientEntries)};` ); + const absoluteClientEntries = Object.fromEntries( + Object.entries(clientEntries).map(([key, val]) => [ + path.join(path.dirname(entriesFile), distPath, key), + basePath + val, + ]) + ); + await setClientEntries(absoluteClientEntries); + await buildRSC(); const origPackageJson = require(path.join(dir, "package.json")); diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index ef3821250..8a5d6714e 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -15,10 +15,31 @@ import { rscPlugin } from "./vite-rsc-plugin.js"; const { renderToPipeableStream } = RSDWServer; +const handleSetClientEntries = async ( + mesg: MessageReq & { type: "setClientEntries" } +) => { + const { id, value } = mesg; + try { + await setClientEntries(value); + const mesg: MessageRes = { + id, + type: "end", + }; + parentPort!.postMessage(mesg); + } catch (err) { + const mesg: MessageRes = { + id, + type: "err", + err, + }; + parentPort!.postMessage(mesg); + } +}; + const handleRender = async (mesg: MessageReq & { type: "render" }) => { - const { id, input, loadClientEntries } = mesg; + const { id, input } = mesg; try { - const pipeable = await renderRSC(input, loadClientEntries); + const pipeable = await renderRSC(input); const writable = new Writable({ write(chunk, encoding, callback) { if (encoding !== ("buffer" as any)) { @@ -102,6 +123,8 @@ parentPort!.on("message", (mesg: MessageReq) => { await vite.close(); parentPort!.close(); }); + } else if (mesg.type === "setClientEntries") { + handleSetClientEntries(mesg); } else if (mesg.type === "render") { handleRender(mesg); } else if (mesg.type === "getCustomModules") { @@ -169,61 +192,46 @@ const getFunctionComponent = async (rscId: string, isBuild: boolean) => { throw new Error("No function component found"); }; -// FIXME better function name? decodeId seems too general -const getDecodeId = async (loadClientEntries: boolean, isBuild: boolean) => { - let clientEntries: Record | undefined; - if (loadClientEntries) { - ({ clientEntries } = await loadServerFile( - isBuild ? distEntriesFile : entriesFile - )); - if (!clientEntries) { - throw new Error("Failed to load clientEntries"); - } - } +let absoluteClientEntries: Record = {}; - const getClientEntry = (id: string) => { - if (!clientEntries) { - return id; +const resolveClientEntry = (filePath: string) => { + const clientEntry = absoluteClientEntries[filePath]; + if (!clientEntry) { + if (absoluteClientEntries["*"] === "*") { + return basePath + path.relative(dir, filePath); } - const clientEntry = - clientEntries[id] || - clientEntries[id.replace(/\.js$/, ".ts")] || - clientEntries[id.replace(/\.js$/, ".tsx")] || - clientEntries[id.replace(/\.js$/, ".jsx")]; - if (!clientEntry) { - throw new Error("No client entry found"); - } - return clientEntry; - }; - - const decodeId = (encodedId: string): [id: string, name: string] => { - let [id, name] = encodedId.split("#") as [string, string]; - const baseDir = isBuild ? path.join(dir, distPath) : dir; - if (id.startsWith(baseDir)) { - id = basePath + getClientEntry(path.relative(baseDir, id)); - } else { - if (isBuild || process.env.NODE_ENV === "production") { - throw new Error("decodeId: no relative path in production: " + id); - } - id = basePath + path.join("@fs", getClientEntry(id)); - } - return [id, name]; - }; - - return decodeId; + throw new Error("No client entry found"); + } + return clientEntry; }; -async function renderRSC( - input: RenderInput, - loadClientEntries: boolean -): Promise { - const decodeId = await getDecodeId(loadClientEntries, false); +async function setClientEntries( + value: "load" | Record +): Promise { + if (value !== "load") { + absoluteClientEntries = value; + return; + } + const { clientEntries } = await loadServerFile(entriesFile); + if (!clientEntries) { + throw new Error("Failed to load clientEntries"); + } + const baseDir = path.dirname(entriesFile); + absoluteClientEntries = Object.fromEntries( + Object.entries(clientEntries).map(([key, val]) => [ + path.join(baseDir, key), + basePath + val, + ]) + ); +} +async function renderRSC(input: RenderInput): Promise { const bundlerConfig = new Proxy( {}, { get(_target, encodedId: string) { - const [id, name] = decodeId(encodedId); + const [filePath, name] = encodedId.split("#") as [string, string]; + const id = resolveClientEntry(filePath); return { id, chunks: [id], name, async: true }; }, } @@ -272,7 +280,12 @@ async function buildRSC(): Promise { return; } - const decodeId = await getDecodeId(true, true); + // FIXME this doesn't seem an ideal solution + const decodeId = (encodedId: string): [id: string, name: string] => { + const [filePath, name] = encodedId.split("#") as [string, string]; + const id = resolveClientEntry(filePath); + return [id, name]; + }; const pathMap = await getBuilder(decodeId); const clientModuleMap = new Map>(); diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index 2bd09de43..cd43203bb 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -19,11 +19,15 @@ type CustomModules = { export type MessageReq = | { type: "shutdown" } + | { + id: number; + type: "setClientEntries"; + value: "load" | Record; + } | { id: number; type: "render"; input: RenderInput; - loadClientEntries: boolean; } | { id: number; @@ -55,10 +59,30 @@ export function shutdown() { let nextId = 1; -export function renderRSC( - input: RenderInput, - loadClientEntries: boolean -): Readable { +export function setClientEntries( + value: "load" | Record +): Promise { + return new Promise((resolve, reject) => { + const id = nextId++; + messageCallbacks.set(id, (mesg) => { + if (mesg.type === "end") { + resolve(); + messageCallbacks.delete(id); + } else if (mesg.type === "err") { + reject(mesg.err); + messageCallbacks.delete(id); + } + }); + const mesg: MessageReq = { + id, + type: "setClientEntries", + value, + }; + worker.postMessage(mesg); + }); +} + +export function renderRSC(input: RenderInput): Readable { const id = nextId++; const passthrough = new PassThrough(); messageCallbacks.set(id, (mesg) => { @@ -78,7 +102,6 @@ export function renderRSC( id, type: "render", input, - loadClientEntries, }; worker.postMessage(mesg); return passthrough; diff --git a/src/middleware/rscDev.ts b/src/middleware/rscDev.ts index 211d7d910..328e91127 100644 --- a/src/middleware/rscDev.ts +++ b/src/middleware/rscDev.ts @@ -39,7 +39,7 @@ const rscDev: MiddlewareCreator = () => { } } if (rscId || rsfId) { - const pipeable = renderRSC({ rscId, props, rsfId, args }, false); + const pipeable = renderRSC({ rscId, props, rsfId, args }); pipeable.on("error", (err) => { console.info("Cannot render RSC", err); res.statusCode = 500; diff --git a/src/middleware/rscPrd.ts b/src/middleware/rscPrd.ts index 45a555544..7d086bfc2 100644 --- a/src/middleware/rscPrd.ts +++ b/src/middleware/rscPrd.ts @@ -2,14 +2,16 @@ import RSDWServer from "react-server-dom-webpack/server.node.unbundled"; import busboy from "busboy"; import type { MiddlewareCreator } from "./lib/common.js"; -import { renderRSC } from "./lib/rsc-handler.js"; +import { renderRSC, setClientEntries } from "./lib/rsc-handler.js"; const { decodeReply, decodeReplyFromBusboy } = RSDWServer; // FIXME we have duplicate code here and rscDev.ts const rscPrd: MiddlewareCreator = () => { + const promise = setClientEntries("load"); return async (req, res, next) => { + await promise; const rscId = req.headers["x-react-server-component-id"]; const rsfId = req.headers["x-react-server-function-id"]; if (Array.isArray(rscId) || Array.isArray(rsfId)) { @@ -41,7 +43,7 @@ const rscPrd: MiddlewareCreator = () => { } } if (rscId || rsfId) { - const pipeable = renderRSC({ rscId, props, rsfId, args }, true); + const pipeable = renderRSC({ rscId, props, rsfId, args }); pipeable.on("error", (err) => { console.info("Cannot render RSC", err); res.statusCode = 500; diff --git a/src/middleware/viteServer.ts b/src/middleware/viteServer.ts index fd38b4216..4a3c3f9b8 100644 --- a/src/middleware/viteServer.ts +++ b/src/middleware/viteServer.ts @@ -7,6 +7,7 @@ import react from "@vitejs/plugin-react"; import type { MiddlewareCreator } from "./lib/common.js"; import { codeToInject } from "./lib/rsc-utils.js"; +import { setClientEntries } from "./lib/rsc-handler.js"; const rscIndexPlugin = (): Plugin => { return { @@ -42,6 +43,14 @@ const viteServer: MiddlewareCreator = (config) => { }); return async (req, res, next) => { const vite = await vitePromise; + const absoluteClientEntries = Object.fromEntries( + Array.from(vite.moduleGraph.idToModuleMap.values()).map( + ({ file, url }) => [file, url] + ) + ); + absoluteClientEntries['*'] = '*'; // HACK to use fallback resolver + // FIXME this is bad in performance, let's revisit it + await setClientEntries(absoluteClientEntries); const indexFallback = async () => { const url = new URL(req.url || "", "http://" + req.headers.host); // TODO make it configurable? From cb22eec729725101d09a04f6eca8440bb460b346 Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 20 May 2023 23:25:42 +0900 Subject: [PATCH 28/37] wip: a workaround for router for now --- src/builder.ts | 2 +- src/middleware/viteServer.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/builder.ts b/src/builder.ts index d9790877b..4b6a21c5c 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -99,7 +99,7 @@ export async function runBuild(config: Config = {}) { ), ], ssr: { - noExternal: ["wakuwork"], // TODO we need to add all possible libs to analyze + noExternal: ["wakuwork"], // TODO we need to add all possible libs }, build: { outDir: distPath, diff --git a/src/middleware/viteServer.ts b/src/middleware/viteServer.ts index 4a3c3f9b8..05750c49a 100644 --- a/src/middleware/viteServer.ts +++ b/src/middleware/viteServer.ts @@ -32,6 +32,7 @@ const viteServer: MiddlewareCreator = (config) => { root: dir, optimizeDeps: { include: ["react-server-dom-webpack/client"], + exclude: ["wakuwork"], // TODO we need to add all possible libs }, plugins: [ // @ts-ignore From 57d69e2dde9bb92dfd2ab482ab9ace44a2bd5cff Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 21 May 2023 08:33:12 +0900 Subject: [PATCH 29/37] we need node 18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e21e1b1c2..489673bef 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ }, "license": "MIT", "engines": { - "node": ">=16.17.0" + "node": ">=18.0.0" }, "dependencies": { "@swc/core": "1.3.56", From 9783fd861b01c6e97ceb2035134d7b155b12f824 Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 21 May 2023 15:03:22 +0900 Subject: [PATCH 30/37] configurable routes path --- examples/07_router/entries.ts | 3 ++- src/router/server.ts | 6 +++--- website/entries.ts | 3 ++- website/routes/practices/router.tsx | 3 ++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/07_router/entries.ts b/examples/07_router/entries.ts index 7be6261b3..7a6ddc3a7 100644 --- a/examples/07_router/entries.ts +++ b/examples/07_router/entries.ts @@ -4,5 +4,6 @@ import url from "node:url"; import { fileRouter } from "wakuwork/router/server"; export const { getEntry, getBuilder, getCustomModules } = fileRouter( - path.join(path.dirname(url.fileURLToPath(import.meta.url)), "routes") + path.dirname(url.fileURLToPath(import.meta.url)), + "routes" ); diff --git a/src/router/server.ts b/src/router/server.ts index bfef7f93c..b2d3c9896 100644 --- a/src/router/server.ts +++ b/src/router/server.ts @@ -100,7 +100,8 @@ const findClientModules = async (base: string, id: string) => { ).flat(); }; -export function fileRouter(base: string) { +export function fileRouter(baseDir: string, routesPath: string) { + const base = path.join(baseDir, routesPath); const getEntry: GetEntry = async (id) => { // This can be too unsecure? FIXME const component = (await import(/* @vite-ignore */ `${base}/${id}.js`)) @@ -184,9 +185,8 @@ globalThis.__WAKUWORK_ROUTER_PREFETCH__ = (pathname, search) => { const getCustomModules: GetCustomModules = async () => { return Object.fromEntries( - // TODO TEMP make "routes" configurable getAllFiles(base).map((file) => [ - `routes/${file.replace(/\.\w+$/, "")}`, + `${routesPath}/${file.replace(/\.\w+$/, "")}`, `${base}/${file}`, ]) ); diff --git a/website/entries.ts b/website/entries.ts index 7be6261b3..7a6ddc3a7 100644 --- a/website/entries.ts +++ b/website/entries.ts @@ -4,5 +4,6 @@ import url from "node:url"; import { fileRouter } from "wakuwork/router/server"; export const { getEntry, getBuilder, getCustomModules } = fileRouter( - path.join(path.dirname(url.fileURLToPath(import.meta.url)), "routes") + path.dirname(url.fileURLToPath(import.meta.url)), + "routes" ); diff --git a/website/routes/practices/router.tsx b/website/routes/practices/router.tsx index d8d59dd0a..866ce31f0 100644 --- a/website/routes/practices/router.tsx +++ b/website/routes/practices/router.tsx @@ -13,7 +13,8 @@ import url from "node:url"; import { fileRouter } from "wakuwork/router/server"; export const { getEntry, prefetcher, prerenderer } = fileRouter( - path.join(path.dirname(url.fileURLToPath(import.meta.url)), "routes") + path.dirname(url.fileURLToPath(import.meta.url)), + "routes" ); `; From 8078d819872447c28ba67bba03adbcea0e3bd4a2 Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 21 May 2023 16:31:57 +0900 Subject: [PATCH 31/37] do not set search prop for intermediate routes (tentative) --- src/router/client.ts | 14 ++++++++++---- src/router/common.ts | 9 ++------- src/router/server.ts | 13 +++++++------ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/router/client.ts b/src/router/client.ts index b0f41e649..aa4da7c24 100644 --- a/src/router/client.ts +++ b/src/router/client.ts @@ -43,8 +43,8 @@ export function useLocation() { return value.location; } -// FIXME normalizing `search` before prefetch would be necessary. -// FIXME ommitting `search` items would be important for caching. +// FIXME normalizing `search` before prefetch might be good. +// FIXME selective `search` would be better. (for intermediate routes too) const prefetchRoutes = (pathname: string, search: string) => { const prefetched = ((globalThis as any).__WAKUWORK_PREFETCHED__ ||= {}); @@ -52,7 +52,7 @@ const prefetchRoutes = (pathname: string, search: string) => { for (let index = 0; index <= pathItems.length; ++index) { const rscId = pathItems.slice(0, index).join("/") || "index"; const props: RouteProps = - index < pathItems.length ? { childIndex: index + 1, search } : { search }; + index < pathItems.length ? { childIndex: index + 1 } : { search }; // FIXME we blindly expect JSON.stringify usage is deterministic const serializedProps = JSON.stringify(props); if (!prefetched[rscId]) { @@ -80,7 +80,13 @@ export function Child({ index }: ChildProps) { const rscId = pathItems.slice(0, index).join("/") || "index"; return createElement( getRoute(rscId), - index < pathItems.length ? { childIndex: index + 1, search } : { search } + index < pathItems.length + ? { + childIndex: index + 1, // we still have a child route + } + : { + search, // attach `search` only for a leaf route for now + } ); } diff --git a/src/router/common.ts b/src/router/common.ts index 677f6bd3b..1d57ba101 100644 --- a/src/router/common.ts +++ b/src/router/common.ts @@ -1,13 +1,8 @@ import type { ReactNode } from "react"; -export type RouteProps = { - childIndex?: number; - search: string; -}; +export type RouteProps = { childIndex: number } | { search: string }; -export type ChildProps = { - index: number; -}; +export type ChildProps = { index: number }; export type LinkProps = { href: string; diff --git a/src/router/server.ts b/src/router/server.ts index b2d3c9896..25f4feb48 100644 --- a/src/router/server.ts +++ b/src/router/server.ts @@ -108,13 +108,15 @@ export function fileRouter(baseDir: string, routesPath: string) { .default; const RouteComponent: any = (props: RouteProps) => { const componentProps: Record = {}; - for (const [key, value] of new URLSearchParams(props.search)) { - componentProps[key] = value; + if ("search" in props) { + for (const [key, value] of new URLSearchParams(props.search)) { + componentProps[key] = value; + } } return createElement( component, componentProps, - props.childIndex + "childIndex" in props ? createElement(ClientChild, { index: props.childIndex }) : null ); @@ -122,6 +124,7 @@ export function fileRouter(baseDir: string, routesPath: string) { return RouteComponent; }; + // We have to make prefetcher consistent with client behavior const prefetcher = async (pathStr: string) => { const url = new URL(pathStr, "http://localhost"); const elements: (readonly [id: string, props: RouteProps])[] = []; @@ -131,9 +134,7 @@ export function fileRouter(baseDir: string, routesPath: string) { const rscId = pathItems.slice(0, index).join("/") || "index"; elements.push([ rscId, - index < pathItems.length - ? { childIndex: index + 1, search } - : { search }, + index < pathItems.length ? { childIndex: index + 1 } : { search }, ]); } const clientModules = new Set( From d59e6fdcf9b97b8a5fc123723d1d40290698e6a2 Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 21 May 2023 17:25:33 +0900 Subject: [PATCH 32/37] uninstall nodemon --- package.json | 5 ++- pnpm-lock.yaml | 85 ++------------------------------------------------ 2 files changed, 4 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index 489673bef..788baac65 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "compile:types": "tsc --project tsconfig.build.json", "test": "tsc --project . --noEmit", "e2e": "cd e2e/01 && playwright test && cd ../02 && playwright test", - "examples:dev": "WAKUWORK_CONFIG=\"{\\\"devServer\\\":{\\\"dir\\\":\\\"./examples/${NAME}\\\"}}\" nodemon --ext ts,tsx --ignore ./examples/${NAME}/src/Counter.tsx --exec 'npm run compile:code && ./cli.js dev'", + "examples:dev": "npm run compile:code && WAKUWORK_CONFIG=\"{\\\"devServer\\\":{\\\"dir\\\":\\\"./examples/${NAME}\\\"}}\" ./cli.js dev", "examples:dev:01_counter": "NAME=01_counter npm run examples:dev", "examples:dev:02_async": "NAME=02_async npm run examples:dev", "examples:dev:03_promise": "NAME=03_promise npm run examples:dev", @@ -64,7 +64,7 @@ "examples:prd:05_mutation": "NAME=05_mutation npm run examples:prd", "examples:prd:06_nesting": "NAME=06_nesting npm run examples:prd", "examples:prd:07_router": "NAME=07_router npm run examples:prd", - "website:dev": "WAKUWORK_CONFIG=\"{\\\"devServer\\\":{\\\"dir\\\":\\\"./website\\\"}}\" nodemon --ext ts,tsx --exec 'npm run compile:code && ./cli.js dev'", + "website:dev": "npm run compile:code && WAKUWORK_CONFIG=\"{\\\"devServer\\\":{\\\"dir\\\":\\\"./website\\\"}}\" ./cli.js dev", "website:build": "npm run compile:code && WAKUWORK_CONFIG=\"{\\\"build\\\":{\\\"dir\\\":\\\"./website\\\"}}\" ./cli.js build", "website:prd": "npm run website:build && WAKUWORK_CONFIG=\"{\\\"prdServer\\\":{\\\"dir\\\":\\\"./website/dist\\\"}}\" ./cli.js start" }, @@ -89,7 +89,6 @@ "@types/react": "^18.2.5", "@types/react-dom": "^18.2.3", "autoprefixer": "^10.4.14", - "nodemon": "^2.0.22", "postcss": "^8.4.23", "react": "18.3.0-canary-aef7ce554-20230503", "react-dom": "18.3.0-canary-aef7ce554-20230503", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b55031a86..ea056f4dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,9 +45,6 @@ devDependencies: autoprefixer: specifier: ^10.4.14 version: 10.4.14(postcss@8.4.23) - nodemon: - specifier: ^2.0.22 - version: 2.0.22 postcss: specifier: ^8.4.23 version: 8.4.23 @@ -957,10 +954,6 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true - /abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: true - /acorn-import-assertions@1.9.0(acorn@8.8.2): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: @@ -1238,18 +1231,6 @@ packages: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} dev: true - /debug@3.2.7(supports-color@5.5.0): - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - supports-color: 5.5.0 - dev: true - /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -1588,6 +1569,7 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + dev: false /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -1622,10 +1604,6 @@ packages: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true - /ignore-by-default@1.0.1: - resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} - dev: true - /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -1839,10 +1817,6 @@ packages: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: false - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true - /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} dependencies: @@ -1863,30 +1837,6 @@ packages: /node-releases@2.0.10: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} - /nodemon@2.0.22: - resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==} - engines: {node: '>=8.10.0'} - hasBin: true - dependencies: - chokidar: 3.5.3 - debug: 3.2.7(supports-color@5.5.0) - ignore-by-default: 1.0.1 - minimatch: 3.1.2 - pstree.remy: 1.1.8 - semver: 5.7.1 - simple-update-notifier: 1.1.0 - supports-color: 5.5.0 - touch: 3.1.0 - undefsafe: 2.0.5 - dev: true - - /nopt@1.0.10: - resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} - hasBin: true - dependencies: - abbrev: 1.1.1 - dev: true - /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2077,10 +2027,6 @@ packages: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: true - /pstree.remy@1.1.8: - resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} - dev: true - /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -2244,20 +2190,10 @@ packages: semver: 6.3.0 dev: true - /semver@5.7.1: - resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} - hasBin: true - dev: true - /semver@6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} hasBin: true - /semver@7.0.0: - resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} - hasBin: true - dev: true - /semver@7.5.0: resolution: {integrity: sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==} engines: {node: '>=10'} @@ -2300,13 +2236,6 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true - /simple-update-notifier@1.1.0: - resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} - engines: {node: '>=8.10.0'} - dependencies: - semver: 7.0.0 - dev: true - /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2400,6 +2329,7 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 + dev: false /supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} @@ -2518,13 +2448,6 @@ packages: ieee754: 1.2.1 dev: true - /touch@3.1.0: - resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} - hasBin: true - dependencies: - nopt: 1.0.10 - dev: true - /trim-repeated@2.0.0: resolution: {integrity: sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==} engines: {node: '>=12'} @@ -2542,10 +2465,6 @@ packages: hasBin: true dev: true - /undefsafe@2.0.5: - resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - dev: true - /update-browserslist-db@1.0.11(browserslist@4.21.5): resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} hasBin: true From cabd78c38ed6ed04fccece4f97108811d258feb5 Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 21 May 2023 22:57:05 +0900 Subject: [PATCH 33/37] support hmr and server reload --- src/cli-dev.ts | 2 + src/middleware/lib/rsc-handler-worker.ts | 14 ++- src/middleware/lib/rsc-handler.ts | 15 +++- src/middleware/lib/vite-plugin-rsc.ts | 106 +++++++++++++++++++++++ src/middleware/lib/vite-rsc-plugin.ts | 52 ----------- src/middleware/viteServer.ts | 7 +- 6 files changed, 139 insertions(+), 57 deletions(-) create mode 100644 src/middleware/lib/vite-plugin-rsc.ts delete mode 100644 src/middleware/lib/vite-rsc-plugin.ts diff --git a/src/cli-dev.ts b/src/cli-dev.ts index d4d4f813b..adb789b89 100644 --- a/src/cli-dev.ts +++ b/src/cli-dev.ts @@ -1,5 +1,7 @@ import { startDevServer } from "./devServer.js"; +process.env.NODE_ENV ||= "development"; + const config = process.env.WAKUWORK_CONFIG && JSON.parse(process.env.WAKUWORK_CONFIG); diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index 8a5d6714e..add9fdd90 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -11,7 +11,7 @@ import { transformRsfId, generatePrefetchCode } from "./rsc-utils.js"; import type { RenderInput, MessageReq, MessageRes } from "./rsc-handler.js"; import type { Config } from "../../config.js"; import type { GetEntry, GetBuilder, GetCustomModules } from "../../server.js"; -import { rscPlugin } from "./vite-rsc-plugin.js"; +import { rscTransformPlugin, rscReloadPlugin } from "./vite-plugin-rsc.js"; const { renderToPipeableStream } = RSDWServer; @@ -164,7 +164,17 @@ const distEntriesFile = path.join( const vitePromise = createServer({ root: dir, ...(process.env.NODE_ENV && { mode: process.env.NODE_ENV }), - plugins: [rscPlugin()], + plugins: [ + rscTransformPlugin(), + ...(process.env.NODE_ENV === "development" + ? [ + rscReloadPlugin(entriesFile, (type) => { + const mesg: MessageRes = { type }; + parentPort!.postMessage(mesg); + }), + ] + : []), + ], ssr: { noExternal: ["wakuwork"], // FIXME this doesn't seem ideal? }, diff --git a/src/middleware/lib/rsc-handler.ts b/src/middleware/lib/rsc-handler.ts index cd43203bb..33bc5d13a 100644 --- a/src/middleware/lib/rsc-handler.ts +++ b/src/middleware/lib/rsc-handler.ts @@ -39,6 +39,7 @@ export type MessageReq = }; export type MessageRes = + | { type: "full-reload" } | { id: number; type: "buf"; buf: ArrayBuffer; offset: number; len: number } | { id: number; type: "end" } | { id: number; type: "err"; err: unknown } @@ -47,9 +48,21 @@ export type MessageRes = const messageCallbacks = new Map void>(); worker.on("message", (mesg: MessageRes) => { - messageCallbacks.get(mesg.id)?.(mesg); + if ("id" in mesg) { + messageCallbacks.get(mesg.id)?.(mesg); + } }); +export function registerReloadCallback(fn: (type: "full-reload") => void) { + const listener = (mesg: MessageRes) => { + if (mesg.type === "full-reload") { + fn(mesg.type); + } + }; + worker.on("message", listener); + return () => worker.off("message", listener); +} + export function shutdown() { return new Promise((resolve) => { worker.on("close", resolve); diff --git a/src/middleware/lib/vite-plugin-rsc.ts b/src/middleware/lib/vite-plugin-rsc.ts new file mode 100644 index 000000000..ec7cd066a --- /dev/null +++ b/src/middleware/lib/vite-plugin-rsc.ts @@ -0,0 +1,106 @@ +import path from "node:path"; +import type { Plugin, ModuleNode } from "vite"; +import * as swc from "@swc/core"; +import * as RSDWNodeLoader from "react-server-dom-webpack/node-loader"; + +export const rscTransformPlugin = (): Plugin => { + return { + name: "rsc-transform-plugin", + async resolveId(id, importer, options) { + if (!id.endsWith(".js")) { + return id; + } + // FIXME This isn't necessary in production mode + for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { + const resolved = await this.resolve(id.slice(0, -3) + ext, importer, { + ...options, + skipSelf: true, + }); + if (resolved) { + return resolved; + } + } + }, + async transform(code, id) { + const resolve = async ( + specifier: string, + { parentURL }: { parentURL: string } + ) => { + if (!specifier) { + return { url: "" }; + } + const url = (await this.resolve(specifier, parentURL, { + skipSelf: true, + }))!.id; + return { url }; + }; + const load = async (url: string) => { + let source = url === id ? code : (await this.load({ id: url })).code; + // HACK move directives before import statements. + source = source!.replace( + /^(import {.*?} from ".*?";)\s*"use (client|server)";/, + '"use $2";$1' + ); + return { format: "module", source }; + }; + RSDWNodeLoader.resolve( + "", + { conditions: ["react-server"], parentURL: "" }, + resolve + ); + return (await RSDWNodeLoader.load(id, null, load)).source; + }, + }; +}; + +export const rscReloadPlugin = ( + entriesFile: string, + fn: (type: "full-reload") => void +): Plugin => { + const isClientEntry = (id: string, code: string) => { + const ext = path.extname(id); + if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) { + const mod = swc.parseSync(code, { + syntax: ext === ".ts" || ext === ".tsx" ? "typescript" : "ecmascript", + tsx: ext === ".tsx", + }); + for (const item of mod.body) { + if ( + item.type === "ExpressionStatement" && + item.expression.type === "StringLiteral" && + item.expression.value === "use client" + ) { + return true; + } + } + } + return false; + }; + const findRootImporters = async (nodes: Set) => { + const rootImporters = new Set(); + for (const node of nodes) { + if (node.importers.size) { + for (const i of await findRootImporters(node.importers)) { + rootImporters.add(i); + } + } else { + rootImporters.add(node); + } + } + return rootImporters; + }; + return { + name: "reload-plugin", + async handleHotUpdate(ctx) { + if ( + Array.from(await findRootImporters(new Set(ctx.modules))).some( + ({ file }) => + file?.replace(/\.\w+$/, "") === entriesFile.replace(/\.\w+$/, "") + ) && + !isClientEntry(ctx.file, await ctx.read()) + ) { + fn("full-reload"); + } + }, + }; +}; diff --git a/src/middleware/lib/vite-rsc-plugin.ts b/src/middleware/lib/vite-rsc-plugin.ts deleted file mode 100644 index e2a2c6e78..000000000 --- a/src/middleware/lib/vite-rsc-plugin.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Plugin } from "vite"; -import * as RSDWNodeLoader from "react-server-dom-webpack/node-loader"; - -export const rscPlugin = (): Plugin => { - return { - name: "rsc-plugin", - async resolveId(id, importer, options) { - if (!id.endsWith(".js")) { - return id; - } - // FIXME This isn't necessary in production mode - for (const ext of [".js", ".ts", ".tsx", ".jsx"]) { - const resolved = await this.resolve(id.slice(0, -3) + ext, importer, { - ...options, - skipSelf: true, - }); - if (resolved) { - return resolved; - } - } - }, - async transform(code, id) { - const resolve = async ( - specifier: string, - { parentURL }: { parentURL: string } - ) => { - if (!specifier) { - return { url: "" }; - } - const url = (await this.resolve(specifier, parentURL, { - skipSelf: true, - }))!.id; - return { url }; - }; - const load = async (url: string) => { - let source = url === id ? code : (await this.load({ id: url })).code; - // HACK move directives before import statements. - source = source!.replace( - /^(import {.*?} from ".*?";)\s*"use (client|server)";/, - '"use $2";$1' - ); - return { format: "module", source }; - }; - RSDWNodeLoader.resolve( - "", - { conditions: ["react-server"], parentURL: "" }, - resolve - ); - return (await RSDWNodeLoader.load(id, null, load)).source; - }, - }; -}; diff --git a/src/middleware/viteServer.ts b/src/middleware/viteServer.ts index 05750c49a..d630611a2 100644 --- a/src/middleware/viteServer.ts +++ b/src/middleware/viteServer.ts @@ -7,7 +7,7 @@ import react from "@vitejs/plugin-react"; import type { MiddlewareCreator } from "./lib/common.js"; import { codeToInject } from "./lib/rsc-utils.js"; -import { setClientEntries } from "./lib/rsc-handler.js"; +import { registerReloadCallback, setClientEntries } from "./lib/rsc-handler.js"; const rscIndexPlugin = (): Plugin => { return { @@ -42,6 +42,9 @@ const viteServer: MiddlewareCreator = (config) => { server: { middlewareMode: true }, appType: "custom", }); + vitePromise.then((vite) => { + registerReloadCallback((type) => vite.ws.send({ type })); + }); return async (req, res, next) => { const vite = await vitePromise; const absoluteClientEntries = Object.fromEntries( @@ -49,7 +52,7 @@ const viteServer: MiddlewareCreator = (config) => { ({ file, url }) => [file, url] ) ); - absoluteClientEntries['*'] = '*'; // HACK to use fallback resolver + absoluteClientEntries["*"] = "*"; // HACK to use fallback resolver // FIXME this is bad in performance, let's revisit it await setClientEntries(absoluteClientEntries); const indexFallback = async () => { From 31287430d064c269f24c13676df92db0fb027652 Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 21 May 2023 23:15:00 +0900 Subject: [PATCH 34/37] no need to check root importers, and it does not work with router --- src/middleware/lib/rsc-handler-worker.ts | 2 +- src/middleware/lib/vite-plugin-rsc.ts | 28 +++--------------------- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index add9fdd90..df47ab453 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -168,7 +168,7 @@ const vitePromise = createServer({ rscTransformPlugin(), ...(process.env.NODE_ENV === "development" ? [ - rscReloadPlugin(entriesFile, (type) => { + rscReloadPlugin((type) => { const mesg: MessageRes = { type }; parentPort!.postMessage(mesg); }), diff --git a/src/middleware/lib/vite-plugin-rsc.ts b/src/middleware/lib/vite-plugin-rsc.ts index ec7cd066a..e809fedd4 100644 --- a/src/middleware/lib/vite-plugin-rsc.ts +++ b/src/middleware/lib/vite-plugin-rsc.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import type { Plugin, ModuleNode } from "vite"; +import type { Plugin } from "vite"; import * as swc from "@swc/core"; import * as RSDWNodeLoader from "react-server-dom-webpack/node-loader"; @@ -53,10 +53,7 @@ export const rscTransformPlugin = (): Plugin => { }; }; -export const rscReloadPlugin = ( - entriesFile: string, - fn: (type: "full-reload") => void -): Plugin => { +export const rscReloadPlugin = (fn: (type: "full-reload") => void): Plugin => { const isClientEntry = (id: string, code: string) => { const ext = path.extname(id); if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) { @@ -76,29 +73,10 @@ export const rscReloadPlugin = ( } return false; }; - const findRootImporters = async (nodes: Set) => { - const rootImporters = new Set(); - for (const node of nodes) { - if (node.importers.size) { - for (const i of await findRootImporters(node.importers)) { - rootImporters.add(i); - } - } else { - rootImporters.add(node); - } - } - return rootImporters; - }; return { name: "reload-plugin", async handleHotUpdate(ctx) { - if ( - Array.from(await findRootImporters(new Set(ctx.modules))).some( - ({ file }) => - file?.replace(/\.\w+$/, "") === entriesFile.replace(/\.\w+$/, "") - ) && - !isClientEntry(ctx.file, await ctx.read()) - ) { + if (ctx.modules.length && !isClientEntry(ctx.file, await ctx.read())) { fn("full-reload"); } }, From d9f37be12171de8f0bd3ab2db5340de7d1d82319 Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 21 May 2023 23:57:17 +0900 Subject: [PATCH 35/37] add comments --- src/builder.ts | 5 ++++- src/middleware/lib/rsc-handler-worker.ts | 2 +- src/middleware/viteServer.ts | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index 4b6a21c5c..7f1cf32ba 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -99,7 +99,10 @@ export async function runBuild(config: Config = {}) { ), ], ssr: { - noExternal: ["wakuwork"], // TODO we need to add all possible libs + // FIXME Without this, wakuwork/router isn't considered to have client + // entries, and "No client entry" error occurs. + // Unless we fix this, RSC-capable packages aren't supported. + noExternal: ["wakuwork"], }, build: { outDir: distPath, diff --git a/src/middleware/lib/rsc-handler-worker.ts b/src/middleware/lib/rsc-handler-worker.ts index df47ab453..7e4fa0d44 100644 --- a/src/middleware/lib/rsc-handler-worker.ts +++ b/src/middleware/lib/rsc-handler-worker.ts @@ -210,7 +210,7 @@ const resolveClientEntry = (filePath: string) => { if (absoluteClientEntries["*"] === "*") { return basePath + path.relative(dir, filePath); } - throw new Error("No client entry found"); + throw new Error("No client entry found for " + filePath); } return clientEntry; }; diff --git a/src/middleware/viteServer.ts b/src/middleware/viteServer.ts index d630611a2..3c6ed0cf9 100644 --- a/src/middleware/viteServer.ts +++ b/src/middleware/viteServer.ts @@ -32,7 +32,9 @@ const viteServer: MiddlewareCreator = (config) => { root: dir, optimizeDeps: { include: ["react-server-dom-webpack/client"], - exclude: ["wakuwork"], // TODO we need to add all possible libs + // FIXME without this, wakuwork router has dual module hazard, + // and "Uncaught Error: Missing Router" happens. + exclude: ["wakuwork"], }, plugins: [ // @ts-ignore From 3b51df594d45690ced9f16cb1dd0fe97df0b9381 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 22 May 2023 11:40:16 +0900 Subject: [PATCH 36/37] clean up unused code --- src/config.ts | 4 ---- src/devServer.ts | 4 +--- src/main.ts | 1 - src/middleware/lib/common.ts | 4 +--- src/prdServer.ts | 4 +--- 5 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/config.ts b/src/config.ts index 2cbe6fbfe..383e38625 100644 --- a/src/config.ts +++ b/src/config.ts @@ -36,7 +36,3 @@ export type Config = { build?: Build; files?: Files; }; - -export function defineConfig(config: Config): Config { - return config; -} diff --git a/src/devServer.ts b/src/devServer.ts index 181c8d685..a8e1c2651 100644 --- a/src/devServer.ts +++ b/src/devServer.ts @@ -2,10 +2,8 @@ import http from "node:http"; import type { Config, Middleware } from "./config.js"; import { pipe } from "./middleware/lib/common.js"; -import type { Shared } from "./middleware/lib/common.js"; export function startDevServer(config: Config = {}) { - const shared: Shared = {}; const middlewares = config.devServer?.middlewares || [ "rewriteRsc", "rscDev", @@ -16,7 +14,7 @@ export function startDevServer(config: Config = {}) { middlewares.map(async (middleware) => { if (typeof middleware === "string") { const mod = await import(`./middleware/${middleware}.js`); - return (mod.default || mod)(config, shared); + return (mod.default || mod)(config); } return middleware; }) diff --git a/src/main.ts b/src/main.ts index 540acbb43..4c8c34e04 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,3 @@ -export { defineConfig } from "./config.js"; export type { Config } from "./config.js"; export { startDevServer } from "./devServer.js"; diff --git a/src/middleware/lib/common.ts b/src/middleware/lib/common.ts index 5e6c8953f..a175a0b1b 100644 --- a/src/middleware/lib/common.ts +++ b/src/middleware/lib/common.ts @@ -1,8 +1,6 @@ import type { Config, Middleware } from "../../config.js"; -export type Shared = {}; - -export type MiddlewareCreator = (config: Config, shared: Shared) => Middleware; +export type MiddlewareCreator = (config: Config) => Middleware; export const pipe = (middlewares: Middleware[]): Middleware => diff --git a/src/prdServer.ts b/src/prdServer.ts index b2eb5c821..1e54a8814 100644 --- a/src/prdServer.ts +++ b/src/prdServer.ts @@ -2,10 +2,8 @@ import http from "node:http"; import type { Config, Middleware } from "./config.js"; import { pipe } from "./middleware/lib/common.js"; -import type { Shared } from "./middleware/lib/common.js"; export function startPrdServer(config: Config = {}) { - const shared: Shared = {}; const middlewares = config.prdServer?.middlewares || [ "staticFile", "rewriteRsc", @@ -17,7 +15,7 @@ export function startPrdServer(config: Config = {}) { middlewares.map(async (middleware) => { if (typeof middleware === "string") { const mod = await import(`./middleware/${middleware}.js`); - return (mod.default || mod)(config, shared); + return (mod.default || mod)(config); } return middleware; }) From 9f235c305e4d1b3bf3e92c97da4323091ed22b8e Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 22 May 2023 12:03:15 +0900 Subject: [PATCH 37/37] run prettier --- src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 41c7092cd..899585613 100644 --- a/src/server.ts +++ b/src/server.ts @@ -23,5 +23,5 @@ export type GetBuilder = ( // This is for ignored dynamic imports // XXX Are there any better ways? export type GetCustomModules = () => Promise<{ - [name: string]: string + [name: string]: string; }>;