2023-06-29 20:18:28 +00:00
|
|
|
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
|
2022-05-11 21:10:38 +00: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 07:42:48 +00: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 15:37:03 +00:00
|
|
|
import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js';
|
2022-05-11 21:10:38 +00:00
|
|
|
import { copyDependenciesToFunction } from '../lib/nft.js';
|
|
|
|
import { getRedirects } from '../lib/redirects.js';
|
2023-07-05 15:45:58 +00:00
|
|
|
import { generateEdgeMiddleware } from './middleware.js';
|
2022-05-11 21:10:38 +00:00
|
|
|
|
|
|
|
const PACKAGE_NAME = '@astrojs/vercel/serverless';
|
2023-07-05 15:45:58 +00:00
|
|
|
export const ASTRO_LOCALS_HEADER = 'x-astro-locals';
|
|
|
|
export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware';
|
2022-05-11 21:10:38 +00:00
|
|
|
|
|
|
|
function getAdapter(): AstroAdapter {
|
|
|
|
return {
|
|
|
|
name: PACKAGE_NAME,
|
|
|
|
serverEntrypoint: `${PACKAGE_NAME}/entrypoint`,
|
|
|
|
exports: ['default'],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-10-14 20:19:35 +00:00
|
|
|
export interface VercelServerlessConfig {
|
|
|
|
includeFiles?: string[];
|
|
|
|
excludeFiles?: string[];
|
2023-02-08 16:32:20 +00:00
|
|
|
analytics?: boolean;
|
2023-05-02 07:42:48 +00:00
|
|
|
imageService?: boolean;
|
|
|
|
imagesConfig?: VercelImageConfig;
|
2022-10-14 20:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default function vercelServerless({
|
|
|
|
includeFiles,
|
|
|
|
excludeFiles,
|
2023-02-08 16:32:20 +00:00
|
|
|
analytics,
|
2023-05-02 07:42:48 +00:00
|
|
|
imageService,
|
|
|
|
imagesConfig,
|
2022-10-14 20:19:35 +00:00
|
|
|
}: VercelServerlessConfig = {}): AstroIntegration {
|
2022-05-11 21:10:38 +00:00
|
|
|
let _config: AstroConfig;
|
2022-10-10 15:37:03 +00:00
|
|
|
let buildTempFolder: URL;
|
2022-05-11 21:10:38 +00:00
|
|
|
let serverEntry: string;
|
2023-06-29 20:18:28 +00:00
|
|
|
let _entryPoints: Map<RouteData, URL>;
|
2023-07-17 12:57:27 +00:00
|
|
|
// Extra files to be merged with `includeFiles` during build
|
|
|
|
const extraFilesToInclude: URL[] = [];
|
2023-06-29 20:18:28 +00: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 21:10:38 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
name: PACKAGE_NAME,
|
|
|
|
hooks: {
|
2023-02-16 15:19:08 +00:00
|
|
|
'astro:config:setup': ({ command, config, updateConfig, injectScript }) => {
|
|
|
|
if (command === 'build' && analytics) {
|
2023-02-08 16:32:20 +00:00
|
|
|
injectScript('page', 'import "@astrojs/vercel/analytics"');
|
|
|
|
}
|
2022-10-12 21:25:51 +00:00
|
|
|
const outDir = getVercelOutput(config.root);
|
2023-05-15 06:13:47 +00:00
|
|
|
const viteDefine = exposeEnv(['VERCEL_ANALYTICS_ID']);
|
2022-10-12 21:25:51 +00:00
|
|
|
updateConfig({
|
|
|
|
outDir,
|
|
|
|
build: {
|
2023-02-21 14:14:47 +00:00
|
|
|
serverEntry: 'entry.mjs',
|
2022-10-12 21:25:51 +00:00
|
|
|
client: new URL('./static/', outDir),
|
|
|
|
server: new URL('./dist/', config.root),
|
2022-10-12 21:27:56 +00:00
|
|
|
},
|
2023-05-15 06:13:47 +00:00
|
|
|
vite: {
|
|
|
|
define: viteDefine,
|
2023-07-14 21:01:06 +00:00
|
|
|
ssr: {
|
2023-07-14 21:03:24 +00:00
|
|
|
external: ['@vercel/nft'],
|
|
|
|
},
|
2023-05-15 06:13:47 +00:00
|
|
|
},
|
2023-05-02 07:42:48 +00:00
|
|
|
...getImageConfig(imageService, imagesConfig, command),
|
2022-10-12 21:25:51 +00:00
|
|
|
});
|
2022-05-11 21:10:38 +00:00
|
|
|
},
|
|
|
|
'astro:config:done': ({ setAdapter, config }) => {
|
2023-05-02 07:42:48 +00:00
|
|
|
throwIfAssetsNotEnabled(config, imageService);
|
2022-05-11 21:10:38 +00:00
|
|
|
setAdapter(getAdapter());
|
|
|
|
_config = config;
|
2022-10-12 21:25:51 +00:00
|
|
|
buildTempFolder = config.build.server;
|
|
|
|
serverEntry = config.build.serverEntry;
|
2022-07-27 15:50:48 +00: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 07:42:48 +00:00
|
|
|
|
2022-07-27 15:50:48 +00:00
|
|
|
`);
|
|
|
|
}
|
2022-05-11 21:10:38 +00:00
|
|
|
},
|
2023-07-05 15:45:58 +00:00
|
|
|
|
|
|
|
'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => {
|
2023-06-29 20:18:28 +00:00
|
|
|
_entryPoints = entryPoints;
|
2023-07-05 15:45:58 +00: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 12:57:27 +00:00
|
|
|
extraFilesToInclude.push(bundledMiddlewarePath);
|
2023-07-05 15:45:58 +00:00
|
|
|
}
|
2023-06-29 20:18:28 +00:00
|
|
|
},
|
2023-07-05 15:45:58 +00:00
|
|
|
|
2022-05-11 21:10:38 +00:00
|
|
|
'astro:build:done': async ({ routes }) => {
|
2022-11-14 18:42:35 +00:00
|
|
|
// Merge any includes from `vite.assetsInclude
|
2022-11-14 18:44:37 +00:00
|
|
|
if (_config.vite.assetsInclude) {
|
2022-11-14 18:42:35 +00: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 12:57:27 +00: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 18:42:35 +00:00
|
|
|
mergeGlobbedIncludes(pattern);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
mergeGlobbedIncludes(_config.vite.assetsInclude);
|
|
|
|
}
|
|
|
|
|
2023-06-29 20:18:28 +00:00
|
|
|
const routeDefinitions: { src: string; dest: string }[] = [];
|
2023-07-17 12:57:27 +00:00
|
|
|
const filesToInclude = includeFiles?.map((file) => new URL(file, _config.root)) || [];
|
|
|
|
filesToInclude.push(...extraFilesToInclude);
|
2022-05-11 21:10:38 +00:00
|
|
|
|
2023-06-29 20:18:28 +00:00
|
|
|
// Multiple entrypoint support
|
2023-06-29 20:21:41 +00:00
|
|
|
if (_entryPoints.size) {
|
|
|
|
for (const [route, entryFile] of _entryPoints) {
|
2023-06-29 20:18:28 +00:00
|
|
|
const func = basename(entryFile.toString()).replace(/\.mjs$/, '');
|
2023-07-05 15:45:58 +00:00
|
|
|
await createFunctionFolder(func, entryFile, filesToInclude);
|
2023-06-29 20:18:28 +00:00
|
|
|
routeDefinitions.push({
|
|
|
|
src: route.pattern.source,
|
2023-06-29 20:21:41 +00:00
|
|
|
dest: func,
|
2023-06-29 20:18:28 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
2023-07-05 15:45:58 +00:00
|
|
|
await createFunctionFolder(
|
|
|
|
'render',
|
|
|
|
new URL(serverEntry, buildTempFolder),
|
|
|
|
filesToInclude
|
|
|
|
);
|
2023-06-29 20:18:28 +00:00
|
|
|
routeDefinitions.push({ src: '/.*', dest: 'render' });
|
|
|
|
}
|
2022-05-11 21:10:38 +00: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 07:42:48 +00:00
|
|
|
...(imageService || imagesConfig
|
|
|
|
? { images: imagesConfig ? imagesConfig : defaultImageConfig }
|
|
|
|
: {}),
|
2022-05-11 21:10:38 +00:00
|
|
|
});
|
2023-06-29 20:18:28 +00:00
|
|
|
|
|
|
|
// Remove temporary folder
|
|
|
|
await removeDir(buildTempFolder);
|
2022-05-11 21:10:38 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2022-05-16 18:34:46 +00:00
|
|
|
|
|
|
|
function getRuntime() {
|
|
|
|
const version = process.version.slice(1); // 'v16.5.0' --> '16.5.0'
|
|
|
|
const major = version.split('.')[0]; // '16.5.0' --> '16'
|
|
|
|
return `nodejs${major}.x`;
|
|
|
|
}
|