From ab9f8df1b72dae62584d72dcde2acfe3cc9bcfc6 Mon Sep 17 00:00:00 2001 From: Oleksii Bulenok Date: Mon, 31 Aug 2026 15:47:21 +0200 Subject: [PATCH] do not rewrite assignments --- .../babel-plugin-lexicon-leaf-imports.test.js | 28 +++++++++++++++++++ plugins/babel-plugin-lexicon-leaf-imports.js | 27 ++++++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/plugins/__tests__/babel-plugin-lexicon-leaf-imports.test.js b/plugins/__tests__/babel-plugin-lexicon-leaf-imports.test.js index 93e9d22011..8e68c7ec6d 100644 --- a/plugins/__tests__/babel-plugin-lexicon-leaf-imports.test.js +++ b/plugins/__tests__/babel-plugin-lexicon-leaf-imports.test.js @@ -78,6 +78,34 @@ describe('transform', () => { expect(out).not.toContain('import *') }) + test('bails when the chain is a write target', () => { + const writes = [ + 'app.bsky.feed.like = 1', + 'app.bsky.feed.like++', + 'delete app.bsky.feed.like', + 'for (app.bsky.feed.like of []) {}', + ';[app.bsky.feed.like] = []', + ';({x: app.bsky.feed.like} = {})', + ] + for (const stmt of writes) { + const out = applyPlugin( + `import {app} from './lexicons'\n${stmt}\n`, + PROBE_FILE, + ) + expect(out).toContain(`from './lexicons'`) + expect(out).not.toContain('import *') + } + }) + + test('rewrites a read of a leaf even when a sibling member is written', () => { + const out = applyPlugin( + `import {app} from './lexicons'\ndelete app.bsky.feed.like.$cached\n`, + PROBE_FILE, + ) + expect(out).toContain('delete _lex_app_bsky_feed_like.$cached') + expect(out).not.toContain(`from './lexicons'`) + }) + test('leaves type-only imports untouched', () => { const src = `import type {app} from './lexicons'\nexport type T = typeof app\n` const out = applyPlugin(src, PROBE_FILE) diff --git a/plugins/babel-plugin-lexicon-leaf-imports.js b/plugins/babel-plugin-lexicon-leaf-imports.js index faf9fb6382..5725d5c462 100644 --- a/plugins/babel-plugin-lexicon-leaf-imports.js +++ b/plugins/babel-plugin-lexicon-leaf-imports.js @@ -21,8 +21,8 @@ * which imports its own copy of the barrel via '../lexicons/index.js'. * * Correctness fallback: if any reference to a barrel binding cannot be rewritten - * (namespace used as a value, computed access, chain ending at a non-leaf), that - * binding is left on the barrel import. The result is always correct, merely + * (namespace used as a value, computed access, chain ending at a non-leaf, + * chain in a write position), that binding is left on the barrel import. The result is always correct, merely * unshaken for that file. Set BSKY_LEXICON_IMPORTS_DEBUG=1 to log such bails. * * Caveat: the rewrite bakes leaf file paths into each consumer's transform @@ -96,6 +96,28 @@ function leafFileFor(dir, segment) { return null } +/** + * True when the member chain is written to rather than read. Such a chain + * cannot be collapsed into a bare identifier: imports are read-only bindings + * (assignment/++ would throw where the original property write may not) and + * `delete ` is a strict-mode SyntaxError in the emitted code. + */ +function isWriteTarget(memberPath) { + const parent = memberPath.parentPath + if (!parent) return false + if (parent.isAssignmentExpression()) { + return parent.node.left === memberPath.node + } + if (parent.isUpdateExpression()) return true + if (parent.isUnaryExpression({operator: 'delete'})) return true + if (parent.isForXStatement()) return parent.node.left === memberPath.node + if (parent.isArrayPattern() || parent.isRestElement()) return true + if (parent.isObjectProperty() && parent.parentPath.isObjectPattern()) { + return parent.node.value === memberPath.node + } + return false +} + /** `rootDir\0segments\0leafFile` -> boolean */ const chainCache = new Map() @@ -208,6 +230,7 @@ module.exports = function lexiconLeafImports(babel, options = {}) { for (;;) { const kind = classify(dir, segment) if (kind === 'file') { + if (isWriteTarget(cur)) return null return {memberPath: cur, leafFile: leafFileFor(dir, segment), segments} } if (kind !== 'dir') return null