diff --git a/.changeset/proud-forks-rescue.md b/.changeset/proud-forks-rescue.md new file mode 100644 index 000000000000..e968a699f10f --- /dev/null +++ b/.changeset/proud-forks-rescue.md @@ -0,0 +1,6 @@ +--- +'@astrojs/vercel': patch +--- + +- Cache result during bundling, to speed up the process of multiple functions; +- Avoid creating multiple symbolic links of the dependencies when building the project with `funcitonPerRoute` enabled; diff --git a/packages/integrations/vercel/src/lib/fs.ts b/packages/integrations/vercel/src/lib/fs.ts index 51b12d52fd5a..3ef9adadbd06 100644 --- a/packages/integrations/vercel/src/lib/fs.ts +++ b/packages/integrations/vercel/src/lib/fs.ts @@ -1,5 +1,6 @@ import type { PathLike } from 'node:fs'; import * as fs from 'node:fs/promises'; +import { existsSync } from 'node:fs'; import nodePath from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -74,11 +75,13 @@ export async function copyFilesToFunction( if (isSymlink) { const realdest = fileURLToPath(new URL(nodePath.relative(commonAncestor, realpath), outDir)); - await fs.symlink( - nodePath.relative(fileURLToPath(new URL('.', dest)), realdest), - dest, - isDir ? 'dir' : 'file' - ); + const target = nodePath.relative(fileURLToPath(new URL('.', dest)), realdest); + // NOTE: when building function per route, dependencies are linked at the first run, then there's no need anymore to do that once more. + // So we check if the destination already exists. If it does, move on. + // Symbolic links here are usually dependencies and not user code. Symbolic links exist because of the pnpm strategy. + if (!existsSync(dest)) { + await fs.symlink(target, dest, isDir ? 'dir' : 'file'); + } } else if (!isDir) { await fs.copyFile(origin, dest); } diff --git a/packages/integrations/vercel/src/lib/nft.ts b/packages/integrations/vercel/src/lib/nft.ts index 10c298a1dc8c..95c06f07cc3a 100644 --- a/packages/integrations/vercel/src/lib/nft.ts +++ b/packages/integrations/vercel/src/lib/nft.ts @@ -1,19 +1,28 @@ import { relative as relativePath } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { relative } from 'node:path'; import { copyFilesToFunction } from './fs.js'; +import type { AstroIntegrationLogger } from 'astro'; -export async function copyDependenciesToFunction({ - entry, - outDir, - includeFiles, - excludeFiles, -}: { - entry: URL; - outDir: URL; - includeFiles: URL[]; - excludeFiles: URL[]; -}): Promise<{ handler: string }> { +export async function copyDependenciesToFunction( + { + entry, + outDir, + includeFiles, + excludeFiles, + logger, + }: { + entry: URL; + outDir: URL; + includeFiles: URL[]; + excludeFiles: URL[]; + logger: AstroIntegrationLogger; + }, + // we want to pass the caching by reference, and not by value + cache: object +): Promise<{ handler: string }> { const entryPath = fileURLToPath(entry); + logger.info(`Bundling function ${relative(fileURLToPath(outDir), entryPath)}`); // Get root of folder of the system (like C:\ on Windows or / on Linux) let base = entry; @@ -31,6 +40,7 @@ export async function copyDependenciesToFunction({ // If you have a route of /dev this appears in source and NFT will try to // scan your local /dev :8 ignore: ['/dev/**'], + cache, }); for (const error of result.warnings) { diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 90d40a52de6d..24db23f80263 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -1,4 +1,10 @@ -import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro'; +import type { + AstroAdapter, + AstroConfig, + AstroIntegration, + RouteData, + AstroIntegrationLogger, +} from 'astro'; import { AstroError } from 'astro/errors'; import glob from 'fast-glob'; import { basename } from 'node:path'; @@ -78,16 +84,27 @@ export default function vercelServerless({ // Extra files to be merged with `includeFiles` during build const extraFilesToInclude: URL[] = []; - async function createFunctionFolder(funcName: string, entry: URL, inc: URL[]) { + const NTF_CACHE = Object.create(null); + + async function createFunctionFolder( + funcName: string, + entry: URL, + inc: URL[], + logger: AstroIntegrationLogger + ) { 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)) || [], - }); + const { handler } = await copyDependenciesToFunction( + { + entry, + outDir: functionFolder, + includeFiles: inc, + excludeFiles: excludeFiles?.map((file) => new URL(file, _config.root)) || [], + logger, + }, + NTF_CACHE + ); // Enable ESM // https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/ @@ -167,7 +184,7 @@ You can set functionPerRoute: false to prevent surpassing the limit.` } }, - 'astro:build:done': async ({ routes }) => { + 'astro:build:done': async ({ routes, logger }) => { // Merge any includes from `vite.assetsInclude if (_config.vite.assetsInclude) { const mergeGlobbedIncludes = (globPattern: unknown) => { @@ -192,7 +209,7 @@ You can set functionPerRoute: false to prevent surpassing the limit.` if (_entryPoints.size) { for (const [route, entryFile] of _entryPoints) { const func = basename(entryFile.toString()).replace(/\.mjs$/, ''); - await createFunctionFolder(func, entryFile, filesToInclude); + await createFunctionFolder(func, entryFile, filesToInclude, logger); routeDefinitions.push({ src: route.pattern.source, dest: func, @@ -202,7 +219,8 @@ You can set functionPerRoute: false to prevent surpassing the limit.` await createFunctionFolder( 'render', new URL(serverEntry, buildTempFolder), - filesToInclude + filesToInclude, + logger ); routeDefinitions.push({ src: '/.*', dest: 'render' }); }