2023-09-01 14:33:04 +01:00
import type {
AstroAdapter ,
AstroConfig ,
AstroIntegration ,
AstroIntegrationLogger ,
2023-09-01 13:35:21 +00:00
RouteData ,
2023-09-01 14:33:04 +01:00
} from 'astro' ;
2023-08-28 17:19:04 +01:00
import { AstroError } from 'astro/errors' ;
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-09-07 18:12:00 +02:00
import {
getAstroImageConfig ,
getDefaultImageConfig ,
2023-09-13 18:40:02 +02:00
type DevImageService ,
2023-09-07 18:12:00 +02:00
type VercelImageConfig ,
} from '../image/shared.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-09-14 14:02:11 +02:00
import {
getSpeedInsightsViteConfig ,
type VercelSpeedInsightsConfig ,
} from '../lib/speed-insights.js' ;
import {
getInjectableWebAnalyticsContent ,
type VercelWebAnalyticsConfig ,
} from '../lib/web-analytics.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
2023-07-20 18:42:00 +00:00
const SUPPORTED_NODE_VERSIONS : Record <
string ,
2023-12-13 04:12:48 -08:00
{ status : 'current' } | { status : 'beta' } | { status : 'deprecated' ; removal : Date }
2023-07-20 18:42:00 +00:00
> = {
2023-07-21 00:09:46 +05:30
16 : { status : 'deprecated' , removal : new Date ( 'February 6 2024' ) } ,
2023-07-20 18:42:00 +00:00
18 : { status : 'current' } ,
2023-12-13 04:12:48 -08:00
20 : { status : 'beta' } ,
2023-07-20 18:42:00 +00:00
} ;
2023-07-21 00:09:46 +05:30
2023-07-28 10:11:13 +01:00
function getAdapter ( {
edgeMiddleware ,
functionPerRoute ,
} : {
edgeMiddleware : boolean ;
functionPerRoute : boolean ;
} ) : AstroAdapter {
2022-05-11 18:10:38 -03:00
return {
name : PACKAGE_NAME ,
serverEntrypoint : ` ${ PACKAGE_NAME } /entrypoint ` ,
exports : [ 'default' ] ,
2023-07-28 10:11:13 +01:00
adapterFeatures : {
edgeMiddleware ,
functionPerRoute ,
} ,
2023-07-28 12:15:09 +01:00
supportedAstroFeatures : {
hybridOutput : 'stable' ,
staticOutput : 'stable' ,
serverOutput : 'stable' ,
assets : {
supportKind : 'stable' ,
isSharpCompatible : true ,
isSquooshCompatible : true ,
} ,
} ,
2022-05-11 18:10:38 -03:00
} ;
}
2022-10-14 17:19:35 -03:00
export interface VercelServerlessConfig {
2023-10-23 10:02:23 +00:00
/** Configuration for [Vercel Web Analytics](https://vercel.com/docs/concepts/analytics). */
2023-09-14 14:02:11 +02:00
webAnalytics? : VercelWebAnalyticsConfig ;
2023-10-23 10:02:23 +00:00
2024-01-04 16:37:13 +00:00
/ * *
* @deprecated This option lets you configure the legacy speed insights API which is now deprecated by Vercel .
2024-01-04 16:38:10 +00:00
*
2024-01-04 16:37:13 +00:00
* See [ Vercel Speed Insights Quickstart ] ( https : //vercel.com/docs/speed-insights/quickstart) for instructions on how to use the library instead.
2024-01-04 16:38:10 +00:00
*
2024-01-04 16:37:13 +00:00
* https : //vercel.com/docs/speed-insights/quickstart
* /
2023-09-14 14:02:11 +02:00
speedInsights? : VercelSpeedInsightsConfig ;
2023-10-23 10:02:23 +00:00
/** Force files to be bundled with your function. This is helpful when you notice missing files. */
2022-10-14 17:19:35 -03:00
includeFiles? : string [ ] ;
2023-10-23 10:02:23 +00:00
/** Exclude any files from the bundling process that would otherwise be included. */
2022-10-14 17:19:35 -03:00
excludeFiles? : string [ ] ;
2023-10-23 10:02:23 +00:00
/** When enabled, an Image Service powered by the Vercel Image Optimization API will be automatically configured and used in production. In development, the image service specified by devImageService will be used instead. */
2023-05-02 09:42:48 +02:00
imageService? : boolean ;
2023-10-23 10:02:23 +00:00
/** Configuration options for [Vercel’ s Image Optimization API](https://vercel.com/docs/concepts/image-optimization). See [Vercel’ s image configuration documentation](https://vercel.com/docs/build-output-api/v3/configuration#images) for a complete list of supported parameters. */
2023-05-02 09:42:48 +02:00
imagesConfig? : VercelImageConfig ;
2023-10-23 10:02:23 +00:00
/** Allows you to configure which image service to use in development when imageService is enabled. */
2023-09-13 18:40:02 +02:00
devImageService? : DevImageService ;
2023-10-23 10:02:23 +00:00
/** Whether to create the Vercel Edge middleware from an Astro middleware in your code base. */
2023-07-28 10:11:13 +01:00
edgeMiddleware? : boolean ;
2023-10-23 10:04:32 +00:00
2023-10-23 10:02:23 +00:00
/** Whether to split builds into a separate function for each route. */
2023-07-28 10:11:13 +01:00
functionPerRoute? : boolean ;
2023-10-23 10:02:23 +00:00
/** The maximum duration (in seconds) that Serverless Functions can run before timing out. See the [Vercel documentation](https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration) for the default and maximum limit for your account plan. */
maxDuration? : number ;
2022-10-14 17:19:35 -03:00
}
export default function vercelServerless ( {
2023-09-14 14:02:11 +02:00
webAnalytics ,
speedInsights ,
2022-10-14 17:19:35 -03:00
includeFiles ,
2024-01-17 13:10:43 +00:00
excludeFiles = [ ] ,
2023-05-02 09:42:48 +02:00
imageService ,
imagesConfig ,
2023-09-13 18:40:02 +02:00
devImageService = 'sharp' ,
2023-09-14 20:15:11 +08:00
functionPerRoute = false ,
2023-07-28 10:11:13 +01:00
edgeMiddleware = false ,
2023-10-23 10:02:23 +00:00
maxDuration ,
2022-10-14 17:19:35 -03:00
} : VercelServerlessConfig = { } ) : AstroIntegration {
2023-10-23 10:02:23 +00:00
if ( maxDuration ) {
if ( typeof maxDuration !== 'number' ) {
throw new TypeError ( ` maxDuration must be a number ` , { cause : maxDuration } ) ;
}
if ( maxDuration <= 0 ) {
throw new TypeError ( ` maxDuration must be a positive number ` , { cause : maxDuration } ) ;
}
}
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
2023-09-01 14:33:04 +01:00
const NTF_CACHE = Object . create ( null ) ;
2022-05-11 18:10:38 -03:00
return {
name : PACKAGE_NAME ,
hooks : {
2023-09-14 14:02:11 +02:00
'astro:config:setup' : async ( { command , config , updateConfig , injectScript , logger } ) = > {
2023-10-23 10:02:23 +00:00
if ( maxDuration && maxDuration > 900 ) {
2023-10-23 10:04:32 +00:00
logger . warn (
` maxDuration is set to ${ maxDuration } seconds, which is longer than the maximum allowed duration of 900 seconds. `
) ;
logger . warn (
` Please make sure that your plan allows for this duration. See https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration for more information. `
) ;
2023-10-23 10:02:23 +00:00
}
2023-11-24 22:00:03 +08:00
if ( webAnalytics ? . enabled ) {
2023-09-14 14:02:11 +02:00
injectScript (
'head-inline' ,
await getInjectableWebAnalyticsContent ( {
mode : command === 'dev' ? 'development' : 'production' ,
} )
) ;
}
2023-11-24 22:00:03 +08:00
if ( command === 'build' && speedInsights ? . enabled ) {
2023-09-14 14:02:11 +02:00
injectScript ( 'page' , 'import "@astrojs/vercel/speed-insights"' ) ;
2023-02-09 00:32:20 +08:00
}
2022-10-12 17:25:51 -04:00
const outDir = getVercelOutput ( config . root ) ;
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 ) ,
2023-10-24 18:26:34 +08:00
redirects : false ,
2022-10-12 21:27:56 +00:00
} ,
2023-05-15 07:13:47 +01:00
vite : {
2023-11-24 22:00:03 +08:00
. . . getSpeedInsightsViteConfig ( speedInsights ? . enabled ) ,
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-09-13 18:40:02 +02:00
. . . getAstroImageConfig (
imageService ,
imagesConfig ,
command ,
devImageService ,
config . image
) ,
2022-10-12 17:25:51 -04:00
} ) ;
2022-05-11 18:10:38 -03:00
} ,
2023-08-31 15:36:37 +01:00
'astro:config:done' : ( { setAdapter , config , logger } ) = > {
if ( functionPerRoute === true ) {
logger . warn (
2024-01-17 13:10:43 +00:00
` \ n ` +
` \ tVercel's hosting plans might have limits to the number of functions you can create. \ n ` +
` \ tMake sure to check your plan carefully to avoid incurring additional costs. \ n ` +
` \ tYou can set functionPerRoute: false to prevent surpassing the limit. \ n `
2023-08-31 15:36:37 +01:00
) ;
}
2023-07-28 10:11:13 +01:00
setAdapter ( getAdapter ( { functionPerRoute , edgeMiddleware } ) ) ;
2022-05-11 18:10:38 -03:00
_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' ) {
2023-08-28 17:19:04 +01:00
throw new AstroError (
'`output: "server"` or `output: "hybrid"` is required to use the serverless adapter.'
) ;
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-09-01 14:33:04 +01:00
'astro:build:done' : async ( { routes , logger } ) = > {
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
2024-01-17 13:10:43 +00:00
const runtime = getRuntime ( process , logger ) ;
2023-09-18 16:52:55 +05:30
2023-06-29 16:18:28 -04:00
// Multiple entrypoint support
2023-06-29 20:21:41 +00:00
if ( _entryPoints . size ) {
2023-09-06 04:46:18 +00:00
const getRouteFuncName = ( route : RouteData ) = > route . component . replace ( 'src/pages/' , '' ) ;
2023-09-06 06:43:53 +02:00
2023-09-06 04:46:18 +00:00
const getFallbackFuncName = ( entryFile : URL ) = >
basename ( entryFile . toString ( ) )
. replace ( 'entry.' , '' )
. replace ( /\.mjs$/ , '' ) ;
2023-09-06 06:43:53 +02:00
2023-09-06 04:46:18 +00:00
for ( const [ route , entryFile ] of _entryPoints ) {
const func = route . component . startsWith ( 'src/pages/' )
2023-09-06 06:43:53 +02:00
? getRouteFuncName ( route )
2023-09-06 04:46:18 +00:00
: getFallbackFuncName ( entryFile ) ;
2023-10-23 10:02:23 +00:00
await createFunctionFolder ( {
functionName : func ,
2024-01-17 13:10:43 +00:00
runtime ,
2023-10-23 10:02:23 +00:00
entry : entryFile ,
config : _config ,
logger ,
NTF_CACHE ,
includeFiles : filesToInclude ,
excludeFiles ,
2023-10-23 10:04:32 +00:00
maxDuration ,
2023-10-23 10:02:23 +00:00
} ) ;
2023-06-29 16:18:28 -04:00
routeDefinitions . push ( {
2023-09-06 04:46:18 +00:00
src : route.pattern.source ,
dest : func ,
} ) ;
}
2023-06-29 16:18:28 -04:00
} else {
2023-10-23 10:02:23 +00:00
await createFunctionFolder ( {
functionName : 'render' ,
2024-01-17 13:10:43 +00:00
runtime ,
2023-10-23 10:02:23 +00:00
entry : new URL ( serverEntry , buildTempFolder ) ,
config : _config ,
logger ,
NTF_CACHE ,
includeFiles : filesToInclude ,
excludeFiles ,
2023-10-23 10:04:32 +00:00
maxDuration ,
2023-10-23 10:02:23 +00:00
} ) ;
2024-01-10 14:52:29 +00:00
for ( const route of routes ) {
2024-01-10 14:53:36 +00:00
if ( route . prerender ) continue ;
2024-01-10 14:52:29 +00:00
routeDefinitions . push ( {
src : route.pattern.source ,
dest : 'render' ,
2024-01-10 14:53:36 +00:00
} ) ;
2024-01-10 14:52:29 +00:00
}
2023-06-29 16:18:28 -04:00
}
2024-01-10 14:52:29 +00:00
const fourOhFourRoute = routes . find ( ( route ) = > route . pathname === '/404' ) ;
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-08-02 08:06:50 +09:00
routes : [
. . . getRedirects ( routes , _config ) ,
{
src : ` ^/ ${ _config . build . assets } /(.*) $ ` ,
headers : { 'cache-control' : 'public, max-age=31536000, immutable' } ,
continue : true ,
} ,
{ handle : 'filesystem' } ,
. . . routeDefinitions ,
2024-01-10 14:53:36 +00:00
. . . ( fourOhFourRoute
? [
{
src : '/.*' ,
dest : fourOhFourRoute.prerender ? '/404.html' : 'render' ,
status : 404 ,
} ,
]
: [ ] ) ,
2023-08-02 08:06:50 +09:00
] ,
2023-05-02 09:42:48 +02:00
. . . ( imageService || imagesConfig
2023-09-07 18:12:00 +02:00
? {
images : imagesConfig
? {
. . . imagesConfig ,
domains : [ . . . imagesConfig . domains , . . . _config . image . domains ] ,
remotePatterns : [
. . . ( imagesConfig . remotePatterns ? ? [ ] ) ,
. . . _config . image . remotePatterns ,
] ,
2023-12-27 17:38:12 +00:00
}
2023-09-07 18:12:00 +02:00
: getDefaultImageConfig ( _config . image ) ,
2023-12-27 17:38:12 +00:00
}
2023-05-02 09:42:48 +02:00
: { } ) ,
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
2024-01-17 13:10:43 +00:00
type Runtime = ` nodejs ${ string } .x ` ;
2023-10-23 10:02:23 +00:00
interface CreateFunctionFolderArgs {
2023-10-23 10:04:32 +00:00
functionName : string ;
2024-01-17 13:10:43 +00:00
runtime : Runtime ;
2023-10-23 10:04:32 +00:00
entry : URL ;
config : AstroConfig ;
logger : AstroIntegrationLogger ;
NTF_CACHE : any ;
includeFiles : URL [ ] ;
2024-01-17 13:10:43 +00:00
excludeFiles : string [ ] ;
2023-11-07 14:01:04 +00:00
maxDuration : number | undefined ;
2023-10-23 10:02:23 +00:00
}
async function createFunctionFolder ( {
functionName ,
2024-01-17 13:10:43 +00:00
runtime ,
2023-10-23 10:02:23 +00:00
entry ,
config ,
logger ,
NTF_CACHE ,
includeFiles ,
excludeFiles ,
maxDuration ,
} : CreateFunctionFolderArgs ) {
2024-01-17 13:10:43 +00:00
// .vercel/output/functions/<name>.func/
2023-10-23 10:02:23 +00:00
const functionFolder = new URL ( ` ./functions/ ${ functionName } .func/ ` , config . outDir ) ;
2024-01-17 13:10:43 +00:00
const packageJson = new URL ( ` ./functions/ ${ functionName } .func/package.json ` , config . outDir ) ;
const vcConfig = new URL ( ` ./functions/ ${ functionName } .func/.vc-config.json ` , config . outDir ) ;
2023-10-23 10:02:23 +00:00
// Copy necessary files (e.g. node_modules/)
const { handler } = await copyDependenciesToFunction (
{
entry ,
outDir : functionFolder ,
includeFiles ,
2024-01-17 13:10:43 +00:00
excludeFiles : excludeFiles.map ( ( file ) = > new URL ( file , config . root ) ) ,
2023-10-23 10:02:23 +00:00
logger ,
} ,
NTF_CACHE
) ;
// Enable ESM
// https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/
2024-01-17 13:10:43 +00:00
await writeJson ( packageJson , { type : 'module' } ) ;
2023-10-23 10:02:23 +00:00
// Serverless function config
// https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration
2024-01-17 13:10:43 +00:00
await writeJson ( vcConfig , {
runtime ,
2023-10-23 10:02:23 +00:00
handler ,
launcherType : 'Nodejs' ,
maxDuration ,
2023-11-07 14:01:04 +00:00
supportsResponseStreaming : true ,
2023-10-23 10:02:23 +00:00
} ) ;
}
2024-01-17 13:10:43 +00:00
function getRuntime ( process : NodeJS.Process , logger : AstroIntegrationLogger ) : Runtime {
const version = process . version . slice ( 1 ) ; // 'v18.19.0' --> '18.19.0'
const major = version . split ( '.' ) [ 0 ] ; // '18.19.0' --> '18'
2023-07-20 18:42:00 +00:00
const support = SUPPORTED_NODE_VERSIONS [ major ] ;
2023-07-21 00:09:46 +05:30
if ( support === undefined ) {
2024-01-17 13:10:43 +00:00
logger . warn (
` \ n ` +
` \ tThe local Node.js version ( ${ major } ) is not supported by Vercel Serverless Functions. \ n ` +
` \ tYour project will use Node.js 18 as the runtime instead. \ n ` +
` \ tConsider switching your local version to 18. \ n `
2023-07-20 18:42:00 +00:00
) ;
2023-07-21 00:09:46 +05:30
}
2024-01-17 13:10:43 +00:00
if ( support . status === 'current' ) {
return ` nodejs ${ major } .x ` ;
} else if ( support ? . status === 'beta' ) {
logger . warn (
` Your project is being built for Node.js ${ major } as the runtime, which is currently in beta for Vercel Serverless Functions. `
2023-12-29 15:48:15 +00:00
) ;
2024-01-17 13:10:43 +00:00
return ` nodejs ${ major } .x ` ;
} else if ( support . status === 'deprecated' ) {
const removeDate = new Intl . DateTimeFormat ( undefined , { dateStyle : 'long' } ) . format (
support . removal
2023-07-20 18:42:00 +00:00
) ;
2024-01-17 13:10:43 +00:00
logger . warn (
` \ n ` +
` \ tYour project is being built for Node.js ${ major } as the runtime. \ n ` +
` \ tThis version is deprecated by Vercel Serverless Functions, and scheduled to be disabled on ${ removeDate } . \ n ` +
` \ tConsider upgrading your local version to 18. \ n `
) ;
return ` nodejs ${ major } .x ` ;
} else {
logger . warn (
` \ n ` +
` \ tThe local Node.js version ( ${ major } ) is not supported by Vercel Serverless Functions. \ n ` +
` \ tYour project will use Node.js 18 as the runtime instead. \ n ` +
` \ tConsider switching your local version to 18. \ n `
2023-07-20 18:42:00 +00:00
) ;
2023-09-18 16:52:55 +05:30
return 'nodejs18.x' ;
}
2022-05-16 15:34:46 -03:00
return ` nodejs ${ major } .x ` ;
}