fix(build): strip only the matching declarator in combined export const (#1972) - #2065
fix(build): strip only the matching declarator in combined export const (#1972)#2065Divkix wants to merge 1 commit into
Conversation
…st (cloudflare#1972) stripServerExports overwrote the whole `export const …;` statement per declarator match, deleting sibling bindings and collapsing two server exports in one declaration to a single stub. Overwrite only the matched declarator's own range so siblings survive and each server export is stubbed independently.
commit: |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Reviewed the fix and ran the targeted suite (vp test run tests/build-optimization.test.ts -t "stripServerExports" — 22 passed). The change is correct, minimal, and matches Next.js's next_ssg.rs / Babel behavior of pruning only the matched declarator.
I verified the key cases directly against the transform:
export const foo = 2, getStaticProps = …→export const foo = 2, getStaticProps = undefined;(sibling preserved)- two server exports in one statement → both stubbed independently, no comma corruption
export const { slug } = params, getStaticProps = …→ destructuring sibling preserved- single-declarator case → byte-for-byte identical output (
export const getStaticProps = undefined;), so the existing test at line 2772 still holds - declaration kind preserved (
export let getStaticPaths = undefined)
The rationale comment is excellent — it captures both failure modes and the MagicString last-write-wins gotcha, which will save future readers a lot of time.
Two non-blocking observations below; neither needs to be addressed in this PR. Looks good to merge.
| for (const declarator of decl.declarations) { | ||
| if (declarator.id?.type === "Identifier" && SERVER_EXPORTS.has(declarator.id.name)) { | ||
| s.overwrite(node.start, node.end, `export const ${declarator.id.name} = undefined;`); | ||
| s.overwrite(declarator.start, declarator.end, `${declarator.id.name} = undefined`); |
There was a problem hiding this comment.
Behavioral note (non-blocking, matches Next.js so no change needed): if a sibling references the stripped declarator in the same statement — e.g. export const getStaticProps = 1, bar = getStaticProps; — bar now resolves to undefined rather than its original value. This mirrors Next.js's SSG transform (the binding is stubbed, not just the export), and such intra-statement references to a server data export are exotic, so this is the correct behavior. Flagging only for awareness.
| // declarator, preserving the sibling. Ported from Next.js | ||
| // test/unit/babel-plugin-next-ssg-transform.test.ts ('should not remove extra | ||
| // named export variable declarations'). | ||
| it("preserves sibling bindings in a combined export const declaration", () => { |
There was a problem hiding this comment.
Optional: the three new tests assert result contains substrings like foo = 2 and getStaticProps = undefined, which is good, but they don't pin the exact comma-joined output. Since the original bug was specifically about MagicString collapsing/dropping declarators across commas, a single exact-string assertion on the rewritten statement (e.g. expect(result).toContain("export const foo = 2, getStaticProps = undefined;")) would lock in that the comma separators survive intact. The parseAst round-trip already guards against syntactic corruption, so this is purely a nice-to-have for regression precision.
|
Review posted successfully. Here's my assessment of PR #2065: SummaryThe fix is correct, minimal, and well-tested. It addresses a genuine vinext-only parity defect in The bug: When a server export (
The fix: Overwrite only the matched declarator's own VerificationI ran the transform directly against the relevant cases and ran the targeted suite (22 passed):
Feedback posted
The PR is ready to merge. |
|
I'm going to close this as the plugin has been significantly changed via #2055 |
Problem
Fixes #1972.
stripServerExports(the client-bundle transform that removes server-only data exports —getServerSideProps/getStaticProps/getStaticPaths) had a correctness bug in itsVariableDeclarationbranch. When a server export was one declarator inside a multi-declaratorexport const …statement, the code overwrote the entire statement with a single stub. Two failures resulted:export const myData = 42, getStaticProps = …becameexport const getStaticProps = undefined;—myDatavanished from the client bundle, producingmyData is not definedat hydration.export const getStaticProps = …, getStaticPaths = …calleds.overwrite(node.start, node.end, …)twice on the identical range; MagicString 0.30.21 silently keeps only the last write, emitting a single stub.Next.js handles this correctly — its SWC transform (
next_ssg.rs) marks only the matched declarator's name asPat::Invalidthen prunes it withdecls.retain(...), keeping siblings, and the legacy Babel plugin calls per-declaratord.remove(). Next.js even ships a dedicated fixture (should-not-remove-extra-named-export-variable-declarations). So this was a vinext-only parity defect.Fix
Overwrite only the matched declarator's own range (
id = init) instead of the whole statement:Declarator ranges are disjoint and exclude the
const/let/varkeyword and the separating commas, so each match is rewritten independently — siblings (including destructuring patterns, whoseidis not anIdentifier) are preserved, and two server exports in one declaration each get their own stub with no overlapping write. For the single-declarator case the output is byte-for-byte identical to before, so existing behavior and tests are unchanged. As a bonus the declaration kind is now preserved (export let getStaticPathsno longer becomesconst).Tests
Three regression tests added to the
stripServerExportsblock intests/build-optimization.test.ts(the first ports Next.jsbabel-plugin-next-ssg-transform.test.ts"should not remove extra named export variable declarations"):export constAll confirmed failing before the fix and passing after. Full unit suite passes (
vp test run --project unit), andvp run build && vp run checkis clean.