mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
parent
251ab8bb12
commit
56f2274907
9 changed files with 255 additions and 84 deletions
|
@ -35,24 +35,14 @@ import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js
|
|||
import type { SSRManifestI18n } from '../app/types.js';
|
||||
import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import { routeIsFallback } from '../redirects/helpers.js';
|
||||
import {
|
||||
RedirectSinglePageBuiltModule,
|
||||
getRedirectLocationOrThrow,
|
||||
routeIsRedirect,
|
||||
} from '../redirects/index.js';
|
||||
import { getRedirectLocationOrThrow, routeIsRedirect } from '../redirects/index.js';
|
||||
import { RenderContext } from '../render-context.js';
|
||||
import { callGetStaticPaths } from '../render/route-cache.js';
|
||||
import { createRequest } from '../request.js';
|
||||
import { matchRoute } from '../routing/match.js';
|
||||
import { getOutputFilename } from '../util.js';
|
||||
import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
|
||||
import {
|
||||
cssOrder,
|
||||
getEntryFilePathFromComponentPath,
|
||||
getPageDataByComponent,
|
||||
mergeInlineCss,
|
||||
} from './internal.js';
|
||||
import { cssOrder, getPageDataByComponent, mergeInlineCss } from './internal.js';
|
||||
import { BuildPipeline } from './pipeline.js';
|
||||
import type {
|
||||
PageBuildData,
|
||||
|
@ -66,46 +56,6 @@ function createEntryURL(filePath: string, outFolder: URL) {
|
|||
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
||||
}
|
||||
|
||||
async function getEntryForRedirectRoute(
|
||||
route: RouteData,
|
||||
internals: BuildInternals,
|
||||
outFolder: URL
|
||||
): Promise<SinglePageBuiltModule> {
|
||||
if (route.type !== 'redirect') {
|
||||
throw new Error(`Expected a redirect route.`);
|
||||
}
|
||||
if (route.redirectRoute) {
|
||||
const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
|
||||
if (filePath) {
|
||||
const url = createEntryURL(filePath, outFolder);
|
||||
const ssrEntryPage: SinglePageBuiltModule = await import(url.toString());
|
||||
return ssrEntryPage;
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectSinglePageBuiltModule;
|
||||
}
|
||||
|
||||
async function getEntryForFallbackRoute(
|
||||
route: RouteData,
|
||||
internals: BuildInternals,
|
||||
outFolder: URL
|
||||
): Promise<SinglePageBuiltModule> {
|
||||
if (route.type !== 'fallback') {
|
||||
throw new Error(`Expected a redirect route.`);
|
||||
}
|
||||
if (route.redirectRoute) {
|
||||
const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
|
||||
if (filePath) {
|
||||
const url = createEntryURL(filePath, outFolder);
|
||||
const ssrEntryPage: SinglePageBuiltModule = await import(url.toString());
|
||||
return ssrEntryPage;
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectSinglePageBuiltModule;
|
||||
}
|
||||
|
||||
// Gives back a facadeId that is relative to the root.
|
||||
// ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro
|
||||
export function rootRelativeFacadeId(facadeId: string, settings: AstroSettings): string {
|
||||
|
@ -185,14 +135,15 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
|
|||
});
|
||||
}
|
||||
|
||||
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
|
||||
const ssrEntryPage = await import(ssrEntryURLPage.toString());
|
||||
const ssrEntryPage = await pipeline.retrieveSsrEntry(pageData.route, filePath);
|
||||
if (options.settings.adapter?.adapterFeatures?.functionPerRoute) {
|
||||
// forcing to use undefined, so we fail in an expected way if the module is not even there.
|
||||
// @ts-expect-error When building for `functionPerRoute`, the module exports a `pageModule` function instead
|
||||
const ssrEntry = ssrEntryPage?.pageModule;
|
||||
if (ssrEntry) {
|
||||
await generatePage(pageData, ssrEntry, builtPaths, pipeline);
|
||||
} else {
|
||||
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
|
||||
throw new Error(
|
||||
`Unable to find the manifest for the module ${ssrEntryURLPage.toString()}. This is unexpected and likely a bug in Astro, please report.`
|
||||
);
|
||||
|
@ -205,18 +156,8 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
|
|||
}
|
||||
} else {
|
||||
for (const [pageData, filePath] of pagesToGenerate) {
|
||||
if (routeIsRedirect(pageData.route)) {
|
||||
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
|
||||
await generatePage(pageData, entry, builtPaths, pipeline);
|
||||
} else if (routeIsFallback(pageData.route)) {
|
||||
const entry = await getEntryForFallbackRoute(pageData.route, internals, outFolder);
|
||||
await generatePage(pageData, entry, builtPaths, pipeline);
|
||||
} else {
|
||||
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
|
||||
const entry: SinglePageBuiltModule = await import(ssrEntryURLPage.toString());
|
||||
|
||||
await generatePage(pageData, entry, builtPaths, pipeline);
|
||||
}
|
||||
const entry = await pipeline.retrieveSsrEntry(pageData.route, filePath);
|
||||
await generatePage(pageData, entry, builtPaths, pipeline);
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
|
@ -232,12 +173,12 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
|
|||
.map((x) => x.transforms.size)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const cpuCount = os.cpus().length;
|
||||
const assetsCreationpipeline = await prepareAssetsGenerationEnv(pipeline, totalCount);
|
||||
const assetsCreationPipeline = await prepareAssetsGenerationEnv(pipeline, totalCount);
|
||||
const queue = new PQueue({ concurrency: Math.max(cpuCount, 1) });
|
||||
|
||||
const assetsTimer = performance.now();
|
||||
for (const [originalPath, transforms] of staticImageList) {
|
||||
await generateImagesForPath(originalPath, transforms, assetsCreationpipeline, queue);
|
||||
await generateImagesForPath(originalPath, transforms, assetsCreationPipeline, queue);
|
||||
}
|
||||
|
||||
await queue.onIdle();
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import type {
|
||||
ComponentInstance,
|
||||
ReroutePayload,
|
||||
RouteData,
|
||||
SSRLoadedRenderer,
|
||||
SSRResult,
|
||||
MiddlewareHandler,
|
||||
ReroutePayload,
|
||||
ComponentInstance,
|
||||
} from '../../@types/astro.js';
|
||||
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
|
||||
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||
|
@ -19,22 +18,42 @@ import {
|
|||
import {
|
||||
type BuildInternals,
|
||||
cssOrder,
|
||||
getEntryFilePathFromComponentPath,
|
||||
getPageDataByComponent,
|
||||
mergeInlineCss,
|
||||
} from './internal.js';
|
||||
import { ASTRO_PAGE_MODULE_ID, ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
|
||||
import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js';
|
||||
import { getVirtualModulePageNameFromPath } from './plugins/util.js';
|
||||
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
|
||||
import type { PageBuildData, StaticBuildOptions } from './types.js';
|
||||
import {
|
||||
ASTRO_PAGE_EXTENSION_POST_PATTERN,
|
||||
getVirtualModulePageNameFromPath,
|
||||
} from './plugins/util.js';
|
||||
import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js';
|
||||
import { i18nHasFallback } from './util.js';
|
||||
import { defineMiddleware } from '../middleware/index.js';
|
||||
import { undefined } from 'zod';
|
||||
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
|
||||
import { getOutDirWithinCwd } from './common.js';
|
||||
|
||||
/**
|
||||
* The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files.
|
||||
*/
|
||||
export class BuildPipeline extends Pipeline {
|
||||
#componentsInterner: WeakMap<RouteData, SinglePageBuiltModule> = new WeakMap<
|
||||
RouteData,
|
||||
SinglePageBuiltModule
|
||||
>();
|
||||
/**
|
||||
* This cache is needed to map a single `RouteData` to its file path.
|
||||
* @private
|
||||
*/
|
||||
#routesByFilePath: WeakMap<RouteData, string> = new WeakMap<RouteData, string>();
|
||||
|
||||
get outFolder() {
|
||||
const ssr = isServerLikeOutput(this.settings.config);
|
||||
return ssr
|
||||
? this.settings.config.build.server
|
||||
: getOutDirWithinCwd(this.settings.config.outDir);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
readonly internals: BuildInternals,
|
||||
readonly manifest: SSRManifest,
|
||||
|
@ -233,14 +252,115 @@ export class BuildPipeline extends Pipeline {
|
|||
}
|
||||
}
|
||||
|
||||
for (const [buildData, filePath] of pages.entries()) {
|
||||
this.#routesByFilePath.set(buildData.route, filePath);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
getComponentByRoute(_routeData: RouteData): Promise<ComponentInstance> {
|
||||
throw new Error('unimplemented');
|
||||
async getComponentByRoute(routeData: RouteData): Promise<ComponentInstance> {
|
||||
if (this.#componentsInterner.has(routeData)) {
|
||||
// SAFETY: checked before
|
||||
const entry = this.#componentsInterner.get(routeData)!;
|
||||
return await entry.page();
|
||||
} else {
|
||||
// SAFETY: the pipeline calls `retrieveRoutesToGenerate`, which is in charge to fill the cache.
|
||||
const filePath = this.#routesByFilePath.get(routeData)!;
|
||||
const module = await this.retrieveSsrEntry(routeData, filePath);
|
||||
return module.page();
|
||||
}
|
||||
}
|
||||
|
||||
tryReroute(_reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]> {
|
||||
throw new Error('unimplemented');
|
||||
async tryReroute(payload: ReroutePayload): Promise<[RouteData, ComponentInstance]> {
|
||||
let foundRoute: RouteData | undefined;
|
||||
// options.manifest is the actual type that contains the information
|
||||
for (const route of this.options.manifest.routes) {
|
||||
if (payload instanceof URL) {
|
||||
if (route.pattern.test(payload.pathname)) {
|
||||
foundRoute = route;
|
||||
break;
|
||||
}
|
||||
} else if (payload instanceof Request) {
|
||||
const url = new URL(payload.url);
|
||||
if (route.pattern.test(url.pathname)) {
|
||||
foundRoute = route;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (route.pattern.test(decodeURI(payload))) {
|
||||
foundRoute = route;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (foundRoute) {
|
||||
const componentInstance = await this.getComponentByRoute(foundRoute);
|
||||
return [foundRoute, componentInstance];
|
||||
} else {
|
||||
throw new Error('Route not found');
|
||||
}
|
||||
}
|
||||
|
||||
async retrieveSsrEntry(route: RouteData, filePath: string): Promise<SinglePageBuiltModule> {
|
||||
if (this.#componentsInterner.has(route)) {
|
||||
// SAFETY: it is checked inside the if
|
||||
return this.#componentsInterner.get(route)!;
|
||||
}
|
||||
let entry;
|
||||
if (routeIsRedirect(route)) {
|
||||
entry = await this.#getEntryForRedirectRoute(route, this.internals, this.outFolder);
|
||||
} else if (routeIsFallback(route)) {
|
||||
entry = await this.#getEntryForFallbackRoute(route, this.internals, this.outFolder);
|
||||
} else {
|
||||
const ssrEntryURLPage = createEntryURL(filePath, this.outFolder);
|
||||
entry = await import(ssrEntryURLPage.toString());
|
||||
}
|
||||
this.#componentsInterner.set(route, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
async #getEntryForFallbackRoute(
|
||||
route: RouteData,
|
||||
internals: BuildInternals,
|
||||
outFolder: URL
|
||||
): Promise<SinglePageBuiltModule> {
|
||||
if (route.type !== 'fallback') {
|
||||
throw new Error(`Expected a redirect route.`);
|
||||
}
|
||||
if (route.redirectRoute) {
|
||||
const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
|
||||
if (filePath) {
|
||||
const url = createEntryURL(filePath, outFolder);
|
||||
const ssrEntryPage: SinglePageBuiltModule = await import(url.toString());
|
||||
return ssrEntryPage;
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectSinglePageBuiltModule;
|
||||
}
|
||||
|
||||
async #getEntryForRedirectRoute(
|
||||
route: RouteData,
|
||||
internals: BuildInternals,
|
||||
outFolder: URL
|
||||
): Promise<SinglePageBuiltModule> {
|
||||
if (route.type !== 'redirect') {
|
||||
throw new Error(`Expected a redirect route.`);
|
||||
}
|
||||
if (route.redirectRoute) {
|
||||
const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
|
||||
if (filePath) {
|
||||
const url = createEntryURL(filePath, outFolder);
|
||||
const ssrEntryPage: SinglePageBuiltModule = await import(url.toString());
|
||||
return ssrEntryPage;
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectSinglePageBuiltModule;
|
||||
}
|
||||
}
|
||||
|
||||
function createEntryURL(filePath: string, outFolder: URL) {
|
||||
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
||||
}
|
||||
|
|
|
@ -195,6 +195,7 @@ export class RenderContext {
|
|||
new Response(null, { status, headers: { Location: path } });
|
||||
|
||||
const reroute = async (reroutePayload: ReroutePayload) => {
|
||||
pipeline.logger.debug('router', 'Called rerouting to:', reroutePayload);
|
||||
try {
|
||||
const [routeData, component] = await pipeline.tryReroute(reroutePayload);
|
||||
this.routeData = routeData;
|
||||
|
@ -212,6 +213,7 @@ export class RenderContext {
|
|||
this.isRerouting = true;
|
||||
return await this.render(component);
|
||||
} catch (e) {
|
||||
pipeline.logger.debug('router', 'Routing failed.', e);
|
||||
return new Response('Not found', {
|
||||
status: 404,
|
||||
statusText: 'Not found',
|
||||
|
@ -381,6 +383,7 @@ export class RenderContext {
|
|||
|
||||
const reroute = async (reroutePayload: ReroutePayload) => {
|
||||
try {
|
||||
pipeline.logger.debug('router', 'Calling rerouting: ', reroutePayload);
|
||||
const [routeData, component] = await pipeline.tryReroute(reroutePayload);
|
||||
this.routeData = routeData;
|
||||
if (reroutePayload instanceof Request) {
|
||||
|
@ -397,6 +400,7 @@ export class RenderContext {
|
|||
this.isRerouting = true;
|
||||
return await this.render(component);
|
||||
} catch (e) {
|
||||
pipeline.logger.debug('router', 'Rerouting failed, returning a 404.', e);
|
||||
return new Response('Not found', {
|
||||
status: 404,
|
||||
statusText: 'Not found',
|
||||
|
|
|
@ -203,7 +203,11 @@ export class DevPipeline extends Pipeline {
|
|||
break;
|
||||
}
|
||||
} else if (payload instanceof Request) {
|
||||
// TODO: handle request, if needed
|
||||
const url = new URL(payload.url);
|
||||
if (route.pattern.test(url.pathname)) {
|
||||
foundRoute = route;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (route.pattern.test(decodeURI(payload))) {
|
||||
foundRoute = route;
|
||||
|
|
|
@ -222,6 +222,7 @@ export async function handleRoute({
|
|||
fallbackRoutes: [],
|
||||
isIndex: false,
|
||||
};
|
||||
|
||||
renderContext = RenderContext.create({
|
||||
pipeline: pipeline,
|
||||
pathname,
|
||||
|
|
21
packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro
vendored
Normal file
21
packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{ params: { id: 'hello' } },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
return Astro.reroute("/")
|
||||
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Dynamic [id].astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>/dynamic/[id].astro</h1>
|
||||
</body>
|
||||
</html>
|
20
packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro
vendored
Normal file
20
packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{ params: { id: 'hello' } },
|
||||
];
|
||||
}
|
||||
|
||||
return Astro.reroute("/")
|
||||
|
||||
---
|
||||
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Spread [...id].astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>/spread/[...id].astro</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -58,8 +58,6 @@ describe('Dev server manual routing', () => {
|
|||
describe('SSG manual routing', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
|
|
|
@ -34,7 +34,69 @@ describe('Dev reroute', () => {
|
|||
});
|
||||
|
||||
it('the render the index page when navigating /blog/salut ', async () => {
|
||||
const html = await fixture.fetch('/blog/hello').then((res) => res.text());
|
||||
const html = await fixture.fetch('/blog/salut').then((res) => res.text());
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
assert.equal($('h1').text(), 'Index');
|
||||
});
|
||||
|
||||
it('the render the index page when navigating dynamic route /dynamic/[id] ', async () => {
|
||||
const html = await fixture.fetch('/dynamic/hello').then((res) => res.text());
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
assert.equal($('h1').text(), 'Index');
|
||||
});
|
||||
|
||||
it('the render the index page when navigating spread route /spread/[...spread] ', async () => {
|
||||
const html = await fixture.fetch('/spread/hello').then((res) => res.text());
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
assert.equal($('h1').text(), 'Index');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Build reroute', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/reroute/',
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('the render the index page when navigating /reroute ', async () => {
|
||||
const html = await fixture.readFile('/reroute/index.html');
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
assert.equal($('h1').text(), 'Index');
|
||||
});
|
||||
|
||||
it('the render the index page when navigating /blog/hello ', async () => {
|
||||
const html = await fixture.readFile('/blog/hello/index.html');
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
assert.equal($('h1').text(), 'Index');
|
||||
});
|
||||
|
||||
it('the render the index page when navigating /blog/salut ', async () => {
|
||||
const html = await fixture.readFile('/blog/salut/index.html');
|
||||
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
assert.equal($('h1').text(), 'Index');
|
||||
});
|
||||
|
||||
it('the render the index page when navigating dynamic route /dynamic/[id] ', async () => {
|
||||
const html = await fixture.readFile('/dynamic/hello/index.html');
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
assert.equal($('h1').text(), 'Index');
|
||||
});
|
||||
|
||||
it('the render the index page when navigating spread route /spread/[...spread] ', async () => {
|
||||
const html = await fixture.readFile('/spread/hello/index.html');
|
||||
const $ = cheerioLoad(html);
|
||||
|
||||
assert.equal($('h1').text(), 'Index');
|
||||
|
|
Loading…
Reference in a new issue