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:
parent
f8198d2502
commit
cd25abae59
6 changed files with 168 additions and 60 deletions
5
.changeset/violet-buckets-call.md
Normal file
5
.changeset/violet-buckets-call.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@astrojs/vercel': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added `includeFiles` and `excludeFiles` options
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue