From 67bd52c2739e7abcc2ec92df731ca19503ea40bf Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 10:47:30 +0900 Subject: [PATCH 01/18] chore: tweak demo --- packages/react-server/examples/og/app/page.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-server/examples/og/app/page.tsx b/packages/react-server/examples/og/app/page.tsx index 1dcf09945..c1a7eb06b 100644 --- a/packages/react-server/examples/og/app/page.tsx +++ b/packages/react-server/examples/og/app/page.tsx @@ -22,6 +22,9 @@ export default function Page(props: PageProps) { /og?title=World +
+ +
); } From a098be2cecd1d3ae1acd385ccd934334b3281f99 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 11:44:15 +0900 Subject: [PATCH 02/18] feat: support next edge wasm module and asset --- .../react-server/examples/og/package.json | 1 + .../react-server/examples/og/vite.config.ts | 113 +++++++++++++++++- pnpm-lock.yaml | 3 + 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/packages/react-server/examples/og/package.json b/packages/react-server/examples/og/package.json index 81a419ae1..9729b73bd 100644 --- a/packages/react-server/examples/og/package.json +++ b/packages/react-server/examples/og/package.json @@ -28,6 +28,7 @@ "@types/react-dom": "latest", "@vercel/ncc": "^0.38.1", "@vercel/nft": "^0.27.3", + "magic-string": "^0.30.8", "rolldown": "^0.12.1", "rollup": "^4.18.1", "vite": "latest" diff --git a/packages/react-server/examples/og/vite.config.ts b/packages/react-server/examples/og/vite.config.ts index 59f9d7907..94569679b 100644 --- a/packages/react-server/examples/og/vite.config.ts +++ b/packages/react-server/examples/og/vite.config.ts @@ -1,6 +1,115 @@ +import fs from "node:fs"; +import path from "node:path"; +import MagicString from "magic-string"; import next from "next/vite"; -import { defineConfig } from "vite"; +import { type ConfigEnv, type Plugin, defineConfig } from "vite"; export default defineConfig({ - plugins: [next()], + plugins: [ + next({ + plugins: [ + { + name: "config", + config() { + return { + resolve: { + alias: { + "@vercel/og": "/node_modules/@vercel/og/dist/index.edge.js", + }, + }, + }; + }, + }, + nextEdgeWasmPlugin(), + nextEdgeAssetPlugin(), + ], + }), + ], }); + +// cf. packages/react-server/examples/basic/vite-plugin-wasm-module.ts +function nextEdgeWasmPlugin(): Plugin { + const MARKER = "\0:virtual:wasm-module"; + let env: ConfigEnv; + return { + name: nextEdgeWasmPlugin.name, + config(_, env_) { + env = env_; + }, + resolveId: { + order: "pre", + async handler(source, importer, options) { + if (source.endsWith(".wasm?module")) { + const resolved = await this.resolve( + source.slice(0, -"?module".length), + importer, + options, + ); + if (resolved) { + return { id: MARKER + resolved.id }; + } + } + }, + }, + load(id) { + if (id.startsWith(MARKER)) { + const file = id.slice(MARKER.length); + + // readFile + instantiate wasm module + if (env.command === "serve") { + return ` + import fs from "node:fs"; + const buffer = fs.readFileSync(${JSON.stringify(file)}); + export default new WebAssembly.Module(buffer); + `; + } + + // emit wasm asset + rewrite import + if (env.command === "build") { + const referenceId = this.emitFile({ + type: "asset", + name: path.basename(file), + source: fs.readFileSync(file), + }); + // temporary placeholder which we replace during `renderChunk` + return `export default "__WASM_IMPORT_URL_${referenceId}"`; + } + } + }, + renderChunk(code, chunk) { + const matches = code.matchAll(/"__WASM_IMPORT_URL_(\w+)"/dg); + const output = new MagicString(code); + for (const match of matches) { + const referenceId = match[1]; + const assetFileName = this.getFileName(referenceId); + const importSource = + "./" + + path.relative( + path.resolve(chunk.fileName, ".."), + path.resolve(assetFileName), + ); + const importName = `__wasm_${referenceId}`; + const [start, end] = match.indices![0]; + output.prepend(`import ${importName} from "${importSource}";\n`); + output.update(start, end, importName); + } + if (output.hasChanged()) { + return output.toString(); + } + }, + }; +} + +// input +// fetch(new URL("some-asset.bin", import.meta.url)) +// output +// ??? +function nextEdgeAssetPlugin(): Plugin { + return { + name: nextEdgeAssetPlugin.name, + transform(code) { + // TODO + code; + }, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 752188223..e71304eb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -410,6 +410,9 @@ importers: '@vercel/nft': specifier: ^0.27.3 version: 0.27.3 + magic-string: + specifier: ^0.30.8 + version: 0.30.10 rolldown: specifier: ^0.12.1 version: 0.12.1 From 32b3818b0a7b83079c497cc04cf50a5876b3db27 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 12:09:16 +0900 Subject: [PATCH 03/18] wip: nextEdgeAssetPlugin for dev --- .../react-server/examples/og/vite.config.ts | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/react-server/examples/og/vite.config.ts b/packages/react-server/examples/og/vite.config.ts index 94569679b..672b9fa36 100644 --- a/packages/react-server/examples/og/vite.config.ts +++ b/packages/react-server/examples/og/vite.config.ts @@ -100,16 +100,49 @@ function nextEdgeWasmPlugin(): Plugin { }; } +// // input // fetch(new URL("some-asset.bin", import.meta.url)) -// output +// +// output (dev) +// import("node:fs").then(fs => new Response(fs.readFileSync(...))) +// +// output (build) // ??? +// function nextEdgeAssetPlugin(): Plugin { + // cf. https://github.com/vitejs/vite/blob/0f56e1724162df76fffd5508148db118767ebe32/packages/vite/src/node/plugins/assetImportMetaUrl.ts#L51-L52 + const FETCH_ASSET_RE = + /\bfetch\s*\(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)\s*(?:,\s*)?\)/dg; + + let env: ConfigEnv; return { name: nextEdgeAssetPlugin.name, - transform(code) { - // TODO - code; + config(_, env_) { + env = env_; + }, + transform(code, id) { + if (code.includes("import.meta.url")) { + const output = new MagicString(code); + const matches = code.matchAll(FETCH_ASSET_RE); + for (const match of matches) { + const urlArg = match[1]!.slice(1, -1); + const absFile = path.resolve(id, "..", urlArg); + if (fs.existsSync(absFile)) { + let replacement: string; + if (env.command === "serve") { + replacement = `import("node:fs").then(fs => new Response(fs.readFileSync(${JSON.stringify(absFile)})))`; + } else { + replacement = "todo"; + } + const [start, end] = match.indices![0]!; + output.update(start, end, replacement); + } + } + if (output.hasChanged()) { + return { code: output.toString(), map: output.generateMap() }; + } + } }, }; } From 4e375b551cb52e2edf5d14addcc986de4eca7711 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 13:00:21 +0900 Subject: [PATCH 04/18] wip: esbuild loaders config --- .../src/vite/adapters/cloudflare/build.ts | 7 +++++++ .../react-server-next/src/vite/adapters/vercel/build.ts | 7 +++++++ packages/react-server/examples/og/vite.config.ts | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/react-server-next/src/vite/adapters/cloudflare/build.ts b/packages/react-server-next/src/vite/adapters/cloudflare/build.ts index 30ed7fad7..3594b15b5 100644 --- a/packages/react-server-next/src/vite/adapters/cloudflare/build.ts +++ b/packages/react-server-next/src/vite/adapters/cloudflare/build.ts @@ -60,6 +60,13 @@ export async function build({ outDir }: { outDir: string }) { format: "esm", platform: "browser", external: ["node:async_hooks"], + // externalize known module types + // https://developers.cloudflare.com/pages/functions/module-support + // https://github.com/cloudflare/workers-sdk/blob/a9b4f252ccbce4856ffc967e51c0aa8cf2e1bb4f/packages/workers-playground/src/QuickEditor/module-collection.ts#L78-L84 + loader: { + ".wasm": "copy", + ".bin": "copy", + }, define: { "process.env.NODE_ENV": `"production"`, }, diff --git a/packages/react-server-next/src/vite/adapters/vercel/build.ts b/packages/react-server-next/src/vite/adapters/vercel/build.ts index bdabeb35c..8d442cbd1 100644 --- a/packages/react-server-next/src/vite/adapters/vercel/build.ts +++ b/packages/react-server-next/src/vite/adapters/vercel/build.ts @@ -103,6 +103,13 @@ export async function build({ format: "esm", platform: runtime === "node" ? "node" : "browser", external: ["node:async_hooks"], + loader: + runtime === "node" + ? undefined + : { + ".wasm": "copy", + ".bin": "copy", + }, define: { "process.env.NODE_ENV": `"production"`, }, diff --git a/packages/react-server/examples/og/vite.config.ts b/packages/react-server/examples/og/vite.config.ts index 672b9fa36..21793dd17 100644 --- a/packages/react-server/examples/og/vite.config.ts +++ b/packages/react-server/examples/og/vite.config.ts @@ -102,13 +102,13 @@ function nextEdgeWasmPlugin(): Plugin { // // input -// fetch(new URL("some-asset.bin", import.meta.url)) +// fetch(new URL("./some-asset.bin", import.meta.url)) // // output (dev) // import("node:fs").then(fs => new Response(fs.readFileSync(...))) // // output (build) -// ??? +// import("./relocated-asset.bin").then(mod => new Response(mod.default)) // function nextEdgeAssetPlugin(): Plugin { // cf. https://github.com/vitejs/vite/blob/0f56e1724162df76fffd5508148db118767ebe32/packages/vite/src/node/plugins/assetImportMetaUrl.ts#L51-L52 From d88325c04253de8112e6970b82773f157468c9ad Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 13:11:57 +0900 Subject: [PATCH 05/18] chore: script --- packages/react-server/examples/og/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-server/examples/og/package.json b/packages/react-server/examples/og/package.json index 9729b73bd..814d25e0c 100644 --- a/packages/react-server/examples/og/package.json +++ b/packages/react-server/examples/og/package.json @@ -8,6 +8,9 @@ "build": "next build", "start": "next start", "copy-assets": "cp node_modules/@vercel/og/dist/*.{ttf,wasm} .vercel/output/functions/index.func", + "cf-build": "CF_PAGES=1 pnpm build", + "cf-preview": "wrangler pages dev ./dist/cloudflare --compatibility-date=2024-01-01 --compatibility-flags=nodejs_compat", + "cf-release": "wrangler pages deploy ./dist/cloudflare --commit-dirty --branch main --project-name next-vite-example-og", "vc-build": "VERCEL=1 pnpm build && pnpm copy-assets", "vc-release-staging": "vercel deploy --prebuilt", "vc-release": "vercel deploy --prod --prebuilt" From 6356490117bb8e9e263522af0b03cdc6f3613364 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 13:31:15 +0900 Subject: [PATCH 06/18] wip: nextEdgeAssetPlugin for build --- .../react-server/examples/og/vite.config.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/react-server/examples/og/vite.config.ts b/packages/react-server/examples/og/vite.config.ts index 21793dd17..d323cc6e1 100644 --- a/packages/react-server/examples/og/vite.config.ts +++ b/packages/react-server/examples/og/vite.config.ts @@ -133,7 +133,12 @@ function nextEdgeAssetPlugin(): Plugin { if (env.command === "serve") { replacement = `import("node:fs").then(fs => new Response(fs.readFileSync(${JSON.stringify(absFile)})))`; } else { - replacement = "todo"; + const referenceId = this.emitFile({ + type: "asset", + name: path.basename(absFile) + ".bin", + source: fs.readFileSync(absFile), + }); + replacement = `"__FETCH_ASSET_IMPORT_${referenceId}".then(mod => new Response(mod.default))`; } const [start, end] = match.indices![0]!; output.update(start, end, replacement); @@ -144,5 +149,25 @@ function nextEdgeAssetPlugin(): Plugin { } } }, + renderChunk(code, chunk) { + const matches = code.matchAll(/"__FETCH_ASSET_IMPORT_(\w+)"/dg); + const output = new MagicString(code); + for (const match of matches) { + const referenceId = match[1]; + const assetFileName = this.getFileName(referenceId); + const importSource = + "./" + + path.relative( + path.resolve(chunk.fileName, ".."), + path.resolve(assetFileName), + ); + const [start, end] = match.indices![0]; + const replacement = `import(${JSON.stringify(importSource)})`; + output.update(start, end, replacement); + } + if (output.hasChanged()) { + return output.toString(); + } + }, }; } From 73e0bcde1baab9c04f14731cffbd4a270243ffaa Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 14:19:15 +0900 Subject: [PATCH 07/18] wip: support mode: "fs" --- .../react-server/examples/og/vite.config.ts | 95 +++++++++++++++---- 1 file changed, 75 insertions(+), 20 deletions(-) diff --git a/packages/react-server/examples/og/vite.config.ts b/packages/react-server/examples/og/vite.config.ts index d323cc6e1..60bbdf45c 100644 --- a/packages/react-server/examples/og/vite.config.ts +++ b/packages/react-server/examples/og/vite.config.ts @@ -20,19 +20,35 @@ export default defineConfig({ }; }, }, - nextEdgeWasmPlugin(), - nextEdgeAssetPlugin(), + wasmModulePlugin({ mode: process.env.CF_PAGES ? "import" : "fs" }), + fetchImportMetaUrlPlugin({ + mode: process.env.CF_PAGES ? "import" : "fs", + }), ], }), ], }); // cf. packages/react-server/examples/basic/vite-plugin-wasm-module.ts -function nextEdgeWasmPlugin(): Plugin { +// +// input +// import wasm from "xxx.wasm?module" +// +// output (fs / dev) +// ?? +// +// output (fs / build) +// ?? +// +// output (import / build) +// ?? +// +function wasmModulePlugin(options: { mode: "fs" | "import" }): Plugin { const MARKER = "\0:virtual:wasm-module"; let env: ConfigEnv; + return { - name: nextEdgeWasmPlugin.name, + name: wasmModulePlugin.name, config(_, env_) { env = env_; }, @@ -56,28 +72,43 @@ function nextEdgeWasmPlugin(): Plugin { const file = id.slice(MARKER.length); // readFile + instantiate wasm module - if (env.command === "serve") { + if (options.mode === "fs") { + let source: string; + if (env.command === "serve") { + source = JSON.stringify(file); + } else { + const referenceId = this.emitFile({ + type: "asset", + name: path.basename(file), + source: fs.readFileSync(file), + }); + source = `fileURLToPath(import.meta.ROLLUP_FILE_URL_${referenceId})`; + } return ` import fs from "node:fs"; - const buffer = fs.readFileSync(${JSON.stringify(file)}); + import { fileURLToPath } from "node:url"; + const buffer = fs.readFileSync(${source}); export default new WebAssembly.Module(buffer); `; } // emit wasm asset + rewrite import - if (env.command === "build") { + if (options.mode === "import") { + if (env.command === "serve") { + throw new Error("unsupported"); + } const referenceId = this.emitFile({ type: "asset", name: path.basename(file), source: fs.readFileSync(file), }); // temporary placeholder which we replace during `renderChunk` - return `export default "__WASM_IMPORT_URL_${referenceId}"`; + return `export default "__WASM_MODULE_IMPORT_${referenceId}"`; } } }, renderChunk(code, chunk) { - const matches = code.matchAll(/"__WASM_IMPORT_URL_(\w+)"/dg); + const matches = code.matchAll(/"__WASM_MODULE_IMPORT_(\w+)"/dg); const output = new MagicString(code); for (const match of matches) { const referenceId = match[1]; @@ -102,22 +133,26 @@ function nextEdgeWasmPlugin(): Plugin { // // input -// fetch(new URL("./some-asset.bin", import.meta.url)) +// fetch(new URL("./xxx", import.meta.url)) // -// output (dev) -// import("node:fs").then(fs => new Response(fs.readFileSync(...))) +// output (fs / dev) +// new Response(fs.readFileSync("/absolute-path-to/xxx") // -// output (build) -// import("./relocated-asset.bin").then(mod => new Response(mod.default)) +// output (fs / build) +// import("node:fs").then(fs => new Response(fs.readFileSync(new URL("./relocated-xxx", import.meta.url).href))) // -function nextEdgeAssetPlugin(): Plugin { +// output (import / build) +// import("./relocated-xxx.bin").then(mod => new Response(mod.default)) +// +function fetchImportMetaUrlPlugin(options: { mode: "fs" | "import" }): Plugin { // cf. https://github.com/vitejs/vite/blob/0f56e1724162df76fffd5508148db118767ebe32/packages/vite/src/node/plugins/assetImportMetaUrl.ts#L51-L52 const FETCH_ASSET_RE = /\bfetch\s*\(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)\s*(?:,\s*)?\)/dg; let env: ConfigEnv; + return { - name: nextEdgeAssetPlugin.name, + name: fetchImportMetaUrlPlugin.name, config(_, env_) { env = env_; }, @@ -129,10 +164,30 @@ function nextEdgeAssetPlugin(): Plugin { const urlArg = match[1]!.slice(1, -1); const absFile = path.resolve(id, "..", urlArg); if (fs.existsSync(absFile)) { - let replacement: string; - if (env.command === "serve") { - replacement = `import("node:fs").then(fs => new Response(fs.readFileSync(${JSON.stringify(absFile)})))`; - } else { + let replacement!: string; + if (options.mode === "fs") { + if (env.command === "serve") { + replacement = `((async () => { + const fs = await import("node:fs"); + return new Response(fs.readFileSync(${JSON.stringify(absFile)})); + })())`; + } else { + const referenceId = this.emitFile({ + type: "asset", + name: path.basename(absFile), + source: fs.readFileSync(absFile), + }); + replacement = `((async () => { + const fs = await import("node:fs"); + const { fileURLToPath } = await import("node:url"); + return new Response(fs.readFileSync(fileURLToPath(import.meta.ROLLUP_FILE_URL_${referenceId}))); + })())`; + } + } + if (options.mode === "import") { + if (env.command === "serve") { + throw new Error("unsupported"); + } const referenceId = this.emitFile({ type: "asset", name: path.basename(absFile) + ".bin", From 1234f2bec4e7926baa83c37d7d20d4881c7eb1b7 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 14:21:38 +0900 Subject: [PATCH 08/18] chore: tweak --- packages/react-server/examples/og/app/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-server/examples/og/app/page.tsx b/packages/react-server/examples/og/app/page.tsx index c1a7eb06b..689b009c0 100644 --- a/packages/react-server/examples/og/app/page.tsx +++ b/packages/react-server/examples/og/app/page.tsx @@ -19,7 +19,7 @@ export default function Page(props: PageProps) { /og
  • - /og?title=World + /og?title=Hello
  • From 968f2fd7334b57a40c925e422488c70a9151d1f1 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 14:22:31 +0900 Subject: [PATCH 09/18] chore: tweak demo --- packages/react-server/examples/og/app/og/route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-server/examples/og/app/og/route.tsx b/packages/react-server/examples/og/app/og/route.tsx index 59def4507..83ff16547 100644 --- a/packages/react-server/examples/og/app/og/route.tsx +++ b/packages/react-server/examples/og/app/og/route.tsx @@ -6,7 +6,7 @@ export async function GET(request: Request) { const { ImageResponse } = await import("@vercel/og"); const url = new URL(request.url); - const title = url.searchParams.get("title") ?? "Hello!"; + const title = url.searchParams.get("title") ?? "Test"; return new ImageResponse(
    Date: Mon, 19 Aug 2024 14:36:46 +0900 Subject: [PATCH 10/18] chore: readme --- packages/react-server/examples/og/README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/react-server/examples/og/README.md b/packages/react-server/examples/og/README.md index e80cff6a7..255bd3661 100644 --- a/packages/react-server/examples/og/README.md +++ b/packages/react-server/examples/og/README.md @@ -1,6 +1,7 @@ # @vercel/og example -https://next-vite-example-og.vercel.app +- https://next-vite-example-og.vercel.app +- https://next-vite-example-og.pages.dev/ ```sh # local @@ -8,11 +9,17 @@ pnpm dev pnpm build pnpm preview -# deploy vercel +# deploy vercel serverless vercel projects add next-vite-example-og vercel link -p next-vite-example-og pnpm vc-build pnpm vc-release + +# deploy cloudflare pages +pnpm cf-build +pnpm cf-preview +wrangler pages project create next-vite-example-og --production-branch main --compatibility-date=2024-01-01 --compatibility-flags=nodejs_compat +pnpm cf-release ``` ## compare bundlers From 5f1ba7bf75b5e9f4b86cc10b8773597383e2ca9b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 14:47:03 +0900 Subject: [PATCH 11/18] wip: esbuildPluginAssetImportMetaUrl --- .../react-server-next/src/vite/adapters/shared.ts | 11 +++++++++++ .../src/vite/adapters/vercel/build.ts | 2 ++ packages/react-server/examples/og/package.json | 3 +-- packages/react-server/examples/og/vite.config.ts | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 packages/react-server-next/src/vite/adapters/shared.ts diff --git a/packages/react-server-next/src/vite/adapters/shared.ts b/packages/react-server-next/src/vite/adapters/shared.ts new file mode 100644 index 000000000..c6133bfc7 --- /dev/null +++ b/packages/react-server-next/src/vite/adapters/shared.ts @@ -0,0 +1,11 @@ +import type * as esbuild from "esbuild"; + +// cf. https://github.com/hi-ogawa/vite-plugins/blob/998561660c8a27e067e976d4ede9dca53984d41a/packages/pre-bundle-new-url/src/index.ts#L26 +export function esbuildPluginAssetImportMetaUrl(): esbuild.Plugin { + return { + name: esbuildPluginAssetImportMetaUrl.name, + setup(build) { + build; + }, + }; +} diff --git a/packages/react-server-next/src/vite/adapters/vercel/build.ts b/packages/react-server-next/src/vite/adapters/vercel/build.ts index 8d442cbd1..18f5f21c0 100644 --- a/packages/react-server-next/src/vite/adapters/vercel/build.ts +++ b/packages/react-server-next/src/vite/adapters/vercel/build.ts @@ -2,6 +2,7 @@ import { existsSync } from "node:fs"; import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import type { PrerenderManifest } from "@hiogawa/react-server/plugin"; +import { esbuildPluginAssetImportMetaUrl } from "../shared"; const configJson = { version: 3, @@ -116,6 +117,7 @@ export async function build({ logOverride: { "ignored-bare-import": "silent", }, + plugins: runtime === "node" ? [esbuildPluginAssetImportMetaUrl()] : [], }); await writeFile( join(buildDir, "esbuild-metafile.json"), diff --git a/packages/react-server/examples/og/package.json b/packages/react-server/examples/og/package.json index 814d25e0c..e95995f99 100644 --- a/packages/react-server/examples/og/package.json +++ b/packages/react-server/examples/og/package.json @@ -7,11 +7,10 @@ "dev": "next dev", "build": "next build", "start": "next start", - "copy-assets": "cp node_modules/@vercel/og/dist/*.{ttf,wasm} .vercel/output/functions/index.func", "cf-build": "CF_PAGES=1 pnpm build", "cf-preview": "wrangler pages dev ./dist/cloudflare --compatibility-date=2024-01-01 --compatibility-flags=nodejs_compat", "cf-release": "wrangler pages deploy ./dist/cloudflare --commit-dirty --branch main --project-name next-vite-example-og", - "vc-build": "VERCEL=1 pnpm build && pnpm copy-assets", + "vc-build": "VERCEL=1 pnpm build", "vc-release-staging": "vercel deploy --prebuilt", "vc-release": "vercel deploy --prod --prebuilt" }, diff --git a/packages/react-server/examples/og/vite.config.ts b/packages/react-server/examples/og/vite.config.ts index 60bbdf45c..23654dde6 100644 --- a/packages/react-server/examples/og/vite.config.ts +++ b/packages/react-server/examples/og/vite.config.ts @@ -29,7 +29,7 @@ export default defineConfig({ ], }); -// cf. packages/react-server/examples/basic/vite-plugin-wasm-module.ts +// cf. https://github.com/hi-ogawa/vite-plugins/blob/998561660c8a27e067e976d4ede9dca53984d41a/packages/react-server/examples/basic/vite-plugin-wasm-module.ts // // input // import wasm from "xxx.wasm?module" From dd6a45f92b8a9562627fa462217a060d8f131098 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 15:01:32 +0900 Subject: [PATCH 12/18] feat: copy esbuildPluginAssetImportMetaUrl --- packages/react-server-next/package.json | 1 + .../src/vite/adapters/shared.ts | 74 ++++++++++++++++++- pnpm-lock.yaml | 3 + 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/react-server-next/package.json b/packages/react-server-next/package.json index 6ed0b5cbd..49e22180a 100644 --- a/packages/react-server-next/package.json +++ b/packages/react-server-next/package.json @@ -50,6 +50,7 @@ "@hiogawa/vite-plugin-ssr-middleware": "workspace:*", "@vitejs/plugin-react-swc": "^3.7.0", "esbuild": "^0.20.2", + "magic-string": "^0.30.8", "vite-tsconfig-paths": "^4.3.2" }, "devDependencies": { diff --git a/packages/react-server-next/src/vite/adapters/shared.ts b/packages/react-server-next/src/vite/adapters/shared.ts index c6133bfc7..50d1826cf 100644 --- a/packages/react-server-next/src/vite/adapters/shared.ts +++ b/packages/react-server-next/src/vite/adapters/shared.ts @@ -1,11 +1,83 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; import type * as esbuild from "esbuild"; +import MagicString from "magic-string"; // cf. https://github.com/hi-ogawa/vite-plugins/blob/998561660c8a27e067e976d4ede9dca53984d41a/packages/pre-bundle-new-url/src/index.ts#L26 export function esbuildPluginAssetImportMetaUrl(): esbuild.Plugin { return { name: esbuildPluginAssetImportMetaUrl.name, setup(build) { - build; + let outdir: string; + build.onStart(() => { + if (build.initialOptions.outdir) { + outdir = build.initialOptions.outdir; + } + if (build.initialOptions.outfile) { + outdir = path.dirname(build.initialOptions.outfile); + } + if (!outdir) { + throw new Error("unreachable"); + } + }); + + build.onLoad( + { filter: /\.[cm]?js$/, namespace: "file" }, + async (args) => { + const data = await fs.promises.readFile(args.path, "utf-8"); + if (data.includes("import.meta.url")) { + const output = new MagicString(data); + + // replace + // new URL("./xxx.bin", import.meta.url) + // with + // new URL("./__asset-xxx-(hash).bin", import.meta.url) + const matches = data.matchAll(assetImportMetaUrlRE); + for (const match of matches) { + const [urlStart, urlEnd] = match.indices![1]!; + const url = match[1]!.slice(1, -1); + if (url[0] !== "/") { + const absUrl = path.resolve(path.dirname(args.path), url); + if (fs.existsSync(absUrl)) { + const assetData = await fs.promises.readFile(absUrl); + const hash = crypto + .createHash("sha1") + .update(assetData) + .digest() + .toString("hex") + .slice(0, 8); + const name = path + .basename(absUrl) + .replace(/[^0-9a-zA-Z]/g, "_"); + const filename = + `__asset-${name}-${hash}` + path.extname(absUrl); + await fs.promises.writeFile( + path.join(outdir, filename), + assetData, + ); + output.update( + urlStart, + urlEnd, + JSON.stringify(`./${filename}`), + ); + } + } + } + if (output.hasChanged()) { + return { + loader: "js", + contents: output.toString(), + }; + } + } + return null; + }, + ); }, }; } + +// https://github.com/vitejs/vite/blob/0f56e1724162df76fffd5508148db118767ebe32/packages/vite/src/node/plugins/assetImportMetaUrl.ts#L51-L52 +const assetImportMetaUrlRE = + /\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/dg; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e71304eb0..ec285b98d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,6 +176,9 @@ importers: esbuild: specifier: ^0.20.2 version: 0.20.2 + magic-string: + specifier: ^0.30.8 + version: 0.30.10 react: specifier: 19.0.0-rc-df5f2736-20240712 version: 19.0.0-rc-df5f2736-20240712 From 0fbee01799c25a27005ce1c8b074c2351bf586f2 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 15:10:20 +0900 Subject: [PATCH 13/18] chore: move code --- .../react-server/examples/og/vite-plugins.ts | 204 ++++++++++++++++++ .../react-server/examples/og/vite.config.ts | 204 +----------------- 2 files changed, 206 insertions(+), 202 deletions(-) create mode 100644 packages/react-server/examples/og/vite-plugins.ts diff --git a/packages/react-server/examples/og/vite-plugins.ts b/packages/react-server/examples/og/vite-plugins.ts new file mode 100644 index 000000000..294b317eb --- /dev/null +++ b/packages/react-server/examples/og/vite-plugins.ts @@ -0,0 +1,204 @@ +import fs from "node:fs"; +import path from "node:path"; +import MagicString from "magic-string"; +import { type ConfigEnv, type Plugin } from "vite"; + +// TODO: package? + +// input +// import wasm from "xxx.wasm?module" +// +// output (fs / dev) +// ?? +// +// output (fs / build) +// ?? +// +// output (import / build) +// ?? +// +export function wasmModulePlugin(options: { mode: "fs" | "import" }): Plugin { + const MARKER = "\0virtual:wasm-module"; + let env: ConfigEnv; + + return { + name: wasmModulePlugin.name, + config(_, env_) { + env = env_; + }, + resolveId: { + order: "pre", + async handler(source, importer, options) { + if (source.endsWith(".wasm?module")) { + const resolved = await this.resolve( + source.slice(0, -"?module".length), + importer, + options, + ); + if (resolved) { + return { id: MARKER + resolved.id }; + } + } + }, + }, + load(id) { + if (id.startsWith(MARKER)) { + const file = id.slice(MARKER.length); + + // readFile + instantiate wasm module + if (options.mode === "fs") { + let source: string; + if (env.command === "serve") { + source = JSON.stringify(file); + } else { + const referenceId = this.emitFile({ + type: "asset", + name: path.basename(file), + source: fs.readFileSync(file), + }); + source = `fileURLToPath(import.meta.ROLLUP_FILE_URL_${referenceId})`; + } + return ` + import fs from "node:fs"; + import { fileURLToPath } from "node:url"; + const buffer = fs.readFileSync(${source}); + export default new WebAssembly.Module(buffer); + `; + } + + // emit wasm asset + rewrite import + if (options.mode === "import") { + if (env.command === "serve") { + throw new Error("unsupported"); + } + const referenceId = this.emitFile({ + type: "asset", + name: path.basename(file), + source: fs.readFileSync(file), + }); + // temporary placeholder which we replace during `renderChunk` + return `export default "__WASM_MODULE_IMPORT_${referenceId}"`; + } + } + }, + renderChunk(code, chunk) { + const matches = code.matchAll(/"__WASM_MODULE_IMPORT_(\w+)"/dg); + const output = new MagicString(code); + for (const match of matches) { + const referenceId = match[1]; + const assetFileName = this.getFileName(referenceId); + const importSource = + "./" + + path.relative( + path.resolve(chunk.fileName, ".."), + path.resolve(assetFileName), + ); + const importName = `__wasm_${referenceId}`; + const [start, end] = match.indices![0]; + output.prepend(`import ${importName} from "${importSource}";\n`); + output.update(start, end, importName); + } + if (output.hasChanged()) { + return output.toString(); + } + }, + }; +} + +// +// input +// fetch(new URL("./xxx", import.meta.url)) +// +// output (fs / dev) +// new Response(fs.readFileSync("/absolute-path-to/xxx") +// +// output (fs / build) +// import("node:fs").then(fs => new Response(fs.readFileSync(new URL("./relocated-xxx", import.meta.url).href))) +// +// output (import / build) +// import("./relocated-xxx.bin").then(mod => new Response(mod.default)) +// +export function fetchImportMetaUrlPlugin(options: { + mode: "fs" | "import"; +}): Plugin { + // cf. https://github.com/vitejs/vite/blob/0f56e1724162df76fffd5508148db118767ebe32/packages/vite/src/node/plugins/assetImportMetaUrl.ts#L51-L52 + const FETCH_ASSET_RE = + /\bfetch\s*\(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)\s*(?:,\s*)?\)/dg; + + let env: ConfigEnv; + + return { + name: fetchImportMetaUrlPlugin.name, + config(_, env_) { + env = env_; + }, + transform(code, id) { + if (code.includes("import.meta.url")) { + const output = new MagicString(code); + const matches = code.matchAll(FETCH_ASSET_RE); + for (const match of matches) { + const urlArg = match[1]!.slice(1, -1); + const absFile = path.resolve(id, "..", urlArg); + if (fs.existsSync(absFile)) { + let replacement!: string; + if (options.mode === "fs") { + if (env.command === "serve") { + replacement = `((async () => { + const fs = await import("node:fs"); + return new Response(fs.readFileSync(${JSON.stringify(absFile)})); + })())`; + } else { + const referenceId = this.emitFile({ + type: "asset", + name: path.basename(absFile), + source: fs.readFileSync(absFile), + }); + replacement = `((async () => { + const fs = await import("node:fs"); + const { fileURLToPath } = await import("node:url"); + return new Response(fs.readFileSync(fileURLToPath(import.meta.ROLLUP_FILE_URL_${referenceId}))); + })())`; + } + } + if (options.mode === "import") { + if (env.command === "serve") { + throw new Error("unsupported"); + } + const referenceId = this.emitFile({ + type: "asset", + name: path.basename(absFile) + ".bin", + source: fs.readFileSync(absFile), + }); + replacement = `"__FETCH_ASSET_IMPORT_${referenceId}".then(mod => new Response(mod.default))`; + } + const [start, end] = match.indices![0]!; + output.update(start, end, replacement); + } + } + if (output.hasChanged()) { + return { code: output.toString(), map: output.generateMap() }; + } + } + }, + renderChunk(code, chunk) { + const matches = code.matchAll(/"__FETCH_ASSET_IMPORT_(\w+)"/dg); + const output = new MagicString(code); + for (const match of matches) { + const referenceId = match[1]; + const assetFileName = this.getFileName(referenceId); + const importSource = + "./" + + path.relative( + path.resolve(chunk.fileName, ".."), + path.resolve(assetFileName), + ); + const [start, end] = match.indices![0]; + const replacement = `import(${JSON.stringify(importSource)})`; + output.update(start, end, replacement); + } + if (output.hasChanged()) { + return output.toString(); + } + }, + }; +} diff --git a/packages/react-server/examples/og/vite.config.ts b/packages/react-server/examples/og/vite.config.ts index 23654dde6..348091247 100644 --- a/packages/react-server/examples/og/vite.config.ts +++ b/packages/react-server/examples/og/vite.config.ts @@ -1,8 +1,6 @@ -import fs from "node:fs"; -import path from "node:path"; -import MagicString from "magic-string"; import next from "next/vite"; -import { type ConfigEnv, type Plugin, defineConfig } from "vite"; +import { defineConfig } from "vite"; +import { fetchImportMetaUrlPlugin, wasmModulePlugin } from "./vite-plugins"; export default defineConfig({ plugins: [ @@ -28,201 +26,3 @@ export default defineConfig({ }), ], }); - -// cf. https://github.com/hi-ogawa/vite-plugins/blob/998561660c8a27e067e976d4ede9dca53984d41a/packages/react-server/examples/basic/vite-plugin-wasm-module.ts -// -// input -// import wasm from "xxx.wasm?module" -// -// output (fs / dev) -// ?? -// -// output (fs / build) -// ?? -// -// output (import / build) -// ?? -// -function wasmModulePlugin(options: { mode: "fs" | "import" }): Plugin { - const MARKER = "\0:virtual:wasm-module"; - let env: ConfigEnv; - - return { - name: wasmModulePlugin.name, - config(_, env_) { - env = env_; - }, - resolveId: { - order: "pre", - async handler(source, importer, options) { - if (source.endsWith(".wasm?module")) { - const resolved = await this.resolve( - source.slice(0, -"?module".length), - importer, - options, - ); - if (resolved) { - return { id: MARKER + resolved.id }; - } - } - }, - }, - load(id) { - if (id.startsWith(MARKER)) { - const file = id.slice(MARKER.length); - - // readFile + instantiate wasm module - if (options.mode === "fs") { - let source: string; - if (env.command === "serve") { - source = JSON.stringify(file); - } else { - const referenceId = this.emitFile({ - type: "asset", - name: path.basename(file), - source: fs.readFileSync(file), - }); - source = `fileURLToPath(import.meta.ROLLUP_FILE_URL_${referenceId})`; - } - return ` - import fs from "node:fs"; - import { fileURLToPath } from "node:url"; - const buffer = fs.readFileSync(${source}); - export default new WebAssembly.Module(buffer); - `; - } - - // emit wasm asset + rewrite import - if (options.mode === "import") { - if (env.command === "serve") { - throw new Error("unsupported"); - } - const referenceId = this.emitFile({ - type: "asset", - name: path.basename(file), - source: fs.readFileSync(file), - }); - // temporary placeholder which we replace during `renderChunk` - return `export default "__WASM_MODULE_IMPORT_${referenceId}"`; - } - } - }, - renderChunk(code, chunk) { - const matches = code.matchAll(/"__WASM_MODULE_IMPORT_(\w+)"/dg); - const output = new MagicString(code); - for (const match of matches) { - const referenceId = match[1]; - const assetFileName = this.getFileName(referenceId); - const importSource = - "./" + - path.relative( - path.resolve(chunk.fileName, ".."), - path.resolve(assetFileName), - ); - const importName = `__wasm_${referenceId}`; - const [start, end] = match.indices![0]; - output.prepend(`import ${importName} from "${importSource}";\n`); - output.update(start, end, importName); - } - if (output.hasChanged()) { - return output.toString(); - } - }, - }; -} - -// -// input -// fetch(new URL("./xxx", import.meta.url)) -// -// output (fs / dev) -// new Response(fs.readFileSync("/absolute-path-to/xxx") -// -// output (fs / build) -// import("node:fs").then(fs => new Response(fs.readFileSync(new URL("./relocated-xxx", import.meta.url).href))) -// -// output (import / build) -// import("./relocated-xxx.bin").then(mod => new Response(mod.default)) -// -function fetchImportMetaUrlPlugin(options: { mode: "fs" | "import" }): Plugin { - // cf. https://github.com/vitejs/vite/blob/0f56e1724162df76fffd5508148db118767ebe32/packages/vite/src/node/plugins/assetImportMetaUrl.ts#L51-L52 - const FETCH_ASSET_RE = - /\bfetch\s*\(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)\s*(?:,\s*)?\)/dg; - - let env: ConfigEnv; - - return { - name: fetchImportMetaUrlPlugin.name, - config(_, env_) { - env = env_; - }, - transform(code, id) { - if (code.includes("import.meta.url")) { - const output = new MagicString(code); - const matches = code.matchAll(FETCH_ASSET_RE); - for (const match of matches) { - const urlArg = match[1]!.slice(1, -1); - const absFile = path.resolve(id, "..", urlArg); - if (fs.existsSync(absFile)) { - let replacement!: string; - if (options.mode === "fs") { - if (env.command === "serve") { - replacement = `((async () => { - const fs = await import("node:fs"); - return new Response(fs.readFileSync(${JSON.stringify(absFile)})); - })())`; - } else { - const referenceId = this.emitFile({ - type: "asset", - name: path.basename(absFile), - source: fs.readFileSync(absFile), - }); - replacement = `((async () => { - const fs = await import("node:fs"); - const { fileURLToPath } = await import("node:url"); - return new Response(fs.readFileSync(fileURLToPath(import.meta.ROLLUP_FILE_URL_${referenceId}))); - })())`; - } - } - if (options.mode === "import") { - if (env.command === "serve") { - throw new Error("unsupported"); - } - const referenceId = this.emitFile({ - type: "asset", - name: path.basename(absFile) + ".bin", - source: fs.readFileSync(absFile), - }); - replacement = `"__FETCH_ASSET_IMPORT_${referenceId}".then(mod => new Response(mod.default))`; - } - const [start, end] = match.indices![0]!; - output.update(start, end, replacement); - } - } - if (output.hasChanged()) { - return { code: output.toString(), map: output.generateMap() }; - } - } - }, - renderChunk(code, chunk) { - const matches = code.matchAll(/"__FETCH_ASSET_IMPORT_(\w+)"/dg); - const output = new MagicString(code); - for (const match of matches) { - const referenceId = match[1]; - const assetFileName = this.getFileName(referenceId); - const importSource = - "./" + - path.relative( - path.resolve(chunk.fileName, ".."), - path.resolve(assetFileName), - ); - const [start, end] = match.indices![0]; - const replacement = `import(${JSON.stringify(importSource)})`; - output.update(start, end, replacement); - } - if (output.hasChanged()) { - return output.toString(); - } - }, - }; -} From 6ff95b85e50a3f091d56f9dbdb3d2d199c9c6ef7 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 15:18:59 +0900 Subject: [PATCH 14/18] refactor: move code --- .../basic/src/routes/test/wasm/page.tsx | 2 +- .../examples/basic/vite-plugin-wasm-module.ts | 92 +++++++++------- .../examples/basic/vite.config.ts | 5 +- .../react-server/examples/og/vite-plugins.ts | 102 ------------------ .../react-server/examples/og/vite.config.ts | 3 +- 5 files changed, 59 insertions(+), 145 deletions(-) diff --git a/packages/react-server/examples/basic/src/routes/test/wasm/page.tsx b/packages/react-server/examples/basic/src/routes/test/wasm/page.tsx index f4950a012..135083645 100644 --- a/packages/react-server/examples/basic/src/routes/test/wasm/page.tsx +++ b/packages/react-server/examples/basic/src/routes/test/wasm/page.tsx @@ -9,7 +9,7 @@ const getHighlither = once(async () => { langs: [js], // non js extension file is not externalized, so we can transform this via `load` hook // https://github.com/vitejs/vite/blob/fcf50c2e881356ea0d725cc563722712a2bf5695/packages/vite/src/node/plugins/resolve.ts#L810-L818 - loadWasm: import("shiki/onig.wasm" as string), + loadWasm: import("shiki/onig.wasm?module" as string), }); }); diff --git a/packages/react-server/examples/basic/vite-plugin-wasm-module.ts b/packages/react-server/examples/basic/vite-plugin-wasm-module.ts index ef6ed9136..8b156e611 100644 --- a/packages/react-server/examples/basic/vite-plugin-wasm-module.ts +++ b/packages/react-server/examples/basic/vite-plugin-wasm-module.ts @@ -3,46 +3,63 @@ import path from "node:path"; import MagicString from "magic-string"; import { type ConfigEnv, type Plugin } from "vite"; -// normalize wasm import for various environments -// - CF (including Vercel Edge): keep import "xxx.wasm" -// - Others: replace it with explicit instantiation of `WebAssembly.Module` -// - inline -// - local asset + fs.readFile - -// references +// cf. // https://developers.cloudflare.com/pages/functions/module-support/#webassembly-modules // https://github.com/withastro/adapters/blob/cd4c0842aadc58defc67f4ccf6d6ef6f0401a9ac/packages/cloudflare/src/utils/cloudflare-module-loader.ts#L213-L216 // https://vercel.com/docs/functions/wasm // https://github.com/unjs/unwasm -export function wasmModulePlugin(options: { - mode: "inline" | "asset-fs" | "asset-import"; -}): Plugin { +// +// input +// import wasm from "xxx.wasm?module" +// +// output (fs / dev) +// ?? +// +// output (fs / build) +// ?? +// +// output (import / build) +// ?? +// +export function wasmModulePlugin(options: { mode: "fs" | "import" }): Plugin { + const MARKER = "\0virtual:wasm-module"; let env: ConfigEnv; + return { name: wasmModulePlugin.name, - config(_config, env_) { + config(_, env_) { env = env_; }, - load(id) { - if (id.endsWith(".wasm")) { - // inline + WebAssembly.Module - if (options.mode === "inline") { - const base64 = fs.readFileSync(id).toString("base64"); - return ` - const buffer = Uint8Array.from(atob("${base64}"), c => c.charCodeAt(0)); - export default new WebAssembly.Module(buffer); - `; + resolveId: { + order: "pre", + async handler(source, importer, options) { + if (source.endsWith(".wasm?module")) { + const resolved = await this.resolve( + source.slice(0, -"?module".length), + importer, + options, + ); + if (resolved) { + return { id: MARKER + resolved.id }; + } } + }, + }, + load(id) { + if (id.startsWith(MARKER)) { + const file = id.slice(MARKER.length); - // file + WebAssembly.Module - if (options.mode === "asset-fs") { - let source = JSON.stringify(id); - if (env.command === "build") { + // readFile + new WebAssembly.Module + if (options.mode === "fs") { + let source: string; + if (env.command === "serve") { + source = JSON.stringify(file); + } else { const referenceId = this.emitFile({ type: "asset", - name: path.basename(id), - source: fs.readFileSync(id), + name: path.basename(file), + source: fs.readFileSync(file), }); source = `fileURLToPath(import.meta.ROLLUP_FILE_URL_${referenceId})`; } @@ -54,22 +71,23 @@ export function wasmModulePlugin(options: { `; } - // keep wasm import - if (options.mode === "asset-import") { - if (env.command === "build") { - const referenceId = this.emitFile({ - type: "asset", - name: path.basename(id), - source: fs.readFileSync(id), - }); - // temporary placeholder replaced during renderChunk - return `export default "__WASM_IMPORT_URL_${referenceId}"`; + // emit wasm asset + rewrite import + if (options.mode === "import") { + if (env.command === "serve") { + throw new Error("unsupported"); } + const referenceId = this.emitFile({ + type: "asset", + name: path.basename(file), + source: fs.readFileSync(file), + }); + // temporary placeholder which we replace during `renderChunk` + return `export default "__WASM_MODULE_IMPORT_${referenceId}"`; } } }, renderChunk(code, chunk) { - const matches = code.matchAll(/"__WASM_IMPORT_URL_(\w+)"/dg); + const matches = code.matchAll(/"__WASM_MODULE_IMPORT_(\w+)"/dg); const output = new MagicString(code); for (const match of matches) { const referenceId = match[1]; diff --git a/packages/react-server/examples/basic/vite.config.ts b/packages/react-server/examples/basic/vite.config.ts index 143e0ba07..3acf27fa3 100644 --- a/packages/react-server/examples/basic/vite.config.ts +++ b/packages/react-server/examples/basic/vite.config.ts @@ -31,10 +31,7 @@ export default defineConfig({ mdx(), testVitePluginVirtual(), wasmModulePlugin({ - mode: - process.env.VERCEL || process.env.CF_PAGES - ? "asset-import" - : "asset-fs", + mode: process.env.VERCEL || process.env.CF_PAGES ? "import" : "fs", }), { name: "cusotm-react-server-config", diff --git a/packages/react-server/examples/og/vite-plugins.ts b/packages/react-server/examples/og/vite-plugins.ts index 294b317eb..a83e0c54c 100644 --- a/packages/react-server/examples/og/vite-plugins.ts +++ b/packages/react-server/examples/og/vite-plugins.ts @@ -3,108 +3,6 @@ import path from "node:path"; import MagicString from "magic-string"; import { type ConfigEnv, type Plugin } from "vite"; -// TODO: package? - -// input -// import wasm from "xxx.wasm?module" -// -// output (fs / dev) -// ?? -// -// output (fs / build) -// ?? -// -// output (import / build) -// ?? -// -export function wasmModulePlugin(options: { mode: "fs" | "import" }): Plugin { - const MARKER = "\0virtual:wasm-module"; - let env: ConfigEnv; - - return { - name: wasmModulePlugin.name, - config(_, env_) { - env = env_; - }, - resolveId: { - order: "pre", - async handler(source, importer, options) { - if (source.endsWith(".wasm?module")) { - const resolved = await this.resolve( - source.slice(0, -"?module".length), - importer, - options, - ); - if (resolved) { - return { id: MARKER + resolved.id }; - } - } - }, - }, - load(id) { - if (id.startsWith(MARKER)) { - const file = id.slice(MARKER.length); - - // readFile + instantiate wasm module - if (options.mode === "fs") { - let source: string; - if (env.command === "serve") { - source = JSON.stringify(file); - } else { - const referenceId = this.emitFile({ - type: "asset", - name: path.basename(file), - source: fs.readFileSync(file), - }); - source = `fileURLToPath(import.meta.ROLLUP_FILE_URL_${referenceId})`; - } - return ` - import fs from "node:fs"; - import { fileURLToPath } from "node:url"; - const buffer = fs.readFileSync(${source}); - export default new WebAssembly.Module(buffer); - `; - } - - // emit wasm asset + rewrite import - if (options.mode === "import") { - if (env.command === "serve") { - throw new Error("unsupported"); - } - const referenceId = this.emitFile({ - type: "asset", - name: path.basename(file), - source: fs.readFileSync(file), - }); - // temporary placeholder which we replace during `renderChunk` - return `export default "__WASM_MODULE_IMPORT_${referenceId}"`; - } - } - }, - renderChunk(code, chunk) { - const matches = code.matchAll(/"__WASM_MODULE_IMPORT_(\w+)"/dg); - const output = new MagicString(code); - for (const match of matches) { - const referenceId = match[1]; - const assetFileName = this.getFileName(referenceId); - const importSource = - "./" + - path.relative( - path.resolve(chunk.fileName, ".."), - path.resolve(assetFileName), - ); - const importName = `__wasm_${referenceId}`; - const [start, end] = match.indices![0]; - output.prepend(`import ${importName} from "${importSource}";\n`); - output.update(start, end, importName); - } - if (output.hasChanged()) { - return output.toString(); - } - }, - }; -} - // // input // fetch(new URL("./xxx", import.meta.url)) diff --git a/packages/react-server/examples/og/vite.config.ts b/packages/react-server/examples/og/vite.config.ts index 348091247..cbb1ef7e4 100644 --- a/packages/react-server/examples/og/vite.config.ts +++ b/packages/react-server/examples/og/vite.config.ts @@ -1,6 +1,7 @@ import next from "next/vite"; import { defineConfig } from "vite"; -import { fetchImportMetaUrlPlugin, wasmModulePlugin } from "./vite-plugins"; +import { wasmModulePlugin } from "../basic/vite-plugin-wasm-module"; +import { fetchImportMetaUrlPlugin } from "./vite-plugins"; export default defineConfig({ plugins: [ From 98b2a723c321ec89067b214461bfb21bb8eb1181 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 15:19:55 +0900 Subject: [PATCH 15/18] ci: setup --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c26e2b640..deb171430 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,9 @@ jobs: - run: pnpm -C packages/react-server/examples/custom-out-dir cf-build - run: pnpm -C packages/react-server/examples/custom-out-dir test-e2e-cf-preview - run: pnpm -C packages/react-server/examples/custom-out-dir vc-build + - run: pnpm -C packages/react-server/examples/og build + - run: pnpm -C packages/react-server/examples/og cf-build + - run: pnpm -C packages/react-server/examples/og vc-build test-react-server-package: runs-on: ubuntu-latest From d7d3f63de39457e37901283cb4ebb6471bd2ef0d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 15:27:09 +0900 Subject: [PATCH 16/18] test: add e2e --- .github/workflows/ci.yml | 3 ++ .../examples/og/e2e/basic.test.ts | 10 ++++++ .../basic-1-chromium-linux.png | Bin 0 -> 3390 bytes .../react-server/examples/og/package.json | 5 ++- .../examples/og/playwright.config.ts | 34 ++++++++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 packages/react-server/examples/og/e2e/basic.test.ts create mode 100644 packages/react-server/examples/og/e2e/basic.test.ts-snapshots/basic-1-chromium-linux.png create mode 100644 packages/react-server/examples/og/playwright.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index deb171430..dec3c4c84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,8 +67,11 @@ jobs: - run: pnpm -C packages/react-server/examples/custom-out-dir cf-build - run: pnpm -C packages/react-server/examples/custom-out-dir test-e2e-cf-preview - run: pnpm -C packages/react-server/examples/custom-out-dir vc-build + - run: pnpm -C packages/react-server/examples/og test-e2e - run: pnpm -C packages/react-server/examples/og build + - run: pnpm -C packages/react-server/examples/og test-e2e-preview - run: pnpm -C packages/react-server/examples/og cf-build + - run: pnpm -C packages/react-server/examples/og test-e2e-cf-preview - run: pnpm -C packages/react-server/examples/og vc-build test-react-server-package: diff --git a/packages/react-server/examples/og/e2e/basic.test.ts b/packages/react-server/examples/og/e2e/basic.test.ts new file mode 100644 index 000000000..2285032b8 --- /dev/null +++ b/packages/react-server/examples/og/e2e/basic.test.ts @@ -0,0 +1,10 @@ +import assert from "node:assert"; +import { expect, test } from "@playwright/test"; + +test("basic", async ({ page }) => { + const res = await page.goto("/og?title=Hi"); + assert(res); + expect(res.status()).toBe(200); + expect(res?.headers()).toMatchObject({ "content-type": "image/png" }); + await expect(page).toHaveScreenshot(); +}); diff --git a/packages/react-server/examples/og/e2e/basic.test.ts-snapshots/basic-1-chromium-linux.png b/packages/react-server/examples/og/e2e/basic.test.ts-snapshots/basic-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..a6bbab8193cbc89543d351041e0120be69a88425 GIT binary patch literal 3390 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i0*Z)=h^hlA#^NA%Cx&(BWL`2b@ci&} zaSW-L^Y-q>tSPsp8Xit=ZthZ3Y+cZFY{_P!gFyzdSGYH9_@K0h=e^^EhPA$LST+i> z2r6FmTVo>NVUo3_Aj#tB0nKl1#Uk;>v!7J1(uw&0!B;r$+WrK!Wy_ZRV`Xr7e*du? z14F`o!3YKh!$wvH0pU@`Ur)Ru{P6Sh^Y8EP|Nrpta4WZX-klwW_V)K{nH&B) z1C4omxSjv|ySvrDzP!w+e12~3=jZ4D|9QN6^=fZ#@AvoiO6yLaHZ3hJ?cb*K+Szl3b@9*DV`}^C`Zt=xWudk1snK_Rs(J-B#YdE+#fsc5YGapC2Fp{{DV{U#)P&fq8`|WpA{rsjEMK_ROuG zpRHL~-ET)5>l;;|na<1o=I*Nd`|Hh{Hwgw2VDMvUb{xb0liE%R@>VrJK5WgtzAk3x zrro=(C3yPeY^@9p4c{$nV0KAh{CzpWV8*<8a^FP`Fg_JkWNp$O4cSFP8CwyWI4n7Ht3`baij5D%231{ZnR&U$5@#E*;@%w6a?%n%WU7hJi zEEBiH=H2D*|6O{2qvrRw-0bYv{(dYUcC&~Xq`iLq`mr3N!ZVObK&Kg`ZO^}NSN`tK z`S(;Fu=?n}v!uKe#jF$bQ_1 Date: Mon, 19 Aug 2024 15:34:29 +0900 Subject: [PATCH 17/18] chore: comment --- .../react-server/examples/basic/vite-plugin-wasm-module.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-server/examples/basic/vite-plugin-wasm-module.ts b/packages/react-server/examples/basic/vite-plugin-wasm-module.ts index 8b156e611..846fbbd15 100644 --- a/packages/react-server/examples/basic/vite-plugin-wasm-module.ts +++ b/packages/react-server/examples/basic/vite-plugin-wasm-module.ts @@ -14,13 +14,13 @@ import { type ConfigEnv, type Plugin } from "vite"; // import wasm from "xxx.wasm?module" // // output (fs / dev) -// ?? +// export default new WebAssembly.Module(fs.readFileSync("/absolute-path-to/xxx.wasm")) // // output (fs / build) -// ?? +// export default new WebAssembly.Module(fs.readFileSync(fileURLToPath(new URL("./relocated-xxx.wasm", import.meta.url).href))) // // output (import / build) -// ?? +// import wasm from "./relocated-xxx.wasm" // export function wasmModulePlugin(options: { mode: "fs" | "import" }): Plugin { const MARKER = "\0virtual:wasm-module"; From 47db39736c6b94d0cdd670fcde15db6aefb1342c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 19 Aug 2024 15:35:25 +0900 Subject: [PATCH 18/18] chore: comment --- .../react-server/examples/basic/src/routes/test/wasm/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-server/examples/basic/src/routes/test/wasm/page.tsx b/packages/react-server/examples/basic/src/routes/test/wasm/page.tsx index 135083645..96f6a5dec 100644 --- a/packages/react-server/examples/basic/src/routes/test/wasm/page.tsx +++ b/packages/react-server/examples/basic/src/routes/test/wasm/page.tsx @@ -7,8 +7,6 @@ const getHighlither = once(async () => { return createHighlighterCore({ themes: [nord], langs: [js], - // non js extension file is not externalized, so we can transform this via `load` hook - // https://github.com/vitejs/vite/blob/fcf50c2e881356ea0d725cc563722712a2bf5695/packages/vite/src/node/plugins/resolve.ts#L810-L818 loadWasm: import("shiki/onig.wasm?module" as string), }); });