Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build to a single file #2873

Merged
merged 14 commits into from
Mar 24, 2022
6 changes: 6 additions & 0 deletions .changeset/small-horses-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'astro': patch
'@astrojs/node': patch
---

Improves the build by building to a single file for rendering
80 changes: 54 additions & 26 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
import type { ComponentInstance, ManifestData, RouteData, SSRLoadedRenderer } from '../../@types/astro';
import type { ComponentInstance, EndpointHandler, ManifestData, RouteData } from '../../@types/astro';
import type { SSRManifest as Manifest, RouteInfo } from './types';

import mime from 'mime';
import { defaultLogOptions } from '../logger.js';
export { deserializeManifest } from './common.js';
import { matchRoute } from '../routing/match.js';
import { render } from '../render/core.js';
import { call as callEndpoint } from '../endpoint/index.js';
import { RouteCache } from '../render/route-cache.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
import { prependForwardSlash } from '../path.js';

export class App {
#manifest: Manifest;
#manifestData: ManifestData;
#rootFolder: URL;
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
#routeCache: RouteCache;
#renderersPromise: Promise<SSRLoadedRenderer[]>;
#encoder = new TextEncoder();

constructor(manifest: Manifest, rootFolder: URL) {
constructor(manifest: Manifest) {
this.#manifest = manifest;
this.#manifestData = {
routes: manifest.routes.map((route) => route.routeData),
};
this.#rootFolder = rootFolder;
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
this.#routeCache = new RouteCache(defaultLogOptions);
this.#renderersPromise = this.#loadRenderers();
}
match(request: Request): RouteData | undefined {
const url = new URL(request.url);
Expand All @@ -42,11 +41,22 @@ export class App {
}
}

const manifest = this.#manifest;
const info = this.#routeDataToRouteInfo.get(routeData!)!;
const [mod, renderers] = await Promise.all([this.#loadModule(info.file), this.#renderersPromise]);
const mod = this.#manifest.pageMap.get(routeData.component)!;

if(routeData.type === 'page') {
return this.#renderPage(request, routeData, mod);
} else if(routeData.type === 'endpoint') {
return this.#callEndpoint(request, routeData, mod);
} else {
throw new Error(`Unsupported route type [${routeData.type}].`);
}
}

async #renderPage(request: Request, routeData: RouteData, mod: ComponentInstance): Promise<Response> {
const url = new URL(request.url);
const manifest = this.#manifest;
const renderers = manifest.renderers;
const info = this.#routeDataToRouteInfo.get(routeData!)!;
const links = createLinkStylesheetElementSet(info.links, manifest.site);
const scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site);

Expand Down Expand Up @@ -80,26 +90,44 @@ export class App {
}

let html = result.html;
return new Response(html, {
let bytes = this.#encoder.encode(html);
return new Response(bytes, {
status: 200,
headers: {
'Content-Type': 'text/html',
'Content-Length': bytes.byteLength.toString()
}
});
}
async #loadRenderers(): Promise<SSRLoadedRenderer[]> {
return await Promise.all(
this.#manifest.renderers.map(async (renderer) => {
const mod = (await import(renderer.serverEntrypoint)) as { default: SSRLoadedRenderer['ssr'] };
return { ...renderer, ssr: mod.default };
})
);
}
async #loadModule(rootRelativePath: string): Promise<ComponentInstance> {
let modUrl = new URL(rootRelativePath, this.#rootFolder).toString();
let mod: ComponentInstance;
try {
mod = await import(modUrl);
return mod;
} catch (err) {
throw new Error(`Unable to import ${modUrl}. Does this file exist?`);

async #callEndpoint(request: Request, routeData: RouteData, mod: ComponentInstance): Promise<Response> {
const url = new URL(request.url);
const handler = mod as unknown as EndpointHandler;
const result = await callEndpoint(handler, {
headers: request.headers,
logging: defaultLogOptions,
method: request.method,
origin: url.origin,
pathname: url.pathname,
routeCache: this.#routeCache,
ssr: true,
});

if(result.type === 'response') {
return result.response;
} else {
const body = result.body;
const headers = new Headers();
const mimeType = mime.getType(url.pathname);
if(mimeType) {
headers.set('Content-Type', mimeType);
}
const bytes = this.#encoder.encode(body);
headers.set('Content-Length', bytes.byteLength.toString());
return new Response(bytes, {
status: 200,
headers
});
}
}
}
2 changes: 1 addition & 1 deletion packages/astro/src/core/app/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ export async function loadManifest(rootFolder: URL): Promise<SSRManifest> {

export async function loadApp(rootFolder: URL): Promise<NodeApp> {
const manifest = await loadManifest(rootFolder);
return new NodeApp(manifest, rootFolder);
return new NodeApp(manifest);
}
7 changes: 5 additions & 2 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { RouteData, SerializedRouteData, MarkdownRenderOptions, AstroRenderer } from '../../@types/astro';
import type { RouteData, SerializedRouteData, MarkdownRenderOptions, ComponentInstance, SSRLoadedRenderer } from '../../@types/astro';

export type ComponentPath = string;

export interface RouteInfo {
routeData: RouteData;
Expand All @@ -17,7 +19,8 @@ export interface SSRManifest {
markdown: {
render: MarkdownRenderOptions;
};
renderers: AstroRenderer[];
pageMap: Map<ComponentPath, ComponentInstance>;
renderers: SSRLoadedRenderer[];
entryModules: Record<string, string>;
}

Expand Down
43 changes: 43 additions & 0 deletions packages/astro/src/core/build/add-rollup-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { InputOptions } from 'rollup';

function fromEntries<V>(entries: [string, V][]) {
const obj: Record<string, V> = {};
for (const [k, v] of entries) {
obj[k] = v;
}
return obj;
}

export function addRollupInput(inputOptions: InputOptions, newInputs: string[]): InputOptions {
// Add input module ids to existing input option, whether it's a string, array or object
// this way you can use multiple html plugins all adding their own inputs
if (!inputOptions.input) {
return { ...inputOptions, input: newInputs };
}

if (typeof inputOptions.input === 'string') {
return {
...inputOptions,
input: [inputOptions.input, ...newInputs],
};
}

if (Array.isArray(inputOptions.input)) {
return {
...inputOptions,
input: [...inputOptions.input, ...newInputs],
};
}

if (typeof inputOptions.input === 'object') {
return {
...inputOptions,
input: {
...inputOptions.input,
...fromEntries(newInputs.map((i) => [i.split('/').slice(-1)[0].split('.')[0], i])),
},
};
}

throw new Error(`Unknown rollup input type. Supported inputs are string, array and object.`);
}
1 change: 1 addition & 0 deletions packages/astro/src/core/build/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AstroConfig, RouteType } from '../../@types/astro';
import type { StaticBuildOptions } from './types';
import npath from 'path';
import { appendForwardSlash } from '../../core/path.js';

Expand Down
76 changes: 28 additions & 48 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
import type { AstroConfig, ComponentInstance, EndpointHandler, SSRLoadedRenderer } from '../../@types/astro';
import type { PageBuildData, StaticBuildOptions, SingleFileBuiltModule } from './types';
import type { BuildInternals } from '../../core/build/internal.js';
import type { RenderOptions } from '../../core/render/core';

import fs from 'fs';
import { bgMagenta, black, cyan, dim, magenta } from 'kleur/colors';
import npath from 'path';
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
import { fileURLToPath } from 'url';
import type { AstroConfig, AstroRenderer, ComponentInstance, EndpointHandler, SSRLoadedRenderer } from '../../@types/astro';
import type { BuildInternals } from '../../core/build/internal.js';
import { debug, error, info } from '../../core/logger.js';
import { prependForwardSlash } from '../../core/path.js';
import type { RenderOptions } from '../../core/render/core';
import { resolveDependency } from '../../core/util.js';
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import { call as callEndpoint } from '../endpoint/index.js';
import { render } from '../render/core.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
import { getOutFile, getOutFolder, getOutRoot } from './common.js';
import type { PageBuildData, StaticBuildOptions } from './types';
import { getOutFile, getOutRoot, getOutFolder, getServerRoot } from './common.js';
import { getPageDataByComponent, eachPageData } from './internal.js';
import { bgMagenta, black, cyan, dim, magenta } from 'kleur/colors';
import { getTimeStat } from './util.js';

// Render is usually compute, which Node.js can't parallelize well.
Expand All @@ -23,24 +24,6 @@ import { getTimeStat } from './util.js';
// 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 = [];
Expand Down Expand Up @@ -86,45 +69,42 @@ export function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | Outp
export async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) {
info(opts.logging, null, `\n${bgMagenta(black(' generating static routes '))}\n`);

// Get renderers to be shared for each page generation.
const renderers = await loadRenderers(opts.astroConfig);
const ssr = !!opts.astroConfig._ctx.adapter?.serverEntrypoint;
const outFolder = ssr ? getServerRoot(opts.astroConfig) : getOutRoot(opts.astroConfig);
const ssrEntryURL = new URL(`./entry.mjs?time=${Date.now()}`, outFolder);
const ssrEntry = await import(ssrEntryURL.toString());

for (let output of result.output) {
if (chunkIsPage(opts.astroConfig, output, internals)) {
await generatePage(output as OutputChunk, opts, internals, facadeIdToPageDataMap, renderers);
}
for(const pageData of eachPageData(internals)) {
await generatePage(opts, internals, pageData, ssrEntry);
}
}

async function generatePage(
output: OutputChunk,
//output: OutputChunk,
opts: StaticBuildOptions,
internals: BuildInternals,
facadeIdToPageDataMap: Map<string, PageBuildData>,
renderers: SSRLoadedRenderer[]
pageData: PageBuildData,
ssrEntry: SingleFileBuiltModule
) {
let timeStart = performance.now();
const { astroConfig } = opts;

let url = new URL('./' + output.fileName, getOutRoot(astroConfig));
const facadeId: string = output.facadeModuleId as string;
let pageData = getByFacadeId<PageBuildData>(facadeId, facadeIdToPageDataMap);
let timeStart = performance.now();
const renderers = ssrEntry.renderers;

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 pageInfo = getPageDataByComponent(internals, pageData.route.component);
const linkIds: string[] = Array.from(pageInfo?.css ?? []);
const hoistedId = pageInfo?.hoistedScript ?? null;

const linkIds = getByFacadeId<string[]>(facadeId, internals.facadeIdToAssetsMap) || [];
const hoistedId = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap) || null;
const pageModule = ssrEntry.pageMap.get(pageData.component);

let compiledModule = await import(url.toString());
if(!pageModule) {
throw new Error(`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`);
}

const generationOptions: Readonly<GeneratePathOptions> = {
pageData,
internals,
linkIds,
hoistedId,
mod: compiledModule,
mod: pageModule,
renderers,
};

Expand Down
Loading