From e6b1264ea2d2df31d006bb7f425b1c69941de36e Mon Sep 17 00:00:00 2001 From: Boy Steven Benaya Aritonang Date: Thu, 19 Mar 2026 23:55:38 +0700 Subject: [PATCH 1/8] fix: scope getStaticProps revalidate parsing --- packages/vinext/src/build/report.ts | 94 +++++++++++++++++++++++++++-- tests/build-report.test.ts | 18 ++++++ 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/packages/vinext/src/build/report.ts b/packages/vinext/src/build/report.ts index b4d2c6135..a0bf5004f 100644 --- a/packages/vinext/src/build/report.ts +++ b/packages/vinext/src/build/report.ts @@ -110,19 +110,101 @@ export function extractExportConstNumber(code: string, name: string): number | n * null — no `revalidate` key found (fully static) */ export function extractGetStaticPropsRevalidate(code: string): number | false | null { - // TODO: This regex matches `revalidate:` anywhere in the file, not scoped to - // the getStaticProps return object. A config object or comment elsewhere in - // the file (e.g. `const defaults = { revalidate: 30 }`) could produce a false - // positive. Rare in practice, but a proper AST-based approach would be more - // accurate. + const searchSpace = extractGetStaticPropsReturnObject(code) ?? code; const re = /\brevalidate\s*:\s*(-?\d+(?:\.\d+)?|Infinity|false)\b/; - const m = re.exec(code); + const m = re.exec(searchSpace); if (!m) return null; if (m[1] === "false") return false; if (m[1] === "Infinity") return Infinity; return parseFloat(m[1]); } +function extractGetStaticPropsReturnObject(code: string): string | null { + const declarationMatch = + /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+getStaticProps\b|(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+getStaticProps\b/.exec( + code, + ); + if (!declarationMatch) return null; + + const declaration = code.slice(declarationMatch.index); + const returnObjectStart = + declaration.search(/\breturn\s*\{/) !== -1 + ? declaration.search(/\breturn\s*\{/) + : declaration.search(/=>\s*\(\s*\{/); + if (returnObjectStart === -1) return declaration; + + const braceStart = declaration.indexOf("{", returnObjectStart); + if (braceStart === -1) return declaration; + + const braceEnd = findMatchingBrace(declaration, braceStart); + if (braceEnd === -1) return declaration; + + return declaration.slice(braceStart, braceEnd + 1); +} + +function findMatchingBrace(code: string, start: number): number { + let depth = 0; + let quote: '"' | "'" | "`" | null = null; + let inLineComment = false; + let inBlockComment = false; + + for (let i = start; i < code.length; i++) { + const char = code[i]; + const next = code[i + 1]; + + if (inLineComment) { + if (char === "\n") inLineComment = false; + continue; + } + + if (inBlockComment) { + if (char === "*" && next === "/") { + inBlockComment = false; + i++; + } + continue; + } + + if (quote) { + if (char === "\\") { + i++; + continue; + } + if (char === quote) quote = null; + continue; + } + + if (char === "/" && next === "/") { + inLineComment = true; + i++; + continue; + } + + if (char === "/" && next === "*") { + inBlockComment = true; + i++; + continue; + } + + if (char === '"' || char === "'" || char === "`") { + quote = char; + continue; + } + + if (char === "{") { + depth++; + continue; + } + + if (char === "}") { + depth--; + if (depth === 0) return i; + } + } + + return -1; +} + // ─── Route classification ───────────────────────────────────────────────────── /** diff --git a/tests/build-report.test.ts b/tests/build-report.test.ts index 76f79b4b7..48e01ccb0 100644 --- a/tests/build-report.test.ts +++ b/tests/build-report.test.ts @@ -167,6 +167,24 @@ describe("extractGetStaticPropsRevalidate", () => { expect(extractGetStaticPropsRevalidate(code)).toBeNull(); }); + it("ignores unrelated revalidate values outside getStaticProps", () => { + const code = `const defaults = { revalidate: 30 }; + +export async function getStaticProps() { + return { props: { ok: true } }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBeNull(); + }); + + it("prefers revalidate inside getStaticProps over unrelated values elsewhere", () => { + const code = `const defaults = { revalidate: 30 }; + +export async function getStaticProps() { + return { props: {}, revalidate: 60 }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBe(60); + }); + it("handles inline comment after value (fixture file style)", () => { // From tests/fixtures/pages-basic/pages/isr-test.tsx: // revalidate: 1, // Revalidate every 1 second From 5a1f4c87f09c20869fef746c99dace6e3ca631ac Mon Sep 17 00:00:00 2001 From: Boy Steven Benaya Aritonang Date: Fri, 20 Mar 2026 00:53:03 +0700 Subject: [PATCH 2/8] fix: handle early returns in getStaticProps revalidate parsing --- packages/vinext/src/build/report.ts | 49 ++++++++++++++++++++++------- tests/build-report.test.ts | 10 ++++++ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/packages/vinext/src/build/report.ts b/packages/vinext/src/build/report.ts index a0bf5004f..c8c9ee192 100644 --- a/packages/vinext/src/build/report.ts +++ b/packages/vinext/src/build/report.ts @@ -110,16 +110,28 @@ export function extractExportConstNumber(code: string, name: string): number | n * null — no `revalidate` key found (fully static) */ export function extractGetStaticPropsRevalidate(code: string): number | false | null { - const searchSpace = extractGetStaticPropsReturnObject(code) ?? code; const re = /\brevalidate\s*:\s*(-?\d+(?:\.\d+)?|Infinity|false)\b/; - const m = re.exec(searchSpace); + const returnObjects = extractGetStaticPropsReturnObjects(code); + + if (returnObjects) { + for (const searchSpace of returnObjects) { + const m = re.exec(searchSpace); + if (!m) continue; + if (m[1] === "false") return false; + if (m[1] === "Infinity") return Infinity; + return parseFloat(m[1]); + } + return null; + } + + const m = re.exec(code); if (!m) return null; if (m[1] === "false") return false; if (m[1] === "Infinity") return Infinity; return parseFloat(m[1]); } -function extractGetStaticPropsReturnObject(code: string): string | null { +function extractGetStaticPropsReturnObjects(code: string): string[] | null { const declarationMatch = /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+getStaticProps\b|(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+getStaticProps\b/.exec( code, @@ -127,19 +139,32 @@ function extractGetStaticPropsReturnObject(code: string): string | null { if (!declarationMatch) return null; const declaration = code.slice(declarationMatch.index); - const returnObjectStart = - declaration.search(/\breturn\s*\{/) !== -1 - ? declaration.search(/\breturn\s*\{/) - : declaration.search(/=>\s*\(\s*\{/); - if (returnObjectStart === -1) return declaration; + const returnObjects: string[] = []; + const returnPattern = /\breturn\s*\{/g; + let returnMatch: RegExpExecArray | null; + + while ((returnMatch = returnPattern.exec(declaration)) !== null) { + const braceStart = declaration.indexOf("{", returnMatch.index); + if (braceStart === -1) continue; + + const braceEnd = findMatchingBrace(declaration, braceStart); + if (braceEnd === -1) continue; + + returnObjects.push(declaration.slice(braceStart, braceEnd + 1)); + } + + if (returnObjects.length > 0) return returnObjects; + + const arrowMatch = declaration.search(/=>\s*\(\s*\{/); + if (arrowMatch === -1) return []; - const braceStart = declaration.indexOf("{", returnObjectStart); - if (braceStart === -1) return declaration; + const braceStart = declaration.indexOf("{", arrowMatch); + if (braceStart === -1) return []; const braceEnd = findMatchingBrace(declaration, braceStart); - if (braceEnd === -1) return declaration; + if (braceEnd === -1) return []; - return declaration.slice(braceStart, braceEnd + 1); + return [declaration.slice(braceStart, braceEnd + 1)]; } function findMatchingBrace(code: string, start: number): number { diff --git a/tests/build-report.test.ts b/tests/build-report.test.ts index 48e01ccb0..89f470885 100644 --- a/tests/build-report.test.ts +++ b/tests/build-report.test.ts @@ -185,6 +185,16 @@ export async function getStaticProps() { expect(extractGetStaticPropsRevalidate(code)).toBe(60); }); + it("finds revalidate in a later return when an earlier return redirects", () => { + const code = `export async function getStaticProps(ctx) { + if (!ctx.params?.slug) { + return { redirect: { destination: "/", permanent: false } }; + } + return { props: { data: 1 }, revalidate: 60 }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBe(60); + }); + it("handles inline comment after value (fixture file style)", () => { // From tests/fixtures/pages-basic/pages/isr-test.tsx: // revalidate: 1, // Revalidate every 1 second From 8e6136fde80b4906ed82cdde158b13fcda278eee Mon Sep 17 00:00:00 2001 From: Boy Steven Benaya Aritonang Date: Fri, 20 Mar 2026 01:08:00 +0700 Subject: [PATCH 3/8] fix: scope getStaticProps parsing to its declaration --- packages/vinext/src/build/report.ts | 47 ++++++++++++++++++++++++++++- tests/build-report.test.ts | 11 +++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/vinext/src/build/report.ts b/packages/vinext/src/build/report.ts index c8c9ee192..ccf5d850a 100644 --- a/packages/vinext/src/build/report.ts +++ b/packages/vinext/src/build/report.ts @@ -138,7 +138,9 @@ function extractGetStaticPropsReturnObjects(code: string): string[] | null { ); if (!declarationMatch) return null; - const declaration = code.slice(declarationMatch.index); + const declaration = extractGetStaticPropsDeclaration(code, declarationMatch); + if (declaration === null) return []; + const returnObjects: string[] = []; const returnPattern = /\breturn\s*\{/g; let returnMatch: RegExpExecArray | null; @@ -156,6 +158,8 @@ function extractGetStaticPropsReturnObjects(code: string): string[] | null { if (returnObjects.length > 0) return returnObjects; const arrowMatch = declaration.search(/=>\s*\(\s*\{/); + // getStaticProps was found but contains no return objects — return empty + // (non-null signals the caller to skip the whole-file fallback). if (arrowMatch === -1) return []; const braceStart = declaration.indexOf("{", arrowMatch); @@ -167,6 +171,47 @@ function extractGetStaticPropsReturnObjects(code: string): string[] | null { return [declaration.slice(braceStart, braceEnd + 1)]; } +function extractGetStaticPropsDeclaration( + code: string, + declarationMatch: RegExpExecArray, +): string | null { + const declarationStart = declarationMatch.index; + const declarationText = declarationMatch[0]; + const declarationTail = code.slice(declarationStart); + + if (declarationText.includes("function getStaticProps")) { + const bodyStart = code.indexOf("{", declarationStart + declarationText.length); + if (bodyStart === -1) return null; + + const bodyEnd = findMatchingBrace(code, bodyStart); + if (bodyEnd === -1) return null; + + return code.slice(bodyStart, bodyEnd + 1); + } + + const implicitArrowMatch = declarationTail.search(/=>\s*\(\s*\{/); + if (implicitArrowMatch !== -1) { + const braceStart = declarationTail.indexOf("{", implicitArrowMatch); + if (braceStart === -1) return null; + + const braceEnd = findMatchingBrace(declarationTail, braceStart); + if (braceEnd === -1) return null; + + return declarationTail.slice(0, braceEnd + 1); + } + + const blockBodyMatch = /=>\s*\{|(?:async\s+)?function\b[^{]*\{/.exec(declarationTail); + if (!blockBodyMatch) return null; + + const braceStart = declarationTail.indexOf("{", blockBodyMatch.index); + if (braceStart === -1) return null; + + const braceEnd = findMatchingBrace(declarationTail, braceStart); + if (braceEnd === -1) return null; + + return declarationTail.slice(braceStart, braceEnd + 1); +} + function findMatchingBrace(code: string, start: number): number { let depth = 0; let quote: '"' | "'" | "`" | null = null; diff --git a/tests/build-report.test.ts b/tests/build-report.test.ts index 89f470885..62f024b06 100644 --- a/tests/build-report.test.ts +++ b/tests/build-report.test.ts @@ -195,6 +195,17 @@ export async function getStaticProps() { expect(extractGetStaticPropsRevalidate(code)).toBe(60); }); + it("ignores revalidate in a function defined after getStaticProps", () => { + const code = `export function getStaticProps() { + return { props: {} }; +} + +export function unrelated() { + return { revalidate: 999 }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBeNull(); + }); + it("handles inline comment after value (fixture file style)", () => { // From tests/fixtures/pages-basic/pages/isr-test.tsx: // revalidate: 1, // Revalidate every 1 second From 98598e2ab9f3039158a0d4d80f98e10a73dc275a Mon Sep 17 00:00:00 2001 From: Boy Steven Benaya Aritonang Date: Fri, 20 Mar 2026 01:10:41 +0700 Subject: [PATCH 4/8] fix: handle destructured getStaticProps params --- packages/vinext/src/build/report.ts | 48 +++++++++++++++++++++++------ tests/build-report.test.ts | 14 +++++++++ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/vinext/src/build/report.ts b/packages/vinext/src/build/report.ts index ccf5d850a..a2b884f5e 100644 --- a/packages/vinext/src/build/report.ts +++ b/packages/vinext/src/build/report.ts @@ -180,13 +180,7 @@ function extractGetStaticPropsDeclaration( const declarationTail = code.slice(declarationStart); if (declarationText.includes("function getStaticProps")) { - const bodyStart = code.indexOf("{", declarationStart + declarationText.length); - if (bodyStart === -1) return null; - - const bodyEnd = findMatchingBrace(code, bodyStart); - if (bodyEnd === -1) return null; - - return code.slice(bodyStart, bodyEnd + 1); + return extractFunctionBody(code, declarationStart + declarationText.length); } const implicitArrowMatch = declarationTail.search(/=>\s*\(\s*\{/); @@ -200,7 +194,12 @@ function extractGetStaticPropsDeclaration( return declarationTail.slice(0, braceEnd + 1); } - const blockBodyMatch = /=>\s*\{|(?:async\s+)?function\b[^{]*\{/.exec(declarationTail); + const functionExpressionMatch = /(?:async\s+)?function\b/.exec(declarationTail); + if (functionExpressionMatch) { + return extractFunctionBody(declarationTail, functionExpressionMatch.index); + } + + const blockBodyMatch = /=>\s*\{/.exec(declarationTail); if (!blockBodyMatch) return null; const braceStart = declarationTail.indexOf("{", blockBodyMatch.index); @@ -212,7 +211,36 @@ function extractGetStaticPropsDeclaration( return declarationTail.slice(braceStart, braceEnd + 1); } +function extractFunctionBody(code: string, functionStart: number): string | null { + const paramsStart = code.indexOf("(", functionStart); + if (paramsStart === -1) return null; + + const paramsEnd = findMatchingParen(code, paramsStart); + if (paramsEnd === -1) return null; + + const bodyStart = code.indexOf("{", paramsEnd + 1); + if (bodyStart === -1) return null; + + const bodyEnd = findMatchingBrace(code, bodyStart); + if (bodyEnd === -1) return null; + + return code.slice(bodyStart, bodyEnd + 1); +} + function findMatchingBrace(code: string, start: number): number { + return findMatchingToken(code, start, "{", "}"); +} + +function findMatchingParen(code: string, start: number): number { + return findMatchingToken(code, start, "(", ")"); +} + +function findMatchingToken( + code: string, + start: number, + openToken: string, + closeToken: string, +): number { let depth = 0; let quote: '"' | "'" | "`" | null = null; let inLineComment = false; @@ -261,12 +289,12 @@ function findMatchingBrace(code: string, start: number): number { continue; } - if (char === "{") { + if (char === openToken) { depth++; continue; } - if (char === "}") { + if (char === closeToken) { depth--; if (depth === 0) return i; } diff --git a/tests/build-report.test.ts b/tests/build-report.test.ts index 62f024b06..87e0eacbf 100644 --- a/tests/build-report.test.ts +++ b/tests/build-report.test.ts @@ -206,6 +206,20 @@ export function unrelated() { expect(extractGetStaticPropsRevalidate(code)).toBeNull(); }); + it("extracts revalidate from a function declaration with destructured params", () => { + const code = `export async function getStaticProps({ params }) { + return { props: { slug: params?.slug ?? null }, revalidate: 60 }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBe(60); + }); + + it("extracts revalidate from a function expression with destructured params", () => { + const code = `export const getStaticProps = async function({ params }) { + return { props: { slug: params?.slug ?? null }, revalidate: 60 }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBe(60); + }); + it("handles inline comment after value (fixture file style)", () => { // From tests/fixtures/pages-basic/pages/isr-test.tsx: // revalidate: 1, // Revalidate every 1 second From 1a86e860d36e65e30594c02b482413a2bbc1b80a Mon Sep 17 00:00:00 2001 From: Boy Steven Benaya Aritonang Date: Fri, 20 Mar 2026 01:14:11 +0700 Subject: [PATCH 5/8] fix: ignore nested helper returns in getStaticProps --- packages/vinext/src/build/report.ts | 141 +++++++++++++++++++++++++--- tests/build-report.test.ts | 11 +++ 2 files changed, 139 insertions(+), 13 deletions(-) diff --git a/packages/vinext/src/build/report.ts b/packages/vinext/src/build/report.ts index a2b884f5e..ff73659a0 100644 --- a/packages/vinext/src/build/report.ts +++ b/packages/vinext/src/build/report.ts @@ -141,19 +141,9 @@ function extractGetStaticPropsReturnObjects(code: string): string[] | null { const declaration = extractGetStaticPropsDeclaration(code, declarationMatch); if (declaration === null) return []; - const returnObjects: string[] = []; - const returnPattern = /\breturn\s*\{/g; - let returnMatch: RegExpExecArray | null; - - while ((returnMatch = returnPattern.exec(declaration)) !== null) { - const braceStart = declaration.indexOf("{", returnMatch.index); - if (braceStart === -1) continue; - - const braceEnd = findMatchingBrace(declaration, braceStart); - if (braceEnd === -1) continue; - - returnObjects.push(declaration.slice(braceStart, braceEnd + 1)); - } + const returnObjects = declaration.trimStart().startsWith("{") + ? collectReturnObjectsFromFunctionBody(declaration) + : []; if (returnObjects.length > 0) return returnObjects; @@ -227,6 +217,131 @@ function extractFunctionBody(code: string, functionStart: number): string | null return code.slice(bodyStart, bodyEnd + 1); } +function collectReturnObjectsFromFunctionBody(code: string): string[] { + const returnObjects: string[] = []; + let quote: '"' | "'" | "`" | null = null; + let inLineComment = false; + let inBlockComment = false; + + for (let i = 0; i < code.length; i++) { + const char = code[i]; + const next = code[i + 1]; + + if (inLineComment) { + if (char === "\n") inLineComment = false; + continue; + } + + if (inBlockComment) { + if (char === "*" && next === "/") { + inBlockComment = false; + i++; + } + continue; + } + + if (quote) { + if (char === "\\") { + i++; + continue; + } + if (char === quote) quote = null; + continue; + } + + if (char === "/" && next === "/") { + inLineComment = true; + i++; + continue; + } + + if (char === "/" && next === "*") { + inBlockComment = true; + i++; + continue; + } + + if (char === '"' || char === "'" || char === "`") { + quote = char; + continue; + } + + if (matchesKeywordAt(code, i, "function")) { + const nestedBody = extractFunctionBody(code, i); + if (nestedBody !== null) { + i += nestedBody.length - 1; + } + continue; + } + + if (matchesKeywordAt(code, i, "class")) { + const classBody = extractClassBody(code, i); + if (classBody !== null) { + i += classBody.length - 1; + } + continue; + } + + if (char === "=" && next === ">") { + const nestedBody = extractArrowFunctionBody(code, i); + if (nestedBody !== null) { + i += nestedBody.length - 1; + } + continue; + } + + if (matchesKeywordAt(code, i, "return")) { + const braceStart = findNextNonWhitespaceIndex(code, i + "return".length); + if (braceStart === -1 || code[braceStart] !== "{") continue; + + const braceEnd = findMatchingBrace(code, braceStart); + if (braceEnd === -1) continue; + + returnObjects.push(code.slice(braceStart, braceEnd + 1)); + i = braceEnd; + } + } + + return returnObjects; +} + +function extractArrowFunctionBody(code: string, arrowIndex: number): string | null { + const bodyStart = findNextNonWhitespaceIndex(code, arrowIndex + 2); + if (bodyStart === -1 || code[bodyStart] !== "{") return null; + + const bodyEnd = findMatchingBrace(code, bodyStart); + if (bodyEnd === -1) return null; + + return code.slice(bodyStart, bodyEnd + 1); +} + +function extractClassBody(code: string, classStart: number): string | null { + const bodyStart = code.indexOf("{", classStart + "class".length); + if (bodyStart === -1) return null; + + const bodyEnd = findMatchingBrace(code, bodyStart); + if (bodyEnd === -1) return null; + + return code.slice(bodyStart, bodyEnd + 1); +} + +function findNextNonWhitespaceIndex(code: string, start: number): number { + for (let i = start; i < code.length; i++) { + if (!/\s/.test(code[i])) return i; + } + return -1; +} + +function matchesKeywordAt(code: string, index: number, keyword: string): boolean { + const before = index === 0 ? "" : code[index - 1]; + const after = code[index + keyword.length] ?? ""; + return ( + code.startsWith(keyword, index) && + (before === "" || !/[A-Za-z0-9_$]/.test(before)) && + (after === "" || !/[A-Za-z0-9_$]/.test(after)) + ); +} + function findMatchingBrace(code: string, start: number): number { return findMatchingToken(code, start, "{", "}"); } diff --git a/tests/build-report.test.ts b/tests/build-report.test.ts index 87e0eacbf..e17220e12 100644 --- a/tests/build-report.test.ts +++ b/tests/build-report.test.ts @@ -220,6 +220,17 @@ export function unrelated() { expect(extractGetStaticPropsRevalidate(code)).toBe(60); }); + it("ignores revalidate in a nested helper function inside getStaticProps", () => { + const code = `export function getStaticProps() { + const helper = () => { + return { revalidate: 999 }; + }; + + return { props: {} }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBeNull(); + }); + it("handles inline comment after value (fixture file style)", () => { // From tests/fixtures/pages-basic/pages/isr-test.tsx: // revalidate: 1, // Revalidate every 1 second From 8942db5cc745229d0fdb5e704db00135a2e1823f Mon Sep 17 00:00:00 2001 From: Boy Steven Benaya Aritonang Date: Fri, 20 Mar 2026 01:16:17 +0700 Subject: [PATCH 6/8] fix: avoid re-export false positives in getStaticProps parsing --- packages/vinext/src/build/report.ts | 10 +++++++++- tests/build-report.test.ts | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/vinext/src/build/report.ts b/packages/vinext/src/build/report.ts index ff73659a0..a27341ba5 100644 --- a/packages/vinext/src/build/report.ts +++ b/packages/vinext/src/build/report.ts @@ -136,7 +136,15 @@ function extractGetStaticPropsReturnObjects(code: string): string[] | null { /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+getStaticProps\b|(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+getStaticProps\b/.exec( code, ); - if (!declarationMatch) return null; + if (!declarationMatch) { + // A file can re-export getStaticProps from another module without defining + // it locally. In that case we can't safely infer revalidate from this file, + // so skip the whole-file fallback to avoid unrelated false positives. + if (/(?:^|\n)\s*export\s*\{[^}]*\bgetStaticProps\b[^}]*\}\s*from\b/.test(code)) { + return []; + } + return null; + } const declaration = extractGetStaticPropsDeclaration(code, declarationMatch); if (declaration === null) return []; diff --git a/tests/build-report.test.ts b/tests/build-report.test.ts index e17220e12..d3505af1b 100644 --- a/tests/build-report.test.ts +++ b/tests/build-report.test.ts @@ -231,6 +231,14 @@ export function unrelated() { expect(extractGetStaticPropsRevalidate(code)).toBeNull(); }); + it("ignores unrelated revalidate when getStaticProps is re-exported from another file", () => { + const code = `const defaults = { revalidate: 30 }; + +export { getStaticProps } from "./shared"; +`; + expect(extractGetStaticPropsRevalidate(code)).toBeNull(); + }); + it("handles inline comment after value (fixture file style)", () => { // From tests/fixtures/pages-basic/pages/isr-test.tsx: // revalidate: 1, // Revalidate every 1 second From 4280a136fed97c49378a7c9d80ccdf19b90e8ff3 Mon Sep 17 00:00:00 2001 From: Boy Steven Benaya Aritonang Date: Fri, 20 Mar 2026 02:33:20 +0700 Subject: [PATCH 7/8] fix: harden getStaticProps revalidate parsing --- packages/vinext/src/build/report.ts | 262 +++++++++++++++++++++++----- tests/build-report.test.ts | 71 ++++++++ 2 files changed, 289 insertions(+), 44 deletions(-) diff --git a/packages/vinext/src/build/report.ts b/packages/vinext/src/build/report.ts index a27341ba5..12d8da982 100644 --- a/packages/vinext/src/build/report.ts +++ b/packages/vinext/src/build/report.ts @@ -115,11 +115,8 @@ export function extractGetStaticPropsRevalidate(code: string): number | false | if (returnObjects) { for (const searchSpace of returnObjects) { - const m = re.exec(searchSpace); - if (!m) continue; - if (m[1] === "false") return false; - if (m[1] === "Infinity") return Infinity; - return parseFloat(m[1]); + const revalidate = extractTopLevelRevalidateValue(searchSpace); + if (revalidate !== null) return revalidate; } return null; } @@ -131,6 +128,110 @@ export function extractGetStaticPropsRevalidate(code: string): number | false | return parseFloat(m[1]); } +function extractTopLevelRevalidateValue(code: string): number | false | null { + let braceDepth = 0; + let parenDepth = 0; + let bracketDepth = 0; + let quote: '"' | "'" | "`" | null = null; + let inLineComment = false; + let inBlockComment = false; + + for (let i = 0; i < code.length; i++) { + const char = code[i]; + const next = code[i + 1]; + + if (inLineComment) { + if (char === "\n") inLineComment = false; + continue; + } + + if (inBlockComment) { + if (char === "*" && next === "/") { + inBlockComment = false; + i++; + } + continue; + } + + if (quote) { + if (char === "\\") { + i++; + continue; + } + if (char === quote) quote = null; + continue; + } + + if (char === "/" && next === "/") { + inLineComment = true; + i++; + continue; + } + + if (char === "/" && next === "*") { + inBlockComment = true; + i++; + continue; + } + + if (char === '"' || char === "'" || char === "`") { + quote = char; + continue; + } + + if (char === "{") { + braceDepth++; + continue; + } + + if (char === "}") { + braceDepth--; + continue; + } + + if (char === "(") { + parenDepth++; + continue; + } + + if (char === ")") { + parenDepth--; + continue; + } + + if (char === "[") { + bracketDepth++; + continue; + } + + if (char === "]") { + bracketDepth--; + continue; + } + + if ( + braceDepth === 1 && + parenDepth === 0 && + bracketDepth === 0 && + matchesKeywordAt(code, i, "revalidate") + ) { + const colonIndex = findNextNonWhitespaceIndex(code, i + "revalidate".length); + if (colonIndex === -1 || code[colonIndex] !== ":") continue; + + const valueStart = findNextNonWhitespaceIndex(code, colonIndex + 1); + if (valueStart === -1) return null; + + const valueMatch = /^(-?\d+(?:\.\d+)?|Infinity|false)\b/.exec(code.slice(valueStart)); + if (!valueMatch) return null; + if (valueMatch[1] === "false") return false; + if (valueMatch[1] === "Infinity") return Infinity; + return parseFloat(valueMatch[1]); + } + } + + return null; +} + function extractGetStaticPropsReturnObjects(code: string): string[] | null { const declarationMatch = /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+getStaticProps\b|(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+getStaticProps\b/.exec( @@ -181,35 +282,38 @@ function extractGetStaticPropsDeclaration( return extractFunctionBody(code, declarationStart + declarationText.length); } - const implicitArrowMatch = declarationTail.search(/=>\s*\(\s*\{/); - if (implicitArrowMatch !== -1) { - const braceStart = declarationTail.indexOf("{", implicitArrowMatch); + const functionExpressionMatch = /(?:async\s+)?function\b/.exec(declarationTail); + if (functionExpressionMatch) { + return extractFunctionBody(declarationTail, functionExpressionMatch.index); + } + + const blockBodyMatch = /=>\s*\{/.exec(declarationTail); + if (blockBodyMatch) { + const braceStart = declarationTail.indexOf("{", blockBodyMatch.index); if (braceStart === -1) return null; const braceEnd = findMatchingBrace(declarationTail, braceStart); if (braceEnd === -1) return null; - return declarationTail.slice(0, braceEnd + 1); - } - - const functionExpressionMatch = /(?:async\s+)?function\b/.exec(declarationTail); - if (functionExpressionMatch) { - return extractFunctionBody(declarationTail, functionExpressionMatch.index); + return declarationTail.slice(braceStart, braceEnd + 1); } - const blockBodyMatch = /=>\s*\{/.exec(declarationTail); - if (!blockBodyMatch) return null; + const implicitArrowMatch = declarationTail.search(/=>\s*\(\s*\{/); + if (implicitArrowMatch === -1) return null; - const braceStart = declarationTail.indexOf("{", blockBodyMatch.index); - if (braceStart === -1) return null; + const implicitBraceStart = declarationTail.indexOf("{", implicitArrowMatch); + if (implicitBraceStart === -1) return null; - const braceEnd = findMatchingBrace(declarationTail, braceStart); - if (braceEnd === -1) return null; + const implicitBraceEnd = findMatchingBrace(declarationTail, implicitBraceStart); + if (implicitBraceEnd === -1) return null; - return declarationTail.slice(braceStart, braceEnd + 1); + return declarationTail.slice(0, implicitBraceEnd + 1); } function extractFunctionBody(code: string, functionStart: number): string | null { + const bodyEnd = findFunctionBodyEnd(code, functionStart); + if (bodyEnd === -1) return null; + const paramsStart = code.indexOf("(", functionStart); if (paramsStart === -1) return null; @@ -219,9 +323,6 @@ function extractFunctionBody(code: string, functionStart: number): string | null const bodyStart = code.indexOf("{", paramsEnd + 1); if (bodyStart === -1) return null; - const bodyEnd = findMatchingBrace(code, bodyStart); - if (bodyEnd === -1) return null; - return code.slice(bodyStart, bodyEnd + 1); } @@ -275,29 +376,35 @@ function collectReturnObjectsFromFunctionBody(code: string): string[] { } if (matchesKeywordAt(code, i, "function")) { - const nestedBody = extractFunctionBody(code, i); - if (nestedBody !== null) { - i += nestedBody.length - 1; + const nestedBodyEnd = findFunctionBodyEnd(code, i); + if (nestedBodyEnd !== -1) { + i = nestedBodyEnd; } continue; } if (matchesKeywordAt(code, i, "class")) { - const classBody = extractClassBody(code, i); - if (classBody !== null) { - i += classBody.length - 1; + const classBodyEnd = findClassBodyEnd(code, i); + if (classBodyEnd !== -1) { + i = classBodyEnd; } continue; } if (char === "=" && next === ">") { - const nestedBody = extractArrowFunctionBody(code, i); - if (nestedBody !== null) { - i += nestedBody.length - 1; + const nestedBodyEnd = findArrowFunctionBodyEnd(code, i); + if (nestedBodyEnd !== -1) { + i = nestedBodyEnd; } continue; } + const methodBodyEnd = findObjectMethodBodyEnd(code, i); + if (methodBodyEnd !== -1) { + i = methodBodyEnd; + continue; + } + if (matchesKeywordAt(code, i, "return")) { const braceStart = findNextNonWhitespaceIndex(code, i + "return".length); if (braceStart === -1 || code[braceStart] !== "{") continue; @@ -313,24 +420,91 @@ function collectReturnObjectsFromFunctionBody(code: string): string[] { return returnObjects; } -function extractArrowFunctionBody(code: string, arrowIndex: number): string | null { - const bodyStart = findNextNonWhitespaceIndex(code, arrowIndex + 2); - if (bodyStart === -1 || code[bodyStart] !== "{") return null; +function findFunctionBodyEnd(code: string, functionStart: number): number { + const paramsStart = code.indexOf("(", functionStart); + if (paramsStart === -1) return -1; - const bodyEnd = findMatchingBrace(code, bodyStart); - if (bodyEnd === -1) return null; + const paramsEnd = findMatchingParen(code, paramsStart); + if (paramsEnd === -1) return -1; - return code.slice(bodyStart, bodyEnd + 1); + const bodyStart = code.indexOf("{", paramsEnd + 1); + if (bodyStart === -1) return -1; + + return findMatchingBrace(code, bodyStart); } -function extractClassBody(code: string, classStart: number): string | null { +function findClassBodyEnd(code: string, classStart: number): number { const bodyStart = code.indexOf("{", classStart + "class".length); - if (bodyStart === -1) return null; + if (bodyStart === -1) return -1; - const bodyEnd = findMatchingBrace(code, bodyStart); - if (bodyEnd === -1) return null; + return findMatchingBrace(code, bodyStart); +} - return code.slice(bodyStart, bodyEnd + 1); +function findArrowFunctionBodyEnd(code: string, arrowIndex: number): number { + const bodyStart = findNextNonWhitespaceIndex(code, arrowIndex + 2); + if (bodyStart === -1 || code[bodyStart] !== "{") return -1; + + return findMatchingBrace(code, bodyStart); +} + +function findObjectMethodBodyEnd(code: string, start: number): number { + let i = start; + + if (matchesKeywordAt(code, i, "async")) { + const afterAsync = findNextNonWhitespaceIndex(code, i + "async".length); + if (afterAsync === -1) return -1; + if (code[afterAsync] !== "(") { + i = afterAsync; + } + } + + if (code[i] === "*") { + i = findNextNonWhitespaceIndex(code, i + 1); + if (i === -1) return -1; + } + + if (!/[A-Za-z_$]/.test(code[i] ?? "")) return -1; + + const nameStart = i; + while (/[A-Za-z0-9_$]/.test(code[i] ?? "")) i++; + const name = code.slice(nameStart, i); + + if ( + name === "if" || + name === "for" || + name === "while" || + name === "switch" || + name === "catch" || + name === "function" || + name === "return" || + name === "const" || + name === "let" || + name === "var" || + name === "new" + ) { + return -1; + } + + if (name === "get" || name === "set") { + const afterAccessor = findNextNonWhitespaceIndex(code, i); + if (afterAccessor === -1) return -1; + if (code[afterAccessor] !== "(") { + i = afterAccessor; + if (!/[A-Za-z_$]/.test(code[i] ?? "")) return -1; + while (/[A-Za-z0-9_$]/.test(code[i] ?? "")) i++; + } + } + + const paramsStart = findNextNonWhitespaceIndex(code, i); + if (paramsStart === -1 || code[paramsStart] !== "(") return -1; + + const paramsEnd = findMatchingParen(code, paramsStart); + if (paramsEnd === -1) return -1; + + const bodyStart = findNextNonWhitespaceIndex(code, paramsEnd + 1); + if (bodyStart === -1 || code[bodyStart] !== "{") return -1; + + return findMatchingBrace(code, bodyStart); } function findNextNonWhitespaceIndex(code: string, start: number): number { diff --git a/tests/build-report.test.ts b/tests/build-report.test.ts index d3505af1b..82abce9c6 100644 --- a/tests/build-report.test.ts +++ b/tests/build-report.test.ts @@ -231,6 +231,77 @@ export function unrelated() { expect(extractGetStaticPropsRevalidate(code)).toBeNull(); }); + it("ignores revalidate in a nested named function inside getStaticProps", () => { + const code = `export function getStaticProps() { + function helper(paramOne, paramTwo, paramThree, paramFour, paramFive) { + return { revalidate: 999 }; + } + + return { props: {} }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBeNull(); + }); + + it("ignores revalidate in a nested implicit-arrow helper inside block-body getStaticProps", () => { + const code = `export const getStaticProps = async () => { + const helper = () => ({ revalidate: 999 }); + + return { props: {} }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBeNull(); + }); + + it("ignores revalidate in a nested implicit-arrow helper inside function-expression getStaticProps", () => { + const code = `export const getStaticProps = async function() { + const helper = () => ({ revalidate: 999 }); + + return { props: {} }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBeNull(); + }); + + it("ignores revalidate nested inside props data", () => { + const code = `export async function getStaticProps() { + return { + props: { + config: { + revalidate: 999, + }, + }, + }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBeNull(); + }); + + it("ignores revalidate in an object-method helper inside getStaticProps", () => { + const code = `export function getStaticProps() { + const helper = { + build() { + return { revalidate: 999 }; + }, + }; + + return { props: {} }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBeNull(); + }); + + it("ignores revalidate in object-method helpers named get and async", () => { + const code = `export function getStaticProps() { + const helper = { + get() { + return { revalidate: 999 }; + }, + async() { + return { revalidate: 998 }; + }, + }; + + return { props: {} }; +}`; + expect(extractGetStaticPropsRevalidate(code)).toBeNull(); + }); + it("ignores unrelated revalidate when getStaticProps is re-exported from another file", () => { const code = `const defaults = { revalidate: 30 }; From f3d2a6081882aa97afc711fdbe54ecc7bb718c7d Mon Sep 17 00:00:00 2001 From: Boy Steven Benaya Aritonang Date: Fri, 20 Mar 2026 03:22:15 +0700 Subject: [PATCH 8/8] chore: polish getStaticProps parser follow-up --- packages/vinext/src/build/report.ts | 19 +++++++++++++------ tests/build-report.test.ts | 2 ++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/vinext/src/build/report.ts b/packages/vinext/src/build/report.ts index 12d8da982..b63deaf42 100644 --- a/packages/vinext/src/build/report.ts +++ b/packages/vinext/src/build/report.ts @@ -110,7 +110,6 @@ export function extractExportConstNumber(code: string, name: string): number | n * null — no `revalidate` key found (fully static) */ export function extractGetStaticPropsRevalidate(code: string): number | false | null { - const re = /\brevalidate\s*:\s*(-?\d+(?:\.\d+)?|Infinity|false)\b/; const returnObjects = extractGetStaticPropsReturnObjects(code); if (returnObjects) { @@ -121,7 +120,7 @@ export function extractGetStaticPropsRevalidate(code: string): number | false | return null; } - const m = re.exec(code); + const m = /\brevalidate\s*:\s*(-?\d+(?:\.\d+)?|Infinity|false)\b/.exec(code); if (!m) return null; if (m[1] === "false") return false; if (m[1] === "Infinity") return Infinity; @@ -399,10 +398,18 @@ function collectReturnObjectsFromFunctionBody(code: string): string[] { continue; } - const methodBodyEnd = findObjectMethodBodyEnd(code, i); - if (methodBodyEnd !== -1) { - i = methodBodyEnd; - continue; + if ( + (char >= "A" && char <= "Z") || + (char >= "a" && char <= "z") || + char === "_" || + char === "$" || + char === "*" + ) { + const methodBodyEnd = findObjectMethodBodyEnd(code, i); + if (methodBodyEnd !== -1) { + i = methodBodyEnd; + continue; + } } if (matchesKeywordAt(code, i, "return")) { diff --git a/tests/build-report.test.ts b/tests/build-report.test.ts index 82abce9c6..ed2114d62 100644 --- a/tests/build-report.test.ts +++ b/tests/build-report.test.ts @@ -145,6 +145,8 @@ describe("extractGetStaticPropsRevalidate", () => { expect(extractGetStaticPropsRevalidate(code)).toBe(60); }); + // These bare return-object cases intentionally exercise the whole-file + // fallback path used when no local getStaticProps declaration is present. it("extracts revalidate: 0 (treat as SSR)", () => { const code = `return { props: {}, revalidate: 0 };`; expect(extractGetStaticPropsRevalidate(code)).toBe(0);