Skip to content

Commit 54cd716

Browse files
chrisbbreuerclaude
andcommitted
test(server): catch a module no auto-import barrel mentions (#2408)
Adding `app/Jobs/ProbeStaleJob.ts` and committing without running `buddy generate` leaves the barrel and the declarations stale together. They agree with each other, so `generated-declarations.test.ts` - which asks whether each DECLARED global exists at runtime - sees nothing, and the job silently is not a global. Two stale files being consistent is what makes it invisible. This asks the other direction: is there a module on disk that no barrel mentions. Four barrels, 662 modules, all currently covered. Verified against the issue's own scenario - the probe job above is named by the test and by file. This is option 3 from #2408 and deliberately the weak one, so the issue stays open. A real freshness check cannot work while generation is config-driven: which files get emitted depends on `feature()` gates reading config reading env, so CI without `.env.keys` legitimately emits a different set than a developer machine, and the diff looks like staleness when nothing is stale. Options 1 and 2 there - generate under a canonical config, or stop committing these - are what make the output reproducible, and both change what apps receive. Matching is on the module's own name, because the barrel spells app modules `../../../app/Jobs/X` and framework ones `../defaults/app/Jobs/X` and both are right. Tests, `.d.ts` and `index.ts` are excluded: 76 of the 698 files under `Actions/` are `.test.ts` and belong in no barrel. Refs #2408 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4ded068 commit 54cd716

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Every source file that should be auto-imported appears in its barrel.
3+
*
4+
* `generated-declarations.test.ts` next door asks the opposite question -
5+
* whether each DECLARED global exists at runtime. Both directions are needed,
6+
* because adding `app/Jobs/ProbeStaleJob.ts` and committing without running
7+
* `buddy generate` leaves the barrel and the declarations stale TOGETHER. They
8+
* agree with each other, so the existing check sees nothing, and the job is
9+
* silently not a global (stacksjs/stacks#2408).
10+
*
11+
* This is option 3 from that issue, and deliberately the weak one. A real
12+
* freshness check - regenerate and diff - cannot work while generation is
13+
* config-driven: which files get emitted depends on `feature()` gates that read
14+
* config that reads env, so CI without `.env.keys` legitimately produces a
15+
* different set than a developer machine. Options 1 and 2 there (generate under
16+
* a canonical config, or stop committing these) are the ones that make the
17+
* output reproducible, and both change what apps receive.
18+
*
19+
* What this does catch is the reported case: a file on disk that no barrel
20+
* mentions. It says nothing about ordering, contents, or the declarations.
21+
*/
22+
import { describe, expect, it } from 'bun:test'
23+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
24+
import { basename, extname, join } from 'node:path'
25+
26+
const root = new URL('../../../../../', import.meta.url).pathname
27+
28+
/** Barrel file → the directories whose modules it should re-export. */
29+
const barrels: Record<string, string[]> = {
30+
'jobs.ts': ['app/Jobs', 'storage/framework/defaults/app/Jobs'],
31+
'listeners.ts': ['app/Listeners', 'storage/framework/defaults/app/Listeners'],
32+
'actions.ts': ['app/Actions', 'storage/framework/defaults/app/Actions'],
33+
'middleware.ts': ['app/Middleware', 'storage/framework/defaults/app/Middleware'],
34+
}
35+
36+
/** Modules, not the things that sit beside them. */
37+
function sourceFiles(dir: string, found: string[] = []): string[] {
38+
if (!existsSync(dir))
39+
return found
40+
41+
for (const entry of readdirSync(dir)) {
42+
const full = join(dir, entry)
43+
if (statSync(full).isDirectory()) {
44+
if (!['dist', 'node_modules', 'tests', '__tests__'].includes(entry))
45+
sourceFiles(full, found)
46+
continue
47+
}
48+
49+
// `index.ts` is a barrel itself; `.d.ts` declares rather than defines; a
50+
// test beside the module it tests is not an import target.
51+
if (!entry.endsWith('.ts') || entry === 'index.ts')
52+
continue
53+
if (entry.endsWith('.d.ts') || entry.endsWith('.test.ts') || entry.endsWith('.spec.ts'))
54+
continue
55+
56+
found.push(full)
57+
}
58+
59+
return found
60+
}
61+
62+
describe('auto-import barrels', () => {
63+
for (const [barrel, dirs] of Object.entries(barrels)) {
64+
it(`mentions every module under ${dirs.map(dir => `${dir}/`).join(' and ')}`, () => {
65+
const barrelPath = join(root, 'storage/framework/auto-imports', barrel)
66+
if (!existsSync(barrelPath))
67+
return
68+
69+
// Match on the module's own name rather than the whole path: the barrel
70+
// spells app files `../../../app/Jobs/X` and framework ones
71+
// `../defaults/app/Jobs/X`, and both are correct.
72+
const referenced = new Set(
73+
[...readFileSync(barrelPath, 'utf-8').matchAll(/'([^']+)'/g)]
74+
.map(match => basename(match[1]!, extname(match[1]!))),
75+
)
76+
77+
const absent = dirs
78+
.flatMap(dir => sourceFiles(join(root, dir)))
79+
.filter(file => !referenced.has(basename(file, '.ts')))
80+
.map(file => file.replace(root, ''))
81+
82+
expect(absent.sort()).toEqual([])
83+
})
84+
}
85+
})

0 commit comments

Comments
 (0)