summaryrefslogtreecommitdiff
path: root/packages/integrations/cloudflare/src/index.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/cloudflare/src/index.ts')
-rw-r--r--packages/integrations/cloudflare/src/index.ts615
1 files changed, 0 insertions, 615 deletions
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
deleted file mode 100644
index 36da696bb..000000000
--- a/packages/integrations/cloudflare/src/index.ts
+++ /dev/null
@@ -1,615 +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: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: !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: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: !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
- )
- );
- }
- },
- },
- };
-}