Adapters v0 (#2855)
* Adapter v0 * Finalizing adapters * Update the lockfile * Add the default adapter after config setup is called * Create the default adapter in config:done * Fix lint error * Remove unused callConfigSetup * remove unused export * Use a test adapter to test SSR * Adds a changeset * Updated based on feedback * Updated the lockfile * Only throw if set to a different adapter * Clean up outdated comments * Move the adapter to an config option * Make adapter optional * Update the docs/changeset to reflect config API change * Clarify regular Node usage
This commit is contained in:
parent
5c96145527
commit
5e52814d97
29 changed files with 886 additions and 496 deletions
29
.changeset/hot-plants-help.md
Normal file
29
.changeset/hot-plants-help.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Adds support for the Node adapter (SSR)
|
||||
|
||||
This provides the first SSR adapter available using the `integrations` API. It is a Node.js adapter that can be used with the `http` module or any framework that wraps it, like Express.
|
||||
|
||||
In your astro.config.mjs use:
|
||||
|
||||
```js
|
||||
import nodejs from '@astrojs/node';
|
||||
|
||||
export default {
|
||||
adapter: nodejs()
|
||||
}
|
||||
```
|
||||
|
||||
After performing a build there will be a `dist/server/entry.mjs` module that works like a middleware function. You can use with any framework that supports the Node `request` and `response` objects. For example, with Express you can do:
|
||||
|
||||
```js
|
||||
import express from 'express';
|
||||
import { handler as ssrHandler } from '@astrojs/node';
|
||||
|
||||
const app = express();
|
||||
app.use(handler);
|
||||
|
||||
app.listen(8080);
|
||||
```
|
|
@ -1,8 +1,10 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import svelte from '@astrojs/svelte';
|
||||
import nodejs from '@astrojs/node';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
adapter: nodejs(),
|
||||
integrations: [svelte()],
|
||||
vite: {
|
||||
server: {
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { execa } from 'execa';
|
||||
|
||||
const api = execa('npm', ['run', 'dev-api']);
|
||||
api.stdout.pipe(process.stdout);
|
||||
api.stderr.pipe(process.stderr);
|
||||
|
||||
const build = execa('pnpm', ['astro', 'build', '--experimental-ssr']);
|
||||
build.stdout.pipe(process.stdout);
|
||||
build.stderr.pipe(process.stderr);
|
||||
await build;
|
||||
|
||||
api.kill();
|
|
@ -7,12 +7,12 @@
|
|||
"dev-server": "astro dev --experimental-ssr",
|
||||
"dev": "concurrently \"npm run dev-api\" \"astro dev --experimental-ssr\"",
|
||||
"start": "astro dev",
|
||||
"build": "echo 'Run pnpm run build-ssr instead'",
|
||||
"build-ssr": "node build.mjs",
|
||||
"build": "astro build --experimental-ssr",
|
||||
"server": "node server/server.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/svelte": "^0.0.2-next.0",
|
||||
"@astrojs/node": "^0.0.1",
|
||||
"astro": "^0.25.0-next.2",
|
||||
"concurrently": "^7.0.0",
|
||||
"lightcookie": "^1.0.25",
|
||||
|
|
|
@ -1,43 +1,31 @@
|
|||
import { createServer } from 'http';
|
||||
import fs from 'fs';
|
||||
import mime from 'mime';
|
||||
import { loadApp } from 'astro/app/node';
|
||||
import { polyfill } from '@astrojs/webapi';
|
||||
import { apiHandler } from './api.mjs';
|
||||
|
||||
polyfill(globalThis);
|
||||
import { handler as ssrHandler } from '../dist/server/entry.mjs';
|
||||
|
||||
const clientRoot = new URL('../dist/client/', import.meta.url);
|
||||
const serverRoot = new URL('../dist/server/', import.meta.url);
|
||||
const app = await loadApp(serverRoot);
|
||||
|
||||
async function handle(req, res) {
|
||||
const route = app.match(req);
|
||||
ssrHandler(req, res, async () => {
|
||||
// Did not match an SSR route
|
||||
|
||||
if (route) {
|
||||
/** @type {Response} */
|
||||
const response = await app.render(req, route);
|
||||
const html = await response.text();
|
||||
res.writeHead(response.status, {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Content-Length': Buffer.byteLength(html, 'utf-8'),
|
||||
});
|
||||
res.end(html);
|
||||
} else if (/^\/api\//.test(req.url)) {
|
||||
return apiHandler(req, res);
|
||||
} else {
|
||||
let local = new URL('.' + req.url, clientRoot);
|
||||
try {
|
||||
const data = await fs.promises.readFile(local);
|
||||
res.writeHead(200, {
|
||||
'Content-Type': mime.getType(req.url),
|
||||
});
|
||||
res.end(data);
|
||||
} catch {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
if (/^\/api\//.test(req.url)) {
|
||||
return apiHandler(req, res);
|
||||
} else {
|
||||
let local = new URL('.' + req.url, clientRoot);
|
||||
try {
|
||||
const data = await fs.promises.readFile(local);
|
||||
res.writeHead(200, {
|
||||
'Content-Type': mime.getType(req.url),
|
||||
});
|
||||
res.end(data);
|
||||
} catch {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
|
|
|
@ -13,11 +13,15 @@
|
|||
"bugs": "https://github.com/withastro/astro/issues",
|
||||
"homepage": "https://astro.build",
|
||||
"types": "./dist/types/@types/astro.d.ts",
|
||||
"typesVersions": {
|
||||
"*": { "app/*": ["./dist/types/core/app/*"] }
|
||||
},
|
||||
"exports": {
|
||||
".": "./astro.js",
|
||||
"./env": "./env.d.ts",
|
||||
"./config": "./config.mjs",
|
||||
"./internal": "./internal.js",
|
||||
"./app": "./dist/core/app/index.js",
|
||||
"./app/node": "./dist/core/app/node.js",
|
||||
"./client/*": "./dist/runtime/client/*",
|
||||
"./components": "./components/index.js",
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { z } from 'zod';
|
|||
import type { AstroConfigSchema } from '../core/config';
|
||||
import type { AstroComponentFactory, Metadata } from '../runtime/server';
|
||||
import type { AstroRequest } from '../core/render/request';
|
||||
export type { SSRManifest } from '../core/app/types';
|
||||
|
||||
export interface AstroBuiltinProps {
|
||||
'client:load'?: boolean;
|
||||
|
@ -37,6 +38,10 @@ export interface CLIFlags {
|
|||
drafts?: boolean;
|
||||
}
|
||||
|
||||
export interface BuildConfig {
|
||||
staticMode: boolean | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Astro.* available in all components
|
||||
* Docs: https://docs.astro.build/reference/api-reference/#astro-global
|
||||
|
@ -154,6 +159,16 @@ export interface AstroUserConfig {
|
|||
*/
|
||||
integrations?: AstroIntegration[];
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name adapter
|
||||
* @type {AstroIntegration}
|
||||
* @default `undefined`
|
||||
* @description
|
||||
* Add an adapter to build for SSR (server-side rendering). An adapter makes it easy to connect a deployed Astro app to a hosting provider or runtime environment.
|
||||
*/
|
||||
adapter?: AstroIntegration;
|
||||
|
||||
/** @deprecated - Use "integrations" instead. Run Astro to learn more about migrating. */
|
||||
renderers?: string[];
|
||||
|
||||
|
@ -461,11 +476,13 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
|
|||
// This is a more detailed type than zod validation gives us.
|
||||
// TypeScript still confirms zod validation matches this type.
|
||||
integrations: AstroIntegration[];
|
||||
adapter?: AstroIntegration;
|
||||
// Private:
|
||||
// We have a need to pass context based on configured state,
|
||||
// that is different from the user-exposed configuration.
|
||||
// TODO: Create an AstroConfig class to manage this, long-term.
|
||||
_ctx: {
|
||||
adapter: AstroAdapter | undefined;
|
||||
renderers: AstroRenderer[];
|
||||
scripts: { stage: InjectedScriptStage; content: string }[];
|
||||
};
|
||||
|
@ -596,6 +613,12 @@ export type Props = Record<string, unknown>;
|
|||
|
||||
type Body = string;
|
||||
|
||||
export interface AstroAdapter {
|
||||
name: string;
|
||||
serverEntrypoint?: string;
|
||||
exports?: string[];
|
||||
}
|
||||
|
||||
export interface EndpointOutput<Output extends Body = Body> {
|
||||
body: Output;
|
||||
}
|
||||
|
@ -642,11 +665,11 @@ export interface AstroIntegration {
|
|||
// more generalized. Consider the SSR use-case as well.
|
||||
// injectElement: (stage: vite.HtmlTagDescriptor, element: string) => void;
|
||||
}) => void;
|
||||
'astro:config:done'?: (options: { config: AstroConfig }) => void | Promise<void>;
|
||||
'astro:config:done'?: (options: {config: AstroConfig, setAdapter: (adapter: AstroAdapter) => void; }) => void | Promise<void>;
|
||||
'astro:server:setup'?: (options: { server: vite.ViteDevServer }) => void | Promise<void>;
|
||||
'astro:server:start'?: (options: { address: AddressInfo }) => void | Promise<void>;
|
||||
'astro:server:done'?: () => void | Promise<void>;
|
||||
'astro:build:start'?: () => void | Promise<void>;
|
||||
'astro:build:start'?: (options: { buildConfig: BuildConfig }) => void | Promise<void>;
|
||||
'astro:build:done'?: (options: { pages: { pathname: string }[]; dir: URL }) => void | Promise<void>;
|
||||
};
|
||||
}
|
||||
|
|
23
packages/astro/src/adapter-ssg/index.ts
Normal file
23
packages/astro/src/adapter-ssg/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import type { AstroAdapter, AstroIntegration } from '../@types/astro';
|
||||
|
||||
export function getAdapter(): AstroAdapter {
|
||||
return {
|
||||
name: '@astrojs/ssg',
|
||||
// This one has no server entrypoint and is mostly just an integration
|
||||
//serverEntrypoint: '@astrojs/ssg/server.js',
|
||||
};
|
||||
}
|
||||
|
||||
export default function createIntegration(): AstroIntegration {
|
||||
return {
|
||||
name: '@astrojs/ssg',
|
||||
hooks: {
|
||||
'astro:config:done': ({ setAdapter }) => {
|
||||
setAdapter(getAdapter());
|
||||
},
|
||||
'astro:build:start': ({ buildConfig }) => {
|
||||
buildConfig.staticMode = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -2,6 +2,7 @@ import type { ComponentInstance, ManifestData, RouteData, SSRLoadedRenderer } fr
|
|||
import type { SSRManifest as Manifest, RouteInfo } from './types';
|
||||
|
||||
import { defaultLogOptions } from '../logger.js';
|
||||
export { deserializeManifest } from './common.js';
|
||||
import { matchRoute } from '../routing/match.js';
|
||||
import { render } from '../render/core.js';
|
||||
import { RouteCache } from '../render/route-cache.js';
|
||||
|
@ -64,7 +65,7 @@ export class App {
|
|||
throw new Error(`Unable to resolve [${specifier}]`);
|
||||
}
|
||||
const bundlePath = manifest.entryModules[specifier];
|
||||
return prependForwardSlash(bundlePath);
|
||||
return bundlePath.startsWith('data:') ? bundlePath : prependForwardSlash(bundlePath);
|
||||
},
|
||||
route: routeData,
|
||||
routeCache: this.#routeCache,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type { SSRManifest, SerializedSSRManifest } from './types';
|
||||
import type { IncomingHttpHeaders } from 'http';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { App } from './index.js';
|
||||
|
@ -16,7 +15,7 @@ function createRequestFromNodeRequest(req: IncomingMessage): Request {
|
|||
return request;
|
||||
}
|
||||
|
||||
class NodeApp extends App {
|
||||
export class NodeApp extends App {
|
||||
match(req: IncomingMessage | Request) {
|
||||
return super.match(req instanceof Request ? req : createRequestFromNodeRequest(req));
|
||||
}
|
||||
|
|
64
packages/astro/src/core/build/common.ts
Normal file
64
packages/astro/src/core/build/common.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import type { AstroConfig, RouteType } from '../../@types/astro';
|
||||
import npath from 'path';
|
||||
import { appendForwardSlash } from '../../core/path.js';
|
||||
|
||||
const STATUS_CODE_PAGES = new Set(['/404', '/500']);
|
||||
|
||||
export function getOutRoot(astroConfig: AstroConfig): URL {
|
||||
return new URL('./', astroConfig.dist);
|
||||
}
|
||||
|
||||
export function getServerRoot(astroConfig: AstroConfig): URL {
|
||||
const rootFolder = getOutRoot(astroConfig);
|
||||
const serverFolder = new URL('./server/', rootFolder);
|
||||
return serverFolder;
|
||||
}
|
||||
|
||||
export function getClientRoot(astroConfig: AstroConfig): URL {
|
||||
const rootFolder = getOutRoot(astroConfig);
|
||||
const serverFolder = new URL('./client/', rootFolder);
|
||||
return serverFolder;
|
||||
}
|
||||
|
||||
export function getOutFolder(astroConfig: AstroConfig, pathname: string, routeType: RouteType): URL {
|
||||
const outRoot = getOutRoot(astroConfig);
|
||||
|
||||
// This is the root folder to write to.
|
||||
switch (routeType) {
|
||||
case 'endpoint':
|
||||
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
||||
case 'page':
|
||||
switch (astroConfig.buildOptions.pageUrlFormat) {
|
||||
case 'directory': {
|
||||
if (STATUS_CODE_PAGES.has(pathname)) {
|
||||
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
||||
}
|
||||
return new URL('.' + appendForwardSlash(pathname), outRoot);
|
||||
}
|
||||
case 'file': {
|
||||
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getOutFile(astroConfig: AstroConfig, outFolder: URL, pathname: string, routeType: RouteType): URL {
|
||||
switch (routeType) {
|
||||
case 'endpoint':
|
||||
return new URL(npath.basename(pathname), outFolder);
|
||||
case 'page':
|
||||
switch (astroConfig.buildOptions.pageUrlFormat) {
|
||||
case 'directory': {
|
||||
if (STATUS_CODE_PAGES.has(pathname)) {
|
||||
const baseName = npath.basename(pathname);
|
||||
return new URL('./' + (baseName || 'index') + '.html', outFolder);
|
||||
}
|
||||
return new URL('./index.html', outFolder);
|
||||
}
|
||||
case 'file': {
|
||||
const baseName = npath.basename(pathname);
|
||||
return new URL('./' + (baseName || 'index') + '.html', outFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
244
packages/astro/src/core/build/generate.ts
Normal file
244
packages/astro/src/core/build/generate.ts
Normal file
|
@ -0,0 +1,244 @@
|
|||
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
|
||||
import type { PageBuildData } from './types';
|
||||
import type { AstroConfig, AstroRenderer, ComponentInstance, EndpointHandler, SSRLoadedRenderer } from '../../@types/astro';
|
||||
import type { StaticBuildOptions } from './types';
|
||||
import type { BuildInternals } from '../../core/build/internal.js';
|
||||
import type { RenderOptions } from '../../core/render/core';
|
||||
|
||||
import fs from 'fs';
|
||||
import npath from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { debug, error } from '../../core/logger.js';
|
||||
import { prependForwardSlash } from '../../core/path.js';
|
||||
import { resolveDependency } from '../../core/util.js';
|
||||
import { call as callEndpoint } from '../endpoint/index.js';
|
||||
import { render } from '../render/core.js';
|
||||
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
||||
import { getOutRoot, getOutFolder, getOutFile } from './common.js';
|
||||
|
||||
|
||||
// Render is usually compute, which Node.js can't parallelize well.
|
||||
// In real world testing, dropping from 10->1 showed a notiable perf
|
||||
// improvement. In the future, we can revisit a smarter parallel
|
||||
// system, possibly one that parallelizes if async IO is detected.
|
||||
const MAX_CONCURRENT_RENDERS = 1;
|
||||
|
||||
// Utility functions
|
||||
async function loadRenderer(renderer: AstroRenderer, config: AstroConfig): Promise<SSRLoadedRenderer> {
|
||||
const mod = (await import(resolveDependency(renderer.serverEntrypoint, config))) as { default: SSRLoadedRenderer['ssr'] };
|
||||
return { ...renderer, ssr: mod.default };
|
||||
}
|
||||
|
||||
async function loadRenderers(config: AstroConfig): Promise<SSRLoadedRenderer[]> {
|
||||
return Promise.all(config._ctx.renderers.map((r) => loadRenderer(r, config)));
|
||||
}
|
||||
|
||||
export function getByFacadeId<T>(facadeId: string, map: Map<string, T>): T | undefined {
|
||||
return (
|
||||
map.get(facadeId) ||
|
||||
// Windows the facadeId has forward slashes, no idea why
|
||||
map.get(facadeId.replace(/\//g, '\\'))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Throttle the rendering a paths to prevents creating too many Promises on the microtask queue.
|
||||
function* throttle(max: number, inPaths: string[]) {
|
||||
let tmp = [];
|
||||
let i = 0;
|
||||
for (let path of inPaths) {
|
||||
tmp.push(path);
|
||||
if (i === max) {
|
||||
yield tmp;
|
||||
// Empties the array, to avoid allocating a new one.
|
||||
tmp.length = 0;
|
||||
i = 0;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// If tmp has items in it, that means there were less than {max} paths remaining
|
||||
// at the end, so we need to yield these too.
|
||||
if (tmp.length) {
|
||||
yield tmp;
|
||||
}
|
||||
}
|
||||
|
||||
// Gives back a facadeId that is relative to the root.
|
||||
// ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro
|
||||
export function rootRelativeFacadeId(facadeId: string, astroConfig: AstroConfig): string {
|
||||
return facadeId.slice(fileURLToPath(astroConfig.projectRoot).length);
|
||||
}
|
||||
|
||||
// Determines of a Rollup chunk is an entrypoint page.
|
||||
export function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | OutputChunk, internals: BuildInternals) {
|
||||
if (output.type !== 'chunk') {
|
||||
return false;
|
||||
}
|
||||
const chunk = output as OutputChunk;
|
||||
if (chunk.facadeModuleId) {
|
||||
const facadeToEntryId = prependForwardSlash(rootRelativeFacadeId(chunk.facadeModuleId, astroConfig));
|
||||
return internals.entrySpecifierToBundleMap.has(facadeToEntryId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) {
|
||||
debug('build', 'Finish build. Begin generating.');
|
||||
|
||||
// Get renderers to be shared for each page generation.
|
||||
const renderers = await loadRenderers(opts.astroConfig);
|
||||
|
||||
for (let output of result.output) {
|
||||
if (chunkIsPage(opts.astroConfig, output, internals)) {
|
||||
await generatePage(output as OutputChunk, opts, internals, facadeIdToPageDataMap, renderers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePage(
|
||||
output: OutputChunk,
|
||||
opts: StaticBuildOptions,
|
||||
internals: BuildInternals,
|
||||
facadeIdToPageDataMap: Map<string, PageBuildData>,
|
||||
renderers: SSRLoadedRenderer[]
|
||||
) {
|
||||
const { astroConfig } = opts;
|
||||
|
||||
let url = new URL('./' + output.fileName, getOutRoot(astroConfig));
|
||||
const facadeId: string = output.facadeModuleId as string;
|
||||
let pageData = getByFacadeId<PageBuildData>(facadeId, facadeIdToPageDataMap);
|
||||
|
||||
if (!pageData) {
|
||||
throw new Error(`Unable to find a PageBuildData for the Astro page: ${facadeId}. There are the PageBuildDatas we have ${Array.from(facadeIdToPageDataMap.keys()).join(', ')}`);
|
||||
}
|
||||
|
||||
const linkIds = getByFacadeId<string[]>(facadeId, internals.facadeIdToAssetsMap) || [];
|
||||
const hoistedId = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap) || null;
|
||||
|
||||
let compiledModule = await import(url.toString());
|
||||
|
||||
const generationOptions: Readonly<GeneratePathOptions> = {
|
||||
pageData,
|
||||
internals,
|
||||
linkIds,
|
||||
hoistedId,
|
||||
mod: compiledModule,
|
||||
renderers,
|
||||
};
|
||||
|
||||
const renderPromises = [];
|
||||
// Throttle the paths to avoid overloading the CPU with too many tasks.
|
||||
for (const paths of throttle(MAX_CONCURRENT_RENDERS, pageData.paths)) {
|
||||
for (const path of paths) {
|
||||
renderPromises.push(generatePath(path, opts, generationOptions));
|
||||
}
|
||||
// This blocks generating more paths until these 10 complete.
|
||||
await Promise.all(renderPromises);
|
||||
// This empties the array without allocating a new one.
|
||||
renderPromises.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
interface GeneratePathOptions {
|
||||
pageData: PageBuildData;
|
||||
internals: BuildInternals;
|
||||
linkIds: string[];
|
||||
hoistedId: string | null;
|
||||
mod: ComponentInstance;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
}
|
||||
|
||||
|
||||
function addPageName(pathname: string, opts: StaticBuildOptions): void {
|
||||
opts.pageNames.push(pathname.replace(/\/?$/, '/').replace(/^\//, ''));
|
||||
}
|
||||
|
||||
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
|
||||
const { astroConfig, logging, origin, routeCache } = opts;
|
||||
const { mod, internals, linkIds, hoistedId, pageData, renderers } = gopts;
|
||||
|
||||
// This adds the page name to the array so it can be shown as part of stats.
|
||||
if (pageData.route.type === 'page') {
|
||||
addPageName(pathname, opts);
|
||||
}
|
||||
|
||||
debug('build', `Generating: ${pathname}`);
|
||||
|
||||
const site = astroConfig.buildOptions.site;
|
||||
const links = createLinkStylesheetElementSet(linkIds.reverse(), site);
|
||||
const scripts = createModuleScriptElementWithSrcSet(hoistedId ? [hoistedId] : [], site);
|
||||
|
||||
// Add all injected scripts to the page.
|
||||
for (const script of astroConfig._ctx.scripts) {
|
||||
if (script.stage === 'head-inline') {
|
||||
scripts.add({
|
||||
props: {},
|
||||
children: script.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const options: RenderOptions = {
|
||||
legacyBuild: false,
|
||||
links,
|
||||
logging,
|
||||
markdownRender: astroConfig.markdownOptions.render,
|
||||
mod,
|
||||
origin,
|
||||
pathname,
|
||||
scripts,
|
||||
renderers,
|
||||
async resolve(specifier: string) {
|
||||
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
|
||||
if (typeof hashedFilePath !== 'string') {
|
||||
// If no "astro:scripts/before-hydration.js" script exists in the build,
|
||||
// then we can assume that no before-hydration scripts are needed.
|
||||
// Return this as placeholder, which will be ignored by the browser.
|
||||
// TODO: In the future, we hope to run this entire script through Vite,
|
||||
// removing the need to maintain our own custom Vite-mimic resolve logic.
|
||||
if (specifier === 'astro:scripts/before-hydration.js') {
|
||||
return 'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
||||
}
|
||||
throw new Error(`Cannot find the built path for ${specifier}`);
|
||||
}
|
||||
const relPath = npath.posix.relative(pathname, '/' + hashedFilePath);
|
||||
const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath;
|
||||
return fullyRelativePath;
|
||||
},
|
||||
method: 'GET',
|
||||
headers: new Headers(),
|
||||
route: pageData.route,
|
||||
routeCache,
|
||||
site: astroConfig.buildOptions.site,
|
||||
ssr: opts.astroConfig.buildOptions.experimentalSsr,
|
||||
};
|
||||
|
||||
let body: string;
|
||||
if (pageData.route.type === 'endpoint') {
|
||||
const result = await callEndpoint(mod as unknown as EndpointHandler, options);
|
||||
|
||||
if (result.type === 'response') {
|
||||
throw new Error(`Returning a Response from an endpoint is not supported in SSG mode.`);
|
||||
}
|
||||
body = result.body;
|
||||
} else {
|
||||
const result = await render(options);
|
||||
|
||||
// If there's a redirect or something, just do nothing.
|
||||
if (result.type !== 'html') {
|
||||
return;
|
||||
}
|
||||
body = result.html;
|
||||
}
|
||||
|
||||
const outFolder = getOutFolder(astroConfig, pathname, pageData.route.type);
|
||||
const outFile = getOutFile(astroConfig, outFolder, pathname, pageData.route.type);
|
||||
await fs.promises.mkdir(outFolder, { recursive: true });
|
||||
await fs.promises.writeFile(outFile, body, 'utf-8');
|
||||
} catch (err) {
|
||||
error(opts.logging, 'build', `Error rendering:`, err);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import type { AstroConfig, ManifestData } from '../../@types/astro';
|
||||
import type { AstroConfig, BuildConfig, ManifestData } from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger';
|
||||
|
||||
import fs from 'fs';
|
||||
|
@ -74,7 +74,8 @@ class AstroBuilder {
|
|||
const viteServer = await vite.createServer(viteConfig);
|
||||
this.viteServer = viteServer;
|
||||
debug('build', timerMessage('Vite started', timer.viteStart));
|
||||
await runHookBuildStart({ config: this.config });
|
||||
const buildConfig: BuildConfig = { staticMode: undefined };
|
||||
await runHookBuildStart({ config: this.config, buildConfig });
|
||||
|
||||
timer.loadStart = performance.now();
|
||||
const { assets, allPages } = await collectPagesData({
|
||||
|
@ -119,6 +120,7 @@ class AstroBuilder {
|
|||
pageNames,
|
||||
routeCache: this.routeCache,
|
||||
viteConfig: this.viteConfig,
|
||||
buildConfig,
|
||||
});
|
||||
} else {
|
||||
await scanBasedBuild({
|
||||
|
|
|
@ -1,108 +1,26 @@
|
|||
import type { RollupOutput } from 'rollup';
|
||||
import type { BuildInternals } from '../../core/build/internal.js';
|
||||
import type { ViteConfigWithSSR } from '../create-vite';
|
||||
import type { PageBuildData, StaticBuildOptions } from './types';
|
||||
|
||||
import glob from 'fast-glob';
|
||||
import fs from 'fs';
|
||||
import npath from 'path';
|
||||
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
|
||||
import { fileURLToPath } from 'url';
|
||||
import type { Manifest as ViteManifest, Plugin as VitePlugin, UserConfig } from 'vite';
|
||||
import * as vite from 'vite';
|
||||
import type { AstroConfig, AstroRenderer, ComponentInstance, EndpointHandler, ManifestData, RouteType, SSRLoadedRenderer } from '../../@types/astro';
|
||||
import type { BuildInternals } from '../../core/build/internal.js';
|
||||
import { createBuildInternals } from '../../core/build/internal.js';
|
||||
import { debug, error } from '../../core/logger.js';
|
||||
import { appendForwardSlash, prependForwardSlash } from '../../core/path.js';
|
||||
import type { RenderOptions } from '../../core/render/core';
|
||||
import { emptyDir, removeDir, resolveDependency } from '../../core/util.js';
|
||||
import { emptyDir, removeDir } from '../../core/util.js';
|
||||
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
||||
import type { SerializedRouteInfo, SerializedSSRManifest } from '../app/types';
|
||||
import type { ViteConfigWithSSR } from '../create-vite';
|
||||
import { call as callEndpoint } from '../endpoint/index.js';
|
||||
import type { LogOptions } from '../logger';
|
||||
import { render } from '../render/core.js';
|
||||
import { RouteCache } from '../render/route-cache.js';
|
||||
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
||||
import { serializeRouteData } from '../routing/index.js';
|
||||
import type { AllPagesData, PageBuildData } from './types';
|
||||
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
||||
|
||||
export interface StaticBuildOptions {
|
||||
allPages: AllPagesData;
|
||||
astroConfig: AstroConfig;
|
||||
logging: LogOptions;
|
||||
manifest: ManifestData;
|
||||
origin: string;
|
||||
pageNames: string[];
|
||||
routeCache: RouteCache;
|
||||
viteConfig: ViteConfigWithSSR;
|
||||
}
|
||||
|
||||
// Render is usually compute, which Node.js can't parallelize well.
|
||||
// In real world testing, dropping from 10->1 showed a notiable perf
|
||||
// improvement. In the future, we can revisit a smarter parallel
|
||||
// system, possibly one that parallelizes if async IO is detected.
|
||||
const MAX_CONCURRENT_RENDERS = 1;
|
||||
|
||||
const STATUS_CODE_PAGES = new Set(['/404', '/500']);
|
||||
|
||||
function addPageName(pathname: string, opts: StaticBuildOptions): void {
|
||||
opts.pageNames.push(pathname.replace(/\/?$/, '/').replace(/^\//, ''));
|
||||
}
|
||||
|
||||
// Gives back a facadeId that is relative to the root.
|
||||
// ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro
|
||||
function rootRelativeFacadeId(facadeId: string, astroConfig: AstroConfig): string {
|
||||
return facadeId.slice(fileURLToPath(astroConfig.projectRoot).length);
|
||||
}
|
||||
|
||||
// Determines of a Rollup chunk is an entrypoint page.
|
||||
function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | OutputChunk, internals: BuildInternals) {
|
||||
if (output.type !== 'chunk') {
|
||||
return false;
|
||||
}
|
||||
const chunk = output as OutputChunk;
|
||||
if (chunk.facadeModuleId) {
|
||||
const facadeToEntryId = prependForwardSlash(rootRelativeFacadeId(chunk.facadeModuleId, astroConfig));
|
||||
return internals.entrySpecifierToBundleMap.has(facadeToEntryId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Throttle the rendering a paths to prevents creating too many Promises on the microtask queue.
|
||||
function* throttle(max: number, inPaths: string[]) {
|
||||
let tmp = [];
|
||||
let i = 0;
|
||||
for (let path of inPaths) {
|
||||
tmp.push(path);
|
||||
if (i === max) {
|
||||
yield tmp;
|
||||
// Empties the array, to avoid allocating a new one.
|
||||
tmp.length = 0;
|
||||
i = 0;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// If tmp has items in it, that means there were less than {max} paths remaining
|
||||
// at the end, so we need to yield these too.
|
||||
if (tmp.length) {
|
||||
yield tmp;
|
||||
}
|
||||
}
|
||||
|
||||
function getByFacadeId<T>(facadeId: string, map: Map<string, T>): T | undefined {
|
||||
return (
|
||||
map.get(facadeId) ||
|
||||
// Windows the facadeId has forward slashes, no idea why
|
||||
map.get(facadeId.replace(/\//g, '\\'))
|
||||
);
|
||||
}
|
||||
import { vitePluginInternals } from './vite-plugin-internals.js';
|
||||
import { vitePluginSSR } from './vite-plugin-ssr.js';
|
||||
import { generatePages } from './generate.js';
|
||||
import { getClientRoot, getServerRoot, getOutRoot } from './common.js';
|
||||
|
||||
export async function staticBuild(opts: StaticBuildOptions) {
|
||||
const { allPages, astroConfig } = opts;
|
||||
|
||||
// Basic options
|
||||
const staticMode = !astroConfig.buildOptions.experimentalSsr;
|
||||
|
||||
// The pages to be built for rendering purposes.
|
||||
const pageInput = new Set<string>();
|
||||
|
||||
|
@ -158,18 +76,16 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
|||
// condition, so we are doing it ourselves
|
||||
emptyDir(astroConfig.dist, new Set('.git'));
|
||||
|
||||
// Run client build first, so the assets can be fed into the SSR rendered version.
|
||||
await clientBuild(opts, internals, jsInput);
|
||||
|
||||
// Build your project (SSR application code, assets, client JS, etc.)
|
||||
const ssrResult = (await ssrBuild(opts, internals, pageInput)) as RollupOutput;
|
||||
|
||||
await clientBuild(opts, internals, jsInput);
|
||||
|
||||
// SSG mode, generate pages.
|
||||
if (staticMode) {
|
||||
// Generate each of the pages.
|
||||
if(opts.buildConfig.staticMode) {
|
||||
await generatePages(ssrResult, opts, internals, facadeIdToPageDataMap);
|
||||
await cleanSsrOutput(opts);
|
||||
} else {
|
||||
await generateManifest(ssrResult, opts, internals);
|
||||
await ssrMoveAssets(opts);
|
||||
}
|
||||
}
|
||||
|
@ -186,13 +102,10 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
|||
build: {
|
||||
...viteConfig.build,
|
||||
emptyOutDir: false,
|
||||
manifest: ssr,
|
||||
manifest: false,
|
||||
outDir: fileURLToPath(out),
|
||||
ssr: true,
|
||||
rollupOptions: {
|
||||
// onwarn(warn) {
|
||||
// console.log(warn);
|
||||
// },
|
||||
input: Array.from(input),
|
||||
output: {
|
||||
format: 'esm',
|
||||
|
@ -209,11 +122,14 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
|||
reportCompressedSize: false,
|
||||
},
|
||||
plugins: [
|
||||
vitePluginNewBuild(input, internals, 'mjs'),
|
||||
vitePluginInternals(input, internals),
|
||||
rollupPluginAstroBuildCSS({
|
||||
internals,
|
||||
}),
|
||||
...(viteConfig.plugins || []),
|
||||
// SSR needs to be last
|
||||
opts.astroConfig._ctx.adapter?.serverEntrypoint &&
|
||||
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter),
|
||||
],
|
||||
publicDir: ssr ? false : viteConfig.publicDir,
|
||||
root: viteConfig.root,
|
||||
|
@ -256,7 +172,7 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
|
|||
target: 'esnext', // must match an esbuild target
|
||||
},
|
||||
plugins: [
|
||||
vitePluginNewBuild(input, internals, 'js'),
|
||||
vitePluginInternals(input, internals),
|
||||
vitePluginHoistedScripts(astroConfig, internals),
|
||||
rollupPluginAstroBuildCSS({
|
||||
internals,
|
||||
|
@ -271,283 +187,6 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
|
|||
});
|
||||
}
|
||||
|
||||
async function loadRenderer(renderer: AstroRenderer, config: AstroConfig): Promise<SSRLoadedRenderer> {
|
||||
const mod = (await import(resolveDependency(renderer.serverEntrypoint, config))) as { default: SSRLoadedRenderer['ssr'] };
|
||||
return { ...renderer, ssr: mod.default };
|
||||
}
|
||||
|
||||
async function loadRenderers(config: AstroConfig): Promise<SSRLoadedRenderer[]> {
|
||||
return Promise.all(config._ctx.renderers.map((r) => loadRenderer(r, config)));
|
||||
}
|
||||
|
||||
async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) {
|
||||
debug('build', 'Finish build. Begin generating.');
|
||||
|
||||
// Get renderers to be shared for each page generation.
|
||||
const renderers = await loadRenderers(opts.astroConfig);
|
||||
|
||||
for (let output of result.output) {
|
||||
if (chunkIsPage(opts.astroConfig, output, internals)) {
|
||||
await generatePage(output as OutputChunk, opts, internals, facadeIdToPageDataMap, renderers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePage(
|
||||
output: OutputChunk,
|
||||
opts: StaticBuildOptions,
|
||||
internals: BuildInternals,
|
||||
facadeIdToPageDataMap: Map<string, PageBuildData>,
|
||||
renderers: SSRLoadedRenderer[]
|
||||
) {
|
||||
const { astroConfig } = opts;
|
||||
|
||||
let url = new URL('./' + output.fileName, getOutRoot(astroConfig));
|
||||
const facadeId: string = output.facadeModuleId as string;
|
||||
let pageData = getByFacadeId<PageBuildData>(facadeId, facadeIdToPageDataMap);
|
||||
|
||||
if (!pageData) {
|
||||
throw new Error(`Unable to find a PageBuildData for the Astro page: ${facadeId}. There are the PageBuildDatas we have ${Array.from(facadeIdToPageDataMap.keys()).join(', ')}`);
|
||||
}
|
||||
|
||||
const linkIds = getByFacadeId<string[]>(facadeId, internals.facadeIdToAssetsMap) || [];
|
||||
const hoistedId = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap) || null;
|
||||
|
||||
let compiledModule = await import(url.toString());
|
||||
|
||||
const generationOptions: Readonly<GeneratePathOptions> = {
|
||||
pageData,
|
||||
internals,
|
||||
linkIds,
|
||||
hoistedId,
|
||||
mod: compiledModule,
|
||||
renderers,
|
||||
};
|
||||
|
||||
const renderPromises = [];
|
||||
// Throttle the paths to avoid overloading the CPU with too many tasks.
|
||||
for (const paths of throttle(MAX_CONCURRENT_RENDERS, pageData.paths)) {
|
||||
for (const path of paths) {
|
||||
renderPromises.push(generatePath(path, opts, generationOptions));
|
||||
}
|
||||
// This blocks generating more paths until these 10 complete.
|
||||
await Promise.all(renderPromises);
|
||||
// This empties the array without allocating a new one.
|
||||
renderPromises.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
interface GeneratePathOptions {
|
||||
pageData: PageBuildData;
|
||||
internals: BuildInternals;
|
||||
linkIds: string[];
|
||||
hoistedId: string | null;
|
||||
mod: ComponentInstance;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
}
|
||||
|
||||
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
|
||||
const { astroConfig, logging, origin, routeCache } = opts;
|
||||
const { mod, internals, linkIds, hoistedId, pageData, renderers } = gopts;
|
||||
|
||||
// This adds the page name to the array so it can be shown as part of stats.
|
||||
if (pageData.route.type === 'page') {
|
||||
addPageName(pathname, opts);
|
||||
}
|
||||
|
||||
debug('build', `Generating: ${pathname}`);
|
||||
|
||||
const site = astroConfig.buildOptions.site;
|
||||
const links = createLinkStylesheetElementSet(linkIds.reverse(), site);
|
||||
const scripts = createModuleScriptElementWithSrcSet(hoistedId ? [hoistedId] : [], site);
|
||||
|
||||
// Add all injected scripts to the page.
|
||||
for (const script of astroConfig._ctx.scripts) {
|
||||
if (script.stage === 'head-inline') {
|
||||
scripts.add({
|
||||
props: {},
|
||||
children: script.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const options: RenderOptions = {
|
||||
legacyBuild: false,
|
||||
links,
|
||||
logging,
|
||||
markdownRender: astroConfig.markdownOptions.render,
|
||||
mod,
|
||||
origin,
|
||||
pathname,
|
||||
scripts,
|
||||
renderers,
|
||||
async resolve(specifier: string) {
|
||||
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
|
||||
if (typeof hashedFilePath !== 'string') {
|
||||
// If no "astro:scripts/before-hydration.js" script exists in the build,
|
||||
// then we can assume that no before-hydration scripts are needed.
|
||||
// Return this as placeholder, which will be ignored by the browser.
|
||||
// TODO: In the future, we hope to run this entire script through Vite,
|
||||
// removing the need to maintain our own custom Vite-mimic resolve logic.
|
||||
if (specifier === 'astro:scripts/before-hydration.js') {
|
||||
return 'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
||||
}
|
||||
throw new Error(`Cannot find the built path for ${specifier}`);
|
||||
}
|
||||
const relPath = npath.posix.relative(pathname, '/' + hashedFilePath);
|
||||
const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath;
|
||||
return fullyRelativePath;
|
||||
},
|
||||
method: 'GET',
|
||||
headers: new Headers(),
|
||||
route: pageData.route,
|
||||
routeCache,
|
||||
site: astroConfig.buildOptions.site,
|
||||
ssr: opts.astroConfig.buildOptions.experimentalSsr,
|
||||
};
|
||||
|
||||
let body: string;
|
||||
if (pageData.route.type === 'endpoint') {
|
||||
const result = await callEndpoint(mod as unknown as EndpointHandler, options);
|
||||
|
||||
if (result.type === 'response') {
|
||||
throw new Error(`Returning a Response from an endpoint is not supported in SSG mode.`);
|
||||
}
|
||||
body = result.body;
|
||||
} else {
|
||||
const result = await render(options);
|
||||
|
||||
// If there's a redirect or something, just do nothing.
|
||||
if (result.type !== 'html') {
|
||||
return;
|
||||
}
|
||||
body = result.html;
|
||||
}
|
||||
|
||||
const outFolder = getOutFolder(astroConfig, pathname, pageData.route.type);
|
||||
const outFile = getOutFile(astroConfig, outFolder, pathname, pageData.route.type);
|
||||
await fs.promises.mkdir(outFolder, { recursive: true });
|
||||
await fs.promises.writeFile(outFile, body, 'utf-8');
|
||||
} catch (err) {
|
||||
error(opts.logging, 'build', `Error rendering:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateManifest(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals) {
|
||||
const { astroConfig, manifest } = opts;
|
||||
const manifestFile = new URL('./manifest.json', getServerRoot(astroConfig));
|
||||
|
||||
const inputManifestJSON = await fs.promises.readFile(manifestFile, 'utf-8');
|
||||
const data: ViteManifest = JSON.parse(inputManifestJSON);
|
||||
|
||||
const rootRelativeIdToChunkMap = new Map<string, OutputChunk>();
|
||||
for (const output of result.output) {
|
||||
if (chunkIsPage(astroConfig, output, internals)) {
|
||||
const chunk = output as OutputChunk;
|
||||
if (chunk.facadeModuleId) {
|
||||
const id = rootRelativeFacadeId(chunk.facadeModuleId, astroConfig);
|
||||
rootRelativeIdToChunkMap.set(id, chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const routes: SerializedRouteInfo[] = [];
|
||||
|
||||
for (const routeData of manifest.routes) {
|
||||
const componentPath = routeData.component;
|
||||
const entry = data[componentPath];
|
||||
|
||||
if (!rootRelativeIdToChunkMap.has(componentPath)) {
|
||||
throw new Error('Unable to find chunk for ' + componentPath);
|
||||
}
|
||||
|
||||
const chunk = rootRelativeIdToChunkMap.get(componentPath)!;
|
||||
const facadeId = chunk.facadeModuleId!;
|
||||
const links = getByFacadeId<string[]>(facadeId, internals.facadeIdToAssetsMap) || [];
|
||||
const hoistedScript = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap);
|
||||
const scripts = hoistedScript ? [hoistedScript] : [];
|
||||
|
||||
routes.push({
|
||||
file: entry?.file,
|
||||
links,
|
||||
scripts,
|
||||
routeData: serializeRouteData(routeData),
|
||||
});
|
||||
}
|
||||
|
||||
const ssrManifest: SerializedSSRManifest = {
|
||||
routes,
|
||||
site: astroConfig.buildOptions.site,
|
||||
markdown: {
|
||||
render: astroConfig.markdownOptions.render,
|
||||
},
|
||||
renderers: astroConfig._ctx.renderers,
|
||||
entryModules: Object.fromEntries(internals.entrySpecifierToBundleMap.entries()),
|
||||
};
|
||||
|
||||
const outputManifestJSON = JSON.stringify(ssrManifest, null, ' ');
|
||||
await fs.promises.writeFile(manifestFile, outputManifestJSON, 'utf-8');
|
||||
}
|
||||
|
||||
function getOutRoot(astroConfig: AstroConfig): URL {
|
||||
return new URL('./', astroConfig.dist);
|
||||
}
|
||||
|
||||
function getServerRoot(astroConfig: AstroConfig): URL {
|
||||
const rootFolder = getOutRoot(astroConfig);
|
||||
const serverFolder = new URL('./server/', rootFolder);
|
||||
return serverFolder;
|
||||
}
|
||||
|
||||
function getClientRoot(astroConfig: AstroConfig): URL {
|
||||
const rootFolder = getOutRoot(astroConfig);
|
||||
const serverFolder = new URL('./client/', rootFolder);
|
||||
return serverFolder;
|
||||
}
|
||||
|
||||
function getOutFolder(astroConfig: AstroConfig, pathname: string, routeType: RouteType): URL {
|
||||
const outRoot = getOutRoot(astroConfig);
|
||||
|
||||
// This is the root folder to write to.
|
||||
switch (routeType) {
|
||||
case 'endpoint':
|
||||
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
||||
case 'page':
|
||||
switch (astroConfig.buildOptions.pageUrlFormat) {
|
||||
case 'directory': {
|
||||
if (STATUS_CODE_PAGES.has(pathname)) {
|
||||
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
||||
}
|
||||
return new URL('.' + appendForwardSlash(pathname), outRoot);
|
||||
}
|
||||
case 'file': {
|
||||
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getOutFile(astroConfig: AstroConfig, outFolder: URL, pathname: string, routeType: RouteType): URL {
|
||||
switch (routeType) {
|
||||
case 'endpoint':
|
||||
return new URL(npath.basename(pathname), outFolder);
|
||||
case 'page':
|
||||
switch (astroConfig.buildOptions.pageUrlFormat) {
|
||||
case 'directory': {
|
||||
if (STATUS_CODE_PAGES.has(pathname)) {
|
||||
const baseName = npath.basename(pathname);
|
||||
return new URL('./' + (baseName || 'index') + '.html', outFolder);
|
||||
}
|
||||
return new URL('./index.html', outFolder);
|
||||
}
|
||||
case 'file': {
|
||||
const baseName = npath.basename(pathname);
|
||||
return new URL('./' + (baseName || 'index') + '.html', outFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanSsrOutput(opts: StaticBuildOptions) {
|
||||
// The SSR output is all .mjs files, the client output is not.
|
||||
|
@ -583,58 +222,5 @@ async function ssrMoveAssets(opts: StaticBuildOptions) {
|
|||
})
|
||||
);
|
||||
|
||||
await removeDir(serverAssets);
|
||||
}
|
||||
|
||||
export function vitePluginNewBuild(input: Set<string>, internals: BuildInternals, ext: 'js' | 'mjs'): VitePlugin {
|
||||
return {
|
||||
name: '@astro/rollup-plugin-new-build',
|
||||
|
||||
config(config, options) {
|
||||
const extra: Partial<UserConfig> = {};
|
||||
const noExternal = [],
|
||||
external = [];
|
||||
if (options.command === 'build' && config.build?.ssr) {
|
||||
noExternal.push('astro');
|
||||
external.push('shiki');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
extra.ssr = {
|
||||
external,
|
||||
noExternal,
|
||||
};
|
||||
return extra;
|
||||
},
|
||||
|
||||
configResolved(resolvedConfig) {
|
||||
// Delete this hook because it causes assets not to be built
|
||||
const plugins = resolvedConfig.plugins as VitePlugin[];
|
||||
const viteAsset = plugins.find((p) => p.name === 'vite:asset');
|
||||
if (viteAsset) {
|
||||
delete viteAsset.generateBundle;
|
||||
}
|
||||
},
|
||||
|
||||
async generateBundle(_options, bundle) {
|
||||
const promises = [];
|
||||
const mapping = new Map<string, string>();
|
||||
for (const specifier of input) {
|
||||
promises.push(
|
||||
this.resolve(specifier).then((result) => {
|
||||
if (result) {
|
||||
mapping.set(result.id, specifier);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
for (const [, chunk] of Object.entries(bundle)) {
|
||||
if (chunk.type === 'chunk' && chunk.facadeModuleId) {
|
||||
const specifier = mapping.get(chunk.facadeModuleId) || chunk.facadeModuleId;
|
||||
internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
removeDir(serverAssets);
|
||||
}
|
||||
|
|
18
packages/astro/src/core/build/types.d.ts
vendored
18
packages/astro/src/core/build/types.d.ts
vendored
|
@ -1,5 +1,8 @@
|
|||
import type { ComponentPreload } from '../render/dev/index';
|
||||
import type { RouteData } from '../../@types/astro';
|
||||
import type { AstroConfig, BuildConfig, ManifestData, RouteData } from '../../@types/astro';
|
||||
import type { ViteConfigWithSSR } from '../../create-vite';
|
||||
import type { LogOptions } from '../../logger';
|
||||
import type { RouteCache } from '../../render/route-cache.js';
|
||||
|
||||
export interface PageBuildData {
|
||||
paths: string[];
|
||||
|
@ -7,3 +10,16 @@ export interface PageBuildData {
|
|||
route: RouteData;
|
||||
}
|
||||
export type AllPagesData = Record<string, PageBuildData>;
|
||||
|
||||
/** Options for the static build */
|
||||
export interface StaticBuildOptions {
|
||||
allPages: AllPagesData;
|
||||
astroConfig: AstroConfig;
|
||||
buildConfig: BuildConfig;
|
||||
logging: LogOptions;
|
||||
manifest: ManifestData;
|
||||
origin: string;
|
||||
pageNames: string[];
|
||||
routeCache: RouteCache;
|
||||
viteConfig: ViteConfigWithSSR;
|
||||
}
|
||||
|
|
55
packages/astro/src/core/build/vite-plugin-internals.ts
Normal file
55
packages/astro/src/core/build/vite-plugin-internals.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import type { Plugin as VitePlugin, UserConfig } from 'vite';
|
||||
import type { BuildInternals } from './internal.js';
|
||||
|
||||
export function vitePluginInternals(input: Set<string>, internals: BuildInternals): VitePlugin {
|
||||
return {
|
||||
name: '@astro/plugin-build-internals',
|
||||
|
||||
config(config, options) {
|
||||
const extra: Partial<UserConfig> = {};
|
||||
const noExternal = [],
|
||||
external = [];
|
||||
if (options.command === 'build' && config.build?.ssr) {
|
||||
noExternal.push('astro');
|
||||
external.push('shiki');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
extra.ssr = {
|
||||
external,
|
||||
noExternal,
|
||||
};
|
||||
return extra;
|
||||
},
|
||||
|
||||
configResolved(resolvedConfig) {
|
||||
// Delete this hook because it causes assets not to be built
|
||||
const plugins = resolvedConfig.plugins as VitePlugin[];
|
||||
const viteAsset = plugins.find((p) => p.name === 'vite:asset');
|
||||
if (viteAsset) {
|
||||
delete viteAsset.generateBundle;
|
||||
}
|
||||
},
|
||||
|
||||
async generateBundle(_options, bundle) {
|
||||
const promises = [];
|
||||
const mapping = new Map<string, string>();
|
||||
for (const specifier of input) {
|
||||
promises.push(
|
||||
this.resolve(specifier).then((result) => {
|
||||
if (result) {
|
||||
mapping.set(result.id, specifier);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
for (const [, chunk] of Object.entries(bundle)) {
|
||||
if (chunk.type === 'chunk' && chunk.facadeModuleId) {
|
||||
const specifier = mapping.get(chunk.facadeModuleId) || chunk.facadeModuleId;
|
||||
internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
119
packages/astro/src/core/build/vite-plugin-ssr.ts
Normal file
119
packages/astro/src/core/build/vite-plugin-ssr.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import type { OutputBundle, OutputChunk } from 'rollup';
|
||||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import type { BuildInternals } from './internal.js';
|
||||
import type { AstroAdapter } from '../../@types/astro';
|
||||
import type { StaticBuildOptions } from './types';
|
||||
import type { SerializedRouteInfo, SerializedSSRManifest } from '../app/types';
|
||||
|
||||
import { chunkIsPage, rootRelativeFacadeId, getByFacadeId } from './generate.js';
|
||||
import { serializeRouteData } from '../routing/index.js';
|
||||
|
||||
const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
||||
|
||||
export function vitePluginSSR(buildOpts: StaticBuildOptions, internals: BuildInternals, adapter: AstroAdapter): VitePlugin {
|
||||
return {
|
||||
name: '@astrojs/vite-plugin-astro-ssr',
|
||||
options(opts) {
|
||||
if(Array.isArray(opts.input)) {
|
||||
opts.input.push(virtualModuleId);
|
||||
} else {
|
||||
return {
|
||||
input: [virtualModuleId]
|
||||
};
|
||||
}
|
||||
},
|
||||
resolveId(id) {
|
||||
if(id === virtualModuleId) {
|
||||
return resolvedVirtualModuleId;
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if(id === resolvedVirtualModuleId) {
|
||||
return `import * as adapter from '${adapter.serverEntrypoint}';
|
||||
import { deserializeManifest as _deserializeManifest } from 'astro/app';
|
||||
const _manifest = _deserializeManifest('${manifestReplace}');
|
||||
|
||||
${adapter.exports ? `const _exports = adapter.createExports(_manifest);
|
||||
${adapter.exports.map(name => `export const ${name} = _exports['${name}'];`).join('\n')}
|
||||
` : ''}
|
||||
const _start = 'start';
|
||||
if(_start in adapter) {
|
||||
adapter[_start](_manifest);
|
||||
}`;
|
||||
}
|
||||
return void 0;
|
||||
},
|
||||
|
||||
generateBundle(opts, bundle) {
|
||||
const manifest = buildManifest(bundle, buildOpts, internals);
|
||||
|
||||
for(const [_chunkName, chunk] of Object.entries(bundle)) {
|
||||
if(chunk.type === 'asset') continue;
|
||||
if(chunk.modules[resolvedVirtualModuleId]) {
|
||||
const exp = new RegExp(`['"]${manifestReplace}['"]`);
|
||||
const code = chunk.code;
|
||||
chunk.code = code.replace(exp, () => {
|
||||
return JSON.stringify(manifest);
|
||||
});
|
||||
chunk.fileName = 'entry.mjs';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildManifest(bundle: OutputBundle, opts: StaticBuildOptions, internals: BuildInternals): SerializedSSRManifest {
|
||||
const { astroConfig, manifest } = opts;
|
||||
|
||||
const rootRelativeIdToChunkMap = new Map<string, OutputChunk>();
|
||||
for (const [_outputName, output] of Object.entries(bundle)) {
|
||||
if (chunkIsPage(astroConfig, output, internals)) {
|
||||
const chunk = output as OutputChunk;
|
||||
if (chunk.facadeModuleId) {
|
||||
const id = rootRelativeFacadeId(chunk.facadeModuleId, astroConfig);
|
||||
rootRelativeIdToChunkMap.set(id, chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const routes: SerializedRouteInfo[] = [];
|
||||
|
||||
for (const routeData of manifest.routes) {
|
||||
const componentPath = routeData.component;
|
||||
|
||||
if (!rootRelativeIdToChunkMap.has(componentPath)) {
|
||||
throw new Error('Unable to find chunk for ' + componentPath);
|
||||
}
|
||||
|
||||
const chunk = rootRelativeIdToChunkMap.get(componentPath)!;
|
||||
const facadeId = chunk.facadeModuleId!;
|
||||
const links = getByFacadeId<string[]>(facadeId, internals.facadeIdToAssetsMap) || [];
|
||||
const hoistedScript = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap);
|
||||
const scripts = hoistedScript ? [hoistedScript] : [];
|
||||
|
||||
routes.push({
|
||||
file: chunk.fileName,
|
||||
links,
|
||||
scripts,
|
||||
routeData: serializeRouteData(routeData),
|
||||
});
|
||||
}
|
||||
|
||||
// HACK! Patch this special one.
|
||||
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
|
||||
entryModules['astro:scripts/before-hydration.js'] = 'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
||||
|
||||
const ssrManifest: SerializedSSRManifest = {
|
||||
routes,
|
||||
site: astroConfig.buildOptions.site,
|
||||
markdown: {
|
||||
render: astroConfig.markdownOptions.render,
|
||||
},
|
||||
renderers: astroConfig._ctx.renderers,
|
||||
entryModules,
|
||||
};
|
||||
|
||||
return ssrManifest;
|
||||
}
|
|
@ -11,6 +11,7 @@ import load from '@proload/core';
|
|||
import loadTypeScript from '@proload/plugin-tsm';
|
||||
import postcssrc from 'postcss-load-config';
|
||||
import { arraify, isObject } from './util.js';
|
||||
import ssgAdapter from '../adapter-ssg/index.js';
|
||||
|
||||
load.use([loadTypeScript]);
|
||||
|
||||
|
@ -82,6 +83,7 @@ export const AstroConfigSchema = z.object({
|
|||
message: `Astro integrations are still experimental, and only official integrations are currently supported`,
|
||||
})
|
||||
),
|
||||
adapter: z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }).optional(),
|
||||
styleOptions: z
|
||||
.object({
|
||||
postcss: z
|
||||
|
@ -210,7 +212,7 @@ export async function validateConfig(userConfig: any, root: string): Promise<Ast
|
|||
});
|
||||
return {
|
||||
...(await AstroConfigRelativeSchema.parseAsync(userConfig)),
|
||||
_ctx: { scripts: [], renderers: [] },
|
||||
_ctx: { scripts: [], renderers: [], adapter: undefined },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import type { AddressInfo } from 'net';
|
||||
import type { ViteDevServer } from 'vite';
|
||||
import { AstroConfig, AstroRenderer } from '../@types/astro.js';
|
||||
import { AstroConfig, AstroRenderer, BuildConfig } from '../@types/astro.js';
|
||||
import { mergeConfig } from '../core/config.js';
|
||||
import ssgAdapter from '../adapter-ssg/index.js';
|
||||
|
||||
export async function runHookConfigSetup({ config: _config, command }: { config: AstroConfig; command: 'dev' | 'build' }): Promise<AstroConfig> {
|
||||
if(_config.adapter) {
|
||||
_config.integrations.push(_config.adapter);
|
||||
}
|
||||
|
||||
let updatedConfig: AstroConfig = { ..._config };
|
||||
for (const integration of _config.integrations) {
|
||||
if (integration.hooks['astro:config:setup']) {
|
||||
|
@ -30,6 +35,25 @@ export async function runHookConfigDone({ config }: { config: AstroConfig }) {
|
|||
if (integration.hooks['astro:config:done']) {
|
||||
await integration.hooks['astro:config:done']({
|
||||
config,
|
||||
setAdapter(adapter) {
|
||||
if(config._ctx.adapter && config._ctx.adapter.name !== adapter.name) {
|
||||
throw new Error(`Adapter already set to ${config._ctx.adapter.name}. You can only have one adapter.`);
|
||||
}
|
||||
config._ctx.adapter = adapter;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Call the default adapter
|
||||
if(!config._ctx.adapter) {
|
||||
const integration = ssgAdapter();
|
||||
config.integrations.push(integration);
|
||||
if(integration.hooks['astro:config:done']) {
|
||||
await integration.hooks['astro:config:done']({
|
||||
config,
|
||||
setAdapter(adapter) {
|
||||
config._ctx.adapter = adapter;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -59,10 +83,10 @@ export async function runHookServerDone({ config }: { config: AstroConfig }) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function runHookBuildStart({ config }: { config: AstroConfig }) {
|
||||
export async function runHookBuildStart({ config, buildConfig }: { config: AstroConfig, buildConfig: BuildConfig }) {
|
||||
for (const integration of config.integrations) {
|
||||
if (integration.hooks['astro:build:start']) {
|
||||
await integration.hooks['astro:build:start']();
|
||||
await integration.hooks['astro:build:start']({ buildConfig });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -184,7 +184,7 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
|||
|
||||
// Removes imports for pure CSS chunks.
|
||||
if (hasPureCSSChunks) {
|
||||
if (internals.pureCSSChunks.has(chunk)) {
|
||||
if (internals.pureCSSChunks.has(chunk) && !chunk.exports.length) {
|
||||
// Delete pure CSS chunks, these are JavaScript chunks that only import
|
||||
// other CSS files, so are empty at the end of bundling.
|
||||
delete bundle[chunkId];
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { expect } from 'chai';
|
||||
import { load as cheerioLoad } from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
import testAdapter from './test-adapter.js';
|
||||
import { App } from '../dist/core/app/index.js';
|
||||
|
||||
// Asset bundling
|
||||
describe('Dynamic pages in SSR', () => {
|
||||
|
@ -12,15 +14,16 @@ describe('Dynamic pages in SSR', () => {
|
|||
buildOptions: {
|
||||
experimentalSsr: true,
|
||||
},
|
||||
adapter: testAdapter()
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Do not have to implement getStaticPaths', async () => {
|
||||
const app = await fixture.loadSSRApp();
|
||||
const {createApp} = await import('./fixtures/ssr-dynamic/dist/server/entry.mjs');
|
||||
const app = createApp(new URL('./fixtures/ssr-dynamic/dist/server/', import.meta.url));
|
||||
const request = new Request('http://example.com/123');
|
||||
const route = app.match(request);
|
||||
const response = await app.render(request, route);
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerioLoad(html);
|
||||
expect($('h1').text()).to.equal('Item 123');
|
||||
|
|
43
packages/astro/test/test-adapter.js
Normal file
43
packages/astro/test/test-adapter.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { viteID } from '../dist/core/util.js';
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {import('../src/@types/astro').AstroIntegration}
|
||||
*/
|
||||
export default function() {
|
||||
return {
|
||||
name: 'my-ssr-adapter',
|
||||
hooks: {
|
||||
'astro:config:setup': ({ updateConfig }) => {
|
||||
updateConfig({
|
||||
vite: {
|
||||
plugins: [
|
||||
{
|
||||
resolveId(id) {
|
||||
if(id === '@my-ssr') {
|
||||
return id;
|
||||
} else if(id === 'astro/app') {
|
||||
const id = viteID(new URL('../dist/core/app/index.js', import.meta.url));
|
||||
return id;
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if(id === '@my-ssr') {
|
||||
return `import { App } from 'astro/app';export function createExports(manifest) { return { manifest, createApp: (root) => new App(manifest, root) }; }`;
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
})
|
||||
},
|
||||
'astro:config:done': ({ setAdapter }) => {
|
||||
setAdapter({
|
||||
name: 'my-ssr-adapter',
|
||||
serverEntrypoint: '@my-ssr',
|
||||
exports: ['manifest', 'createApp']
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -82,7 +82,6 @@ export async function loadFixture(inlineConfig) {
|
|||
const previewServer = await preview(config, { logging: 'error', ...opts });
|
||||
return previewServer;
|
||||
},
|
||||
loadSSRApp: () => loadApp(new URL('./server/', config.dist)),
|
||||
readFile: (filePath) => fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.dist), 'utf8'),
|
||||
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.dist)),
|
||||
clean: () => fs.promises.rm(config.dist, { maxRetries: 10, recursive: true, force: true }),
|
||||
|
|
32
packages/integrations/node/package.json
Normal file
32
packages/integrations/node/package.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "@astrojs/node",
|
||||
"description": "Deploy your site to a Node.js server",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"types": "./dist/index.d.ts",
|
||||
"author": "withastro",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/withastro/astro.git",
|
||||
"directory": "packages/integrations/node"
|
||||
},
|
||||
"bugs": "https://github.com/withastro/astro/issues",
|
||||
"homepage": "https://astro.build",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./server.js": "./dist/server.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
|
||||
"dev": "astro-scripts dev \"src/**/*.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/webapi": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*"
|
||||
}
|
||||
}
|
53
packages/integrations/node/readme.md
Normal file
53
packages/integrations/node/readme.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
# @astrojs/node
|
||||
|
||||
An experimental static-side rendering adapter for use with Node.js servers.
|
||||
|
||||
In your astro.config.mjs use:
|
||||
|
||||
```js
|
||||
import nodejs from '@astrojs/node';
|
||||
|
||||
export default {
|
||||
adapter: nodejs()
|
||||
}
|
||||
```
|
||||
|
||||
After performing a build there will be a `dist/server/entry.mjs` module that works like a middleware function. You can use with any framework that supports the Node `request` and `response` objects. For example, with Express you can do:
|
||||
|
||||
```js
|
||||
import express from 'express';
|
||||
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
||||
|
||||
const app = express();
|
||||
app.use(ssrHandler);
|
||||
|
||||
app.listen(8080);
|
||||
```
|
||||
|
||||
# Using `http`
|
||||
|
||||
This adapter does not require you use Express and can work with even the `http` and `https` modules. The adapter does following the Expression convention of calling a function when either
|
||||
|
||||
- A route is not found for the request.
|
||||
- There was an error rendering.
|
||||
|
||||
You can use these to implement your own 404 behavior like so:
|
||||
|
||||
```js
|
||||
import http from 'http';
|
||||
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
||||
|
||||
http.createServer(function(req, res) {
|
||||
ssrHandler(req, res, err => {
|
||||
if(err) {
|
||||
res.writeHead(500);
|
||||
res.end(err.toString());
|
||||
} else {
|
||||
// Serve your static assets here maybe?
|
||||
// 404?
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}).listen(8080);
|
||||
```
|
20
packages/integrations/node/src/index.ts
Normal file
20
packages/integrations/node/src/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { AstroAdapter, AstroIntegration } from 'astro';
|
||||
|
||||
export function getAdapter(): AstroAdapter {
|
||||
return {
|
||||
name: '@astrojs/node',
|
||||
serverEntrypoint: '@astrojs/node/server.js',
|
||||
exports: ['handler'],
|
||||
};
|
||||
}
|
||||
|
||||
export default function createIntegration(): AstroIntegration {
|
||||
return {
|
||||
name: '@astrojs/node',
|
||||
hooks: {
|
||||
'astro:config:done': ({ setAdapter }) => {
|
||||
setAdapter(getAdapter());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
48
packages/integrations/node/src/server.ts
Normal file
48
packages/integrations/node/src/server.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import type { SSRManifest } from 'astro';
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { NodeApp } from 'astro/app/node';
|
||||
import { polyfill } from '@astrojs/webapi';
|
||||
|
||||
polyfill(globalThis, {
|
||||
exclude: 'window document'
|
||||
});
|
||||
|
||||
export function createExports(manifest: SSRManifest) {
|
||||
const app = new NodeApp(manifest, new URL(import.meta.url));
|
||||
return {
|
||||
async handler(req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) {
|
||||
const route = app.match(req);
|
||||
|
||||
if(route) {
|
||||
try {
|
||||
const response = await app.render(req);
|
||||
await writeWebResponse(res, response);
|
||||
} catch(err: unknown) {
|
||||
if(next) {
|
||||
next(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else if(next) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function writeWebResponse(res: ServerResponse, webResponse: Response) {
|
||||
const { status, headers, body } = webResponse;
|
||||
res.writeHead(status, Object.fromEntries(headers.entries()));
|
||||
if (body) {
|
||||
const reader = body.getReader();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
if (value) {
|
||||
res.write(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.end();
|
||||
}
|
10
packages/integrations/node/tsconfig.json
Normal file
10
packages/integrations/node/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"module": "ES2020",
|
||||
"outDir": "./dist",
|
||||
"target": "ES2020"
|
||||
}
|
||||
}
|
|
@ -286,6 +286,7 @@ importers:
|
|||
|
||||
examples/ssr:
|
||||
specifiers:
|
||||
'@astrojs/node': ^0.0.1
|
||||
'@astrojs/svelte': ^0.0.2-next.0
|
||||
astro: ^0.25.0-next.2
|
||||
concurrently: ^7.0.0
|
||||
|
@ -296,6 +297,7 @@ importers:
|
|||
dependencies:
|
||||
svelte: 3.46.4
|
||||
devDependencies:
|
||||
'@astrojs/node': link:../../packages/integrations/node
|
||||
'@astrojs/svelte': link:../../packages/integrations/svelte
|
||||
astro: link:../../packages/astro
|
||||
concurrently: 7.0.0
|
||||
|
@ -1175,6 +1177,17 @@ importers:
|
|||
astro: link:../../astro
|
||||
astro-scripts: link:../../../scripts
|
||||
|
||||
packages/integrations/node:
|
||||
specifiers:
|
||||
'@astrojs/webapi': ^0.11.0
|
||||
astro: workspace:*
|
||||
astro-scripts: workspace:*
|
||||
dependencies:
|
||||
'@astrojs/webapi': link:../../webapi
|
||||
devDependencies:
|
||||
astro: link:../../astro
|
||||
astro-scripts: link:../../../scripts
|
||||
|
||||
packages/integrations/partytown:
|
||||
specifiers:
|
||||
'@builder.io/partytown': ^0.4.5
|
||||
|
|
Loading…
Reference in a new issue