import { test } from 'brittle'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import { scanStream } from '../lib/scan-stream.js'
import { TreeReducer } from '../lib/reducers/tree-reducer.js'
const HERE = dirname(fileURLToPath(import.meta.url))
const HYPER_FLEEKY = resolve(HERE, '../../')
test('scan-stream: emits scan-start before any other event', async (t) => {
const seen = []
const target = resolve(HYPER_FLEEKY, 'hyper-orgs')
await new Promise((resolveP, reject) => {
const src = scanStream(target)
for (const ev of ['scan-start', 'module-start', 'file-discovered', 'file-parsed', 'import-edge', 'module-end', 'scan-end']) {
src.on(ev, () => { if (!seen.includes(ev)) seen.push(ev) })
}
src.once('scan-end', resolveP)
src.once('error', reject)
})
t.is(seen[0], 'scan-start', 'scan-start fires first')
t.ok(seen.includes('module-start'), 'module-start fired')
t.ok(seen.includes('file-discovered'), 'file-discovered fired')
t.ok(seen.includes('file-parsed'), 'file-parsed fired')
t.is(seen[seen.length - 1], 'scan-end', 'scan-end fires last')
})
test('scan-stream: caller can attach listeners before walk starts', async (t) => {
// The walk is deferred via queueMicrotask, so listeners attached synchronously
// after scanStream() returns must still see scan-start.
const src = scanStream(resolve(HYPER_FLEEKY, 'hyper-orgs'))
let gotStart = false
await new Promise((resolve) => {
src.on('scan-start', () => { gotStart = true })
src.once('scan-end', resolve)
})
t.ok(gotStart, 'listeners attached after scanStream() still see scan-start')
})
test('scan-stream: TreeReducer rebuilds the same shape as walkGraph', async (t) => {
const target = resolve(HYPER_FLEEKY, 'hyper-orgs')
const graph = await new Promise((resolveP, reject) => {
const src = scanStream(target)
const tree = new TreeReducer().attach(src)
src.once('scan-end', () => resolveP(tree.finalize()))
src.once('error', reject)
})
t.ok(graph.modules.size === 1, '1 module')
t.ok(graph.modules.has('hyper-orgs'), 'has hyper-orgs')
t.ok(graph.root.endsWith('hyper-orgs'), 'root is correct')
t.is(graph.projectType, 'js', 'project type detected')
const mod = graph.modules.get('hyper-orgs')
t.ok(mod.entry, 'has entry')
t.ok(mod.files.size > 0, 'has files')
})
test('scan-stream: multiple subscribers see the same events', async (t) => {
let aCount = 0
let bCount = 0
const target = resolve(HYPER_FLEEKY, 'hyper-orgs')
await new Promise((resolveP) => {
const src = scanStream(target)
src.on('file-discovered', () => aCount++)
src.on('file-discovered', () => bCount++)
src.once('scan-end', resolveP)
})
t.is(aCount, bCount, 'both subscribers received same number of events')
t.ok(aCount > 0, 'at least one file event fired')
})
test('scan-stream: shallow mode skips internal files and import resolution', async (t) => {
// hyper-orgs has several files in lib/; shallow mode should only emit events
// for the entry file (index.js) and emit no import edges.
const target = resolve(HYPER_FLEEKY, 'hyper-orgs')
const discoveredFiles = []
const edges = []
let shallowFlag = null
await new Promise((resolveP) => {
const src = scanStream(target, { shallow: true })
src.on('scan-start', e => { shallowFlag = e.shallow })
src.on('file-discovered', e => discoveredFiles.push(e.relInPkg))
src.on('import-edge', e => edges.push(e))
src.once('scan-end', resolveP)
})
t.is(shallowFlag, true, 'scan-start carries shallow: true')
t.is(edges.length, 0, 'no import edges in shallow mode')
t.ok(discoveredFiles.length <= 2, `at most 2 files discovered (got ${discoveredFiles.length}: ${discoveredFiles.join(', ')})`)
// The entry file should be among the discovered files
t.ok(discoveredFiles.some(f => f === 'index.js'), 'entry file (index.js) discovered')
})
test('scan-stream: shallow mode is fast on a multi-module scan', async (t) => {
// Rough timing assertion — shallow scans of a multi-project root should
// complete in well under 10s. This is the whole reason shallow exists.
const start = Date.now()
let moduleCount = 0
await new Promise((resolveP) => {
const src = scanStream(HYPER_FLEEKY, { shallow: true, maxDepth: 1 })
src.on('module-start', () => moduleCount++)
src.once('scan-end', resolveP)
})
const duration = Date.now() - start
t.ok(moduleCount > 10, `found ${moduleCount} modules`)
t.ok(duration < 10000, `shallow scan completed in ${duration}ms (< 10s)`)
})
test('scan-stream: emits import-edge events for resolved local imports', async (t) => {
const edges = []
const target = resolve(HYPER_FLEEKY, 'hyper-orgs')
await new Promise((resolveP) => {
const src = scanStream(target)
src.on('import-edge', e => edges.push(e))
src.once('scan-end', resolveP)
})
t.ok(edges.length > 0, `at least one import edge (got ${edges.length})`)
for (const e of edges) {
t.ok(e.fromRelInPkg, 'edge has from')
t.ok(e.resolvedRelPath, 'edge has resolved path')
t.ok(e.specifier, 'edge has specifier')
}
})