Build to a single file (#2873)
* 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
This commit is contained in:
parent
e2885df50b
commit
e4025d1f53
25 changed files with 429 additions and 189 deletions
6
.changeset/small-horses-protect.md
Normal file
6
.changeset/small-horses-protect.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
'@astrojs/node': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improves the build by building to a single file for rendering
|
|
@ -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 type { SSRManifest as Manifest, RouteInfo } from './types';
|
||||||
|
|
||||||
|
import mime from 'mime';
|
||||||
import { defaultLogOptions } from '../logger.js';
|
import { defaultLogOptions } from '../logger.js';
|
||||||
export { deserializeManifest } from './common.js';
|
export { deserializeManifest } from './common.js';
|
||||||
import { matchRoute } from '../routing/match.js';
|
import { matchRoute } from '../routing/match.js';
|
||||||
import { render } from '../render/core.js';
|
import { render } from '../render/core.js';
|
||||||
|
import { call as callEndpoint } from '../endpoint/index.js';
|
||||||
import { RouteCache } from '../render/route-cache.js';
|
import { RouteCache } from '../render/route-cache.js';
|
||||||
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
||||||
import { prependForwardSlash } from '../path.js';
|
import { prependForwardSlash } from '../path.js';
|
||||||
|
@ -12,20 +14,17 @@ import { prependForwardSlash } from '../path.js';
|
||||||
export class App {
|
export class App {
|
||||||
#manifest: Manifest;
|
#manifest: Manifest;
|
||||||
#manifestData: ManifestData;
|
#manifestData: ManifestData;
|
||||||
#rootFolder: URL;
|
|
||||||
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
|
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
|
||||||
#routeCache: RouteCache;
|
#routeCache: RouteCache;
|
||||||
#renderersPromise: Promise<SSRLoadedRenderer[]>;
|
#encoder = new TextEncoder();
|
||||||
|
|
||||||
constructor(manifest: Manifest, rootFolder: URL) {
|
constructor(manifest: Manifest) {
|
||||||
this.#manifest = manifest;
|
this.#manifest = manifest;
|
||||||
this.#manifestData = {
|
this.#manifestData = {
|
||||||
routes: manifest.routes.map((route) => route.routeData),
|
routes: manifest.routes.map((route) => route.routeData),
|
||||||
};
|
};
|
||||||
this.#rootFolder = rootFolder;
|
|
||||||
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
|
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
|
||||||
this.#routeCache = new RouteCache(defaultLogOptions);
|
this.#routeCache = new RouteCache(defaultLogOptions);
|
||||||
this.#renderersPromise = this.#loadRenderers();
|
|
||||||
}
|
}
|
||||||
match(request: Request): RouteData | undefined {
|
match(request: Request): RouteData | undefined {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
@ -42,11 +41,22 @@ export class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const manifest = this.#manifest;
|
const mod = this.#manifest.pageMap.get(routeData.component)!;
|
||||||
const info = this.#routeDataToRouteInfo.get(routeData!)!;
|
|
||||||
const [mod, renderers] = await Promise.all([this.#loadModule(info.file), this.#renderersPromise]);
|
|
||||||
|
|
||||||
|
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 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 links = createLinkStylesheetElementSet(info.links, manifest.site);
|
||||||
const scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site);
|
const scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site);
|
||||||
|
|
||||||
|
@ -80,26 +90,44 @@ export class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = result.html;
|
let html = result.html;
|
||||||
return new Response(html, {
|
let bytes = this.#encoder.encode(html);
|
||||||
|
return new Response(bytes, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'Content-Length': bytes.byteLength.toString()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async #loadRenderers(): Promise<SSRLoadedRenderer[]> {
|
|
||||||
return await Promise.all(
|
async #callEndpoint(request: Request, routeData: RouteData, mod: ComponentInstance): Promise<Response> {
|
||||||
this.#manifest.renderers.map(async (renderer) => {
|
const url = new URL(request.url);
|
||||||
const mod = (await import(renderer.serverEntrypoint)) as { default: SSRLoadedRenderer['ssr'] };
|
const handler = mod as unknown as EndpointHandler;
|
||||||
return { ...renderer, ssr: mod.default };
|
const result = await callEndpoint(handler, {
|
||||||
})
|
headers: request.headers,
|
||||||
);
|
logging: defaultLogOptions,
|
||||||
}
|
method: request.method,
|
||||||
async #loadModule(rootRelativePath: string): Promise<ComponentInstance> {
|
origin: url.origin,
|
||||||
let modUrl = new URL(rootRelativePath, this.#rootFolder).toString();
|
pathname: url.pathname,
|
||||||
let mod: ComponentInstance;
|
routeCache: this.#routeCache,
|
||||||
try {
|
ssr: true,
|
||||||
mod = await import(modUrl);
|
});
|
||||||
return mod;
|
|
||||||
} catch (err) {
|
if(result.type === 'response') {
|
||||||
throw new Error(`Unable to import ${modUrl}. Does this file exist?`);
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,5 +33,5 @@ export async function loadManifest(rootFolder: URL): Promise<SSRManifest> {
|
||||||
|
|
||||||
export async function loadApp(rootFolder: URL): Promise<NodeApp> {
|
export async function loadApp(rootFolder: URL): Promise<NodeApp> {
|
||||||
const manifest = await loadManifest(rootFolder);
|
const manifest = await loadManifest(rootFolder);
|
||||||
return new NodeApp(manifest, rootFolder);
|
return new NodeApp(manifest);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
export interface RouteInfo {
|
||||||
routeData: RouteData;
|
routeData: RouteData;
|
||||||
|
@ -17,7 +19,8 @@ export interface SSRManifest {
|
||||||
markdown: {
|
markdown: {
|
||||||
render: MarkdownRenderOptions;
|
render: MarkdownRenderOptions;
|
||||||
};
|
};
|
||||||
renderers: AstroRenderer[];
|
pageMap: Map<ComponentPath, ComponentInstance>;
|
||||||
|
renderers: SSRLoadedRenderer[];
|
||||||
entryModules: Record<string, string>;
|
entryModules: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
43
packages/astro/src/core/build/add-rollup-input.ts
Normal file
43
packages/astro/src/core/build/add-rollup-input.ts
Normal file
|
@ -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,4 +1,5 @@
|
||||||
import type { AstroConfig, RouteType } from '../../@types/astro';
|
import type { AstroConfig, RouteType } from '../../@types/astro';
|
||||||
|
import type { StaticBuildOptions } from './types';
|
||||||
import npath from 'path';
|
import npath from 'path';
|
||||||
import { appendForwardSlash } from '../../core/path.js';
|
import { appendForwardSlash } from '../../core/path.js';
|
||||||
|
|
||||||
|
|
|
@ -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 type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
|
||||||
import { fileURLToPath } from 'url';
|
import type { AstroConfig, ComponentInstance, EndpointHandler, SSRLoadedRenderer } from '../../@types/astro';
|
||||||
import type { AstroConfig, AstroRenderer, ComponentInstance, EndpointHandler, SSRLoadedRenderer } from '../../@types/astro';
|
import type { PageBuildData, StaticBuildOptions, SingleFileBuiltModule } from './types';
|
||||||
import type { BuildInternals } from '../../core/build/internal.js';
|
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 { debug, error, info } from '../../core/logger.js';
|
||||||
import { prependForwardSlash } from '../../core/path.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 { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
import { call as callEndpoint } from '../endpoint/index.js';
|
import { call as callEndpoint } from '../endpoint/index.js';
|
||||||
import { render } from '../render/core.js';
|
import { render } from '../render/core.js';
|
||||||
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
||||||
import { getOutFile, getOutFolder, getOutRoot } from './common.js';
|
import { getOutFile, getOutRoot, getOutFolder, getServerRoot } from './common.js';
|
||||||
import type { PageBuildData, StaticBuildOptions } from './types';
|
import { getPageDataByComponent, eachPageData } from './internal.js';
|
||||||
|
import { bgMagenta, black, cyan, dim, magenta } from 'kleur/colors';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
|
||||||
// Render is usually compute, which Node.js can't parallelize well.
|
// 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.
|
// system, possibly one that parallelizes if async IO is detected.
|
||||||
const MAX_CONCURRENT_RENDERS = 1;
|
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.
|
// Throttle the rendering a paths to prevents creating too many Promises on the microtask queue.
|
||||||
function* throttle(max: number, inPaths: string[]) {
|
function* throttle(max: number, inPaths: string[]) {
|
||||||
let tmp = [];
|
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<string, PageBuildData>) {
|
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`);
|
info(opts.logging, null, `\n${bgMagenta(black(' generating static routes '))}\n`);
|
||||||
|
|
||||||
// Get renderers to be shared for each page generation.
|
const ssr = !!opts.astroConfig._ctx.adapter?.serverEntrypoint;
|
||||||
const renderers = await loadRenderers(opts.astroConfig);
|
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) {
|
for(const pageData of eachPageData(internals)) {
|
||||||
if (chunkIsPage(opts.astroConfig, output, internals)) {
|
await generatePage(opts, internals, pageData, ssrEntry);
|
||||||
await generatePage(output as OutputChunk, opts, internals, facadeIdToPageDataMap, renderers);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generatePage(
|
async function generatePage(
|
||||||
output: OutputChunk,
|
//output: OutputChunk,
|
||||||
opts: StaticBuildOptions,
|
opts: StaticBuildOptions,
|
||||||
internals: BuildInternals,
|
internals: BuildInternals,
|
||||||
facadeIdToPageDataMap: Map<string, PageBuildData>,
|
pageData: PageBuildData,
|
||||||
renderers: SSRLoadedRenderer[]
|
ssrEntry: SingleFileBuiltModule
|
||||||
) {
|
) {
|
||||||
let timeStart = performance.now();
|
let timeStart = performance.now();
|
||||||
const { astroConfig } = opts;
|
const renderers = ssrEntry.renderers;
|
||||||
|
|
||||||
let url = new URL('./' + output.fileName, getOutRoot(astroConfig));
|
const pageInfo = getPageDataByComponent(internals, pageData.route.component);
|
||||||
const facadeId: string = output.facadeModuleId as string;
|
const linkIds: string[] = Array.from(pageInfo?.css ?? []);
|
||||||
let pageData = getByFacadeId<PageBuildData>(facadeId, facadeIdToPageDataMap);
|
const hoistedId = pageInfo?.hoistedScript ?? null;
|
||||||
|
|
||||||
if (!pageData) {
|
const pageModule = ssrEntry.pageMap.get(pageData.component);
|
||||||
throw new Error(`Unable to find a PageBuildData for the Astro page: ${facadeId}. There are the PageBuildDatas we have ${Array.from(facadeIdToPageDataMap.keys()).join(', ')}`);
|
|
||||||
|
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<string[]>(facadeId, internals.facadeIdToAssetsMap) || [];
|
|
||||||
const hoistedId = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap) || null;
|
|
||||||
|
|
||||||
let compiledModule = await import(url.toString());
|
|
||||||
|
|
||||||
const generationOptions: Readonly<GeneratePathOptions> = {
|
const generationOptions: Readonly<GeneratePathOptions> = {
|
||||||
pageData,
|
pageData,
|
||||||
internals,
|
internals,
|
||||||
linkIds,
|
linkIds,
|
||||||
hoistedId,
|
hoistedId,
|
||||||
mod: compiledModule,
|
mod: pageModule,
|
||||||
renderers,
|
renderers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,47 @@
|
||||||
|
import type { RouteData } from '../../@types/astro';
|
||||||
import type { RenderedChunk } from 'rollup';
|
import type { RenderedChunk } from 'rollup';
|
||||||
|
import type { PageBuildData, ViteID } from './types';
|
||||||
|
|
||||||
|
import { viteID } from '../util.js';
|
||||||
|
|
||||||
export interface BuildInternals {
|
export interface BuildInternals {
|
||||||
// Pure CSS chunks are chunks that only contain CSS.
|
// Pure CSS chunks are chunks that only contain CSS.
|
||||||
pureCSSChunks: Set<RenderedChunk>;
|
pureCSSChunks: Set<RenderedChunk>;
|
||||||
// chunkToReferenceIdMap maps them to a hash id used to find the final file.
|
|
||||||
chunkToReferenceIdMap: Map<string, string>;
|
|
||||||
|
|
||||||
// This is a mapping of pathname to the string source of all collected
|
|
||||||
// inline <style> for a page.
|
|
||||||
astroStyleMap: Map<string, string>;
|
|
||||||
// This is a virtual JS module that imports all dependent styles for a page.
|
|
||||||
astroPageStyleMap: Map<string, string>;
|
|
||||||
|
|
||||||
// A mapping to entrypoints (facadeId) to assets (styles) that are added.
|
|
||||||
facadeIdToAssetsMap: Map<string, string[]>;
|
|
||||||
|
|
||||||
|
// TODO document what this is
|
||||||
hoistedScriptIdToHoistedMap: Map<string, Set<string>>;
|
hoistedScriptIdToHoistedMap: Map<string, Set<string>>;
|
||||||
facadeIdToHoistedEntryMap: Map<string, string>;
|
|
||||||
|
|
||||||
// A mapping of specifiers like astro/client/idle.js to the hashed bundled name.
|
// A mapping of specifiers like astro/client/idle.js to the hashed bundled name.
|
||||||
// Used to render pages with the correct specifiers.
|
// Used to render pages with the correct specifiers.
|
||||||
entrySpecifierToBundleMap: Map<string, string>;
|
entrySpecifierToBundleMap: Map<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map for page-specific information.
|
||||||
|
*/
|
||||||
|
pagesByComponent: Map<string, PageBuildData>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map for page-specific information by Vite ID (a path-like string)
|
||||||
|
*/
|
||||||
|
pagesByViteID: Map<ViteID, PageBuildData>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chunkToReferenceIdMap maps them to a hash id used to find the final file.
|
||||||
|
* @deprecated This Map is only used for the legacy build.
|
||||||
|
*/
|
||||||
|
chunkToReferenceIdMap: Map<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a mapping of pathname to the string source of all collected inline <style> for a page.
|
||||||
|
* @deprecated This Map is only used for the legacy build.
|
||||||
|
*/
|
||||||
|
astroStyleMap: Map<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a virtual JS module that imports all dependent styles for a page.
|
||||||
|
* @deprecated This Map is only used for the legacy build.
|
||||||
|
*/
|
||||||
|
astroPageStyleMap: Map<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,21 +60,52 @@ export function createBuildInternals(): BuildInternals {
|
||||||
// This is a virtual JS module that imports all dependent styles for a page.
|
// This is a virtual JS module that imports all dependent styles for a page.
|
||||||
const astroPageStyleMap = new Map<string, string>();
|
const astroPageStyleMap = new Map<string, string>();
|
||||||
|
|
||||||
// A mapping to entrypoints (facadeId) to assets (styles) that are added.
|
|
||||||
const facadeIdToAssetsMap = new Map<string, string[]>();
|
|
||||||
|
|
||||||
// These are for tracking hoisted script bundling
|
// These are for tracking hoisted script bundling
|
||||||
const hoistedScriptIdToHoistedMap = new Map<string, Set<string>>();
|
const hoistedScriptIdToHoistedMap = new Map<string, Set<string>>();
|
||||||
const facadeIdToHoistedEntryMap = new Map<string, string>();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pureCSSChunks,
|
pureCSSChunks,
|
||||||
chunkToReferenceIdMap,
|
chunkToReferenceIdMap,
|
||||||
astroStyleMap,
|
astroStyleMap,
|
||||||
astroPageStyleMap,
|
astroPageStyleMap,
|
||||||
facadeIdToAssetsMap,
|
|
||||||
hoistedScriptIdToHoistedMap,
|
hoistedScriptIdToHoistedMap,
|
||||||
facadeIdToHoistedEntryMap,
|
|
||||||
entrySpecifierToBundleMap: new Map<string, string>(),
|
entrySpecifierToBundleMap: new Map<string, string>(),
|
||||||
|
|
||||||
|
pagesByComponent: new Map(),
|
||||||
|
pagesByViteID: new Map(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function trackPageData(internals: BuildInternals, component: string, pageData: PageBuildData, componentModuleId: string, componentURL: URL): void {
|
||||||
|
pageData.moduleSpecifier = componentModuleId;
|
||||||
|
internals.pagesByComponent.set(component, pageData);
|
||||||
|
internals.pagesByViteID.set(viteID(componentURL), pageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function * getPageDatasByChunk(internals: BuildInternals, chunk: RenderedChunk): Generator<PageBuildData, void, unknown> {
|
||||||
|
const pagesByViteID = internals.pagesByViteID;
|
||||||
|
for(const [modulePath] of Object.entries(chunk.modules)) {
|
||||||
|
if(pagesByViteID.has(modulePath)) {
|
||||||
|
yield pagesByViteID.get(modulePath)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPageDataByComponent(internals: BuildInternals, component: string): PageBuildData | undefined {
|
||||||
|
if(internals.pagesByComponent.has(component)) {
|
||||||
|
return internals.pagesByComponent.get(component);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPageDataByViteID(internals: BuildInternals, viteid: ViteID): PageBuildData | undefined {
|
||||||
|
if(internals.pagesByViteID.has(viteid)) {
|
||||||
|
return internals.pagesByViteID.get(viteid);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function * eachPageData(internals: BuildInternals) {
|
||||||
|
yield * internals.pagesByComponent.values();
|
||||||
|
}
|
||||||
|
|
|
@ -58,8 +58,13 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
|
||||||
clearInterval(routeCollectionLogTimeout);
|
clearInterval(routeCollectionLogTimeout);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
allPages[route.component] = {
|
allPages[route.component] = {
|
||||||
|
component: route.component,
|
||||||
route,
|
route,
|
||||||
paths: [route.pathname],
|
paths: [route.pathname],
|
||||||
|
moduleSpecifier: '',
|
||||||
|
css: new Set(),
|
||||||
|
hoistedScript: undefined,
|
||||||
|
scripts: new Set(),
|
||||||
preload: await ssrPreload({
|
preload: await ssrPreload({
|
||||||
astroConfig,
|
astroConfig,
|
||||||
filePath: new URL(`./${route.component}`, astroConfig.projectRoot),
|
filePath: new URL(`./${route.component}`, astroConfig.projectRoot),
|
||||||
|
@ -120,8 +125,13 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
|
||||||
}
|
}
|
||||||
const finalPaths = result.staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean);
|
const finalPaths = result.staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean);
|
||||||
allPages[route.component] = {
|
allPages[route.component] = {
|
||||||
|
component: route.component,
|
||||||
route,
|
route,
|
||||||
paths: finalPaths,
|
paths: finalPaths,
|
||||||
|
moduleSpecifier: '',
|
||||||
|
css: new Set(),
|
||||||
|
hoistedScript: undefined,
|
||||||
|
scripts: new Set(),
|
||||||
preload: await ssrPreload({
|
preload: await ssrPreload({
|
||||||
astroConfig,
|
astroConfig,
|
||||||
filePath: new URL(`./${route.component}`, astroConfig.projectRoot),
|
filePath: new URL(`./${route.component}`, astroConfig.projectRoot),
|
||||||
|
|
|
@ -16,8 +16,11 @@ import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js'
|
||||||
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
||||||
import { vitePluginInternals } from './vite-plugin-internals.js';
|
import { vitePluginInternals } from './vite-plugin-internals.js';
|
||||||
import { vitePluginSSR } from './vite-plugin-ssr.js';
|
import { vitePluginSSR } from './vite-plugin-ssr.js';
|
||||||
|
import { vitePluginPages } from './vite-plugin-pages.js';
|
||||||
import { generatePages } from './generate.js';
|
import { generatePages } from './generate.js';
|
||||||
|
import { trackPageData } from './internal.js';
|
||||||
import { getClientRoot, getServerRoot, getOutRoot } from './common.js';
|
import { getClientRoot, getServerRoot, getOutRoot } from './common.js';
|
||||||
|
import { isBuildingToSSR } from '../util.js';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
|
||||||
export async function staticBuild(opts: StaticBuildOptions) {
|
export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
|
@ -45,6 +48,9 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
const astroModuleURL = new URL('./' + component, astroConfig.projectRoot);
|
const astroModuleURL = new URL('./' + component, astroConfig.projectRoot);
|
||||||
const astroModuleId = prependForwardSlash(component);
|
const astroModuleId = prependForwardSlash(component);
|
||||||
|
|
||||||
|
// Track the page data in internals
|
||||||
|
trackPageData(internals, component, pageData, astroModuleId, astroModuleURL);
|
||||||
|
|
||||||
if (pageData.route.type === 'page') {
|
if (pageData.route.type === 'page') {
|
||||||
const [renderers, mod] = pageData.preload;
|
const [renderers, mod] = pageData.preload;
|
||||||
const metadata = mod.$$metadata;
|
const metadata = mod.$$metadata;
|
||||||
|
@ -96,7 +102,6 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
|
|
||||||
timer.generate = performance.now();
|
timer.generate = performance.now();
|
||||||
if (opts.buildConfig.staticMode) {
|
if (opts.buildConfig.staticMode) {
|
||||||
console.log('huh?');
|
|
||||||
await generatePages(ssrResult, opts, internals, facadeIdToPageDataMap);
|
await generatePages(ssrResult, opts, internals, facadeIdToPageDataMap);
|
||||||
await cleanSsrOutput(opts);
|
await cleanSsrOutput(opts);
|
||||||
} else {
|
} else {
|
||||||
|
@ -122,10 +127,10 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
||||||
outDir: fileURLToPath(out),
|
outDir: fileURLToPath(out),
|
||||||
ssr: true,
|
ssr: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: Array.from(input),
|
input: [],
|
||||||
output: {
|
output: {
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
entryFileNames: 'entry.[hash].mjs',
|
entryFileNames: 'entry.mjs',
|
||||||
chunkFileNames: 'chunks/chunk.[hash].mjs',
|
chunkFileNames: 'chunks/chunk.[hash].mjs',
|
||||||
assetFileNames: 'assets/asset.[hash][extname]',
|
assetFileNames: 'assets/asset.[hash][extname]',
|
||||||
},
|
},
|
||||||
|
@ -139,12 +144,14 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vitePluginInternals(input, internals),
|
vitePluginInternals(input, internals),
|
||||||
|
vitePluginPages(opts, internals),
|
||||||
rollupPluginAstroBuildCSS({
|
rollupPluginAstroBuildCSS({
|
||||||
internals,
|
internals,
|
||||||
}),
|
}),
|
||||||
...(viteConfig.plugins || []),
|
...(viteConfig.plugins || []),
|
||||||
// SSR needs to be last
|
// SSR needs to be last
|
||||||
opts.astroConfig._ctx.adapter?.serverEntrypoint && vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter),
|
isBuildingToSSR(opts.astroConfig) &&
|
||||||
|
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter!),
|
||||||
],
|
],
|
||||||
publicDir: ssr ? false : viteConfig.publicDir,
|
publicDir: ssr ? false : viteConfig.publicDir,
|
||||||
root: viteConfig.root,
|
root: viteConfig.root,
|
||||||
|
|
17
packages/astro/src/core/build/types.d.ts
vendored
17
packages/astro/src/core/build/types.d.ts
vendored
|
@ -1,15 +1,23 @@
|
||||||
import type { ComponentPreload } from '../render/dev/index';
|
import type { ComponentPreload } from '../render/dev/index';
|
||||||
import type { AstroConfig, BuildConfig, ManifestData, RouteData } from '../../@types/astro';
|
import type { AstroConfig, BuildConfig, ManifestData, RouteData, ComponentInstance, SSRLoadedRenderer } from '../../@types/astro';
|
||||||
import type { ViteConfigWithSSR } from '../../create-vite';
|
import type { ViteConfigWithSSR } from '../../create-vite';
|
||||||
import type { LogOptions } from '../../logger';
|
import type { LogOptions } from '../../logger';
|
||||||
import type { RouteCache } from '../../render/route-cache.js';
|
import type { RouteCache } from '../../render/route-cache.js';
|
||||||
|
|
||||||
|
export type ComponentPath = string;
|
||||||
|
export type ViteID = string;
|
||||||
|
|
||||||
export interface PageBuildData {
|
export interface PageBuildData {
|
||||||
|
component: ComponentPath;
|
||||||
paths: string[];
|
paths: string[];
|
||||||
preload: ComponentPreload;
|
preload: ComponentPreload;
|
||||||
route: RouteData;
|
route: RouteData;
|
||||||
|
moduleSpecifier: string;
|
||||||
|
css: Set<string>;
|
||||||
|
hoistedScript: string | undefined;
|
||||||
|
scripts: Set<string>;
|
||||||
}
|
}
|
||||||
export type AllPagesData = Record<string, PageBuildData>;
|
export type AllPagesData = Record<ComponentPath, PageBuildData>;
|
||||||
|
|
||||||
/** Options for the static build */
|
/** Options for the static build */
|
||||||
export interface StaticBuildOptions {
|
export interface StaticBuildOptions {
|
||||||
|
@ -23,3 +31,8 @@ export interface StaticBuildOptions {
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
viteConfig: ViteConfigWithSSR;
|
viteConfig: ViteConfigWithSSR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SingleFileBuiltModule {
|
||||||
|
pageMap: Map<ComponentPath, ComponentInstance>;
|
||||||
|
renderers: SSRLoadedRenderer[];
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import type { AstroConfig } from '../../@types/astro';
|
import type { AstroConfig } from '../../@types/astro';
|
||||||
import type { Plugin as VitePlugin } from 'vite';
|
import type { Plugin as VitePlugin } from 'vite';
|
||||||
import type { BuildInternals } from '../../core/build/internal.js';
|
import type { BuildInternals } from '../../core/build/internal.js';
|
||||||
import { fileURLToPath } from 'url';
|
import { viteID } from '../util.js';
|
||||||
|
import { getPageDataByViteID } from './internal.js';
|
||||||
|
|
||||||
function virtualHoistedEntry(id: string) {
|
function virtualHoistedEntry(id: string) {
|
||||||
return id.endsWith('.astro/hoisted.js') || id.endsWith('.md/hoisted.js');
|
return id.endsWith('.astro/hoisted.js') || id.endsWith('.md/hoisted.js');
|
||||||
|
@ -37,8 +38,12 @@ export function vitePluginHoistedScripts(astroConfig: AstroConfig, internals: Bu
|
||||||
if (output.type === 'chunk' && output.facadeModuleId && virtualHoistedEntry(output.facadeModuleId)) {
|
if (output.type === 'chunk' && output.facadeModuleId && virtualHoistedEntry(output.facadeModuleId)) {
|
||||||
const facadeId = output.facadeModuleId!;
|
const facadeId = output.facadeModuleId!;
|
||||||
const pathname = facadeId.slice(0, facadeId.length - '/hoisted.js'.length);
|
const pathname = facadeId.slice(0, facadeId.length - '/hoisted.js'.length);
|
||||||
const filename = fileURLToPath(new URL('.' + pathname, astroConfig.projectRoot));
|
|
||||||
internals.facadeIdToHoistedEntryMap.set(filename, id);
|
const vid = viteID(new URL('.' + pathname, astroConfig.projectRoot));
|
||||||
|
const pageInfo = getPageDataByViteID(internals, vid);
|
||||||
|
if(pageInfo) {
|
||||||
|
pageInfo.hoistedScript = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
58
packages/astro/src/core/build/vite-plugin-pages.ts
Normal file
58
packages/astro/src/core/build/vite-plugin-pages.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
|
||||||
|
import type { Plugin as VitePlugin } from 'vite';
|
||||||
|
import type { BuildInternals } from './internal.js';
|
||||||
|
import type { StaticBuildOptions } from './types';
|
||||||
|
import { addRollupInput } from './add-rollup-input.js';
|
||||||
|
import { eachPageData } from './internal.js';
|
||||||
|
import { isBuildingToSSR } from '../util.js';
|
||||||
|
|
||||||
|
export const virtualModuleId = '@astrojs-pages-virtual-entry';
|
||||||
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
|
|
||||||
|
export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
||||||
|
return {
|
||||||
|
name: '@astro/plugin-build-pages',
|
||||||
|
|
||||||
|
options(options) {
|
||||||
|
if(!isBuildingToSSR(opts.astroConfig)) {
|
||||||
|
return addRollupInput(options, [virtualModuleId]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveId(id) {
|
||||||
|
if(id === virtualModuleId) {
|
||||||
|
return resolvedVirtualModuleId;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
load(id) {
|
||||||
|
if(id === resolvedVirtualModuleId) {
|
||||||
|
let importMap = '';
|
||||||
|
let imports = [];
|
||||||
|
let i = 0;
|
||||||
|
for(const pageData of eachPageData(internals)) {
|
||||||
|
const variable = `_page${i}`;
|
||||||
|
imports.push(`import * as ${variable} from '${pageData.moduleSpecifier}';`);
|
||||||
|
importMap += `['${pageData.component}', ${variable}],`;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
i = 0;
|
||||||
|
let rendererItems = '';
|
||||||
|
for(const renderer of opts.astroConfig._ctx.renderers) {
|
||||||
|
const variable = `_renderer${i}`;
|
||||||
|
imports.push(`import ${variable} from '${renderer.serverEntrypoint}';`);
|
||||||
|
rendererItems += `Object.assign(${JSON.stringify(renderer)}, { ssr: ${variable} }),`
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const def = `${imports.join('\n')}
|
||||||
|
|
||||||
|
export const pageMap = new Map([${importMap}]);
|
||||||
|
export const renderers = [${rendererItems}];`;
|
||||||
|
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,11 +5,13 @@ import type { AstroAdapter } from '../../@types/astro';
|
||||||
import type { StaticBuildOptions } from './types';
|
import type { StaticBuildOptions } from './types';
|
||||||
import type { SerializedRouteInfo, SerializedSSRManifest } from '../app/types';
|
import type { SerializedRouteInfo, SerializedSSRManifest } from '../app/types';
|
||||||
|
|
||||||
import { chunkIsPage, rootRelativeFacadeId, getByFacadeId } from './generate.js';
|
|
||||||
import { serializeRouteData } from '../routing/index.js';
|
import { serializeRouteData } from '../routing/index.js';
|
||||||
|
import { eachPageData } from './internal.js';
|
||||||
|
import { addRollupInput } from './add-rollup-input.js';
|
||||||
|
import { virtualModuleId as pagesVirtualModuleId } from './vite-plugin-pages.js';
|
||||||
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
|
|
||||||
const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
||||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
||||||
|
|
||||||
|
@ -17,13 +19,7 @@ export function vitePluginSSR(buildOpts: StaticBuildOptions, internals: BuildInt
|
||||||
return {
|
return {
|
||||||
name: '@astrojs/vite-plugin-astro-ssr',
|
name: '@astrojs/vite-plugin-astro-ssr',
|
||||||
options(opts) {
|
options(opts) {
|
||||||
if (Array.isArray(opts.input)) {
|
return addRollupInput(opts, [virtualModuleId]);
|
||||||
opts.input.push(virtualModuleId);
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
input: [virtualModuleId],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
resolveId(id) {
|
resolveId(id) {
|
||||||
if (id === virtualModuleId) {
|
if (id === virtualModuleId) {
|
||||||
|
@ -33,8 +29,12 @@ export function vitePluginSSR(buildOpts: StaticBuildOptions, internals: BuildInt
|
||||||
load(id) {
|
load(id) {
|
||||||
if (id === resolvedVirtualModuleId) {
|
if (id === resolvedVirtualModuleId) {
|
||||||
return `import * as adapter from '${adapter.serverEntrypoint}';
|
return `import * as adapter from '${adapter.serverEntrypoint}';
|
||||||
|
import * as _main from '${pagesVirtualModuleId}';
|
||||||
import { deserializeManifest as _deserializeManifest } from 'astro/app';
|
import { deserializeManifest as _deserializeManifest } from 'astro/app';
|
||||||
const _manifest = _deserializeManifest('${manifestReplace}');
|
const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
|
||||||
|
pageMap: _main.pageMap,
|
||||||
|
renderers: _main.renderers
|
||||||
|
});
|
||||||
|
|
||||||
${
|
${
|
||||||
adapter.exports
|
adapter.exports
|
||||||
|
@ -52,57 +52,38 @@ if(_start in adapter) {
|
||||||
},
|
},
|
||||||
|
|
||||||
generateBundle(opts, bundle) {
|
generateBundle(opts, bundle) {
|
||||||
const manifest = buildManifest(bundle, buildOpts, internals);
|
const manifest = buildManifest(buildOpts, internals);
|
||||||
|
|
||||||
for (const [_chunkName, chunk] of Object.entries(bundle)) {
|
for(const [_chunkName, chunk] of Object.entries(bundle)) {
|
||||||
if (chunk.type === 'asset') continue;
|
if(chunk.type === 'asset') continue;
|
||||||
if (chunk.modules[resolvedVirtualModuleId]) {
|
if(chunk.modules[resolvedVirtualModuleId]) {
|
||||||
const exp = new RegExp(`['"]${manifestReplace}['"]`);
|
const exp = new RegExp(`['"]${manifestReplace}['"]`);
|
||||||
const code = chunk.code;
|
const code = chunk.code;
|
||||||
chunk.code = code.replace(exp, () => {
|
chunk.code = code.replace(exp, () => {
|
||||||
return JSON.stringify(manifest);
|
return JSON.stringify(manifest);
|
||||||
});
|
});
|
||||||
chunk.fileName = 'entry.mjs';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildManifest(bundle: OutputBundle, opts: StaticBuildOptions, internals: BuildInternals): SerializedSSRManifest {
|
function buildManifest(opts: StaticBuildOptions, internals: BuildInternals): SerializedSSRManifest {
|
||||||
const { astroConfig, manifest } = opts;
|
const { astroConfig } = 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[] = [];
|
const routes: SerializedRouteInfo[] = [];
|
||||||
|
|
||||||
for (const routeData of manifest.routes) {
|
for(const pageData of eachPageData(internals)) {
|
||||||
const componentPath = routeData.component;
|
const scripts = Array.from(pageData.scripts);
|
||||||
|
if(pageData.hoistedScript) {
|
||||||
if (!rootRelativeIdToChunkMap.has(componentPath)) {
|
scripts.unshift(pageData.hoistedScript);
|
||||||
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({
|
routes.push({
|
||||||
file: chunk.fileName,
|
file: '',
|
||||||
links,
|
links: Array.from(pageData.css),
|
||||||
scripts,
|
scripts,
|
||||||
routeData: serializeRouteData(routeData),
|
routeData: serializeRouteData(pageData.route),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +97,8 @@ function buildManifest(bundle: OutputBundle, opts: StaticBuildOptions, internals
|
||||||
markdown: {
|
markdown: {
|
||||||
render: astroConfig.markdownOptions.render,
|
render: astroConfig.markdownOptions.render,
|
||||||
},
|
},
|
||||||
renderers: astroConfig._ctx.renderers,
|
pageMap: null as any,
|
||||||
|
renderers: [],
|
||||||
entryModules,
|
entryModules,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { RouteData, SerializedRouteData } from '../../../@types/astro';
|
import type { RouteData, SerializedRouteData } from '../../../@types/astro';
|
||||||
|
|
||||||
function createRouteData(pattern: RegExp, params: string[], component: string, pathname: string | undefined): RouteData {
|
function createRouteData(pattern: RegExp, params: string[], component: string, pathname: string | undefined, type: 'page' | 'endpoint'): RouteData {
|
||||||
return {
|
return {
|
||||||
type: 'page',
|
type,
|
||||||
pattern,
|
pattern,
|
||||||
params,
|
params,
|
||||||
component,
|
component,
|
||||||
|
@ -20,7 +20,7 @@ export function serializeRouteData(routeData: RouteData): SerializedRouteData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserializeRouteData(rawRouteData: SerializedRouteData) {
|
export function deserializeRouteData(rawRouteData: SerializedRouteData) {
|
||||||
const { component, params, pathname } = rawRouteData;
|
const { component, params, pathname, type } = rawRouteData;
|
||||||
const pattern = new RegExp(rawRouteData.pattern);
|
const pattern = new RegExp(rawRouteData.pattern);
|
||||||
return createRouteData(pattern, params, component, pathname);
|
return createRouteData(pattern, params, component, pathname, type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,6 +133,10 @@ export function emptyDir(_dir: URL, skip?: Set<string>): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isBuildingToSSR(config: AstroConfig): boolean {
|
||||||
|
return !!config._ctx.adapter?.serverEntrypoint;
|
||||||
|
}
|
||||||
|
|
||||||
// Vendored from https://github.com/genmon/aboutfeeds/blob/main/tools/pretty-feed-v3.xsl
|
// Vendored from https://github.com/genmon/aboutfeeds/blob/main/tools/pretty-feed-v3.xsl
|
||||||
/** Basic stylesheet for RSS feeds */
|
/** Basic stylesheet for RSS feeds */
|
||||||
export const PRETTY_FEED_V3 = `<?xml version="1.0" encoding="utf-8"?>
|
export const PRETTY_FEED_V3 = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
@ -235,3 +239,4 @@ This file is in BETA. Please test and contribute to the discussion:
|
||||||
</html>
|
</html>
|
||||||
</xsl:template>
|
</xsl:template>
|
||||||
</xsl:stylesheet>`;
|
</xsl:stylesheet>`;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import * as path from 'path';
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
import { Plugin as VitePlugin } from 'vite';
|
import { Plugin as VitePlugin } from 'vite';
|
||||||
import { isCSSRequest } from '../core/render/dev/css.js';
|
import { isCSSRequest } from '../core/render/dev/css.js';
|
||||||
|
import { getPageDatasByChunk } from '../core/build/internal.js';
|
||||||
|
|
||||||
const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css';
|
const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css';
|
||||||
|
|
||||||
|
@ -137,12 +138,8 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
||||||
internals.chunkToReferenceIdMap.set(chunk.fileName, referenceId);
|
internals.chunkToReferenceIdMap.set(chunk.fileName, referenceId);
|
||||||
if (chunk.type === 'chunk') {
|
if (chunk.type === 'chunk') {
|
||||||
const fileName = this.getFileName(referenceId);
|
const fileName = this.getFileName(referenceId);
|
||||||
if (chunk.facadeModuleId) {
|
for(const pageData of getPageDatasByChunk(internals, chunk)) {
|
||||||
const facadeId = chunk.facadeModuleId!;
|
pageData.css.add(fileName);
|
||||||
if (!internals.facadeIdToAssetsMap.has(facadeId)) {
|
|
||||||
internals.facadeIdToAssetsMap.set(facadeId, []);
|
|
||||||
}
|
|
||||||
internals.facadeIdToAssetsMap.get(facadeId)!.push(fileName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,22 +158,15 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
||||||
|
|
||||||
for (const [chunkId, chunk] of Object.entries(bundle)) {
|
for (const [chunkId, chunk] of Object.entries(bundle)) {
|
||||||
if (chunk.type === 'chunk') {
|
if (chunk.type === 'chunk') {
|
||||||
// If the chunk has a facadeModuleId it is an entrypoint chunk.
|
|
||||||
// This find shared chunks of CSS and adds them to the main CSS chunks,
|
// This find shared chunks of CSS and adds them to the main CSS chunks,
|
||||||
// so that shared CSS is added to the page.
|
// so that shared CSS is added to the page.
|
||||||
if (chunk.facadeModuleId) {
|
for(const { css: cssSet } of getPageDatasByChunk(internals, chunk)) {
|
||||||
if (!internals.facadeIdToAssetsMap.has(chunk.facadeModuleId)) {
|
|
||||||
internals.facadeIdToAssetsMap.set(chunk.facadeModuleId, []);
|
|
||||||
}
|
|
||||||
const assets = internals.facadeIdToAssetsMap.get(chunk.facadeModuleId)!;
|
|
||||||
const assetSet = new Set(assets);
|
|
||||||
for (const imp of chunk.imports) {
|
for (const imp of chunk.imports) {
|
||||||
if (internals.chunkToReferenceIdMap.has(imp) && !pureChunkFilenames.has(imp)) {
|
if (internals.chunkToReferenceIdMap.has(imp) && !pureChunkFilenames.has(imp)) {
|
||||||
const referenceId = internals.chunkToReferenceIdMap.get(imp)!;
|
const referenceId = internals.chunkToReferenceIdMap.get(imp)!;
|
||||||
const fileName = this.getFileName(referenceId);
|
const fileName = this.getFileName(referenceId);
|
||||||
if (!assetSet.has(fileName)) {
|
if (!cssSet.has(fileName)) {
|
||||||
assetSet.add(fileName);
|
cssSet.add(fileName);
|
||||||
assets.push(fileName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
packages/astro/test/fixtures/ssr-api-route/src/pages/food.json.js
vendored
Normal file
10
packages/astro/test/fixtures/ssr-api-route/src/pages/food.json.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
export function get() {
|
||||||
|
return {
|
||||||
|
body: JSON.stringify([
|
||||||
|
{ name: 'lettuce' },
|
||||||
|
{ name: 'broccoli' },
|
||||||
|
{ name: 'pizza' }
|
||||||
|
])
|
||||||
|
};
|
||||||
|
}
|
6
packages/astro/test/fixtures/ssr-api-route/src/pages/index.astro
vendored
Normal file
6
packages/astro/test/fixtures/ssr-api-route/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<html>
|
||||||
|
<head><title>Testing</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Testing</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
39
packages/astro/test/ssr-api-route.test.js
Normal file
39
packages/astro/test/ssr-api-route.test.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
import testAdapter from './test-adapter.js';
|
||||||
|
|
||||||
|
// Asset bundling
|
||||||
|
describe('API routes in SSR', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
projectRoot: './fixtures/ssr-api-route/',
|
||||||
|
buildOptions: {
|
||||||
|
experimentalSsr: true,
|
||||||
|
},
|
||||||
|
adapter: testAdapter()
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Basic pages work', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/');
|
||||||
|
const response = await app.render(request);
|
||||||
|
const html = await response.text();
|
||||||
|
expect(html).to.not.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can load the API route too', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/food.json');
|
||||||
|
const response = await app.render(request);
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.headers.get('Content-Type')).to.equal('application/json');
|
||||||
|
expect(response.headers.get('Content-Length')).to.not.be.empty;
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.length).to.equal(3);
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,10 +2,10 @@ import { expect } from 'chai';
|
||||||
import { load as cheerioLoad } from 'cheerio';
|
import { load as cheerioLoad } from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture } from './test-utils.js';
|
||||||
import testAdapter from './test-adapter.js';
|
import testAdapter from './test-adapter.js';
|
||||||
import { App } from '../dist/core/app/index.js';
|
|
||||||
|
|
||||||
// Asset bundling
|
// Asset bundling
|
||||||
describe('Dynamic pages in SSR', () => {
|
describe('Dynamic pages in SSR', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
let fixture;
|
let fixture;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
@ -20,8 +20,7 @@ describe('Dynamic pages in SSR', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Do not have to implement getStaticPaths', async () => {
|
it('Do not have to implement getStaticPaths', async () => {
|
||||||
const { createApp } = await import('./fixtures/ssr-dynamic/dist/server/entry.mjs');
|
const app = await fixture.loadTestAdapterApp();
|
||||||
const app = createApp(new URL('./fixtures/ssr-dynamic/dist/server/', import.meta.url));
|
|
||||||
const request = new Request('http://example.com/123');
|
const request = new Request('http://example.com/123');
|
||||||
const response = await app.render(request);
|
const response = await app.render(request);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import cheerio from 'cheerio';
|
import { load as cheerioLoad } from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture } from './test-utils.js';
|
||||||
|
|
||||||
function addLeadingSlash(path) {
|
function addLeadingSlash(path) {
|
||||||
|
@ -23,7 +23,7 @@ describe('Static build', () => {
|
||||||
|
|
||||||
it('can build pages using fetchContent', async () => {
|
it('can build pages using fetchContent', async () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerioLoad(html);
|
||||||
const link = $('.posts a');
|
const link = $('.posts a');
|
||||||
const href = link.attr('href');
|
const href = link.attr('href');
|
||||||
expect(href).to.be.equal('/subpath/posts/thoughts');
|
expect(href).to.be.equal('/subpath/posts/thoughts');
|
||||||
|
@ -69,7 +69,7 @@ describe('Static build', () => {
|
||||||
function createFindEvidence(expected, prefix) {
|
function createFindEvidence(expected, prefix) {
|
||||||
return async function findEvidence(pathname) {
|
return async function findEvidence(pathname) {
|
||||||
const html = await fixture.readFile(pathname);
|
const html = await fixture.readFile(pathname);
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerioLoad(html);
|
||||||
const links = $('link[rel=stylesheet]');
|
const links = $('link[rel=stylesheet]');
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
const href = $(link).attr('href').slice('/subpath'.length);
|
const href = $(link).attr('href').slice('/subpath'.length);
|
||||||
|
@ -118,23 +118,23 @@ describe('Static build', () => {
|
||||||
describe('Hoisted scripts', () => {
|
describe('Hoisted scripts', () => {
|
||||||
it('Get bundled together on the page', async () => {
|
it('Get bundled together on the page', async () => {
|
||||||
const html = await fixture.readFile('/hoisted/index.html');
|
const html = await fixture.readFile('/hoisted/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerioLoad(html);
|
||||||
expect($('script[type="module"]').length).to.equal(1, 'hoisted script added');
|
expect($('script[type="module"]').length).to.equal(1, 'hoisted script added');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Do not get added to the wrong page', async () => {
|
it('Do not get added to the wrong page', async () => {
|
||||||
const hoistedHTML = await fixture.readFile('/hoisted/index.html');
|
const hoistedHTML = await fixture.readFile('/hoisted/index.html');
|
||||||
const $ = cheerio.load(hoistedHTML);
|
const $ = cheerioLoad(hoistedHTML);
|
||||||
const href = $('script[type="module"]').attr('src');
|
const href = $('script[type="module"]').attr('src');
|
||||||
const indexHTML = await fixture.readFile('/index.html');
|
const indexHTML = await fixture.readFile('/index.html');
|
||||||
const $$ = cheerio.load(indexHTML);
|
const $$ = cheerioLoad(indexHTML);
|
||||||
expect($$(`script[src="${href}"]`).length).to.equal(0, 'no script added to different page');
|
expect($$(`script[src="${href}"]`).length).to.equal(0, 'no script added to different page');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('honors ssr config', async () => {
|
it('honors ssr config', async () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerioLoad(html);
|
||||||
expect($('#ssr-config').text()).to.equal('testing');
|
expect($('#ssr-config').text()).to.equal('testing');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,8 +22,8 @@ export default function () {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
load(id) {
|
load(id) {
|
||||||
if (id === '@my-ssr') {
|
if(id === '@my-ssr') {
|
||||||
return `import { App } from 'astro/app';export function createExports(manifest) { return { manifest, createApp: (root) => new App(manifest, root) }; }`;
|
return `import { App } from 'astro/app';export function createExports(manifest) { return { manifest, createApp: () => new App(manifest) }; }`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,7 @@ polyfill(globalThis, {
|
||||||
* @typedef {import('../src/core/dev/index').DevServer} DevServer
|
* @typedef {import('../src/core/dev/index').DevServer} DevServer
|
||||||
* @typedef {import('../src/@types/astro').AstroConfig} AstroConfig
|
* @typedef {import('../src/@types/astro').AstroConfig} AstroConfig
|
||||||
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
|
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
|
||||||
|
* @typedef {import('../src/core/app/index').App} App
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @typedef {Object} Fixture
|
* @typedef {Object} Fixture
|
||||||
|
@ -30,6 +31,7 @@ polyfill(globalThis, {
|
||||||
* @property {() => Promise<DevServer>} startDevServer
|
* @property {() => Promise<DevServer>} startDevServer
|
||||||
* @property {() => Promise<PreviewServer>} preview
|
* @property {() => Promise<PreviewServer>} preview
|
||||||
* @property {() => Promise<void>} clean
|
* @property {() => Promise<void>} clean
|
||||||
|
* @property {() => Promise<App>} loadTestAdapterApp
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,6 +87,11 @@ export async function loadFixture(inlineConfig) {
|
||||||
readFile: (filePath) => fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.dist), 'utf8'),
|
readFile: (filePath) => fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.dist), 'utf8'),
|
||||||
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.dist)),
|
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.dist)),
|
||||||
clean: () => fs.promises.rm(config.dist, { maxRetries: 10, recursive: true, force: true }),
|
clean: () => fs.promises.rm(config.dist, { maxRetries: 10, recursive: true, force: true }),
|
||||||
|
loadTestAdapterApp: async () => {
|
||||||
|
const url = new URL('./server/entry.mjs', config.dist);
|
||||||
|
const {createApp} = await import(url);
|
||||||
|
return createApp();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { SSRManifest } from 'astro';
|
import type { SSRManifest } from 'astro';
|
||||||
import type { IncomingMessage, ServerResponse } from 'http';
|
import type { IncomingMessage, ServerResponse } from 'http';
|
||||||
|
import type { Readable } from 'stream';
|
||||||
import { NodeApp } from 'astro/app/node';
|
import { NodeApp } from 'astro/app/node';
|
||||||
import { polyfill } from '@astrojs/webapi';
|
import { polyfill } from '@astrojs/webapi';
|
||||||
|
|
||||||
|
@ -8,7 +9,7 @@ polyfill(globalThis, {
|
||||||
});
|
});
|
||||||
|
|
||||||
export function createExports(manifest: SSRManifest) {
|
export function createExports(manifest: SSRManifest) {
|
||||||
const app = new NodeApp(manifest, new URL(import.meta.url));
|
const app = new NodeApp(manifest);
|
||||||
return {
|
return {
|
||||||
async handler(req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) {
|
async handler(req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) {
|
||||||
const route = app.match(req);
|
const route = app.match(req);
|
||||||
|
@ -35,13 +36,8 @@ async function writeWebResponse(res: ServerResponse, webResponse: Response) {
|
||||||
const { status, headers, body } = webResponse;
|
const { status, headers, body } = webResponse;
|
||||||
res.writeHead(status, Object.fromEntries(headers.entries()));
|
res.writeHead(status, Object.fromEntries(headers.entries()));
|
||||||
if (body) {
|
if (body) {
|
||||||
const reader = body.getReader();
|
for await(const chunk of (body as unknown as Readable)) {
|
||||||
while (true) {
|
res.write(chunk);
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
if (value) {
|
|
||||||
res.write(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.end();
|
res.end();
|
||||||
|
|
Loading…
Reference in a new issue