Skip to content

Commit

Permalink
Build to a single file (#2873)
Browse files Browse the repository at this point in the history
* Build to a single file

* Updates based on initial code review

* Adds a changeset

* Use the default export for cjs module

* Await generatePages

* Prevent timing from causing module to not import

* Fix shared CSS

* Properly handle windows ids

* Dont shadow

* Fix ts errors

* Remove console.log
  • Loading branch information
matthewp authored Mar 24, 2022
1 parent e2885df commit e4025d1
Show file tree
Hide file tree
Showing 25 changed files with 428 additions and 188 deletions.
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

0 comments on commit e4025d1

Please sign in to comment.