This commit is contained in:
Oleksii Bulenok
2026-08-28 18:44:02 +02:00
parent 707121b45a
commit 5b5ce6db5d
2 changed files with 915 additions and 0 deletions
@@ -0,0 +1,539 @@
/*
* Tests for babel-plugin-lexicon-leaf-imports, in three layers:
*
* 1. Transform unit tests: run the plugin alone over small snippets against
* the real src/lexicons tree and assert the rewrite / bail behavior.
*
* 2. App-source proof: enumerate every chain reachable through the barrels'
* own `export * as` graph (the only chains user code can write), generate
* a probe file referencing all of them, transform it, and typecheck the
* output with the app tsconfig. This proves every rewritten specifier
* resolves to a real module. The transform itself also exercises the
* plugin's verifyChain proof for every single chain.
*
* 3. SDK dist proof: the plugin also rewrites @bsky/sdk's compiled output
* (plain JS). Transform every dist file that imports the lexicon barrel
* and typecheck the result with checkJs, where imports resolve to the
* SDK's shipped .d.ts files - so unresolvable specifiers (TS2307) and
* missing members on a rewritten leaf namespace (TS2339) both surface.
* tsc never checks JS under node_modules, so transformed files are
* written to a temp mirror and resolved back into the real dist via
* rootDirs. checkJs has inherent noise on compiled output, so the shadow
* diagnostics are compared against a baseline run of the untransformed
* files: only diagnostics introduced by the plugin fail the test.
*/
const {transformSync} = require('@babel/core')
const {parse} = require('@babel/parser')
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const ts = require('typescript')
const plugin = require('../babel-plugin-lexicon-leaf-imports')
const ROOT = path.resolve(__dirname, '../..')
const LEXICONS_ROOT = path.join(ROOT, 'src', 'lexicons')
const SDK_DIST = path.join(ROOT, 'node_modules', '@bsky', 'sdk', 'dist')
const TYPECHECK_TIMEOUT_MS = 240_000
function applyPlugin(code, filename) {
return transformSync(code, {
filename,
configFile: false,
babelrc: false,
parserOpts: {plugins: ['typescript']},
plugins: [[plugin, {roots: [LEXICONS_ROOT]}]],
}).code
}
/** A virtual .ts path inside src/ - the file itself never exists on disk. */
const PROBE_FILE = path.join(ROOT, 'src', '__lexicon_leaf_probe__.ts')
describe('transform', () => {
test('rewrites a barrel member chain to a leaf namespace import', () => {
const out = applyPlugin(
`import {app} from './lexicons'\nvoid app.bsky.feed.like\n`,
PROBE_FILE,
)
expect(out).toContain(
`import * as _lex_app_bsky_feed_like from "./lexicons/app/bsky/feed/like"`,
)
expect(out).toContain('void _lex_app_bsky_feed_like')
expect(out).not.toContain(`from './lexicons'`)
})
test('bails when the namespace is used as a value', () => {
const src = `import {app} from './lexicons'\nconsole.log(app)\n`
const out = applyPlugin(src, PROBE_FILE)
expect(out).toContain(`from './lexicons'`)
expect(out).not.toContain('import *')
})
test('bails when the chain stops at a non-leaf barrel', () => {
const src = `import {app} from './lexicons'\nvoid app.bsky\n`
const out = applyPlugin(src, PROBE_FILE)
expect(out).toContain(`from './lexicons'`)
expect(out).not.toContain('import *')
})
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)
expect(out).toContain(`from './lexicons'`)
expect(out).not.toContain('import *')
})
test('ignores imports that are not lexicon barrels', () => {
const src = `import {app} from './other'\nvoid app.bsky.feed.like\n`
const out = applyPlugin(src, PROBE_FILE)
expect(out).toContain(`from './other'`)
expect(out).toContain('void app.bsky.feed.like')
expect(out).not.toContain('import *')
})
})
/**
* Enumerate every leaf chain by following `export * as <name> from '...'`
* through the barrel graph, mirroring the plugin's own leaf/barrel rule: a
* target with a sibling directory is a barrel to recurse into, otherwise a
* leaf. Chains come out as e.g. ['app', 'bsky', 'feed', 'like'].
*/
function collectLeafChains(rootDir) {
const chains = []
function resolveTarget(fromFile, spec) {
const abs = path.resolve(path.dirname(fromFile), spec)
if (/\.(ts|js)$/.test(abs) && fs.existsSync(abs)) return abs
for (const ext of ['.ts', '.js']) {
if (fs.existsSync(abs + ext)) return abs + ext
}
throw new Error(`cannot resolve '${spec}' from ${fromFile}`)
}
function walk(barrelFile, segments) {
const ast = parse(fs.readFileSync(barrelFile, 'utf8'), {
sourceType: 'module',
plugins: ['typescript'],
})
for (const stmt of ast.program.body) {
if (stmt.type !== 'ExportNamedDeclaration' || !stmt.source) continue
if (stmt.exportKind === 'type') continue
for (const spec of stmt.specifiers) {
if (spec.type !== 'ExportNamespaceSpecifier') continue
const name =
spec.exported.type === 'Identifier'
? spec.exported.name
: spec.exported.value
const target = resolveTarget(barrelFile, stmt.source.value)
const asDir = target.replace(/\.(ts|js)$/, '')
if (fs.existsSync(asDir) && fs.statSync(asDir).isDirectory()) {
walk(target, [...segments, name])
} else {
chains.push([...segments, name])
}
}
}
}
walk(resolveTarget(path.join(rootDir, 'index.ts'), './index'), [])
return chains
}
function formatDiagnostics(diags) {
return diags
.slice(0, 20)
.map(d => {
const msg = ts.flattenDiagnosticMessageText(d.messageText, ' ')
if (d.file && d.start !== undefined) {
const {line} = d.file.getLineAndCharacterOfPosition(d.start)
return `${d.file.fileName}:${line + 1} TS${d.code}: ${msg}`
}
return `TS${d.code}: ${msg}`
})
.join('\n')
}
function fileDiagnostics(program, sourceFile) {
return [
...program.getSyntacticDiagnostics(sourceFile),
...program.getSemanticDiagnostics(sourceFile),
].filter(d => d.category === ts.DiagnosticCategory.Error)
}
function loadAppCompilerOptions() {
const configPath = path.join(ROOT, 'tsconfig.json')
const config = ts.readConfigFile(configPath, ts.sys.readFile).config
const parsed = ts.parseJsonConfigFileContent(
{...config, include: [], files: []},
ts.sys,
ROOT,
undefined,
configPath,
)
return {...parsed.options, noEmit: true, skipLibCheck: true}
}
/** A CompilerHost serving in-memory content for the paths in `overlays`. */
function createOverlayHost(options, overlays) {
const host = ts.createCompilerHost(options)
const origGetSourceFile = host.getSourceFile.bind(host)
const origFileExists = host.fileExists.bind(host)
const origReadFile = host.readFile.bind(host)
host.fileExists = f => overlays.has(f) || origFileExists(f)
host.readFile = f => overlays.get(f) ?? origReadFile(f)
host.getSourceFile = (f, lang, ...rest) =>
overlays.has(f)
? ts.createSourceFile(f, overlays.get(f), lang)
: origGetSourceFile(f, lang, ...rest)
return host
}
describe('app sources: every barrel chain rewrites and typechecks', () => {
test(
'probe referencing all leaf chains',
() => {
const chains = collectLeafChains(LEXICONS_ROOT)
/* The tree is large; a sudden collapse means the walker broke. */
expect(chains.length).toBeGreaterThan(100)
const barrelRoots = [...new Set(chains.map(c => c[0]))]
const probeSource =
`import {${barrelRoots.join(', ')}} from './lexicons'\n` +
chains.map(c => `void ${c.join('.')}`).join('\n') +
'\n'
/*
* This also runs the plugin's verifyChain proof for every chain: any
* filesystem/barrel divergence throws here.
*/
const out = applyPlugin(probeSource, PROBE_FILE)
expect(out).not.toContain(`from './lexicons'`)
const leafImports = out.match(/from "\.\/lexicons\//g) ?? []
expect(leafImports).toHaveLength(chains.length)
/*
* Typecheck the transformed probe with the app tsconfig. The probe is
* overlaid at a virtual path inside src/ so its relative leaf imports
* resolve against the real tree.
*/
const options = loadAppCompilerOptions()
const host = createOverlayHost(options, new Map([[PROBE_FILE, out]]))
const program = ts.createProgram([PROBE_FILE], options, host)
const probeSf = program.getSourceFile(PROBE_FILE)
expect(probeSf).toBeDefined()
const errors = fileDiagnostics(program, probeSf)
if (errors.length > 0) {
throw new Error(
`transformed probe has type errors:\n${formatDiagnostics(errors)}`,
)
}
/*
* Canary: prove this program setup actually flags a bad specifier, so
* a broken overlay host cannot produce a vacuous pass.
*/
const canary =
out + `import * as _bad from './lexicons/app/bsky/feed/__nope__'\n`
const canaryHost = createOverlayHost(
options,
new Map([[PROBE_FILE, canary]]),
)
const canaryProgram = ts.createProgram([PROBE_FILE], options, canaryHost)
const canaryDiags = fileDiagnostics(
canaryProgram,
canaryProgram.getSourceFile(PROBE_FILE),
)
expect(canaryDiags.map(d => d.code)).toContain(2307)
},
TYPECHECK_TIMEOUT_MS,
)
})
describe('@bsky/sdk dist: rewrites typecheck against shipped .d.ts', () => {
let tmpDir
afterAll(() => {
if (tmpDir) fs.rmSync(tmpDir, {recursive: true, force: true})
})
function findBarrelImporters(dir) {
const found = []
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
found.push(...findBarrelImporters(full))
} else if (
entry.name.endsWith('.js') &&
/^import[^\n]*['"][^'"]*lexicons\/index\.js['"]/m.test(
fs.readFileSync(full, 'utf8'),
)
) {
found.push(full)
}
}
return found
}
test(
'transformed dist files introduce no new diagnostics',
() => {
const importers = findBarrelImporters(SDK_DIST)
expect(importers.length).toBeGreaterThan(0)
const transformed = []
for (const file of importers) {
const code = fs.readFileSync(file, 'utf8')
/* Real filename so the plugin's @bsky/sdk path detection triggers. */
const out = applyPlugin(code, file)
if (out.trim() !== code.trim()) {
transformed.push({file, original: code, shadow: out})
}
}
expect(transformed.length).toBeGreaterThan(0)
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lexicon-leafcheck-'))
const mirrors = {
shadow: path.join(tmpDir, 'shadow'),
baseline: path.join(tmpDir, 'baseline'),
}
for (const kind of ['shadow', 'baseline']) {
fs.mkdirSync(mirrors[kind], {recursive: true})
fs.writeFileSync(
path.join(mirrors[kind], 'package.json'),
'{"type": "module"}\n',
)
}
const mirrorPath = (kind, file) =>
path.join(mirrors[kind], 'dist', path.relative(SDK_DIST, file))
for (const {file, original, shadow} of transformed) {
for (const [kind, code] of [
['shadow', shadow],
['baseline', original],
]) {
const dest = mirrorPath(kind, file)
fs.mkdirSync(path.dirname(dest), {recursive: true})
fs.writeFileSync(dest, code)
}
}
function diagnose(kind) {
const options = {
allowJs: true,
checkJs: true,
noEmit: true,
skipLibCheck: true,
strict: true,
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.Bundler,
/*
* Relative imports in the mirror (both untouched ones like
* './api.js' and the plugin's '../lexicons/...' rewrites) resolve
* into the real dist, landing on its .d.ts files.
*/
rootDirs: [path.join(mirrors[kind], 'dist'), SDK_DIST],
}
const roots = transformed.map(t => mirrorPath(kind, t.file))
const program = ts.createProgram(roots, options)
const byFile = new Map()
for (const t of transformed) {
const sf = program.getSourceFile(mirrorPath(kind, t.file))
if (!sf) {
throw new Error(`${mirrorPath(kind, t.file)} missing from program`)
}
byFile.set(t.file, fileDiagnostics(program, sf))
}
return byFile
}
/*
* Canary: prove the checkJs machinery actually checks members through
* the SDK's .d.ts files. If this setup ever degrades to not-checking,
* a real regression would pass silently - so require a known-bad
* member access to be flagged.
*/
{
const canary = path.join(mirrors.shadow, 'dist', '__canary__.js')
fs.writeFileSync(
canary,
`import * as leaf from './lexicons/index.js'\nvoid leaf.__does_not_exist__\n`,
)
const options = {
allowJs: true,
checkJs: true,
noEmit: true,
skipLibCheck: true,
strict: true,
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.Bundler,
rootDirs: [path.join(mirrors.shadow, 'dist'), SDK_DIST],
}
const program = ts.createProgram([canary], options)
const diags = fileDiagnostics(program, program.getSourceFile(canary))
expect(diags.map(d => d.code)).toContain(2339)
fs.rmSync(canary)
}
const baseline = diagnose('baseline')
const shadow = diagnose('shadow')
const diagKey = d =>
`TS${d.code}: ${ts.flattenDiagnosticMessageText(d.messageText, ' ')}`
const regressions = []
for (const {file} of transformed) {
const known = new Set(baseline.get(file).map(diagKey))
for (const d of shadow.get(file)) {
if (!known.has(diagKey(d))) regressions.push(d)
}
}
if (regressions.length > 0) {
throw new Error(
`plugin introduced diagnostics in @bsky/sdk dist:\n${formatDiagnostics(regressions)}`,
)
}
},
TYPECHECK_TIMEOUT_MS,
)
})
describe('app callsites: transformed sources typecheck', () => {
/*
* The real-usage complement to the probe: transform every app file that
* imports the barrel - with babel-plugin-module-resolver ahead of the
* plugin, as in babel.config.js, so the '#/lexicons' -> relative-path
* interop and ordering are exercised - and typecheck the transformed files
* in place of the originals.
*
* Types are kept (no preset-typescript) so tsc has something to check.
* One behavioral difference follows: in the real pipeline type-only
* references are stripped before the plugin's Program exit, so removing a
* fully-rewritten specifier is always safe there. Here type positions
* survive, and Babel's scope does not count them as references - so when
* the plugin drops a specifier the type positions still need it. Those
* names are re-added as a type-only barrel import, which is exactly their
* production status: erased at runtime, checked against the barrel.
*
* checked-vs-baseline: diagnostics of each transformed file are compared
* against the same file run through the identical parse/print pipeline
* WITHOUT the leaf plugin. Reprinting artifacts (e.g. a reflowed
* ts-expect-error directive missing its line) then affect both sides
* equally and diff out - only differences the plugin caused can fail.
*/
test(
'every file importing the barrel',
() => {
const consumers = []
;(function walk(dir) {
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
walk(full)
} else if (
/\.tsx?$/.test(entry.name) &&
!entry.name.endsWith('.d.ts') &&
fs.readFileSync(full, 'utf8').includes(`from '#/lexicons'`)
) {
consumers.push(full)
}
}
})(path.join(ROOT, 'src'))
expect(consumers.length).toBeGreaterThan(100)
/** Names bound by value imports of the barrel, keyed off any path form. */
function barrelImportNames(code, file) {
const names = new Set()
const ast = parse(code, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
})
for (const stmt of ast.program.body) {
if (stmt.type !== 'ImportDeclaration') continue
const source = stmt.source.value
const abs = source.startsWith('.')
? path
.resolve(path.dirname(file), source)
.replace(/[\\/]index$/, '')
: null
if (source !== '#/lexicons' && abs !== LEXICONS_ROOT) continue
for (const spec of stmt.specifiers) {
if (spec.type === 'ImportSpecifier') names.add(spec.local.name)
}
}
return names
}
function transformConsumer(code, file, withPlugin) {
let out = transformSync(code, {
filename: file,
cwd: ROOT,
configFile: false,
babelrc: false,
parserOpts: {plugins: ['typescript', 'jsx']},
plugins: [
[
require.resolve('babel-plugin-module-resolver'),
{alias: {'#': './src', crypto: './src/platform/crypto.ts'}},
],
...(withPlugin ? [[plugin, {roots: [LEXICONS_ROOT]}]] : []),
],
}).code
const dropped = [...barrelImportNames(code, file)].filter(
n => !barrelImportNames(out, file).has(n),
)
if (dropped.length > 0) {
out = `import type {${dropped.join(', ')}} from '#/lexicons'\n` + out
}
return out
}
const shadowOverlays = new Map()
const baselineOverlays = new Map()
let rewritten = 0
for (const file of consumers) {
const code = fs.readFileSync(file, 'utf8')
const out = transformConsumer(code, file, true)
shadowOverlays.set(file, out)
baselineOverlays.set(file, transformConsumer(code, file, false))
if (out.includes('_lex_')) rewritten++
}
expect(rewritten).toBeGreaterThan(100)
const options = loadAppCompilerOptions()
function diagnose(overlays) {
const host = createOverlayHost(options, overlays)
const program = ts.createProgram(consumers, options, host)
const byFile = new Map()
for (const file of consumers) {
const sf = program.getSourceFile(file)
if (!sf) throw new Error(`${file} missing from program`)
byFile.set(file, fileDiagnostics(program, sf))
}
return byFile
}
const baseline = diagnose(baselineOverlays)
const shadow = diagnose(shadowOverlays)
const diagKey = d =>
`TS${d.code}: ${ts.flattenDiagnosticMessageText(d.messageText, ' ')}`
const regressions = []
for (const file of consumers) {
const known = new Set(baseline.get(file).map(diagKey))
for (const d of shadow.get(file)) {
if (!known.has(diagKey(d))) regressions.push(d)
}
}
if (regressions.length > 0) {
throw new Error(
`plugin introduced diagnostics in app sources:\n${formatDiagnostics(regressions)}`,
)
}
},
TYPECHECK_TIMEOUT_MS,
)
})
@@ -0,0 +1,376 @@
/*
* Differential resolution test for babel-plugin-lexicon-leaf-imports.
*
* For every project file that imports the lexicon barrel, this test computes
* where each member chain (`app.bsky.feed.like`) SHOULD lead by walking the
* barrels' actual `export * as` statements (an oracle independent of the
* plugin's filesystem heuristic), then runs the real transform and resolves
* the leaf imports it emitted. The two resolutions must agree - compared by
* file content hash, so the assertion is "the import binds to the same code"
* rather than a path-string comparison.
*
* Chains the oracle cannot follow statically (computed access, namespace used
* as a value) must bail in the plugin too: the barrel import has to survive in
* the output exactly when the oracle predicts a bail.
*/
import crypto from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import * as babel from '@babel/core'
import {parse} from '@babel/parser'
// eslint-disable-next-line @typescript-eslint/no-require-imports
const traverse = require('@babel/traverse').default
const ROOT = path.resolve(__dirname, '../..')
const PLUGIN = path.join(ROOT, 'plugins/babel-plugin-lexicon-leaf-imports.js')
const APP_BARREL_ENTRY = path.join(ROOT, 'src/lexicons/index.ts')
const SDK_DIST = path.join(ROOT, 'node_modules/@bsky/sdk/dist')
const SDK_BARREL_RE = /(^|\/)lexicons\/index\.js$/
function listFiles(dir: string, exts: string[]): string[] {
const out: string[] = []
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
out.push(...listFiles(full, exts))
} else if (exts.some(ext => entry.name.endsWith(ext))) {
out.push(full)
}
}
return out
}
function resolveSpec(fromFile: string, spec: string): string | null {
const abs = path.resolve(path.dirname(fromFile), spec)
if (/\.(ts|tsx|js)$/.test(abs) && fs.existsSync(abs)) return abs
for (const ext of ['.ts', '.tsx', '.js']) {
if (fs.existsSync(abs + ext)) return abs + ext
}
return null
}
const hashCache = new Map<string, string>()
function contentHash(file: string): string {
let h = hashCache.get(file)
if (!h) {
h = crypto.createHash('sha1').update(fs.readFileSync(file)).digest('hex')
hashCache.set(file, h)
}
return h
}
/**
* The oracle's own barrel parser (written independently of the plugin's):
* Map of exported name -> source specifier for a file consisting purely of
* `export * as X from '...'` statements, or null for any other file shape -
* which is exactly what distinguishes a leaf from a barrel.
*/
const barrelCache = new Map<string, Map<string, string> | null>()
function barrelExports(file: string): Map<string, string> | null {
let map = barrelCache.get(file)
if (map !== undefined) return map
map = new Map()
try {
const ast = parse(fs.readFileSync(file, 'utf8'), {
sourceType: 'module',
plugins: ['typescript'],
})
for (const stmt of ast.program.body) {
if (stmt.type === 'ExportNamedDeclaration' && stmt.exportKind === 'type')
continue
if (
stmt.type !== 'ExportNamedDeclaration' ||
!stmt.source ||
stmt.declaration ||
stmt.specifiers.length === 0 ||
!stmt.specifiers.every(s => s.type === 'ExportNamespaceSpecifier')
) {
map = null
break
}
for (const s of stmt.specifiers) {
if (s.type === 'ExportNamespaceSpecifier') {
map.set(s.exported.name, stmt.source.value)
}
}
}
} catch {
map = null
}
barrelCache.set(file, map)
return map
}
type ChainResult =
{leaf: string; chain: string[]} | {bail: true} | {error: string}
/**
* Follow one member chain through the barrels' export statements: at each
* step look the segment up in the current barrel's `export * as` map and
* resolve its source. A resolved file that is not itself a pure-namespace
* barrel is the leaf. Needing another segment when the expression has none
* left (or has a non-static one) is a bail, mirroring the plugin's contract.
*/
function walkChain(
ref: any,
entryBarrel: string,
rootSegment: string,
): ChainResult {
let curFile = entryBarrel
let segment = rootSegment
let cur = ref
const chain = [rootSegment]
for (;;) {
const exports = barrelExports(curFile)
if (!exports) return {error: `${curFile} is not a pure namespace barrel`}
const spec = exports.get(segment)
if (!spec) return {error: `'${chain.join('.')}' not exported by ${curFile}`}
const next = resolveSpec(curFile, spec)
if (!next) return {error: `cannot resolve '${spec}' from ${curFile}`}
if (barrelExports(next) === null) return {leaf: next, chain}
const parent = cur.parentPath
if (
!parent?.isMemberExpression() ||
parent.node.object !== cur.node ||
parent.node.computed ||
parent.node.property.type !== 'Identifier'
) {
return {bail: true}
}
curFile = next
segment = parent.node.property.name
chain.push(segment)
cur = parent
}
}
/**
* References in type positions are stripped before the plugin runs, so the
* oracle must ignore them too. In type positions chains appear as
* TSQualifiedName (or a bare TSTypeQuery for `typeof app`), never as
* MemberExpression.
*/
function isTypeReference(ref: any): boolean {
return ref.parentPath?.isTSQualifiedName() || ref.parentPath?.isTSTypeQuery()
}
type Expectation = {
/** chain joined with '.' -> absolute leaf file */
leaves: Map<string, string>
/** true when the plugin must keep (part of) the barrel import */
barrelRetained: boolean
errors: string[]
}
function computeExpectation(file: string, code: string): Expectation {
const isSdk = file.startsWith(SDK_DIST)
const leaves = new Map<string, string>()
const errors: string[] = []
let barrelRetained = false
const ast = parse(code, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
})
traverse(ast, {
Program(programPath: any) {
for (const stmt of programPath.get('body')) {
if (!stmt.isImportDeclaration()) continue
if (stmt.node.importKind === 'type') continue
const source = stmt.node.source.value
let entryBarrel: string | null = null
if (!isSdk && source === '#/lexicons') {
entryBarrel = APP_BARREL_ENTRY
} else if (isSdk && SDK_BARREL_RE.test(source)) {
entryBarrel = resolveSpec(file, source)
}
if (!entryBarrel) continue
for (const spec of stmt.get('specifiers')) {
/*
* Type-only specifiers are stripped from the output together with
* their references, so they neither rewrite nor retain the barrel.
* Non-named value specifiers (namespace/default) are kept by the
* plugin and do retain it.
*/
if (spec.node.importKind === 'type') continue
if (!spec.isImportSpecifier()) {
barrelRetained = true
continue
}
const imported = spec.node.imported
const rootSegment =
imported.type === 'Identifier' ? imported.name : imported.value
const binding = programPath.scope.getBinding(spec.node.local.name)
if (!binding) continue
const chains: Array<{leaf: string; chain: string[]}> = []
let bailed = false
for (const ref of binding.referencePaths) {
if (isTypeReference(ref)) continue
const r = walkChain(ref, entryBarrel, rootSegment)
if ('error' in r) {
errors.push(r.error)
bailed = true
break
}
if ('bail' in r) {
bailed = true
break
}
chains.push(r)
}
if (bailed) {
barrelRetained = true
} else {
for (const {leaf, chain} of chains) {
leaves.set(chain.join('.'), leaf)
}
}
}
}
programPath.stop()
},
})
return {leaves, barrelRetained, errors}
}
/**
* The real transform, reduced to the plugins that participate in import
* rewriting. react-compiler, lingui, and worklets are omitted for speed; they
* do not touch import declarations, and their interaction with this plugin is
* covered by the full Jest suite running the complete config.
*/
function transformActual(file: string, code: string): string {
const result = babel.transformSync(code, {
filename: file,
cwd: ROOT,
configFile: false,
babelrc: false,
presets: [
[
require.resolve('@babel/preset-typescript'),
{isTSX: /\.tsx$/.test(file), allExtensions: true},
],
],
plugins: [
[
require.resolve('babel-plugin-module-resolver'),
{alias: {'#': './src'}},
],
[PLUGIN, {roots: [path.join(ROOT, 'src/lexicons')]}],
],
})
return result!.code!
}
type Actual = {
/** absolute resolved leaf files from emitted `_lex_*` namespace imports */
leaves: Map<string, string>
barrelRetained: boolean
}
function collectActual(file: string, output: string): Actual {
const isSdk = file.startsWith(SDK_DIST)
const leaves = new Map<string, string>()
let barrelRetained = false
const ast = parse(output, {sourceType: 'module', plugins: ['jsx']})
for (const stmt of ast.program.body) {
if (stmt.type !== 'ImportDeclaration') continue
const source = stmt.source.value
const ns = stmt.specifiers.find(
s => s.type === 'ImportNamespaceSpecifier' && /^_lex_/.test(s.local.name),
)
if (ns) {
const resolved = resolveSpec(file, source)
leaves.set(source, resolved ?? `<unresolvable: ${source}>`)
continue
}
const resolved = source.startsWith('.') ? resolveSpec(file, source) : null
if (
resolved === APP_BARREL_ENTRY ||
(isSdk && SDK_BARREL_RE.test(source) && resolved)
) {
barrelRetained = true
}
}
return {leaves, barrelRetained}
}
function checkFile(file: string): string[] {
const failures: string[] = []
const rel = path.relative(ROOT, file)
const code = fs.readFileSync(file, 'utf8')
const expected = computeExpectation(file, code)
for (const err of expected.errors) {
failures.push(`${rel}: oracle error: ${err}`)
}
let output: string
try {
output = transformActual(file, code)
} catch (e) {
failures.push(`${rel}: transform threw: ${(e as Error).message}`)
return failures
}
const actual = collectActual(file, output)
const actualByHash = new Map<string, string>()
for (const [source, resolved] of actual.leaves) {
if (resolved.startsWith('<')) {
failures.push(`${rel}: emitted import does not resolve: '${source}'`)
} else {
actualByHash.set(contentHash(resolved), resolved)
}
}
const expectedHashes = new Set<string>()
for (const [chain, leaf] of expected.leaves) {
const hash = contentHash(leaf)
expectedHashes.add(hash)
if (!actualByHash.has(hash)) {
failures.push(
`${rel}: chain '${chain}' should bind to ${path.relative(ROOT, leaf)} ` +
`but no emitted import matches its content`,
)
}
}
for (const [hash, resolved] of actualByHash) {
if (!expectedHashes.has(hash)) {
failures.push(
`${rel}: emitted import of ${path.relative(ROOT, resolved)} ` +
`matches no barrel chain in the source`,
)
}
}
if (actual.barrelRetained !== expected.barrelRetained) {
failures.push(
`${rel}: barrel import ${actual.barrelRetained ? 'survived' : 'removed'} ` +
`but oracle expected ${expected.barrelRetained ? 'a bail' : 'full rewrite'}`,
)
}
return failures
}
describe('lexicon leaf import rewrites resolve to the same modules as the barrels', () => {
test('app sources', () => {
const consumers = listFiles(path.join(ROOT, 'src'), ['.ts', '.tsx']).filter(
f => fs.readFileSync(f, 'utf8').includes(`from '#/lexicons'`),
)
expect(consumers.length).toBeGreaterThan(100)
const failures = consumers.flatMap(checkFile)
expect(failures).toEqual([])
}, 240_000)
test('@bsky/sdk compiled output', () => {
const consumers = listFiles(SDK_DIST, ['.js']).filter(f =>
fs.readFileSync(f, 'utf8').includes(`lexicons/index.js'`),
)
expect(consumers.length).toBeGreaterThan(0)
const failures = consumers.flatMap(checkFile)
expect(failures).toEqual([])
}, 120_000)
})