Allow SSR dynamic routes to not implement getStaticPaths (#2815)
* Allow SSR dynamic routes to not implement getStaticPaths * Adds a changeset * Update based on code-review comments
This commit is contained in:
parent
f6709d7259
commit
7b9d042dde
12 changed files with 81 additions and 25 deletions
5
.changeset/spotty-houses-stare.md
Normal file
5
.changeset/spotty-houses-stare.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Allows dynamic routes in SSR to avoid implementing getStaticPaths
|
|
@ -1,4 +1,4 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
renderers: ['@astrojs/renderer-svelte'],
|
||||
|
|
|
@ -2,18 +2,9 @@
|
|||
import Header from '../../components/Header.astro';
|
||||
import Container from '../../components/Container.astro';
|
||||
import AddToCart from '../../components/AddToCart.svelte';
|
||||
import { getProducts, getProduct } from '../../api';
|
||||
import { getProduct } from '../../api';
|
||||
import '../../styles/common.css';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const products = await getProducts();
|
||||
return products.map(product => {
|
||||
return {
|
||||
params: { id: product.id.toString() }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const id = Number(Astro.request.params.id);
|
||||
const product = await getProduct(id);
|
||||
---
|
||||
|
|
|
@ -83,6 +83,7 @@ class AstroBuilder {
|
|||
origin,
|
||||
routeCache: this.routeCache,
|
||||
viteServer: this.viteServer,
|
||||
ssr: this.config.buildOptions.experimentalSsr,
|
||||
});
|
||||
|
||||
// Filter pages by using conditions based on their frontmatter.
|
||||
|
|
|
@ -17,6 +17,7 @@ export interface CollectPagesDataOptions {
|
|||
origin: string;
|
||||
routeCache: RouteCache;
|
||||
viteServer: ViteDevServer;
|
||||
ssr: boolean;
|
||||
}
|
||||
|
||||
export interface CollectPagesDataResult {
|
||||
|
@ -109,11 +110,11 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
|
|||
}
|
||||
|
||||
async function getStaticPathsForRoute(opts: CollectPagesDataOptions, route: RouteData): Promise<RouteCacheEntry> {
|
||||
const { astroConfig, logging, routeCache, viteServer } = opts;
|
||||
const { astroConfig, logging, routeCache, ssr, viteServer } = opts;
|
||||
if (!viteServer) throw new Error(`vite.createServer() not called!`);
|
||||
const filePath = new URL(`./${route.component}`, astroConfig.projectRoot);
|
||||
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
||||
const result = await callGetStaticPaths(mod, route, false, logging);
|
||||
const result = await callGetStaticPaths({ mod, route, isValidate: false, logging, ssr });
|
||||
routeCache.set(route, result);
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ interface GetParamsAndPropsOptions {
|
|||
routeCache: RouteCache;
|
||||
pathname: string;
|
||||
logging: LogOptions;
|
||||
ssr: boolean;
|
||||
}
|
||||
|
||||
export const enum GetParamsAndPropsError {
|
||||
|
@ -20,7 +21,7 @@ export const enum GetParamsAndPropsError {
|
|||
}
|
||||
|
||||
export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise<[Params, Props] | GetParamsAndPropsError> {
|
||||
const { logging, mod, route, routeCache, pathname } = opts;
|
||||
const { logging, mod, route, routeCache, pathname, ssr} = opts;
|
||||
// Handle dynamic routes
|
||||
let params: Params = {};
|
||||
let pageProps: Props;
|
||||
|
@ -37,18 +38,18 @@ export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise
|
|||
// TODO(fks): Can we refactor getParamsAndProps() to receive routeCacheEntry
|
||||
// as a prop, and not do a live lookup/populate inside this lower function call.
|
||||
if (!routeCacheEntry) {
|
||||
routeCacheEntry = await callGetStaticPaths(mod, route, true, logging);
|
||||
routeCacheEntry = await callGetStaticPaths({ mod, route, isValidate: true, logging, ssr });
|
||||
routeCache.set(route, routeCacheEntry);
|
||||
}
|
||||
const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, params);
|
||||
if (!matchedStaticPath) {
|
||||
if (!matchedStaticPath && !ssr) {
|
||||
return GetParamsAndPropsError.NoMatchingStaticPath;
|
||||
}
|
||||
// Note: considered using Object.create(...) for performance
|
||||
// Since this doesn't inherit an object's properties, this caused some odd user-facing behavior.
|
||||
// Ex. console.log(Astro.props) -> {}, but console.log(Astro.props.property) -> 'expected value'
|
||||
// Replaced with a simple spread as a compromise
|
||||
pageProps = matchedStaticPath.props ? { ...matchedStaticPath.props } : {};
|
||||
pageProps = matchedStaticPath?.props ? { ...matchedStaticPath.props } : {};
|
||||
} else {
|
||||
pageProps = {};
|
||||
}
|
||||
|
@ -83,6 +84,7 @@ export async function render(opts: RenderOptions): Promise<{ type: 'html'; html:
|
|||
route,
|
||||
routeCache,
|
||||
pathname,
|
||||
ssr,
|
||||
});
|
||||
|
||||
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||
|
|
|
@ -11,19 +11,29 @@ function stringifyParams(params: Params) {
|
|||
return JSON.stringify(params, Object.keys(params).sort());
|
||||
}
|
||||
|
||||
export async function callGetStaticPaths(mod: ComponentInstance, route: RouteData, isValidate: boolean, logging: LogOptions): Promise<RouteCacheEntry> {
|
||||
validateGetStaticPathsModule(mod);
|
||||
interface CallGetStaticPathsOptions {
|
||||
mod: ComponentInstance;
|
||||
route: RouteData;
|
||||
isValidate: boolean;
|
||||
logging: LogOptions;
|
||||
ssr: boolean;
|
||||
}
|
||||
|
||||
export async function callGetStaticPaths({ isValidate, logging, mod, route, ssr}: CallGetStaticPathsOptions): Promise<RouteCacheEntry> {
|
||||
validateGetStaticPathsModule(mod, { ssr });
|
||||
const resultInProgress = {
|
||||
rss: [] as RSS[],
|
||||
};
|
||||
const staticPaths: GetStaticPathsResult = await (
|
||||
await mod.getStaticPaths!({
|
||||
|
||||
let staticPaths: GetStaticPathsResult = [];
|
||||
if(mod.getStaticPaths) {
|
||||
staticPaths = (await mod.getStaticPaths({
|
||||
paginate: generatePaginateFunction(route),
|
||||
rss: (data) => {
|
||||
resultInProgress.rss.push(data);
|
||||
},
|
||||
})
|
||||
).flat();
|
||||
})).flat();
|
||||
}
|
||||
|
||||
const keyedStaticPaths = staticPaths as GetStaticPathsResultKeyed;
|
||||
keyedStaticPaths.keyed = new Map<string, GetStaticPathsItem>();
|
||||
|
|
|
@ -2,12 +2,16 @@ import type { ComponentInstance, GetStaticPathsResult } from '../../@types/astro
|
|||
import type { LogOptions } from '../logger';
|
||||
import { warn } from '../logger.js';
|
||||
|
||||
interface ValidationOptions {
|
||||
ssr: boolean;
|
||||
}
|
||||
|
||||
/** Throw error for deprecated/malformed APIs */
|
||||
export function validateGetStaticPathsModule(mod: ComponentInstance) {
|
||||
export function validateGetStaticPathsModule(mod: ComponentInstance, { ssr }: ValidationOptions) {
|
||||
if ((mod as any).createCollection) {
|
||||
throw new Error(`[createCollection] deprecated. Please use getStaticPaths() instead.`);
|
||||
}
|
||||
if (!mod.getStaticPaths) {
|
||||
if (!mod.getStaticPaths && !ssr) {
|
||||
throw new Error(`[getStaticPaths] getStaticPaths() function is required. Make sure that you \`export\` the function from your component.`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,6 +152,7 @@ async function handleRequest(
|
|||
routeCache,
|
||||
pathname: rootRelativeUrl,
|
||||
logging,
|
||||
ssr: config.buildOptions.experimentalSsr,
|
||||
});
|
||||
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||
warn(logging, 'getStaticPaths', `Route pattern matched, but no matching static path found. (${pathname})`);
|
||||
|
|
11
packages/astro/test/fixtures/ssr-dynamic/src/pages/[id].astro
vendored
Normal file
11
packages/astro/test/fixtures/ssr-dynamic/src/pages/[id].astro
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
const val = Number(Astro.request.params.id);
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Test app</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Item { val }</h1>
|
||||
</body>
|
||||
</html>
|
28
packages/astro/test/ssr-dynamic.test.js
Normal file
28
packages/astro/test/ssr-dynamic.test.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { expect } from 'chai';
|
||||
import { load as cheerioLoad } from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
// Asset bundling
|
||||
describe('Dynamic pages in SSR', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/ssr-dynamic/',
|
||||
buildOptions: {
|
||||
experimentalSsr: true,
|
||||
}
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Do not have to implement getStaticPaths', async () => {
|
||||
const app = await fixture.loadSSRApp();
|
||||
const request = new Request("http://example.com/123");
|
||||
const route = app.match(request);
|
||||
const response = await app.render(request, route);
|
||||
const html = await response.text();
|
||||
const $ = cheerioLoad(html);
|
||||
expect($('h1').text()).to.equal('Item 123');
|
||||
});
|
||||
});
|
|
@ -6,6 +6,7 @@ import { loadConfig } from '../dist/core/config.js';
|
|||
import dev from '../dist/core/dev/index.js';
|
||||
import build from '../dist/core/build/index.js';
|
||||
import preview from '../dist/core/preview/index.js';
|
||||
import { loadApp } from '../dist/core/app/node.js';
|
||||
import os from 'os';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
|
@ -86,6 +87,7 @@ export async function loadFixture(inlineConfig) {
|
|||
inlineConfig.devOptions.port = previewServer.port; // update port for fetch
|
||||
return previewServer;
|
||||
},
|
||||
loadSSRApp: () => loadApp(new URL('./server/', config.dist)),
|
||||
readFile: (filePath) => fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.dist), 'utf8'),
|
||||
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.dist)),
|
||||
clean: () => fs.promises.rm(config.dist, { maxRetries: 10, recursive: true, force: true }),
|
||||
|
|
Loading…
Reference in a new issue