feat(vercel): includeFiles and excludeFiles (#5085)

* refactor + include/excludeFiles

* Changeset

* Updated README.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Missing .js

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Juan Martín Seery 2022-10-14 17:19:35 -03:00 committed by GitHub
parent f8198d2502
commit cd25abae59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 168 additions and 60 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/vercel': minor
---
Added `includeFiles` and `excludeFiles` options

View file

@ -84,7 +84,49 @@ vercel deploy --prebuilt
## Configuration ## 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 ## Troubleshooting

View file

@ -1,6 +1,8 @@
import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; 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'; import { getRedirects } from '../lib/redirects.js';
const PACKAGE_NAME = '@astrojs/vercel/edge'; 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 _config: AstroConfig;
let buildTempFolder: URL;
let functionFolder: URL; let functionFolder: URL;
let serverEntry: string; let serverEntry: string;
let needsBuildConfig = false; let needsBuildConfig = false;
@ -30,13 +37,15 @@ export default function vercelEdge(): AstroIntegration {
build: { build: {
serverEntry: 'entry.mjs', serverEntry: 'entry.mjs',
client: new URL('./static/', outDir), client: new URL('./static/', outDir),
server: new URL('./functions/render.func/', config.outDir), server: new URL('./dist/', config.root),
}, },
}); });
}, },
'astro:config:done': ({ setAdapter, config }) => { 'astro:config:done': ({ setAdapter, config }) => {
setAdapter(getAdapter()); setAdapter(getAdapter());
_config = config; _config = config;
buildTempFolder = config.build.server;
functionFolder = new URL('./functions/render.func/', config.outDir);
serverEntry = config.build.serverEntry; serverEntry = config.build.serverEntry;
functionFolder = config.build.server; functionFolder = config.build.server;
@ -50,8 +59,8 @@ export default function vercelEdge(): AstroIntegration {
'astro:build:start': ({ buildConfig }) => { 'astro:build:start': ({ buildConfig }) => {
if (needsBuildConfig) { if (needsBuildConfig) {
buildConfig.client = new URL('./static/', _config.outDir); buildConfig.client = new URL('./static/', _config.outDir);
buildTempFolder = buildConfig.server = new URL('./dist/', _config.root);
serverEntry = buildConfig.serverEntry = 'entry.mjs'; serverEntry = buildConfig.serverEntry = 'entry.mjs';
functionFolder = buildConfig.server = new URL('./functions/render.func/', _config.outDir);
} }
}, },
'astro:build:setup': ({ vite, target }) => { 'astro:build:setup': ({ vite, target }) => {
@ -79,11 +88,25 @@ export default function vercelEdge(): AstroIntegration {
} }
}, },
'astro:build:done': async ({ routes }) => { '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 // Edge function config
// https://vercel.com/docs/build-output-api/v3#vercel-primitives/edge-functions/configuration // https://vercel.com/docs/build-output-api/v3#vercel-primitives/edge-functions/configuration
await writeJson(new URL(`./.vc-config.json`, functionFolder), { await writeJson(new URL(`./.vc-config.json`, functionFolder), {
runtime: 'edge', runtime: 'edge',
entrypoint: serverEntry, entrypoint: relativePath(commonAncestor, fileURLToPath(entry)),
}); });
// Output configuration // Output configuration

View file

@ -1,5 +1,7 @@
import type { PathLike } from 'node:fs'; import type { PathLike } from 'node:fs';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import nodePath from 'node:path';
import { fileURLToPath } from 'node:url';
export async function writeJson<T>(path: PathLike, data: T) { export async function writeJson<T>(path: PathLike, data: T) {
await fs.writeFile(path, JSON.stringify(data), { encoding: 'utf-8' }); await fs.writeFile(path, JSON.stringify(data), { encoding: 'utf-8' });
@ -15,3 +17,58 @@ export async function emptyDir(dir: PathLike): Promise<void> {
} }
export const getVercelOutput = (root: URL) => new URL('./.vercel/output/', root); 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<string>} The common ancestor of the copied files.
*/
export async function copyFilesToFunction(
files: URL[],
outDir: URL,
exclude: URL[] = []
): Promise<string> {
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;
}

View file

@ -1,12 +1,20 @@
import { nodeFileTrace } from '@vercel/nft'; import { nodeFileTrace } from '@vercel/nft';
import * as fs from 'node:fs/promises'; import { relative as relativePath } from 'node:path';
import nodePath from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
export async function copyDependenciesToFunction( import { copyFilesToFunction } from './fs.js';
entry: URL,
outDir: URL export async function copyDependenciesToFunction({
): Promise<{ handler: string }> { entry,
outDir,
includeFiles,
excludeFiles,
}: {
entry: URL;
outDir: URL;
includeFiles: URL[];
excludeFiles: URL[];
}): Promise<{ handler: string }> {
const entryPath = fileURLToPath(entry); const entryPath = fileURLToPath(entry);
// Get root of folder of the system (like C:\ on Windows or / on Linux) // 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), base: fileURLToPath(base),
}); });
if (result.fileList.size === 0) throw new Error('[@astrojs/vercel] No files found');
for (const error of result.warnings) { for (const error of result.warnings) {
if (error.message.startsWith('Failed to resolve dependency')) { if (error.message.startsWith('Failed to resolve dependency')) {
const [, module, file] = /Cannot find module '(.+?)' loaded from (.+)/.exec(error.message)!; const [, module, file] = /Cannot find module '(.+?)' loaded from (.+)/.exec(error.message)!;
@ -42,49 +48,14 @@ export async function copyDependenciesToFunction(
} }
} }
const fileList = [...result.fileList]; const commonAncestor = await copyFilesToFunction(
[...result.fileList].map((file) => new URL(file, base)).concat(includeFiles),
let commonAncestor = nodePath.dirname(fileList[0]); outDir,
for (const file of fileList.slice(1)) { excludeFiles
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);
}
}
return { return {
// serverEntry location inside the outDir // serverEntry location inside the outDir
handler: nodePath.relative(nodePath.join(fileURLToPath(base), commonAncestor), entryPath), handler: relativePath(commonAncestor, entryPath),
}; };
} }

View file

@ -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 _config: AstroConfig;
let buildTempFolder: URL; let buildTempFolder: URL;
let functionFolder: URL; let functionFolder: URL;
@ -59,10 +67,12 @@ export default function vercelEdge(): AstroIntegration {
}, },
'astro:build:done': async ({ routes }) => { 'astro:build:done': async ({ routes }) => {
// Copy necessary files (e.g. node_modules/) // Copy necessary files (e.g. node_modules/)
const { handler } = await copyDependenciesToFunction( const { handler } = await copyDependenciesToFunction({
new URL(serverEntry, buildTempFolder), entry: new URL(serverEntry, buildTempFolder),
functionFolder outDir: functionFolder,
); includeFiles: includeFiles?.map((file) => new URL(file, _config.root)) || [],
excludeFiles: excludeFiles?.map((file) => new URL(file, _config.root)) || [],
});
// Remove temporary folder // Remove temporary folder
await removeDir(buildTempFolder); await removeDir(buildTempFolder);