refactor
This commit is contained in:
@@ -30,6 +30,7 @@ const path = require('node:path')
|
||||
const ts = require('typescript')
|
||||
|
||||
const plugin = require('../babel-plugin-lexicon-leaf-imports')
|
||||
const {barrelExports, resolveModuleFile} = require('../lexiconBarrels')
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..')
|
||||
const LEXICONS_ROOT = path.join(ROOT, 'src', 'lexicons')
|
||||
@@ -187,35 +188,23 @@ 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}`)
|
||||
const abs = resolveModuleFile(fromFile, spec)
|
||||
if (!abs) throw new Error(`cannot resolve '${spec}' from ${fromFile}`)
|
||||
return abs
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
const exports = barrelExports(barrelFile)
|
||||
if (!exports) {
|
||||
throw new Error(`${barrelFile} is not a pure namespace barrel`)
|
||||
}
|
||||
for (const [name, spec] of exports) {
|
||||
const target = resolveTarget(barrelFile, spec)
|
||||
const asDir = target.replace(/\.(ts|js)$/, '')
|
||||
if (fs.existsSync(asDir) && fs.statSync(asDir).isDirectory()) {
|
||||
walk(target, [...segments, name])
|
||||
} else {
|
||||
chains.push([...segments, name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
* 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.
|
||||
*
|
||||
* The mechanical layer (specifier resolution, barrel parsing) is shared with
|
||||
* the plugin via lexiconBarrels.js so the copies cannot drift; the oracle's
|
||||
* independence lies in walking the export graph itself instead of trusting
|
||||
* the plugin's filesystem-name heuristic.
|
||||
*/
|
||||
import crypto from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
@@ -20,6 +25,12 @@ import path from 'node:path'
|
||||
import * as babel from '@babel/core'
|
||||
import {parse} from '@babel/parser'
|
||||
|
||||
import {
|
||||
barrelExports,
|
||||
resolveModuleFile,
|
||||
SDK_BARREL_RE,
|
||||
} from '../lexiconBarrels'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const traverse = require('@babel/traverse').default
|
||||
|
||||
@@ -27,7 +38,6 @@ 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[] = []
|
||||
@@ -42,15 +52,6 @@ function listFiles(dir: string, exts: string[]): string[] {
|
||||
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)
|
||||
@@ -61,48 +62,6 @@ function contentHash(file: string): string {
|
||||
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}
|
||||
|
||||
@@ -127,7 +86,7 @@ function walkChain(
|
||||
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)
|
||||
const next = resolveModuleFile(curFile, spec)
|
||||
if (!next) return {error: `cannot resolve '${spec}' from ${curFile}`}
|
||||
if (barrelExports(next) === null) return {leaf: next, chain}
|
||||
const parent = cur.parentPath
|
||||
@@ -184,7 +143,7 @@ function computeExpectation(file: string, code: string): Expectation {
|
||||
if (!isSdk && source === '#/lexicons') {
|
||||
entryBarrel = APP_BARREL_ENTRY
|
||||
} else if (isSdk && SDK_BARREL_RE.test(source)) {
|
||||
entryBarrel = resolveSpec(file, source)
|
||||
entryBarrel = resolveModuleFile(file, source)
|
||||
}
|
||||
if (!entryBarrel) continue
|
||||
|
||||
@@ -283,11 +242,11 @@ function collectActual(file: string, output: string): Actual {
|
||||
s => s.type === 'ImportNamespaceSpecifier' && /^_lex_/.test(s.local.name),
|
||||
)
|
||||
if (ns) {
|
||||
const resolved = resolveSpec(file, source)
|
||||
const resolved = resolveModuleFile(file, source)
|
||||
leaves.set(source, resolved ?? `<unresolvable: ${source}>`)
|
||||
continue
|
||||
}
|
||||
const resolved = source.startsWith('.') ? resolveSpec(file, source) : null
|
||||
const resolved = source.startsWith('.') ? resolveModuleFile(file, source) : null
|
||||
if (
|
||||
resolved === APP_BARREL_ENTRY ||
|
||||
(isSdk && SDK_BARREL_RE.test(source) && resolved)
|
||||
|
||||
@@ -52,9 +52,15 @@
|
||||
*/
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const parser = require('@babel/parser')
|
||||
|
||||
const EXTS = ['.ts', '.js']
|
||||
const {
|
||||
EXTS,
|
||||
SDK_BARREL_RE,
|
||||
resolveModuleFile,
|
||||
barrelExports,
|
||||
clearBarrelExportCache,
|
||||
} = require('./lexiconBarrels')
|
||||
|
||||
const statCache = new Map()
|
||||
|
||||
/** @returns {'dir' | 'file' | null} */
|
||||
@@ -90,58 +96,6 @@ function leafFileFor(dir, segment) {
|
||||
return null
|
||||
}
|
||||
|
||||
/** Absolute barrel file -> Map(exported name -> source specifier), or null. */
|
||||
const barrelExportCache = new Map()
|
||||
|
||||
/**
|
||||
* Parse a barrel file into its namespace re-export map. Returns null if the
|
||||
* file contains anything other than `export * as X from '...'` statements
|
||||
* (plus type-only exports) - chains through such a file cannot be proven, so
|
||||
* lookups fail and the caller bails.
|
||||
*/
|
||||
function barrelExports(file) {
|
||||
let map = barrelExportCache.get(file)
|
||||
if (map !== undefined) return map
|
||||
map = new Map()
|
||||
try {
|
||||
const ast = parser.parse(fs.readFileSync(file, 'utf8'), {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript'],
|
||||
})
|
||||
for (const stmt of ast.program.body) {
|
||||
if (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) {
|
||||
const name =
|
||||
s.exported.type === 'Identifier' ? s.exported.name : s.exported.value
|
||||
map.set(name, stmt.source.value)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
map = null
|
||||
}
|
||||
barrelExportCache.set(file, map)
|
||||
return map
|
||||
}
|
||||
|
||||
function resolveBarrelTarget(fromFile, spec) {
|
||||
const abs = path.resolve(path.dirname(fromFile), spec)
|
||||
if (/\.(ts|js)$/.test(abs) && fs.existsSync(abs)) return abs
|
||||
for (const ext of EXTS) {
|
||||
if (fs.existsSync(abs + ext)) return abs + ext
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** `rootDir\0segments\0leafFile` -> boolean */
|
||||
const chainCache = new Map()
|
||||
|
||||
@@ -164,7 +118,7 @@ function verifyChain(rootDir, segments, leafFile) {
|
||||
if (ok) {
|
||||
for (const segment of segments) {
|
||||
const spec = barrelExports(cur)?.get(segment)
|
||||
cur = spec ? resolveBarrelTarget(cur, spec) : null
|
||||
cur = spec ? resolveModuleFile(cur, spec) : null
|
||||
if (!cur) {
|
||||
ok = false
|
||||
break
|
||||
@@ -198,14 +152,13 @@ function invalidateStaleCaches(rootDir) {
|
||||
const prev = rootEpochs.get(rootDir)
|
||||
if (prev !== undefined && prev !== mtime) {
|
||||
statCache.clear()
|
||||
barrelExportCache.clear()
|
||||
clearBarrelExportCache()
|
||||
chainCache.clear()
|
||||
}
|
||||
rootEpochs.set(rootDir, mtime)
|
||||
}
|
||||
|
||||
const SDK_SEGMENT = `${path.sep}@bsky${path.sep}sdk${path.sep}`
|
||||
const SDK_BARREL_RE = /(^|\/)lexicons\/index\.js$/
|
||||
|
||||
module.exports = function lexiconLeafImports(babel, options = {}) {
|
||||
const {types: t} = babel
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Shared mechanics for reading the generated lexicon barrels, used by
|
||||
* babel-plugin-lexicon-leaf-imports and its test suites. The tests keep their
|
||||
* own walking logic (an export-graph oracle vs the plugin's filesystem
|
||||
* heuristic); only this mechanical layer - how a specifier resolves to a file
|
||||
* and how a barrel parses into its re-export map - is shared, so the copies
|
||||
* cannot drift apart.
|
||||
*/
|
||||
const fs = require('node:fs')
|
||||
const parser = require('@babel/parser')
|
||||
const path = require('node:path')
|
||||
|
||||
/** Extensions codegen emits: `.ts` for app sources, `.js` for the SDK dist. */
|
||||
const EXTS = ['.ts', '.js']
|
||||
const EXT_RE = new RegExp(`\\.(${EXTS.map(e => e.slice(1)).join('|')})$`)
|
||||
|
||||
/** Matches the SDK's compiled barrel entry, e.g. '../lexicons/index.js'. */
|
||||
const SDK_BARREL_RE = /(^|\/)lexicons\/index\.js$/
|
||||
|
||||
/**
|
||||
* Resolve a barrel/leaf specifier relative to the importing file: the exact
|
||||
* path when it already carries a known extension, otherwise the first of
|
||||
* EXTS that exists.
|
||||
*
|
||||
* @param {string} fromFile
|
||||
* @param {string} spec
|
||||
* @returns {string | null} absolute file path, or null
|
||||
*/
|
||||
function resolveModuleFile(fromFile, spec) {
|
||||
const abs = path.resolve(path.dirname(fromFile), spec)
|
||||
if (EXT_RE.test(abs) && fs.existsSync(abs)) return abs
|
||||
for (const ext of EXTS) {
|
||||
if (fs.existsSync(abs + ext)) return abs + ext
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Absolute barrel file -> Map(exported name -> source specifier), or null. */
|
||||
const barrelExportCache = new Map()
|
||||
|
||||
/**
|
||||
* Parse a barrel file into its namespace re-export map. Returns null if the
|
||||
* file contains anything other than `export * as X from '...'` statements
|
||||
* (plus type-only exports) - chains through such a file cannot be proven, so
|
||||
* lookups fail and the caller bails.
|
||||
*
|
||||
* @param {string} file
|
||||
* @returns {Map<string, string> | null}
|
||||
*/
|
||||
function barrelExports(file) {
|
||||
let map = barrelExportCache.get(file)
|
||||
if (map !== undefined) return map
|
||||
map = new Map()
|
||||
try {
|
||||
const ast = parser.parse(fs.readFileSync(file, 'utf8'), {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript'],
|
||||
})
|
||||
for (const stmt of ast.program.body) {
|
||||
if (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) {
|
||||
const name =
|
||||
s.exported.type === 'Identifier' ? s.exported.name : s.exported.value
|
||||
map.set(name, stmt.source.value)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
map = null
|
||||
}
|
||||
barrelExportCache.set(file, map)
|
||||
return map
|
||||
}
|
||||
|
||||
function clearBarrelExportCache() {
|
||||
barrelExportCache.clear()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EXTS,
|
||||
SDK_BARREL_RE,
|
||||
resolveModuleFile,
|
||||
barrelExports,
|
||||
clearBarrelExportCache,
|
||||
}
|
||||
Reference in New Issue
Block a user