summaryrefslogtreecommitdiff
path: root/packages/integrations/vercel/src
diff options
context:
space:
mode:
authorGravatar Juan Martín Seery <me@juanm04.com> 2022-05-11 18:10:38 -0300
committerGravatar GitHub <noreply@github.com> 2022-05-11 15:10:38 -0600
commit114bf63e11f28299b13178ef1a412eed37ab7909 (patch)
tree80e3512af2a5ff86b662444f57994cfd5c25fc2c /packages/integrations/vercel/src
parent46cd8b9eb4c5e9b526a6cba288070630b8dcbbf5 (diff)
downloadastro-114bf63e11f28299b13178ef1a412eed37ab7909.tar.gz
astro-114bf63e11f28299b13178ef1a412eed37ab7909.tar.zst
astro-114bf63e11f28299b13178ef1a412eed37ab7909.zip
refactor(vercel): Build Output API v3 (#3216)
* Removed ignores * Migration to v3 * More changes * Remove legacy redirects * Fail when there is no ENABLE_VC_BUILD * Fix edge * Updated readme * Changeset * Added static mode * Updated documentation * Updated shim * Made edge work! * Updated changeset * Ensure empty dir * Fixed redirects for dynamic paths * Removed extra declaration * Splited imports * Updated readme * Fixed some urls * Deprecated shim! * [test]: Vercel NFT * Beautify * Edge bundle to node 14.19 Vercel runs 14.19.1 (I've checked it manually) * Re-added shim (#3304) * Added `node:` prefix * Use the same bundling as Deno for Edge * Remove esbuild * Fixed shim * Moved nft * Updated changeset * Added note about Edge * fix typo * Added support for Node 16 (vercel/vercel#7772)
Diffstat (limited to 'packages/integrations/vercel/src')
-rw-r--r--packages/integrations/vercel/src/edge/adapter.ts74
-rw-r--r--packages/integrations/vercel/src/edge/entrypoint.ts21
-rw-r--r--packages/integrations/vercel/src/edge/shim.ts1
-rw-r--r--packages/integrations/vercel/src/index.ts149
-rw-r--r--packages/integrations/vercel/src/lib/fs.ts13
-rw-r--r--packages/integrations/vercel/src/lib/nft.ts38
-rw-r--r--packages/integrations/vercel/src/lib/redirects.ts83
-rw-r--r--packages/integrations/vercel/src/serverless/adapter.ts78
-rw-r--r--packages/integrations/vercel/src/serverless/entrypoint.ts (renamed from packages/integrations/vercel/src/server-entrypoint.ts)2
-rw-r--r--packages/integrations/vercel/src/serverless/request-transform.ts (renamed from packages/integrations/vercel/src/request-transform.ts)4
-rw-r--r--packages/integrations/vercel/src/static/adapter.ts50
11 files changed, 361 insertions, 152 deletions
diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts
new file mode 100644
index 000000000..664eb2c43
--- /dev/null
+++ b/packages/integrations/vercel/src/edge/adapter.ts
@@ -0,0 +1,74 @@
+import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
+
+import { writeJson, getVercelOutput } from '../lib/fs.js';
+import { getRedirects } from '../lib/redirects.js';
+
+const PACKAGE_NAME = '@astrojs/vercel/edge';
+
+function getAdapter(): AstroAdapter {
+ return {
+ name: PACKAGE_NAME,
+ serverEntrypoint: `${PACKAGE_NAME}/entrypoint`,
+ exports: ['default'],
+ };
+}
+
+export default function vercelEdge(): AstroIntegration {
+ let _config: AstroConfig;
+ let functionFolder: URL;
+ let serverEntry: string;
+
+ return {
+ name: PACKAGE_NAME,
+ hooks: {
+ 'astro:config:setup': ({ config }) => {
+ config.outDir = getVercelOutput(config.root);
+ },
+ 'astro:config:done': ({ setAdapter, config }) => {
+ setAdapter(getAdapter());
+ _config = config;
+ },
+ 'astro:build:setup': ({ vite, target }) => {
+ if (target === 'server') {
+ vite.resolve ||= {};
+ vite.resolve.alias ||= {};
+ const alias = vite.resolve.alias as Record<string, string>;
+ alias['react-dom/server'] = 'react-dom/server.browser';
+ vite.ssr = {
+ noExternal: true,
+ };
+ }
+ },
+ 'astro:build:start': async ({ buildConfig }) => {
+ if (String(process.env.ENABLE_VC_BUILD) !== '1') {
+ throw new Error(
+ `The enviroment variable "ENABLE_VC_BUILD" was not found. Make sure you have it set to "1" in your Vercel project.\nLearn how to set enviroment variables here: https://vercel.com/docs/concepts/projects/environment-variables`
+ );
+ }
+
+ buildConfig.serverEntry = serverEntry = 'entry.mjs';
+ buildConfig.client = new URL('./static/', _config.outDir);
+ buildConfig.server = functionFolder = new URL('./functions/render.func/', _config.outDir);
+ },
+ 'astro:build:done': async ({ routes }) => {
+ // 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,
+ });
+
+ // Output configuration
+ // https://vercel.com/docs/build-output-api/v3#build-output-configuration
+ await writeJson(new URL(`./config.json`, _config.outDir), {
+ version: 3,
+ routes: [
+ ...getRedirects(routes, _config),
+ { handle: 'filesystem' },
+ { src: '/.*', middlewarePath: 'render' },
+ ],
+ });
+ },
+ },
+ };
+}
diff --git a/packages/integrations/vercel/src/edge/entrypoint.ts b/packages/integrations/vercel/src/edge/entrypoint.ts
new file mode 100644
index 000000000..af1496f60
--- /dev/null
+++ b/packages/integrations/vercel/src/edge/entrypoint.ts
@@ -0,0 +1,21 @@
+import './shim.js';
+
+import type { SSRManifest } from 'astro';
+import { App } from 'astro/app';
+
+export function createExports(manifest: SSRManifest) {
+ const app = new App(manifest);
+
+ const handler = async (request: Request): Promise<Response> => {
+ if (app.match(request)) {
+ return await app.render(request);
+ }
+
+ return new Response(null, {
+ status: 404,
+ statusText: 'Not found',
+ });
+ };
+
+ return { default: handler };
+}
diff --git a/packages/integrations/vercel/src/edge/shim.ts b/packages/integrations/vercel/src/edge/shim.ts
new file mode 100644
index 000000000..1a73feb39
--- /dev/null
+++ b/packages/integrations/vercel/src/edge/shim.ts
@@ -0,0 +1 @@
+process.argv = [];
diff --git a/packages/integrations/vercel/src/index.ts b/packages/integrations/vercel/src/index.ts
deleted file mode 100644
index 838844a08..000000000
--- a/packages/integrations/vercel/src/index.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
-import type { PathLike } from 'fs';
-import fs from 'fs/promises';
-import { fileURLToPath } from 'url';
-import esbuild from 'esbuild';
-
-const writeJson = (path: PathLike, data: any) =>
- fs.writeFile(path, JSON.stringify(data), { encoding: 'utf-8' });
-
-const ENTRYFILE = '__astro_entry';
-
-export function getAdapter(): AstroAdapter {
- return {
- name: '@astrojs/vercel',
- serverEntrypoint: '@astrojs/vercel/server-entrypoint',
- exports: ['default'],
- };
-}
-
-export default function vercel(): AstroIntegration {
- let _config: AstroConfig;
- let _serverEntry: URL;
-
- return {
- name: '@astrojs/vercel',
- hooks: {
- 'astro:config:setup': ({ config }) => {
- config.outDir = new URL('./.output/', config.root);
- config.build.format = 'directory';
- },
- 'astro:config:done': ({ setAdapter, config }) => {
- setAdapter(getAdapter());
- _config = config;
- _serverEntry = new URL(`./server/pages/${ENTRYFILE}.js`, config.outDir);
- },
- 'astro:build:setup': ({ vite, target }) => {
- if (target === 'server') {
- vite.build!.rollupOptions = {
- input: [],
- output: {
- format: 'cjs',
- file: fileURLToPath(_serverEntry),
- dir: undefined,
- entryFileNames: undefined,
- chunkFileNames: undefined,
- assetFileNames: undefined,
- inlineDynamicImports: true,
- },
- };
- }
- },
- 'astro:build:start': async ({ buildConfig }) => {
- buildConfig.serverEntry = `${ENTRYFILE}.js`;
- buildConfig.client = new URL('./static/', _config.outDir);
- buildConfig.server = new URL('./server/pages/', _config.outDir);
-
- if (String(process.env.ENABLE_FILE_SYSTEM_API) !== '1') {
- console.warn(
- `The enviroment variable "ENABLE_FILE_SYSTEM_API" was not found. Make sure you have it set to "1" in your Vercel project.\nLearn how to set enviroment variables here: https://vercel.com/docs/concepts/projects/environment-variables`
- );
- }
- },
- 'astro:build:done': async ({ routes }) => {
- // Bundle dependecies
- await esbuild.build({
- entryPoints: [fileURLToPath(_serverEntry)],
- outfile: fileURLToPath(_serverEntry),
- bundle: true,
- format: 'cjs',
- platform: 'node',
- target: 'node14',
- allowOverwrite: true,
- minifyWhitespace: true,
- });
-
- let staticRoutes: RouteData[] = [];
- let dynamicRoutes: RouteData[] = [];
-
- for (const route of routes) {
- if (route.params.length === 0) staticRoutes.push(route);
- else dynamicRoutes.push(route);
- }
-
- // Routes Manifest
- // https://vercel.com/docs/file-system-api#configuration/routes
- await writeJson(new URL(`./routes-manifest.json`, _config.outDir), {
- version: 3,
- basePath: '/',
- pages404: false,
- redirects:
- _config.trailingSlash !== 'ignore'
- ? routes
- .filter((route) => route.type === 'page' && !route.pathname?.endsWith('/'))
- .map((route) => {
- const path =
- '/' +
- route.segments
- .map((segments) =>
- segments
- .map((part) =>
- part.spread
- ? `:${part.content}*`
- : part.dynamic
- ? `:${part.content}`
- : part.content
- )
- .join('')
- )
- .join('/');
-
- let source, destination;
-
- if (_config.trailingSlash === 'always') {
- source = path;
- destination = path + '/';
- } else {
- source = path + '/';
- destination = path;
- }
-
- return { source, destination, statusCode: 308 };
- })
- : undefined,
- rewrites: staticRoutes.map((route) => {
- let source = route.pathname as string;
-
- if (
- route.type === 'page' &&
- _config.trailingSlash === 'always' &&
- !source.endsWith('/')
- ) {
- source += '/';
- }
-
- return {
- source,
- regex: route.pattern.toString(),
- destination: `/${ENTRYFILE}`,
- };
- }),
- dynamicRoutes: dynamicRoutes.map((route) => ({
- page: `/${ENTRYFILE}`,
- regex: route.pattern.toString(),
- })),
- });
- },
- },
- };
-}
diff --git a/packages/integrations/vercel/src/lib/fs.ts b/packages/integrations/vercel/src/lib/fs.ts
new file mode 100644
index 000000000..e192ba554
--- /dev/null
+++ b/packages/integrations/vercel/src/lib/fs.ts
@@ -0,0 +1,13 @@
+import type { PathLike } from 'node:fs';
+import * as fs from 'node:fs/promises';
+
+export async function writeJson<T extends any>(path: PathLike, data: T) {
+ await fs.writeFile(path, JSON.stringify(data), { encoding: 'utf-8' });
+}
+
+export async function emptyDir(dir: PathLike): Promise<void> {
+ await fs.rm(dir, { recursive: true, force: true, maxRetries: 3 });
+ await fs.mkdir(dir, { recursive: true });
+}
+
+export const getVercelOutput = (root: URL) => new URL('./.vercel/output/', root);
diff --git a/packages/integrations/vercel/src/lib/nft.ts b/packages/integrations/vercel/src/lib/nft.ts
new file mode 100644
index 000000000..d88429714
--- /dev/null
+++ b/packages/integrations/vercel/src/lib/nft.ts
@@ -0,0 +1,38 @@
+import * as fs from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
+import { nodeFileTrace } from '@vercel/nft';
+
+export async function copyDependenciesToFunction(
+ root: URL,
+ functionFolder: URL,
+ serverEntry: string
+) {
+ const entryPath = fileURLToPath(new URL(`./${serverEntry}`, functionFolder));
+
+ const result = await nodeFileTrace([entryPath], {
+ base: fileURLToPath(root),
+ });
+
+ for (const file of result.fileList) {
+ if (file.startsWith('.vercel/')) continue;
+ const origin = new URL(file, root);
+ const dest = new URL(file, functionFolder);
+
+ const meta = await fs.stat(origin);
+ const isSymlink = (await fs.lstat(origin)).isSymbolicLink();
+
+ // Create directories recursively
+ if (meta.isDirectory() && !isSymlink) {
+ await fs.mkdir(new URL('..', dest), { recursive: true });
+ } else {
+ await fs.mkdir(new URL('.', dest), { recursive: true });
+ }
+
+ if (isSymlink) {
+ const link = await fs.readlink(origin);
+ await fs.symlink(link, dest, meta.isDirectory() ? 'dir' : 'file');
+ } else {
+ await fs.copyFile(origin, dest);
+ }
+ }
+}
diff --git a/packages/integrations/vercel/src/lib/redirects.ts b/packages/integrations/vercel/src/lib/redirects.ts
new file mode 100644
index 000000000..b52ce8725
--- /dev/null
+++ b/packages/integrations/vercel/src/lib/redirects.ts
@@ -0,0 +1,83 @@
+import type { AstroConfig, RoutePart, RouteData } from 'astro';
+
+// https://vercel.com/docs/project-configuration#legacy/routes
+interface VercelRoute {
+ src: string;
+ methods?: string[];
+ dest?: string;
+ headers?: Record<string, string>;
+ status?: number;
+ continue?: boolean;
+}
+
+// Copied from /home/juanm04/dev/misc/astro/packages/astro/src/core/routing/manifest/create.ts
+// 2022-04-26
+function getMatchPattern(segments: RoutePart[][]) {
+ return segments
+ .map((segment) => {
+ return segment[0].spread
+ ? '(?:\\/(.*?))?'
+ : '\\/' +
+ segment
+ .map((part) => {
+ if (part)
+ return part.dynamic
+ ? '([^/]+?)'
+ : part.content
+ .normalize()
+ .replace(/\?/g, '%3F')
+ .replace(/#/g, '%23')
+ .replace(/%5B/g, '[')
+ .replace(/%5D/g, ']')
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ })
+ .join('');
+ })
+ .join('');
+}
+
+function getReplacePattern(segments: RoutePart[][]) {
+ let n = 0;
+ let result = '';
+
+ for (const segment of segments) {
+ for (const part of segment) {
+ if (part.dynamic) result += '$' + ++n;
+ else result += part.content;
+ }
+ result += '/';
+ }
+
+ // Remove trailing slash
+ result = result.slice(0, -1);
+
+ return result;
+}
+
+export function getRedirects(routes: RouteData[], config: AstroConfig): VercelRoute[] {
+ let redirects: VercelRoute[] = [];
+
+ if (config.trailingSlash === 'always') {
+ for (const route of routes) {
+ if (route.type !== 'page' || route.segments.length === 0) continue;
+
+ redirects.push({
+ src: config.base + getMatchPattern(route.segments),
+ headers: { Location: config.base + getReplacePattern(route.segments) + '/' },
+ status: 308,
+ });
+ }
+ } else if (config.trailingSlash === 'never') {
+ for (const route of routes) {
+ if (route.type !== 'page' || route.segments.length === 0) continue;
+
+ redirects.push({
+ src: config.base + getMatchPattern(route.segments) + '/',
+ headers: { Location: config.base + getReplacePattern(route.segments) },
+ status: 308,
+ });
+ }
+ }
+
+ return redirects;
+}
diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts
new file mode 100644
index 000000000..babda1f84
--- /dev/null
+++ b/packages/integrations/vercel/src/serverless/adapter.ts
@@ -0,0 +1,78 @@
+import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
+
+import { writeJson, getVercelOutput } from '../lib/fs.js';
+import { copyDependenciesToFunction } from '../lib/nft.js';
+import { getRedirects } from '../lib/redirects.js';
+
+const PACKAGE_NAME = '@astrojs/vercel/serverless';
+
+function getAdapter(): AstroAdapter {
+ return {
+ name: PACKAGE_NAME,
+ serverEntrypoint: `${PACKAGE_NAME}/entrypoint`,
+ exports: ['default'],
+ };
+}
+
+export interface Options {
+ nodeVersion?: '12.x' | '14.x' | '16.x';
+}
+
+export default function vercelEdge({ nodeVersion = '16.x' }: Options = {}): AstroIntegration {
+ let _config: AstroConfig;
+ let functionFolder: URL;
+ let serverEntry: string;
+
+ return {
+ name: PACKAGE_NAME,
+ hooks: {
+ 'astro:config:setup': ({ config }) => {
+ config.outDir = getVercelOutput(config.root);
+ },
+ 'astro:config:done': ({ setAdapter, config }) => {
+ setAdapter(getAdapter());
+ _config = config;
+ },
+ 'astro:build:start': async ({ buildConfig }) => {
+ if (String(process.env.ENABLE_VC_BUILD) !== '1') {
+ throw new Error(
+ `The enviroment variable "ENABLE_VC_BUILD" was not found. Make sure you have it set to "1" in your Vercel project.\nLearn how to set enviroment variables here: https://vercel.com/docs/concepts/projects/environment-variables`
+ );
+ }
+
+ buildConfig.serverEntry = serverEntry = 'entry.js';
+ buildConfig.client = new URL('./static/', _config.outDir);
+ buildConfig.server = functionFolder = new URL('./functions/render.func/', _config.outDir);
+ },
+ 'astro:build:done': async ({ routes }) => {
+ // Copy necessary files (e.g. node_modules/)
+ await copyDependenciesToFunction(_config.root, functionFolder, serverEntry);
+
+ // Enable ESM
+ // https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/
+ await writeJson(new URL(`./package.json`, functionFolder), {
+ type: 'module',
+ });
+
+ // Serverless function config
+ // https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration
+ await writeJson(new URL(`./.vc-config.json`, functionFolder), {
+ runtime: `nodejs${nodeVersion}`,
+ handler: serverEntry,
+ launcherType: 'Nodejs',
+ });
+
+ // Output configuration
+ // https://vercel.com/docs/build-output-api/v3#build-output-configuration
+ await writeJson(new URL(`./config.json`, _config.outDir), {
+ version: 3,
+ routes: [
+ ...getRedirects(routes, _config),
+ { handle: 'filesystem' },
+ { src: '/.*', dest: 'render' },
+ ],
+ });
+ },
+ },
+ };
+}
diff --git a/packages/integrations/vercel/src/server-entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts
index e32dbdd86..6ef7ccef9 100644
--- a/packages/integrations/vercel/src/server-entrypoint.ts
+++ b/packages/integrations/vercel/src/serverless/entrypoint.ts
@@ -1,7 +1,7 @@
import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
import { polyfill } from '@astrojs/webapi';
-import type { IncomingMessage, ServerResponse } from 'http';
+import type { IncomingMessage, ServerResponse } from 'node:http';
import { getRequest, setResponse } from './request-transform.js';
diff --git a/packages/integrations/vercel/src/request-transform.ts b/packages/integrations/vercel/src/serverless/request-transform.ts
index 0a87ca642..7cdb2550a 100644
--- a/packages/integrations/vercel/src/request-transform.ts
+++ b/packages/integrations/vercel/src/serverless/request-transform.ts
@@ -1,5 +1,5 @@
-import { Readable } from 'stream';
-import type { IncomingMessage, ServerResponse } from 'http';
+import { Readable } from 'node:stream';
+import type { IncomingMessage, ServerResponse } from 'node:http';
/*
Credits to the SvelteKit team
diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts
new file mode 100644
index 000000000..489ab356b
--- /dev/null
+++ b/packages/integrations/vercel/src/static/adapter.ts
@@ -0,0 +1,50 @@
+import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
+
+import { getVercelOutput, writeJson, emptyDir } from '../lib/fs.js';
+import { getRedirects } from '../lib/redirects.js';
+
+const PACKAGE_NAME = '@astrojs/vercel/static';
+
+function getAdapter(): AstroAdapter {
+ return { name: PACKAGE_NAME };
+}
+
+export default function vercelStatic(): AstroIntegration {
+ let _config: AstroConfig;
+
+ return {
+ name: '@astrojs/vercel',
+ hooks: {
+ 'astro:config:setup': ({ config }) => {
+ config.outDir = new URL('./static/', getVercelOutput(config.root));
+ config.build.format = 'directory';
+ },
+ 'astro:config:done': ({ setAdapter, config }) => {
+ setAdapter(getAdapter());
+ _config = config;
+ },
+ 'astro:build:start': async ({ buildConfig }) => {
+ if (String(process.env.ENABLE_VC_BUILD) !== '1') {
+ throw new Error(
+ `The enviroment variable "ENABLE_VC_BUILD" was not found. Make sure you have it set to "1" in your Vercel project.\nLearn how to set enviroment variables here: https://vercel.com/docs/concepts/projects/environment-variables`
+ );
+ }
+
+ buildConfig.staticMode = true;
+
+ // Ensure to have `.vercel/output` empty.
+ // This is because, when building to static, outDir = .vercel/output/static/,
+ // so .vercel/output itself won't get cleaned.
+ await emptyDir(getVercelOutput(_config.root));
+ },
+ 'astro:build:done': async ({ routes }) => {
+ // Output configuration
+ // https://vercel.com/docs/build-output-api/v3#build-output-configuration
+ await writeJson(new URL(`./config.json`, getVercelOutput(_config.root)), {
+ version: 3,
+ routes: [...getRedirects(routes, _config), { handle: 'filesystem' }],
+ });
+ },
+ },
+ };
+}