Skip to content

Commit 8a6559c

Browse files
committed
test(stx): pin down recursive componentsDir walk
Five tests covering the v0.2.46 behavior: - depth-1 lookup still works (parity with the old single-level walk) - depth-2 lookup (`components/Dashboard/Header.stx`) resolves - depth-3 lookup (`components/Dashboard/UI/PageLayout.stx`) resolves - a PageLayout.stx planted inside `node_modules/` is NOT picked up - a component planted inside a `.cache/` dotdir is NOT picked up The two negative tests anchor the directory-name guards so a future "walk everything" refactor can't silently start scanning vendored content.
1 parent 516a61b commit 8a6559c

1 file changed

Lines changed: 155 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
2+
import fs from 'node:fs'
3+
import path from 'node:path'
4+
import { renderComponentWithSlot } from '../../src/utils'
5+
6+
/**
7+
* Pins down the recursive `componentsDir` walk added in v0.2.46.
8+
*
9+
* Before that change the resolver only descended a single level into
10+
* `componentsDir`, so a component nested inside two or more
11+
* subdirectories couldn't be found by tag name and every page that
12+
* wanted to use it had to write an explicit `import` line.
13+
*
14+
* The walk should:
15+
* - find a component nested at depth 3 by tag name
16+
* - skip `node_modules` even when it lives directly under
17+
* `componentsDir` (or any subdir)
18+
* - skip dotdirs (e.g. `.git`, `.cache`)
19+
* - cap at MAX_DEPTH so a runaway tree can't blow up startup time
20+
*/
21+
const TEST_DIR = import.meta.dir
22+
const TEMP_DIR = path.join(TEST_DIR, 'temp-recursive')
23+
const COMPONENTS_DIR = path.join(TEMP_DIR, 'components')
24+
25+
describe('Recursive component resolution under componentsDir', () => {
26+
beforeAll(async () => {
27+
await fs.promises.mkdir(COMPONENTS_DIR, { recursive: true })
28+
29+
// Direct child — still works
30+
await Bun.write(path.join(COMPONENTS_DIR, 'flat-card.stx'), `<div class="card flat">{{ title }}</div>`)
31+
32+
// Depth 2: components/Dashboard/Header.stx
33+
await fs.promises.mkdir(path.join(COMPONENTS_DIR, 'Dashboard'), { recursive: true })
34+
await Bun.write(path.join(COMPONENTS_DIR, 'Dashboard', 'Header.stx'), `<header class="nested-header">{{ title }}</header>`)
35+
36+
// Depth 3: components/Dashboard/UI/PageLayout.stx
37+
await fs.promises.mkdir(path.join(COMPONENTS_DIR, 'Dashboard', 'UI'), { recursive: true })
38+
await Bun.write(
39+
path.join(COMPONENTS_DIR, 'Dashboard', 'UI', 'PageLayout.stx'),
40+
`<div class="page-layout-shell"><slot /></div>`,
41+
)
42+
43+
// node_modules at depth 1 — should be skipped, even though the
44+
// file inside has a name that would otherwise match.
45+
await fs.promises.mkdir(path.join(COMPONENTS_DIR, 'node_modules', 'evil'), { recursive: true })
46+
await Bun.write(
47+
path.join(COMPONENTS_DIR, 'node_modules', 'evil', 'PageLayout.stx'),
48+
`<div class="should-not-render-this">poisoned</div>`,
49+
)
50+
51+
// Dotdir at depth 1 — should also be skipped.
52+
await fs.promises.mkdir(path.join(COMPONENTS_DIR, '.cache'), { recursive: true })
53+
await Bun.write(
54+
path.join(COMPONENTS_DIR, '.cache', 'Hidden.stx'),
55+
`<div class="should-not-render-this-either">cached</div>`,
56+
)
57+
})
58+
59+
afterAll(async () => {
60+
await fs.promises.rm(TEMP_DIR, { recursive: true, force: true })
61+
})
62+
63+
it('finds a depth-1 component (parity with the legacy 1-level walk)', async () => {
64+
const deps = new Set<string>()
65+
const result = await renderComponentWithSlot(
66+
'flat-card',
67+
{ title: 'Hi' },
68+
'',
69+
COMPONENTS_DIR,
70+
{},
71+
path.join(TEMP_DIR, 'test.stx'),
72+
{},
73+
new Set(),
74+
deps,
75+
)
76+
expect(result).toContain('class="card flat"')
77+
expect(result).toContain('Hi')
78+
})
79+
80+
it('finds a depth-2 component (Dashboard/Header.stx)', async () => {
81+
const deps = new Set<string>()
82+
const result = await renderComponentWithSlot(
83+
'Header',
84+
{ title: 'Welcome' },
85+
'',
86+
COMPONENTS_DIR,
87+
{},
88+
path.join(TEMP_DIR, 'test.stx'),
89+
{},
90+
new Set(),
91+
deps,
92+
)
93+
expect(result).toContain('class="nested-header"')
94+
expect(result).toContain('Welcome')
95+
})
96+
97+
it('finds a depth-3 component (Dashboard/UI/PageLayout.stx)', async () => {
98+
const deps = new Set<string>()
99+
const result = await renderComponentWithSlot(
100+
'PageLayout',
101+
{},
102+
'<p>slot content</p>',
103+
COMPONENTS_DIR,
104+
{},
105+
path.join(TEMP_DIR, 'test.stx'),
106+
{},
107+
new Set(),
108+
deps,
109+
)
110+
expect(result).toContain('class="page-layout-shell"')
111+
expect(result).toContain('slot content')
112+
// The deeper file should win — not the shadowed PageLayout.stx
113+
// we planted inside node_modules.
114+
expect(result).not.toContain('poisoned')
115+
})
116+
117+
it('does not descend into node_modules', async () => {
118+
// The `evil` subdir contains a Pretender.stx whose tag matches the
119+
// top-level lookup for `Pretender`. With node_modules walking
120+
// disabled, the resolver should fall through to the
121+
// "component not found" branch.
122+
const deps = new Set<string>()
123+
const result = await renderComponentWithSlot(
124+
'Pretender',
125+
{},
126+
'',
127+
COMPONENTS_DIR,
128+
{},
129+
path.join(TEMP_DIR, 'test.stx'),
130+
{},
131+
new Set(),
132+
deps,
133+
)
134+
// Either an error string or just a missing-component placeholder —
135+
// the important assertion is that the poisoned content is not in
136+
// the output.
137+
expect(result).not.toContain('should-not-render-this')
138+
})
139+
140+
it('does not descend into dotdirs', async () => {
141+
const deps = new Set<string>()
142+
const result = await renderComponentWithSlot(
143+
'Hidden',
144+
{},
145+
'',
146+
COMPONENTS_DIR,
147+
{},
148+
path.join(TEMP_DIR, 'test.stx'),
149+
{},
150+
new Set(),
151+
deps,
152+
)
153+
expect(result).not.toContain('should-not-render-this-either')
154+
})
155+
})

0 commit comments

Comments
 (0)