refactor
This commit is contained in:
@@ -30,6 +30,7 @@ const path = require('node:path')
|
|||||||
const ts = require('typescript')
|
const ts = require('typescript')
|
||||||
|
|
||||||
const plugin = require('../babel-plugin-lexicon-leaf-imports')
|
const plugin = require('../babel-plugin-lexicon-leaf-imports')
|
||||||
|
const {barrelExports, resolveModuleFile} = require('../lexiconBarrels')
|
||||||
|
|
||||||
const ROOT = path.resolve(__dirname, '../..')
|
const ROOT = path.resolve(__dirname, '../..')
|
||||||
const LEXICONS_ROOT = path.join(ROOT, 'src', 'lexicons')
|
const LEXICONS_ROOT = path.join(ROOT, 'src', 'lexicons')
|
||||||
@@ -187,35 +188,23 @@ function collectLeafChains(rootDir) {
|
|||||||
const chains = []
|
const chains = []
|
||||||
|
|
||||||
function resolveTarget(fromFile, spec) {
|
function resolveTarget(fromFile, spec) {
|
||||||
const abs = path.resolve(path.dirname(fromFile), spec)
|
const abs = resolveModuleFile(fromFile, spec)
|
||||||
if (/\.(ts|js)$/.test(abs) && fs.existsSync(abs)) return abs
|
if (!abs) throw new Error(`cannot resolve '${spec}' from ${fromFile}`)
|
||||||
for (const ext of ['.ts', '.js']) {
|
return abs
|
||||||
if (fs.existsSync(abs + ext)) return abs + ext
|
|
||||||
}
|
|
||||||
throw new Error(`cannot resolve '${spec}' from ${fromFile}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function walk(barrelFile, segments) {
|
function walk(barrelFile, segments) {
|
||||||
const ast = parse(fs.readFileSync(barrelFile, 'utf8'), {
|
const exports = barrelExports(barrelFile)
|
||||||
sourceType: 'module',
|
if (!exports) {
|
||||||
plugins: ['typescript'],
|
throw new Error(`${barrelFile} is not a pure namespace barrel`)
|
||||||
})
|
}
|
||||||
for (const stmt of ast.program.body) {
|
for (const [name, spec] of exports) {
|
||||||
if (stmt.type !== 'ExportNamedDeclaration' || !stmt.source) continue
|
const target = resolveTarget(barrelFile, spec)
|
||||||
if (stmt.exportKind === 'type') continue
|
const asDir = target.replace(/\.(ts|js)$/, '')
|
||||||
for (const spec of stmt.specifiers) {
|
if (fs.existsSync(asDir) && fs.statSync(asDir).isDirectory()) {
|
||||||
if (spec.type !== 'ExportNamespaceSpecifier') continue
|
walk(target, [...segments, name])
|
||||||
const name =
|
} else {
|
||||||
spec.exported.type === 'Identifier'
|
chains.push([...segments, name])
|
||||||
? 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])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@
|
|||||||
* Chains the oracle cannot follow statically (computed access, namespace used
|
* 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
|
* 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 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 crypto from 'node:crypto'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
@@ -20,6 +25,12 @@ import path from 'node:path'
|
|||||||
import * as babel from '@babel/core'
|
import * as babel from '@babel/core'
|
||||||
import {parse} from '@babel/parser'
|
import {parse} from '@babel/parser'
|
||||||
|
|
||||||
|
import {
|
||||||
|
barrelExports,
|
||||||
|
resolveModuleFile,
|
||||||
|
SDK_BARREL_RE,
|
||||||
|
} from '../lexiconBarrels'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
const traverse = require('@babel/traverse').default
|
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 PLUGIN = path.join(ROOT, 'plugins/babel-plugin-lexicon-leaf-imports.js')
|
||||||
const APP_BARREL_ENTRY = path.join(ROOT, 'src/lexicons/index.ts')
|
const APP_BARREL_ENTRY = path.join(ROOT, 'src/lexicons/index.ts')
|
||||||
const SDK_DIST = path.join(ROOT, 'node_modules/@bsky/sdk/dist')
|
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[] {
|
function listFiles(dir: string, exts: string[]): string[] {
|
||||||
const out: string[] = []
|
const out: string[] = []
|
||||||
@@ -42,15 +52,6 @@ function listFiles(dir: string, exts: string[]): string[] {
|
|||||||
return out
|
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>()
|
const hashCache = new Map<string, string>()
|
||||||
function contentHash(file: string): string {
|
function contentHash(file: string): string {
|
||||||
let h = hashCache.get(file)
|
let h = hashCache.get(file)
|
||||||
@@ -61,48 +62,6 @@ function contentHash(file: string): string {
|
|||||||
return 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 =
|
type ChainResult =
|
||||||
{leaf: string; chain: string[]} | {bail: true} | {error: string}
|
{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`}
|
if (!exports) return {error: `${curFile} is not a pure namespace barrel`}
|
||||||
const spec = exports.get(segment)
|
const spec = exports.get(segment)
|
||||||
if (!spec) return {error: `'${chain.join('.')}' not exported by ${curFile}`}
|
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 (!next) return {error: `cannot resolve '${spec}' from ${curFile}`}
|
||||||
if (barrelExports(next) === null) return {leaf: next, chain}
|
if (barrelExports(next) === null) return {leaf: next, chain}
|
||||||
const parent = cur.parentPath
|
const parent = cur.parentPath
|
||||||
@@ -184,7 +143,7 @@ function computeExpectation(file: string, code: string): Expectation {
|
|||||||
if (!isSdk && source === '#/lexicons') {
|
if (!isSdk && source === '#/lexicons') {
|
||||||
entryBarrel = APP_BARREL_ENTRY
|
entryBarrel = APP_BARREL_ENTRY
|
||||||
} else if (isSdk && SDK_BARREL_RE.test(source)) {
|
} else if (isSdk && SDK_BARREL_RE.test(source)) {
|
||||||
entryBarrel = resolveSpec(file, source)
|
entryBarrel = resolveModuleFile(file, source)
|
||||||
}
|
}
|
||||||
if (!entryBarrel) continue
|
if (!entryBarrel) continue
|
||||||
|
|
||||||
@@ -283,11 +242,11 @@ function collectActual(file: string, output: string): Actual {
|
|||||||
s => s.type === 'ImportNamespaceSpecifier' && /^_lex_/.test(s.local.name),
|
s => s.type === 'ImportNamespaceSpecifier' && /^_lex_/.test(s.local.name),
|
||||||
)
|
)
|
||||||
if (ns) {
|
if (ns) {
|
||||||
const resolved = resolveSpec(file, source)
|
const resolved = resolveModuleFile(file, source)
|
||||||
leaves.set(source, resolved ?? `<unresolvable: ${source}>`)
|
leaves.set(source, resolved ?? `<unresolvable: ${source}>`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const resolved = source.startsWith('.') ? resolveSpec(file, source) : null
|
const resolved = source.startsWith('.') ? resolveModuleFile(file, source) : null
|
||||||
if (
|
if (
|
||||||
resolved === APP_BARREL_ENTRY ||
|
resolved === APP_BARREL_ENTRY ||
|
||||||
(isSdk && SDK_BARREL_RE.test(source) && resolved)
|
(isSdk && SDK_BARREL_RE.test(source) && resolved)
|
||||||
|
|||||||
@@ -52,9 +52,15 @@
|
|||||||
*/
|
*/
|
||||||
const fs = require('node:fs')
|
const fs = require('node:fs')
|
||||||
const path = require('node:path')
|
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()
|
const statCache = new Map()
|
||||||
|
|
||||||
/** @returns {'dir' | 'file' | null} */
|
/** @returns {'dir' | 'file' | null} */
|
||||||
@@ -90,58 +96,6 @@ function leafFileFor(dir, segment) {
|
|||||||
return null
|
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 */
|
/** `rootDir\0segments\0leafFile` -> boolean */
|
||||||
const chainCache = new Map()
|
const chainCache = new Map()
|
||||||
|
|
||||||
@@ -164,7 +118,7 @@ function verifyChain(rootDir, segments, leafFile) {
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
const spec = barrelExports(cur)?.get(segment)
|
const spec = barrelExports(cur)?.get(segment)
|
||||||
cur = spec ? resolveBarrelTarget(cur, spec) : null
|
cur = spec ? resolveModuleFile(cur, spec) : null
|
||||||
if (!cur) {
|
if (!cur) {
|
||||||
ok = false
|
ok = false
|
||||||
break
|
break
|
||||||
@@ -198,14 +152,13 @@ function invalidateStaleCaches(rootDir) {
|
|||||||
const prev = rootEpochs.get(rootDir)
|
const prev = rootEpochs.get(rootDir)
|
||||||
if (prev !== undefined && prev !== mtime) {
|
if (prev !== undefined && prev !== mtime) {
|
||||||
statCache.clear()
|
statCache.clear()
|
||||||
barrelExportCache.clear()
|
clearBarrelExportCache()
|
||||||
chainCache.clear()
|
chainCache.clear()
|
||||||
}
|
}
|
||||||
rootEpochs.set(rootDir, mtime)
|
rootEpochs.set(rootDir, mtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SDK_SEGMENT = `${path.sep}@bsky${path.sep}sdk${path.sep}`
|
const SDK_SEGMENT = `${path.sep}@bsky${path.sep}sdk${path.sep}`
|
||||||
const SDK_BARREL_RE = /(^|\/)lexicons\/index\.js$/
|
|
||||||
|
|
||||||
module.exports = function lexiconLeafImports(babel, options = {}) {
|
module.exports = function lexiconLeafImports(babel, options = {}) {
|
||||||
const {types: t} = babel
|
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