aboutsummaryrefslogtreecommitdiff
path: root/packages/integrations/cloudflare/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/cloudflare/src')
-rw-r--r--packages/integrations/cloudflare/src/entrypoints/image-endpoint.ts2
-rw-r--r--packages/integrations/cloudflare/src/entrypoints/image-service.ts3
-rw-r--r--packages/integrations/cloudflare/src/entrypoints/server.advanced.ts50
-rw-r--r--packages/integrations/cloudflare/src/entrypoints/server.directory.ts65
-rw-r--r--packages/integrations/cloudflare/src/getAdapter.ts40
-rw-r--r--packages/integrations/cloudflare/src/index.ts693
-rw-r--r--packages/integrations/cloudflare/src/tmp-types.d.ts139
-rw-r--r--packages/integrations/cloudflare/src/util.ts16
-rw-r--r--packages/integrations/cloudflare/src/utils/assets.ts27
-rw-r--r--packages/integrations/cloudflare/src/utils/deduplicatePatterns.ts27
-rw-r--r--packages/integrations/cloudflare/src/utils/generate-routes-json.ts282
-rw-r--r--packages/integrations/cloudflare/src/utils/image-config.ts2
-rw-r--r--packages/integrations/cloudflare/src/utils/local-runtime.ts368
-rw-r--r--packages/integrations/cloudflare/src/utils/parser.ts192
-rw-r--r--packages/integrations/cloudflare/src/utils/prependForwardSlash.ts3
-rw-r--r--packages/integrations/cloudflare/src/utils/rewriteWasmImportPath.ts29
-rw-r--r--packages/integrations/cloudflare/src/utils/sharpBundlePatch.ts20
-rw-r--r--packages/integrations/cloudflare/src/utils/wasm-module-loader.ts58
18 files changed, 534 insertions, 1482 deletions
diff --git a/packages/integrations/cloudflare/src/entrypoints/image-endpoint.ts b/packages/integrations/cloudflare/src/entrypoints/image-endpoint.ts
index e69de29bb..ab51f42f2 100644
--- a/packages/integrations/cloudflare/src/entrypoints/image-endpoint.ts
+++ b/packages/integrations/cloudflare/src/entrypoints/image-endpoint.ts
@@ -0,0 +1,2 @@
+// NOTE: this file is empty on purpose
+// it allows use to offer `imageService: 'compile'`
diff --git a/packages/integrations/cloudflare/src/entrypoints/image-service.ts b/packages/integrations/cloudflare/src/entrypoints/image-service.ts
index 8adc5ce8a..08a638d97 100644
--- a/packages/integrations/cloudflare/src/entrypoints/image-service.ts
+++ b/packages/integrations/cloudflare/src/entrypoints/image-service.ts
@@ -1,7 +1,8 @@
import type { ExternalImageService } from 'astro';
+import { joinPaths } from '@astrojs/internal-helpers/path';
import { baseService } from 'astro/assets';
-import { isESMImportedImage, isRemoteAllowed, joinPaths } from '../utils/assets.js';
+import { isESMImportedImage, isRemoteAllowed } from '../utils/assets.js';
const service: ExternalImageService = {
...baseService,
diff --git a/packages/integrations/cloudflare/src/entrypoints/server.advanced.ts b/packages/integrations/cloudflare/src/entrypoints/server.advanced.ts
index f5c596a7d..d7f9e1f24 100644
--- a/packages/integrations/cloudflare/src/entrypoints/server.advanced.ts
+++ b/packages/integrations/cloudflare/src/entrypoints/server.advanced.ts
@@ -1,59 +1,71 @@
import type {
- Request as CFRequest,
- CacheStorage,
+ CacheStorage as CLOUDFLARE_CACHESTORAGE,
+ Request as CLOUDFLARE_REQUEST,
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> };
+ ASSETS: { fetch: (req: Request | string) => Promise<Response> };
+ ASTRO_STUDIO_APP_TOKEN?: string;
};
-export interface AdvancedRuntime<T extends object = object> {
+export interface Runtime<T extends object = object> {
runtime: {
waitUntil: (promise: Promise<any>) => void;
env: Env & T;
- cf: CFRequest['cf'];
- caches: CacheStorage;
+ cf: CLOUDFLARE_REQUEST['cf'];
+ caches: CLOUDFLARE_CACHESTORAGE;
};
}
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 fetch = async (
+ request: Request & CLOUDFLARE_REQUEST,
+ env: Env,
+ context: ExecutionContext
+ ) => {
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);
+ return env.ASSETS.fetch(request.url.replace(/\.html$/, ''));
}
const routeData = app.match(request);
+ if (!routeData) {
+ // https://developers.cloudflare.com/pages/functions/api-reference/#envassetsfetch
+ const asset = await env.ASSETS.fetch(
+ request.url.replace(/index.html$/, '').replace(/\.html$/, '')
+ );
+ if (asset.status !== 404) {
+ return asset;
+ }
+ }
+
Reflect.set(
request,
Symbol.for('astro.clientAddress'),
request.headers.get('cf-connecting-ip')
);
- const locals: AdvancedRuntime = {
+ process.env.ASTRO_STUDIO_APP_TOKEN ??= (() => {
+ if (typeof env.ASTRO_STUDIO_APP_TOKEN === 'string') {
+ return env.ASTRO_STUDIO_APP_TOKEN;
+ }
+ })();
+
+ const locals: Runtime = {
runtime: {
waitUntil: (promise: Promise<any>) => {
context.waitUntil(promise);
},
env: env,
cf: request.cf,
- caches: caches as unknown as CacheStorage,
+ caches: caches as unknown as CLOUDFLARE_CACHESTORAGE,
},
};
diff --git a/packages/integrations/cloudflare/src/entrypoints/server.directory.ts b/packages/integrations/cloudflare/src/entrypoints/server.directory.ts
deleted file mode 100644
index 97ff80705..000000000
--- a/packages/integrations/cloudflare/src/entrypoints/server.directory.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import type { Request as CFRequest, CacheStorage, 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: CacheStorage;
- };
-}
-
-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);
- }
-
- const routeData = app.match(request);
- 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 as unknown as CacheStorage,
- },
- };
-
- const 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 { onRequest, manifest };
-}
diff --git a/packages/integrations/cloudflare/src/getAdapter.ts b/packages/integrations/cloudflare/src/getAdapter.ts
deleted file mode 100644
index 0a4879557..000000000
--- a/packages/integrations/cloudflare/src/getAdapter.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { AstroAdapter, AstroFeatureMap } from 'astro';
-
-export function getAdapter({
- isModeDirectory,
- functionPerRoute,
-}: {
- isModeDirectory: boolean;
- functionPerRoute: boolean;
-}): AstroAdapter {
- const astroFeatures: AstroFeatureMap = {
- hybridOutput: 'stable',
- staticOutput: 'unsupported',
- serverOutput: 'stable',
- assets: {
- supportKind: 'stable',
- isSharpCompatible: false,
- isSquooshCompatible: false,
- },
- };
-
- 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,
- };
-}
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
index d27a46f78..1427dc938 100644
--- a/packages/integrations/cloudflare/src/index.ts
+++ b/packages/integrations/cloudflare/src/index.ts
@@ -1,88 +1,68 @@
import type { AstroConfig, AstroIntegration, RouteData } from 'astro';
-import type { LocalPagesRuntime, LocalWorkersRuntime, RUNTIME } from './utils/local-runtime.js';
-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 { createReadStream } from 'node:fs';
+import { appendFile, rename, stat } from 'node:fs/promises';
+import { createInterface } from 'node:readline/promises';
+import {
+ appendForwardSlash,
+ prependForwardSlash,
+ removeLeadingForwardSlash,
+} from '@astrojs/internal-helpers/path';
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
import { AstroError } from 'astro/errors';
-import esbuild from 'esbuild';
-import glob from 'tiny-glob';
-import { getAdapter } from './getAdapter.js';
-import { deduplicatePatterns } from './utils/deduplicatePatterns.js';
-import { prepareImageConfig } from './utils/image-config.js';
-import { getLocalRuntime, getRuntimeConfig } from './utils/local-runtime.js';
-import { prependForwardSlash } from './utils/prependForwardSlash.js';
-import { rewriteWasmImportPath } from './utils/rewriteWasmImportPath.js';
-import { patchSharpBundle } from './utils/sharpBundlePatch.js';
+import { getPlatformProxy } from 'wrangler';
+import { createRoutesFile, getParts } from './utils/generate-routes-json.js';
+import { setImageConfig } from './utils/image-config.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';
+export type { Runtime } from './entrypoints/server.advanced.js';
+
export type Options = {
- /**
- * @deprecated Removed in v10. The 'directory' mode was discontinued because it redundantly bundles code, slowing down your site. Prefer using Astro API Endpoints over `/functions`. The new default mode is 'advanced'.
- */
- mode?: 'directory' | 'advanced';
- /**
- * @deprecated Removed in v10. This setting is obsolete as Cloudflare handles all functions in a single execution context, negating the need for multiple functions per project.
- */
- functionPerRoute?: boolean;
+ /** Options for handling images. */
imageService?: 'passthrough' | 'cloudflare' | 'compile';
+ /** Configuration for `_routes.json` generation. A _routes.json file controls when your Function is invoked. This file will include three different properties:
+ *
+ * - version: Defines the version of the schema. Currently there is only one version of the schema (version 1), however, we may add more in the future and aim to be backwards compatible.
+ * - include: Defines routes that will be invoked by Functions. Accepts wildcard behavior.
+ * - exclude: Defines routes that will not be invoked by Functions. Accepts wildcard behavior. `exclude` always take priority over `include`.
+ *
+ * Wildcards match any number of path segments (slashes). For example, `/users/*` will match everything after the `/users/` path.
+ *
+ */
routes?: {
- /**
- * @deprecated Removed in v10. You will have two options going forward, using auto generated `_route.json` file or provide your own one in `public/_routes.json`. The previous method caused confusion and inconsistencies.
- */
- strategy?: 'auto' | 'include' | 'exclude';
- /**
- * @deprecated Removed in v10. Use `routes.extend.include` instead.
- */
- include?: string[];
- /**
- * @deprecated Removed in v10. Use `routes.extend.exclude` instead.
- */
- exclude?: string[];
+ /** Extend `_routes.json` */
+ extend: {
+ /** Paths which should be routed to the SSR function */
+ include?: {
+ /** Generally this is in pathname format, but does support wildcards, e.g. `/users`, `/products/*` */
+ pattern: string;
+ }[];
+ /** Paths which should be routed as static assets */
+ exclude?: {
+ /** Generally this is in pathname format, but does support wildcards, e.g. `/static`, `/assets/*`, `/images/avatar.jpg` */
+ pattern: string;
+ }[];
+ };
};
/**
- * @deprecated Removed in v10. Configure bindings in `wrangler.toml`. Leveraging Cloudflare's API simplifies setup and ensures full compatibility with Wrangler configurations. Use `platformProxy` instead.
+ * Proxy configuration for the platform.
*/
- runtime?:
- | { mode: 'off' }
- | {
- mode: Extract<RUNTIME, { type: 'pages' }>['mode'];
- type: Extract<RUNTIME, { type: 'pages' }>['type'];
- persistTo?: Extract<RUNTIME, { type: 'pages' }>['persistTo'];
- bindings?: Extract<RUNTIME, { type: 'pages' }>['bindings'];
- }
- | {
- mode: Extract<RUNTIME, { type: 'workers' }>['mode'];
- type: Extract<RUNTIME, { type: 'workers' }>['type'];
- persistTo?: Extract<RUNTIME, { type: 'workers' }>['persistTo'];
- };
+ platformProxy?: {
+ /** Toggle the proxy. Default `undefined`, which equals to `false`. */
+ enabled?: boolean;
+ /** Path to the configuration file. Default `wrangler.toml`. */
+ configPath?: string;
+ /** Enable experimental support for JSON configuration. Default `false`. */
+ experimentalJsonConfig?: boolean;
+ /** Configuration persistence settings. Default '.wrangler/state/v3' */
+ persist?: boolean | { path: string };
+ };
+ /** Enable WebAssembly support */
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 _localRuntime: LocalPagesRuntime | LocalWorkersRuntime;
- let _entryPoints = new Map<RouteData, URL>();
-
- const SERVER_BUILD_FOLDER = '/$server_build/';
-
- const isModeDirectory = args?.mode === 'directory';
- const functionPerRoute = args?.functionPerRoute ?? false;
-
- const runtimeMode = getRuntimeConfig(args?.runtime);
return {
name: '@astrojs/cloudflare',
@@ -90,9 +70,12 @@ export default function createIntegration(args?: Options): AstroIntegration {
'astro:config:setup': ({ command, config, updateConfig, logger }) => {
updateConfig({
build: {
- client: new URL(`.${config.base}`, config.outDir),
- server: new URL(`.${SERVER_BUILD_FOLDER}`, config.outDir),
- serverEntry: '_worker.mjs',
+ client: new URL(
+ `.${prependForwardSlash(appendForwardSlash(config.base))}`,
+ config.outDir
+ ),
+ server: new URL('./_worker.js/', config.outDir),
+ serverEntry: 'index.js',
redirects: false,
},
vite: {
@@ -100,65 +83,65 @@ export default function createIntegration(args?: Options): AstroIntegration {
plugins: [
wasmModuleLoader({
disabled: !args?.wasmModuleImports,
- assetsDirectory: config.build.assets,
}),
],
},
- image: prepareImageConfig(args?.imageService ?? 'DEFAULT', config.image, command, logger),
+ image: setImageConfig(args?.imageService ?? 'DEFAULT', config.image, command, logger),
});
},
'astro:config:done': ({ setAdapter, config }) => {
- setAdapter(getAdapter({ isModeDirectory, functionPerRoute }));
_config = config;
- _buildConfig = config.build;
- if (_config.output === 'static') {
+ 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.'
- );
- }
+ setAdapter({
+ name: '@astrojs/cloudflare',
+ serverEntrypoint: '@astrojs/cloudflare/entrypoints/server.advanced.js',
+ exports: ['default'],
+ adapterFeatures: {
+ functionPerRoute: false,
+ edgeMiddleware: false,
+ },
+ supportedAstroFeatures: {
+ serverOutput: 'stable',
+ hybridOutput: 'stable',
+ staticOutput: 'unsupported',
+ i18nDomains: 'experimental',
+ assets: {
+ supportKind: 'stable',
+ isSharpCompatible: false,
+ isSquooshCompatible: false,
+ },
+ },
+ });
},
- 'astro:server:setup': ({ server, logger }) => {
- if (runtimeMode.mode === 'local') {
- server.middlewares.use(async function middleware(req, res, next) {
- _localRuntime = getLocalRuntime(_config, runtimeMode, logger);
+ 'astro:server:setup': async ({ server }) => {
+ if (args?.platformProxy?.enabled === true) {
+ const platformProxy = await getPlatformProxy({
+ configPath: args.platformProxy.configPath ?? 'wrangler.toml',
+ experimentalJsonConfig: args.platformProxy.experimentalJsonConfig ?? false,
+ persist: args.platformProxy.persist ?? true,
+ });
- const bindings = await _localRuntime.getBindings();
- const secrets = await _localRuntime.getSecrets();
- const caches = await _localRuntime.getCaches();
- const cf = await _localRuntime.getCF();
+ const clientLocalsSymbol = Symbol.for('astro.locals');
- const clientLocalsSymbol = Symbol.for('astro.locals');
+ server.middlewares.use(async function middleware(req, res, next) {
Reflect.set(req, clientLocalsSymbol, {
runtime: {
- env: {
- CF_PAGES_URL: `http://${req.headers.host}`,
- ...bindings,
- ...secrets,
- },
- cf: cf,
- caches: caches,
- waitUntil: (_promise: Promise<any>) => {
- return;
- },
+ env: platformProxy.env,
+ cf: platformProxy.cf,
+ caches: platformProxy.caches,
+ ctx: platformProxy.ctx,
},
});
next();
});
}
},
- 'astro:server:done': async ({ logger }) => {
- if (_localRuntime) {
- logger.info('Cleaning up the local Cloudflare runtime.');
- await _localRuntime.dispose();
- }
- },
'astro:build:setup': ({ vite, target }) => {
if (target === 'server') {
vite.resolve ||= {};
@@ -169,6 +152,14 @@ export default function createIntegration(args?: Options): AstroIntegration {
find: 'react-dom/server',
replacement: 'react-dom/server.browser',
},
+ {
+ find: 'solid-js/web',
+ replacement: 'solid-js/web/dist/server',
+ },
+ {
+ find: 'solid-js',
+ replacement: 'solid-js/dist/server',
+ },
];
if (Array.isArray(vite.resolve.alias)) {
@@ -178,446 +169,116 @@ export default function createIntegration(args?: Options): AstroIntegration {
(vite.resolve.alias as Record<string, string>)[alias.find] = alias.replacement;
}
}
+
vite.ssr ||= {};
vite.ssr.target = 'webworker';
+ vite.ssr.noExternal = true;
+ vite.ssr.external = _config.vite.ssr?.external ?? [];
+
+ vite.build ||= {};
+ vite.build.rollupOptions ||= {};
+ vite.build.rollupOptions.output ||= {};
+ // @ts-expect-error
+ vite.build.rollupOptions.output.banner ||=
+ 'globalThis.process ??= {}; globalThis.process.env ??= {};';
+
+ vite.build.rollupOptions.external = _config.vite.build?.rollupOptions?.external ?? [];
// 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.
+ // in a global way, so we shim their access as `process.env.*`. This is not the recommended way for users to access environment variables. But we'll add this for compatibility for chosen variables. Mainly to support `@astrojs/db`
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 });
- }
-
- if (isModeDirectory && 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 esbuildPlugins = [];
- if (args?.imageService === 'compile') {
- esbuildPlugins.push(patchSharpBundle());
- }
-
- const relativePathToAssets = relative(
- dirname(fileURLToPath(urlWithinFunctions)),
- fileURLToPath(assetsUrl)
- );
- if (args?.wasmModuleImports) {
- esbuildPlugins.push(rewriteWasmImportPath({ relativePathToAssets }));
- }
-
- await esbuild.build({
- target: 'es2022',
- platform: 'browser',
- conditions: ['workerd', 'worker', 'browser'],
- external: [
- 'node:assert',
- 'node:async_hooks',
- 'node:buffer',
- 'node:crypto',
- 'node:diagnostics_channel',
- 'node:events',
- 'node:path',
- 'node:process',
- 'node:stream',
- 'node:string_decoder',
- 'node:util',
- 'cloudflare:*',
- ],
- 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: esbuildPlugins,
- });
- }
-
- 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'));
-
- const esbuildPlugins = [];
- if (args?.imageService === 'compile') {
- esbuildPlugins.push(patchSharpBundle());
- }
-
- if (args?.wasmModuleImports) {
- esbuildPlugins.push(
- rewriteWasmImportPath({
- relativePathToAssets: isModeDirectory
- ? relative(fileURLToPath(functionsUrl), fileURLToPath(assetsUrl))
- : relative(fileURLToPath(_buildConfig.client), fileURLToPath(assetsUrl)),
- })
- );
- }
-
- await esbuild.build({
- target: 'es2022',
- platform: 'browser',
- conditions: ['workerd', 'worker', 'browser'],
- external: [
- 'node:assert',
- 'node:async_hooks',
- 'node:buffer',
- 'node:crypto',
- 'node:diagnostics_channel',
- 'node:events',
- 'node:path',
- 'node:process',
- 'node:stream',
- 'node:string_decoder',
- 'node:util',
- 'cloudflare:*',
- ],
- 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: esbuildPlugins,
- });
-
- // 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'];
-
+ 'astro:build:done': async ({ pages, routes, dir, logger }) => {
+ const PLATFORM_FILES = ['_headers', '_redirects', '_routes.json'];
if (_config.base !== '/') {
- for (const file of cloudflareSpecialFiles) {
+ for (const file of PLATFORM_FILES) {
try {
- await fs.promises.rename(
- new URL(file, _buildConfig.client),
- new URL(file, _config.outDir)
- );
+ await rename(new URL(file, _config.build.client), new URL(file, _config.outDir));
} catch (e) {
- // ignore
+ logger.error(
+ `There was an error moving ${file} to the root of the output directory.`
+ );
}
}
}
- // 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.
- */
- let notFoundIsSSR = false;
- 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) => {
- if (route.component === 'src/pages/404.astro' && route.prerender === false)
- notFoundIsSSR = true;
- 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 (const 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);
+ let redirectsExists = false;
+ try {
+ const redirectsStat = await stat(new URL('./_redirects', _config.outDir));
+ if (redirectsStat.isFile()) {
+ redirectsExists = true;
}
+ } catch (error) {
+ redirectsExists = false;
+ }
- const redirectsExists = await fs.promises
- .stat(new URL('./_redirects', _config.outDir))
- .then((stat) => stat.isFile())
- .catch(() => false);
+ const redirects: RouteData['segments'][] = [];
+ if (redirectsExists) {
+ const rl = createInterface({
+ input: createReadStream(new URL('./_redirects', _config.outDir)),
+ crlfDelay: Number.POSITIVE_INFINITY,
+ });
- // 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;
- }
- // convert /products/:id to /products/*
- return (
- parts[0]
+ for await (const line of rl) {
+ const parts = line.split(' ');
+ if (parts.length >= 2) {
+ const p = removeLeadingForwardSlash(parts[0])
+ .split('/')
+ .filter(Boolean)
+ .map((s: string) => {
+ const syntax = s
.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);
+ .replace(/\?.*$/, '');
+ return getParts(syntax);
+ });
+ redirects.push(p);
}
}
+ }
- 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 = ['/'];
+ let routesExists = false;
+ try {
+ const routesStat = await stat(new URL('./_routes.json', _config.outDir));
+ if (routesStat.isFile()) {
+ routesExists = true;
}
+ } catch (error) {
+ routesExists = false;
+ }
- // Strategy `exclude`: include everything, and then exclude all static paths
- const excludeStrategy =
- strategy === 'include'
- ? undefined
- : {
- include: ['/*'],
- exclude: deduplicatePatterns(staticPathList.concat(args?.routes?.exclude ?? [])),
- };
-
- switch (args?.routes?.strategy) {
- case 'include':
- await fs.promises.writeFile(
- new URL('./_routes.json', _config.outDir),
- JSON.stringify(
- {
- version: 1,
- ...includeStrategy,
- },
- null,
- 2
- )
- );
- break;
-
- case 'exclude':
- await fs.promises.writeFile(
- new URL('./_routes.json', _config.outDir),
- JSON.stringify(
- {
- version: 1,
- ...excludeStrategy,
- },
- null,
- 2
- )
- );
- break;
-
- default:
- {
- const includeStrategyLength = includeStrategy
- ? includeStrategy.include.length + includeStrategy.exclude.length
- : Number.POSITIVE_INFINITY;
+ if (!routesExists) {
+ await createRoutesFile(
+ _config,
+ logger,
+ routes,
+ pages,
+ redirects,
+ args?.routes?.extend?.include,
+ args?.routes?.extend?.exclude
+ );
+ }
- const excludeStrategyLength = excludeStrategy
- ? excludeStrategy.include.length + excludeStrategy.exclude.length
- : Number.POSITIVE_INFINITY;
+ const redirectRoutes: [RouteData, string][] = [];
+ for (const route of routes) {
+ if (route.type === 'redirect') redirectRoutes.push([route, '']);
+ }
- const winningStrategy = notFoundIsSSR
- ? excludeStrategy
- : includeStrategyLength <= excludeStrategyLength
- ? includeStrategy
- : excludeStrategy;
+ const trueRedirects = createRedirectsFromAstroRoutes({
+ config: _config,
+ routeToDynamicTargetMap: new Map(Array.from(redirectRoutes)),
+ dir,
+ });
- await fs.promises.writeFile(
- new URL('./_routes.json', _config.outDir),
- JSON.stringify(
- {
- version: 1,
- ...winningStrategy,
- },
- null,
- 2
- )
- );
- }
- break;
+ if (!trueRedirects.empty()) {
+ try {
+ await appendFile(new URL('./_redirects', _config.outDir), trueRedirects.print());
+ } catch (error) {
+ logger.error('Failed to write _redirects file');
}
}
},
diff --git a/packages/integrations/cloudflare/src/tmp-types.d.ts b/packages/integrations/cloudflare/src/tmp-types.d.ts
deleted file mode 100644
index e9efa7f01..000000000
--- a/packages/integrations/cloudflare/src/tmp-types.d.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-type CF_Json = import('miniflare').Json;
-type CF_Request = import('miniflare').Request;
-type CF_Response = import('miniflare').Response;
-
-interface CF_RawConfig {
- /**
- * The name of your worker. Alphanumeric + dashes only.
- *
- * @inheritable
- */
- name: string | undefined;
- /**
- * These specify any Workers KV Namespaces you want to
- * access from inside your Worker.
- *
- * To learn more about KV Namespaces,
- * see the documentation at https://developers.cloudflare.com/workers/learning/how-kv-works
- *
- * NOTE: This field is not automatically inherited from the top level environment,
- * and so must be specified in every named environment.
- *
- * @default `[]`
- * @nonInheritable
- */
- kv_namespaces: {
- /** The binding name used to refer to the KV Namespace */
- binding: string;
- /** The ID of the KV namespace */
- id: string;
- /** The ID of the KV namespace used during `wrangler dev` */
- preview_id?: string;
- }[];
- /**
- * A map of environment variables to set when deploying your worker.
- *
- * NOTE: This field is not automatically inherited from the top level environment,
- * and so must be specified in every named environment.
- *
- * @default `{}`
- * @nonInheritable
- */
- vars: Record<string, string | CF_Json>;
- /**
- * Specifies D1 databases that are bound to this Worker environment.
- *
- * NOTE: This field is not automatically inherited from the top level environment,
- * and so must be specified in every named environment.
- *
- * @default `[]`
- * @nonInheritable
- */
- d1_databases: {
- /** The binding name used to refer to the D1 database in the worker. */
- binding: string;
- /** The name of this D1 database. */
- database_name: string;
- /** The UUID of this D1 database (not required). */
- database_id: string;
- /** The UUID of this D1 database for Wrangler Dev (if specified). */
- preview_database_id?: string;
- /** The name of the migrations table for this D1 database (defaults to 'd1_migrations'). */
- migrations_table?: string;
- /** The path to the directory of migrations for this D1 database (defaults to './migrations'). */
- migrations_dir?: string;
- /** Internal use only. */
- database_internal_env?: string;
- }[];
- /**
- * Specifies R2 buckets that are bound to this Worker environment.
- *
- * NOTE: This field is not automatically inherited from the top level environment,
- * and so must be specified in every named environment.
- *
- * @default `[]`
- * @nonInheritable
- */
- r2_buckets: {
- /** The binding name used to refer to the R2 bucket in the worker. */
- binding: string;
- /** The name of this R2 bucket at the edge. */
- bucket_name: string;
- /** The preview name of this R2 bucket at the edge. */
- preview_bucket_name?: string;
- /** The jurisdiction that the bucket exists in. Default if not present. */
- jurisdiction?: string;
- }[];
- /**
- * A list of durable objects that your worker should be bound to.
- *
- * For more information about Durable Objects, see the documentation at
- * https://developers.cloudflare.com/workers/learning/using-durable-objects
- *
- * NOTE: This field is not automatically inherited from the top level environment,
- * and so must be specified in every named environment.
- *
- * @default `{bindings:[]}`
- * @nonInheritable
- */
- durable_objects: {
- bindings: {
- /** The name of the binding used to refer to the Durable Object */
- name: string;
- /** The exported class name of the Durable Object */
- class_name: string;
- /** The script where the Durable Object is defined (if it's external to this worker) */
- script_name?: string;
- /** The service environment of the script_name to bind to */
- environment?: string;
- }[];
- };
- /**
- * A list of service bindings that your worker should be bound to.
- *
- * For more information about Service bindings, see the documentation at
- * https://developers.cloudflare.com/workers/configuration/bindings/about-service-bindings/
- *
- * NOTE: This field is not automatically inherited from the top level environment,
- * and so must be specified in every named environment.
- *
- * @default `[]`
- * @nonInheritable
- */
- services: {
- binding: string;
- service: string;
- }[];
-}
-
-interface CF_ServiceDesignator {
- name: string;
- env?: string;
-}
-
-type CF_ServiceFetch = (request: CF_Request) => Promise<CF_Response> | CF_Response;
-
-type CF_File<Contents = string> =
- | { path: string } // `path` resolved relative to cwd
- | { contents: Contents; path?: string }; // `contents` used instead, `path` can be specified if needed e.g. for module resolution
-type CF_BinaryFile = CF_File<Uint8Array>; // Note: Node's `Buffer`s are instances of `Uint8Array`
diff --git a/packages/integrations/cloudflare/src/util.ts b/packages/integrations/cloudflare/src/util.ts
deleted file mode 100644
index 0ca79de20..000000000
--- a/packages/integrations/cloudflare/src/util.ts
+++ /dev/null
@@ -1,16 +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.`
- );
- },
- }
- );
-}
diff --git a/packages/integrations/cloudflare/src/utils/assets.ts b/packages/integrations/cloudflare/src/utils/assets.ts
index edc563c05..d43271e09 100644
--- a/packages/integrations/cloudflare/src/utils/assets.ts
+++ b/packages/integrations/cloudflare/src/utils/assets.ts
@@ -1,11 +1,9 @@
+import { isRemotePath } from '@astrojs/internal-helpers/path';
import type { AstroConfig, ImageMetadata, RemotePattern } from 'astro';
export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
return typeof src === 'object';
}
-export function isRemotePath(src: string) {
- return /^(http|ftp|https|ws):?\/\//.test(src) || src.startsWith('data:');
-}
export function matchHostname(url: URL, hostname?: string, allowWildcard?: boolean) {
if (!hostname) {
return true;
@@ -82,26 +80,3 @@ export function isRemoteAllowed(
export function isString(path: unknown): path is string {
return typeof path === 'string' || path instanceof String;
}
-export function removeTrailingForwardSlash(path: string) {
- return path.endsWith('/') ? path.slice(0, path.length - 1) : path;
-}
-export function removeLeadingForwardSlash(path: string) {
- return path.startsWith('/') ? path.substring(1) : path;
-}
-export function trimSlashes(path: string) {
- return path.replace(/^\/|\/$/g, '');
-}
-export function joinPaths(...paths: (string | undefined)[]) {
- return paths
- .filter(isString)
- .map((path, i) => {
- if (i === 0) {
- return removeTrailingForwardSlash(path);
- }
- if (i === paths.length - 1) {
- return removeLeadingForwardSlash(path);
- }
- return trimSlashes(path);
- })
- .join('/');
-}
diff --git a/packages/integrations/cloudflare/src/utils/deduplicatePatterns.ts b/packages/integrations/cloudflare/src/utils/deduplicatePatterns.ts
deleted file mode 100644
index b408083ba..000000000
--- a/packages/integrations/cloudflare/src/utils/deduplicatePatterns.ts
+++ /dev/null
@@ -1,27 +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
- const uniquePatterns = [...new Set(patterns)];
- for (const pattern of uniquePatterns) {
- if (pattern.endsWith('*')) {
- openPatterns.push(new RegExp(`^${pattern.replace(/(\*\/)*\*$/g, '(?=.{2,}).+[^*\n]$')}`));
- }
- }
-
- return uniquePatterns
- .sort((a, b) => a.length - b.length)
- .filter((pattern) => {
- if (openPatterns.some((p) => p.test(pattern))) return false;
-
- return true;
- });
-}
diff --git a/packages/integrations/cloudflare/src/utils/generate-routes-json.ts b/packages/integrations/cloudflare/src/utils/generate-routes-json.ts
new file mode 100644
index 000000000..39b5e5f83
--- /dev/null
+++ b/packages/integrations/cloudflare/src/utils/generate-routes-json.ts
@@ -0,0 +1,282 @@
+import type { AstroConfig, AstroIntegrationLogger, RouteData, RoutePart } from 'astro';
+
+import { existsSync } from 'node:fs';
+import { writeFile } from 'node:fs/promises';
+import { posix } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import {
+ prependForwardSlash,
+ removeLeadingForwardSlash,
+ removeTrailingForwardSlash,
+} from '@astrojs/internal-helpers/path';
+import glob from 'tiny-glob';
+
+// Copied from https://github.com/withastro/astro/blob/3776ecf0aa9e08a992d3ae76e90682fd04093721/packages/astro/src/core/routing/manifest/create.ts#L45-L70
+// We're not sure how to improve this regex yet
+const ROUTE_DYNAMIC_SPLIT = /\[(.+?\(.+?\)|.+?)\]/;
+const ROUTE_SPREAD = /^\.{3}.+$/;
+export function getParts(part: string) {
+ const result: RoutePart[] = [];
+ part.split(ROUTE_DYNAMIC_SPLIT).map((str, i) => {
+ if (!str) return;
+ const dynamic = i % 2 === 1;
+
+ const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str];
+
+ if (!content || (dynamic && !/^(?:\.\.\.)?[\w$]+$/.test(content))) {
+ throw new Error('Parameter name must match /^[a-zA-Z0-9_$]+$/');
+ }
+
+ result.push({
+ content,
+ dynamic,
+ spread: dynamic && ROUTE_SPREAD.test(content),
+ });
+ });
+
+ return result;
+}
+
+async function writeRoutesFileToOutDir(
+ _config: AstroConfig,
+ logger: AstroIntegrationLogger,
+ include: string[],
+ exclude: string[]
+) {
+ try {
+ await writeFile(
+ new URL('./_routes.json', _config.outDir),
+ JSON.stringify(
+ {
+ version: 1,
+ include: include,
+ exclude: exclude,
+ },
+ null,
+ 2
+ ),
+ 'utf-8'
+ );
+ } catch (error) {
+ logger.error("There was an error writing the '_routes.json' file to the output directory.");
+ }
+}
+
+function segmentsToCfSyntax(segments: RouteData['segments'], _config: AstroConfig) {
+ const pathSegments = [];
+ if (removeLeadingForwardSlash(removeTrailingForwardSlash(_config.base)).length > 0) {
+ pathSegments.push(removeLeadingForwardSlash(removeTrailingForwardSlash(_config.base)));
+ }
+ for (const segment of segments.flat()) {
+ if (segment.dynamic) pathSegments.push('*');
+ else pathSegments.push(segment.content);
+ }
+ return pathSegments;
+}
+
+class TrieNode {
+ children: Map<string, TrieNode> = new Map();
+ isEndOfPath = false;
+ hasWildcardChild = false;
+}
+
+class PathTrie {
+ root: TrieNode;
+
+ constructor() {
+ this.root = new TrieNode();
+ }
+
+ insert(path: string[]) {
+ let node = this.root;
+ for (const segment of path) {
+ if (segment === '*') {
+ node.hasWildcardChild = true;
+ break;
+ }
+ if (!node.children.has(segment)) {
+ node.children.set(segment, new TrieNode());
+ }
+
+ // biome-ignore lint/style/noNonNullAssertion: The `if` condition above ensures that the segment exists inside the map
+ node = node.children.get(segment)!;
+ }
+
+ node.isEndOfPath = true;
+ }
+
+ /**
+ * Depth-first search (dfs), traverses the "graph" segment by segment until the end or wildcard (*).
+ * It makes sure that all necessary paths are returned, but not paths with an existing wildcard prefix.
+ * e.g. if we have a path like /foo/* and /foo/bar, we only want to return /foo/*
+ */
+ private dfs(node: TrieNode, path: string[], allPaths: string[][]): void {
+ if (node.hasWildcardChild) {
+ allPaths.push([...path, '*']);
+ return;
+ }
+
+ if (node.isEndOfPath) {
+ allPaths.push([...path]);
+ }
+
+ for (const [segment, childNode] of node.children) {
+ this.dfs(childNode, [...path, segment], allPaths);
+ }
+ }
+
+ getAllPaths(): string[][] {
+ const allPaths: string[][] = [];
+ this.dfs(this.root, [], allPaths);
+ return allPaths;
+ }
+}
+
+export async function createRoutesFile(
+ _config: AstroConfig,
+ logger: AstroIntegrationLogger,
+ routes: RouteData[],
+ pages: {
+ pathname: string;
+ }[],
+ redirects: RouteData['segments'][],
+ includeExtends:
+ | {
+ pattern: string;
+ }[]
+ | undefined,
+ excludeExtends:
+ | {
+ pattern: string;
+ }[]
+ | undefined
+) {
+ const includePaths: string[][] = [];
+ const excludePaths: string[][] = [];
+
+ let hasPrerendered404 = false;
+ for (const route of routes) {
+ const convertedPath = segmentsToCfSyntax(route.segments, _config);
+ if (route.pathname === '/404' && route.prerender === true) hasPrerendered404 = true;
+
+ switch (route.type) {
+ case 'page':
+ if (route.prerender === false) includePaths.push(convertedPath);
+
+ break;
+
+ case 'endpoint':
+ if (route.prerender === false) includePaths.push(convertedPath);
+ else excludePaths.push(convertedPath);
+
+ break;
+
+ case 'redirect':
+ excludePaths.push(convertedPath);
+
+ break;
+
+ default:
+ /**
+ * We don't know the type, so we are conservative!
+ * Invoking the function on these is a safe-bet because
+ * the function will fallback to static asset fetching
+ */
+ includePaths.push(convertedPath);
+
+ break;
+ }
+ }
+
+ for (const page of pages) {
+ const pageSegments = removeLeadingForwardSlash(page.pathname)
+ .split(posix.sep)
+ .filter(Boolean)
+ .map((s) => {
+ return getParts(s);
+ });
+ excludePaths.push(segmentsToCfSyntax(pageSegments, _config));
+ }
+
+ if (existsSync(fileURLToPath(_config.publicDir))) {
+ const staticFiles = await glob(`${fileURLToPath(_config.publicDir)}/**/*`, {
+ cwd: fileURLToPath(_config.publicDir),
+ filesOnly: true,
+ dot: true,
+ });
+ for (const staticFile of staticFiles) {
+ if (['_headers', '_redirects', '_routes.json'].includes(staticFile)) continue;
+ const staticPath = staticFile;
+
+ const segments = removeLeadingForwardSlash(staticPath)
+ .split(posix.sep)
+ .filter(Boolean)
+ .map((s: string) => {
+ return getParts(s);
+ });
+ excludePaths.push(segmentsToCfSyntax(segments, _config));
+ }
+ }
+
+ /**
+ * All files in the `_config.build.assets` path, e.g. `_astro`
+ * are considered static assets and should not be handled by the function
+ * therefore we exclude a wildcard for that, e.g. `/_astro/*`
+ */
+ const assetsPath = segmentsToCfSyntax(
+ [
+ [{ content: _config.build.assets, dynamic: false, spread: false }],
+ [{ content: '', dynamic: true, spread: false }],
+ ],
+ _config
+ );
+ excludePaths.push(assetsPath);
+
+ for (const redirect of redirects) {
+ excludePaths.push(segmentsToCfSyntax(redirect, _config));
+ }
+
+ const includeTrie = new PathTrie();
+ for (const includePath of includePaths) {
+ includeTrie.insert(includePath);
+ }
+ const deduplicatedIncludePaths = includeTrie.getAllPaths();
+
+ const excludeTrie = new PathTrie();
+ for (const excludePath of excludePaths) {
+ excludeTrie.insert(excludePath);
+ }
+ const deduplicatedExcludePaths = excludeTrie.getAllPaths();
+
+ /**
+ * Cloudflare allows no more than 100 include/exclude rules combined
+ * https://developers.cloudflare.com/pages/functions/routing/#limits
+ */
+ const CLOUDFLARE_COMBINED_LIMIT = 100;
+ if (
+ !hasPrerendered404 ||
+ deduplicatedIncludePaths.length + (includeExtends?.length ?? 0) > CLOUDFLARE_COMBINED_LIMIT ||
+ deduplicatedExcludePaths.length + (excludeExtends?.length ?? 0) > CLOUDFLARE_COMBINED_LIMIT
+ ) {
+ await writeRoutesFileToOutDir(
+ _config,
+ logger,
+ ['/*'].concat(includeExtends?.map((entry) => entry.pattern) ?? []),
+ deduplicatedExcludePaths
+ .map((path) => `${prependForwardSlash(path.join('/'))}`)
+ .concat(excludeExtends?.map((entry) => entry.pattern) ?? [])
+ .slice(0, 99)
+ );
+ } else {
+ await writeRoutesFileToOutDir(
+ _config,
+ logger,
+ deduplicatedIncludePaths
+ .map((path) => `${prependForwardSlash(path.join('/'))}`)
+ .concat(includeExtends?.map((entry) => entry.pattern) ?? []),
+ deduplicatedExcludePaths
+ .map((path) => `${prependForwardSlash(path.join('/'))}`)
+ .concat(excludeExtends?.map((entry) => entry.pattern) ?? [])
+ );
+ }
+}
diff --git a/packages/integrations/cloudflare/src/utils/image-config.ts b/packages/integrations/cloudflare/src/utils/image-config.ts
index d14827c95..4dccea104 100644
--- a/packages/integrations/cloudflare/src/utils/image-config.ts
+++ b/packages/integrations/cloudflare/src/utils/image-config.ts
@@ -1,7 +1,7 @@
import type { AstroConfig, AstroIntegrationLogger } from 'astro';
import { passthroughImageService, sharpImageService } from 'astro/config';
-export function prepareImageConfig(
+export function setImageConfig(
service: string,
config: AstroConfig['image'],
command: 'dev' | 'build' | 'preview',
diff --git a/packages/integrations/cloudflare/src/utils/local-runtime.ts b/packages/integrations/cloudflare/src/utils/local-runtime.ts
deleted file mode 100644
index 8318fb1e3..000000000
--- a/packages/integrations/cloudflare/src/utils/local-runtime.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-import type {
- D1Database,
- DurableObjectNamespace,
- IncomingRequestCfProperties,
- KVNamespace,
- R2Bucket,
-} from '@cloudflare/workers-types/experimental';
-import type { AstroConfig, AstroIntegrationLogger } from 'astro';
-import type { ExternalServer, Json, ReplaceWorkersTypes, WorkerOptions } from 'miniflare';
-import type { Options } from '../index.js';
-
-import assert from 'node:assert';
-import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import TOML from '@iarna/toml';
-import { AstroError } from 'astro/errors';
-import dotenv from 'dotenv';
-import { Miniflare } from 'miniflare';
-
-interface NodeJSError extends Error {
- code?: string;
-}
-
-type BASE_RUNTIME = {
- mode: 'local';
- type: 'pages' | 'workers';
- persistTo: string;
- bindings: Record<
- string,
- | { type: 'var'; value: string | Json }
- | { type: 'kv' }
- | { type: 'r2' }
- | {
- type: 'd1';
- }
- | {
- type: 'durable-object';
- className: string;
- service?: {
- name: string;
- env?: string;
- };
- }
- | ({
- type: 'service';
- } & (
- | {
- address: string;
- protocol?: 'http' | 'https';
- }
- | {
- service: string;
- }
- ))
- >;
-};
-
-export type RUNTIME =
- | {
- mode: BASE_RUNTIME['mode'];
- type: 'pages';
- persistTo: BASE_RUNTIME['persistTo'];
- bindings: BASE_RUNTIME['bindings'];
- }
- | {
- mode: BASE_RUNTIME['mode'];
- type: 'workers';
- persistTo: BASE_RUNTIME['persistTo'];
- };
-
-class LocalRuntime {
- private _astroConfig: AstroConfig;
- private _logger: AstroIntegrationLogger;
- private _miniflare: Miniflare;
-
- private miniflareBindings:
- | Record<
- string,
- | D1Database
- | ReplaceWorkersTypes<R2Bucket>
- | ReplaceWorkersTypes<KVNamespace>
- | ReplaceWorkersTypes<DurableObjectNamespace>
- | Json
- >
- | undefined;
- private secrets: Record<string, string> | undefined;
- private cfObject: IncomingRequestCfProperties | undefined;
-
- public constructor(
- astroConfig: AstroConfig,
- runtimeConfig: BASE_RUNTIME,
- logger: AstroIntegrationLogger
- ) {
- this._astroConfig = astroConfig;
- this._logger = logger;
-
- const varBindings: Required<Pick<WorkerOptions, 'bindings'>>['bindings'] = {};
- const kvBindings: Required<Pick<WorkerOptions, 'kvNamespaces'>>['kvNamespaces'] = [];
- const d1Bindings: Required<Pick<WorkerOptions, 'd1Databases'>>['d1Databases'] = [];
- const r2Bindings: Required<Pick<WorkerOptions, 'r2Buckets'>>['r2Buckets'] = [];
- const durableObjectBindings: Required<Pick<WorkerOptions, 'durableObjects'>>['durableObjects'] =
- {};
- const serviceBindings: Required<Pick<WorkerOptions, 'serviceBindings'>>['serviceBindings'] = {};
-
- for (const bindingName in runtimeConfig.bindings) {
- const bindingData = runtimeConfig.bindings[bindingName];
- switch (bindingData.type) {
- case 'var':
- varBindings[bindingName] = bindingData.value;
- break;
- case 'kv':
- kvBindings.push(bindingName);
- break;
- case 'd1':
- d1Bindings.push(bindingName);
- break;
- case 'r2':
- r2Bindings.push(bindingName);
- break;
- case 'durable-object':
- durableObjectBindings[bindingName] = {
- className: bindingData.className,
- scriptName: bindingData.service?.name,
- };
- break;
- case 'service':
- if ('address' in bindingData) {
- // Pages mode
- const isHttps = bindingData.protocol === 'https';
-
- const serviceBindingConfig: ExternalServer = isHttps
- ? { address: bindingData.address, https: {} }
- : { address: bindingData.address, http: {} };
-
- serviceBindings[bindingName] = {
- external: serviceBindingConfig,
- };
- } else if ('service' in bindingData) {
- // Worker mode
- serviceBindings[bindingName] = bindingData.service;
- }
- break;
- }
- }
-
- this._miniflare = new Miniflare({
- cachePersist: `${runtimeConfig.persistTo}/cache`,
- d1Persist: `${runtimeConfig.persistTo}/d1`,
- r2Persist: `${runtimeConfig.persistTo}/r2`,
- kvPersist: `${runtimeConfig.persistTo}/kv`,
- durableObjectsPersist: `${runtimeConfig.persistTo}/do`,
- workers: [
- {
- name: 'worker',
- script: '',
- modules: true,
- cacheWarnUsage: true,
- cache: true,
- bindings: varBindings,
- d1Databases: d1Bindings,
- r2Buckets: r2Bindings,
- kvNamespaces: kvBindings,
- durableObjects: durableObjectBindings,
- serviceBindings: serviceBindings,
- },
- ],
- });
- }
-
- public async getBindings() {
- await this._miniflare.ready;
- if (!this.miniflareBindings) {
- this.miniflareBindings = await this._miniflare.getBindings();
- }
- return this.miniflareBindings;
- }
-
- public async getSecrets() {
- await this._miniflare.ready;
- if (!this.secrets) {
- try {
- this.secrets = dotenv.parse(
- readFileSync(fileURLToPath(new URL('./.dev.vars', this._astroConfig.root)))
- );
- } catch (error) {
- const e = error as NodeJSError;
- if (e.code === 'ENOENT') {
- this._logger.info(
- 'There is no `.dev.vars` file in the root directory, if you have encrypted secrets or environmental variables Cloudflare recommends you to put them in this file'
- );
- this.secrets = {};
- } else {
- throw new AstroError('Failed to load secrets file', e.message);
- }
- }
- }
- return this.secrets;
- }
-
- public async getCaches() {
- await this._miniflare.ready;
- return this._miniflare.getCaches();
- }
-
- public async getCF() {
- await this._miniflare.ready;
-
- const MAX_CACHE_AGE = 1000 * 60 * 60 * 24 * 7; // 7 days;
- // Try load cached cfObject, if this fails, we'll catch the error and refetch.
- // If this succeeds, and the file is stale, that's fine: it's very likely
- // we'll be fetching the same data anyways.
- try {
- const cachedCFObject = JSON.parse(
- readFileSync(fileURLToPath(new URL('cf.json', this._astroConfig.cacheDir)), 'utf8')
- );
- const cfObjectStats = statSync(fileURLToPath(new URL('cf.json', this._astroConfig.cacheDir)));
- assert(Date.now() - cfObjectStats.mtimeMs <= MAX_CACHE_AGE);
- this.cfObject = cachedCFObject;
- } catch {}
-
- const CF_ENDPOINT = 'https://workers.cloudflare.com/cf.json';
- if (!this.cfObject) {
- this.cfObject = await fetch(CF_ENDPOINT).then((res) => res.json());
- if (!existsSync(this._astroConfig.cacheDir)) {
- mkdirSync(this._astroConfig.cacheDir);
- }
- writeFileSync(
- fileURLToPath(new URL('cf.json', this._astroConfig.cacheDir)),
- JSON.stringify(this.cfObject),
- 'utf8'
- );
- }
- return this.cfObject;
- }
-
- public async dispose() {
- await this._miniflare.dispose();
- }
-}
-
-export class LocalWorkersRuntime extends LocalRuntime {
- constructor(
- astroConfig: AstroConfig,
- runtimeConfig: Extract<RUNTIME, { type: 'workers' }>,
- logger: AstroIntegrationLogger
- ) {
- let _wranglerConfig: CF_RawConfig | undefined;
- try {
- _wranglerConfig = TOML.parse(
- readFileSync(fileURLToPath(new URL('./wrangler.toml', astroConfig.root)), 'utf-8').replace(
- /\r\n/g,
- '\n'
- )
- ) as unknown as CF_RawConfig;
- } catch (error) {
- const e = error as NodeJSError;
- if (e.code === 'ENOENT') {
- logger.error('Missing file `wrangler.toml in root directory`');
- } else {
- throw new AstroError('Failed to load wrangler config', e.message);
- }
- }
- const runtimeConfigWithWrangler: BASE_RUNTIME = {
- ...runtimeConfig,
- bindings: {},
- };
- if (_wranglerConfig?.vars) {
- for (const key in _wranglerConfig.vars) {
- runtimeConfigWithWrangler.bindings[key] = {
- type: 'var',
- value: _wranglerConfig.vars[key],
- };
- }
- }
- if (_wranglerConfig?.kv_namespaces) {
- for (const ns of _wranglerConfig.kv_namespaces) {
- runtimeConfigWithWrangler.bindings[ns.binding] = {
- type: 'kv',
- };
- }
- }
- if (_wranglerConfig?.d1_databases) {
- for (const db of _wranglerConfig.d1_databases) {
- runtimeConfigWithWrangler.bindings[db.binding] = {
- type: 'd1',
- };
- }
- }
- if (_wranglerConfig?.r2_buckets) {
- for (const bucket of _wranglerConfig.r2_buckets) {
- runtimeConfigWithWrangler.bindings[bucket.binding] = {
- type: 'r2',
- };
- }
- }
- if (_wranglerConfig?.durable_objects) {
- for (const durableObject of _wranglerConfig.durable_objects.bindings) {
- runtimeConfigWithWrangler.bindings[durableObject.name] = {
- type: 'durable-object',
- className: durableObject.class_name,
- service: durableObject.script_name
- ? {
- name: durableObject.script_name,
- }
- : undefined,
- };
- }
- }
- if (_wranglerConfig?.services) {
- for (const service of _wranglerConfig.services) {
- runtimeConfigWithWrangler.bindings[service.binding] = {
- type: 'service',
- service: service.service,
- };
- }
- }
-
- super(astroConfig, runtimeConfigWithWrangler, logger);
- }
-}
-
-export class LocalPagesRuntime extends LocalRuntime {
- // biome-ignore lint/complexity/noUselessConstructor: not types information yet, so we need to disable the rule for the time being
- public constructor(
- astroConfig: AstroConfig,
- runtimeConfig: Extract<RUNTIME, { type: 'pages' }>,
- logger: AstroIntegrationLogger
- ) {
- super(astroConfig, runtimeConfig, logger);
- }
-}
-
-let localRuntime: LocalPagesRuntime | LocalWorkersRuntime | undefined;
-export function getLocalRuntime(
- astroConfig: AstroConfig,
- runtimeConfig: RUNTIME,
- logger: AstroIntegrationLogger
-): LocalPagesRuntime | LocalWorkersRuntime {
- if (localRuntime) return localRuntime;
-
- if (runtimeConfig.type === 'pages') {
- localRuntime = new LocalPagesRuntime(astroConfig, runtimeConfig, logger);
- } else {
- localRuntime = new LocalWorkersRuntime(astroConfig, runtimeConfig, logger);
- }
-
- return localRuntime;
-}
-
-export function getRuntimeConfig(userConfig?: Options['runtime']): { mode: 'off' } | RUNTIME {
- if (!userConfig || userConfig.mode === 'off') return { mode: 'off' };
-
- // we know that we have `mode: local` below
- if (userConfig.type === 'pages')
- return {
- mode: 'local',
- type: 'pages',
- persistTo: userConfig.persistTo ?? '.wrangler/state/v3',
- bindings: userConfig.bindings ?? {},
- };
-
- // we know that we have `type: workers` below
- return {
- mode: 'local',
- type: 'workers',
- persistTo: userConfig.persistTo ?? '.wrangler/state/v3',
- };
-}
diff --git a/packages/integrations/cloudflare/src/utils/parser.ts b/packages/integrations/cloudflare/src/utils/parser.ts
deleted file mode 100644
index 0bae2c673..000000000
--- a/packages/integrations/cloudflare/src/utils/parser.ts
+++ /dev/null
@@ -1,192 +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 * as fs from 'node:fs';
-import { dirname, resolve } from 'node:path';
-import type {} from '@cloudflare/workers-types/experimental';
-import TOML from '@iarna/toml';
-import dotenv from 'dotenv';
-import { findUpSync } from 'find-up';
-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,
- };
- }
- return config.vars;
-}
-
-function parseConfig() {
- if (_wrangler) return _wrangler;
- // biome-ignore lint/suspicious/noImplicitAnyLet: correct usage
- 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 }
- >;
- const bindings = rawConfig.durable_objects.bindings;
- for (const binding of bindings) {
- Reflect.set(output, binding.name, { className: binding.class_name });
- }
- return output;
-}
diff --git a/packages/integrations/cloudflare/src/utils/prependForwardSlash.ts b/packages/integrations/cloudflare/src/utils/prependForwardSlash.ts
deleted file mode 100644
index a034c1268..000000000
--- a/packages/integrations/cloudflare/src/utils/prependForwardSlash.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function prependForwardSlash(path: string) {
- return path[0] === '/' ? path : `/${path}`;
-}
diff --git a/packages/integrations/cloudflare/src/utils/rewriteWasmImportPath.ts b/packages/integrations/cloudflare/src/utils/rewriteWasmImportPath.ts
deleted file mode 100644
index c266f19b1..000000000
--- a/packages/integrations/cloudflare/src/utils/rewriteWasmImportPath.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { basename } from 'node:path';
-import type esbuild from 'esbuild';
-
-/**
- *
- * @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
- };
- });
- },
- };
-}
diff --git a/packages/integrations/cloudflare/src/utils/sharpBundlePatch.ts b/packages/integrations/cloudflare/src/utils/sharpBundlePatch.ts
deleted file mode 100644
index ea9015be2..000000000
--- a/packages/integrations/cloudflare/src/utils/sharpBundlePatch.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type esbuild from 'esbuild';
-
-export function patchSharpBundle(): esbuild.Plugin {
- return {
- name: 'sharp-patch',
- setup(build) {
- build.onResolve({ filter: /^sharp/ }, (args) => ({
- path: args.path,
- namespace: 'sharp-ns',
- }));
-
- build.onLoad({ filter: /.*/, namespace: 'sharp-ns' }, (a) => {
- return {
- contents: JSON.stringify(''),
- loader: 'json',
- };
- });
- },
- };
-}
diff --git a/packages/integrations/cloudflare/src/utils/wasm-module-loader.ts b/packages/integrations/cloudflare/src/utils/wasm-module-loader.ts
index c186d2648..dd61f501a 100644
--- a/packages/integrations/cloudflare/src/utils/wasm-module-loader.ts
+++ b/packages/integrations/cloudflare/src/utils/wasm-module-loader.ts
@@ -13,10 +13,8 @@ import type { AstroConfig } from 'astro';
*/
export function wasmModuleLoader({
disabled,
- assetsDirectory,
}: {
disabled: boolean;
- assetsDirectory: string;
}): NonNullable<AstroConfig['vite']['plugins']>[number] {
const postfix = '.wasm?module';
let isDev = false;
@@ -31,7 +29,12 @@ export function wasmModuleLoader({
// 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 } },
+ build: {
+ rollupOptions: {
+ // mark the wasm files as external so that they are not bundled and instead are loaded from the files
+ external: [/^__WASM_ASSET__.+\.wasm$/i, /^__WASM_ASSET__.+\.wasm.mjs$/i],
+ },
+ },
};
},
@@ -50,10 +53,8 @@ export function wasmModuleLoader({
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
-`;
+ 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;
@@ -68,8 +69,8 @@ export default wasmModule
// 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),
+ fileName: assetName,
+ source: data,
});
// however, by default, the SSG generator cannot import the .wasm as a module, so embed as a base64 string
@@ -79,10 +80,7 @@ export default wasmModule
code: base64Module,
});
- return `
-import wasmModule from "__WASM_ASSET__${chunkId}.wasm.mjs";
-export default wasmModule;
- `;
+ return `import wasmModule from "__WASM_ASSET__${chunkId}.wasm.mjs";export default wasmModule;`;
},
// output original wasm file relative to the chunk
@@ -91,13 +89,33 @@ export default wasmModule;
if (!/__WASM_ASSET__/g.test(code)) return;
- const final = code.replaceAll(/__WASM_ASSET__([A-Za-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}`;
- });
+ const isPrerendered = Object.keys(chunk.modules).some(
+ (moduleId) => this.getModuleInfo(moduleId)?.meta?.astro?.pageOptions?.prerender === true
+ );
+
+ let final = code;
+
+ // SSR
+ if (!isPrerendered) {
+ final = code.replaceAll(/__WASM_ASSET__([A-Za-z\d]+).wasm.mjs/g, (s, assetId) => {
+ const fileName = this.getFileName(assetId).replace(/\.mjs$/, '');
+ const relativePath = path
+ .relative(path.dirname(chunk.fileName), fileName)
+ .replaceAll('\\', '/'); // fix windows paths for import
+ return `./${relativePath}`;
+ });
+ }
+
+ // SSG
+ if (isPrerendered) {
+ final = code.replaceAll(/__WASM_ASSET__([A-Za-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 };
},