Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vercel): includeFiles and excludeFiles #5085

Merged
merged 7 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/violet-buckets-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/vercel': minor
---

Added `includeFiles` and `excludeFiles` options
44 changes: 43 additions & 1 deletion packages/integrations/vercel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 28 additions & 5 deletions packages/integrations/vercel/src/edge/adapter.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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 }) => {
Expand Down Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions packages/integrations/vercel/src/lib/fs.ts
Original file line number Diff line number Diff line change
@@ -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<T>(path: PathLike, data: T) {
await fs.writeFile(path, JSON.stringify(data), { encoding: 'utf-8' });
Expand All @@ -15,3 +17,58 @@ export async function emptyDir(dir: PathLike): Promise<void> {
}

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;
}
69 changes: 20 additions & 49 deletions packages/integrations/vercel/src/lib/nft.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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)!;
Expand All @@ -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),
};
}
20 changes: 15 additions & 5 deletions packages/integrations/vercel/src/serverless/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down