invalidate caches on lexicons regeneration
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user