From cd25abae594f9c42d3766753dfeee4f476311f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Fri, 14 Oct 2022 17:19:35 -0300 Subject: [PATCH] feat(vercel): `includeFiles` and `excludeFiles` (#5085) * refactor + include/excludeFiles * Changeset * Updated README.md Co-authored-by: Sarah Rainsberger * Missing .js Co-authored-by: Sarah Rainsberger --- .changeset/violet-buckets-call.md | 5 ++ packages/integrations/vercel/README.md | 44 +++++++++++- .../integrations/vercel/src/edge/adapter.ts | 33 +++++++-- packages/integrations/vercel/src/lib/fs.ts | 57 +++++++++++++++ packages/integrations/vercel/src/lib/nft.ts | 69 ++++++------------- .../vercel/src/serverless/adapter.ts | 20 ++++-- 6 files changed, 168 insertions(+), 60 deletions(-) create mode 100644 .changeset/violet-buckets-call.md diff --git a/.changeset/violet-buckets-call.md b/.changeset/violet-buckets-call.md new file mode 100644 index 000000000..37688d16b --- /dev/null +++ b/.changeset/violet-buckets-call.md @@ -0,0 +1,5 @@ +--- +'@astrojs/vercel': minor +--- + +Added `includeFiles` and `excludeFiles` options diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index 532cf3141..ca9610a61 100644 --- a/packages/integrations/vercel/README.md +++ b/packages/integrations/vercel/README.md @@ -84,7 +84,49 @@ vercel deploy --prebuilt ## Configuration -This adapter does not expose any configuration options. +To configure this adapter, pass an object to the `vercel()` function call in `astro.config.mjs`: + +### includeFiles + +> **Type:** `string[]` +> **Available for:** Edge, Serverless + +Use this property to force files to be bundled with your function. This is helpful when you notice missing files. + +```js +import { defineConfig } from 'astro/config'; +import vercel from '@astrojs/vercel/serverless'; + +export default defineConfig({ + output: 'server', + adapter: vercel({ + includeFiles: ['./my-data.json'] + }) +}); +``` + +> **Note** +> When building for the Edge, all the depencies get bundled in a single file to save space. **No extra file will be bundled**. So, if you _need_ some file inside the function, you have to specify it in `includeFiles`. + + +### excludeFiles + +> **Type:** `string[]` +> **Available for:** Serverless + +Use this property to exclude any files from the bundling process that would otherwise be included. + +```js +import { defineConfig } from 'astro/config'; +import vercel from '@astrojs/vercel/serverless'; + +export default defineConfig({ + output: 'server', + adapter: vercel({ + excludeFiles: ['./src/some_big_file.jpg'] + }) +}); +``` ## Troubleshooting diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index c842cb38f..73cc89483 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -1,6 +1,8 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; +import { relative as relativePath } from 'node:path'; +import { fileURLToPath } from 'node:url'; -import { getVercelOutput, writeJson } from '../lib/fs.js'; +import { getVercelOutput, removeDir, writeJson, copyFilesToFunction } from '../lib/fs.js'; import { getRedirects } from '../lib/redirects.js'; const PACKAGE_NAME = '@astrojs/vercel/edge'; @@ -13,8 +15,13 @@ function getAdapter(): AstroAdapter { }; } -export default function vercelEdge(): AstroIntegration { +export interface VercelEdgeConfig { + includeFiles?: string[]; +} + +export default function vercelEdge({ includeFiles = [] }: VercelEdgeConfig = {}): AstroIntegration { let _config: AstroConfig; + let buildTempFolder: URL; let functionFolder: URL; let serverEntry: string; let needsBuildConfig = false; @@ -30,13 +37,15 @@ export default function vercelEdge(): AstroIntegration { build: { serverEntry: 'entry.mjs', client: new URL('./static/', outDir), - server: new URL('./functions/render.func/', config.outDir), + server: new URL('./dist/', config.root), }, }); }, 'astro:config:done': ({ setAdapter, config }) => { setAdapter(getAdapter()); _config = config; + buildTempFolder = config.build.server; + functionFolder = new URL('./functions/render.func/', config.outDir); serverEntry = config.build.serverEntry; functionFolder = config.build.server; @@ -50,8 +59,8 @@ export default function vercelEdge(): AstroIntegration { 'astro:build:start': ({ buildConfig }) => { if (needsBuildConfig) { buildConfig.client = new URL('./static/', _config.outDir); + buildTempFolder = buildConfig.server = new URL('./dist/', _config.root); serverEntry = buildConfig.serverEntry = 'entry.mjs'; - functionFolder = buildConfig.server = new URL('./functions/render.func/', _config.outDir); } }, 'astro:build:setup': ({ vite, target }) => { @@ -79,11 +88,25 @@ export default function vercelEdge(): AstroIntegration { } }, 'astro:build:done': async ({ routes }) => { + const entry = new URL(serverEntry, buildTempFolder); + + // Copy entry and other server files + const commonAncestor = await copyFilesToFunction( + [ + new URL(serverEntry, buildTempFolder), + ...includeFiles.map((file) => new URL(file, _config.root)), + ], + functionFolder + ); + + // Remove temporary folder + await removeDir(buildTempFolder); + // Edge function config // https://vercel.com/docs/build-output-api/v3#vercel-primitives/edge-functions/configuration await writeJson(new URL(`./.vc-config.json`, functionFolder), { runtime: 'edge', - entrypoint: serverEntry, + entrypoint: relativePath(commonAncestor, fileURLToPath(entry)), }); // Output configuration diff --git a/packages/integrations/vercel/src/lib/fs.ts b/packages/integrations/vercel/src/lib/fs.ts index 64c4c69ba..47b218ce5 100644 --- a/packages/integrations/vercel/src/lib/fs.ts +++ b/packages/integrations/vercel/src/lib/fs.ts @@ -1,5 +1,7 @@ import type { PathLike } from 'node:fs'; import * as fs from 'node:fs/promises'; +import nodePath from 'node:path'; +import { fileURLToPath } from 'node:url'; export async function writeJson(path: PathLike, data: T) { await fs.writeFile(path, JSON.stringify(data), { encoding: 'utf-8' }); @@ -15,3 +17,58 @@ export async function emptyDir(dir: PathLike): Promise { } export const getVercelOutput = (root: URL) => new URL('./.vercel/output/', root); + +/** + * Copies files into a folder keeping the folder structure intact. + * The resulting file tree will start at the common ancestor. + * + * @param {URL[]} files A list of files to copy (absolute path). + * @param {URL} outDir Destination folder where to copy the files to (absolute path). + * @param {URL[]} [exclude] A list of files to exclude (absolute path). + * @returns {Promise} The common ancestor of the copied files. + */ +export async function copyFilesToFunction( + files: URL[], + outDir: URL, + exclude: URL[] = [] +): Promise { + const excludeList = exclude.map(fileURLToPath); + const fileList = files.map(fileURLToPath).filter((f) => !excludeList.includes(f)); + + if (files.length === 0) throw new Error('[@astrojs/vercel] No files found to copy'); + + let commonAncestor = nodePath.dirname(fileList[0]); + for (const file of fileList.slice(1)) { + while (!file.startsWith(commonAncestor)) { + commonAncestor = nodePath.dirname(commonAncestor); + } + } + + for (const origin of fileList) { + const dest = new URL(nodePath.relative(commonAncestor, origin), outDir); + + const realpath = await fs.realpath(origin); + const isSymlink = realpath !== origin; + const isDir = (await fs.stat(origin)).isDirectory(); + + // Create directories recursively + if (isDir && !isSymlink) { + await fs.mkdir(new URL('..', dest), { recursive: true }); + } else { + await fs.mkdir(new URL('.', dest), { recursive: true }); + } + + 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' + ); + } else if (!isDir) { + await fs.copyFile(origin, dest); + } + } + + return commonAncestor; +} diff --git a/packages/integrations/vercel/src/lib/nft.ts b/packages/integrations/vercel/src/lib/nft.ts index ba3677583..6a9ac116e 100644 --- a/packages/integrations/vercel/src/lib/nft.ts +++ b/packages/integrations/vercel/src/lib/nft.ts @@ -1,12 +1,20 @@ import { nodeFileTrace } from '@vercel/nft'; -import * as fs from 'node:fs/promises'; -import nodePath from 'node:path'; +import { relative as relativePath } from 'node:path'; import { fileURLToPath } from 'node:url'; -export async function copyDependenciesToFunction( - entry: URL, - outDir: URL -): Promise<{ handler: string }> { +import { copyFilesToFunction } from './fs.js'; + +export async function copyDependenciesToFunction({ + entry, + outDir, + includeFiles, + excludeFiles, +}: { + entry: URL; + outDir: URL; + includeFiles: URL[]; + excludeFiles: URL[]; +}): Promise<{ handler: string }> { const entryPath = fileURLToPath(entry); // Get root of folder of the system (like C:\ on Windows or / on Linux) @@ -19,8 +27,6 @@ export async function copyDependenciesToFunction( base: fileURLToPath(base), }); - if (result.fileList.size === 0) throw new Error('[@astrojs/vercel] No files found'); - for (const error of result.warnings) { if (error.message.startsWith('Failed to resolve dependency')) { const [, module, file] = /Cannot find module '(.+?)' loaded from (.+)/.exec(error.message)!; @@ -42,49 +48,14 @@ export async function copyDependenciesToFunction( } } - const fileList = [...result.fileList]; - - let commonAncestor = nodePath.dirname(fileList[0]); - for (const file of fileList.slice(1)) { - while (!file.startsWith(commonAncestor)) { - commonAncestor = nodePath.dirname(commonAncestor); - } - } - - for (const file of fileList) { - const origin = new URL(file, base); - const dest = new URL(nodePath.relative(commonAncestor, file), outDir); - - const realpath = await fs.realpath(origin); - const isSymlink = realpath !== fileURLToPath(origin); - const isDir = (await fs.stat(origin)).isDirectory(); - - // Create directories recursively - if (isDir && !isSymlink) { - await fs.mkdir(new URL('..', dest), { recursive: true }); - } else { - await fs.mkdir(new URL('.', dest), { recursive: true }); - } - - if (isSymlink) { - const realdest = fileURLToPath( - new URL( - nodePath.relative(nodePath.join(fileURLToPath(base), commonAncestor), realpath), - outDir - ) - ); - await fs.symlink( - nodePath.relative(fileURLToPath(new URL('.', dest)), realdest), - dest, - isDir ? 'dir' : 'file' - ); - } else if (!isDir) { - await fs.copyFile(origin, dest); - } - } + const commonAncestor = await copyFilesToFunction( + [...result.fileList].map((file) => new URL(file, base)).concat(includeFiles), + outDir, + excludeFiles + ); return { // serverEntry location inside the outDir - handler: nodePath.relative(nodePath.join(fileURLToPath(base), commonAncestor), entryPath), + handler: relativePath(commonAncestor, entryPath), }; } diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 5b65c331e..3c7ba15de 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -14,7 +14,15 @@ function getAdapter(): AstroAdapter { }; } -export default function vercelEdge(): AstroIntegration { +export interface VercelServerlessConfig { + includeFiles?: string[]; + excludeFiles?: string[]; +} + +export default function vercelServerless({ + includeFiles, + excludeFiles, +}: VercelServerlessConfig = {}): AstroIntegration { let _config: AstroConfig; let buildTempFolder: URL; let functionFolder: URL; @@ -59,10 +67,12 @@ export default function vercelEdge(): AstroIntegration { }, 'astro:build:done': async ({ routes }) => { // Copy necessary files (e.g. node_modules/) - const { handler } = await copyDependenciesToFunction( - new URL(serverEntry, buildTempFolder), - functionFolder - ); + const { handler } = await copyDependenciesToFunction({ + entry: new URL(serverEntry, buildTempFolder), + outDir: functionFolder, + includeFiles: includeFiles?.map((file) => new URL(file, _config.root)) || [], + excludeFiles: excludeFiles?.map((file) => new URL(file, _config.root)) || [], + }); // Remove temporary folder await removeDir(buildTempFolder);