Skip to content

Commit 95c373b

Browse files
committed
test: add test to prevent rogue external stubs
Add comprehensive test suite to ensure external dependencies in dist/external are properly bundled and not left as stub re-exports. This test: - Recursively scans all .js files in dist/external - Detects stub re-export patterns that indicate incomplete bundling - Flags very small files (< 50 bytes) that are likely stubs - Allows intentional stubs (like @npmcli/package-json) to be whitelisted - Specifically verifies all @InQuirer modules are properly bundled This prevents a repeat of the issue where @inquirer/input, @inquirer/password, and @inquirer/search were left as stubs, causing socket-cli build failures.
1 parent 88485e3 commit 95c373b

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed

test/build-externals.test.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* @fileoverview Tests to ensure external dependencies are properly bundled.
3+
* This prevents accidental stub re-exports in dist/external.
4+
*/
5+
6+
import { promises as fs } from 'node:fs'
7+
import path from 'node:path'
8+
import { describe, expect, it } from 'vitest'
9+
10+
const rootDir = process.cwd()
11+
const distExternalDir = path.join(rootDir, 'dist', 'external')
12+
13+
// Stub re-export patterns that indicate incomplete bundling
14+
const STUB_PATTERNS = [
15+
/^\s*module\.exports\s*=\s*require\s*\(/,
16+
/^\s*export\s+\{\s*\}\s*from\s+/,
17+
/^\s*export\s+\*\s+from\s+/,
18+
]
19+
20+
/**
21+
* Check if a file content is a stub re-export
22+
*/
23+
function isStubReexport(content: string): boolean {
24+
return STUB_PATTERNS.some(pattern => pattern.test(content.trim()))
25+
}
26+
27+
/**
28+
* Get all .js files in a directory recursively
29+
*/
30+
async function getAllJsFiles(dir: string): Promise<string[]> {
31+
async function walk(currentDir: string): Promise<string[]> {
32+
const entries = await fs.readdir(currentDir, { withFileTypes: true })
33+
const filePromises: Array<Promise<string[]>> = []
34+
35+
for (const entry of entries) {
36+
const fullPath = path.join(currentDir, entry.name)
37+
38+
if (entry.isDirectory()) {
39+
filePromises.push(walk(fullPath))
40+
} else if (entry.isFile() && entry.name.endsWith('.js')) {
41+
filePromises.push(Promise.resolve([fullPath]))
42+
}
43+
}
44+
45+
const results = await Promise.all(filePromises)
46+
return results.flat()
47+
}
48+
49+
return await walk(dir)
50+
}
51+
52+
describe('build-externals', () => {
53+
it('should have bundled dist/external directory', async () => {
54+
try {
55+
await fs.access(distExternalDir)
56+
} catch {
57+
expect.fail(`dist/external directory does not exist at ${distExternalDir}`)
58+
}
59+
})
60+
61+
it('should not have stub re-exports in bundled files', async () => {
62+
const jsFiles = await getAllJsFiles(distExternalDir)
63+
64+
// Should have external files
65+
expect(jsFiles.length).toBeGreaterThan(0)
66+
67+
// Intentional stubs that are copied from src/external as-is (not bundled)
68+
// These are too complex or optional to bundle
69+
const intentionalStubs = [
70+
'@npmcli/package-json/index.js',
71+
'@npmcli/package-json/lib/read-package.js',
72+
'@npmcli/package-json/lib/sort.js',
73+
]
74+
75+
const checkPromises = jsFiles.map(async file => {
76+
const [content, stat] = await Promise.all([
77+
fs.readFile(file, 'utf8'),
78+
fs.stat(file),
79+
])
80+
const relativePath = path.relative(distExternalDir, file)
81+
const issues: Array<{ file: string; reason: string }> = []
82+
83+
// Skip intentional stub files
84+
if (intentionalStubs.some(stub => relativePath.endsWith(stub))) {
85+
return issues
86+
}
87+
88+
// Check for stub re-export patterns
89+
if (isStubReexport(content)) {
90+
issues.push({
91+
file: relativePath,
92+
reason: 'Contains stub re-export pattern',
93+
})
94+
}
95+
96+
// Check for very small files that are likely stubs (< 100 bytes of actual code)
97+
// Exclude files that are intentionally small (like 1-2KB minified)
98+
if (stat.size < 50 && isStubReexport(content)) {
99+
issues.push({
100+
file: relativePath,
101+
reason: `Very small file (${stat.size} bytes) that appears to be a stub`,
102+
})
103+
}
104+
105+
return issues
106+
})
107+
108+
const allIssues = (await Promise.all(checkPromises)).flat()
109+
110+
if (allIssues.length > 0) {
111+
const errorMessage = [
112+
'Found unexpected stub re-exports in dist/external:',
113+
...allIssues.map(
114+
f => ` - ${f.file}: ${f.reason}`,
115+
),
116+
'',
117+
'Make sure these packages are added to the bundling configuration in scripts/build-externals.mjs',
118+
'or add them to the intentionalStubs list if they should remain as stubs.',
119+
].join('\n')
120+
121+
expect.fail(errorMessage)
122+
}
123+
})
124+
125+
it('should have @inquirer modules properly bundled', async () => {
126+
const requiredInquirerModules = ['input', 'password', 'search', 'confirm', 'select']
127+
const inquirerDir = path.join(distExternalDir, '@inquirer')
128+
129+
try {
130+
await fs.access(inquirerDir)
131+
} catch {
132+
expect.fail(`@inquirer directory not found at ${inquirerDir}`)
133+
}
134+
135+
const checkPromises = requiredInquirerModules.map(async module => {
136+
const modulePath = path.join(inquirerDir, `${module}.js`)
137+
138+
try {
139+
const [stat, content] = await Promise.all([
140+
fs.stat(modulePath),
141+
fs.readFile(modulePath, 'utf8'),
142+
])
143+
144+
if (stat.size <= 1000) {
145+
expect.fail(`@inquirer/${module} should be properly bundled (> 1KB), got ${stat.size} bytes`)
146+
}
147+
148+
if (isStubReexport(content)) {
149+
expect.fail(`@inquirer/${module} should not be a stub re-export`)
150+
}
151+
} catch (error) {
152+
expect.fail(`@inquirer/${module} not found or not properly bundled at ${modulePath}: ${error instanceof Error ? error.message : String(error)}`)
153+
}
154+
})
155+
156+
await Promise.all(checkPromises)
157+
})
158+
})

0 commit comments

Comments
 (0)