2023-06-05 13:05:47 +00:00
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects' ;
2023-06-30 09:09:21 +00:00
import type { AstroAdapter , AstroConfig , AstroIntegration , RouteData } from 'astro' ;
2022-06-16 14:12:25 +00:00
import esbuild from 'esbuild' ;
import * as fs from 'fs' ;
2022-11-21 13:31:21 +00:00
import * as os from 'os' ;
2023-06-30 09:09:21 +00:00
import { dirname } from 'path' ;
2022-11-21 13:31:21 +00:00
import glob from 'tiny-glob' ;
2023-01-30 19:50:44 +00:00
import { fileURLToPath , pathToFileURL } from 'url' ;
2022-06-16 14:12:25 +00:00
2022-08-08 17:10:48 +00:00
type Options = {
mode : 'directory' | 'advanced' ;
} ;
2022-10-12 21:25:51 +00:00
interface BuildConfig {
server : URL ;
client : URL ;
serverEntry : string ;
2023-06-30 09:09:21 +00:00
split? : boolean ;
2022-10-12 21:25:51 +00:00
}
2022-08-08 17:10:48 +00:00
export function getAdapter ( isModeDirectory : boolean ) : AstroAdapter {
return isModeDirectory
? {
2023-06-30 09:12:30 +00:00
name : '@astrojs/cloudflare' ,
serverEntrypoint : '@astrojs/cloudflare/server.directory.js' ,
exports : [ 'onRequest' , 'manifest' ] ,
}
2022-08-08 17:10:48 +00:00
: {
2023-06-30 09:12:30 +00:00
name : '@astrojs/cloudflare' ,
serverEntrypoint : '@astrojs/cloudflare/server.advanced.js' ,
exports : [ 'default' ] ,
} ;
2022-06-16 14:12:25 +00:00
}
2022-07-27 20:15:19 +00:00
const SHIM = ` globalThis.process = {
argv : [ ] ,
env : { } ,
} ; ` ;
2022-11-21 13:31:21 +00:00
const SERVER_BUILD_FOLDER = '/$server_build/' ;
2022-08-08 17:10:48 +00:00
export default function createIntegration ( args? : Options ) : AstroIntegration {
2022-06-16 14:12:25 +00:00
let _config : AstroConfig ;
let _buildConfig : BuildConfig ;
2022-08-08 17:10:48 +00:00
const isModeDirectory = args ? . mode === 'directory' ;
2023-06-30 09:09:21 +00:00
let _entryPoints = new Map < RouteData , URL > ( ) ;
2022-06-16 14:12:25 +00:00
return {
name : '@astrojs/cloudflare' ,
hooks : {
2022-10-12 21:25:51 +00:00
'astro:config:setup' : ( { config , updateConfig } ) = > {
updateConfig ( {
build : {
2022-11-21 13:31:21 +00:00
client : new URL ( ` . ${ config . base } ` , config . outDir ) ,
server : new URL ( ` . ${ SERVER_BUILD_FOLDER } ` , config . outDir ) ,
2023-01-26 17:43:39 +00:00
serverEntry : '_worker.mjs' ,
2023-06-05 13:03:20 +00:00
redirects : false ,
2022-10-12 21:27:56 +00:00
} ,
2022-10-12 21:25:51 +00:00
} ) ;
} ,
2022-06-16 14:12:25 +00:00
'astro:config:done' : ( { setAdapter , config } ) = > {
2022-08-08 17:10:48 +00:00
setAdapter ( getAdapter ( isModeDirectory ) ) ;
2022-06-16 14:12:25 +00:00
_config = config ;
2022-10-12 21:25:51 +00:00
_buildConfig = config . build ;
2022-07-25 04:18:02 +00:00
2022-07-25 04:20:38 +00:00
if ( config . output === 'static' ) {
2022-07-27 15:50:48 +00:00
throw new Error ( `
2023-05-17 13:23:20 +00: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 15:50:48 +00:00
` );
2022-07-25 04:18:02 +00:00
}
2022-11-21 13:31:21 +00: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 14:12:25 +00:00
} ,
'astro:build:setup' : ( { vite , target } ) = > {
if ( target === 'server' ) {
2023-05-17 07:44:20 +00:00
vite . resolve || = { } ;
vite . resolve . alias || = { } ;
2022-06-16 14:12:25 +00: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 07:44:20 +00:00
vite . ssr || = { } ;
vite . ssr . target = 'webworker' ;
2022-06-16 14:12:25 +00:00
}
} ,
2023-07-03 12:59:43 +00:00
'astro:build:ssr' : ( { entryPoints } ) = > {
2023-06-30 09:09:21 +00:00
_entryPoints = entryPoints ;
} ,
2023-06-05 13:03:20 +00:00
'astro:build:done' : async ( { pages , routes , dir } ) = > {
2023-06-30 09:09:21 +00:00
const functionsUrl = new URL ( 'functions/' , _config . root ) ;
if ( isModeDirectory ) {
await fs . promises . mkdir ( functionsUrl , { recursive : true } ) ;
}
if ( isModeDirectory && _buildConfig . split ) {
2023-06-30 09:12:30 +00:00
const entryPointsRouteData = [ . . . _entryPoints . keys ( ) ] ;
const entryPointsURL = [ . . . _entryPoints . values ( ) ] ;
2023-06-30 09:09:21 +00:00
const entryPaths = entryPointsURL . map ( ( entry ) = > fileURLToPath ( entry ) ) ;
const outputDir = fileURLToPath ( new URL ( '.astro' , _buildConfig . server ) ) ;
// NOTE: AFAIK, esbuild keeps the order of the entryPoints array
const { outputFiles } = await esbuild . build ( {
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' ,
} ,
write : false ,
} ) ;
2022-06-16 14:12:25 +00:00
2023-06-30 09:09:21 +00:00
// loop through all bundled files and write them to the functions folder
for ( const [ index , outputFile ] of outputFiles . entries ( ) ) {
// we need to make sure the filename in the functions folder
// matches to cloudflares routing capabilities (see their docs)
// IN: src/pages/[language]/files/[...path].astro
// OUT: [language]/files/[[path]].js
const fileName = entryPointsRouteData [ index ] . component
. replace ( 'src/pages/' , '' )
. replace ( '.astro' , '.js' )
2023-07-03 12:59:43 +00:00
. replace ( /(\[\.\.\.)(\w+)(\])/g , ( _match , _p1 , p2 ) = > {
2023-06-30 09:09:21 +00:00
return ` [[ ${ p2 } ]] ` ;
} ) ;
2023-06-30 09:12:30 +00:00
const fileUrl = new URL ( fileName , functionsUrl ) ;
2023-06-30 09:09:21 +00:00
const newFileDir = dirname ( fileURLToPath ( fileUrl ) ) ;
if ( ! fs . existsSync ( newFileDir ) ) {
fs . mkdirSync ( newFileDir , { recursive : true } ) ;
}
await fs . promises . writeFile ( fileUrl , outputFile . contents ) ;
}
} 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 13:31:21 +00: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 ) {
const staticPathList : Array < string > = (
await glob ( ` ${ fileURLToPath ( _buildConfig . client ) } /**/* ` , {
cwd : fileURLToPath ( _config . outDir ) ,
filesOnly : true ,
} )
)
. filter ( ( file : string ) = > cloudflareSpecialFiles . indexOf ( file ) < 0 )
. map ( ( file : string ) = > ` / ${ file } ` ) ;
2023-01-26 17:45:39 +00:00
for ( let page of pages ) {
2023-03-13 14:58:21 +00:00
let pagePath = prependForwardSlash ( page . pathname ) ;
if ( _config . base !== '/' ) {
2023-03-16 14:42:36 +00:00
const base = _config . base . endsWith ( '/' ) ? _config . base . slice ( 0 , - 1 ) : _config . base ;
2023-03-13 14:58:21 +00:00
pagePath = ` ${ base } ${ pagePath } ` ;
}
staticPathList . push ( pagePath ) ;
2023-01-26 17:43:39 +00:00
}
2022-11-21 13:31:21 +00:00
const redirectsExists = await fs . promises
. stat ( new URL ( './_redirects' , _config . outDir ) )
. then ( ( stat ) = > stat . isFile ( ) )
. catch ( ( ) = > false ) ;
2022-11-21 13:33:45 +00:00
// convert all redirect source paths into a list of routes
// and add them to the static path
2022-11-21 13:31:21 +00: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-06-05 13:05:47 +00:00
const redirectRoutes = routes . filter ( ( r ) = > r . type === 'redirect' ) ;
2023-06-05 13:03:20 +00:00
const trueRedirects = createRedirectsFromAstroRoutes ( {
config : _config ,
routes : redirectRoutes ,
dir ,
} ) ;
2023-06-05 13:05:47 +00:00
if ( ! trueRedirects . empty ( ) ) {
2023-06-05 13:03:20 +00:00
await fs . promises . appendFile (
new URL ( './_redirects' , _config . outDir ) ,
trueRedirects . print ( )
) ;
}
2022-11-21 13:31:21 +00:00
await fs . promises . writeFile (
new URL ( './_routes.json' , _config . outDir ) ,
JSON . stringify (
{
version : 1 ,
include : [ '/*' ] ,
exclude : staticPathList ,
} ,
null ,
2
)
) ;
}
2022-06-16 14:12:25 +00:00
} ,
} ,
} ;
}
2023-01-26 17:43:39 +00:00
function prependForwardSlash ( path : string ) {
return path [ 0 ] === '/' ? path : '/' + path ;
}