0
Fork 0
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:
Bjorn Lu 2024-08-14 18:05:19 +08:00 committed by GitHub
parent d3d99fba26
commit a23c69d0d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 212 additions and 9 deletions

View 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;
}
},
},
};
}
```

View file

@ -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>;
}
}
}

View file

@ -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(),

View file

@ -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) {

View file

@ -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;

View file

@ -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(),

View file

@ -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;
}

View file

@ -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 () => {