From 63141f3f3e4a57d2f55ccfebd7e506ea1033a1ab Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Fri, 22 Sep 2023 14:28:03 +0200 Subject: [PATCH 01/90] fix: properly generate code for multiple images in same markdown file (#8633) --- .changeset/mean-forks-ring.md | 5 +++++ packages/astro/src/vite-plugin-markdown/index.ts | 6 ++++-- packages/astro/test/core-image.test.js | 2 +- packages/astro/test/fixtures/core-image/src/pages/post.md | 1 + 4 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .changeset/mean-forks-ring.md diff --git a/.changeset/mean-forks-ring.md b/.changeset/mean-forks-ring.md new file mode 100644 index 000000000..1aa40a6b5 --- /dev/null +++ b/.changeset/mean-forks-ring.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix build not working when having multiple images in the same Markdown file diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index 7d4a97392..dfc0bc0e0 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -120,13 +120,15 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug ${layout ? `import Layout from ${JSON.stringify(layout)};` : ''} import { getImage } from "astro:assets"; - ${imagePaths.map((entry) => `import Astro__${entry.safeName} from ${JSON.stringify(entry.raw)};`)} + ${imagePaths + .map((entry) => `import Astro__${entry.safeName} from ${JSON.stringify(entry.raw)};`) + .join('\n')} const images = async function() { return { ${imagePaths .map((entry) => `"${entry.raw}": await getImage({src: Astro__${entry.safeName}})`) - .join('\n')} + .join(',\n')} } } diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js index 7a0a46822..a6ee4342c 100644 --- a/packages/astro/test/core-image.test.js +++ b/packages/astro/test/core-image.test.js @@ -310,7 +310,7 @@ describe('astro:image', () => { it('Adds the tag', () => { let $img = $('img'); - expect($img).to.have.a.lengthOf(1); + expect($img).to.have.a.lengthOf(2); // Verbose test for the full URL to make sure the image went through the full pipeline expect( diff --git a/packages/astro/test/fixtures/core-image/src/pages/post.md b/packages/astro/test/fixtures/core-image/src/pages/post.md index 98da01ce4..822ed6189 100644 --- a/packages/astro/test/fixtures/core-image/src/pages/post.md +++ b/packages/astro/test/fixtures/core-image/src/pages/post.md @@ -1,3 +1,4 @@ ![My article cover](../assets/penguin1.jpg) +![My article cover](../assets/penguin2.jpg) Image worked From bd00ad776db9d6d3bde323d0ecf8065f186b3a57 Mon Sep 17 00:00:00 2001 From: Yan Thomas <61414485+Yan-Thomas@users.noreply.github.com> Date: Fri, 22 Sep 2023 09:57:27 -0300 Subject: [PATCH 02/90] Fix subheading inconsistency (#8623) --- packages/integrations/node/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/integrations/node/README.md b/packages/integrations/node/README.md index 5b49a2716..ecc4eb02f 100644 --- a/packages/integrations/node/README.md +++ b/packages/integrations/node/README.md @@ -2,7 +2,7 @@ This adapter allows Astro to deploy your SSR site to Node targets. -- [Why Astro Node](#why-astro-node) +- [Why Astro Node.js](#why-astro-nodejs) - [Installation](#installation) - [Configuration](#configuration) - [Usage](#usage) @@ -10,7 +10,7 @@ This adapter allows Astro to deploy your SSR site to Node targets. - [Contributing](#contributing) - [Changelog](#changelog) -## Why @astrojs/node +## Why Astro Node.js If you're using Astro as a static site builder—its behavior out of the box—you don't need an adapter. From b64dd45c0d641f9f2ed997e2cbdf8a6b0193195f Mon Sep 17 00:00:00 2001 From: Reuben Tier <64310361+TheOtterlord@users.noreply.github.com> Date: Fri, 22 Sep 2023 14:18:46 +0100 Subject: [PATCH 03/90] Fix behaviour regression in create-astro (#8634) --- .changeset/neat-islands-wink.md | 5 +++++ packages/create-astro/src/actions/template.ts | 11 +++++------ packages/create-astro/test/template.test.js | 7 +++++++ 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .changeset/neat-islands-wink.md diff --git a/.changeset/neat-islands-wink.md b/.changeset/neat-islands-wink.md new file mode 100644 index 000000000..fff9012ad --- /dev/null +++ b/.changeset/neat-islands-wink.md @@ -0,0 +1,5 @@ +--- +'create-astro': patch +--- + +Fix `--yes` behaviour to prevent it overriding `--template` diff --git a/packages/create-astro/src/actions/template.ts b/packages/create-astro/src/actions/template.ts index 253c9fab1..eaebe2360 100644 --- a/packages/create-astro/src/actions/template.ts +++ b/packages/create-astro/src/actions/template.ts @@ -9,10 +9,11 @@ import { error, info, spinner, title } from '../messages.js'; export async function template( ctx: Pick ) { - if (ctx.yes) { - ctx.template = 'basics'; - await info('tmpl', `Using ${color.reset(ctx.template)}${color.dim(' as project template')}`); - } else if (!ctx.template) { + if (!ctx.template && ctx.yes) ctx.template = 'basics'; + + if (ctx.template) { + await info('tmpl', `Using ${color.reset(ctx.template)}${color.dim(' as project template')}`); + } else { const { template: tmpl } = await ctx.prompt({ name: 'template', type: 'select', @@ -26,8 +27,6 @@ export async function template( ], }); ctx.template = tmpl; - } else { - await info('tmpl', `Using ${color.reset(ctx.template)}${color.dim(' as project template')}`); } if (ctx.dryRun) { diff --git a/packages/create-astro/test/template.test.js b/packages/create-astro/test/template.test.js index 66c7f5446..cf6b45f77 100644 --- a/packages/create-astro/test/template.test.js +++ b/packages/create-astro/test/template.test.js @@ -33,4 +33,11 @@ describe('template', () => { expect(fixture.hasMessage('Using blog as project template')).to.be.true; }); + + it('minimal (--yes)', async () => { + const context = { template: 'minimal', cwd: '', dryRun: true, yes: true, prompt: () => {} }; + await template(context); + + expect(fixture.hasMessage('Using minimal as project template')).to.be.true; + }) }); From f35a55bd4f5c722a82326f351e1adb448b6f9476 Mon Sep 17 00:00:00 2001 From: bluwy Date: Fri, 22 Sep 2023 13:20:50 +0000 Subject: [PATCH 04/90] [ci] format --- packages/create-astro/src/actions/template.ts | 8 ++++---- packages/create-astro/test/template.test.js | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/create-astro/src/actions/template.ts b/packages/create-astro/src/actions/template.ts index eaebe2360..8d22e95b1 100644 --- a/packages/create-astro/src/actions/template.ts +++ b/packages/create-astro/src/actions/template.ts @@ -9,11 +9,11 @@ import { error, info, spinner, title } from '../messages.js'; export async function template( ctx: Pick ) { - if (!ctx.template && ctx.yes) ctx.template = 'basics'; + if (!ctx.template && ctx.yes) ctx.template = 'basics'; - if (ctx.template) { - await info('tmpl', `Using ${color.reset(ctx.template)}${color.dim(' as project template')}`); - } else { + if (ctx.template) { + await info('tmpl', `Using ${color.reset(ctx.template)}${color.dim(' as project template')}`); + } else { const { template: tmpl } = await ctx.prompt({ name: 'template', type: 'select', diff --git a/packages/create-astro/test/template.test.js b/packages/create-astro/test/template.test.js index cf6b45f77..aef7e1944 100644 --- a/packages/create-astro/test/template.test.js +++ b/packages/create-astro/test/template.test.js @@ -34,10 +34,10 @@ describe('template', () => { expect(fixture.hasMessage('Using blog as project template')).to.be.true; }); - it('minimal (--yes)', async () => { - const context = { template: 'minimal', cwd: '', dryRun: true, yes: true, prompt: () => {} }; - await template(context); + it('minimal (--yes)', async () => { + const context = { template: 'minimal', cwd: '', dryRun: true, yes: true, prompt: () => {} }; + await template(context); - expect(fixture.hasMessage('Using minimal as project template')).to.be.true; - }) + expect(fixture.hasMessage('Using minimal as project template')).to.be.true; + }); }); From a3bee1477efe1da2107ea961095212f94ba45962 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Fri, 22 Sep 2023 08:47:40 -0600 Subject: [PATCH 05/90] Update: Improve manual install guide of `@astrojs/tailwind` integration (#8619) --- packages/integrations/tailwind/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/integrations/tailwind/README.md b/packages/integrations/tailwind/README.md index 8634d4b16..269931e1e 100644 --- a/packages/integrations/tailwind/README.md +++ b/packages/integrations/tailwind/README.md @@ -61,6 +61,26 @@ export default defineConfig({ }); ``` +Then, create a `tailwind.config.cjs` file in your project's root directory. You can use the following command to generate a basic configuration file for you: + +```sh +npx tailwindcss init +``` + +Finally, add this basic configuration to your `tailwind.config.cjs` file: + +```js ins={4} "content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}']" +// tailwind.config.cjs +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], + theme: { + extend: {}, + }, + plugins: [], +} +``` + ## Usage When you install the integration, Tailwind's utility classes should be ready to go right away. Head to the [Tailwind docs](https://tailwindcss.com/docs/utility-first) to learn how to use Tailwind, and if you see a utility class you want to try, add it to any HTML element to your project! From b1310e6f13c029e880145cc08a7f31129412a06c Mon Sep 17 00:00:00 2001 From: bluwy Date: Fri, 22 Sep 2023 14:49:43 +0000 Subject: [PATCH 06/90] [ci] format --- packages/integrations/tailwind/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/tailwind/README.md b/packages/integrations/tailwind/README.md index 269931e1e..a0225db77 100644 --- a/packages/integrations/tailwind/README.md +++ b/packages/integrations/tailwind/README.md @@ -78,7 +78,7 @@ module.exports = { extend: {}, }, plugins: [], -} +}; ``` ## Usage From faeead42325f378f9edac4e081eb7d6d50905136 Mon Sep 17 00:00:00 2001 From: Adrian Lyjak Date: Fri, 22 Sep 2023 10:58:00 -0400 Subject: [PATCH 07/90] feat(@astrojs/cloudflare): Add support for wasm module imports (#8542) Co-authored-by: Sarah Rainsberger --- .changeset/shy-cycles-obey.md | 5 + packages/astro/test/test-utils.js | 1 + packages/integrations/cloudflare/README.md | 43 +++++ packages/integrations/cloudflare/package.json | 4 +- packages/integrations/cloudflare/src/index.ts | 144 ++++++++++++---- .../cloudflare/src/wasm-module-loader.ts | 119 +++++++++++++ .../cloudflare/test/basics.test.js | 16 +- .../integrations/cloudflare/test/cf.test.js | 18 +- .../cloudflare/test/directory.test.js | 1 + .../fixtures/wasm-directory/astro.config.mjs | 10 ++ .../test/fixtures/wasm-directory/package.json | 9 + .../wasm-directory/src/pages/index.ts | 18 ++ .../fixtures/wasm-directory/src/util/add.wasm | Bin 0 -> 41 bytes .../wasm-function-per-route/astro.config.mjs | 12 ++ .../wasm-function-per-route/package.json | 9 + .../src/pages/deeply/nested/route.ts | 14 ++ .../src/pages/index.ts | 14 ++ .../wasm-function-per-route/src/util/add.ts | 6 + .../wasm-function-per-route/src/util/add.wasm | Bin 0 -> 41 bytes .../src/util/indirection.ts | 9 + .../test/fixtures/wasm/astro.config.mjs | 9 + .../test/fixtures/wasm/package.json | 9 + .../fixtures/wasm/src/pages/add/[a]/[b].ts | 20 +++ .../test/fixtures/wasm/src/pages/hybrid.ts | 16 ++ .../test/fixtures/wasm/src/util/add.wasm | Bin 0 -> 41 bytes .../test/function-per-route.test.js | 3 +- .../cloudflare/test/runtime.test.js | 16 +- .../cloudflare/test/test-utils.js | 157 +++++++++++++----- .../cloudflare/test/wasm-directory.test.js | 36 ++++ .../test/wasm-function-per-route.test.js | 41 +++++ .../integrations/cloudflare/test/wasm.test.js | 85 ++++++++++ .../cloudflare/test/with-solid-js.test.js | 16 +- pnpm-lock.yaml | 49 +++--- 33 files changed, 787 insertions(+), 122 deletions(-) create mode 100644 .changeset/shy-cycles-obey.md create mode 100644 packages/integrations/cloudflare/src/wasm-module-loader.ts create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-directory/astro.config.mjs create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-directory/package.json create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-directory/src/pages/index.ts create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-directory/src/util/add.wasm create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/astro.config.mjs create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/package.json create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/deeply/nested/route.ts create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/index.ts create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/add.ts create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/add.wasm create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/indirection.ts create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm/astro.config.mjs create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm/package.json create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm/src/pages/hybrid.ts create mode 100644 packages/integrations/cloudflare/test/fixtures/wasm/src/util/add.wasm create mode 100644 packages/integrations/cloudflare/test/wasm-directory.test.js create mode 100644 packages/integrations/cloudflare/test/wasm-function-per-route.test.js create mode 100644 packages/integrations/cloudflare/test/wasm.test.js diff --git a/.changeset/shy-cycles-obey.md b/.changeset/shy-cycles-obey.md new file mode 100644 index 000000000..2c79702ae --- /dev/null +++ b/.changeset/shy-cycles-obey.md @@ -0,0 +1,5 @@ +--- +'@astrojs/cloudflare': minor +--- + +Add support for loading wasm modules in the cloudflare adapter diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index fafd3046c..e71daa9ba 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -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} pathExists * @property {(url: string, opts: Parameters[1]) => Promise} fetch * @property {(path: string) => Promise} readFile * @property {(path: string, updater: (content: string) => string) => Promise} writeFile diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md index 719a5688c..1a1419cff 100644 --- a/packages/integrations/cloudflare/README.md +++ b/packages/integrations/cloudflare/README.md @@ -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. diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index 644b36bc7..ea175e438 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -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" } diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index 24c22d8f1..4ae43a110 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -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()) + .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 = 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 + }; + }); + }, + }; +} diff --git a/packages/integrations/cloudflare/src/wasm-module-loader.ts b/packages/integrations/cloudflare/src/wasm-module-loader.ts new file mode 100644 index 000000000..7d34d48c3 --- /dev/null +++ b/packages/integrations/cloudflare/src/wasm-module-loader.ts @@ -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); +} diff --git a/packages/integrations/cloudflare/test/basics.test.js b/packages/integrations/cloudflare/test/basics.test.js index 726a19fc6..eb4509da1 100644 --- a/packages/integrations/cloudflare/test/basics.test.js +++ b/packages/integrations/cloudflare/test/basics.test.js @@ -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); diff --git a/packages/integrations/cloudflare/test/cf.test.js b/packages/integrations/cloudflare/test/cf.test.js index 53b1bbf2c..78a18dcdf 100644 --- a/packages/integrations/cloudflare/test/cf.test.js +++ b/packages/integrations/cloudflare/test/cf.test.js @@ -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 () => { diff --git a/packages/integrations/cloudflare/test/directory.test.js b/packages/integrations/cloudflare/test/directory.test.js index a252b03e9..8390699e3 100644 --- a/packages/integrations/cloudflare/test/directory.test.js +++ b/packages/integrations/cloudflare/test/directory.test.js @@ -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 () => { diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-directory/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/wasm-directory/astro.config.mjs new file mode 100644 index 000000000..a30cd2086 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/wasm-directory/astro.config.mjs @@ -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' +}); diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-directory/package.json b/packages/integrations/cloudflare/test/fixtures/wasm-directory/package.json new file mode 100644 index 000000000..859aa4f40 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/wasm-directory/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/astro-cloudflare-wasm-function-per-route", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/cloudflare": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-directory/src/pages/index.ts b/packages/integrations/cloudflare/test/fixtures/wasm-directory/src/pages/index.ts new file mode 100644 index 000000000..2c9ff6d44 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/wasm-directory/src/pages/index.ts @@ -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 { + + return new Response(JSON.stringify({ answer: addModule.exports.add(40, 2) }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); +} diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-directory/src/util/add.wasm b/packages/integrations/cloudflare/test/fixtures/wasm-directory/src/util/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..357f72da7a0db8add83699082fd51d46bf3352fb GIT binary patch literal 41 wcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9eqM0DJxgJ^%m! literal 0 HcmV?d00001 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 new file mode 100644 index 000000000..7f741d884 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/astro.config.mjs @@ -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 } } +}); diff --git a/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/package.json b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/package.json new file mode 100644 index 000000000..238c1e313 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/astro-cloudflare-wasm-directory", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/cloudflare": "workspace:*", + "astro": "workspace:*" + } +} 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 new file mode 100644 index 000000000..20797c0c6 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/deeply/nested/route.ts @@ -0,0 +1,14 @@ +import { type APIContext, type EndpointOutput } from 'astro'; +import { add } from '../../../util/add'; + +export async function GET( + context: APIContext +): Promise { + + 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 new file mode 100644 index 000000000..b6417dde9 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/pages/index.ts @@ -0,0 +1,14 @@ +import { type APIContext, type EndpointOutput } from 'astro'; +import { add } from '../util/add'; + +export async function GET( + context: APIContext +): Promise { + + 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 000000000..ee336277b --- /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/add.wasm b/packages/integrations/cloudflare/test/fixtures/wasm-function-per-route/src/util/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..357f72da7a0db8add83699082fd51d46bf3352fb GIT binary patch literal 41 wcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9eqM0DJxgJ^%m! literal 0 HcmV?d00001 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 000000000..6fbb04c49 --- /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/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/wasm/astro.config.mjs new file mode 100644 index 000000000..b5e68667e --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/wasm/astro.config.mjs @@ -0,0 +1,9 @@ +import { defineConfig } from 'astro/config'; +import cloudflare from '@astrojs/cloudflare'; + +export default defineConfig({ + adapter: cloudflare({ + wasmModuleImports: true + }), + output: 'server' +}); diff --git a/packages/integrations/cloudflare/test/fixtures/wasm/package.json b/packages/integrations/cloudflare/test/fixtures/wasm/package.json new file mode 100644 index 000000000..4abd8513c --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/wasm/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/astro-cloudflare-wasm", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/cloudflare": "workspace:*", + "astro": "workspace:*" + } +} 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 new file mode 100644 index 000000000..130b2b2a4 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/add/[a]/[b].ts @@ -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 { + 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/fixtures/wasm/src/pages/hybrid.ts b/packages/integrations/cloudflare/test/fixtures/wasm/src/pages/hybrid.ts new file mode 100644 index 000000000..7bb470dff --- /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/fixtures/wasm/src/util/add.wasm b/packages/integrations/cloudflare/test/fixtures/wasm/src/util/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..357f72da7a0db8add83699082fd51d46bf3352fb GIT binary patch literal 41 wcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9eqM0DJxgJ^%m! literal 0 HcmV?d00001 diff --git a/packages/integrations/cloudflare/test/function-per-route.test.js b/packages/integrations/cloudflare/test/function-per-route.test.js index d20b0fa7c..f0287c717 100644 --- a/packages/integrations/cloudflare/test/function-per-route.test.js +++ b/packages/integrations/cloudflare/test/function-per-route.test.js @@ -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 () => { diff --git a/packages/integrations/cloudflare/test/runtime.test.js b/packages/integrations/cloudflare/test/runtime.test.js index 8bb38d7e5..e0d77d5c6 100644 --- a/packages/integrations/cloudflare/test/runtime.test.js +++ b/packages/integrations/cloudflare/test/runtime.test.js @@ -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); diff --git a/packages/integrations/cloudflare/test/test-utils.js b/packages/integrations/cloudflare/test/test-utils.js index 36515f831..50226c0c1 100644 --- a/packages/integrations/cloudflare/test/test-utils.js +++ b/packages/integrations/cloudflare/test/test-utils.js @@ -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, stop: Promise }} WranglerCLI + * @typedef {{ stop: Promise, 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} */ -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; +}; diff --git a/packages/integrations/cloudflare/test/wasm-directory.test.js b/packages/integrations/cloudflare/test/wasm-directory.test.js new file mode 100644 index 000000000..0f387a660 --- /dev/null +++ b/packages/integrations/cloudflare/test/wasm-directory.test.js @@ -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 }); + }); +}); diff --git a/packages/integrations/cloudflare/test/wasm-function-per-route.test.js b/packages/integrations/cloudflare/test/wasm-function-per-route.test.js new file mode 100644 index 000000000..f751f1ff4 --- /dev/null +++ b/packages/integrations/cloudflare/test/wasm-function-per-route.test.js @@ -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 }); + }); +}); diff --git a/packages/integrations/cloudflare/test/wasm.test.js b/packages/integrations/cloudflare/test/wasm.test.js new file mode 100644 index 000000000..279a00cd1 --- /dev/null +++ b/packages/integrations/cloudflare/test/wasm.test.js @@ -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'); + }); + }); +}); diff --git a/packages/integrations/cloudflare/test/with-solid-js.test.js b/packages/integrations/cloudflare/test/with-solid-js.test.js index c644163b0..501a947d0 100644 --- a/packages/integrations/cloudflare/test/with-solid-js.test.js +++ b/packages/integrations/cloudflare/test/with-solid-js.test.js @@ -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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dceb985fa..0e2fbd876 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 From 0352dec47b955699d91efa5d499420ca56f67e80 Mon Sep 17 00:00:00 2001 From: ematipico Date: Fri, 22 Sep 2023 15:00:18 +0000 Subject: [PATCH 08/90] [ci] format --- packages/integrations/cloudflare/README.md | 8 ++++---- packages/integrations/cloudflare/src/index.ts | 2 +- packages/integrations/cloudflare/test/test-utils.js | 8 ++++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md index 1a1419cff..8789109df 100644 --- a/packages/integrations/cloudflare/README.md +++ b/packages/integrations/cloudflare/README.md @@ -197,9 +197,9 @@ export default defineConfig({ default: `false` -Whether or not to import `.wasm` files [directly as ES modules](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration). +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. +Add `wasmModuleImports: true` to `astro.config.mjs` to enable in both the Cloudflare build and the Astro dev server. ```diff // astro.config.mjs @@ -214,9 +214,9 @@ 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 following is an example of importing a Wasm module that then responds to requests by adding the request's number parameters together. +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 diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index 4ae43a110..6a2b5c343 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -350,7 +350,7 @@ export default function createIntegration(args?: Options): AstroIntegration { 'node:stream', 'node:string_decoder', 'node:util', - ], + ], entryPoints: pathsGroup, outbase: absolutePagesDirname, outdir: outputDir, diff --git a/packages/integrations/cloudflare/test/test-utils.js b/packages/integrations/cloudflare/test/test-utils.js index 50226c0c1..5e0c698f4 100644 --- a/packages/integrations/cloudflare/test/test-utils.js +++ b/packages/integrations/cloudflare/test/test-utils.js @@ -41,7 +41,11 @@ export async function runCLI( let cli; let lastErr; while (triesRemaining > 0) { - cli = await tryRunCLI(basePath, { silent, timeout, forceRotatePort: triesRemaining !== maxAttempts }); + cli = await tryRunCLI(basePath, { + silent, + timeout, + forceRotatePort: triesRemaining !== maxAttempts, + }); try { await cli.ready; return cli; @@ -148,7 +152,7 @@ const isPortOpen = async (port) => { resolve(true); s.close(); }); - s.listen(port, "0.0.0.0"); + s.listen(port, '0.0.0.0'); }); }; From 974d5117abc8b47f8225e455b9285c88e305272f Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Fri, 22 Sep 2023 18:01:22 +0200 Subject: [PATCH 09/90] fix: no deletion of scripts during view transition (#8636) --- .changeset/short-cougars-worry.md | 5 +++++ packages/astro/components/ViewTransitions.astro | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/short-cougars-worry.md diff --git a/.changeset/short-cougars-worry.md b/.changeset/short-cougars-worry.md new file mode 100644 index 000000000..05c1c20b5 --- /dev/null +++ b/.changeset/short-cougars-worry.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +fix: no deletion of scripts during view transition diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro index aa266af13..230b2f302 100644 --- a/packages/astro/components/ViewTransitions.astro +++ b/packages/astro/components/ViewTransitions.astro @@ -219,13 +219,13 @@ const { fallback = 'animate' } = Astro.props as Props; for (const s2 of newDocument.scripts) { if ( // Inline - (s1.textContent && s1.textContent === s2.textContent) || + (!s1.src && s1.textContent === s2.textContent) || // External - (s1.type === s2.type && s1.src === s2.src) + (s1.src && s1.type === s2.type && s1.src === s2.src) ) { - s2.remove(); - } else { - s1.remove(); + // the old script is in the new document: we mark it as executed to prevent re-execution + s2.dataset.astroExec = ''; + break; } } } From f36c4295be1ef2bcfa4aecb3c59551388419c53d Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 23 Sep 2023 02:12:36 +0800 Subject: [PATCH 10/90] Warn on empty content collections (#8640) * Warn on empty content collections * Update packages/astro/src/core/errors/errors-data.ts Co-authored-by: Reuben Tier <64310361+TheOtterlord@users.noreply.github.com> --------- Co-authored-by: Reuben Tier <64310361+TheOtterlord@users.noreply.github.com> --- .changeset/spotty-jokes-arrive.md | 5 +++++ packages/astro/src/content/runtime.ts | 18 +++--------------- packages/astro/src/core/errors/errors-data.ts | 1 + 3 files changed, 9 insertions(+), 15 deletions(-) create mode 100644 .changeset/spotty-jokes-arrive.md diff --git a/.changeset/spotty-jokes-arrive.md b/.changeset/spotty-jokes-arrive.md new file mode 100644 index 000000000..8638f8b8b --- /dev/null +++ b/.changeset/spotty-jokes-arrive.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Warn on empty content collections diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index eeaa60e6c..2c5a120ca 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -1,6 +1,5 @@ import type { MarkdownHeading } from '@astrojs/markdown-remark'; import { ZodIssueCode, string as zodString } from 'zod'; -import type { AstroIntegration } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { prependForwardSlash } from '../core/path.js'; import { @@ -56,7 +55,9 @@ export function createGetCollection({ } else if (collection in dataCollectionToEntryMap) { type = 'data'; } else { - return warnOfEmptyCollection(collection); + // eslint-disable-next-line no-console + console.warn(`The collection **${collection}** does not exist or is empty. Ensure a collection directory with this name exists.`); + return; } const lazyImports = Object.values( type === 'content' @@ -390,16 +391,3 @@ type PropagatedAssetsModule = { function isPropagatedAssetsModule(module: any): module is PropagatedAssetsModule { return typeof module === 'object' && module != null && '__astroPropagation' in module; } - -function warnOfEmptyCollection(collection: string): AstroIntegration { - return { - name: 'astro-collection', - hooks: { - 'astro:server:start': ({ logger }) => { - logger.warn( - `The collection **${collection}** does not exist or is empty. Ensure a collection directory with this name exists.` - ); - }, - }, - }; -} diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index e4fe35540..ace362cef 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1158,6 +1158,7 @@ export const ContentSchemaContainsSlugError = { /** * @docs * @message A collection queried via `getCollection()` does not exist. + * @deprecated Collections that do not exist no longer result in an error. A warning is omitted instead. * @description * When querying a collection, ensure a collection directory with the requested name exists under `src/content/`. */ From 4f2bf2156f3fa1508d61f0b3f4a6f25399d0bd4e Mon Sep 17 00:00:00 2001 From: matthewp Date: Fri, 22 Sep 2023 18:15:46 +0000 Subject: [PATCH 11/90] [ci] format --- packages/astro/src/content/runtime.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index 2c5a120ca..96d6c1141 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -56,7 +56,9 @@ export function createGetCollection({ type = 'data'; } else { // eslint-disable-next-line no-console - console.warn(`The collection **${collection}** does not exist or is empty. Ensure a collection directory with this name exists.`); + console.warn( + `The collection **${collection}** does not exist or is empty. Ensure a collection directory with this name exists.` + ); return; } const lazyImports = Object.values( From 139b0f54d9ad92059c4bca55a08c70c3c563a4e7 Mon Sep 17 00:00:00 2001 From: Jacob Lamb Date: Fri, 22 Sep 2023 14:00:38 -0700 Subject: [PATCH 12/90] Refine CLI flag descriptions for clarity (#8545) * Refine CLI flag descriptions for clarity * Update README.md * Update README.md * Update README.md * Update help.ts * Update packages/create-astro/src/actions/help.ts Co-authored-by: Sarah Rainsberger * Update packages/create-astro/README.md Co-authored-by: Sarah Rainsberger * Update packages/create-astro/README.md Co-authored-by: Sarah Rainsberger * Update README.md --------- Co-authored-by: Nate Moore Co-authored-by: Sarah Rainsberger --- packages/create-astro/README.md | 23 +++++++++++++---------- packages/create-astro/src/actions/help.ts | 6 +++--- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/create-astro/README.md b/packages/create-astro/README.md index ba406d942..0f9fbde5a 100644 --- a/packages/create-astro/README.md +++ b/packages/create-astro/README.md @@ -45,16 +45,19 @@ npm create astro@latest my-astro-project -- --template cassidoo/shopify-react-as May be provided in place of prompts -| Name | Description | -| :--------------------------- | :----------------------------------------------------- | -| `--template ` | Specify your template. | -| `--install` / `--no-install` | Install dependencies (or not). | -| `--git` / `--no-git` | Initialize git repo (or not). | -| `--yes` (`-y`) | Skip all prompt by accepting defaults. | -| `--no` (`-n`) | Skip all prompt by declining defaults. | -| `--dry-run` | Walk through steps without executing. | -| `--skip-houston` | Skip Houston animation. | -| `--typescript