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({
|
export default defineConfig({
|
||||||
renderers: ['@astrojs/renderer-svelte'],
|
renderers: ['@astrojs/renderer-svelte'],
|
||||||
|
|
|
@ -2,18 +2,9 @@
|
||||||
import Header from '../../components/Header.astro';
|
import Header from '../../components/Header.astro';
|
||||||
import Container from '../../components/Container.astro';
|
import Container from '../../components/Container.astro';
|
||||||
import AddToCart from '../../components/AddToCart.svelte';
|
import AddToCart from '../../components/AddToCart.svelte';
|
||||||
import { getProducts, getProduct } from '../../api';
|
import { getProduct } from '../../api';
|
||||||
import '../../styles/common.css';
|
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 id = Number(Astro.request.params.id);
|
||||||
const product = await getProduct(id);
|
const product = await getProduct(id);
|
||||||
---
|
---
|
||||||
|
|
|
@ -83,6 +83,7 @@ class AstroBuilder {
|
||||||
origin,
|
origin,
|
||||||
routeCache: this.routeCache,
|
routeCache: this.routeCache,
|
||||||
viteServer: this.viteServer,
|
viteServer: this.viteServer,
|
||||||
|
ssr: this.config.buildOptions.experimentalSsr,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter pages by using conditions based on their frontmatter.
|
// Filter pages by using conditions based on their frontmatter.
|
||||||
|
|
|
@ -17,6 +17,7 @@ export interface CollectPagesDataOptions {
|
||||||
origin: string;
|
origin: string;
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
viteServer: ViteDevServer;
|
viteServer: ViteDevServer;
|
||||||
|
ssr: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectPagesDataResult {
|
export interface CollectPagesDataResult {
|
||||||
|
@ -109,11 +110,11 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getStaticPathsForRoute(opts: CollectPagesDataOptions, route: RouteData): Promise<RouteCacheEntry> {
|
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!`);
|
if (!viteServer) throw new Error(`vite.createServer() not called!`);
|
||||||
const filePath = new URL(`./${route.component}`, astroConfig.projectRoot);
|
const filePath = new URL(`./${route.component}`, astroConfig.projectRoot);
|
||||||
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
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);
|
routeCache.set(route, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ interface GetParamsAndPropsOptions {
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
|
ssr: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum GetParamsAndPropsError {
|
export const enum GetParamsAndPropsError {
|
||||||
|
@ -20,7 +21,7 @@ export const enum GetParamsAndPropsError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise<[Params, Props] | 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
|
// Handle dynamic routes
|
||||||
let params: Params = {};
|
let params: Params = {};
|
||||||
let pageProps: Props;
|
let pageProps: Props;
|
||||||
|
@ -37,18 +38,18 @@ export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise
|
||||||
// TODO(fks): Can we refactor getParamsAndProps() to receive routeCacheEntry
|
// TODO(fks): Can we refactor getParamsAndProps() to receive routeCacheEntry
|
||||||
// as a prop, and not do a live lookup/populate inside this lower function call.
|
// as a prop, and not do a live lookup/populate inside this lower function call.
|
||||||
if (!routeCacheEntry) {
|
if (!routeCacheEntry) {
|
||||||
routeCacheEntry = await callGetStaticPaths(mod, route, true, logging);
|
routeCacheEntry = await callGetStaticPaths({ mod, route, isValidate: true, logging, ssr });
|
||||||
routeCache.set(route, routeCacheEntry);
|
routeCache.set(route, routeCacheEntry);
|
||||||
}
|
}
|
||||||
const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, params);
|
const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, params);
|
||||||
if (!matchedStaticPath) {
|
if (!matchedStaticPath && !ssr) {
|
||||||
return GetParamsAndPropsError.NoMatchingStaticPath;
|
return GetParamsAndPropsError.NoMatchingStaticPath;
|
||||||
}
|
}
|
||||||
// Note: considered using Object.create(...) for performance
|
// Note: considered using Object.create(...) for performance
|
||||||
// Since this doesn't inherit an object's properties, this caused some odd user-facing behavior.
|
// 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'
|
// Ex. console.log(Astro.props) -> {}, but console.log(Astro.props.property) -> 'expected value'
|
||||||
// Replaced with a simple spread as a compromise
|
// Replaced with a simple spread as a compromise
|
||||||
pageProps = matchedStaticPath.props ? { ...matchedStaticPath.props } : {};
|
pageProps = matchedStaticPath?.props ? { ...matchedStaticPath.props } : {};
|
||||||
} else {
|
} else {
|
||||||
pageProps = {};
|
pageProps = {};
|
||||||
}
|
}
|
||||||
|
@ -83,6 +84,7 @@ export async function render(opts: RenderOptions): Promise<{ type: 'html'; html:
|
||||||
route,
|
route,
|
||||||
routeCache,
|
routeCache,
|
||||||
pathname,
|
pathname,
|
||||||
|
ssr,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||||
|
|
|
@ -11,19 +11,29 @@ function stringifyParams(params: Params) {
|
||||||
return JSON.stringify(params, Object.keys(params).sort());
|
return JSON.stringify(params, Object.keys(params).sort());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function callGetStaticPaths(mod: ComponentInstance, route: RouteData, isValidate: boolean, logging: LogOptions): Promise<RouteCacheEntry> {
|
interface CallGetStaticPathsOptions {
|
||||||
validateGetStaticPathsModule(mod);
|
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 = {
|
const resultInProgress = {
|
||||||
rss: [] as RSS[],
|
rss: [] as RSS[],
|
||||||
};
|
};
|
||||||
const staticPaths: GetStaticPathsResult = await (
|
|
||||||
await mod.getStaticPaths!({
|
let staticPaths: GetStaticPathsResult = [];
|
||||||
|
if(mod.getStaticPaths) {
|
||||||
|
staticPaths = (await mod.getStaticPaths({
|
||||||
paginate: generatePaginateFunction(route),
|
paginate: generatePaginateFunction(route),
|
||||||
rss: (data) => {
|
rss: (data) => {
|
||||||
resultInProgress.rss.push(data);
|
resultInProgress.rss.push(data);
|
||||||
},
|
},
|
||||||
})
|
})).flat();
|
||||||
).flat();
|
}
|
||||||
|
|
||||||
const keyedStaticPaths = staticPaths as GetStaticPathsResultKeyed;
|
const keyedStaticPaths = staticPaths as GetStaticPathsResultKeyed;
|
||||||
keyedStaticPaths.keyed = new Map<string, GetStaticPathsItem>();
|
keyedStaticPaths.keyed = new Map<string, GetStaticPathsItem>();
|
||||||
|
|
|
@ -2,12 +2,16 @@ import type { ComponentInstance, GetStaticPathsResult } from '../../@types/astro
|
||||||
import type { LogOptions } from '../logger';
|
import type { LogOptions } from '../logger';
|
||||||
import { warn } from '../logger.js';
|
import { warn } from '../logger.js';
|
||||||
|
|
||||||
|
interface ValidationOptions {
|
||||||
|
ssr: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** Throw error for deprecated/malformed APIs */
|
/** Throw error for deprecated/malformed APIs */
|
||||||
export function validateGetStaticPathsModule(mod: ComponentInstance) {
|
export function validateGetStaticPathsModule(mod: ComponentInstance, { ssr }: ValidationOptions) {
|
||||||
if ((mod as any).createCollection) {
|
if ((mod as any).createCollection) {
|
||||||
throw new Error(`[createCollection] deprecated. Please use getStaticPaths() instead.`);
|
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.`);
|
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,
|
routeCache,
|
||||||
pathname: rootRelativeUrl,
|
pathname: rootRelativeUrl,
|
||||||
logging,
|
logging,
|
||||||
|
ssr: config.buildOptions.experimentalSsr,
|
||||||
});
|
});
|
||||||
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||||
warn(logging, 'getStaticPaths', `Route pattern matched, but no matching static path found. (${pathname})`);
|
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 dev from '../dist/core/dev/index.js';
|
||||||
import build from '../dist/core/build/index.js';
|
import build from '../dist/core/build/index.js';
|
||||||
import preview from '../dist/core/preview/index.js';
|
import preview from '../dist/core/preview/index.js';
|
||||||
|
import { loadApp } from '../dist/core/app/node.js';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import stripAnsi from 'strip-ansi';
|
import stripAnsi from 'strip-ansi';
|
||||||
|
|
||||||
|
@ -86,6 +87,7 @@ export async function loadFixture(inlineConfig) {
|
||||||
inlineConfig.devOptions.port = previewServer.port; // update port for fetch
|
inlineConfig.devOptions.port = previewServer.port; // update port for fetch
|
||||||
return previewServer;
|
return previewServer;
|
||||||
},
|
},
|
||||||
|
loadSSRApp: () => loadApp(new URL('./server/', config.dist)),
|
||||||
readFile: (filePath) => fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.dist), 'utf8'),
|
readFile: (filePath) => fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.dist), 'utf8'),
|
||||||
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.dist)),
|
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.dist)),
|
||||||
clean: () => fs.promises.rm(config.dist, { maxRetries: 10, recursive: true, force: true }),
|
clean: () => fs.promises.rm(config.dist, { maxRetries: 10, recursive: true, force: true }),
|
||||||
|
|
Loading…
Reference in a new issue