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 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<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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<ComponentPath, ComponentInstance>;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
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 { StaticBuildOptions } from './types';
|
||||
import npath from 'path';
|
||||
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 { 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<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 = [];
|
||||
|
@ -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 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<PageBuildData>(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<string[]>(facadeId, internals.facadeIdToAssetsMap) || [];
|
||||
const hoistedId = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap) || null;
|
||||
|
||||
let compiledModule = await import(url.toString());
|
||||
|
||||
const generationOptions: Readonly<GeneratePathOptions> = {
|
||||
pageData,
|
||||
internals,
|
||||
linkIds,
|
||||
hoistedId,
|
||||
mod: compiledModule,
|
||||
mod: pageModule,
|
||||
renderers,
|
||||
};
|
||||
|
||||
|
|
|
@ -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<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>>;
|
||||
facadeIdToHoistedEntryMap: Map<string, string>;
|
||||
|
||||
// A mapping of specifiers like astro/client/idle.js to the hashed bundled name.
|
||||
// Used to render pages with the correct specifiers.
|
||||
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.
|
||||
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
|
||||
const hoistedScriptIdToHoistedMap = new Map<string, Set<string>>();
|
||||
const facadeIdToHoistedEntryMap = new Map<string, string>();
|
||||
|
||||
return {
|
||||
pureCSSChunks,
|
||||
chunkToReferenceIdMap,
|
||||
astroStyleMap,
|
||||
astroPageStyleMap,
|
||||
facadeIdToAssetsMap,
|
||||
hoistedScriptIdToHoistedMap,
|
||||
facadeIdToHoistedEntryMap,
|
||||
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);
|
||||
}, 10000);
|
||||
allPages[route.component] = {
|
||||
component: route.component,
|
||||
route,
|
||||
paths: [route.pathname],
|
||||
moduleSpecifier: '',
|
||||
css: new Set(),
|
||||
hoistedScript: undefined,
|
||||
scripts: new Set(),
|
||||
preload: await ssrPreload({
|
||||
astroConfig,
|
||||
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);
|
||||
allPages[route.component] = {
|
||||
component: route.component,
|
||||
route,
|
||||
paths: finalPaths,
|
||||
moduleSpecifier: '',
|
||||
css: new Set(),
|
||||
hoistedScript: undefined,
|
||||
scripts: new Set(),
|
||||
preload: await ssrPreload({
|
||||
astroConfig,
|
||||
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 { vitePluginInternals } from './vite-plugin-internals.js';
|
||||
import { vitePluginSSR } from './vite-plugin-ssr.js';
|
||||
import { vitePluginPages } from './vite-plugin-pages.js';
|
||||
import { generatePages } from './generate.js';
|
||||
import { trackPageData } from './internal.js';
|
||||
import { getClientRoot, getServerRoot, getOutRoot } from './common.js';
|
||||
import { isBuildingToSSR } from '../util.js';
|
||||
import { getTimeStat } from './util.js';
|
||||
|
||||
export async function staticBuild(opts: StaticBuildOptions) {
|
||||
|
@ -45,6 +48,9 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
|||
const astroModuleURL = new URL('./' + component, astroConfig.projectRoot);
|
||||
const astroModuleId = prependForwardSlash(component);
|
||||
|
||||
// Track the page data in internals
|
||||
trackPageData(internals, component, pageData, astroModuleId, astroModuleURL);
|
||||
|
||||
if (pageData.route.type === 'page') {
|
||||
const [renderers, mod] = pageData.preload;
|
||||
const metadata = mod.$$metadata;
|
||||
|
@ -96,7 +102,6 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
|||
|
||||
timer.generate = performance.now();
|
||||
if (opts.buildConfig.staticMode) {
|
||||
console.log('huh?');
|
||||
await generatePages(ssrResult, opts, internals, facadeIdToPageDataMap);
|
||||
await cleanSsrOutput(opts);
|
||||
} else {
|
||||
|
@ -122,10 +127,10 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
|||
outDir: fileURLToPath(out),
|
||||
ssr: true,
|
||||
rollupOptions: {
|
||||
input: Array.from(input),
|
||||
input: [],
|
||||
output: {
|
||||
format: 'esm',
|
||||
entryFileNames: 'entry.[hash].mjs',
|
||||
entryFileNames: 'entry.mjs',
|
||||
chunkFileNames: 'chunks/chunk.[hash].mjs',
|
||||
assetFileNames: 'assets/asset.[hash][extname]',
|
||||
},
|
||||
|
@ -139,12 +144,14 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
|||
},
|
||||
plugins: [
|
||||
vitePluginInternals(input, internals),
|
||||
vitePluginPages(opts, internals),
|
||||
rollupPluginAstroBuildCSS({
|
||||
internals,
|
||||
}),
|
||||
...(viteConfig.plugins || []),
|
||||
// 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,
|
||||
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 { 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 { LogOptions } from '../../logger';
|
||||
import type { RouteCache } from '../../render/route-cache.js';
|
||||
|
||||
export type ComponentPath = string;
|
||||
export type ViteID = string;
|
||||
|
||||
export interface PageBuildData {
|
||||
component: ComponentPath;
|
||||
paths: string[];
|
||||
preload: ComponentPreload;
|
||||
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 */
|
||||
export interface StaticBuildOptions {
|
||||
|
@ -23,3 +31,8 @@ export interface StaticBuildOptions {
|
|||
routeCache: RouteCache;
|
||||
viteConfig: ViteConfigWithSSR;
|
||||
}
|
||||
|
||||
export interface SingleFileBuiltModule {
|
||||
pageMap: Map<ComponentPath, ComponentInstance>;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { AstroConfig } from '../../@types/astro';
|
||||
import type { Plugin as VitePlugin } from 'vite';
|
||||
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) {
|
||||
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)) {
|
||||
const facadeId = output.facadeModuleId!;
|
||||
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 { SerializedRouteInfo, SerializedSSRManifest } from '../app/types';
|
||||
|
||||
import { chunkIsPage, rootRelativeFacadeId, getByFacadeId } from './generate.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';
|
||||
|
||||
const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
||||
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
|
||||
|
||||
|
@ -17,13 +19,7 @@ export function vitePluginSSR(buildOpts: StaticBuildOptions, internals: BuildInt
|
|||
return {
|
||||
name: '@astrojs/vite-plugin-astro-ssr',
|
||||
options(opts) {
|
||||
if (Array.isArray(opts.input)) {
|
||||
opts.input.push(virtualModuleId);
|
||||
} else {
|
||||
return {
|
||||
input: [virtualModuleId],
|
||||
};
|
||||
}
|
||||
return addRollupInput(opts, [virtualModuleId]);
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id === virtualModuleId) {
|
||||
|
@ -33,8 +29,12 @@ export function vitePluginSSR(buildOpts: StaticBuildOptions, internals: BuildInt
|
|||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
return `import * as adapter from '${adapter.serverEntrypoint}';
|
||||
import * as _main from '${pagesVirtualModuleId}';
|
||||
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
|
||||
|
@ -52,57 +52,38 @@ if(_start in adapter) {
|
|||
},
|
||||
|
||||
generateBundle(opts, bundle) {
|
||||
const manifest = buildManifest(bundle, buildOpts, internals);
|
||||
const manifest = buildManifest(buildOpts, internals);
|
||||
|
||||
for (const [_chunkName, chunk] of Object.entries(bundle)) {
|
||||
if (chunk.type === 'asset') continue;
|
||||
if (chunk.modules[resolvedVirtualModuleId]) {
|
||||
for(const [_chunkName, chunk] of Object.entries(bundle)) {
|
||||
if(chunk.type === 'asset') continue;
|
||||
if(chunk.modules[resolvedVirtualModuleId]) {
|
||||
const exp = new RegExp(`['"]${manifestReplace}['"]`);
|
||||
const code = chunk.code;
|
||||
chunk.code = code.replace(exp, () => {
|
||||
return JSON.stringify(manifest);
|
||||
});
|
||||
chunk.fileName = 'entry.mjs';
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildManifest(bundle: OutputBundle, opts: StaticBuildOptions, internals: BuildInternals): SerializedSSRManifest {
|
||||
const { astroConfig, manifest } = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
function buildManifest(opts: StaticBuildOptions, internals: BuildInternals): SerializedSSRManifest {
|
||||
const { astroConfig } = opts;
|
||||
|
||||
const routes: SerializedRouteInfo[] = [];
|
||||
|
||||
for (const routeData of manifest.routes) {
|
||||
const componentPath = routeData.component;
|
||||
|
||||
if (!rootRelativeIdToChunkMap.has(componentPath)) {
|
||||
throw new Error('Unable to find chunk for ' + componentPath);
|
||||
for(const pageData of eachPageData(internals)) {
|
||||
const scripts = Array.from(pageData.scripts);
|
||||
if(pageData.hoistedScript) {
|
||||
scripts.unshift(pageData.hoistedScript);
|
||||
}
|
||||
|
||||
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({
|
||||
file: chunk.fileName,
|
||||
links,
|
||||
file: '',
|
||||
links: Array.from(pageData.css),
|
||||
scripts,
|
||||
routeData: serializeRouteData(routeData),
|
||||
routeData: serializeRouteData(pageData.route),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -116,7 +97,8 @@ function buildManifest(bundle: OutputBundle, opts: StaticBuildOptions, internals
|
|||
markdown: {
|
||||
render: astroConfig.markdownOptions.render,
|
||||
},
|
||||
renderers: astroConfig._ctx.renderers,
|
||||
pageMap: null as any,
|
||||
renderers: [],
|
||||
entryModules,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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 {
|
||||
type: 'page',
|
||||
type,
|
||||
pattern,
|
||||
params,
|
||||
component,
|
||||
|
@ -20,7 +20,7 @@ export function serializeRouteData(routeData: RouteData): SerializedRouteData {
|
|||
}
|
||||
|
||||
export function deserializeRouteData(rawRouteData: SerializedRouteData) {
|
||||
const { component, params, pathname } = rawRouteData;
|
||||
const { component, params, pathname, type } = rawRouteData;
|
||||
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
|
||||
/** Basic stylesheet for RSS feeds */
|
||||
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>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>`;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as path from 'path';
|
|||
import esbuild from 'esbuild';
|
||||
import { Plugin as VitePlugin } from 'vite';
|
||||
import { isCSSRequest } from '../core/render/dev/css.js';
|
||||
import { getPageDatasByChunk } from '../core/build/internal.js';
|
||||
|
||||
const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css';
|
||||
|
||||
|
@ -137,12 +138,8 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
|||
internals.chunkToReferenceIdMap.set(chunk.fileName, referenceId);
|
||||
if (chunk.type === 'chunk') {
|
||||
const fileName = this.getFileName(referenceId);
|
||||
if (chunk.facadeModuleId) {
|
||||
const facadeId = chunk.facadeModuleId!;
|
||||
if (!internals.facadeIdToAssetsMap.has(facadeId)) {
|
||||
internals.facadeIdToAssetsMap.set(facadeId, []);
|
||||
}
|
||||
internals.facadeIdToAssetsMap.get(facadeId)!.push(fileName);
|
||||
for(const pageData of getPageDatasByChunk(internals, chunk)) {
|
||||
pageData.css.add(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,22 +158,15 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
|||
|
||||
for (const [chunkId, chunk] of Object.entries(bundle)) {
|
||||
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,
|
||||
// so that shared CSS is added to the page.
|
||||
if (chunk.facadeModuleId) {
|
||||
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 { css: cssSet } of getPageDatasByChunk(internals, chunk)) {
|
||||
for (const imp of chunk.imports) {
|
||||
if (internals.chunkToReferenceIdMap.has(imp) && !pureChunkFilenames.has(imp)) {
|
||||
const referenceId = internals.chunkToReferenceIdMap.get(imp)!;
|
||||
const fileName = this.getFileName(referenceId);
|
||||
if (!assetSet.has(fileName)) {
|
||||
assetSet.add(fileName);
|
||||
assets.push(fileName);
|
||||
if (!cssSet.has(fileName)) {
|
||||
cssSet.add(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 { loadFixture } from './test-utils.js';
|
||||
import testAdapter from './test-adapter.js';
|
||||
import { App } from '../dist/core/app/index.js';
|
||||
|
||||
// Asset bundling
|
||||
describe('Dynamic pages in SSR', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
|
@ -20,8 +20,7 @@ describe('Dynamic pages in SSR', () => {
|
|||
});
|
||||
|
||||
it('Do not have to implement getStaticPaths', async () => {
|
||||
const { createApp } = await import('./fixtures/ssr-dynamic/dist/server/entry.mjs');
|
||||
const app = createApp(new URL('./fixtures/ssr-dynamic/dist/server/', import.meta.url));
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/123');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { expect } from 'chai';
|
||||
import cheerio from 'cheerio';
|
||||
import { load as cheerioLoad } from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
function addLeadingSlash(path) {
|
||||
|
@ -23,7 +23,7 @@ describe('Static build', () => {
|
|||
|
||||
it('can build pages using fetchContent', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const $ = cheerioLoad(html);
|
||||
const link = $('.posts a');
|
||||
const href = link.attr('href');
|
||||
expect(href).to.be.equal('/subpath/posts/thoughts');
|
||||
|
@ -69,7 +69,7 @@ describe('Static build', () => {
|
|||
function createFindEvidence(expected, prefix) {
|
||||
return async function findEvidence(pathname) {
|
||||
const html = await fixture.readFile(pathname);
|
||||
const $ = cheerio.load(html);
|
||||
const $ = cheerioLoad(html);
|
||||
const links = $('link[rel=stylesheet]');
|
||||
for (const link of links) {
|
||||
const href = $(link).attr('href').slice('/subpath'.length);
|
||||
|
@ -118,23 +118,23 @@ describe('Static build', () => {
|
|||
describe('Hoisted scripts', () => {
|
||||
it('Get bundled together on the page', async () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('Do not get added to the wrong page', async () => {
|
||||
const hoistedHTML = await fixture.readFile('/hoisted/index.html');
|
||||
const $ = cheerio.load(hoistedHTML);
|
||||
const $ = cheerioLoad(hoistedHTML);
|
||||
const href = $('script[type="module"]').attr('src');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
it('honors ssr config', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const $ = cheerioLoad(html);
|
||||
expect($('#ssr-config').text()).to.equal('testing');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,8 +22,8 @@ export default function () {
|
|||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === '@my-ssr') {
|
||||
return `import { App } from 'astro/app';export function createExports(manifest) { return { manifest, createApp: (root) => new App(manifest, root) }; }`;
|
||||
if(id === '@my-ssr') {
|
||||
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/@types/astro').AstroConfig} AstroConfig
|
||||
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
|
||||
* @typedef {import('../src/core/app/index').App} App
|
||||
*
|
||||
*
|
||||
* @typedef {Object} Fixture
|
||||
|
@ -30,6 +31,7 @@ polyfill(globalThis, {
|
|||
* @property {() => Promise<DevServer>} startDevServer
|
||||
* @property {() => Promise<PreviewServer>} preview
|
||||
* @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'),
|
||||
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.dist)),
|
||||
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 { IncomingMessage, ServerResponse } from 'http';
|
||||
import type { Readable } from 'stream';
|
||||
import { NodeApp } from 'astro/app/node';
|
||||
import { polyfill } from '@astrojs/webapi';
|
||||
|
||||
|
@ -8,7 +9,7 @@ polyfill(globalThis, {
|
|||
});
|
||||
|
||||
export function createExports(manifest: SSRManifest) {
|
||||
const app = new NodeApp(manifest, new URL(import.meta.url));
|
||||
const app = new NodeApp(manifest);
|
||||
return {
|
||||
async handler(req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) {
|
||||
const route = app.match(req);
|
||||
|
@ -35,13 +36,8 @@ async function writeWebResponse(res: ServerResponse, webResponse: Response) {
|
|||
const { status, headers, body } = webResponse;
|
||||
res.writeHead(status, Object.fromEntries(headers.entries()));
|
||||
if (body) {
|
||||
const reader = body.getReader();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
if (value) {
|
||||
res.write(value);
|
||||
}
|
||||
for await(const chunk of (body as unknown as Readable)) {
|
||||
res.write(chunk);
|
||||
}
|
||||
}
|
||||
res.end();
|
||||
|
|
Loading…
Reference in a new issue