summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/astro/src/core/build/vite-plugin-ssr.ts1
-rw-r--r--packages/integrations/vercel/package.json5
-rw-r--r--packages/integrations/vercel/src/index.ts89
-rw-r--r--packages/integrations/vercel/src/request-transform.ts95
-rw-r--r--packages/integrations/vercel/src/server-entrypoint.ts34
-rw-r--r--packages/integrations/vercel/src/shims.ts5
-rw-r--r--pnpm-lock.yaml4
7 files changed, 169 insertions, 64 deletions
diff --git a/packages/astro/src/core/build/vite-plugin-ssr.ts b/packages/astro/src/core/build/vite-plugin-ssr.ts
index f057dc2ce..d56543232 100644
--- a/packages/astro/src/core/build/vite-plugin-ssr.ts
+++ b/packages/astro/src/core/build/vite-plugin-ssr.ts
@@ -42,6 +42,7 @@ ${
adapter.exports
? `const _exports = adapter.createExports(_manifest, _args);
${adapter.exports.map((name) => `export const ${name} = _exports['${name}'];`).join('\n')}
+${adapter.exports.includes('_default') ? `export default _default` : ''}
`
: ''
}
diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json
index d7713183c..53dc7327b 100644
--- a/packages/integrations/vercel/package.json
+++ b/packages/integrations/vercel/package.json
@@ -15,6 +15,7 @@
"homepage": "https://astro.build",
"exports": {
".": "./dist/index.js",
+ "./server-entrypoint": "./dist/server-entrypoint.js",
"./package.json": "./package.json"
},
"scripts": {
@@ -22,9 +23,7 @@
"dev": "astro-scripts dev \"src/**/*.ts\""
},
"dependencies": {
- "@astrojs/webapi": "^0.11.0",
- "esbuild": "0.14.25",
- "globby": "^12.2.0"
+ "@astrojs/webapi": "^0.11.0"
},
"devDependencies": {
"astro": "workspace:*",
diff --git a/packages/integrations/vercel/src/index.ts b/packages/integrations/vercel/src/index.ts
index 5d6794206..648f624b0 100644
--- a/packages/integrations/vercel/src/index.ts
+++ b/packages/integrations/vercel/src/index.ts
@@ -1,79 +1,64 @@
-import type { AstroIntegration, AstroConfig } from 'astro';
-import type { IncomingMessage, ServerResponse } from 'http';
+import type { AstroAdapter, AstroIntegration } from 'astro';
import type { PathLike } from 'fs';
-
import fs from 'fs/promises';
-import { fileURLToPath } from 'url';
-import { globby } from 'globby';
-import esbuild from 'esbuild';
-
-export type VercelRequest = IncomingMessage;
-export type VercelResponse = ServerResponse;
-export type VercelHandler = (request: VercelRequest, response: VercelResponse) => void | Promise<void>;
const writeJson = (path: PathLike, data: any) => fs.writeFile(path, JSON.stringify(data), { encoding: 'utf-8' });
-const ENDPOINT_GLOB = 'api/**/*.{js,ts,tsx}';
+export function getAdapter(): AstroAdapter {
+ return {
+ name: '@astrojs/vercel',
+ serverEntrypoint: '@astrojs/vercel/server-entrypoint',
+ exports: ['_default'],
+ };
+}
-function vercelFunctions(): AstroIntegration {
- let _config: AstroConfig;
- let output: URL;
+export default function vercel(): AstroIntegration {
+ let entryFile: string;
return {
name: '@astrojs/vercel',
hooks: {
- 'astro:config:setup': ({ config, ignorePages }) => {
- output = new URL('./.output/', config.projectRoot);
- config.dist = new URL('./static/', output);
+ 'astro:config:setup': ({ config }) => {
+ config.dist = new URL('./.output/', config.projectRoot);
config.buildOptions.pageUrlFormat = 'directory';
- ignorePages(ENDPOINT_GLOB);
},
- 'astro:config:done': async ({ config }) => {
- _config = config;
+ 'astro:config:done': ({ setAdapter }) => {
+ setAdapter(getAdapter());
},
- 'astro:build:start': async () => {
- await fs.rm(output, { recursive: true, force: true });
+ 'astro:build:start': async ({ buildConfig, config }) => {
+ entryFile = buildConfig.serverEntry;
+ buildConfig.client = new URL('./static/', config.dist);
+ buildConfig.server = new URL('./functions/', config.dist);
},
- 'astro:build:done': async ({ pages }) => {
- // Split pages from the rest of files
- await Promise.all(
- pages.map(async ({ pathname }) => {
- const origin = new URL(`./static/${pathname}index.html`, output);
- const finalDir = new URL(`./server/pages/${pathname}`, output);
-
- await fs.mkdir(finalDir, { recursive: true });
- await fs.copyFile(origin, new URL(`./index.html`, finalDir));
- await fs.rm(origin);
- })
- );
+ 'astro:build:done': async ({ dir, routes }) => {
+ await writeJson(new URL(`./functions/package.json`, dir), {
+ type: 'commonjs',
+ });
// Routes Manifest
// https://vercel.com/docs/file-system-api#configuration/routes
- await writeJson(new URL(`./routes-manifest.json`, output), {
+ await writeJson(new URL(`./routes-manifest.json`, dir), {
version: 3,
basePath: '/',
pages404: false,
+ rewrites: routes.map((route) => ({
+ source: route.pathname,
+ destination: '/__astro_entry',
+ })),
});
- const endpoints = await globby([ENDPOINT_GLOB, '!_*'], { onlyFiles: true, cwd: _config.pages });
-
- if (endpoints.length === 0) return;
-
- await esbuild.build({
- entryPoints: endpoints.map((endpoint) => new URL(endpoint, _config.pages)).map(fileURLToPath),
- outdir: fileURLToPath(new URL('./server/pages/api/', output)),
- outbase: fileURLToPath(new URL('./api/', _config.pages)),
- inject: [fileURLToPath(new URL('./shims.js', import.meta.url))],
- bundle: true,
- target: 'node14',
- platform: 'node',
- format: 'cjs',
+ // Functions Manifest
+ // https://vercel.com/docs/file-system-api#configuration/functions
+ await writeJson(new URL(`./functions-manifest.json`, dir), {
+ version: 1,
+ pages: {
+ __astro_entry: {
+ runtime: 'nodejs14',
+ handler: `functions/${entryFile}`,
+ },
+ },
});
-
- await writeJson(new URL(`./package.json`, output), { type: 'commonjs' });
},
},
};
}
-
-export default vercelFunctions;
diff --git a/packages/integrations/vercel/src/request-transform.ts b/packages/integrations/vercel/src/request-transform.ts
new file mode 100644
index 000000000..0a87ca642
--- /dev/null
+++ b/packages/integrations/vercel/src/request-transform.ts
@@ -0,0 +1,95 @@
+import { Readable } from 'stream';
+import type { IncomingMessage, ServerResponse } from 'http';
+
+/*
+ Credits to the SvelteKit team
+ https://github.com/sveltejs/kit/blob/69913e9fda054fa6a62a80e2bb4ee7dca1005796/packages/kit/src/node.js
+*/
+
+function get_raw_body(req: IncomingMessage) {
+ return new Promise<Uint8Array | null>((fulfil, reject) => {
+ const h = req.headers;
+
+ if (!h['content-type']) {
+ return fulfil(null);
+ }
+
+ req.on('error', reject);
+
+ const length = Number(h['content-length']);
+
+ // https://github.com/jshttp/type-is/blob/c1f4388c71c8a01f79934e68f630ca4a15fffcd6/index.js#L81-L95
+ if (isNaN(length) && h['transfer-encoding'] == null) {
+ return fulfil(null);
+ }
+
+ let data = new Uint8Array(length || 0);
+
+ if (length > 0) {
+ let offset = 0;
+ req.on('data', (chunk) => {
+ const new_len = offset + Buffer.byteLength(chunk);
+
+ if (new_len > length) {
+ return reject({
+ status: 413,
+ reason: 'Exceeded "Content-Length" limit',
+ });
+ }
+
+ data.set(chunk, offset);
+ offset = new_len;
+ });
+ } else {
+ req.on('data', (chunk) => {
+ const new_data = new Uint8Array(data.length + chunk.length);
+ new_data.set(data, 0);
+ new_data.set(chunk, data.length);
+ data = new_data;
+ });
+ }
+
+ req.on('end', () => {
+ fulfil(data);
+ });
+ });
+}
+
+export async function getRequest(base: string, req: IncomingMessage): Promise<Request> {
+ let headers = req.headers as Record<string, string>;
+ if (req.httpVersionMajor === 2) {
+ // we need to strip out the HTTP/2 pseudo-headers because node-fetch's
+ // Request implementation doesn't like them
+ headers = Object.assign({}, headers);
+ delete headers[':method'];
+ delete headers[':path'];
+ delete headers[':authority'];
+ delete headers[':scheme'];
+ }
+ return new Request(base + req.url, {
+ method: req.method,
+ headers,
+ body: await get_raw_body(req), // TODO stream rather than buffer
+ });
+}
+
+export async function setResponse(res: ServerResponse, response: Response): Promise<void> {
+ const headers = Object.fromEntries(response.headers);
+
+ if (response.headers.has('set-cookie')) {
+ // @ts-expect-error (headers.raw() is non-standard)
+ headers['set-cookie'] = response.headers.raw()['set-cookie'];
+ }
+
+ res.writeHead(response.status, headers);
+
+ if (response.body instanceof Readable) {
+ response.body.pipe(res);
+ } else {
+ if (response.body) {
+ res.write(await response.arrayBuffer());
+ }
+
+ res.end();
+ }
+}
diff --git a/packages/integrations/vercel/src/server-entrypoint.ts b/packages/integrations/vercel/src/server-entrypoint.ts
new file mode 100644
index 000000000..df01fe32a
--- /dev/null
+++ b/packages/integrations/vercel/src/server-entrypoint.ts
@@ -0,0 +1,34 @@
+import type { SSRManifest } from 'astro';
+import { App } from 'astro/app';
+import { polyfill } from '@astrojs/webapi';
+import type { IncomingMessage, ServerResponse } from 'http';
+
+import { getRequest, setResponse } from './request-transform.js';
+
+polyfill(globalThis, {
+ exclude: 'window document',
+});
+
+export const createExports = (manifest: SSRManifest) => {
+ const app = new App(manifest);
+
+ const _default = async (req: IncomingMessage, res: ServerResponse) => {
+ let request: Request;
+
+ try {
+ request = await getRequest(`https://${req.headers.host}`, req);
+ } catch (err: any) {
+ res.statusCode = err.status || 400;
+ return res.end(err.reason || 'Invalid request body');
+ }
+
+ if (!app.match(request)) {
+ res.statusCode = 404;
+ return res.end('Not found');
+ }
+
+ await setResponse(res, await app.render(request));
+ };
+
+ return { _default };
+};
diff --git a/packages/integrations/vercel/src/shims.ts b/packages/integrations/vercel/src/shims.ts
deleted file mode 100644
index 01f7b39bf..000000000
--- a/packages/integrations/vercel/src/shims.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { polyfill } from '@astrojs/webapi';
-
-polyfill(globalThis, {
- exclude: 'window document',
-});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 997848edc..efa7ac934 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1350,12 +1350,8 @@ importers:
'@astrojs/webapi': ^0.11.0
astro: workspace:*
astro-scripts: workspace:*
- esbuild: 0.14.25
- globby: ^12.2.0
dependencies:
'@astrojs/webapi': link:../../webapi
- esbuild: 0.14.25
- globby: 12.2.0
devDependencies:
astro: link:../../astro
astro-scripts: link:../../../scripts