Skip to content

Commit

Permalink
feat: reroute for SSR (#10845)
Browse files Browse the repository at this point in the history
* feat: implement reroute in dev (#10818)

* chore: implement reroute in dev

* chore: revert naming change

* chore: conditionally create the new request

* chore: handle error

* remove only

* remove only

* chore: add tests and remove logs

* chore: fix regression

* chore: fix regression route matching

* chore: remove unwanted test

* feat: reroute in SSG (#10843)

* feat: rerouting in ssg

* linting

* feat: rerouting in ssg

* linting

* feat: reroute for SSR

* fix rebase

* fix merge issue
  • Loading branch information
ematipico committed May 7, 2024
1 parent 319bd05 commit 2ee5817
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 65 deletions.
52 changes: 7 additions & 45 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import type {
ComponentInstance,
ManifestData,
RouteData,
SSRManifest,
} from '../../@types/astro.js';
import type { ManifestData, RouteData, SSRManifest } from '../../@types/astro.js';
import { normalizeTheLocale } from '../../i18n/index.js';
import type { SinglePageBuiltModule } from '../build/types.js';
import {
DEFAULT_404_COMPONENT,
REROUTABLE_STATUS_CODES,
REROUTE_DIRECTIVE_HEADER,
clientAddressSymbol,
Expand All @@ -26,7 +19,6 @@ import {
prependForwardSlash,
removeTrailingForwardSlash,
} from '../path.js';
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
import { RenderContext } from '../render-context.js';
import { createAssetLink } from '../render/ssr-element.js';
import { ensure404Route } from '../routing/astro-designed-error-pages.js';
Expand Down Expand Up @@ -96,7 +88,7 @@ export class App {
routes: manifest.routes.map((route) => route.routeData),
});
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
this.#pipeline = this.#createPipeline(streaming);
this.#pipeline = this.#createPipeline(this.#manifestData, streaming);
this.#adapterLogger = new AstroIntegrationLogger(
this.#logger.options,
this.#manifest.adapterName
Expand All @@ -110,18 +102,19 @@ export class App {
/**
* Creates a pipeline by reading the stored manifest
*
* @param manifestData
* @param streaming
* @private
*/
#createPipeline(streaming = false) {
#createPipeline(manifestData: ManifestData, streaming = false) {
if (this.#manifest.checkOrigin) {
this.#manifest.middleware = sequence(
createOriginCheckMiddleware(),
this.#manifest.middleware
);
}

return AppPipeline.create({
return AppPipeline.create(manifestData, {
logger: this.#logger,
manifest: this.#manifest,
mode: 'production',
Expand Down Expand Up @@ -309,7 +302,7 @@ export class App {
}
const pathname = this.#getPathnameFromRequest(request);
const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
const mod = await this.#getModuleForRoute(routeData);
const mod = await this.#pipeline.getModuleForRoute(routeData);

let response;
try {
Expand Down Expand Up @@ -405,7 +398,7 @@ export class App {

return this.#mergeResponses(response, originalResponse, override);
}
const mod = await this.#getModuleForRoute(errorRouteData);
const mod = await this.#pipeline.getModuleForRoute(errorRouteData);
try {
const renderContext = RenderContext.create({
locals,
Expand Down Expand Up @@ -493,35 +486,4 @@ export class App {
if (route.endsWith('/500')) return 500;
return 200;
}

async #getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> {
if (route.component === DEFAULT_404_COMPONENT) {
return {
page: async () =>
({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance,
renderers: [],
};
}
if (route.type === 'redirect') {
return RedirectSinglePageBuiltModule;
} else {
if (this.#manifest.pageMap) {
const importComponentInstance = this.#manifest.pageMap.get(route.component);
if (!importComponentInstance) {
throw new Error(
`Unexpectedly unable to find a component instance for route ${route.route}`
);
}
const pageModule = await importComponentInstance();
return pageModule;
} else if (this.#manifest.pageModule) {
const importComponentInstance = this.#manifest.pageModule;
return importComponentInstance;
} else {
throw new Error(
"Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue."
);
}
}
}
}
115 changes: 96 additions & 19 deletions packages/astro/src/core/app/pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
import type {
ComponentInstance,
ReroutePayload,
ManifestData,
RouteData,
SSRElement,
SSRResult,
ComponentInstance,
ReroutePayload,
} from '../../@types/astro.js';
import { Pipeline } from '../base-pipeline.js';
import { DEFAULT_404_COMPONENT } from '../constants.js';
import { RedirectSinglePageBuiltModule } from '../redirects/component.js';
import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js';
import type { SinglePageBuiltModule } from '../build/types.js';

export class AppPipeline extends Pipeline {
static create({
logger,
manifest,
mode,
renderers,
resolve,
serverLike,
streaming,
}: Pick<
AppPipeline,
'logger' | 'manifest' | 'mode' | 'renderers' | 'resolve' | 'serverLike' | 'streaming'
>) {
return new AppPipeline(logger, manifest, mode, renderers, resolve, serverLike, streaming);
#manifestData: ManifestData | undefined;

static create(
manifestData: ManifestData,
{
logger,
manifest,
mode,
renderers,
resolve,
serverLike,
streaming,
}: Pick<
AppPipeline,
'logger' | 'manifest' | 'mode' | 'renderers' | 'resolve' | 'serverLike' | 'streaming'
>
) {
const pipeline = new AppPipeline(
logger,
manifest,
mode,
renderers,
resolve,
serverLike,
streaming
);
pipeline.#manifestData = manifestData;
return pipeline;
}

headElements(routeData: RouteData): Pick<SSRResult, 'scripts' | 'styles' | 'links'> {
Expand All @@ -47,11 +66,69 @@ export class AppPipeline extends Pipeline {
}

componentMetadata() {}
getComponentByRoute(_routeData: RouteData): Promise<ComponentInstance> {
throw new Error('unimplemented');
async getComponentByRoute(routeData: RouteData): Promise<ComponentInstance> {
const module = await this.getModuleForRoute(routeData);
return module.page();
}

tryReroute(_reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]> {
throw new Error('unimplemented');
async tryReroute(payload: ReroutePayload): Promise<[RouteData, ComponentInstance]> {
let foundRoute;

for (const route of this.#manifestData!.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 {
// TODO: handle error properly
throw new Error('Route not found');
}
}

async getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> {
if (route.component === DEFAULT_404_COMPONENT) {
return {
page: async () =>
({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance,
renderers: [],
};
}
if (route.type === 'redirect') {
return RedirectSinglePageBuiltModule;
} else {
if (this.manifest.pageMap) {
const importComponentInstance = this.manifest.pageMap.get(route.component);
if (!importComponentInstance) {
throw new Error(
`Unexpectedly unable to find a component instance for route ${route.route}`
);
}
return await importComponentInstance();
} else if (this.manifest.pageModule) {
return this.manifest.pageModule;
} else {
throw new Error(
"Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue."
);
}
}
}
}
1 change: 0 additions & 1 deletion packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,6 @@ export class RenderContext {
}
};


return {
generator: astroStaticPartial.generator,
glob: astroStaticPartial.glob,
Expand Down
63 changes: 63 additions & 0 deletions packages/astro/test/reroute.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, before, after } from 'node:test';
import { loadFixture } from './test-utils.js';
import { load as cheerioLoad } from 'cheerio';
import assert from 'node:assert/strict';
import testAdapter from './test-adapter.js';

describe('Dev reroute', () => {
/** @type {import('./test-utils').Fixture} */
Expand Down Expand Up @@ -102,3 +103,65 @@ describe('Build reroute', () => {
assert.equal($('h1').text(), 'Index');
});
});

describe('SSR reroute', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let app;

before(async () => {
fixture = await loadFixture({
root: './fixtures/reroute/',
output: 'server',
adapter: testAdapter(),
});
await fixture.build();
app = await fixture.loadTestAdapterApp();
});

it('the render the index page when navigating /reroute ', async () => {
const request = new Request('http://example.com/reroute');
const response = await app.render(request);
const html = await response.text();
const $ = cheerioLoad(html);

assert.equal($('h1').text(), 'Index');
});

it('the render the index page when navigating /blog/hello ', async () => {
const request = new Request('http://example.com/blog/hello');
const response = await app.render(request);
const html = await response.text();
const $ = cheerioLoad(html);

assert.equal($('h1').text(), 'Index');
});

it('the render the index page when navigating /blog/salut ', async () => {
const request = new Request('http://example.com/blog/salut');
const response = await app.render(request);
const html = await response.text();

const $ = cheerioLoad(html);

assert.equal($('h1').text(), 'Index');
});

it('the render the index page when navigating dynamic route /dynamic/[id] ', async () => {
const request = new Request('http://example.com/dynamic/hello');
const response = await app.render(request);
const html = await response.text();
const $ = cheerioLoad(html);

assert.equal($('h1').text(), 'Index');
});

it('the render the index page when navigating spread route /spread/[...spread] ', async () => {
const request = new Request('http://example.com/spread/hello');
const response = await app.render(request);
const html = await response.text();
const $ = cheerioLoad(html);

assert.equal($('h1').text(), 'Index');
});
});

0 comments on commit 2ee5817

Please sign in to comment.