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
87 changes: 61 additions & 26 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
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 { 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';

const supportedFileNameToMimeTypes = new Map<string, string>([
['json', 'application/json']
]);

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();
this
}
match(request: Request): RouteData | undefined {
const url = new URL(request.url);
Expand All @@ -42,11 +45,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 +94,47 @@ 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 ext = /\.([a-z]+)/.exec(url.pathname);
const headers = new Headers();
if(ext) {
const mime = supportedFileNameToMimeTypes.get(ext[1]);
if(mime) {
headers.set('Content-Type', mime);
}
}
const bytes = this.#encoder.encode(body);
headers.set('Content-Length', bytes.byteLength.toString());
return new Response(bytes, {
status: 200,
headers
});
}
}
}
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
44 changes: 21 additions & 23 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 { StaticBuildOptions, SingleFileBuiltModule } from './types';
import type { BuildInternals } from '../../core/build/internal.js';
import type { RenderOptions } from '../../core/render/core';

Expand All @@ -14,7 +14,8 @@ 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';
import { getOutRoot, getOutFolder, getOutFile, getServerRoot } from './common.js';
import { getPageDataByComponent, eachPageData } from './internal.js';

// Render is usually compute, which Node.js can't parallelize well.
// In real world testing, dropping from 10->1 showed a notiable perf
Expand Down Expand Up @@ -85,44 +86,41 @@ export function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | Outp
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);
const ssr = !!opts.astroConfig._ctx.adapter?.serverEntrypoint;
const outFolder = ssr ? getServerRoot(opts.astroConfig) : getOutRoot(opts.astroConfig);
const ssrEntryURL = new URL('./entry.mjs', 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)) {
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
) {
const { astroConfig } = opts;

let url = new URL('./' + output.fileName, getOutRoot(astroConfig));
const facadeId: string = output.facadeModuleId as string;
let pageData = getByFacadeId<PageBuildData>(facadeId, facadeIdToPageDataMap);
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