-
Notifications
You must be signed in to change notification settings - Fork 322
feat: static pre-rendering at build time #274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 40 commits
5e56610
2149bed
bbcf6e4
f44f446
1b8a0f9
df3830f
ce830ad
5be5d1c
69a5835
0ad2979
0c2c9f6
dcc6f65
b0dd390
64a6496
0a8f485
aab3b9b
2eee3d7
9271012
b53c335
cd6b149
4021f5d
8d1471c
8562487
5c8447d
46342f9
3e46d71
a9392bc
e442ab7
34da71d
184f4d5
be65641
8b31729
d90fb0d
a9c12b8
dfc4a8b
be4686c
e437567
2d54763
48e382d
7698281
5c2e1b2
95cfad9
60ed01d
6cbca62
b619afc
da5af57
61d0f08
1a1c906
efa09e6
3e26eb5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -37,6 +37,20 @@ export interface RouteRow { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ─── Regex-based export detection ──────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Shared regex builders — a single source of truth for the fn/var patterns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // used by both hasNamedExport and isLocallyDefinedExport. Extracting them here | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // prevents the two functions from silently drifting if the pattern is ever | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // updated (e.g. to handle `export declare function` or TypeScript's | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // `export const foo: Foo = ...`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function makeFnRe(name: string): RegExp { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new RegExp(`(?:^|\\n)\\s*export\\s+(?:async\\s+)?function\\s+${name}\\b`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function makeVarRe(name: string): RegExp { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new RegExp(`(?:^|\\n)\\s*export\\s+(?:const|let|var)\\s+${name}\\s*[=:]`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns true if the source code contains a named export with the given name. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Handles all three common export forms: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -46,12 +60,10 @@ export interface RouteRow { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function hasNamedExport(code: string, name: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Function / generator / async function declaration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fnRe = new RegExp(`(?:^|\\n)\\s*export\\s+(?:async\\s+)?function\\s+${name}\\b`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (fnRe.test(code)) return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (makeFnRe(name).test(code)) return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Variable declaration (const / let / var) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const varRe = new RegExp(`(?:^|\\n)\\s*export\\s+(?:const|let|var)\\s+${name}\\s*[=:]`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (varRe.test(code)) return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (makeVarRe(name).test(code)) return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Re-export specifier: export { foo } or export { foo as bar } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const reRe = new RegExp(`export\\s*\\{[^}]*\\b${name}\\b[^}]*\\}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -60,6 +72,30 @@ export function hasNamedExport(code: string, name: string): boolean { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns true if the source code contains a named export that is locally | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * defined in this file (function declaration or variable assignment), as | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * opposed to a re-export from another module (`export { foo } from '...'`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * This distinction matters for `getStaticProps`: a locally-defined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `getStaticProps` has its `revalidate` value readable in this file. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * A re-exported one (`export { getStaticProps } from './data'`) has its | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * implementation in another module — we cannot inspect its revalidate value. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Uses the same fn/var regex builders as `hasNamedExport` (via `makeFnRe` / | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `makeVarRe`) so the two functions always use identical patterns. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Deliberately excludes the re-export `{ foo }` branch. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function isLocallyDefinedExport(code: string, name: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Function / generator / async function declaration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (makeFnRe(name).test(code)) return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Variable declaration (const / let / var). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Note: intentionally omits the re-export `export { name }` form — that | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // indicates the implementation lives in another module, not locally here. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return makeVarRe(name).test(code); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+89
to
+97
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should fix: duplicated regex patterns from The Consider implementing
Suggested change
Or better — extract the shared regex builders into named helpers so both functions reference the same source of truth. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Extracts the string value of `export const <name> = "value"`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Handles optional TypeScript type annotations: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -146,6 +182,15 @@ export function classifyPagesRoute(filePath: string): { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (hasNamedExport(code, "getStaticProps")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If getStaticProps is a re-export from another module (e.g. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // `export { getStaticProps } from './data'`), the revalidate value lives in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // that other module — we cannot inspect it here. Conservatively treat | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // re-exported getStaticProps as ISR so the page is skipped during | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // pre-rendering rather than pinned with a year-long cache header. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isLocallyDefinedExport(code, "getStaticProps")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the correct conservative choice. When One thing to consider: export const getStaticProps: GetStaticProps = async (ctx) => { ... }This is handled by const gsp = async () => { ... }
export { gsp as getStaticProps }would be detected by
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the right conservative choice. One edge case to note for awareness: |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { type: "isr" }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const revalidate = extractGetStaticPropsRevalidate(code); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (revalidate === null || revalidate === false || revalidate === Infinity) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -221,25 +266,41 @@ export function classifyAppRoute( | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Builds a sorted list of RouteRow objects from the discovered routes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Routes are sorted alphabetically by path, matching filesystem order. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param options.knownRoutes - Optional map of pattern → classification override. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * When provided, entries from this map take precedence over the regex-based | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * analysis for matching routes. Used to pass in confirmed classifications from | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * the static export or pre-render phase (which has runtime-verified knowledge | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * of route types), replacing "unknown" with accurate data. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function buildReportRows(options: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageRoutes?: Route[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiRoutes?: Route[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| appRoutes?: AppRoute[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| knownRoutes?: Map<string, { type: RouteType; revalidate?: number }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }): RouteRow[] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows: RouteRow[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const known = options.knownRoutes; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const route of options.pageRoutes ?? []) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { type, revalidate } = classifyPagesRoute(route.filePath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const override = known?.get(route.pattern); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { type, revalidate } = override ?? classifyPagesRoute(route.filePath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rows.push({ pattern: route.pattern, type, revalidate }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const route of options.apiRoutes ?? []) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rows.push({ pattern: route.pattern, type: "api" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const override = known?.get(route.pattern); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rows.push({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pattern: route.pattern, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: override?.type ?? "api", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| revalidate: override?.revalidate, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const route of options.appRoutes ?? []) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { type, revalidate } = classifyAppRoute(route.pagePath, route.routePath, route.isDynamic); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const override = known?.get(route.pattern); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { type, revalidate } = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| override ?? classifyAppRoute(route.pagePath, route.routePath, route.isDynamic); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rows.push({ pattern: route.pattern, type, revalidate }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -336,10 +397,16 @@ function findDir(root: string, ...candidates: string[]): string | null { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Next.js-style build report to stdout. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Called at the end of `vinext build` in cli.ts. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param options.knownRoutes - Optional map of pattern → confirmed classification. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * When provided, these override the regex-based analysis for matching routes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Callers should populate this from the static export or pre-render results | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * to provide fully accurate route type information. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function printBuildReport(options: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| root: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageExtensions?: string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| knownRoutes?: Map<string, { type: RouteType; revalidate?: number }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { root } = options; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -352,7 +419,7 @@ export async function printBuildReport(options: { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Dynamic import to avoid loading routing code unless needed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { appRouter } = await import("../routing/app-router.js"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const routes = await appRouter(appDir, options.pageExtensions); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = buildReportRows({ appRoutes: routes }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = buildReportRows({ appRoutes: routes, knownRoutes: options.knownRoutes }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (rows.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("\n" + formatBuildReport(rows, "app")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -364,7 +431,7 @@ export async function printBuildReport(options: { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| pagesRouter(pagesDir, options.pageExtensions), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiRouter(pagesDir, options.pageExtensions), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = buildReportRows({ pageRoutes, apiRoutes }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = buildReportRows({ pageRoutes, apiRoutes, knownRoutes: options.knownRoutes }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (rows.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("\n" + formatBuildReport(rows, "pages")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should fix:
isLocallyDefinedExportduplicates the first two branches ofhasNamedExportThe function/variable regexes in
isLocallyDefinedExport(lines 75-76, 79-80) are identical copies of those inhasNamedExport(lines 49-50, 53-54). If either regex is updated (e.g., to handleexport declare function,export generator function*, or TypeScriptexport const getStaticProps: GetStaticProps = ...), the other will silently fall out of sync.Consider implementing
isLocallyDefinedExportin terms ofhasNamedExport:Or extract the shared regexes into constants/helpers. The current duplication is small but the functions are called in a correctness-critical path (ISR detection affects cache TTLs).