diff --git a/.changeset/small-horses-protect.md b/.changeset/small-horses-protect.md new file mode 100644 index 000000000..ba6ec4fd7 --- /dev/null +++ b/.changeset/small-horses-protect.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'@astrojs/node': patch +--- + +Improves the build by building to a single file for rendering diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index b14fad504..050802ee0 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -1,10 +1,12 @@ -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'; @@ -12,20 +14,17 @@ import { prependForwardSlash } from '../path.js'; export class App { #manifest: Manifest; #manifestData: ManifestData; - #rootFolder: URL; #routeDataToRouteInfo: Map; #routeCache: RouteCache; - #renderersPromise: Promise; + #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); @@ -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 { 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); @@ -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 { - 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 { - 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 { + 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 + }); } } } diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index 310244c74..8eee93d0f 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -33,5 +33,5 @@ export async function loadManifest(rootFolder: URL): Promise { export async function loadApp(rootFolder: URL): Promise { const manifest = await loadManifest(rootFolder); - return new NodeApp(manifest, rootFolder); + return new NodeApp(manifest); } diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index d78a94050..ea4bd9cc0 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -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; @@ -17,7 +19,8 @@ export interface SSRManifest { markdown: { render: MarkdownRenderOptions; }; - renderers: AstroRenderer[]; + pageMap: Map; + renderers: SSRLoadedRenderer[]; entryModules: Record; } diff --git a/packages/astro/src/core/build/add-rollup-input.ts b/packages/astro/src/core/build/add-rollup-input.ts new file mode 100644 index 000000000..79feb3a7d --- /dev/null +++ b/packages/astro/src/core/build/add-rollup-input.ts @@ -0,0 +1,43 @@ +import { InputOptions } from 'rollup'; + +function fromEntries(entries: [string, V][]) { + const obj: Record = {}; + 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.`); +} diff --git a/packages/astro/src/core/build/common.ts b/packages/astro/src/core/build/common.ts index fca513781..074170762 100644 --- a/packages/astro/src/core/build/common.ts +++ b/packages/astro/src/core/build/common.ts @@ -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'; diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index d3d2365b4..cbd5b3c2b 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -1,20 +1,21 @@ -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 { 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 npath from 'path'; +import { fileURLToPath } from 'url'; 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. @@ -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 { - const mod = (await import(resolveDependency(renderer.serverEntrypoint, config))) as { default: SSRLoadedRenderer['ssr'] }; - return { ...renderer, ssr: mod.default }; -} - -async function loadRenderers(config: AstroConfig): Promise { - return Promise.all(config._ctx.renderers.map((r) => loadRenderer(r, config))); -} - -export function getByFacadeId(facadeId: string, map: Map): 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 = []; @@ -86,45 +69,42 @@ export function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | Outp export async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map) { 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, - renderers: SSRLoadedRenderer[] + pageData: PageBuildData, + ssrEntry: SingleFileBuiltModule ) { - let timeStart = performance.now(); - const { astroConfig } = opts; + let timeStart = performance.now(); + const renderers = ssrEntry.renderers; - let url = new URL('./' + output.fileName, getOutRoot(astroConfig)); - const facadeId: string = output.facadeModuleId as string; - let pageData = getByFacadeId(facadeId, facadeIdToPageDataMap); + const pageInfo = getPageDataByComponent(internals, pageData.route.component); + const linkIds: string[] = Array.from(pageInfo?.css ?? []); + const hoistedId = pageInfo?.hoistedScript ?? null; - 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 pageModule = ssrEntry.pageMap.get(pageData.component); + + 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 linkIds = getByFacadeId(facadeId, internals.facadeIdToAssetsMap) || []; - const hoistedId = getByFacadeId(facadeId, internals.facadeIdToHoistedEntryMap) || null; - - let compiledModule = await import(url.toString()); - const generationOptions: Readonly = { pageData, internals, linkIds, hoistedId, - mod: compiledModule, + mod: pageModule, renderers, }; diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index 9185e7e89..62186f678 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -1,26 +1,47 @@ +import type { RouteData } from '../../@types/astro'; import type { RenderedChunk } from 'rollup'; +import type { PageBuildData, ViteID } from './types'; + +import { viteID } from '../util.js'; export interface BuildInternals { // Pure CSS chunks are chunks that only contain CSS. pureCSSChunks: Set; - // chunkToReferenceIdMap maps them to a hash id used to find the final file. - chunkToReferenceIdMap: Map; - - // This is a mapping of pathname to the string source of all collected - // inline