Skip to content

Commit

Permalink
Deprecates exporting prerender with dynamic values (#11657)
Browse files Browse the repository at this point in the history
* wip

* done i think

* Add changeset

* Use hook instead

* Reorder hooks [skip ci]

* Update .changeset/eleven-pens-glow.md

Co-authored-by: Sarah Rainsberger <[email protected]>

* Fix run

* Fix link

* Add link

Co-authored-by: Sarah Rainsberger <[email protected]>

* More accurate migration [skip ci]

---------

Co-authored-by: Sarah Rainsberger <[email protected]>
Co-authored-by: Sarah Rainsberger <[email protected]>
  • Loading branch information
3 people authored Aug 14, 2024
1 parent d3d99fb commit a23c69d
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 9 deletions.
41 changes: 41 additions & 0 deletions .changeset/eleven-pens-glow.md
Original file line number Diff line number Diff line change
@@ -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;
}
},
},
};
}
```
19 changes: 19 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -3128,6 +3143,10 @@ declare global {
logger: AstroIntegrationLogger;
cacheManifest: boolean;
}) => void | Promise<void>;
'astro:route:setup': (options: {
route: RouteOptions;
logger: AstroIntegrationLogger;
}) => void | Promise<void>;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/create-vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
42 changes: 42 additions & 0 deletions packages/astro/src/integrations/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
17 changes: 16 additions & 1 deletion packages/astro/src/vite-plugin-env/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down
36 changes: 30 additions & 6 deletions packages/astro/src/vite-plugin-scanner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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;
}
62 changes: 62 additions & 0 deletions packages/integrations/node/test/prerender.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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;

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

0 comments on commit a23c69d

Please sign in to comment.