diff options
author | 2022-05-11 18:10:38 -0300 | |
---|---|---|
committer | 2022-05-11 15:10:38 -0600 | |
commit | 114bf63e11f28299b13178ef1a412eed37ab7909 (patch) | |
tree | 80e3512af2a5ff86b662444f57994cfd5c25fc2c /packages/integrations/vercel/src | |
parent | 46cd8b9eb4c5e9b526a6cba288070630b8dcbbf5 (diff) | |
download | astro-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.ts | 74 | ||||
-rw-r--r-- | packages/integrations/vercel/src/edge/entrypoint.ts | 21 | ||||
-rw-r--r-- | packages/integrations/vercel/src/edge/shim.ts | 1 | ||||
-rw-r--r-- | packages/integrations/vercel/src/index.ts | 149 | ||||
-rw-r--r-- | packages/integrations/vercel/src/lib/fs.ts | 13 | ||||
-rw-r--r-- | packages/integrations/vercel/src/lib/nft.ts | 38 | ||||
-rw-r--r-- | packages/integrations/vercel/src/lib/redirects.ts | 83 | ||||
-rw-r--r-- | packages/integrations/vercel/src/serverless/adapter.ts | 78 | ||||
-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.ts | 50 |
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' }], + }); + }, + }, + }; +} |