Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reroute in SSG #10843

Merged
merged 2 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading