0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-10 23:01:26 -05:00

feat: reroute for SSR

This commit is contained in:
Emanuele Stoppa 2024-04-22 14:31:59 +01:00
parent 8da194d3d6
commit b9265f85d7
4 changed files with 163 additions and 54 deletions

View file

@ -96,7 +96,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
@ -110,10 +110,11 @@ 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(),
@ -121,7 +122,7 @@ export class App {
);
}
return AppPipeline.create({
return AppPipeline.create(manifestData, {
logger: this.#logger,
manifest: this.#manifest,
mode: 'production',
@ -309,7 +310,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 {
@ -405,7 +406,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,
@ -493,35 +494,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."
);
}
}
}
}

View file

@ -1,5 +1,6 @@
import type {
ComponentInstance,
ManifestData,
ReroutePayload,
RouteData,
SSRElement,
@ -7,21 +8,39 @@ import type {
} from '../../@types/astro.js';
import { Pipeline } from '../base-pipeline.js';
import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js';
import type { SinglePageBuiltModule } from '../build/types.js';
import { DEFAULT_404_COMPONENT } from '../constants.js';
import { RedirectSinglePageBuiltModule } from '../redirects/index.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'> {
@ -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."
);
}
}
}
}

View file

@ -220,7 +220,6 @@ export class DevPipeline extends Pipeline {
const componentInstance = await this.getComponentByRoute(foundRoute);
return [foundRoute, componentInstance];
} else {
// TODO: handle error properly
throw new Error('Route not found');
}
}

View file

@ -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} */
@ -165,3 +166,65 @@ describe.only('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');
});
});