2023-06-05 08:05:47 -05:00
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects' ;
2023-06-30 04:09:21 -05:00
import type { AstroAdapter , AstroConfig , AstroIntegration , RouteData } from 'astro' ;
2022-06-16 09:12:25 -05:00
import esbuild from 'esbuild' ;
2023-07-17 19:17:59 -05:00
import * as fs from 'node:fs' ;
import * as os from 'node:os' ;
import { sep } from 'node:path' ;
import { fileURLToPath , pathToFileURL } from 'node:url' ;
2022-11-21 08:31:21 -05:00
import glob from 'tiny-glob' ;
2022-06-16 09:12:25 -05:00
2022-08-08 12:10:48 -05:00
type Options = {
mode : 'directory' | 'advanced' ;
} ;
2022-10-12 16:25:51 -05:00
interface BuildConfig {
server : URL ;
client : URL ;
serverEntry : string ;
2023-06-30 04:09:21 -05:00
split? : boolean ;
2022-10-12 16:25:51 -05:00
}
2022-08-08 12:10:48 -05:00
export function getAdapter ( isModeDirectory : boolean ) : AstroAdapter {
return isModeDirectory
? {
2023-07-17 08:15:32 -05:00
name : '@astrojs/cloudflare' ,
serverEntrypoint : '@astrojs/cloudflare/server.directory.js' ,
exports : [ 'onRequest' , 'manifest' ] ,
2023-07-28 06:15:09 -05:00
supportedAstroFeatures : {
hybridOutput : 'stable' ,
staticOutput : 'unsupported' ,
serverOutput : 'stable' ,
assets : {
supportKind : 'unsupported' ,
isSharpCompatible : false ,
isSquooshCompatible : false ,
} ,
} ,
2023-07-17 08:15:32 -05:00
}
2022-08-08 12:10:48 -05:00
: {
2023-07-17 08:15:32 -05:00
name : '@astrojs/cloudflare' ,
serverEntrypoint : '@astrojs/cloudflare/server.advanced.js' ,
exports : [ 'default' ] ,
2023-07-28 06:15:09 -05:00
supportedAstroFeatures : {
hybridOutput : 'stable' ,
staticOutput : 'unsupported' ,
serverOutput : 'stable' ,
assets : {
supportKind : 'stable' ,
isSharpCompatible : false ,
isSquooshCompatible : false ,
} ,
} ,
2023-07-17 08:15:32 -05:00
} ;
2022-06-16 09:12:25 -05:00
}
2022-07-27 15:15:19 -05:00
const SHIM = ` globalThis.process = {
argv : [ ] ,
env : { } ,
} ; ` ;
2022-11-21 08:31:21 -05:00
const SERVER_BUILD_FOLDER = '/$server_build/' ;
2023-08-09 22:04:09 -05:00
/ * *
* These route types are candiates for being part of the ` _routes.json ` ` include ` array .
* /
const potentialFunctionRouteTypes = [ 'endpoint' , 'page' ] ;
2022-08-08 12:10:48 -05:00
export default function createIntegration ( args? : Options ) : AstroIntegration {
2022-06-16 09:12:25 -05:00
let _config : AstroConfig ;
let _buildConfig : BuildConfig ;
2022-08-08 12:10:48 -05:00
const isModeDirectory = args ? . mode === 'directory' ;
2023-06-30 04:09:21 -05:00
let _entryPoints = new Map < RouteData , URL > ( ) ;
2022-06-16 09:12:25 -05:00
return {
name : '@astrojs/cloudflare' ,
hooks : {
2022-10-12 16:25:51 -05:00
'astro:config:setup' : ( { config , updateConfig } ) = > {
updateConfig ( {
build : {
2022-11-21 08:31:21 -05:00
client : new URL ( ` . ${ config . base } ` , config . outDir ) ,
server : new URL ( ` . ${ SERVER_BUILD_FOLDER } ` , config . outDir ) ,
2023-01-26 12:43:39 -05:00
serverEntry : '_worker.mjs' ,
2023-06-05 08:03:20 -05:00
redirects : false ,
2022-10-12 16:27:56 -05:00
} ,
2022-10-12 16:25:51 -05:00
} ) ;
} ,
2022-06-16 09:12:25 -05:00
'astro:config:done' : ( { setAdapter , config } ) = > {
2022-08-08 12:10:48 -05:00
setAdapter ( getAdapter ( isModeDirectory ) ) ;
2022-06-16 09:12:25 -05:00
_config = config ;
2022-10-12 16:25:51 -05:00
_buildConfig = config . build ;
2022-07-24 23:18:02 -05:00
2022-07-24 23:20:38 -05:00
if ( config . output === 'static' ) {
2022-07-27 10:50:48 -05:00
throw new Error ( `
2023-05-17 08:23:20 -05:00
[ @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.
2022-07-27 10:50:48 -05:00
` );
2022-07-24 23:18:02 -05:00
}
2022-11-21 08:31:21 -05:00
if ( config . base === SERVER_BUILD_FOLDER ) {
throw new Error ( `
[ @astrojs / cloudflare ] \ ` base: " ${ SERVER_BUILD_FOLDER } " \` is not allowed. Please change your \` base \` config to something else. ` ) ;
}
2022-06-16 09:12:25 -05:00
} ,
'astro:build:setup' : ( { vite , target } ) = > {
if ( target === 'server' ) {
2023-05-17 02:44:20 -05:00
vite . resolve || = { } ;
vite . resolve . alias || = { } ;
2022-06-16 09:12:25 -05:00
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 ;
}
}
2023-05-17 02:44:20 -05:00
vite . ssr || = { } ;
vite . ssr . target = 'webworker' ;
2023-07-17 07:57:08 -05:00
// 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 ,
} ;
2022-06-16 09:12:25 -05:00
}
} ,
2023-07-03 07:59:43 -05:00
'astro:build:ssr' : ( { entryPoints } ) = > {
2023-06-30 04:09:21 -05:00
_entryPoints = entryPoints ;
} ,
2023-06-05 08:03:20 -05:00
'astro:build:done' : async ( { pages , routes , dir } ) = > {
2023-06-30 04:09:21 -05:00
const functionsUrl = new URL ( 'functions/' , _config . root ) ;
if ( isModeDirectory ) {
await fs . promises . mkdir ( functionsUrl , { recursive : true } ) ;
}
if ( isModeDirectory && _buildConfig . split ) {
2023-06-30 04:12:30 -05:00
const entryPointsURL = [ . . . _entryPoints . values ( ) ] ;
2023-06-30 04:09:21 -05:00
const entryPaths = entryPointsURL . map ( ( entry ) = > fileURLToPath ( entry ) ) ;
2023-07-17 08:15:32 -05:00
const outputUrl = new URL ( '$astro' , _buildConfig . server ) ;
2023-07-17 08:12:41 -05:00
const outputDir = fileURLToPath ( outputUrl ) ;
2023-06-30 04:09:21 -05:00
2023-07-17 08:12:41 -05:00
await esbuild . build ( {
2023-06-30 04:09:21 -05:00
target : 'es2020' ,
platform : 'browser' ,
conditions : [ 'workerd' , 'worker' , 'browser' ] ,
entryPoints : entryPaths ,
outdir : outputDir ,
allowOverwrite : true ,
format : 'esm' ,
bundle : true ,
minify : _config.vite?.build?.minify !== false ,
banner : {
js : SHIM ,
} ,
logOverride : {
'ignored-bare-import' : 'silent' ,
} ,
} ) ;
2022-06-16 09:12:25 -05:00
2023-07-17 08:15:32 -05:00
const outputFiles : Array < string > = await glob ( ` **/* ` , {
cwd : outputDir ,
filesOnly : true ,
} ) ;
2023-07-17 08:12:41 -05:00
// 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 ) ;
2023-07-17 08:15:32 -05:00
const finalSegments = path . map ( ( segment ) = >
segment
. replace ( /(\_)(\w+)(\_)/g , ( _ , __ , prop ) = > {
return ` [ ${ prop } ] ` ;
} )
. replace ( /(\_\-\-\-)(\w+)(\_)/g , ( _ , __ , prop ) = > {
return ` [[ ${ prop } ]] ` ;
} )
2023-07-17 08:12:41 -05:00
) ;
finalSegments [ finalSegments . length - 1 ] = finalSegments [ finalSegments . length - 1 ]
. replace ( 'entry.' , '' )
. replace ( /(.*)\.(\w+)\.(\w+)$/g , ( _ , fileName , __ , newExt ) = > {
return ` ${ fileName } . ${ newExt } ` ;
2023-07-17 08:15:32 -05:00
} ) ;
2023-07-17 08:12:41 -05:00
const finalDirPath = finalSegments . slice ( 0 , - 1 ) . join ( sep ) ;
const finalPath = finalSegments . join ( sep ) ;
const newDirUrl = new URL ( finalDirPath , functionsUrl ) ;
2023-07-17 08:15:32 -05:00
await fs . promises . mkdir ( newDirUrl , { recursive : true } ) ;
2023-07-17 08:12:41 -05:00
const oldFileUrl = new URL ( ` $ astro/ ${ outputFile } ` , outputUrl ) ;
const newFileUrl = new URL ( finalPath , functionsUrl ) ;
await fs . promises . rename ( oldFileUrl , newFileUrl ) ;
2023-06-30 04:09:21 -05:00
}
} 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 : 'es2020' ,
platform : 'browser' ,
conditions : [ 'workerd' , 'worker' , 'browser' ] ,
entryPoints : [ entryPath ] ,
outfile : buildPath ,
allowOverwrite : true ,
format : 'esm' ,
bundle : true ,
minify : _config.vite?.build?.minify !== false ,
banner : {
js : SHIM ,
} ,
logOverride : {
'ignored-bare-import' : 'silent' ,
} ,
} ) ;
// 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
2022-11-21 08:31:21 -05:00
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
}
}
}
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
// (without calling the function)
if ( ! routesExists ) {
2023-08-09 22:04:09 -05:00
const functionEndpoints = routes
// Certain route types, when their prerender option is set to false, a 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 ,
} ;
} ) ;
2022-11-21 08:31:21 -05:00
const staticPathList : Array < string > = (
await glob ( ` ${ fileURLToPath ( _buildConfig . client ) } /**/* ` , {
cwd : fileURLToPath ( _config . outDir ) ,
filesOnly : true ,
} )
)
. filter ( ( file : string ) = > cloudflareSpecialFiles . indexOf ( file ) < 0 )
2023-08-09 22:04:09 -05:00
. map ( ( file : string ) = > ` / ${ file . replace ( /\\/g , '/' ) } ` ) ;
2022-11-21 08:31:21 -05:00
2023-01-26 12:45:39 -05:00
for ( let page of pages ) {
2023-03-13 09:58:21 -05:00
let pagePath = prependForwardSlash ( page . pathname ) ;
if ( _config . base !== '/' ) {
2023-03-16 09:42:36 -05:00
const base = _config . base . endsWith ( '/' ) ? _config . base . slice ( 0 , - 1 ) : _config . base ;
2023-03-13 09:58:21 -05:00
pagePath = ` ${ base } ${ pagePath } ` ;
}
staticPathList . push ( pagePath ) ;
2023-01-26 12:43:39 -05:00
}
2022-11-21 08:31:21 -05:00
const redirectsExists = await fs . promises
. stat ( new URL ( './_redirects' , _config . outDir ) )
. then ( ( stat ) = > stat . isFile ( ) )
. catch ( ( ) = > false ) ;
2022-11-21 08:33:45 -05:00
// convert all redirect source paths into a list of routes
// and add them to the static path
2022-11-21 08:31:21 -05:00
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 ) ;
}
}
2023-07-13 03:21:33 -05:00
const redirectRoutes : [ RouteData , string ] [ ] = routes
. filter ( ( r ) = > r . type === 'redirect' )
. map ( ( r ) = > {
return [ r , '' ] ;
} ) ;
2023-06-05 08:03:20 -05:00
const trueRedirects = createRedirectsFromAstroRoutes ( {
config : _config ,
2023-07-13 03:21:33 -05:00
routeToDynamicTargetMap : new Map ( Array . from ( redirectRoutes ) ) ,
2023-06-05 08:03:20 -05:00
dir ,
} ) ;
2023-06-05 08:05:47 -05:00
if ( ! trueRedirects . empty ( ) ) {
2023-06-05 08:03:20 -05:00
await fs . promises . appendFile (
new URL ( './_redirects' , _config . outDir ) ,
trueRedirects . print ( )
) ;
}
2023-08-09 22:04:09 -05:00
staticPathList . push ( . . . routes . filter ( ( r ) = > r . type === 'redirect' ) . map ( ( r ) = > r . route ) ) ;
// In order to product the shortest list of patterns, we first try to
// include all function endpoints, and then exclude all static paths
let include = deduplicatePatterns (
functionEndpoints . map ( ( endpoint ) = > endpoint . includePattern )
) ;
let exclude = deduplicatePatterns (
staticPathList . filter ( ( file : string ) = >
functionEndpoints . some ( ( endpoint ) = > endpoint . regexp . test ( file ) )
)
) ;
// 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 ( include . length === 0 ) {
include = [ '/' ] ;
exclude = [ '/' ] ;
}
// If using only an exclude list would produce a shorter list of patterns,
// we use that instead
if ( include . length + exclude . length > staticPathList . length ) {
include = [ '/*' ] ;
exclude = deduplicatePatterns ( staticPathList ) ;
}
2022-11-21 08:31:21 -05:00
await fs . promises . writeFile (
new URL ( './_routes.json' , _config . outDir ) ,
JSON . stringify (
{
version : 1 ,
2023-08-09 22:04:09 -05:00
include ,
exclude ,
2022-11-21 08:31:21 -05:00
} ,
null ,
2
)
) ;
}
2022-06-16 09:12:25 -05:00
} ,
} ,
} ;
}
2023-01-26 12:43:39 -05:00
function prependForwardSlash ( path : string ) {
return path [ 0 ] === '/' ? path : '/' + path ;
}
2023-08-09 22:04:09 -05:00
/ * *
* 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
* /
function deduplicatePatterns ( patterns : string [ ] ) {
const openPatterns : RegExp [ ] = [ ] ;
return [ . . . new Set ( patterns ) ]
. sort ( ( a , b ) = > a . length - b . length )
. filter ( ( pattern ) = > {
if ( openPatterns . some ( ( p ) = > p . test ( pattern ) ) ) {
return false ;
}
if ( pattern . endsWith ( '*' ) ) {
openPatterns . push ( new RegExp ( ` ^ ${ pattern . replace ( /(\*\/)*\*$/g , '.*' ) } ` ) ) ;
}
return true ;
} ) ;
}