Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(@astrojs/cloudflare): Add support for wasm module imports #8542

Merged
merged 5 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shy-cycles-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/cloudflare': minor
---

Add support for loading wasm modules in the cloudflare adapter
1 change: 1 addition & 0 deletions packages/astro/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ process.env.ASTRO_TELEMETRY_DISABLED = true;
* @typedef {Object} Fixture
* @property {typeof build} build
* @property {(url: string) => string} resolveUrl
* @property {(path: string) => Promise<boolean>} pathExists
* @property {(url: string, opts: Parameters<typeof fetch>[1]) => Promise<Response>} fetch
* @property {(path: string) => Promise<string>} readFile
* @property {(path: string, updater: (content: string) => string) => Promise<void>} writeFile
Expand Down
43 changes: 43 additions & 0 deletions packages/integrations/cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,49 @@ export default defineConfig({
});
```

## Wasm module imports

`wasmModuleImports: boolean`

default: `false`

Whether or not to import `.wasm` files [directly as ES modules](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration).

Add `wasmModuleImports: true` to `astro.config.mjs` to enable in both the Cloudflare build and the Astro dev server.

```diff
// astro.config.mjs
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.

The following is an example of importing a Wasm module that then responds to requests by adding the request's number parameters together.

```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) {
const a = Number.parseInt(context.params.a);
const b = Number.parseInt(context.params.b);
return new Response(`${addModule.exports.add(a, b)}`);
}
```

While this example is trivial, Wasm can be used to accelerate computationally intensive operations which do not involve significant I/O such as embedding an image processing library.

## 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.
Expand Down
4 changes: 2 additions & 2 deletions packages/integrations/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"dotenv": "^16.3.1",
"esbuild": "^0.19.2",
"find-up": "^6.3.0",
"tiny-glob": "^0.2.9"
"tiny-glob": "^0.2.9",
"vite": "^4.4.9"
},
"peerDependencies": {
"astro": "workspace:^3.1.2"
Expand All @@ -59,7 +60,6 @@
"astro-scripts": "workspace:*",
"chai": "^4.3.7",
"cheerio": "1.0.0-rc.12",
"kill-port": "^2.0.1",
"mocha": "^10.2.0",
"wrangler": "^3.5.1"
}
Expand Down
146 changes: 115 additions & 31 deletions packages/integrations/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import { AstroError } from 'astro/errors';
import esbuild from 'esbuild';
import * as fs from 'node:fs';
import * as os from 'node:os';
import { sep } from 'node:path';
import { basename, dirname, relative, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import glob from 'tiny-glob';
import { getEnvVars } from './parser.js';
import { wasmModuleLoader } from './wasm-module-loader.js';

export type { AdvancedRuntime } from './server.advanced.js';
export type { DirectoryRuntime } from './server.directory.js';
Expand All @@ -26,11 +27,13 @@ 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 {
server: URL;
client: URL;
assets: string;
serverEntry: string;
split?: boolean;
}
Expand Down Expand Up @@ -189,6 +192,15 @@ export default function createIntegration(args?: Options): AstroIntegration {
serverEntry: '_worker.mjs',
redirects: false,
},
vite: {
// load .wasm files as WebAssembly modules
plugins: [
wasmModuleLoader({
disabled: !args?.wasmModuleImports,
assetsDirectory: config.build.assets,
}),
],
},
});
},
'astro:config:done': ({ setAdapter, config }) => {
Expand Down Expand Up @@ -280,6 +292,7 @@ export default function createIntegration(args?: Options): AstroIntegration {
},
'astro:build:done': async ({ pages, routes, dir }) => {
const functionsUrl = new URL('functions/', _config.root);
const assetsUrl = new URL(_buildConfig.assets, _buildConfig.client);

if (isModeDirectory) {
await fs.promises.mkdir(functionsUrl, { recursive: true });
Expand All @@ -291,36 +304,71 @@ export default function createIntegration(args?: Options): AstroIntegration {
const entryPaths = entryPointsURL.map((entry) => fileURLToPath(entry));
const outputUrl = new URL('$astro', _buildConfig.server);
const outputDir = fileURLToPath(outputUrl);

await esbuild.build({
target: 'es2020',
platform: 'browser',
conditions: ['workerd', 'worker', 'browser'],
external: [
'node:assert',
'node:async_hooks',
'node:buffer',
'node:diagnostics_channel',
'node:events',
'node:path',
'node:process',
'node:stream',
'node:string_decoder',
'node:util',
],
entryPoints: entryPaths,
outdir: outputDir,
allowOverwrite: true,
format: 'esm',
bundle: true,
minify: _config.vite?.build?.minify !== false,
banner: {
js: SHIM,
},
logOverride: {
'ignored-bare-import': 'silent',
},
});
//
// 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 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
.reduce((sum, thisPath) => {
const depthFromRoot = thisPath.split(sep).length;
sum.set(depthFromRoot, (sum.get(depthFromRoot) || []).concat(thisPath));
return sum;
}, new Map<number, string[]>())
.values();

for (const pathsGroup of entryPathsGroupedByDepth) {
// 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];
const absolutePagesDirname = fileURLToPath(new URL(pagesDirname, _buildConfig.server));
const urlWithinFunctions = new URL(
relative(absolutePagesDirname, pathsGroup[0]),
functionsUrl
);
const relativePathToAssets = relative(
dirname(fileURLToPath(urlWithinFunctions)),
fileURLToPath(assetsUrl)
);
await esbuild.build({
target: 'es2020',
platform: 'browser',
conditions: ['workerd', 'worker', 'browser'],
external: [
'node:assert',
'node:async_hooks',
'node:buffer',
'node:diagnostics_channel',
'node:events',
'node:path',
'node:process',
'node:stream',
'node:string_decoder',
'node:util',
],
entryPoints: pathsGroup,
outbase: absolutePagesDirname,
outdir: outputDir,
allowOverwrite: true,
format: 'esm',
bundle: true,
minify: _config.vite?.build?.minify !== false,
banner: {
js: SHIM,
},
logOverride: {
'ignored-bare-import': 'silent',
},
plugins: !args?.wasmModuleImports
? []
: [rewriteWasmImportPath({ relativePathToAssets })],
});
}

const outputFiles: Array<string> = await glob(`**/*`, {
cwd: outputDir,
Expand Down Expand Up @@ -393,6 +441,15 @@ export default function createIntegration(args?: Options): AstroIntegration {
logOverride: {
'ignored-bare-import': 'silent',
},
plugins: !args?.wasmModuleImports
? []
: [
rewriteWasmImportPath({
relativePathToAssets: isModeDirectory
? relative(fileURLToPath(functionsUrl), fileURLToPath(assetsUrl))
: relative(fileURLToPath(_buildConfig.client), fileURLToPath(assetsUrl)),
}),
],
});

// Rename to worker.js
Expand Down Expand Up @@ -602,3 +659,30 @@ function deduplicatePatterns(patterns: string[]) {
return true;
});
}

/**
*
* @param relativePathToAssets - relative path from the final location for the current esbuild output bundle, to the assets directory.
*/
function rewriteWasmImportPath({
relativePathToAssets,
}: {
relativePathToAssets: string;
}): esbuild.Plugin {
return {
name: 'wasm-loader',
setup(build) {
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
external: true, // mark it as external in the bundle
};
});
},
};
}
Loading