feat(@astrojs/cloudflare): Add support for wasm module imports (#8542)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
b1310e6f13
commit
faeead4232
33 changed files with 787 additions and 122 deletions
5
.changeset/shy-cycles-obey.md
Normal file
5
.changeset/shy-cycles-obey.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/cloudflare': minor
|
||||
---
|
||||
|
||||
Add support for loading wasm modules in the cloudflare adapter
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 }) => {
|
||||
|
@ -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 });
|
||||
|
@ -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);
|
||||
//
|
||||
// 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();
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
119
packages/integrations/cloudflare/src/wasm-module-loader.ts
Normal file
119
packages/integrations/cloudflare/src/wasm-module-loader.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
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
|
||||
* @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,
|
||||
assetsDirectory,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
assetsDirectory: string;
|
||||
}): Plugin {
|
||||
const postfix = '.wasm?module';
|
||||
let isDev = false;
|
||||
|
||||
return {
|
||||
name: 'vite:wasm-module-loader',
|
||||
enforce: 'pre',
|
||||
configResolved(config) {
|
||||
isDev = config.command === 'serve';
|
||||
},
|
||||
config(_, __) {
|
||||
// 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\.mjs$/i } },
|
||||
};
|
||||
},
|
||||
|
||||
load(id, _) {
|
||||
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);
|
||||
|
||||
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) {
|
||||
// no need to wire up the assets in dev mode, just rewrite
|
||||
return base64Module;
|
||||
} else {
|
||||
// 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',
|
||||
// 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),
|
||||
});
|
||||
|
||||
// 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 wasmModule from "__WASM_ASSET__${chunkId}.wasm.mjs";
|
||||
export default wasmModule;
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
||||
// output original wasm file relative to the chunk
|
||||
renderChunk(code, chunk, _) {
|
||||
if (isDev) return;
|
||||
|
||||
if (!/__WASM_ASSET__/g.test(code)) return;
|
||||
|
||||
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)
|
||||
.replaceAll('\\', '/'); // fix windows paths for import
|
||||
return `./${relativePath}`;
|
||||
});
|
||||
|
||||
return { code: final };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
|
@ -14,20 +14,22 @@ describe('Basic app', () => {
|
|||
});
|
||||
await fixture.build();
|
||||
|
||||
cli = await runCLI('./fixtures/basics/', { silent: true, port: 8789 });
|
||||
await cli.ready.catch((e) => {
|
||||
console.log(e);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
cli = await runCLI('./fixtures/basics/', {
|
||||
silent: true,
|
||||
onTimeout: (ex) => {
|
||||
console.log(ex);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cli.stop();
|
||||
await cli?.stop();
|
||||
});
|
||||
|
||||
it('can render', async () => {
|
||||
let res = await fetch(`http://127.0.0.1:8789/`);
|
||||
let res = await fetch(`http://127.0.0.1:${cli.port}/`);
|
||||
expect(res.status).to.equal(200);
|
||||
let html = await res.text();
|
||||
let $ = cheerio.load(html);
|
||||
|
|
|
@ -17,20 +17,22 @@ describe('Wrangler Cloudflare Runtime', () => {
|
|||
});
|
||||
await fixture.build();
|
||||
|
||||
cli = await runCLI('./fixtures/cf/', { silent: true, port: 8786 });
|
||||
await cli.ready.catch((e) => {
|
||||
console.log(e);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
cli = await runCLI('./fixtures/cf/', {
|
||||
silent: true,
|
||||
onTimeout: (ex) => {
|
||||
console.log(ex);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cli.stop();
|
||||
await cli?.stop();
|
||||
});
|
||||
|
||||
it('Load cf and caches API', async () => {
|
||||
let res = await fetch(`http://127.0.0.1:8786/`);
|
||||
let res = await fetch(`http://127.0.0.1:${cli.port}/`);
|
||||
expect(res.status).to.equal(200);
|
||||
let html = await res.text();
|
||||
let $ = cheerio.load(html);
|
||||
|
@ -63,7 +65,7 @@ describe('Astro Cloudflare Runtime', () => {
|
|||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
await devServer?.stop();
|
||||
});
|
||||
|
||||
it('Populates CF, Vars & Bindings', async () => {
|
||||
|
|
|
@ -4,6 +4,7 @@ import cloudflare from '../dist/index.js';
|
|||
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
describe('mode: "directory"', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
|
|
10
packages/integrations/cloudflare/test/fixtures/wasm-directory/astro.config.mjs
vendored
Normal file
10
packages/integrations/cloudflare/test/fixtures/wasm-directory/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import cloudflare from '@astrojs/cloudflare';
|
||||
|
||||
export default defineConfig({
|
||||
adapter: cloudflare({
|
||||
mode: 'directory',
|
||||
wasmModuleImports: true
|
||||
}),
|
||||
output: 'server'
|
||||
});
|
9
packages/integrations/cloudflare/test/fixtures/wasm-directory/package.json
vendored
Normal file
9
packages/integrations/cloudflare/test/fixtures/wasm-directory/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/astro-cloudflare-wasm-function-per-route",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
18
packages/integrations/cloudflare/test/fixtures/wasm-directory/src/pages/index.ts
vendored
Normal file
18
packages/integrations/cloudflare/test/fixtures/wasm-directory/src/pages/index.ts
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
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<EndpointOutput | Response> {
|
||||
|
||||
return new Response(JSON.stringify({ answer: addModule.exports.add(40, 2) }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
BIN
packages/integrations/cloudflare/test/fixtures/wasm-directory/src/util/add.wasm
vendored
Normal file
BIN
packages/integrations/cloudflare/test/fixtures/wasm-directory/src/util/add.wasm
vendored
Normal file
Binary file not shown.
12
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/astro.config.mjs
vendored
Normal file
12
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import cloudflare from '@astrojs/cloudflare';
|
||||
|
||||
export default defineConfig({
|
||||
adapter: cloudflare({
|
||||
mode: 'directory',
|
||||
functionPerRoute: true,
|
||||
wasmModuleImports: true
|
||||
}),
|
||||
output: 'server',
|
||||
vite: { build: { minify: false } }
|
||||
});
|
9
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/package.json
vendored
Normal file
9
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/astro-cloudflare-wasm-directory",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { type APIContext, type EndpointOutput } from 'astro';
|
||||
import { add } from '../../../util/add';
|
||||
|
||||
export async function GET(
|
||||
context: APIContext
|
||||
): Promise<EndpointOutput | Response> {
|
||||
|
||||
return new Response(JSON.stringify({ answer: add(80, 4) }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
14
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/index.ts
vendored
Normal file
14
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/index.ts
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { type APIContext, type EndpointOutput } from 'astro';
|
||||
import { add } from '../util/add';
|
||||
|
||||
export async function GET(
|
||||
context: APIContext
|
||||
): Promise<EndpointOutput | Response> {
|
||||
|
||||
return new Response(JSON.stringify({ answer: add(40, 2) }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
6
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/add.ts
vendored
Normal file
6
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/add.ts
vendored
Normal file
|
@ -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);
|
||||
}
|
BIN
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/add.wasm
vendored
Normal file
BIN
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/add.wasm
vendored
Normal file
Binary file not shown.
|
@ -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);
|
||||
}
|
9
packages/integrations/cloudflare/test/fixtures/wasm/astro.config.mjs
vendored
Normal file
9
packages/integrations/cloudflare/test/fixtures/wasm/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import cloudflare from '@astrojs/cloudflare';
|
||||
|
||||
export default defineConfig({
|
||||
adapter: cloudflare({
|
||||
wasmModuleImports: true
|
||||
}),
|
||||
output: 'server'
|
||||
});
|
9
packages/integrations/cloudflare/test/fixtures/wasm/package.json
vendored
Normal file
9
packages/integrations/cloudflare/test/fixtures/wasm/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/astro-cloudflare-wasm",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
20
packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts
vendored
Normal file
20
packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { type APIContext, type EndpointOutput } from 'astro';
|
||||
// @ts-ignore
|
||||
import mod from '../../../util/add.wasm?module';
|
||||
|
||||
const addModule: any = new WebAssembly.Instance(mod);
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export async function GET(
|
||||
context: APIContext
|
||||
): Promise<EndpointOutput | Response> {
|
||||
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',
|
||||
},
|
||||
});
|
||||
}
|
16
packages/integrations/cloudflare/test/fixtures/wasm/src/pages/hybrid.ts
vendored
Normal file
16
packages/integrations/cloudflare/test/fixtures/wasm/src/pages/hybrid.ts
vendored
Normal file
|
@ -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<EndpointOutput | Response> {
|
||||
return new Response(JSON.stringify({ answer: addModule.exports.add(20, 1) }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
BIN
packages/integrations/cloudflare/test/fixtures/wasm/src/util/add.wasm
vendored
Normal file
BIN
packages/integrations/cloudflare/test/fixtures/wasm/src/util/add.wasm
vendored
Normal file
Binary file not shown.
|
@ -3,6 +3,7 @@ import { expect } from 'chai';
|
|||
|
||||
/** @type {import('./test-utils.js').Fixture} */
|
||||
describe('Cloudflare SSR functionPerRoute', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
|
@ -13,7 +14,7 @@ describe('Cloudflare SSR functionPerRoute', () => {
|
|||
});
|
||||
|
||||
after(() => {
|
||||
fixture.clean();
|
||||
fixture?.clean();
|
||||
});
|
||||
|
||||
it('generates functions folders inside the project root, and checks that each page is emitted by astro', async () => {
|
||||
|
|
|
@ -17,20 +17,22 @@ describe('Runtime Locals', () => {
|
|||
});
|
||||
await fixture.build();
|
||||
|
||||
cli = await runCLI('./fixtures/runtime/', { silent: true, port: 8793 });
|
||||
await cli.ready.catch((e) => {
|
||||
console.log(e);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
cli = await runCLI('./fixtures/runtime/', {
|
||||
silent: true,
|
||||
onTimeout: (ex) => {
|
||||
console.log(ex);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cli.stop();
|
||||
await cli?.stop();
|
||||
});
|
||||
|
||||
it('has CF and Caches', async () => {
|
||||
let res = await fetch(`http://127.0.0.1:8793/`);
|
||||
let res = await fetch(`http://127.0.0.1:${cli.port}/`);
|
||||
expect(res.status).to.equal(200);
|
||||
let html = await res.text();
|
||||
let $ = cheerio.load(html);
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import kill from 'kill-port';
|
||||
import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js';
|
||||
|
||||
import * as net from 'node:net';
|
||||
export { fixLineEndings } from '../../../astro/test/test-utils.js';
|
||||
|
||||
/**
|
||||
* @typedef {{ ready: Promise<void>, stop: Promise<void> }} WranglerCLI
|
||||
* @typedef {{ stop: Promise<void>, port: number }} WranglerCLI
|
||||
* @typedef {import('../../../astro/test/test-utils').Fixture} Fixture
|
||||
*/
|
||||
|
||||
|
@ -21,70 +19,147 @@ const wranglerPath = fileURLToPath(
|
|||
new URL('../node_modules/wrangler/bin/wrangler.js', import.meta.url)
|
||||
);
|
||||
|
||||
let lastPort = 8788;
|
||||
|
||||
/**
|
||||
* @returns {Promise<WranglerCLI>}
|
||||
*/
|
||||
export async function runCLI(basePath, { silent, port }) {
|
||||
// Hack: force existing process on port to be killed
|
||||
try {
|
||||
await kill(port, 'tcp');
|
||||
} catch {
|
||||
// Will throw if port is not in use, but that's fine
|
||||
export async function runCLI(
|
||||
basePath,
|
||||
{
|
||||
silent,
|
||||
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 ${maxAttempts} tries.`, { cause: ex });
|
||||
},
|
||||
}
|
||||
) {
|
||||
let triesRemaining = maxAttempts;
|
||||
let timeout = timeoutMillis;
|
||||
let cli;
|
||||
let lastErr;
|
||||
while (triesRemaining > 0) {
|
||||
cli = await tryRunCLI(basePath, { silent, timeout, forceRotatePort: triesRemaining !== maxAttempts });
|
||||
try {
|
||||
await cli.ready;
|
||||
return cli;
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
console.error((err.message || err.name || err) + ' after ' + timeout + 'ms');
|
||||
cli.stop();
|
||||
triesRemaining -= 1;
|
||||
timeout *= backoffFactor;
|
||||
}
|
||||
}
|
||||
onTimeout(lastErr);
|
||||
return cli;
|
||||
}
|
||||
|
||||
const script = fileURLToPath(new URL(`${basePath}/dist/_worker.js`, import.meta.url));
|
||||
const p = spawn('node', [
|
||||
wranglerPath,
|
||||
'dev',
|
||||
script,
|
||||
'--port',
|
||||
port,
|
||||
'--log-level',
|
||||
'info',
|
||||
'--persist-to',
|
||||
`${basePath}/.wrangler/state`,
|
||||
]);
|
||||
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));
|
||||
const p = spawn(
|
||||
'node',
|
||||
[
|
||||
wranglerPath,
|
||||
'pages',
|
||||
'dev',
|
||||
'dist',
|
||||
'--port',
|
||||
port,
|
||||
'--log-level',
|
||||
'info',
|
||||
'--persist-to',
|
||||
'.wrangler/state',
|
||||
],
|
||||
{
|
||||
cwd: fixtureDir,
|
||||
}
|
||||
);
|
||||
|
||||
p.stderr.setEncoding('utf-8');
|
||||
p.stdout.setEncoding('utf-8');
|
||||
|
||||
const timeout = 20_000;
|
||||
|
||||
const ready = new Promise(async (resolve, reject) => {
|
||||
const failed = setTimeout(() => {
|
||||
p.kill();
|
||||
p.kill('SIGKILL');
|
||||
reject(new Error(`Timed out starting the wrangler CLI`));
|
||||
}, timeout);
|
||||
|
||||
(async function () {
|
||||
for (const msg of p.stderr) {
|
||||
if (!silent) {
|
||||
console.error(msg);
|
||||
}
|
||||
}
|
||||
})();
|
||||
const success = () => {
|
||||
clearTimeout(failed);
|
||||
resolve();
|
||||
};
|
||||
|
||||
for await (const msg of p.stdout) {
|
||||
p.on('exit', (code) => reject(`wrangler terminated unexpectedly with exit code ${code}`));
|
||||
|
||||
p.stderr.on('data', (data) => {
|
||||
if (!silent) {
|
||||
console.log(msg);
|
||||
process.stdout.write(data);
|
||||
}
|
||||
if (msg.includes(`[mf:inf] Ready on`)) {
|
||||
break;
|
||||
});
|
||||
let allData = '';
|
||||
p.stdout.on('data', (data) => {
|
||||
if (!silent) {
|
||||
process.stdout.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(failed);
|
||||
resolve();
|
||||
allData += data;
|
||||
if (allData.includes(`[mf:inf] Ready on`)) {
|
||||
success();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
port,
|
||||
ready,
|
||||
stop() {
|
||||
return new Promise((resolve, reject) => {
|
||||
p.on('close', () => resolve());
|
||||
const timer = setTimeout(() => {
|
||||
p.kill('SIGKILL');
|
||||
}, 1000);
|
||||
p.on('close', () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
p.on('error', (err) => reject(err));
|
||||
p.kill();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const isPortOpen = async (port) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let s = net.createServer();
|
||||
s.once('error', (err) => {
|
||||
s.close();
|
||||
if (err['code'] == 'EADDRINUSE') {
|
||||
resolve(false);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
s.once('listening', () => {
|
||||
resolve(true);
|
||||
s.close();
|
||||
});
|
||||
s.listen(port, "0.0.0.0");
|
||||
});
|
||||
};
|
||||
|
||||
const getNextOpenPort = async (startFrom) => {
|
||||
let openPort = null;
|
||||
while (startFrom < 65535 || !!openPort) {
|
||||
if (await isPortOpen(startFrom)) {
|
||||
openPort = startFrom;
|
||||
break;
|
||||
}
|
||||
startFrom++;
|
||||
}
|
||||
return openPort;
|
||||
};
|
||||
|
|
36
packages/integrations/cloudflare/test/wasm-directory.test.js
Normal file
36
packages/integrations/cloudflare/test/wasm-directory.test.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { loadFixture, runCLI } from './test-utils.js';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('Wasm directory mode import', () => {
|
||||
/** @type {import('./test-utils.js').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils.js').WranglerCLI} */
|
||||
let cli;
|
||||
|
||||
before(async function () {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/wasm-directory/',
|
||||
});
|
||||
await fixture.build();
|
||||
|
||||
cli = await runCLI('./fixtures/wasm-directory/', {
|
||||
silent: true,
|
||||
onTimeout: (ex) => {
|
||||
console.log(ex);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cli?.stop();
|
||||
});
|
||||
|
||||
it('can render', async () => {
|
||||
let res = await fetch(`http://127.0.0.1:${cli.port}/`);
|
||||
expect(res.status).to.equal(200);
|
||||
const json = await res.json();
|
||||
expect(json).to.deep.equal({ answer: 42 });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
import { loadFixture, runCLI } from './test-utils.js';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('Wasm function per route import', () => {
|
||||
/** @type {import('./test-utils.js').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils.js').WranglerCLI} */
|
||||
let cli;
|
||||
|
||||
before(async function () {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/wasm-function-per-route/',
|
||||
});
|
||||
await fixture.build();
|
||||
|
||||
cli = await runCLI('./fixtures/wasm-function-per-route/', {
|
||||
silent: true,
|
||||
onTimeout: (ex) => {
|
||||
console.log(ex);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cli?.stop();
|
||||
});
|
||||
|
||||
it('can render', async () => {
|
||||
let res = await fetch(`http://127.0.0.1:${cli.port}/`);
|
||||
expect(res.status).to.equal(200);
|
||||
let json = await res.json();
|
||||
expect(json).to.deep.equal({ answer: 42 });
|
||||
|
||||
res = await fetch(`http://127.0.0.1:${cli.port}/deeply/nested/route`);
|
||||
expect(res.status).to.equal(200);
|
||||
json = await res.json();
|
||||
expect(json).to.deep.equal({ answer: 84 });
|
||||
});
|
||||
});
|
85
packages/integrations/cloudflare/test/wasm.test.js
Normal file
85
packages/integrations/cloudflare/test/wasm.test.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
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} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils.js').WranglerCLI} */
|
||||
let cli;
|
||||
|
||||
before(async function () {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/wasm/',
|
||||
});
|
||||
await fixture.build();
|
||||
|
||||
cli = await runCLI('./fixtures/wasm/', {
|
||||
silent: true,
|
||||
onTimeout: (ex) => {
|
||||
console.log(ex);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cli?.stop();
|
||||
});
|
||||
|
||||
it('can render', async () => {
|
||||
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 });
|
||||
});
|
||||
});
|
||||
describe('astro dev server', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/wasm/',
|
||||
});
|
||||
devServer = undefined;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer?.stop();
|
||||
});
|
||||
|
||||
it('can serve wasm', async () => {
|
||||
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: 63 });
|
||||
});
|
||||
|
||||
it('fails to build intelligently when wasm is disabled', async () => {
|
||||
let ex;
|
||||
try {
|
||||
await fixture.build({
|
||||
adapter: cloudflare({
|
||||
wasmModuleImports: false,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
ex = err;
|
||||
}
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -14,20 +14,22 @@ describe('With SolidJS', () => {
|
|||
});
|
||||
await fixture.build();
|
||||
|
||||
cli = await runCLI('./fixtures/with-solid-js/', { silent: true, port: 8790 });
|
||||
await cli.ready.catch((e) => {
|
||||
console.log(e);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
cli = await runCLI('./fixtures/with-solid-js/', {
|
||||
silent: true,
|
||||
onTimeout: (ex) => {
|
||||
console.log(ex);
|
||||
// if fail to start, skip for now as it's very flaky
|
||||
this.skip();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cli.stop();
|
||||
await cli?.stop();
|
||||
});
|
||||
|
||||
it('renders the solid component', async () => {
|
||||
let res = await fetch(`http://127.0.0.1:8790/`);
|
||||
let res = await fetch(`http://127.0.0.1:${cli.port}/`);
|
||||
expect(res.status).to.equal(200);
|
||||
let html = await res.text();
|
||||
let $ = cheerio.load(html);
|
||||
|
|
|
@ -3649,6 +3649,9 @@ importers:
|
|||
tiny-glob:
|
||||
specifier: ^0.2.9
|
||||
version: 0.2.9
|
||||
vite:
|
||||
specifier: ^4.4.9
|
||||
version: 4.4.9(@types/node@18.17.8)(sass@1.66.1)
|
||||
devDependencies:
|
||||
'@types/iarna__toml':
|
||||
specifier: ^2.0.2
|
||||
|
@ -3665,9 +3668,6 @@ importers:
|
|||
cheerio:
|
||||
specifier: 1.0.0-rc.12
|
||||
version: 1.0.0-rc.12
|
||||
kill-port:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
mocha:
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0
|
||||
|
@ -3738,6 +3738,33 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/cloudflare/test/fixtures/wasm:
|
||||
dependencies:
|
||||
'@astrojs/cloudflare':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/cloudflare/test/fixtures/wasm-directory:
|
||||
dependencies:
|
||||
'@astrojs/cloudflare':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/cloudflare/test/fixtures/wasm-function-per-route:
|
||||
dependencies:
|
||||
'@astrojs/cloudflare':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/cloudflare/test/fixtures/with-solid-js:
|
||||
dependencies:
|
||||
'@astrojs/cloudflare':
|
||||
|
@ -12043,10 +12070,6 @@ packages:
|
|||
call-bind: 1.0.2
|
||||
get-intrinsic: 1.2.1
|
||||
|
||||
/get-them-args@1.3.2:
|
||||
resolution: {integrity: sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==}
|
||||
dev: true
|
||||
|
||||
/giget@1.1.2:
|
||||
resolution: {integrity: sha512-HsLoS07HiQ5oqvObOI+Qb2tyZH4Gj5nYGfF9qQcZNrPw+uEFhdXtgJr01aO2pWadGHucajYDLxxbtQkm97ON2A==}
|
||||
hasBin: true
|
||||
|
@ -13165,14 +13188,6 @@ packages:
|
|||
commander: 8.3.0
|
||||
dev: true
|
||||
|
||||
/kill-port@2.0.1:
|
||||
resolution: {integrity: sha512-e0SVOV5jFo0mx8r7bS29maVWp17qGqLBZ5ricNSajON6//kmb7qqqNnml4twNE8Dtj97UQD+gNFOaipS/q1zzQ==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
get-them-args: 1.3.2
|
||||
shell-exec: 1.0.2
|
||||
dev: true
|
||||
|
||||
/kind-of@6.0.3:
|
||||
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -16320,10 +16335,6 @@ packages:
|
|||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/shell-exec@1.0.2:
|
||||
resolution: {integrity: sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==}
|
||||
dev: true
|
||||
|
||||
/shell-quote@1.8.1:
|
||||
resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
|
||||
dev: true
|
||||
|
|
Loading…
Reference in a new issue