Preserve native destructuring for CommonJS exports to keep iterator semantics#3705
Preserve native destructuring for CommonJS exports to keep iterator semantics#3705Copilot wants to merge 8 commits into
Conversation
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/93212f5b-14c8-4376-8039-d01e510fd47c Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
|
This does what it says on the tin, but, I'm not sure what affect this has on @blickly Is this the change you were hoping to get? |
|
Honestly, I was expecting something more like: but this version is technically not wrong, and is definitely and improvement over the old behavior. |
|
Yeah. I think what you've suggested would be better just because it avoids the lexer problem. @copilot please try |
…ibility Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/567139d4-d663-4e26-bad2-33690c892e51 Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Reworked in 4a6c647 to emit the lexer-friendly form. For const [A, V] = foo();
exports.A = A, exports.V = V;The variable declaration is preserved as-is (so array destructuring keeps native iterator semantics), and |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Updates CommonJS module emit to preserve native destructuring for exported binding patterns (especially arrays) so iterator semantics aren’t lost, while still emitting explicit exports.X = X assignments for tooling compatibility.
Changes:
- Preserve exported binding-pattern variable declarations (instead of flattening to index/property access) and emit per-identifier
exports.X = Xassignments. - Add a new helper to walk binding patterns (nesting/rest/defaults/omissions) and generate the export assignments with suppressed synthetic comment/source-map metadata.
- Add a regression test and update multiple compiler baselines to reflect the new emit shape.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/transformers/moduletransforms/commonjsmodule.go | Preserves exported destructuring declarations and adds helper to emit exports.X = X for each bound identifier. |
| testdata/tests/cases/compiler/exportDestructuringIterator.ts | New test case covering exported destructuring patterns under module=commonjs. |
| testdata/baselines/reference/compiler/exportDestructuringIterator.js | New baseline verifying native destructuring + explicit export assignments. |
| testdata/baselines/reference/compiler/exportDestructuringIterator.types | New type baseline for the added test. |
| testdata/baselines/reference/compiler/exportDestructuringIterator.symbols | New symbol baseline for the added test. |
| testdata/baselines/reference/compiler/exportDestructuring.js | Baseline update reflecting the new emit for exported array destructuring. |
| testdata/baselines/reference/submodule/compiler/exportObjectRest(module=commonjs,target=esnext).js | Baseline update to preserve native object rest destructuring + explicit export assignments. |
| testdata/baselines/reference/submodule/compiler/exportObjectRest(module=commonjs,target=esnext).js.diff | Diff baseline capturing the change in output shape. |
| testdata/baselines/reference/submodule/compiler/exportEmptyObjectBindingPattern(module=commonjs,target=esnext).js | Baseline update to emit native empty object destructuring. |
| testdata/baselines/reference/submodule/compiler/exportEmptyObjectBindingPattern(module=commonjs,target=esnext).js.diff | Diff baseline capturing the change in output shape. |
| testdata/baselines/reference/submodule/compiler/exportEmptyArrayBindingPattern(module=commonjs,target=esnext).js | Baseline update to emit native empty array destructuring. |
| testdata/baselines/reference/submodule/compiler/exportEmptyArrayBindingPattern(module=commonjs,target=esnext).js.diff | Diff baseline capturing the change in output shape. |
| testdata/baselines/reference/submodule/compiler/downlevelLetConst13(target=es2015).js | Baseline update for exported destructuring emit now producing locals + export assignments. |
| testdata/baselines/reference/submodule/compiler/downlevelLetConst13(target=es2015).js.diff | Diff baseline capturing the change in output shape. |
| testdata/baselines/reference/submodule/compiler/destructuringInVariableDeclarations1.js | Baseline update for exported destructuring emit now producing locals + export assignment. |
| testdata/baselines/reference/submodule/compiler/destructuringInVariableDeclarations1.js.diff | Diff baseline capturing the change in output shape. |
| testdata/baselines/reference/submodule/compiler/declarationEmitRetainsJsdocyComments.js | Baseline update reflecting preserved binding-pattern declaration with JSDoc-like comment placement. |
| testdata/baselines/reference/submodule/compiler/declarationEmitRetainsJsdocyComments.js.diff | Diff baseline capturing the change in output shape. |
| testdata/baselines/reference/submodule/compiler/bindingPatternOmittedExpressionNesting.js | Baseline update reflecting preserved native destructuring for a pattern consisting only of omissions/nesting. |
| testdata/baselines/reference/submodule/compiler/bindingPatternOmittedExpressionNesting.js.diff | Diff baseline capturing the change in output shape. |
|
@copilot please consider the review comments |
…nd test coverage Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/d6e032e2-fc6a-4294-9dab-57025399232b Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Addressed all three review comments in de75861:
|
|
@copilot isn't it a bad idea to reuse the same node twice in the tree? are you sure you've done this correctly? |
Good catch to double-check — the binding pattern node itself is only placed at one spot in the emitted tree (as the The reason for sharing |
| func (tx *CommonJSModuleTransformer) appendExportAssignmentsForBindingPattern(name *ast.Node, pushExpression func(*ast.Expression)) { | ||
| for _, element := range name.Elements() { | ||
| if ast.IsOmittedExpression(element) { | ||
| continue | ||
| } | ||
| bindingName := element.Name() |
| // local binding (`y`). Identifiers that have additional export specifiers (e.g. `export { y as z }` | ||
| // elsewhere) are left to `appendExportsOfVariableStatement`, which emits the alias via the export | ||
| // specifier map. |
| let { | ||
| /** | ||
| * comment5 | ||
| */ | ||
| exports.someMethod = null.someMethod; | ||
| someMethod } = null; | ||
| exports.someMethod = someMethod; |
…submoduleAccepted Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/da1644b8-c27c-4ddf-b063-125d398754f7 Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Addressed in 81c74ea:
(The trailing-whitespace baseline comment from the reviewer is a pre-existing printer behavior, not something introduced by this PR.) |
| // rewrites on binding names stay consistent between the two. | ||
| visitedName := tx.Visitor().VisitNode(v.Name()) | ||
|
|
||
| pushVariable(tx.Factory().NewVariableDeclaration( |
There was a problem hiding this comment.
I'm like 80% sure this should be Update instead of New - actually, hold on, nothing changes from the transform here (quite literally we're using the variable declaration verbatim now), so all this peeling apart and putting back together is extraneous. It should just be pushVariable(v) followed by a tx.appendExportAssignmentsForBindingPattern(v.Name(), pushExpression), no?
There was a problem hiding this comment.
Yeah, it could (the PR evolved so this I think is leftover).
@copilot please address :)
There was a problem hiding this comment.
Simplified in 6134812 — now just pushVariable(variable) followed by tx.appendExportAssignmentsForBindingPattern(v.Name(), pushExpression). The reconstruction with NewVariableDeclaration + VisitNode is removed.
…structing it Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/383e98b6-360f-4055-8133-f773e16d1802 Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
|
I mentioned this online, but I'll include it here for posterity - transforming const [A, B] = whatever
exports.A = A, exports.B = Bdoes look nicer, but it breaks export visibility timing compared to the current (array-assuming) emit. So if import * as self from "./test.js"
const obj = { [Symbol.iterator]: function* () { yield "ok"; yield self.A; } }
export const [A, B] = objboth I don't know immediately how to transform it both to respect the iterator and the export timing without being very verbose with converting the array destructuring to an iterator you manually |
|
If the goal is to follow both the iterator protocol and export ordering, the original That said, I question if we need to hold to that constraint here. No argument from me that the suggested emit isn't fully ES spec-compliant, but we don't need to fix that here. Getting TSGo to parity with TS6 is the goal, and TS6 already has the exact same timing caveats when run with Also, I'm not sure I followed exactly what you meant by "compared to the current (array-assuming) emit." The array-assuming version seems even more broken, since it doesn't support iterators at all, much less iterators that depend on interleaving iteration with export visibility. |
|
I think another option is to do: [exports.A, exports.B] = foo()
0 && exports.A = void 0;
0 && exports.B = void 0;Or something, which is somewhat similar to what esbuild does to "declare" exports: https://jakebailey.dev/esbuild-playground/#v3=PTAEAEDMEsBsFMB2BDAtvAXKAxge0TAOYB0AVgM74BQA3laKAERIAuATgJ4AKu0iL5RlgDajYHwAm8AB7EBjALoAaek0i42qZCyFNsFRioaMADrG3rNuxolxTGVAL5UqICDAQp0WSTLnkqGRMNFlBIAFdEbBZofFBYXEIACgBKUDoGPERKBGIE5MYAd1xcRhSnKiA |
|
Wait, we don't need any of this, we already emit a big I think we can just do a simple destructuring like |
…tterns Assign directly to exports.X in destructuring patterns instead of flattening to array indexing. This preserves iterator semantics and correct export visibility timing. The void 0 prologue already satisfies cjs-module-lexer, so no additional hints are needed.
…tructuring-transpilation # Conflicts: # testdata/submoduleAccepted.txt
v.Name()once and reuse the visited node for both the preserved declaration and the generatedexports.X = Xassignments, so any visitor rewrites stay consistent.EFNoSourceMapto the syntheticpropertyAccessso it actually matches the stated intent of suppressing comments and source-map ranges on the synthetic export reference.exportDestructuringIterator.tswith: an array pattern with omitted elements ([, e, , f]), a nested array-of-array/object pattern ([[g, h], { i, j: k }]), and a nested object-with-nested-pattern ({ m: [n, o], p: { q } }) to cover the recursion and omitted-element paths ofappendExportAssignmentsForBindingPattern.nameparameter topatterninappendExportAssignmentsForBindingPatternmodule=commonjs, Array destructuring is transpiled with wrong (non-iterator) behavior #3701pushVariable(variable)instead of reconstructing withNewVariableDeclaration