import { test } from 'brittle'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import { scan } from '../lib/scanner.js'
import { buildSections } from '../lib/sections.js'
const HERE = dirname(fileURLToPath(import.meta.url))
const FIXTURE = resolve(HERE, 'fixtures/connectors')
// The fixture's ground truth, by construction:
//
// index.js -alpha(lib/a.js) -beta(lib/b.js) +alpha(re-export) +entry
// lib/a.js -shared(util.js) +alpha +neverUsed
// lib/b.js -shared(util.js) -missingThing(./nope.js MISSING) +beta
// lib/util.js +shared +alsoDead
//
// So: neverUsed and alsoDead are DEAD (+ that nothing takes).
// missingThing is UNRESOLVED (- that nothing supplies).
// alpha and entry are the module's public boundary, NOT dead.
test('sections: every file becomes a germ with connectors', async (t) => {
const tree = await scan(FIXTURE)
const index = buildSections(tree)
const files = index.sections.filter(s => s.scale === 'file')
t.is(files.length, 6, 'six file germs')
const names = files.map(s => s.germ.name).sort()
t.alike(names, ['examples/demo.js', 'index.js', 'lib/a.js', 'lib/b.js', 'lib/dyn.js', 'lib/util.js'], 'germ names')
})
test('sections: imports are minus connectors, exports are plus', async (t) => {
const tree = await scan(FIXTURE)
const index = buildSections(tree)
const util = index.sections.find(s => s.germ.name === 'lib/util.js')
const plus = util.conns.filter(c => c.pol === '+').map(c => c.type).sort()
t.alike(plus, ['alsoDead', 'shared'], 'util supplies shared + alsoDead')
t.is(util.conns.filter(c => c.pol === '-').length, 0, 'util demands nothing')
const b = index.sections.find(s => s.germ.name === 'lib/b.js')
const minus = b.conns.filter(c => c.pol === '-').map(c => c.type).sort()
t.alike(minus, ['missingThing', 'shared'], 'b demands shared + missingThing')
})
test('sections: mating pairs a minus with the plus that supplies it', async (t) => {
const tree = await scan(FIXTURE)
const index = buildSections(tree)
const a = index.sections.find(s => s.germ.name === 'lib/a.js')
const sharedDemand = a.conns.find(c => c.pol === '-' && c.type === 'shared')
t.ok(sharedDemand.mate, 'a.js -shared is mated')
const mate = index.connectorById(sharedDemand.mate)
t.is(mate.pol, '+', 'mated to a supply')
t.is(mate.type, 'shared', 'same type')
t.is(mate.germId, 'file:connectors-fixture:lib/util.js', 'supplied by util.js')
})
test('sections: unmated plus on a non-entry file is a DEAD export', async (t) => {
const tree = await scan(FIXTURE)
const index = buildSections(tree)
const dead = index.misfits().dead.map(m => m.type).sort()
t.alike(dead, ['alsoDead', 'demo', 'neverUsed'], 'exactly the dead exports')
})
test('sections: entry-file exports are public boundary, never dead', async (t) => {
const tree = await scan(FIXTURE)
const index = buildSections(tree)
const dead = index.misfits().dead.map(m => m.type)
t.absent(dead.includes('entry'), 'entry is public API, not dead')
t.absent(dead.includes('alpha'), 're-exported alpha is public API, not dead')
})
test('sections: unmated minus is an UNRESOLVED demand', async (t) => {
const tree = await scan(FIXTURE)
const index = buildSections(tree)
const unresolved = index.misfits().unresolved.map(m => m.type)
t.alike(unresolved, ['missingThing'], 'the one import that resolves to nothing')
})
test('sections: a dynamically imported export is mated, not dead', async (t) => {
const tree = await scan(FIXTURE)
const index = buildSections(tree)
// index.js reaches lib/dyn.js only via `await import('./lib/dyn.js')`.
// A parser that sees static imports only would call lazyThing a dead export.
const dead = index.misfits().dead.map(m => m.type)
t.absent(dead.includes('lazyThing'), 'lazyThing is imported dynamically, not dead')
const dyn = index.sections.find(s => s.germ.name === 'lib/dyn.js')
const supply = dyn.conns.find(c => c.pol === '+' && c.type === 'lazyThing')
t.is(supply.mates.length, 1, 'mated to exactly one demand')
})
test('sections: a devDependency used in examples/tests is not a leak', async (t) => {
const tree = await scan(FIXTURE)
const index = buildSections(tree)
const m = index.misfits()
// examples/demo.js imports brittle (a devDependency) and b4a (a dependency).
// Neither is missing — reading only `dependencies` would report brittle leaked.
const leaked = m.leak.map(l => l.specifier)
t.absent(leaked.includes('brittle'), 'brittle is declared in devDependencies')
t.absent(leaked.includes('b4a'), 'b4a is declared in dependencies')
const unused = m.unused.map(u => u.specifier)
t.absent(unused.includes('brittle'), 'brittle is imported, so not unused')
t.absent(unused.includes('b4a'), 'b4a is imported, so not unused')
})
test('sections: a re-export is a demand AND a supply', async (t) => {
const tree = await scan(FIXTURE)
const index = buildSections(tree)
// index.js does `export { shared } from './lib/util.js'` and imports nothing.
// That is sugar for import-then-export, so it must create a demand too —
// otherwise nothing ever reaches util.js and it reads as an orphan.
const entry = index.sections.find(s => s.germ.name === 'index.js')
const demand = entry.conns.find(c => c.pol === '-' && c.type === 'shared')
t.ok(demand, 'the re-export creates a demand')
t.ok(demand && demand.mate, 'and that demand is mated to util.js')
const supply = entry.conns.find(c => c.pol === '+' && c.type === 'shared')
t.ok(supply && supply.reExport, 'it is still a supply, marked as a pass-through')
})
test('sections: a disjunct string round-trips the germ', async (t) => {
const tree = await scan(FIXTURE)
const index = buildSections(tree)
const b = index.sections.find(s => s.germ.name === 'lib/b.js')
const d = index.disjunct(b)
t.ok(d.includes('-shared'), 'shows the shared demand')
t.ok(d.includes('-missingThing'), 'shows the missing demand')
t.ok(d.includes('+beta'), 'shows the beta supply')
})