2023-06-29 16:18:28 -04:00
import type { AstroAdapter , AstroConfig , AstroIntegration , RouteData } from 'astro' ;
2022-05-11 18:10:38 -03:00
2022-11-14 18:44:37 +00:00
import glob from 'fast-glob' ;
2023-06-29 20:21:41 +00:00
import { basename } from 'node:path' ;
2023-07-18 00:20:47 +00:00
import { fileURLToPath , pathToFileURL } from 'node:url' ;
2023-05-02 09:42:48 +02:00
import {
defaultImageConfig ,
getImageConfig ,
throwIfAssetsNotEnabled ,
type VercelImageConfig ,
} from '../image/shared.js' ;
2023-05-15 06:16:32 +00:00
import { exposeEnv } from '../lib/env.js' ;
2022-10-10 12:37:03 -03:00
import { getVercelOutput , removeDir , writeJson } from '../lib/fs.js' ;
2022-05-11 18:10:38 -03:00
import { copyDependenciesToFunction } from '../lib/nft.js' ;
import { getRedirects } from '../lib/redirects.js' ;
2023-07-05 16:45:58 +01:00
import { generateEdgeMiddleware } from './middleware.js' ;
2022-05-11 18:10:38 -03:00
const PACKAGE_NAME = '@astrojs/vercel/serverless' ;
2023-07-05 16:45:58 +01:00
export const ASTRO_LOCALS_HEADER = 'x-astro-locals' ;
export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware' ;
2022-05-11 18:10:38 -03:00
2023-07-21 00:09:46 +05:30
// https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js#node.js-version
const SUPPORTED_NODE_VERSIONS : Record < string , { status : 'current' } | { status : 'deprecated' , removal : Date } > = {
14 : { status : 'deprecated' , removal : new Date ( 'August 15 2023' ) } ,
16 : { status : 'deprecated' , removal : new Date ( 'February 6 2024' ) } ,
18 : { status : 'current' }
}
2022-05-11 18:10:38 -03:00
function getAdapter ( ) : AstroAdapter {
return {
name : PACKAGE_NAME ,
serverEntrypoint : ` ${ PACKAGE_NAME } /entrypoint ` ,
exports : [ 'default' ] ,
} ;
}
2022-10-14 17:19:35 -03:00
export interface VercelServerlessConfig {
includeFiles? : string [ ] ;
excludeFiles? : string [ ] ;
2023-02-09 00:32:20 +08:00
analytics? : boolean ;
2023-05-02 09:42:48 +02:00
imageService? : boolean ;
imagesConfig? : VercelImageConfig ;
2022-10-14 17:19:35 -03:00
}
export default function vercelServerless ( {
includeFiles ,
excludeFiles ,
2023-02-09 00:32:20 +08:00
analytics ,
2023-05-02 09:42:48 +02:00
imageService ,
imagesConfig ,
2022-10-14 17:19:35 -03:00
} : VercelServerlessConfig = { } ) : AstroIntegration {
2022-05-11 18:10:38 -03:00
let _config : AstroConfig ;
2022-10-10 12:37:03 -03:00
let buildTempFolder : URL ;
2022-05-11 18:10:38 -03:00
let serverEntry : string ;
2023-06-29 16:18:28 -04:00
let _entryPoints : Map < RouteData , URL > ;
2023-07-17 20:57:27 +08:00
// Extra files to be merged with `includeFiles` during build
const extraFilesToInclude : URL [ ] = [ ] ;
2023-06-29 16:18:28 -04:00
async function createFunctionFolder ( funcName : string , entry : URL , inc : URL [ ] ) {
const functionFolder = new URL ( ` ./functions/ ${ funcName } .func/ ` , _config . outDir ) ;
// Copy necessary files (e.g. node_modules/)
const { handler } = await copyDependenciesToFunction ( {
entry ,
outDir : functionFolder ,
includeFiles : inc ,
excludeFiles : excludeFiles?.map ( ( file ) = > new URL ( file , _config . root ) ) || [ ] ,
} ) ;
// Enable ESM
// https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/
await writeJson ( new URL ( ` ./package.json ` , functionFolder ) , {
type : 'module' ,
} ) ;
// Serverless function config
// https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration
await writeJson ( new URL ( ` ./.vc-config.json ` , functionFolder ) , {
runtime : getRuntime ( ) ,
handler ,
launcherType : 'Nodejs' ,
} ) ;
}
2022-05-11 18:10:38 -03:00
return {
name : PACKAGE_NAME ,
hooks : {
2023-02-16 16:19:08 +01:00
'astro:config:setup' : ( { command , config , updateConfig , injectScript } ) = > {
if ( command === 'build' && analytics ) {
2023-02-09 00:32:20 +08:00
injectScript ( 'page' , 'import "@astrojs/vercel/analytics"' ) ;
}
2022-10-12 17:25:51 -04:00
const outDir = getVercelOutput ( config . root ) ;
2023-05-15 07:13:47 +01:00
const viteDefine = exposeEnv ( [ 'VERCEL_ANALYTICS_ID' ] ) ;
2022-10-12 17:25:51 -04:00
updateConfig ( {
outDir ,
build : {
2023-02-21 22:14:47 +08:00
serverEntry : 'entry.mjs' ,
2022-10-12 17:25:51 -04:00
client : new URL ( './static/' , outDir ) ,
server : new URL ( './dist/' , config . root ) ,
2022-10-12 21:27:56 +00:00
} ,
2023-05-15 07:13:47 +01:00
vite : {
define : viteDefine ,
2023-07-14 16:01:06 -05:00
ssr : {
2023-07-14 21:03:24 +00:00
external : [ '@vercel/nft' ] ,
} ,
2023-05-15 07:13:47 +01:00
} ,
2023-05-02 09:42:48 +02:00
. . . getImageConfig ( imageService , imagesConfig , command ) ,
2022-10-12 17:25:51 -04:00
} ) ;
2022-05-11 18:10:38 -03:00
} ,
'astro:config:done' : ( { setAdapter , config } ) = > {
2023-05-02 09:42:48 +02:00
throwIfAssetsNotEnabled ( config , imageService ) ;
2022-05-11 18:10:38 -03:00
setAdapter ( getAdapter ( ) ) ;
_config = config ;
2022-10-12 17:25:51 -04:00
buildTempFolder = config . build . server ;
serverEntry = config . build . serverEntry ;
2022-07-27 11:50:48 -04:00
2022-07-27 15:52:44 +00:00
if ( config . output === 'static' ) {
throw new Error ( `
2023-05-17 13:23:20 +00:00
[ @astrojs / vercel ] \ ` output: "server" \` or \` output: "hybrid" \` is required to use the serverless adapter.
2023-05-02 09:42:48 +02:00
2022-07-27 11:50:48 -04:00
` );
}
2022-05-11 18:10:38 -03:00
} ,
2023-07-05 16:45:58 +01:00
'astro:build:ssr' : async ( { entryPoints , middlewareEntryPoint } ) = > {
2023-06-29 16:18:28 -04:00
_entryPoints = entryPoints ;
2023-07-05 16:45:58 +01:00
if ( middlewareEntryPoint ) {
const outPath = fileURLToPath ( buildTempFolder ) ;
const vercelEdgeMiddlewareHandlerPath = new URL (
VERCEL_EDGE_MIDDLEWARE_FILE ,
_config . srcDir
) ;
const bundledMiddlewarePath = await generateEdgeMiddleware (
middlewareEntryPoint ,
outPath ,
vercelEdgeMiddlewareHandlerPath
) ;
// let's tell the adapter that we need to save this file
2023-07-17 20:57:27 +08:00
extraFilesToInclude . push ( bundledMiddlewarePath ) ;
2023-07-05 16:45:58 +01:00
}
2023-06-29 16:18:28 -04:00
} ,
2023-07-05 16:45:58 +01:00
2022-05-11 18:10:38 -03:00
'astro:build:done' : async ( { routes } ) = > {
2022-11-14 13:42:35 -05:00
// Merge any includes from `vite.assetsInclude
2022-11-14 18:44:37 +00:00
if ( _config . vite . assetsInclude ) {
2022-11-14 13:42:35 -05:00
const mergeGlobbedIncludes = ( globPattern : unknown ) = > {
2022-11-14 18:44:37 +00:00
if ( typeof globPattern === 'string' ) {
const entries = glob . sync ( globPattern ) . map ( ( p ) = > pathToFileURL ( p ) ) ;
2023-07-17 20:57:27 +08:00
extraFilesToInclude . push ( . . . entries ) ;
2022-11-14 18:44:37 +00:00
} else if ( Array . isArray ( globPattern ) ) {
for ( const pattern of globPattern ) {
2022-11-14 13:42:35 -05:00
mergeGlobbedIncludes ( pattern ) ;
}
}
} ;
mergeGlobbedIncludes ( _config . vite . assetsInclude ) ;
}
2023-06-29 16:18:28 -04:00
const routeDefinitions : { src : string ; dest : string } [ ] = [ ] ;
2023-07-17 20:57:27 +08:00
const filesToInclude = includeFiles ? . map ( ( file ) = > new URL ( file , _config . root ) ) || [ ] ;
filesToInclude . push ( . . . extraFilesToInclude ) ;
2022-05-11 18:10:38 -03:00
2023-06-29 16:18:28 -04:00
// Multiple entrypoint support
2023-06-29 20:21:41 +00:00
if ( _entryPoints . size ) {
for ( const [ route , entryFile ] of _entryPoints ) {
2023-06-29 16:18:28 -04:00
const func = basename ( entryFile . toString ( ) ) . replace ( /\.mjs$/ , '' ) ;
2023-07-05 16:45:58 +01:00
await createFunctionFolder ( func , entryFile , filesToInclude ) ;
2023-06-29 16:18:28 -04:00
routeDefinitions . push ( {
src : route.pattern.source ,
2023-06-29 20:21:41 +00:00
dest : func ,
2023-06-29 16:18:28 -04:00
} ) ;
}
} else {
2023-07-05 16:45:58 +01:00
await createFunctionFolder (
'render' ,
new URL ( serverEntry , buildTempFolder ) ,
filesToInclude
) ;
2023-06-29 16:18:28 -04:00
routeDefinitions . push ( { src : '/.*' , dest : 'render' } ) ;
}
2022-05-11 18:10:38 -03:00
// Output configuration
// https://vercel.com/docs/build-output-api/v3#build-output-configuration
await writeJson ( new URL ( ` ./config.json ` , _config . outDir ) , {
version : 3 ,
2023-06-29 20:21:41 +00:00
routes : [ . . . getRedirects ( routes , _config ) , { handle : 'filesystem' } , . . . routeDefinitions ] ,
2023-05-02 09:42:48 +02:00
. . . ( imageService || imagesConfig
? { images : imagesConfig ? imagesConfig : defaultImageConfig }
: { } ) ,
2022-05-11 18:10:38 -03:00
} ) ;
2023-06-29 16:18:28 -04:00
// Remove temporary folder
await removeDir ( buildTempFolder ) ;
2022-05-11 18:10:38 -03:00
} ,
} ,
} ;
}
2022-05-16 15:34:46 -03:00
function getRuntime() {
const version = process . version . slice ( 1 ) ; // 'v16.5.0' --> '16.5.0'
const major = version . split ( '.' ) [ 0 ] ; // '16.5.0' --> '16'
2023-07-21 00:09:46 +05:30
const support = SUPPORTED_NODE_VERSIONS [ major ]
if ( support === undefined ) {
console . warn ( ` [ ${ PACKAGE_NAME } ] The local Node.js version ( ${ major } ) is not supported by Vercel Serverless Functions. ` )
console . warn ( ` [ ${ PACKAGE_NAME } ] Your project will use Node.js 18 as the runtime instead. ` )
console . warn ( ` [ ${ PACKAGE_NAME } ] Consider switching your local version to 18. ` )
return 'nodejs18.x' ;
}
if ( support . status === 'deprecated' ) {
console . warn ( ` [ ${ PACKAGE_NAME } ] Your project is being built for Node.js ${ major } as the runtime. ` )
console . warn ( ` [ ${ PACKAGE_NAME } ] This version is deprecated by Vercel Serverless Functions, and scheduled to be disabled on ${ new Intl . DateTimeFormat ( undefined , { dateStyle : "long" } ).format(support.removal)}. ` )
console . warn ( ` [ ${ PACKAGE_NAME } ] Consider upgrading your local version to 18. ` )
}
2022-05-16 15:34:46 -03:00
return ` nodejs ${ major } .x ` ;
}