diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md
index 91537134bc7e..5ae5271cd08d 100644
--- a/packages/integrations/cloudflare/README.md
+++ b/packages/integrations/cloudflare/README.md
@@ -209,7 +209,7 @@ export default defineConfig({
})
```
-Once enabled, you can import a web assembly module in astro with a `.wasm?module` import
+Once enabled, you can import a web assembly module in astro with a `.wasm?module` import. The integration supports WASM module imports in both server and hybrid mode for a consistent development experience.
```javascript
// pages/add/[a]/[b].js
@@ -225,6 +225,8 @@ export async function GET(context) {
}
```
+
+
## Headers, Redirects and function invocation routes
Cloudflare has support for adding custom [headers](https://developers.cloudflare.com/pages/platform/headers/), configuring static [redirects](https://developers.cloudflare.com/pages/platform/redirects/) and defining which routes should [invoke functions](https://developers.cloudflare.com/pages/platform/functions/routing/#function-invocation-routes). Cloudflare looks for `_headers`, `_redirects`, and `_routes.json` files in your build output directory to configure these features. This means they should be placed in your Astro project’s `public/` directory.
diff --git a/packages/integrations/cloudflare/src/env.d.ts b/packages/integrations/cloudflare/src/env.d.ts
deleted file mode 100644
index 8c34fb45e7cf..000000000000
--- a/packages/integrations/cloudflare/src/env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
\ No newline at end of file
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
index 89a308da26c8..4ae43a110010 100644
--- a/packages/integrations/cloudflare/src/index.ts
+++ b/packages/integrations/cloudflare/src/index.ts
@@ -194,7 +194,12 @@ export default function createIntegration(args?: Options): AstroIntegration {
},
vite: {
// load .wasm files as WebAssembly modules
- plugins: [wasmModuleLoader(!args?.wasmModuleImports)],
+ plugins: [
+ wasmModuleLoader({
+ disabled: !args?.wasmModuleImports,
+ assetsDirectory: config.build.assets,
+ }),
+ ],
},
});
},
@@ -302,7 +307,9 @@ export default function createIntegration(args?: Options): AstroIntegration {
//
// 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
+ // This is inefficient, so wasmModuleImports is opt-in. This could potentially be improved in the future by
+ // taking advantage of the esbuild "onEnd" hook to rewrite import code per entry point relative to where the final
+ // destination of the entrypoint is
const entryPathsGroupedByDepth = !args.wasmModuleImports
? [entryPaths]
: entryPaths
@@ -314,8 +321,8 @@ export default function createIntegration(args?: Options): AstroIntegration {
.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
+ // for some reason this exports to "entry.pages" on windows instead of "pages" on unix environments.
+ // This deduces the name of the "pages" build directory
const pagesDirname = relative(fileURLToPath(_buildConfig.server), pathsGroup[0]).split(
sep
)[0];
@@ -357,7 +364,9 @@ export default function createIntegration(args?: Options): AstroIntegration {
logOverride: {
'ignored-bare-import': 'silent',
},
- plugins: !args?.wasmModuleImports ? [] : [rewriteWasmImportPath({ relativePathToAssets })],
+ plugins: !args?.wasmModuleImports
+ ? []
+ : [rewriteWasmImportPath({ relativePathToAssets })],
});
}
@@ -663,8 +672,11 @@ function rewriteWasmImportPath({
return {
name: 'wasm-loader',
setup(build) {
- build.onResolve({ filter: /.*\.wasm$/ }, (args) => {
- const updatedPath = [relativePathToAssets, basename(args.path)].join('/');
+ build.onResolve({ filter: /.*\.wasm.mjs$/ }, (args) => {
+ const updatedPath = [
+ relativePathToAssets.replaceAll('\\', '/'),
+ basename(args.path).replace(/\.mjs$/, ''),
+ ].join('/');
return {
path: updatedPath, // change the reference to the changed module
diff --git a/packages/integrations/cloudflare/src/wasm-module-loader.ts b/packages/integrations/cloudflare/src/wasm-module-loader.ts
index e203b8c63dc2..7d34d48c3df7 100644
--- a/packages/integrations/cloudflare/src/wasm-module-loader.ts
+++ b/packages/integrations/cloudflare/src/wasm-module-loader.ts
@@ -8,9 +8,16 @@ import { type Plugin } from 'vite';
* 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
+ * @param assetsDirectory - the folder name for the assets directory in the build directory. Usually '_astro'
* @returns Vite plugin to load WASM tagged with '?module' as a WASM modules
*/
-export function wasmModuleLoader(disabled: boolean): Plugin {
+export function wasmModuleLoader({
+ disabled,
+ assetsDirectory,
+}: {
+ disabled: boolean;
+ assetsDirectory: string;
+}): Plugin {
const postfix = '.wasm?module';
let isDev = false;
@@ -24,7 +31,7 @@ export function wasmModuleLoader(disabled: boolean): Plugin {
// let vite know that file format and the magic import string is intentional, and will be handled in this plugin
return {
assetsInclude: ['**/*.wasm?module'],
- build: { rollupOptions: { external: /^__WASM_ASSET__.+\.wasm$/i } },
+ build: { rollupOptions: { external: /^__WASM_ASSET__.+\.wasm\.mjs$/i } },
};
},
@@ -39,26 +46,43 @@ export function wasmModuleLoader(disabled: boolean): Plugin {
}
const filePath = id.slice(0, -1 * '?module'.length);
+
+ const data = fs.readFileSync(filePath);
+ const base64 = data.toString('base64');
+
+ const base64Module = `
+const wasmModule = new WebAssembly.Module(Uint8Array.from(atob("${base64}"), c => c.charCodeAt(0)));
+export default wasmModule
+`;
if (isDev) {
- // when running in vite serve, do the file system reading dance
- return `
- import fs from "node:fs"
- const wasmModule = new WebAssembly.Module(fs.readFileSync("${filePath}"));
- export default wasmModule;
- `;
+ // no need to wire up the assets in dev mode, just rewrite
+ return base64Module;
} else {
- // build to just a re-export of the original asset contents
- const assetId = this.emitFile({
+ // just some shared ID
+ let hash = hashString(base64);
+ // emit the wasm binary as an asset file, to be picked up later by the esbuild bundle for the worker.
+ // give it a shared deterministic name to make things easy for esbuild to switch on later
+ const assetName = path.basename(filePath).split('.')[0] + '.' + hash + '.wasm';
+ this.emitFile({
type: 'asset',
- name: path.basename(filePath),
+ // put it explicitly in the _astro assets directory with `fileName` rather than `name` so that
+ // vite doesn't give it a random id in its name. We need to be able to easily rewrite from
+ // the .mjs loader and the actual wasm asset later in the ESbuild for the worker
+ fileName: path.join(assetsDirectory, assetName),
source: fs.readFileSync(filePath),
});
- // import from magic asset string to be replaced later
+ // however, by default, the SSG generator cannot import the .wasm as a module, so embed as a base64 string
+ const chunkId = this.emitFile({
+ type: 'prebuilt-chunk',
+ fileName: assetName + '.mjs',
+ code: base64Module,
+ });
+
return `
- import init from "__WASM_ASSET__${assetId}.wasm"
- export default init
- `;
+import wasmModule from "__WASM_ASSET__${chunkId}.wasm.mjs";
+export default wasmModule;
+ `;
}
},
@@ -66,11 +90,13 @@ export function wasmModuleLoader(disabled: boolean): Plugin {
renderChunk(code, chunk, _) {
if (isDev) return;
- if (!/__WASM_ASSET__([a-z\d]+)\.wasm/g.test(code)) return;
+ if (!/__WASM_ASSET__/g.test(code)) return;
- const final = code.replaceAll(/__WASM_ASSET__([a-z\d]+)\.wasm/g, (s, assetId) => {
+ const final = code.replaceAll(/__WASM_ASSET__([a-z\d]+).wasm.mjs/g, (s, assetId) => {
const fileName = this.getFileName(assetId);
- const relativePath = path.relative(path.dirname(chunk.fileName), fileName);
+ const relativePath = path
+ .relative(path.dirname(chunk.fileName), fileName)
+ .replaceAll('\\', '/'); // fix windows paths for import
return `./${relativePath}`;
});
@@ -78,3 +104,16 @@ export function wasmModuleLoader(disabled: boolean): Plugin {
},
};
}
+
+/**
+ * Returns a deterministic 32 bit hash code from a string
+ */
+function hashString(str: string): string {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash &= hash; // Convert to 32bit integer
+ }
+ return new Uint32Array([hash])[0].toString(36);
+}
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 e3acfd188bb9..7f741d884a1d 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
@@ -7,5 +7,6 @@ export default defineConfig({
functionPerRoute: true,
wasmModuleImports: true
}),
- output: 'server'
+ output: 'server',
+ vite: { build: { minify: false } }
});
diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/deeply/nested/route.ts b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/deeply/nested/route.ts
index c80b6dbca9b8..20797c0c6423 100644
--- a/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/deeply/nested/route.ts
+++ b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/deeply/nested/route.ts
@@ -1,15 +1,11 @@
import { type APIContext, type EndpointOutput } from 'astro';
-// @ts-ignore
-import mod from '../../../util/add.wasm?module';
-
-const addModule: any = new WebAssembly.Instance(mod);
-
+import { add } from '../../../util/add';
export async function GET(
context: APIContext
): Promise {
- return new Response(JSON.stringify({ answer: addModule.exports.add(80, 4) }), {
+ return new Response(JSON.stringify({ answer: add(80, 4) }), {
status: 200,
headers: {
'Content-Type': 'application/json',
diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/index.ts b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/index.ts
index 2c9ff6d4470d..b6417dde999e 100644
--- a/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/index.ts
+++ b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/index.ts
@@ -1,15 +1,11 @@
import { type APIContext, type EndpointOutput } from 'astro';
-// @ts-ignore
-import mod from '../util/add.wasm?module';
-
-const addModule: any = new WebAssembly.Instance(mod);
-
+import { add } from '../util/add';
export async function GET(
context: APIContext
): Promise {
- return new Response(JSON.stringify({ answer: addModule.exports.add(40, 2) }), {
+ return new Response(JSON.stringify({ answer: add(40, 2) }), {
status: 200,
headers: {
'Content-Type': 'application/json',
diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/add.ts b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/add.ts
new file mode 100644
index 000000000000..ee336277b761
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/add.ts
@@ -0,0 +1,6 @@
+// extra layer of indirection to stress the esbuild
+import { addImpl } from "./indirection";
+
+export function add(a: number, b: number): number {
+ return addImpl(a, b);
+}
diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/indirection.ts b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/indirection.ts
new file mode 100644
index 000000000000..6fbb04c4949d
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/indirection.ts
@@ -0,0 +1,9 @@
+// extra layer of indirection to stress the esbuild
+// @ts-ignore
+import mod from './add.wasm?module';
+
+const addModule: any = new WebAssembly.Instance(mod);
+
+export function addImpl(a: number, b: number): number {
+ return addModule.exports.add(a, b);
+}
diff --git a/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts b/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts
index ddc4c5ce4fd9..130b2b2a4b7b 100644
--- a/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts
+++ b/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts
@@ -4,6 +4,7 @@ import mod from '../../../util/add.wasm?module';
const addModule: any = new WebAssembly.Instance(mod);
+export const prerender = false;
export async function GET(
context: APIContext
diff --git a/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/hybrid.ts b/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/hybrid.ts
new file mode 100644
index 000000000000..7bb470dffd8e
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/hybrid.ts
@@ -0,0 +1,16 @@
+import { type APIContext, type EndpointOutput } from 'astro';
+// @ts-ignore
+import mod from '../util/add.wasm?module';
+
+const addModule: any = new WebAssembly.Instance(mod);
+
+export async function GET(
+ context: APIContext
+): Promise {
+ return new Response(JSON.stringify({ answer: addModule.exports.add(20, 1) }), {
+ 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 d55e0f78ba57..50226c0c1e47 100644
--- a/packages/integrations/cloudflare/test/test-utils.js
+++ b/packages/integrations/cloudflare/test/test-utils.js
@@ -41,13 +41,13 @@ export async function runCLI(
let cli;
let lastErr;
while (triesRemaining > 0) {
- cli = await tryRunCLI(basePath, { silent, timeout });
+ cli = await tryRunCLI(basePath, { silent, timeout, forceRotatePort: triesRemaining !== maxAttempts });
try {
await cli.ready;
return cli;
} catch (err) {
lastErr = err;
- console.error(err.message + ' after ' + timeout + 'ms');
+ console.error((err.message || err.name || err) + ' after ' + timeout + 'ms');
cli.stop();
triesRemaining -= 1;
timeout *= backoffFactor;
@@ -57,8 +57,8 @@ export async function runCLI(
return cli;
}
-async function tryRunCLI(basePath, { silent, timeout }) {
- const port = await getNextOpenPort(lastPort);
+async function tryRunCLI(basePath, { silent, timeout, forceRotatePort = false }) {
+ const port = await getNextOpenPort(lastPort + (forceRotatePort ? 1 : 0));
lastPort = port;
const fixtureDir = fileURLToPath(new URL(`${basePath}`, import.meta.url));
@@ -148,7 +148,7 @@ const isPortOpen = async (port) => {
resolve(true);
s.close();
});
- s.listen(port);
+ s.listen(port, "0.0.0.0");
});
};
diff --git a/packages/integrations/cloudflare/test/wasm.test.js b/packages/integrations/cloudflare/test/wasm.test.js
index 8cb5fd718686..279a00cd1bac 100644
--- a/packages/integrations/cloudflare/test/wasm.test.js
+++ b/packages/integrations/cloudflare/test/wasm.test.js
@@ -2,7 +2,6 @@ import { loadFixture, runCLI } from './test-utils.js';
import { expect } from 'chai';
import cloudflare from '../dist/index.js';
-
describe('Wasm import', () => {
describe('in cloudflare workerd', () => {
/** @type {import('./test-utils.js').Fixture} */
@@ -64,16 +63,23 @@ describe('Wasm import', () => {
it('fails to build intelligently when wasm is disabled', async () => {
let ex;
try {
- devServer = await fixture.build({
+ await fixture.build({
adapter: cloudflare({
- wasmModuleImports: false
+ wasmModuleImports: false,
}),
});
} catch (err) {
- ex = err
+ ex = err;
}
- expect(ex?.message).to.have.string('add `wasmModuleImports: true` to your astro config')
+ expect(ex?.message).to.have.string('add `wasmModuleImports: true` to your astro config');
});
+ it('can import wasm in both SSR and SSG pages', async () => {
+ await fixture.build({ output: 'hybrid' });
+ const staticContents = await fixture.readFile('./hybrid');
+ expect(staticContents).to.be.equal('{"answer":21}');
+ const assets = await fixture.readdir('./_astro');
+ expect(assets.map((x) => x.slice(x.lastIndexOf('.')))).to.contain('.wasm');
+ });
});
});