invalidate caches on lexicons regeneration

This commit is contained in:
Oleksii Bulenok
2026-08-31 14:22:53 +02:00
parent 85f946b7f4
commit 2274a6b5b8
2 changed files with 109 additions and 2 deletions
@@ -93,6 +93,78 @@ describe('transform', () => {
})
})
/*
* The plugin memoizes filesystem stats, barrel export maps, and verified
* chains in module-level caches that outlive individual transforms. A lexicon
* regen inside a long-lived worker (Metro dev server, jest --watch) must
* invalidate them - the plugin uses the root index mtime as the epoch.
*/
describe('cache invalidation across a lexicon regen', () => {
let tmp
let lexRoot
function write(rel, content) {
const file = path.join(tmp, rel)
fs.mkdirSync(path.dirname(file), {recursive: true})
fs.writeFileSync(file, content)
}
beforeAll(() => {
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'lexicon-regen-'))
lexRoot = path.join(tmp, 'lexicons')
write('lexicons/index.ts', `export * as app from './app'\n`)
write('lexicons/app.ts', `export * as bsky from './app/bsky'\n`)
write('lexicons/app/bsky.ts', `export * as feed from './bsky/feed'\n`)
write('lexicons/app/bsky/feed.ts', `export * as like from './feed/like'\n`)
write('lexicons/app/bsky/feed/like.ts', `export const $type = 'test'\n`)
})
afterAll(() => {
fs.rmSync(tmp, {recursive: true, force: true})
})
function transform() {
return transformSync(
`import {app} from './lexicons'\nvoid app.bsky.feed.like\n`,
{
filename: path.join(tmp, 'consumer.ts'),
configFile: false,
babelrc: false,
parserOpts: {plugins: ['typescript']},
plugins: [[plugin, {roots: [lexRoot]}]],
},
).code
}
test('a layout change is picked up without a process restart', () => {
expect(transform()).toContain(
`import * as _lex_app_bsky_feed_like from "./lexicons/app/bsky/feed/like"`,
)
/*
* Simulate `lex build --clear` deepening the leaf into a barrel. Codegen
* rewrites the whole tree, so the root index mtime always moves; force it
* forward explicitly since same-millisecond writes would hide the change.
*/
write(
'lexicons/app/bsky/feed/like.ts',
`export * as main from './like/main'\n`,
)
write('lexicons/app/bsky/feed/like/main.ts', `export const $type = 'test'\n`)
const bumped = new Date(Date.now() + 10_000)
fs.utimesSync(path.join(lexRoot, 'index.ts'), bumped, bumped)
/*
* The chain now stops at a barrel, so the correct result is a bail that
* keeps the barrel import. Stale caches would instead replay the rewrite
* against the old layout.
*/
const out = transform()
expect(out).toContain(`from './lexicons'`)
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
+37 -2
View File
@@ -142,7 +142,7 @@ function resolveBarrelTarget(fromFile, spec) {
return null
}
/** `rootDir\0segments` -> boolean */
/** `rootDir\0segments\0leafFile` -> boolean */
const chainCache = new Map()
/**
@@ -152,7 +152,11 @@ const chainCache = new Map()
* divergence; the caller turns that into a hard build error.
*/
function verifyChain(rootDir, segments, leafFile) {
const key = rootDir + '\0' + segments.join('.')
/*
* leafFile is part of the key: the cached boolean encodes `walk === leafFile`,
* so a hit for the same chain but a different planned leaf must not be reused.
*/
const key = rootDir + '\0' + segments.join('.') + '\0' + leafFile
let ok = chainCache.get(key)
if (ok !== undefined) return ok
let cur = leafFileFor(rootDir, 'index')
@@ -172,6 +176,34 @@ function verifyChain(rootDir, segments, leafFile) {
return ok
}
/** Barrel root dir -> mtimeMs of its index file when the caches were filled. */
const rootEpochs = new Map()
/*
* The caches above are module-level and would otherwise outlive a lexicon
* regen inside a long-lived Metro or Jest watch worker, serving stale stats,
* export maps, and verified chains until the process restarts. Codegen
* (`lex build --clear`) rewrites the whole tree including the root index, so
* the index mtime works as an epoch: when it moves, drop all three caches.
* Costs one statSync per file that actually imports a barrel.
*/
function invalidateStaleCaches(rootDir) {
let mtime = -1
for (const ext of EXTS) {
try {
mtime = fs.statSync(path.join(rootDir, 'index' + ext)).mtimeMs
break
} catch {}
}
const prev = rootEpochs.get(rootDir)
if (prev !== undefined && prev !== mtime) {
statCache.clear()
barrelExportCache.clear()
chainCache.clear()
}
rootEpochs.set(rootDir, mtime)
}
const SDK_SEGMENT = `${path.sep}@bsky${path.sep}sdk${path.sep}`
const SDK_BARREL_RE = /(^|\/)lexicons\/index\.js$/
@@ -271,6 +303,9 @@ module.exports = function lexiconLeafImports(babel, options = {}) {
if (dir) targets.push({imp: stmt, dir})
}
if (targets.length === 0) return
for (const {dir} of targets) {
invalidateStaleCaches(dir)
}
/*
* Type-only references were stripped by the TypeScript transform