Skip to content

Commit ed564c5

Browse files
committed
fix(build): normalize Windows paths in export validation
Normalize path separators to forward slashes before checking if path starts with 'external/' to fix Windows build failures. Use normalizePath from #socketsecurity/lib/paths.
1 parent 76389a4 commit ed564c5

File tree

1 file changed

+127
-0
lines changed

1 file changed

+127
-0
lines changed

scripts/validate/dist-exports.mjs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* @fileoverview Validate that all dist/* exports work correctly without .default
3+
* Ensures require('./dist/foo') returns the actual value, not wrapped in { default: value }
4+
*/
5+
6+
import { createRequire } from 'node:module'
7+
import { readdirSync } from 'node:fs'
8+
import path from 'node:path'
9+
import { fileURLToPath } from 'node:url'
10+
11+
import colors from 'yoctocolors-cjs'
12+
13+
import { isQuiet } from '#socketsecurity/lib/argv/flags'
14+
import { normalizePath } from '#socketsecurity/lib/paths'
15+
import { pluralize } from '#socketsecurity/lib/words'
16+
17+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
18+
const distDir = path.resolve(__dirname, '..', 'dist')
19+
const require = createRequire(import.meta.url)
20+
21+
/**
22+
* Get all .js files in a directory recursively.
23+
*/
24+
function getJsFiles(dir, files = []) {
25+
const entries = readdirSync(dir, { withFileTypes: true })
26+
27+
for (const entry of entries) {
28+
const fullPath = path.join(dir, entry.name)
29+
30+
if (entry.isDirectory()) {
31+
getJsFiles(fullPath, files)
32+
} else if (entry.isFile() && entry.name.endsWith('.js')) {
33+
files.push(fullPath)
34+
}
35+
}
36+
37+
return files
38+
}
39+
40+
/**
41+
* Check if a module export needs .default or works directly.
42+
*/
43+
function checkExport(filePath) {
44+
// Skip external packages - they are internal implementation details
45+
// used by public dist/* modules. We only validate public exports.
46+
const relativePath = path.relative(distDir, filePath)
47+
// Normalize path for cross-platform compatibility (Windows uses backslashes)
48+
const normalizedPath = normalizePath(relativePath)
49+
if (normalizedPath.startsWith('external/')) {
50+
return { path: filePath, ok: true, skipped: true }
51+
}
52+
53+
try {
54+
const mod = require(filePath)
55+
56+
// Handle primitive exports (strings, numbers, etc.)
57+
if (typeof mod !== 'object' || mod === null) {
58+
return { path: filePath, ok: true }
59+
}
60+
61+
const hasDefault = 'default' in mod && mod.default !== undefined
62+
63+
// If module has .default and the direct export is empty/different,
64+
// it's likely incorrectly exported
65+
if (hasDefault) {
66+
const directKeys = Object.keys(mod).filter(k => k !== 'default')
67+
// If only key is 'default', the export is wrapped incorrectly
68+
if (directKeys.length === 0) {
69+
return {
70+
path: filePath,
71+
ok: false,
72+
reason: 'Export wrapped in { default: value } - needs .default',
73+
}
74+
}
75+
}
76+
77+
return { path: filePath, ok: true }
78+
} catch (error) {
79+
return {
80+
path: filePath,
81+
ok: false,
82+
reason: `Failed to require: ${error.message}`,
83+
}
84+
}
85+
}
86+
87+
async function main() {
88+
const quiet = isQuiet()
89+
const verbose = process.argv.includes('--verbose')
90+
91+
if (!quiet && verbose) {
92+
console.log(`${colors.cyan('→')} Validating dist exports`)
93+
}
94+
95+
const files = getJsFiles(distDir)
96+
const results = files.map(checkExport)
97+
const failures = results.filter(r => !r.ok)
98+
99+
const checked = results.filter(r => !r.skipped)
100+
101+
if (failures.length > 0) {
102+
if (!quiet) {
103+
console.error(
104+
colors.red('✗') +
105+
` Found ${failures.length} public ${pluralize('export', { count: failures.length })} with incorrect exports:`,
106+
)
107+
for (const failure of failures) {
108+
const relativePath = path.relative(distDir, failure.path)
109+
console.error(` ${colors.red('✗')} ${relativePath}`)
110+
console.error(` ${failure.reason}`)
111+
}
112+
}
113+
process.exitCode = 1
114+
} else {
115+
if (!quiet) {
116+
console.log(
117+
colors.green('✓') +
118+
` Validated ${checked.length} public ${pluralize('export', { count: checked.length })} - all work without .default`,
119+
)
120+
}
121+
}
122+
}
123+
124+
main().catch(error => {
125+
console.error(`${colors.red('✗')} Validation failed:`, error.message)
126+
process.exitCode = 1
127+
})

0 commit comments

Comments
 (0)