Skip to content

Commit

Permalink
feat: reroute in SSG (#10843)
Browse files Browse the repository at this point in the history
* feat: rerouting in ssg

* linting
  • Loading branch information
ematipico authored Apr 22, 2024
1 parent ba505e3 commit 8e32ed8
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 84 deletions.
77 changes: 9 additions & 68 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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.`
);
Expand All @@ -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(
Expand All @@ -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();
Expand Down
144 changes: 132 additions & 12 deletions packages/astro/src/core/build/pipeline.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -232,14 +251,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);
}
4 changes: 4 additions & 0 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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',
Expand Down Expand Up @@ -336,6 +338,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) {
Expand All @@ -352,6 +355,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',
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/src/vite-plugin-astro-server/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/vite-plugin-astro-server/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export async function handleRoute({
fallbackRoutes: [],
isIndex: false,
};

renderContext = RenderContext.create({
pipeline: pipeline,
pathname,
Expand Down
21 changes: 21 additions & 0 deletions packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit 8e32ed8

Please sign in to comment.