chore(cloudflare): remove adapter

This commit is contained in:
Alexander Niebuhr 2023-10-05 21:09:59 +02:00
parent e5e6cf16eb
commit 325080c543
No known key found for this signature in database
99 changed files with 76 additions and 5118 deletions

View file

@ -1,3 +0,0 @@
# Astro cloudflare directory mode creates a function directory
functions
.mf

File diff suppressed because one or more lines are too long

View file

@ -1,399 +1,3 @@
# @astrojs/cloudflare
An SSR adapter for use with Cloudflare Pages Functions targets. Write your code in Astro/Javascript and deploy to Cloudflare Pages.
## Install
Add the Cloudflare adapter to enable SSR in your Astro project with the following `astro add` command. This will install the adapter and make the appropriate changes to your `astro.config.mjs` file in one step.
```sh
# Using NPM
npx astro add cloudflare
# Using Yarn
yarn astro add cloudflare
# Using PNPM
pnpm astro add cloudflare
```
If you prefer to install the adapter manually instead, complete the following two steps:
1. Add the Cloudflare adapter to your project's dependencies using your preferred package manager. If youre using npm or arent sure, run this in the terminal:
```bash
npm install @astrojs/cloudflare
```
2. Add the following to your `astro.config.mjs` file:
```diff lang="js"
// astro.config.mjs
import { defineConfig } from 'astro/config';
+ import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
+ output: 'server',
+ adapter: cloudflare(),
});
```
## Options
### `mode`
`mode: "advanced" | "directory"`
default `"advanced"`
This configuration option defines how your Astro project is deployed to Cloudflare Pages.
- `advanced` mode picks up the `_worker.js` file in the `dist` folder
- `directory` mode picks up the files in the `functions` folder, by default only one `[[path]].js` file is generated
Switching to directory mode allows you to add additional files manually such as [Cloudflare Pages Plugins](https://developers.cloudflare.com/pages/platform/functions/plugins/), [Cloudflare Pages Middleware](https://developers.cloudflare.com/pages/platform/functions/middleware/) or custom functions using [Cloudflare Pages Functions Routing](https://developers.cloudflare.com/pages/platform/functions/routing/).
```js
// astro.config.mjs
export default defineConfig({
adapter: cloudflare({ mode: 'directory' }),
});
```
To compile a separate bundle for each page, set the `functionPerRoute` option in your Cloudflare adapter config. This option requires some manual maintenance of the `functions` folder. Files emitted by Astro will overwrite existing files with identical names in the `functions` folder, so you must choose unique file names for each file you manually add. Additionally, the adapter will never empty the `functions` folder of outdated files, so you must clean up the folder manually when you remove pages.
```diff lang="js"
// astro.config.mjs
import {defineConfig} from "astro/config";
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
adapter: cloudflare({
mode: 'directory',
+ functionPerRoute: true
})
})
```
This adapter doesn't support the [`edgeMiddleware`](https://docs.astro.build/en/reference/adapter-reference/#edgemiddleware) option.
### `routes.strategy`
`routes.strategy: "auto" | "include" | "exclude"`
default `"auto"`
Determines how `routes.json` will be generated if no [custom `_routes.json`](#custom-_routesjson) is provided.
There are three options available:
- **`"auto"` (default):** Will automatically select the strategy that generates the fewest entries. This should almost always be sufficient, so choose this option unless you have a specific reason not to.
- **`include`:** Pages and endpoints that are not pre-rendered are listed as `include` entries, telling Cloudflare to invoke these routes as functions. `exclude` entries are only used to resolve conflicts. Usually the best strategy when your website has mostly static pages and only a few dynamic pages or endpoints.
Example: For `src/pages/index.astro` (static), `src/pages/company.astro` (static), `src/pages/users/faq.astro` (static) and `/src/pages/users/[id].astro` (SSR) this will produce the following `_routes.json`:
```json
{
"version": 1,
"include": [
"/_image", // Astro's image endpoint
"/users/*" // Dynamic route
],
"exclude": [
// Static routes that needs to be exempted from the dynamic wildcard route above
"/users/faq/",
"/users/faq/index.html"
]
}
```
- **`exclude`:** Pre-rendered pages are listed as `exclude` entries (telling Cloudflare to handle these routes as static assets). Usually the best strategy when your website has mostly dynamic pages or endpoints and only a few static pages.
Example: For the same pages as in the previous example this will produce the following `_routes.json`:
```json
{
"version": 1,
"include": [
"/*" // Handle everything as function except the routes below
],
"exclude": [
// All static assets
"/",
"/company/",
"/index.html",
"/users/faq/",
"/favicon.png",
"/company/index.html",
"/users/faq/index.html"
]
}
```
### `routes.include`
`routes.include: string[]`
default `[]`
If you want to use the automatic `_routes.json` generation, but want to include additional routes (e.g. when having custom functions in the `functions` folder), you can use the `routes.include` option to add additional routes to the `include` array.
### `routes.exclude`
`routes.exclude: string[]`
default `[]`
If you want to use the automatic `_routes.json` generation, but want to exclude additional routes, you can use the `routes.exclude` option to add additional routes to the `exclude` array.
The following example automatically generates `_routes.json` while including and excluding additional routes. Note that that is only necessary if you have custom functions in the `functions` folder that are not handled by Astro.
```diff lang="js"
// astro.config.mjs
export default defineConfig({
adapter: cloudflare({
mode: 'directory',
+ routes: {
+ strategy: 'include',
+ include: ['/users/*'], // handled by custom function: functions/users/[id].js
+ exclude: ['/users/faq'], // handled by static page: pages/users/faq.astro
+ },
}),
});
```
### `wasmModuleImports`
`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) using the `.wasm?module` import syntax.
Add `wasmModuleImports: true` to `astro.config.mjs` to enable this functionality in both the Cloudflare build and the Astro dev server. Read more about [using Wasm modules](#use-wasm-modules)
```diff lang="js"
// astro.config.mjs
import {defineConfig} from "astro/config";
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
adapter: cloudflare({
+ wasmModuleImports: true
}),
output: 'server'
})
```
### `runtime`
`runtime: "off" | "local"`
default `"off"`
Determines whether and how the Cloudflare Runtime is added to `astro dev`.
The Cloudflare Runtime includes [Cloudflare bindings](https://developers.cloudflare.com/pages/platform/functions/bindings), [environment variables](https://developers.cloudflare.com/pages/platform/functions/bindings/#environment-variables), and the [cf object](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties). Read more about [accessing the Cloudflare Runtime](#cloudflare-runtime).
- `local`: uses bindings mocking and locally static placeholders
- `off`: no access to the Cloudflare runtime using `astro dev`. You can alternatively use [Preview with Wrangler](#preview-with-wrangler)
```diff lang="js"
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare({
+ runtime: 'local',
}),
});
```
## Cloudflare runtime
Gives you access to [environment variables](https://developers.cloudflare.com/pages/platform/functions/bindings/#environment-variables), and [Cloudflare bindings](https://developers.cloudflare.com/pages/platform/functions/bindings).
Currently supported bindings:
- [Cloudflare D1](https://developers.cloudflare.com/d1/)
- [Cloudflare R2](https://developers.cloudflare.com/r2/)
- [Cloudflare Workers KV](https://developers.cloudflare.com/kv/)
- [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/)
You can access the runtime from Astro components through `Astro.locals` inside any .astro` file.
```astro
---
// src/pages/index.astro
const runtime = Astro.locals.runtime;
---
<pre>{JSON.stringify(runtime.env)}</pre>
```
You can access the runtime from API endpoints through `context.locals`:
```js
// src/pages/api/someFile.js
export function GET(context) {
const runtime = context.locals.runtime;
return new Response('Some body');
}
```
### Typing
If you have configured `mode: advanced`, you can type the `runtime` object using `AdvancedRuntime`:
```ts
// src/env.d.ts
/// <reference types="astro/client" />
type KVNamespace = import('@cloudflare/workers-types/experimental').KVNamespace;
type ENV = {
SERVER_URL: string;
KV_BINDING: KVNamespace;
};
type Runtime = import('@astrojs/cloudflare').AdvancedRuntime<ENV>;
declare namespace App {
interface Locals extends Runtime {
user: {
name: string;
surname: string;
};
}
}
```
If you have configured `mode: directory`, you can type the `runtime` object using `DirectoryRuntime`:
```ts
// src/env.d.ts
/// <reference types="astro/client" />
type KVNamespace = import('@cloudflare/workers-types/experimental').KVNamespace;
type ENV = {
SERVER_URL: string;
KV_BINDING: KVNamespace;
};
type Runtime = import('@astrojs/cloudflare').DirectoryRuntime<ENV>;
declare namespace App {
interface Locals extends Runtime {
user: {
name: string;
surname: string;
};
}
}
```
## Platform
### Headers
You can attach [custom headers](https://developers.cloudflare.com/pages/platform/headers/) to your responses by adding a `_headers` file in your Astro project's `public/` folder. This file will be copied to your build output directory.
### Redirects
You can declare [custom redirects](https://developers.cloudflare.com/pages/platform/redirects/) using Cloudflare Pages. This allows you to redirect requests to a different URL. You can add a `_redirects` file in your Astro project's `public/` folder. This file will be copied to your build output directory.
### Routes
You can define which routes are invoking functions and which are static assets, using [Cloudflare routing](https://developers.cloudflare.com/pages/platform/functions/routing/#functions-invocation-routes) via a `_routes.json` file. This file is automatically generated by Astro.
#### Custom `_routes.json`
By default, `@astrojs/cloudflare` will generate a `_routes.json` file with `include` and `exclude` rules based on your applications's dynamic and static routes.
This will enable Cloudflare to serve files and process static redirects without a function invocation. Creating a custom `_routes.json` will override this automatic optimization. See [Cloudflare's documentation on creating a custom `routes.json`](https://developers.cloudflare.com/pages/platform/functions/routing/#create-a-_routesjson-file) for more details.
## Use Wasm modules
The following is an example of importing a Wasm module that then responds to requests by adding the request's number parameters together.
```js
// 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.
## Node.js compatibility
Astro's Cloudflare adapter allows you to use any Node.js runtime API supported by Cloudflare:
- assert
- AsyncLocalStorage
- Buffer
- Diagnostics Channel
- EventEmitter
- path
- process
- Streams
- StringDecoder
- util
To use these APIs, your page or endpoint must be server-side rendered (not pre-rendered) and must use the the `import {} from 'node:*'` import syntax.
```js
// pages/api/endpoint.js
export const prerender = false;
import { Buffer } from 'node:buffer';
```
Additionally, you'll need to enable the Compatibility Flag in Cloudflare. The configuration for this flag may vary based on where you deploy your Astro site. For detailed guidance, please refer to the [Cloudflare documentation on enabling Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs).
## Preview with Wrangler
To use [`wrangler`](https://developers.cloudflare.com/workers/wrangler/) to run your application locally, update the preview script:
```json
//package.json
"preview": "wrangler pages dev ./dist"
```
[`wrangler`](https://developers.cloudflare.com/workers/wrangler/) gives you access to [Cloudflare bindings](https://developers.cloudflare.com/pages/platform/functions/bindings), [environment variables](https://developers.cloudflare.com/pages/platform/functions/bindings/#environment-variables), and the [cf object](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties). Getting hot reloading or the astro dev server to work with Wrangler might require custom setup. See [community examples](https://github.com/withastro/roadmap/discussions/590).
### Meaningful error messages
Currently, errors during running your application in Wrangler are not very useful, due to the minification of your code. For better debugging, you can add `vite.build.minify = false` setting to your `astro.config.mjs`.
```diff lang="js"
// astro.config.mjs
export default defineConfig({
adapter: cloudflare(),
output: 'server',
+ vite: {
+ build: {
+ minify: false,
+ },
+ },
});
```
## Troubleshooting
For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help!
You can also check our [Astro Integration Documentation][astro-integration] for more on integrations.
## Contributing
This package is maintained by Astro's Core team. You're welcome to submit an issue or PR!
[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/
The Cloudflare adapter package has moved. Please see [the new repository for the Cloudflare adapter](https://github.com/withastro/adapters/tree/main/packages/cloudflare).

View file

@ -1,65 +1,7 @@
{
"name": "@astrojs/cloudflare",
"description": "Deploy your site to Cloudflare Workers/Pages",
"version": "7.5.1",
"type": "module",
"types": "./dist/index.d.ts",
"author": "withastro",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"directory": "packages/integrations/cloudflare"
},
"keywords": [
"withastro",
"astro-adapter"
],
"bugs": "https://github.com/withastro/astro/issues",
"homepage": "https://docs.astro.build/en/guides/integrations-guide/cloudflare/",
"exports": {
".": "./dist/index.js",
"./entrypoints/server.advanced.js": "./dist/entrypoints/server.advanced.js",
"./entrypoints/server.directory.js": "./dist/entrypoints/server.directory.js",
"./package.json": "./package.json"
},
"files": [
"dist"
],
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "mocha --exit --timeout 30000 test/",
"test:match": "mocha --exit --timeout 30000 -g"
},
"dependencies": {
"@astrojs/underscore-redirects": "workspace:*",
"@cloudflare/workers-types": "^4.20230821.0",
"miniflare": "^3.20230918.0",
"@iarna/toml": "^2.2.5",
"@miniflare/cache": "^2.14.1",
"@miniflare/shared": "^2.14.1",
"@miniflare/storage-memory": "^2.14.1",
"dotenv": "^16.3.1",
"esbuild": "^0.19.2",
"find-up": "^6.3.0",
"tiny-glob": "^0.2.9",
"vite": "^4.4.9"
},
"peerDependencies": {
"astro": "workspace:^3.2.3"
},
"devDependencies": {
"@types/iarna__toml": "^2.0.2",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^4.3.7",
"cheerio": "1.0.0-rc.12",
"mocha": "^10.2.0",
"wrangler": "^3.5.1"
},
"publishConfig": {
"provenance": true
}
"version": "0.0.0",
"private": true,
"keywords": [],
"dont_remove": "This is a placeholder for the same of the docs smoke test"
}

View file

@ -1,75 +0,0 @@
import type { Request as CFRequest, ExecutionContext } from '@cloudflare/workers-types';
import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
import { getProcessEnvProxy, isNode } from '../util.js';
if (!isNode) {
process.env = getProcessEnvProxy();
}
type Env = {
ASSETS: { fetch: (req: Request) => Promise<Response> };
};
export interface AdvancedRuntime<T extends object = object> {
runtime: {
waitUntil: (promise: Promise<any>) => void;
env: Env & T;
cf: CFRequest['cf'];
caches: typeof caches;
};
}
export function createExports(manifest: SSRManifest) {
const app = new App(manifest);
const fetch = async (request: Request & CFRequest, env: Env, context: ExecutionContext) => {
// TODO: remove this any cast in the future
// REF: the type cast to any is needed because the Cloudflare Env Type is not assignable to type 'ProcessEnv'
process.env = env as any;
const { pathname } = new URL(request.url);
// static assets fallback, in case default _routes.json is not used
if (manifest.assets.has(pathname)) {
return env.ASSETS.fetch(request);
}
let routeData = app.match(request, { matchNotFound: true });
if (routeData) {
Reflect.set(
request,
Symbol.for('astro.clientAddress'),
request.headers.get('cf-connecting-ip')
);
const locals: AdvancedRuntime = {
runtime: {
waitUntil: (promise: Promise<any>) => {
context.waitUntil(promise);
},
env: env,
cf: request.cf,
caches: caches,
},
};
let response = await app.render(request, routeData, locals);
if (app.setCookieHeaders) {
for (const setCookieHeader of app.setCookieHeaders(response)) {
response.headers.append('Set-Cookie', setCookieHeader);
}
}
return response;
}
return new Response(null, {
status: 404,
statusText: 'Not found',
});
};
return { default: { fetch } };
}

View file

@ -1,73 +0,0 @@
import type { Request as CFRequest, EventContext } from '@cloudflare/workers-types';
import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
import { getProcessEnvProxy, isNode } from '../util.js';
if (!isNode) {
process.env = getProcessEnvProxy();
}
export interface DirectoryRuntime<T extends object = object> {
runtime: {
waitUntil: (promise: Promise<any>) => void;
env: EventContext<unknown, string, unknown>['env'] & T;
cf: CFRequest['cf'];
caches: typeof caches;
};
}
export function createExports(manifest: SSRManifest) {
const app = new App(manifest);
const onRequest = async (context: EventContext<unknown, string, unknown>) => {
const request = context.request as CFRequest & Request;
const { env } = context;
// TODO: remove this any cast in the future
// REF: the type cast to any is needed because the Cloudflare Env Type is not assignable to type 'ProcessEnv'
process.env = env as any;
const { pathname } = new URL(request.url);
// static assets fallback, in case default _routes.json is not used
if (manifest.assets.has(pathname)) {
return env.ASSETS.fetch(request);
}
let routeData = app.match(request, { matchNotFound: true });
if (routeData) {
Reflect.set(
request,
Symbol.for('astro.clientAddress'),
request.headers.get('cf-connecting-ip')
);
const locals: DirectoryRuntime = {
runtime: {
waitUntil: (promise: Promise<any>) => {
context.waitUntil(promise);
},
env: context.env,
cf: request.cf,
caches: caches,
},
};
let response = await app.render(request, routeData, locals);
if (app.setCookieHeaders) {
for (const setCookieHeader of app.setCookieHeaders(response)) {
response.headers.append('Set-Cookie', setCookieHeader);
}
}
return response;
}
return new Response(null, {
status: 404,
statusText: 'Not found',
});
};
return { onRequest, manifest };
}

View file

@ -1,40 +0,0 @@
import type { AstroAdapter, AstroFeatureMap } from 'astro';
export function getAdapter({
isModeDirectory,
functionPerRoute,
}: {
isModeDirectory: boolean;
functionPerRoute: boolean;
}): AstroAdapter {
const astroFeatures = {
hybridOutput: 'stable',
staticOutput: 'unsupported',
serverOutput: 'stable',
assets: {
supportKind: 'stable',
isSharpCompatible: false,
isSquooshCompatible: false,
},
} satisfies AstroFeatureMap;
if (isModeDirectory) {
return {
name: '@astrojs/cloudflare',
serverEntrypoint: '@astrojs/cloudflare/entrypoints/server.directory.js',
exports: ['onRequest', 'manifest'],
adapterFeatures: {
functionPerRoute,
edgeMiddleware: false,
},
supportedAstroFeatures: astroFeatures,
};
}
return {
name: '@astrojs/cloudflare',
serverEntrypoint: '@astrojs/cloudflare/entrypoints/server.advanced.js',
exports: ['default'],
supportedAstroFeatures: astroFeatures,
};
}

View file

@ -1,611 +0,0 @@
import type { AstroConfig, AstroIntegration, RouteData } from 'astro';
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
import { AstroError } from 'astro/errors';
import esbuild from 'esbuild';
import { Miniflare } from 'miniflare';
import * as fs from 'node:fs';
import * as os from 'node:os';
import { dirname, relative, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import glob from 'tiny-glob';
import { getAdapter } from './getAdapter.js';
import { deduplicatePatterns } from './utils/deduplicatePatterns.js';
import { getCFObject } from './utils/getCFObject.js';
import {
getD1Bindings,
getDOBindings,
getEnvVars,
getKVBindings,
getR2Bindings,
} from './utils/parser.js';
import { prependForwardSlash } from './utils/prependForwardSlash.js';
import { rewriteWasmImportPath } from './utils/rewriteWasmImportPath.js';
import { wasmModuleLoader } from './utils/wasm-module-loader.js';
export type { AdvancedRuntime } from './entrypoints/server.advanced.js';
export type { DirectoryRuntime } from './entrypoints/server.directory.js';
type Options = {
mode?: 'directory' | 'advanced';
functionPerRoute?: boolean;
/** Configure automatic `routes.json` generation */
routes?: {
/** Strategy for generating `include` and `exclude` patterns
* - `auto`: Will use the strategy that generates the least amount of entries.
* - `include`: For each page or endpoint in your application that is not prerendered, an entry in the `include` array will be generated. For each page that is prerendered and whoose path is matched by an `include` entry, an entry in the `exclude` array will be generated.
* - `exclude`: One `"/*"` entry in the `include` array will be generated. For each page that is prerendered, an entry in the `exclude` array will be generated.
* */
strategy?: 'auto' | 'include' | 'exclude';
/** Additional `include` patterns */
include?: string[];
/** Additional `exclude` patterns */
exclude?: string[];
};
/**
* 'off': current behaviour (wrangler is needed)
* 'local': use a static req.cf object, and env vars defined in wrangler.toml & .dev.vars (astro dev is enough)
* '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;
}
export default function createIntegration(args?: Options): AstroIntegration {
let _config: AstroConfig;
let _buildConfig: BuildConfig;
let _mf: Miniflare;
let _entryPoints = new Map<RouteData, URL>();
const SERVER_BUILD_FOLDER = '/$server_build/';
const isModeDirectory = args?.mode === 'directory';
const functionPerRoute = args?.functionPerRoute ?? false;
const runtimeMode = args?.runtime ?? 'off';
return {
name: '@astrojs/cloudflare',
hooks: {
'astro:config:setup': ({ config, updateConfig }) => {
updateConfig({
build: {
client: new URL(`.${config.base}`, config.outDir),
server: new URL(`.${SERVER_BUILD_FOLDER}`, config.outDir),
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 }) => {
setAdapter(getAdapter({ isModeDirectory, functionPerRoute }));
_config = config;
_buildConfig = config.build;
if (_config.output === 'static') {
throw new AstroError(
'[@astrojs/cloudflare] `output: "server"` or `output: "hybrid"` is required to use this adapter. Otherwise, this adapter is not necessary to deploy a static site to Cloudflare.'
);
}
if (_config.base === SERVER_BUILD_FOLDER) {
throw new AstroError(
'[@astrojs/cloudflare] `base: "${SERVER_BUILD_FOLDER}"` is not allowed. Please change your `base` config to something else.'
);
}
},
'astro:server:setup': ({ server }) => {
if (runtimeMode !== 'off') {
server.middlewares.use(async function middleware(req, res, next) {
try {
const cf = await getCFObject(runtimeMode);
const vars = await getEnvVars();
const D1Bindings = await getD1Bindings();
const R2Bindings = await getR2Bindings();
const KVBindings = await getKVBindings();
const DOBindings = await getDOBindings();
let bindingsEnv = new Object({});
// fix for the error "kj/filesystem-disk-unix.c++:1709: warning: PWD environment variable doesn't match current directory."
// note: This mismatch might be primarily due to the test runner.
const originalPWD = process.env.PWD;
process.env.PWD = process.cwd();
_mf = new Miniflare({
modules: true,
script: '',
cache: true,
cachePersist: true,
cacheWarnUsage: true,
d1Databases: D1Bindings,
d1Persist: true,
r2Buckets: R2Bindings,
r2Persist: true,
kvNamespaces: KVBindings,
kvPersist: true,
durableObjects: DOBindings,
durableObjectsPersist: true,
});
await _mf.ready;
for (const D1Binding of D1Bindings) {
const db = await _mf.getD1Database(D1Binding);
Reflect.set(bindingsEnv, D1Binding, db);
}
for (const R2Binding of R2Bindings) {
const bucket = await _mf.getR2Bucket(R2Binding);
Reflect.set(bindingsEnv, R2Binding, bucket);
}
for (const KVBinding of KVBindings) {
const namespace = await _mf.getKVNamespace(KVBinding);
Reflect.set(bindingsEnv, KVBinding, namespace);
}
for (const key in DOBindings) {
if (Object.prototype.hasOwnProperty.call(DOBindings, key)) {
const DO = await _mf.getDurableObjectNamespace(key);
Reflect.set(bindingsEnv, key, DO);
}
}
const mfCache = await _mf.getCaches();
process.env.PWD = originalPWD;
const clientLocalsSymbol = Symbol.for('astro.locals');
Reflect.set(req, clientLocalsSymbol, {
runtime: {
env: {
// default binding for static assets will be dynamic once we support mocking of bindings
ASSETS: {},
// this is just a VAR for CF to change build behavior, on dev it should be 0
CF_PAGES: '0',
// will be fetched from git dynamically once we support mocking of bindings
CF_PAGES_BRANCH: 'TBA',
// will be fetched from git dynamically once we support mocking of bindings
CF_PAGES_COMMIT_SHA: 'TBA',
CF_PAGES_URL: `http://${req.headers.host}`,
...bindingsEnv,
...vars,
},
cf: cf,
waitUntil: (_promise: Promise<any>) => {
return;
},
caches: mfCache,
},
});
next();
} catch {
next();
}
});
}
},
'astro:server:done': async ({ logger }) => {
if (_mf) {
logger.info('Cleaning up the Miniflare instance, and shutting down the workerd server.');
await _mf.dispose();
}
},
'astro:build:setup': ({ vite, target }) => {
if (target === 'server') {
vite.resolve ||= {};
vite.resolve.alias ||= {};
const aliases = [{ find: 'react-dom/server', replacement: 'react-dom/server.browser' }];
if (Array.isArray(vite.resolve.alias)) {
vite.resolve.alias = [...vite.resolve.alias, ...aliases];
} else {
for (const alias of aliases) {
(vite.resolve.alias as Record<string, string>)[alias.find] = alias.replacement;
}
}
vite.ssr ||= {};
vite.ssr.target = 'webworker';
// Cloudflare env is only available per request. This isn't feasible for code that access env vars
// in a global way, so we shim their access as `process.env.*`. We will populate `process.env` later
// in its fetch handler.
vite.define = {
'process.env': 'process.env',
...vite.define,
};
}
},
'astro:build:ssr': ({ entryPoints }) => {
_entryPoints = entryPoints;
},
'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 });
}
// TODO: remove _buildConfig.split in Astro 4.0
if (isModeDirectory && (_buildConfig.split || functionPerRoute)) {
const entryPointsURL = [..._entryPoints.values()];
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();
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: 'es2022',
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: `globalThis.process = {
argv: [],
env: {},
};`,
},
logOverride: {
'ignored-bare-import': 'silent',
},
plugins: !args?.wasmModuleImports
? []
: [rewriteWasmImportPath({ relativePathToAssets })],
});
}
const outputFiles: Array<string> = await glob(`**/*`, {
cwd: outputDir,
filesOnly: true,
});
// move the files into the functions folder
// & make sure the file names match Cloudflare syntax for routing
for (const outputFile of outputFiles) {
const path = outputFile.split(sep);
const finalSegments = path.map((segment) =>
segment
.replace(/(\_)(\w+)(\_)/g, (_, __, prop) => {
return `[${prop}]`;
})
.replace(/(\_\-\-\-)(\w+)(\_)/g, (_, __, prop) => {
return `[[${prop}]]`;
})
);
finalSegments[finalSegments.length - 1] = finalSegments[finalSegments.length - 1]
.replace('entry.', '')
.replace(/(.*)\.(\w+)\.(\w+)$/g, (_, fileName, __, newExt) => {
return `${fileName}.${newExt}`;
});
const finalDirPath = finalSegments.slice(0, -1).join(sep);
const finalPath = finalSegments.join(sep);
const newDirUrl = new URL(finalDirPath, functionsUrl);
await fs.promises.mkdir(newDirUrl, { recursive: true });
const oldFileUrl = new URL(`$astro/${outputFile}`, outputUrl);
const newFileUrl = new URL(finalPath, functionsUrl);
await fs.promises.rename(oldFileUrl, newFileUrl);
}
} else {
const entryPath = fileURLToPath(new URL(_buildConfig.serverEntry, _buildConfig.server));
const entryUrl = new URL(_buildConfig.serverEntry, _config.outDir);
const buildPath = fileURLToPath(entryUrl);
// A URL for the final build path after renaming
const finalBuildUrl = pathToFileURL(buildPath.replace(/\.mjs$/, '.js'));
await esbuild.build({
target: 'es2022',
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: [entryPath],
outfile: buildPath,
allowOverwrite: true,
format: 'esm',
bundle: true,
minify: _config.vite?.build?.minify !== false,
banner: {
js: `globalThis.process = {
argv: [],
env: {},
};`,
},
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
await fs.promises.rename(buildPath, finalBuildUrl);
if (isModeDirectory) {
const directoryUrl = new URL('[[path]].js', functionsUrl);
await fs.promises.rename(finalBuildUrl, directoryUrl);
}
}
// throw the server folder in the bin
const serverUrl = new URL(_buildConfig.server);
await fs.promises.rm(serverUrl, { recursive: true, force: true });
// move cloudflare specific files to the root
const cloudflareSpecialFiles = ['_headers', '_redirects', '_routes.json'];
if (_config.base !== '/') {
for (const file of cloudflareSpecialFiles) {
try {
await fs.promises.rename(
new URL(file, _buildConfig.client),
new URL(file, _config.outDir)
);
} catch (e) {
// ignore
}
}
}
// Add also the worker file so it's excluded from the _routes.json generation
if (!isModeDirectory) {
cloudflareSpecialFiles.push('_worker.js');
}
const routesExists = await fs.promises
.stat(new URL('./_routes.json', _config.outDir))
.then((stat) => stat.isFile())
.catch(() => false);
// this creates a _routes.json, in case there is none present to enable
// cloudflare to handle static files and support _redirects configuration
if (!routesExists) {
/**
* These route types are candiates for being part of the `_routes.json` `include` array.
*/
const potentialFunctionRouteTypes = ['endpoint', 'page'];
const functionEndpoints = routes
// Certain route types, when their prerender option is set to false, run on the server as function invocations
.filter((route) => potentialFunctionRouteTypes.includes(route.type) && !route.prerender)
.map((route) => {
const includePattern =
'/' +
route.segments
.flat()
.map((segment) => (segment.dynamic ? '*' : segment.content))
.join('/');
const regexp = new RegExp(
'^\\/' +
route.segments
.flat()
.map((segment) => (segment.dynamic ? '(.*)' : segment.content))
.join('\\/') +
'$'
);
return {
includePattern,
regexp,
};
});
const staticPathList: Array<string> = (
await glob(`${fileURLToPath(_buildConfig.client)}/**/*`, {
cwd: fileURLToPath(_config.outDir),
filesOnly: true,
dot: true,
})
)
.filter((file: string) => cloudflareSpecialFiles.indexOf(file) < 0)
.map((file: string) => `/${file.replace(/\\/g, '/')}`);
for (let page of pages) {
let pagePath = prependForwardSlash(page.pathname);
if (_config.base !== '/') {
const base = _config.base.endsWith('/') ? _config.base.slice(0, -1) : _config.base;
pagePath = `${base}${pagePath}`;
}
staticPathList.push(pagePath);
}
const redirectsExists = await fs.promises
.stat(new URL('./_redirects', _config.outDir))
.then((stat) => stat.isFile())
.catch(() => false);
// convert all redirect source paths into a list of routes
// and add them to the static path
if (redirectsExists) {
const redirects = (
await fs.promises.readFile(new URL('./_redirects', _config.outDir), 'utf-8')
)
.split(os.EOL)
.map((line) => {
const parts = line.split(' ');
if (parts.length < 2) {
return null;
} else {
// convert /products/:id to /products/*
return (
parts[0]
.replace(/\/:.*?(?=\/|$)/g, '/*')
// remove query params as they are not supported by cloudflare
.replace(/\?.*$/, '')
);
}
})
.filter(
(line, index, arr) => line !== null && arr.indexOf(line) === index
) as string[];
if (redirects.length > 0) {
staticPathList.push(...redirects);
}
}
const redirectRoutes: [RouteData, string][] = routes
.filter((r) => r.type === 'redirect')
.map((r) => {
return [r, ''];
});
const trueRedirects = createRedirectsFromAstroRoutes({
config: _config,
routeToDynamicTargetMap: new Map(Array.from(redirectRoutes)),
dir,
});
if (!trueRedirects.empty()) {
await fs.promises.appendFile(
new URL('./_redirects', _config.outDir),
trueRedirects.print()
);
}
staticPathList.push(...routes.filter((r) => r.type === 'redirect').map((r) => r.route));
const strategy = args?.routes?.strategy ?? 'auto';
// Strategy `include`: include all function endpoints, and then exclude static paths that would be matched by an include pattern
const includeStrategy =
strategy === 'exclude'
? undefined
: {
include: deduplicatePatterns(
functionEndpoints
.map((endpoint) => endpoint.includePattern)
.concat(args?.routes?.include ?? [])
),
exclude: deduplicatePatterns(
staticPathList
.filter((file: string) =>
functionEndpoints.some((endpoint) => endpoint.regexp.test(file))
)
.concat(args?.routes?.exclude ?? [])
),
};
// Cloudflare requires at least one include pattern:
// https://developers.cloudflare.com/pages/platform/functions/routing/#limits
// So we add a pattern that we immediately exclude again
if (includeStrategy?.include.length === 0) {
includeStrategy.include = ['/'];
includeStrategy.exclude = ['/'];
}
// Strategy `exclude`: include everything, and then exclude all static paths
const excludeStrategy =
strategy === 'include'
? undefined
: {
include: ['/*'],
exclude: deduplicatePatterns(staticPathList.concat(args?.routes?.exclude ?? [])),
};
const includeStrategyLength = includeStrategy
? includeStrategy.include.length + includeStrategy.exclude.length
: Infinity;
const excludeStrategyLength = excludeStrategy
? excludeStrategy.include.length + excludeStrategy.exclude.length
: Infinity;
const winningStrategy =
includeStrategyLength <= excludeStrategyLength ? includeStrategy : excludeStrategy;
await fs.promises.writeFile(
new URL('./_routes.json', _config.outDir),
JSON.stringify(
{
version: 1,
...winningStrategy,
},
null,
2
)
);
}
},
},
};
}

View file

@ -1,19 +0,0 @@
export const isNode =
typeof process === 'object' && Object.prototype.toString.call(process) === '[object process]';
export function getProcessEnvProxy() {
return new Proxy(
{},
{
get: (target, prop) => {
console.warn(
// NOTE: \0 prevents Vite replacement
`Unable to access \`import.meta\0.env.${prop.toString()}\` on initialization ` +
`as the Cloudflare platform only provides the environment variables per request. ` +
`Please move the environment variable access inside a function ` +
`that's only called after a request has been received.`
);
},
}
);
}

View file

@ -1,26 +0,0 @@
/**
* Remove duplicates and redundant patterns from an `include` or `exclude` list.
* Otherwise Cloudflare will throw an error on deployment. Plus, it saves more entries.
* E.g. `['/foo/*', '/foo/*', '/foo/bar'] => ['/foo/*']`
* @param patterns a list of `include` or `exclude` patterns
* @returns a deduplicated list of patterns
*/
export function deduplicatePatterns(patterns: string[]) {
const openPatterns: RegExp[] = [];
// A value in the set may only occur once; it is unique in the set's collection.
// ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
return [...new Set(patterns)]
.sort((a, b) => a.length - b.length)
.filter((pattern) => {
if (openPatterns.some((p) => p.test(pattern))) {
return false;
}
if (pattern.endsWith('*')) {
openPatterns.push(new RegExp(`^${pattern.replace(/(\*\/)*\*$/g, '.*')}`));
}
return true;
});
}

View file

@ -1,70 +0,0 @@
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types/experimental';
export async function getCFObject(
runtimeMode: string
): Promise<IncomingRequestCfProperties | void> {
const CF_ENDPOINT = 'https://workers.cloudflare.com/cf.json';
const CF_FALLBACK: IncomingRequestCfProperties = {
asOrganization: '',
asn: 395747,
colo: 'DFW',
city: 'Austin',
region: 'Texas',
regionCode: 'TX',
metroCode: '635',
postalCode: '78701',
country: 'US',
continent: 'NA',
timezone: 'America/Chicago',
latitude: '30.27130',
longitude: '-97.74260',
clientTcpRtt: 0,
httpProtocol: 'HTTP/1.1',
requestPriority: 'weight=192;exclusive=0',
tlsCipher: 'AEAD-AES128-GCM-SHA256',
tlsVersion: 'TLSv1.3',
tlsClientAuth: {
certPresented: '0',
certVerified: 'NONE',
certRevoked: '0',
certIssuerDN: '',
certSubjectDN: '',
certIssuerDNRFC2253: '',
certSubjectDNRFC2253: '',
certIssuerDNLegacy: '',
certSubjectDNLegacy: '',
certSerial: '',
certIssuerSerial: '',
certSKI: '',
certIssuerSKI: '',
certFingerprintSHA1: '',
certFingerprintSHA256: '',
certNotBefore: '',
certNotAfter: '',
},
edgeRequestKeepAliveStatus: 0,
hostMetadata: undefined,
clientTrustScore: 99,
botManagement: {
corporateProxy: false,
verifiedBot: false,
ja3Hash: '25b4882c2bcb50cd6b469ff28c596742',
staticResource: false,
detectionIds: [],
score: 99,
},
};
if (runtimeMode === 'local') {
return CF_FALLBACK;
} else if (runtimeMode === 'remote') {
try {
const res = await fetch(CF_ENDPOINT);
const cfText = await res.text();
const storedCf = JSON.parse(cfText);
return storedCf;
} catch (e: any) {
return CF_FALLBACK;
}
}
}

View file

@ -1,191 +0,0 @@
/**
* This file is a derivative work of wrangler by Cloudflare
* An upstream request for exposing this API was made here:
* https://github.com/cloudflare/workers-sdk/issues/3897
*
* Until further notice, we will be using this file as a workaround
* TODO: Tackle this file, once their is an decision on the upstream request
*/
import type {} from '@cloudflare/workers-types/experimental';
import TOML from '@iarna/toml';
import dotenv from 'dotenv';
import { findUpSync } from 'find-up';
import * as fs from 'node:fs';
import { dirname, resolve } from 'node:path';
let _wrangler: any;
function findWranglerToml(
referencePath: string = process.cwd(),
preferJson = false
): string | undefined {
if (preferJson) {
return (
findUpSync(`wrangler.json`, { cwd: referencePath }) ??
findUpSync(`wrangler.toml`, { cwd: referencePath })
);
}
return findUpSync(`wrangler.toml`, { cwd: referencePath });
}
type File = {
file?: string;
fileText?: string;
};
type Location = File & {
line: number;
column: number;
length?: number;
lineText?: string;
suggestion?: string;
};
type Message = {
text: string;
location?: Location;
notes?: Message[];
kind?: 'warning' | 'error';
};
class ParseError extends Error implements Message {
readonly text: string;
readonly notes: Message[];
readonly location?: Location;
readonly kind: 'warning' | 'error';
constructor({ text, notes, location, kind }: Message) {
super(text);
this.name = this.constructor.name;
this.text = text;
this.notes = notes ?? [];
this.location = location;
this.kind = kind ?? 'error';
}
}
const TOML_ERROR_NAME = 'TomlError';
const TOML_ERROR_SUFFIX = ' at row ';
type TomlError = Error & {
line: number;
col: number;
};
function parseTOML(input: string, file?: string): TOML.JsonMap | never {
try {
// Normalize CRLF to LF to avoid hitting https://github.com/iarna/iarna-toml/issues/33.
const normalizedInput = input.replace(/\r\n/g, '\n');
return TOML.parse(normalizedInput);
} catch (err) {
const { name, message, line, col } = err as TomlError;
if (name !== TOML_ERROR_NAME) {
throw err;
}
const text = message.substring(0, message.lastIndexOf(TOML_ERROR_SUFFIX));
const lineText = input.split('\n')[line];
const location = {
lineText,
line: line + 1,
column: col - 1,
file,
fileText: input,
};
throw new ParseError({ text, location });
}
}
export interface DotEnv {
path: string;
parsed: dotenv.DotenvParseOutput;
}
function tryLoadDotEnv(path: string): DotEnv | undefined {
try {
const parsed = dotenv.parse(fs.readFileSync(path));
return { path, parsed };
} catch (e) {
// logger.debug(`Failed to load .env file "${path}":`, e);
}
}
/**
* Loads a dotenv file from <path>, preferring to read <path>.<environment> if
* <environment> is defined and that file exists.
*/
export function loadDotEnv(path: string): DotEnv | undefined {
return tryLoadDotEnv(path);
}
function getVarsForDev(config: any, configPath: string | undefined): any {
const configDir = resolve(dirname(configPath ?? '.'));
const devVarsPath = resolve(configDir, '.dev.vars');
const loaded = loadDotEnv(devVarsPath);
if (loaded !== undefined) {
return {
...config.vars,
...loaded.parsed,
};
} else {
return config.vars;
}
}
function parseConfig() {
if (_wrangler) return _wrangler;
let rawConfig;
const configPath = findWranglerToml(process.cwd(), false); // false = args.experimentalJsonConfig
if (!configPath) {
throw new Error('Could not find wrangler.toml');
}
// Load the configuration from disk if available
if (configPath?.endsWith('toml')) {
rawConfig = parseTOML(fs.readFileSync(configPath).toString(), configPath);
}
_wrangler = { rawConfig, configPath };
return { rawConfig, configPath };
}
export async function getEnvVars() {
const { rawConfig, configPath } = parseConfig();
const vars = getVarsForDev(rawConfig, configPath);
return vars;
}
export async function getD1Bindings() {
const { rawConfig } = parseConfig();
if (!rawConfig) return [];
if (!rawConfig?.d1_databases) return [];
const bindings = (rawConfig?.d1_databases as []).map(
(binding: { binding: string }) => binding.binding
);
return bindings;
}
export async function getR2Bindings() {
const { rawConfig } = parseConfig();
if (!rawConfig) return [];
if (!rawConfig?.r2_buckets) return [];
const bindings = (rawConfig?.r2_buckets as []).map(
(binding: { binding: string }) => binding.binding
);
return bindings;
}
export async function getKVBindings() {
const { rawConfig } = parseConfig();
if (!rawConfig) return [];
if (!rawConfig?.kv_namespaces) return [];
const bindings = (rawConfig?.kv_namespaces as []).map(
(binding: { binding: string }) => binding.binding
);
return bindings;
}
export function getDOBindings(): Record<
string,
{ scriptName?: string | undefined; unsafeUniqueKey?: string | undefined; className: string }
> {
const { rawConfig } = parseConfig();
if (!rawConfig) return {};
if (!rawConfig?.durable_objects) return {};
const output = new Object({}) as Record<
string,
{ scriptName?: string | undefined; unsafeUniqueKey?: string | undefined; className: string }
>;
for (const binding of rawConfig?.durable_objects.bindings) {
Reflect.set(output, binding.name, { className: binding.class_name });
}
return output;
}

View file

@ -1,3 +0,0 @@
export function prependForwardSlash(path: string) {
return path[0] === '/' ? path : '/' + path;
}

View file

@ -1,29 +0,0 @@
import esbuild from 'esbuild';
import { basename } from 'node:path';
/**
*
* @param relativePathToAssets - relative path from the final location for the current esbuild output bundle, to the assets directory.
*/
export 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,
external: true, // mark it as external in the bundle
};
});
},
};
}

View file

@ -1,119 +0,0 @@
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);
}

View file

@ -1,39 +0,0 @@
import { loadFixture, runCLI } from './test-utils.js';
import { expect } from 'chai';
import * as cheerio from 'cheerio';
describe('Basic app', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
/** @type {import('./test-utils').WranglerCLI} */
let cli;
before(async function () {
fixture = await loadFixture({
root: './fixtures/basics/',
});
await fixture.build();
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();
});
it('can render', async () => {
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);
expect($('h1').text()).to.equal('Testing');
expect($('#env').text()).to.equal('secret');
});
});

View file

@ -1,127 +0,0 @@
import { loadFixture, runCLI } from './test-utils.js';
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import cloudflare from '../dist/index.js';
describe('Wrangler Cloudflare Runtime', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
/** @type {import('./test-utils').WranglerCLI} */
let cli;
before(async function () {
fixture = await loadFixture({
root: './fixtures/cf/',
output: 'server',
adapter: cloudflare(),
});
await fixture.build();
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();
});
it('Load cf and caches API', async () => {
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);
expect($('#hasRuntime').text()).to.equal('true');
expect($('#hasCache').text()).to.equal('true');
});
});
describe('Astro Cloudflare Runtime', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let devServer;
before(async () => {
fixture = await loadFixture({
root: './fixtures/cf/',
output: 'server',
adapter: cloudflare({
runtime: 'local',
}),
});
process.chdir('./test/fixtures/cf');
devServer = await fixture.startDevServer();
});
after(async () => {
await devServer?.stop();
});
it('adds cf object', async () => {
let res = await fixture.fetch('/');
expect(res.status).to.equal(200);
let html = await res.text();
let $ = cheerio.load(html);
expect($('#hasCF').text()).to.equal('true');
});
it('adds cache mocking', async () => {
let res = await fixture.fetch('/caches');
expect(res.status).to.equal(200);
let html = await res.text();
let $ = cheerio.load(html);
expect($('#hasCACHE').text()).to.equal('true');
});
it('adds D1 mocking', async () => {
expect(await fixture.pathExists('../.mf/d1')).to.be.true;
let res = await fixture.fetch('/d1');
expect(res.status).to.equal(200);
let html = await res.text();
let $ = cheerio.load(html);
expect($('#hasDB').text()).to.equal('true');
expect($('#hasPRODDB').text()).to.equal('true');
expect($('#hasACCESS').text()).to.equal('true');
});
it('adds R2 mocking', async () => {
expect(await fixture.pathExists('../.mf/r2')).to.be.true;
let res = await fixture.fetch('/r2');
expect(res.status).to.equal(200);
let html = await res.text();
let $ = cheerio.load(html);
expect($('#hasBUCKET').text()).to.equal('true');
expect($('#hasPRODBUCKET').text()).to.equal('true');
expect($('#hasACCESS').text()).to.equal('true');
});
it('adds KV mocking', async () => {
expect(await fixture.pathExists('../.mf/kv')).to.be.true;
let res = await fixture.fetch('/kv');
expect(res.status).to.equal(200);
let html = await res.text();
let $ = cheerio.load(html);
expect($('#hasKV').text()).to.equal('true');
expect($('#hasPRODKV').text()).to.equal('true');
expect($('#hasACCESS').text()).to.equal('true');
});
it('adds DO mocking', async () => {
expect(await fixture.pathExists('../.mf/do')).to.be.true;
let res = await fixture.fetch('/do');
expect(res.status).to.equal(200);
let html = await res.text();
let $ = cheerio.load(html);
expect($('#hasDO').text()).to.equal('true');
});
});

View file

@ -1,36 +0,0 @@
import { loadFixture } from './test-utils.js';
import { expect } from 'chai';
import cloudflare from '../dist/index.js';
/** @type {import('./test-utils').Fixture} */
describe('mode: "directory"', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/basics/',
output: 'server',
adapter: cloudflare({ mode: 'directory' }),
redirects: {
'/old': '/',
},
});
await fixture.build();
});
it('generates functions folder inside the project root', async () => {
expect(await fixture.pathExists('../functions')).to.be.true;
expect(await fixture.pathExists('../functions/[[path]].js')).to.be.true;
});
it('generates a redirects file', async () => {
try {
let _redirects = await fixture.readFile('/_redirects');
let parts = _redirects.split(/\s+/);
expect(parts).to.deep.equal(['/old', '/', '301']);
} catch {
expect(false).to.equal(true);
}
});
});

View file

@ -1,10 +0,0 @@
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
// test env var
process.env.SECRET_STUFF = 'secret'
export default defineConfig({
adapter: cloudflare(),
output: 'server'
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/astro-cloudflare-basics",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -1,9 +0,0 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
<div id="env">{import.meta.env.SECRET_STUFF}</div>
</body>
</html>

View file

@ -1 +0,0 @@
DATABASE_URL="postgresql://lorem"

View file

@ -1,9 +0,0 @@
{
"name": "@test/astro-cloudflare-cf",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -1,15 +0,0 @@
---
const runtime = Astro.locals.runtime;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CACHES</title>
</head>
<body>
<pre id="hasCACHE">{!!runtime.caches}</pre>
</body>
</html>

View file

@ -1,21 +0,0 @@
---
const runtime = Astro.locals.runtime;
const db = runtime.env?.D1;
await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)");
await db.exec("INSERT INTO test (name) VALUES ('true')");
const result = await db.prepare("SELECT * FROM test").all();
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D1</title>
</head>
<body>
<pre id="hasDB">{!!runtime.env?.D1}</pre>
<pre id="hasPRODDB">{!!runtime.env?.D1_PROD}</pre>
<pre id="hasACCESS">{!!result.results[0].name}</pre>
</body>
</html>

View file

@ -1,15 +0,0 @@
---
const runtime = Astro.locals.runtime;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DO</title>
</head>
<body>
<pre id="hasDO">{!!runtime.env.DO}</pre>
</body>
</html>

View file

@ -1,12 +0,0 @@
---
const runtime = Astro.locals.runtime;
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
<div id="hasCF">{!!runtime.cf?.colo}</div>
</body>
</html>

View file

@ -1,20 +0,0 @@
---
const runtime = Astro.locals.runtime;
const kv = runtime.env?.KV;
await kv.put("test", "true");
const result = await kv.get("test")
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KV</title>
</head>
<body>
<pre id="hasKV">{!!runtime.env?.KV}</pre>
<pre id="hasPRODKV">{!!runtime.env?.KV_PROD}</pre>
<pre id="hasACCESS">{!!result}</pre>
</body>
</html>

View file

@ -1,20 +0,0 @@
---
const runtime = Astro.locals.runtime;
const bucket = runtime.env?.R2;
await bucket.put("test", "true");
const result = await (await bucket.get("test")).text()
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>R2</title>
</head>
<body>
<pre id="hasBUCKET">{!!runtime.env?.R2}</pre>
<pre id="hasPRODBUCKET">{!!runtime.env?.R2_PROD}</pre>
<pre id="hasACCESS">{!!result}</pre>
</body>
</html>

View file

@ -1,37 +0,0 @@
name = "test"
kv_namespaces = [
{ binding = "KV", id = "<YOUR_ID>", preview_id = "<YOUR_ID>" },
{ binding = "KV_PROD", id = "<YOUR_ID>", preview_id = "<YOUR_ID>" }
]
[vars]
COOL = "ME"
[[d1_databases]]
binding = "D1" # Should match preview_database_id, i.e. available in your Worker on env.DB
database_name = "<DATABASE_NAME>"
database_id = "<unique-ID-for-your-database>"
preview_database_id = "D1" # Required for Pages local development
[[d1_databases]]
binding = "D1_PROD" # Should match preview_database_id
database_name = "<DATABASE_NAME>"
database_id = "<unique-ID-for-your-database>"
preview_database_id = "D1_PROD" # Required for Pages local development
[[r2_buckets]]
binding = 'R2' # <~ valid JavaScript variable name
bucket_name = '<YOUR_BUCKET_NAME>'
[[r2_buckets]]
binding = 'R2_PROD' # <~ valid JavaScript variable name
bucket_name = '<YOUR_BUCKET_NAME>'
[[durable_objects.bindings]]
name = "DO"
class_name = "DurableObjectExample"
[[durable_objects.bindings]]
name = "DO_PROD"
class_name = "DurableObjectProductionExample"

View file

@ -1,15 +0,0 @@
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
adapter: cloudflare({
mode: 'directory',
functionPerRoute: true
}),
output: 'server',
vite: {
build: {
minify: false,
},
},
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/astro-cloudflare-function-per-route",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -1,10 +0,0 @@
import { defineMiddleware } from "astro/middleware";
export const onRequest = defineMiddleware(({ locals, request }, next) => {
// intercept response data from a request
// optionally, transform the response by modifying `locals`
locals.title = "New title"
// return a Response or the result of calling `next()`
return next()
});

View file

@ -1,37 +0,0 @@
---
const files = [
{
slug: undefined,
title: 'Root level',
},
{
slug: 'test.png',
title: "One level"
},
{
slug: 'assets/test.png',
title: "Two levels"
},
{
slug: 'assets/images/test.png',
title: 'Three levels',
}
];
const { path } = Astro.params;
const page = files.find((page) => page.slug === path);
const { title } = page;
---
<html>
<body>
<h1>Files / Rest Parameters / {title}</h1>
<p>DEBUG: {path} </p>
<p><a href="/">index</a></p>
</body>
<style>
h1 {
background-color: yellow;
}
</style>
</html>

View file

@ -1,14 +0,0 @@
---
const { person, car } = Astro.params;
---
<html>
<body>
<h1> {person} / {car}</h1>
<p><a href="/">index</a></p>
</body>
<style>
h1 {
background-color: blue;
}
</style>
</html>

View file

@ -1,14 +0,0 @@
---
const { post } = Astro.params;
---
<html>
<body>
<h1>Blog / {post}</h1>
<p><a href="/">index</a></p>
</body>
<style>
h1 {
background-color: pink;
}
</style>
</html>

View file

@ -1,11 +0,0 @@
<html>
<body>
<h1>Blog / Cool</h1>
<p><a href="/">index</a></p>
</body>
<style>
h1 {
background-color: orange;
}
</style>
</html>

View file

@ -1,37 +0,0 @@
---
const files = [
{
slug: undefined,
title: 'Root level',
},
{
slug: 'test.png',
title: "One level"
},
{
slug: 'assets/test.png',
title: "Two levels"
},
{
slug: 'assets/images/test.png',
title: 'Three levels',
}
];
const { path } = Astro.params;
const page = files.find((page) => page.slug === path);
const { title } = page;
---
<html>
<body>
<h1>Files / Rest Parameters / {title}</h1>
<p>DEBUG: {path} </p>
<p><a href="/">index</a></p>
</body>
<style>
h1 {
background-color: yellow;
}
</style>
</html>

View file

@ -1,22 +0,0 @@
---
const data = Astro.locals;
---
<html>
<body>
<h1>Index</h1>
<p>Middleware ({data.title})</p>
<p><a href="/prerender/">prerender</a></p>
<p><a href="/blog/cool/">sub-route</a></p>
<p><a href="/blog/dynamic-post/">dynamic route in static sub-route</a></p>
<p><a href="/mustermann/bmw/">dynamic route in dynamic sub-route</a></p>
<p><a href="/files/">rest parameters root level</a></p>
<p><a href="/files/test.png/">rest parameters one level</a></p>
<p><a href="/files/assets/test.png/">rest parameters two level</a></p>
<p><a href="/files/assets/images/test.png/">rest parameters three level</a></p>
</body>
<style>
h1 {
background-color: red;
}
</style>
</html>

View file

@ -1,14 +0,0 @@
---
export const prerender = true;
---
<html>
<body>
<h1>Prerender</h1>
<p><a href="/">index</a></p>
</body>
<style>
h1 {
background-color: yellow;
}
</style>
</html>

View file

@ -1,6 +0,0 @@
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
adapter: cloudflare()
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/astro-cloudflare-no-output",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -1,7 +0,0 @@
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
adapter: cloudflare(),
output: 'server',
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/astro-cloudflare-prerender",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -1,8 +0,0 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
</body>
</html>

View file

@ -1,11 +0,0 @@
---
export const prerender = import.meta.env.PRERENDER;
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
</body>
</html>

View file

@ -1,9 +0,0 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
// adapter will be set dynamically by the test
output: 'hybrid',
redirects: {
'/a/redirect': '/',
},
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/astro-cloudflare-routes-json",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -1 +0,0 @@
/redirectme / 302

View file

@ -1,5 +0,0 @@
---
export const prerender=false;
---
ok

View file

@ -1,5 +0,0 @@
---
export const prerender=false;
---
ok

View file

@ -1,5 +0,0 @@
---
export const prerender=false;
---
ok

View file

@ -1,5 +0,0 @@
---
export const prerender=false;
---
ok

View file

@ -1,5 +0,0 @@
---
export const prerender=false;
---
ok

View file

@ -1,5 +0,0 @@
---
export const prerender=false;
---
ok

View file

@ -1 +0,0 @@
export const prerender = false;

View file

@ -1,8 +0,0 @@
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
adapter: cloudflare(),
output: 'server',
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/astro-cloudflare-runtime",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -1,15 +0,0 @@
---
const runtime = Astro.locals.runtime;
const env = runtime.env;
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
<div id="env">{JSON.stringify(env)}</div>
<div id="hasRuntime">{!!runtime.cf?.colo}</div>
<div id="hasCache">{!!runtime.caches}</div>
</body>
</html>

View file

@ -1,10 +0,0 @@
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
adapter: cloudflare({
mode: 'directory',
wasmModuleImports: true
}),
output: 'server'
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/astro-cloudflare-wasm-function-per-route",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -1,18 +0,0 @@
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',
},
});
}

View file

@ -1,12 +0,0 @@
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 } }
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/astro-cloudflare-wasm-directory",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -1,14 +0,0 @@
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',
},
});
}

View file

@ -1,14 +0,0 @@
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',
},
});
}

View file

@ -1,6 +0,0 @@
// extra layer of indirection to stress the esbuild
import { addImpl } from "./indirection";
export function add(a: number, b: number): number {
return addImpl(a, b);
}

View file

@ -1,9 +0,0 @@
// 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);
}

View file

@ -1,9 +0,0 @@
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
adapter: cloudflare({
wasmModuleImports: true
}),
output: 'server'
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/astro-cloudflare-wasm",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -1,20 +0,0 @@
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',
},
});
}

View file

@ -1,16 +0,0 @@
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',
},
});
}

View file

@ -1,9 +0,0 @@
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
import solidJs from "@astrojs/solid-js";
export default defineConfig({
integrations: [solidJs()],
adapter: cloudflare(),
output: 'server',
});

View file

@ -1,11 +0,0 @@
{
"name": "@test/astro-cloudflare-with-solid-js",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"@astrojs/solid-js": "workspace:*",
"astro": "workspace:*",
"solid-js": "^1.7.11"
}
}

View file

@ -1 +0,0 @@
export const Component = () => <div class="solid">Solid Content</div>

View file

@ -1,13 +0,0 @@
---
import {Component} from "../components/Component";
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
<Component />
</body>
</html>

View file

@ -1,36 +0,0 @@
import { loadFixture } from './test-utils.js';
import { expect } from 'chai';
/** @type {import('./test-utils.js').Fixture} */
describe('Cloudflare SSR functionPerRoute', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/function-per-route/',
});
await fixture.build();
});
after(() => {
fixture?.clean();
});
it('generates functions folders inside the project root, and checks that each page is emitted by astro', async () => {
expect(await fixture.pathExists('../functions')).to.be.true;
expect(await fixture.pathExists('../functions/index.js')).to.be.true;
expect(await fixture.pathExists('../functions/blog/cool.js')).to.be.true;
expect(await fixture.pathExists('../functions/blog/[post].js')).to.be.true;
expect(await fixture.pathExists('../functions/[person]/[car].js')).to.be.true;
expect(await fixture.pathExists('../functions/files/[[path]].js')).to.be.true;
expect(await fixture.pathExists('../functions/[language]/files/[[path]].js')).to.be.true;
expect(await fixture.pathExists('../functions/trpc/[trpc].js')).to.be.true;
expect(await fixture.pathExists('../functions/javascript.js')).to.be.true;
expect(await fixture.pathExists('../functions/test.json.js')).to.be.true;
});
it('generates pre-rendered files', async () => {
expect(await fixture.pathExists('./prerender/index.html')).to.be.true;
});
});

View file

@ -1,24 +0,0 @@
import { loadFixture } from './test-utils.js';
import { expect } from 'chai';
describe('Missing output config', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/no-output/',
});
});
it('throws during the build', async () => {
let error = undefined;
try {
await fixture.build();
} catch (err) {
error = err;
}
expect(error).to.not.be.equal(undefined);
expect(error.message).to.include(`output: "server"`);
});
});

View file

@ -1,58 +0,0 @@
import { loadFixture } from './test-utils.js';
import { expect } from 'chai';
describe('Prerendering', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
process.env.PRERENDER = true;
fixture = await loadFixture({
root: './fixtures/prerender/',
});
await fixture.build();
});
after(() => {
delete process.env.PRERENDER;
fixture.clean();
});
it('includes non prerendered routes in the routes.json config', async () => {
const foundRoutes = JSON.parse(await fixture.readFile('/_routes.json'));
expect(foundRoutes).to.deep.equal({
version: 1,
include: ['/', '/_image'],
exclude: [],
});
});
});
describe('Hybrid rendering', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
process.env.PRERENDER = false;
fixture = await loadFixture({
root: './fixtures/prerender/',
output: 'hybrid',
});
await fixture.build();
});
after(() => {
delete process.env.PRERENDER;
});
it('includes non prerendered routes in the routes.json config', async () => {
const foundRoutes = JSON.parse(await fixture.readFile('/_routes.json'));
expect(foundRoutes).to.deep.equal({
version: 1,
include: ['/one', '/_image'],
exclude: [],
});
});
});

View file

@ -1,211 +0,0 @@
import { expect } from 'chai';
import { loadFixture } from './test-utils.js';
import cloudflare from '../dist/index.js';
/** @type {import('./test-utils.js').Fixture} */
describe('_routes.json generation', () => {
for (const mode of ['directory', 'advanced']) {
for (const functionPerRoute of [false, true]) {
describe(`with mode=${mode}, functionPerRoute=${functionPerRoute}`, () => {
describe('of both functions and static files', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/routes-json/',
srcDir: './src/mixed',
adapter: cloudflare({
mode,
functionPerRoute,
}),
});
await fixture.build();
});
it('creates `include` for functions and `exclude` for static files where needed', async () => {
const _routesJson = await fixture.readFile('/_routes.json');
const routes = JSON.parse(_routesJson);
expect(routes).to.deep.equal({
version: 1,
include: ['/a/*', '/_image'],
exclude: ['/a/', '/a/redirect', '/a/index.html'],
});
});
});
describe('of only functions', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/routes-json/',
srcDir: './src/dynamicOnly',
adapter: cloudflare({
mode,
functionPerRoute,
}),
});
await fixture.build();
});
it('creates a wildcard `include` and `exclude` only for static assets and redirects', async () => {
const _routesJson = await fixture.readFile('/_routes.json');
const routes = JSON.parse(_routesJson);
expect(routes).to.deep.equal({
version: 1,
include: ['/*'],
exclude: ['/public.txt', '/redirectme', '/a/redirect'],
});
});
});
describe('of only static files', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/routes-json/',
srcDir: './src/staticOnly',
adapter: cloudflare({
mode,
functionPerRoute,
}),
});
await fixture.build();
});
it('create only one `include` and `exclude` that are supposed to match nothing', async () => {
const _routesJson = await fixture.readFile('/_routes.json');
const routes = JSON.parse(_routesJson);
expect(routes).to.deep.equal({
version: 1,
include: ['/_image'],
exclude: [],
});
});
});
describe('with strategy `"include"`', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/routes-json/',
srcDir: './src/dynamicOnly',
adapter: cloudflare({
mode,
functionPerRoute,
routes: { strategy: 'include' },
}),
});
await fixture.build();
});
it('creates `include` entries even though the `"exclude"` strategy would have produced less entries.', async () => {
const _routesJson = await fixture.readFile('/_routes.json');
const routes = JSON.parse(_routesJson);
expect(routes).to.deep.equal({
version: 1,
include: ['/', '/_image', '/dynamic1', '/dynamic2', '/dynamic3'],
exclude: [],
});
});
});
describe('with strategy `"exclude"`', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/routes-json/',
srcDir: './src/staticOnly',
adapter: cloudflare({
mode,
functionPerRoute,
routes: { strategy: 'exclude' },
}),
});
await fixture.build();
});
it('creates `exclude` entries even though the `"include"` strategy would have produced less entries.', async () => {
const _routesJson = await fixture.readFile('/_routes.json');
const routes = JSON.parse(_routesJson);
expect(routes).to.deep.equal({
version: 1,
include: ['/*'],
exclude: ['/', '/index.html', '/public.txt', '/redirectme', '/a/redirect'],
});
});
});
describe('with additional `include` entries', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/routes-json/',
srcDir: './src/mixed',
adapter: cloudflare({
mode,
functionPerRoute,
routes: {
strategy: 'include',
include: ['/another', '/a/redundant'],
},
}),
});
await fixture.build();
});
it('creates `include` for functions and `exclude` for static files where needed', async () => {
const _routesJson = await fixture.readFile('/_routes.json');
const routes = JSON.parse(_routesJson);
expect(routes).to.deep.equal({
version: 1,
include: ['/a/*', '/_image', '/another'],
exclude: ['/a/', '/a/redirect', '/a/index.html'],
});
});
});
describe('with additional `exclude` entries', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/routes-json/',
srcDir: './src/mixed',
adapter: cloudflare({
mode,
functionPerRoute,
routes: {
strategy: 'include',
exclude: ['/another', '/a/*', '/a/index.html'],
},
}),
});
await fixture.build();
});
it('creates `include` for functions and `exclude` for static files where needed', async () => {
const _routesJson = await fixture.readFile('/_routes.json');
const routes = JSON.parse(_routesJson);
expect(routes).to.deep.equal({
version: 1,
include: ['/a/*', '/_image'],
exclude: ['/a/', '/a/*', '/another'],
});
});
});
});
}
}
});

View file

@ -1,44 +0,0 @@
import { loadFixture, runCLI } from './test-utils.js';
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import cloudflare from '../dist/index.js';
describe('Runtime Locals', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;
/** @type {import('./test-utils.js').WranglerCLI} */
let cli;
before(async function () {
fixture = await loadFixture({
root: './fixtures/runtime/',
output: 'server',
adapter: cloudflare(),
});
await fixture.build();
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();
});
it('has CF and Caches', async () => {
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);
expect($('#env').text()).to.contain('SECRET_STUFF');
expect($('#env').text()).to.contain('secret');
expect($('#hasRuntime').text()).to.contain('true');
expect($('#hasCache').text()).to.equal('true');
});
});

View file

@ -1,169 +0,0 @@
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
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 {{ stop: Promise<void>, port: number }} WranglerCLI
* @typedef {import('../../../astro/test/test-utils').Fixture} Fixture
*/
export function loadFixture(config) {
if (config?.root) {
config.root = new URL(config.root, import.meta.url);
}
return baseLoadFixture(config);
}
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,
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;
}
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 ready = new Promise(async (resolve, reject) => {
const failed = setTimeout(() => {
p.kill('SIGKILL');
reject(new Error(`Timed out starting the wrangler CLI`));
}, timeout);
const success = () => {
clearTimeout(failed);
resolve();
};
p.on('exit', (code) => reject(`wrangler terminated unexpectedly with exit code ${code}`));
p.stderr.on('data', (data) => {
if (!silent) {
process.stdout.write(data);
}
});
let allData = '';
p.stdout.on('data', (data) => {
if (!silent) {
process.stdout.write(data);
}
allData += data;
if (allData.includes(`[mf:inf] Ready on`)) {
success();
}
});
});
return {
port,
ready,
stop() {
return new Promise((resolve, reject) => {
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;
};

View file

@ -1,36 +0,0 @@
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 });
});
});

View file

@ -1,41 +0,0 @@
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 });
});
});

View file

@ -1,85 +0,0 @@
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');
});
});
});

View file

@ -1,38 +0,0 @@
import { loadFixture, runCLI } from './test-utils.js';
import { expect } from 'chai';
import * as cheerio from 'cheerio';
describe('With SolidJS', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
/** @type {import('./test-utils').WranglerCLI} */
let cli;
before(async function () {
fixture = await loadFixture({
root: './fixtures/with-solid-js/',
});
await fixture.build();
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();
});
it('renders the solid component', async () => {
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);
expect($('.solid').text()).to.equal('Solid Content');
});
});

View file

@ -1,6 +0,0 @@
# for tests only
send_metrics = false
[vars]
SECRET_STUFF = "secret"

View file

@ -1,7 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
"outDir": "./dist"
}
}

File diff suppressed because it is too large Load diff