diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md index ded7a8c54b217..a76834e0e1064 100644 --- a/packages/integrations/cloudflare/README.md +++ b/packages/integrations/cloudflare/README.md @@ -185,21 +185,35 @@ export default defineConfig({ ## WASM module imports -Cloudflare has native support for importing `.wasm` files [directly as ES modules](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration). You can import a web assembly module in astro with `.wasm?module` syntax. This is in order to differentiate from the built in `.wasm?url` and `.wasm?init` bindings that won't work with cloudflare. +`wasmModuleImports: boolean` -```typescript -import { type APIContext, type EndpointOutput } from 'astro'; +Cloudflare has native support for importing `.wasm` files [directly as ES modules](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration). To enable importing them as modules in the cloudflare build and astro dev server, add `wasmModuleImports: true` to your config. + +```diff +import {defineConfig} from "astro/config"; +import cloudflare from '@astrojs/cloudflare'; + +export default defineConfig({ + adapter: cloudflare({ ++ wasmModuleImports: true + }), + output: 'server' +}) +``` + +Once enabled, you can import a web assembly module in astro with a `.wasm?module` import + +```javascript +// pages/add/[a]/[b].js import mod from '../util/add.wasm?module'; +// instantiate ahead of time to share module const addModule: any = new WebAssembly.Instance(mod); -export async function GET(context: APIContext): Promise { - return new Response(JSON.stringify({ answer: addModule.exports.add(40, 2) }), { - status: 200, - headers: { - 'Content-Type': 'application/json', - }, - }); +export async function GET(context) { + const a = Number.parseInt(context.params.a!); + const b = Number.parseInt(context.params.b!); + return new Response(`${a} + ${b} = ${addModule.exports.add(a, b)}`); } ``` diff --git a/packages/integrations/cloudflare/src/env.d.ts b/packages/integrations/cloudflare/src/env.d.ts new file mode 100644 index 0000000000000..8c34fb45e7cf4 --- /dev/null +++ b/packages/integrations/cloudflare/src/env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index 9c4b9494d8144..190270d891099 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -27,6 +27,7 @@ type Options = { * 'remote': use a dynamic real-live req.cf object, and env vars defined in wrangler.toml & .dev.vars (astro dev is enough) */ runtime?: 'off' | 'local' | 'remote'; + wasmModuleImports?: boolean; }; interface BuildConfig { @@ -193,7 +194,7 @@ export default function createIntegration(args?: Options): AstroIntegration { }, vite: { // load .wasm files as WebAssembly modules - plugins: [wasmModuleLoader()], + plugins: [wasmModuleLoader(!args?.wasmModuleImports)], }, }); }, @@ -299,19 +300,20 @@ export default function createIntegration(args?: Options): AstroIntegration { const outputUrl = new URL('$astro', _buildConfig.server); const outputDir = fileURLToPath(outputUrl); // - // Sadly, this needs to build esbuild for each depth of routes/entrypoints independently so that relative - // import paths to the assets are correct. - // This is inefficient: - // - is there any ways to import from the root to keep these consistent? - // - if not, would be nice to not group like this if there's no wasm... Could we determine that ahead of time? - // - or perhaps wasm should be entirely opt-in? - const entryPathsGroupedByDepth = entryPaths.reduce((sum, thisPath) => { - const depthFromRoot = thisPath.split(sep).length; - sum.set(depthFromRoot, (sum.get(depthFromRoot) || []).concat(thisPath)); - return sum; - }, new Map()); - - for (const pathsGroup of entryPathsGroupedByDepth.values()) { + // Sadly, when wasmModuleImports is enabled, this needs to build esbuild for each depth of routes/entrypoints + // independently so that relative import paths to the assets are the correct depth of '../' traversals + // This is inefficient, so wasmModuleImports is opt-in + const entryPathsGroupedByDepth = !args.wasmModuleImports + ? [entryPaths] + : entryPaths + .reduce((sum, thisPath) => { + const depthFromRoot = thisPath.split(sep).length; + sum.set(depthFromRoot, (sum.get(depthFromRoot) || []).concat(thisPath)); + return sum; + }, new Map()) + .values(); + + for (const pathsGroup of entryPathsGroupedByDepth) { // for some reason this exports to "entry.pages" instead of "pages" on windows. // look up the pages with relative logic const pagesDirname = relative(fileURLToPath(_buildConfig.server), pathsGroup[0]).split( @@ -343,7 +345,7 @@ export default function createIntegration(args?: Options): AstroIntegration { logOverride: { 'ignored-bare-import': 'silent', }, - plugins: [rewriteWasmImportPath({ relativePathToAssets })], + plugins: !args?.wasmModuleImports ? [] : [rewriteWasmImportPath({ relativePathToAssets })], }); } @@ -406,13 +408,15 @@ export default function createIntegration(args?: Options): AstroIntegration { logOverride: { 'ignored-bare-import': 'silent', }, - plugins: [ - rewriteWasmImportPath({ - relativePathToAssets: isModeDirectory - ? relative(fileURLToPath(functionsUrl), fileURLToPath(assetsUrl)) - : relative(fileURLToPath(_buildConfig.client), fileURLToPath(assetsUrl)), - }), - ], + plugins: !args?.wasmModuleImports + ? [] + : [ + rewriteWasmImportPath({ + relativePathToAssets: isModeDirectory + ? relative(fileURLToPath(functionsUrl), fileURLToPath(assetsUrl)) + : relative(fileURLToPath(_buildConfig.client), fileURLToPath(assetsUrl)), + }), + ], }); // Rename to worker.js diff --git a/packages/integrations/cloudflare/src/wasm-module-loader.ts b/packages/integrations/cloudflare/src/wasm-module-loader.ts index 498637d92c439..e203b8c63dc29 100644 --- a/packages/integrations/cloudflare/src/wasm-module-loader.ts +++ b/packages/integrations/cloudflare/src/wasm-module-loader.ts @@ -6,9 +6,11 @@ import { type Plugin } from 'vite'; * Loads '*.wasm?module' imports as WebAssembly modules, which is the only way to load WASM in cloudflare workers. * Current proposal for WASM modules: https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration * Cloudflare worker WASM from javascript support: https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/ + * @param disabled - if true throws a helpful error message if wasm is encountered and wasm imports are not enabled, + * otherwise it will error obscurely in the esbuild and vite builds * @returns Vite plugin to load WASM tagged with '?module' as a WASM modules */ -export function wasmModuleLoader(): Plugin { +export function wasmModuleLoader(disabled: boolean): Plugin { const postfix = '.wasm?module'; let isDev = false; @@ -30,6 +32,11 @@ export function wasmModuleLoader(): Plugin { if (!id.endsWith(postfix)) { return; } + if (disabled) { + throw new Error( + `WASM module's cannot be loaded unless you add \`wasmModuleImports: true\` to your astro config.` + ); + } const filePath = id.slice(0, -1 * '?module'.length); if (isDev) { diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-directory/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/wasm-directory/astro.config.mjs index 34504fb0513dd..a30cd20861a67 100644 --- a/packages/integrations/cloudflare/test/fixtures/wasm-directory/astro.config.mjs +++ b/packages/integrations/cloudflare/test/fixtures/wasm-directory/astro.config.mjs @@ -3,7 +3,8 @@ import cloudflare from '@astrojs/cloudflare'; export default defineConfig({ adapter: cloudflare({ - mode: 'directory' + mode: 'directory', + wasmModuleImports: true }), output: 'server' }); diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/astro.config.mjs index 331bc3d4273c9..e3acfd188bb98 100644 --- a/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/astro.config.mjs +++ b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/astro.config.mjs @@ -4,7 +4,8 @@ import cloudflare from '@astrojs/cloudflare'; export default defineConfig({ adapter: cloudflare({ mode: 'directory', - functionPerRoute: true + functionPerRoute: true, + wasmModuleImports: true }), output: 'server' }); diff --git a/packages/integrations/cloudflare/test/fixtures/wasm/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/wasm/astro.config.mjs index 80aac8077d537..b5e68667ec5d4 100644 --- a/packages/integrations/cloudflare/test/fixtures/wasm/astro.config.mjs +++ b/packages/integrations/cloudflare/test/fixtures/wasm/astro.config.mjs @@ -2,6 +2,8 @@ import { defineConfig } from 'astro/config'; import cloudflare from '@astrojs/cloudflare'; export default defineConfig({ - adapter: cloudflare(), + adapter: cloudflare({ + wasmModuleImports: true + }), output: 'server' }); diff --git a/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/index.ts b/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts similarity index 70% rename from packages/integrations/cloudflare/test/fixtures/wasm/src/pages/index.ts rename to packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts index 2c9ff6d4470db..ddc4c5ce4fd95 100644 --- a/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/index.ts +++ b/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts @@ -1,6 +1,6 @@ import { type APIContext, type EndpointOutput } from 'astro'; // @ts-ignore -import mod from '../util/add.wasm?module'; +import mod from '../../../util/add.wasm?module'; const addModule: any = new WebAssembly.Instance(mod); @@ -8,8 +8,9 @@ const addModule: any = new WebAssembly.Instance(mod); export async function GET( context: APIContext ): Promise { - - return new Response(JSON.stringify({ answer: addModule.exports.add(40, 2) }), { + const a = Number.parseInt(context.params.a!); + const b = Number.parseInt(context.params.b!); + return new Response(JSON.stringify({ answer: addModule.exports.add(a, b) }), { status: 200, headers: { 'Content-Type': 'application/json', diff --git a/packages/integrations/cloudflare/test/test-utils.js b/packages/integrations/cloudflare/test/test-utils.js index 4dc96c1f99f45..d55e0f78ba575 100644 --- a/packages/integrations/cloudflare/test/test-utils.js +++ b/packages/integrations/cloudflare/test/test-utils.js @@ -28,15 +28,15 @@ export async function runCLI( basePath, { silent, - retryTimeout = 3, - timeoutMillis = 1000, - backoffFactor = 3, + maxAttempts = 3, + timeoutMillis = 2500, // really short because it often seems to just hang on the first try, but work subsequently, no matter the wait + backoffFactor = 2, // | - 2.5s -- 5s ---- 10s -> onTimeout onTimeout = (ex) => { - new Error(`Timed out starting the wrangler CLI after ${retryTimeout} tries.`, { cause: ex }); + new Error(`Timed out starting the wrangler CLI after ${maxAttempts} tries.`, { cause: ex }); }, } ) { - let triesRemaining = retryTimeout; + let triesRemaining = maxAttempts; let timeout = timeoutMillis; let cli; let lastErr; @@ -58,7 +58,7 @@ export async function runCLI( } async function tryRunCLI(basePath, { silent, timeout }) { - const port = await getNextOpenPort(lastPort + 1); + const port = await getNextOpenPort(lastPort); lastPort = port; const fixtureDir = fileURLToPath(new URL(`${basePath}`, import.meta.url)); diff --git a/packages/integrations/cloudflare/test/wasm.test.js b/packages/integrations/cloudflare/test/wasm.test.js index 332e00230188f..8cb5fd7186863 100644 --- a/packages/integrations/cloudflare/test/wasm.test.js +++ b/packages/integrations/cloudflare/test/wasm.test.js @@ -1,5 +1,7 @@ import { loadFixture, runCLI } from './test-utils.js'; import { expect } from 'chai'; +import cloudflare from '../dist/index.js'; + describe('Wasm import', () => { describe('in cloudflare workerd', () => { @@ -29,7 +31,7 @@ describe('Wasm import', () => { }); it('can render', async () => { - let res = await fetch(`http://127.0.0.1:${cli.port}/`); + let res = await fetch(`http://127.0.0.1:${cli.port}/add/40/2`); expect(res.status).to.equal(200); const json = await res.json(); expect(json).to.deep.equal({ answer: 42 }); @@ -44,7 +46,7 @@ describe('Wasm import', () => { fixture = await loadFixture({ root: './fixtures/wasm/', }); - devServer = await fixture.startDevServer(); + devServer = undefined; }); after(async () => { @@ -52,10 +54,26 @@ describe('Wasm import', () => { }); it('can serve wasm', async () => { - let res = await fetch(`http://localhost:${devServer.address.port}/`); + devServer = await fixture.startDevServer(); + let res = await fetch(`http://localhost:${devServer.address.port}/add/60/3`); expect(res.status).to.equal(200); const json = await res.json(); - expect(json).to.deep.equal({ answer: 42 }); + expect(json).to.deep.equal({ answer: 63 }); }); + + it('fails to build intelligently when wasm is disabled', async () => { + let ex; + try { + devServer = await fixture.build({ + adapter: cloudflare({ + wasmModuleImports: false + }), + }); + } catch (err) { + ex = err + } + expect(ex?.message).to.have.string('add `wasmModuleImports: true` to your astro config') + }); + }); });