mirror of
https://github.com/withastro/astro.git
synced 2025-01-06 22:10:10 -05:00
Rework - now it's all SSR
This commit is contained in:
parent
1278d39b86
commit
697ff4dfb4
7 changed files with 169 additions and 64 deletions
|
@ -42,6 +42,7 @@ ${
|
||||||
adapter.exports
|
adapter.exports
|
||||||
? `const _exports = adapter.createExports(_manifest, _args);
|
? `const _exports = adapter.createExports(_manifest, _args);
|
||||||
${adapter.exports.map((name) => `export const ${name} = _exports['${name}'];`).join('\n')}
|
${adapter.exports.map((name) => `export const ${name} = _exports['${name}'];`).join('\n')}
|
||||||
|
${adapter.exports.includes('_default') ? `export default _default` : ''}
|
||||||
`
|
`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
"homepage": "https://astro.build",
|
"homepage": "https://astro.build",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
|
"./server-entrypoint": "./dist/server-entrypoint.js",
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -22,9 +23,7 @@
|
||||||
"dev": "astro-scripts dev \"src/**/*.ts\""
|
"dev": "astro-scripts dev \"src/**/*.ts\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/webapi": "^0.11.0",
|
"@astrojs/webapi": "^0.11.0"
|
||||||
"esbuild": "0.14.25",
|
|
||||||
"globby": "^12.2.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"astro": "workspace:*",
|
"astro": "workspace:*",
|
||||||
|
|
|
@ -1,79 +1,64 @@
|
||||||
import type { AstroIntegration, AstroConfig } from 'astro';
|
import type { AstroAdapter, AstroIntegration } from 'astro';
|
||||||
import type { IncomingMessage, ServerResponse } from 'http';
|
|
||||||
import type { PathLike } from 'fs';
|
import type { PathLike } from 'fs';
|
||||||
|
|
||||||
import fs from 'fs/promises';
|
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 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 {
|
export default function vercel(): AstroIntegration {
|
||||||
let _config: AstroConfig;
|
let entryFile: string;
|
||||||
let output: URL;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: '@astrojs/vercel',
|
name: '@astrojs/vercel',
|
||||||
hooks: {
|
hooks: {
|
||||||
'astro:config:setup': ({ config, ignorePages }) => {
|
'astro:config:setup': ({ config }) => {
|
||||||
output = new URL('./.output/', config.projectRoot);
|
config.dist = new URL('./.output/', config.projectRoot);
|
||||||
config.dist = new URL('./static/', output);
|
|
||||||
config.buildOptions.pageUrlFormat = 'directory';
|
config.buildOptions.pageUrlFormat = 'directory';
|
||||||
ignorePages(ENDPOINT_GLOB);
|
|
||||||
},
|
},
|
||||||
'astro:config:done': async ({ config }) => {
|
'astro:config:done': ({ setAdapter }) => {
|
||||||
_config = config;
|
setAdapter(getAdapter());
|
||||||
},
|
},
|
||||||
'astro:build:start': async () => {
|
'astro:build:start': async ({ buildConfig, config }) => {
|
||||||
await fs.rm(output, { recursive: true, force: true });
|
entryFile = buildConfig.serverEntry;
|
||||||
|
buildConfig.client = new URL('./static/', config.dist);
|
||||||
|
buildConfig.server = new URL('./functions/', config.dist);
|
||||||
},
|
},
|
||||||
'astro:build:done': async ({ pages }) => {
|
'astro:build:done': async ({ dir, routes }) => {
|
||||||
// Split pages from the rest of files
|
await writeJson(new URL(`./functions/package.json`, dir), {
|
||||||
await Promise.all(
|
type: 'commonjs',
|
||||||
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);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Routes Manifest
|
// Routes Manifest
|
||||||
// https://vercel.com/docs/file-system-api#configuration/routes
|
// 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,
|
version: 3,
|
||||||
basePath: '/',
|
basePath: '/',
|
||||||
pages404: false,
|
pages404: false,
|
||||||
|
rewrites: routes.map((route) => ({
|
||||||
|
source: route.pathname,
|
||||||
|
destination: '/__astro_entry',
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const endpoints = await globby([ENDPOINT_GLOB, '!_*'], { onlyFiles: true, cwd: _config.pages });
|
// Functions Manifest
|
||||||
|
// https://vercel.com/docs/file-system-api#configuration/functions
|
||||||
if (endpoints.length === 0) return;
|
await writeJson(new URL(`./functions-manifest.json`, dir), {
|
||||||
|
version: 1,
|
||||||
await esbuild.build({
|
pages: {
|
||||||
entryPoints: endpoints.map((endpoint) => new URL(endpoint, _config.pages)).map(fileURLToPath),
|
__astro_entry: {
|
||||||
outdir: fileURLToPath(new URL('./server/pages/api/', output)),
|
runtime: 'nodejs14',
|
||||||
outbase: fileURLToPath(new URL('./api/', _config.pages)),
|
handler: `functions/${entryFile}`,
|
||||||
inject: [fileURLToPath(new URL('./shims.js', import.meta.url))],
|
},
|
||||||
bundle: true,
|
},
|
||||||
target: 'node14',
|
|
||||||
platform: 'node',
|
|
||||||
format: 'cjs',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeJson(new URL(`./package.json`, output), { type: 'commonjs' });
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default vercelFunctions;
|
|
||||||
|
|
95
packages/integrations/vercel/src/request-transform.ts
Normal file
95
packages/integrations/vercel/src/request-transform.ts
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
34
packages/integrations/vercel/src/server-entrypoint.ts
Normal file
34
packages/integrations/vercel/src/server-entrypoint.ts
Normal file
|
@ -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 };
|
||||||
|
};
|
|
@ -1,5 +0,0 @@
|
||||||
import { polyfill } from '@astrojs/webapi';
|
|
||||||
|
|
||||||
polyfill(globalThis, {
|
|
||||||
exclude: 'window document',
|
|
||||||
});
|
|
|
@ -1350,12 +1350,8 @@ importers:
|
||||||
'@astrojs/webapi': ^0.11.0
|
'@astrojs/webapi': ^0.11.0
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
astro-scripts: workspace:*
|
astro-scripts: workspace:*
|
||||||
esbuild: 0.14.25
|
|
||||||
globby: ^12.2.0
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/webapi': link:../../webapi
|
'@astrojs/webapi': link:../../webapi
|
||||||
esbuild: 0.14.25
|
|
||||||
globby: 12.2.0
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
astro: link:../../astro
|
astro: link:../../astro
|
||||||
astro-scripts: link:../../../scripts
|
astro-scripts: link:../../../scripts
|
||||||
|
|
Loading…
Reference in a new issue