Exempt hidden dirs that conventionally hold UI source from the skip rule

Greptile's review correctly flagged a regression in the blanket
hidden-dir skip: .vitepress/theme/*.vue and .storybook/ preview files are
real UI source that the walker scanned before this branch. Both the
walker and the scan-target filter now carry a two-entry allowlist
(HIDDEN_SOURCE_DIRS) for those conventional locations; every other
hidden dir keeps being skipped.

Prepared with AI assistance (Claude Code), directed by @pbakaus.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-25 18:04:55 -07:00
co-authored by Claude Code
parent 9f008ebf82
commit a1a6441ba1
4 changed files with 38 additions and 4 deletions
+7 -1
View File
@@ -16,6 +16,12 @@ const SKIP_DIRS = new Set([
'node_modules', 'dist', 'build', '__pycache__',
]);
// The exceptions to the hidden-dir rule: hidden directories that
// conventionally hold real UI source rather than tooling or vendored code.
// VitePress keeps custom theme components in .vitepress/theme/*.vue, and
// Storybook keeps preview decorators/styles in .storybook/.
const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.storybook']);
const SCANNABLE_EXTENSIONS = new Set([
'.html', '.htm', '.css', '.scss', '.sass', '.less',
'.jsx', '.tsx', '.js', '.ts',
@@ -30,7 +36,7 @@ function walkDir(dir) {
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return files; }
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name)) continue;
if (entry.isDirectory() && entry.name.startsWith('.')) continue;
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) files.push(...walkDir(full));
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
+3 -1
View File
@@ -167,7 +167,9 @@ const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
function isVendoredPath(rel) {
const dirSegments = rel.split(/[\\/]/).slice(0, -1);
return dirSegments.some(
(seg) => seg.startsWith('.') || seg === 'node_modules' || seg === 'dist' || seg === 'build' || seg === '__pycache__',
(seg) =>
(seg.startsWith('.') && seg !== '.vitepress' && seg !== '.storybook') ||
seg === 'node_modules' || seg === 'dist' || seg === 'build' || seg === '__pycache__',
);
}
+17
View File
@@ -152,6 +152,23 @@ describe('gatherSignals', () => {
assert.deepEqual(s.scan.targets, ['src/Hero.tsx']); // harness path filtered out
});
it('keeps hidden-source-dir files (VitePress/Storybook) in scan targets', async () => {
const { execFileSync } = await import('node:child_process');
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
git('init', '-q');
git('config', 'user.email', 't@example.com');
git('config', 'user.name', 'Test');
write('.vitepress/theme/Layout.vue', '<template><div/></template>\n');
write('.claude/skills/impeccable/scripts/detector.js', 'export const x = 1;\n');
git('add', '.');
git('commit', '-qm', 'init');
write('.vitepress/theme/Layout.vue', '<template><span/></template>\n'); // real UI source
write('.claude/skills/impeccable/scripts/detector.js', 'export const x = 2;\n'); // vendored
const s = await gatherSignals(scratch);
assert.equal(s.scan.via, 'git-changes');
assert.deepEqual(s.scan.targets, ['.vitepress/theme/Layout.vue']);
});
it('falls through to source dirs when only harness files changed (#303)', async () => {
const { execFileSync } = await import('node:child_process');
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
+11 -2
View File
@@ -1846,8 +1846,17 @@ describe('walkDir', () => {
write('.cursor/skills/impeccable/example.css');
write('.impeccable/live/preview.html');
write('node_modules/pkg/index.js');
const files = walkDir(tmp);
expect(files).toEqual([path.join(tmp, 'src', 'app.css')]);
// Hidden dirs that conventionally hold real UI source are the
// exception: VitePress themes and Storybook preview files must keep
// being scanned (they were before the hidden-dir rule existed).
write('.vitepress/theme/Layout.vue');
write('.storybook/preview.css');
const files = walkDir(tmp).sort();
expect(files).toEqual([
path.join(tmp, '.storybook', 'preview.css'),
path.join(tmp, '.vitepress', 'theme', 'Layout.vue'),
path.join(tmp, 'src', 'app.css'),
]);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}