mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
Deprecates exporting prerender with dynamic values (#11657)
* wip * done i think * Add changeset * Use hook instead * Reorder hooks [skip ci] * Update .changeset/eleven-pens-glow.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Fix run * Fix link * Add link Co-authored-by: Sarah Rainsberger <sarah11918@users.noreply.github.com> * More accurate migration [skip ci] --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> Co-authored-by: Sarah Rainsberger <sarah11918@users.noreply.github.com>
This commit is contained in:
parent
d3d99fba26
commit
a23c69d0d0
8 changed files with 212 additions and 9 deletions
41
.changeset/eleven-pens-glow.md
Normal file
41
.changeset/eleven-pens-glow.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Deprecates the option for route-generating files to export a dynamic value for `prerender`. Only static values are now supported (e.g. `export const prerender = true` or `= false`). This allows for better treeshaking and bundling configuration in the future.
|
||||
|
||||
Adds a new [`"astro:route:setup"` hook](https://docs.astro.build/en/reference/integrations-reference/#astroroutesetup) to the Integrations API to allow you to dynamically set options for a route at build or request time through an integration, such as enabling [on-demand server rendering](https://docs.astro.build/en/guides/server-side-rendering/#opting-in-to-pre-rendering-in-server-mode).
|
||||
|
||||
To migrate from a dynamic export to the new hook, update or remove any dynamic `prerender` exports from individual routing files:
|
||||
|
||||
```diff
|
||||
// src/pages/blog/[slug].astro
|
||||
- export const prerender = import.meta.env.PRERENDER
|
||||
```
|
||||
|
||||
Instead, create an integration with the `"astro:route:setup"` hook and update the route's `prerender` option:
|
||||
|
||||
```js
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import { loadEnv } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [setPrerender()],
|
||||
});
|
||||
|
||||
function setPrerender() {
|
||||
const { PRERENDER } = loadEnv(process.env.NODE_ENV, process.cwd(), '');
|
||||
|
||||
return {
|
||||
name: 'set-prerender',
|
||||
hooks: {
|
||||
'astro:route:setup': ({ route }) => {
|
||||
if (route.component.endsWith('/blog/[slug].astro')) {
|
||||
route.prerender = PRERENDER;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
|
@ -2223,6 +2223,21 @@ export interface ResolvedInjectedRoute extends InjectedRoute {
|
|||
resolvedEntryPoint?: URL;
|
||||
}
|
||||
|
||||
export interface RouteOptions {
|
||||
/**
|
||||
* The path to this route relative to the project root. The slash is normalized as forward slash
|
||||
* across all OS.
|
||||
* @example "src/pages/blog/[...slug].astro"
|
||||
*/
|
||||
readonly component: string;
|
||||
/**
|
||||
* Whether this route should be prerendered. If the route has an explicit `prerender` export,
|
||||
* the value will be passed here. Otherwise, it's undefined and will fallback to a prerender
|
||||
* default depending on the `output` option.
|
||||
*/
|
||||
prerender?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved Astro Config
|
||||
* Config with user settings along with all defaults filled in.
|
||||
|
@ -3128,6 +3143,10 @@ declare global {
|
|||
logger: AstroIntegrationLogger;
|
||||
cacheManifest: boolean;
|
||||
}) => void | Promise<void>;
|
||||
'astro:route:setup': (options: {
|
||||
route: RouteOptions;
|
||||
logger: AstroIntegrationLogger;
|
||||
}) => void | Promise<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ export async function createVite(
|
|||
// The server plugin is for dev only and having it run during the build causes
|
||||
// the build to run very slow as the filewatcher is triggered often.
|
||||
mode !== 'build' && vitePluginAstroServer({ settings, logger, fs }),
|
||||
envVitePlugin({ settings }),
|
||||
envVitePlugin({ settings, logger }),
|
||||
astroEnv({ settings, mode, fs, sync }),
|
||||
markdownVitePlugin({ settings, logger }),
|
||||
htmlVitePlugin(),
|
||||
|
|
|
@ -153,7 +153,7 @@ export function isPage(file: URL, settings: AstroSettings): boolean {
|
|||
export function isEndpoint(file: URL, settings: AstroSettings): boolean {
|
||||
if (!isInPagesDir(file, settings.config)) return false;
|
||||
if (!isPublicRoute(file, settings.config)) return false;
|
||||
return !endsWithPageExt(file, settings);
|
||||
return !endsWithPageExt(file, settings) && !file.toString().includes('?astro');
|
||||
}
|
||||
|
||||
export function isServerLikeOutput(config: AstroConfig) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
DataEntryType,
|
||||
HookParameters,
|
||||
RouteData,
|
||||
RouteOptions,
|
||||
} from '../@types/astro.js';
|
||||
import type { SerializedSSRManifest } from '../core/app/types.js';
|
||||
import type { PageBuildData } from '../core/build/types.js';
|
||||
|
@ -558,6 +559,47 @@ export async function runHookBuildDone({
|
|||
}
|
||||
}
|
||||
|
||||
export async function runHookRouteSetup({
|
||||
route,
|
||||
settings,
|
||||
logger,
|
||||
}: {
|
||||
route: RouteOptions;
|
||||
settings: AstroSettings;
|
||||
logger: Logger;
|
||||
}) {
|
||||
const prerenderChangeLogs: { integrationName: string; value: boolean | undefined }[] = [];
|
||||
|
||||
for (const integration of settings.config.integrations) {
|
||||
if (integration?.hooks?.['astro:route:setup']) {
|
||||
const originalRoute = { ...route };
|
||||
const integrationLogger = getLogger(integration, logger);
|
||||
|
||||
await withTakingALongTimeMsg({
|
||||
name: integration.name,
|
||||
hookName: 'astro:route:setup',
|
||||
hookResult: integration.hooks['astro:route:setup']({
|
||||
route,
|
||||
logger: integrationLogger,
|
||||
}),
|
||||
logger,
|
||||
});
|
||||
|
||||
if (route.prerender !== originalRoute.prerender) {
|
||||
prerenderChangeLogs.push({ integrationName: integration.name, value: route.prerender });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prerenderChangeLogs.length > 1) {
|
||||
logger.debug(
|
||||
'router',
|
||||
`The ${route.component} route's prerender option has been changed multiple times by integrations:\n` +
|
||||
prerenderChangeLogs.map((log) => `- ${log.integrationName}: ${log.value}`).join('\n'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function isFunctionPerRouteEnabled(adapter: AstroAdapter | undefined): boolean {
|
||||
if (adapter?.adapterFeatures?.functionPerRoute === true) {
|
||||
return true;
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { transform } from 'esbuild';
|
||||
import { bold } from 'kleur/colors';
|
||||
import MagicString from 'magic-string';
|
||||
import type * as vite from 'vite';
|
||||
import { loadEnv } from 'vite';
|
||||
import type { AstroConfig, AstroSettings } from '../@types/astro.js';
|
||||
import type { Logger } from '../core/logger/core.js';
|
||||
|
||||
interface EnvPluginOptions {
|
||||
settings: AstroSettings;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
// Match `import.meta.env` directly without trailing property access
|
||||
|
@ -116,7 +119,7 @@ async function replaceDefine(
|
|||
};
|
||||
}
|
||||
|
||||
export default function envVitePlugin({ settings }: EnvPluginOptions): vite.Plugin {
|
||||
export default function envVitePlugin({ settings, logger }: EnvPluginOptions): vite.Plugin {
|
||||
let privateEnv: Record<string, string>;
|
||||
let defaultDefines: Record<string, string>;
|
||||
let isDev: boolean;
|
||||
|
@ -170,13 +173,25 @@ export default function envVitePlugin({ settings }: EnvPluginOptions): vite.Plug
|
|||
s.prepend(devImportMetaEnvPrepend);
|
||||
|
||||
// EDGE CASE: We need to do a static replacement for `export const prerender` for `vite-plugin-scanner`
|
||||
// TODO: Remove in Astro 5
|
||||
let exportConstPrerenderStr: string | undefined;
|
||||
s.replace(exportConstPrerenderRe, (m, key) => {
|
||||
exportConstPrerenderStr = m;
|
||||
if (privateEnv[key] != null) {
|
||||
return `export const prerender = ${privateEnv[key]}`;
|
||||
} else {
|
||||
return m;
|
||||
}
|
||||
});
|
||||
if (exportConstPrerenderStr) {
|
||||
logger.warn(
|
||||
'router',
|
||||
`Exporting dynamic values from prerender is deprecated. Please use an integration with the "astro:route:setup" hook ` +
|
||||
`to update the route's \`prerender\` option instead. This allows for better treeshaking and bundling configuration ` +
|
||||
`in the future. See https://docs.astro.build/en/reference/integrations-reference/#astroroutesetup for a migration example.` +
|
||||
`\nFound \`${bold(exportConstPrerenderStr)}\` in ${bold(id)}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
code: s.toString(),
|
||||
|
|
|
@ -2,11 +2,13 @@ import { extname } from 'node:path';
|
|||
import { bold } from 'kleur/colors';
|
||||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import { normalizePath } from 'vite';
|
||||
import type { AstroSettings } from '../@types/astro.js';
|
||||
import type { AstroSettings, RouteOptions } from '../@types/astro.js';
|
||||
import { type Logger } from '../core/logger/core.js';
|
||||
import { isEndpoint, isPage, isServerLikeOutput } from '../core/util.js';
|
||||
import { rootRelativePath } from '../core/viteUtils.js';
|
||||
import { runHookRouteSetup } from '../integrations/hooks.js';
|
||||
import { getPrerenderDefault } from '../prerender/utils.js';
|
||||
import type { PageOptions } from '../vite-plugin-astro/types.js';
|
||||
import { scan } from './scan.js';
|
||||
|
||||
export interface AstroPluginScannerOptions {
|
||||
|
@ -39,12 +41,8 @@ export default function astroScannerPlugin({
|
|||
const fileIsPage = isPage(fileURL, settings);
|
||||
const fileIsEndpoint = isEndpoint(fileURL, settings);
|
||||
if (!(fileIsPage || fileIsEndpoint)) return;
|
||||
const defaultPrerender = getPrerenderDefault(settings.config);
|
||||
const pageOptions = await scan(code, id, settings);
|
||||
const pageOptions = await getPageOptions(code, id, fileURL, settings, logger);
|
||||
|
||||
if (typeof pageOptions.prerender === 'undefined') {
|
||||
pageOptions.prerender = defaultPrerender;
|
||||
}
|
||||
// `getStaticPaths` warning is just a string check, should be good enough for most cases
|
||||
if (
|
||||
!pageOptions.prerender &&
|
||||
|
@ -76,3 +74,29 @@ export default function astroScannerPlugin({
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getPageOptions(
|
||||
code: string,
|
||||
id: string,
|
||||
fileURL: URL,
|
||||
settings: AstroSettings,
|
||||
logger: Logger,
|
||||
): Promise<PageOptions> {
|
||||
// Run initial scan
|
||||
const pageOptions = await scan(code, id, settings);
|
||||
|
||||
// Run integration hooks to alter page options
|
||||
const route: RouteOptions = {
|
||||
component: rootRelativePath(settings.config.root, fileURL, false),
|
||||
prerender: pageOptions.prerender,
|
||||
};
|
||||
await runHookRouteSetup({ route, settings, logger });
|
||||
pageOptions.prerender = route.prerender;
|
||||
|
||||
// Fallback if unset
|
||||
if (typeof pageOptions.prerender === 'undefined') {
|
||||
pageOptions.prerender = getPrerenderDefault(settings.config);
|
||||
}
|
||||
|
||||
return pageOptions;
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ describe('Prerendering', () => {
|
|||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal($('h1').text(), 'Two');
|
||||
assert.ok(fixture.pathExists('/client/two/index.html'));
|
||||
});
|
||||
|
||||
it('Can render prerendered route with redirect and query params', async () => {
|
||||
|
@ -131,6 +132,7 @@ describe('Prerendering', () => {
|
|||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal($('h1').text(), 'Two');
|
||||
assert.ok(fixture.pathExists('/client/two/index.html'));
|
||||
});
|
||||
|
||||
it('Can render prerendered route with redirect and query params', async () => {
|
||||
|
@ -152,6 +154,64 @@ describe('Prerendering', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Via integration', () => {
|
||||
before(async () => {
|
||||
process.env.PRERENDER = false;
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/prerender/',
|
||||
output: 'server',
|
||||
outDir: './dist/via-integration',
|
||||
build: {
|
||||
client: './dist/via-integration/client',
|
||||
server: './dist/via-integration/server',
|
||||
},
|
||||
adapter: nodejs({ mode: 'standalone' }),
|
||||
integrations: [
|
||||
{
|
||||
name: 'test',
|
||||
hooks: {
|
||||
'astro:route:setup': ({ route }) => {
|
||||
if (route.component.endsWith('two.astro')) {
|
||||
route.prerender = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await fixture.build();
|
||||
const { startServer } = await fixture.loadAdapterEntryModule();
|
||||
let res = startServer();
|
||||
server = res.server;
|
||||
await waitServerListen(server.server);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.stop();
|
||||
await fixture.clean();
|
||||
delete process.env.PRERENDER;
|
||||
});
|
||||
|
||||
it('Can render SSR route', async () => {
|
||||
const res = await fetch(`http://${server.host}:${server.port}/one`);
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal($('h1').text(), 'One');
|
||||
});
|
||||
|
||||
it('Can render prerendered route', async () => {
|
||||
const res = await fetch(`http://${server.host}:${server.port}/two`);
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal($('h1').text(), 'Two');
|
||||
assert.ok(fixture.pathExists('/client/two/index.html'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dev', () => {
|
||||
let devServer;
|
||||
|
||||
|
@ -243,6 +303,7 @@ describe('Hybrid rendering', () => {
|
|||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal($('h1').text(), 'One');
|
||||
assert.ok(fixture.pathExists('/client/one/index.html'));
|
||||
});
|
||||
|
||||
it('Can render prerendered route with redirect and query params', async () => {
|
||||
|
@ -316,6 +377,7 @@ describe('Hybrid rendering', () => {
|
|||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal($('h1').text(), 'One');
|
||||
assert.ok(fixture.pathExists('/client/one/index.html'));
|
||||
});
|
||||
|
||||
it('Can render prerendered route with redirect and query params', async () => {
|
||||
|
|
Loading…
Reference in a new issue