Skip to content

Commit 2245c53

Browse files
committed
fix(build): prevent absolute paths in esbuild output and add bundle validation
- Update alias plugin to use original package name (args.path) instead of resolved absolute path when marking as external - Add bundle validation test to detect absolute paths and ensure external dependencies remain external in ESM bundles This fixes the issue where import statements with absolute paths appeared in dist/index.mjs, which prevented consuming packages from building correctly.
1 parent 3b53689 commit 2245c53

2 files changed

Lines changed: 124 additions & 9 deletions

File tree

.config/esbuild.config.mjs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,15 @@ function createAliasPlugin() {
2727
return {
2828
name: 'local-package-aliases',
2929
setup(build) {
30-
// Intercept imports for aliased packages
31-
for (const [packageName, aliasPath] of Object.entries(aliases)) {
32-
// Match both exact package name and subpath imports
30+
// Intercept imports for aliased packages.
31+
for (const [packageName, _aliasPath] of Object.entries(aliases)) {
32+
// Match both exact package name and subpath imports.
3333
build.onResolve(
3434
{ filter: new RegExp(`^${packageName}(/|$)`) },
3535
args => {
36-
// Handle subpath imports like '@socketsecurity/lib/spinner'
37-
const subpath = args.path.slice(packageName.length + 1)
38-
const resolvedPath = subpath
39-
? path.join(aliasPath, subpath)
40-
: aliasPath
41-
return { path: resolvedPath, external: true }
36+
// Mark as external using the original package name to avoid absolute paths in output.
37+
// This ensures require('@socketsecurity/lib') instead of require('/absolute/path/to/socket-lib/dist').
38+
return { path: args.path, external: true }
4239
},
4340
)
4441
}

test/bundle-validation.test.mts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* @fileoverview Bundle validation tests to ensure build output quality.
3+
* Verifies that dist files don't contain absolute paths or external dependencies.
4+
*/
5+
6+
import { promises as fs } from 'node:fs'
7+
import path from 'node:path'
8+
import { fileURLToPath } from 'node:url'
9+
10+
import { describe, expect, it } from 'vitest'
11+
12+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
13+
const packagePath = path.resolve(__dirname, '..')
14+
const distPath = path.join(packagePath, 'dist')
15+
16+
/**
17+
* Check if content contains absolute paths.
18+
* Detects paths like /Users/, C:\, /home/, etc.
19+
*/
20+
function hasAbsolutePaths(content: string): {
21+
hasIssue: boolean
22+
matches: string[]
23+
} {
24+
// Match absolute paths but exclude URLs and node: protocol.
25+
const patterns = [
26+
// Match require('/abs/path') or require('C:\\path').
27+
/require\(["'](?:\/[^"'\n]+|[A-Z]:\\[^"'\n]+)["']\)/g,
28+
// Match import from '/abs/path'.
29+
/import\s+.*?from\s+["'](?:\/[^"'\n]+|[A-Z]:\\[^"'\n]+)["']/g,
30+
]
31+
32+
const matches: string[] = []
33+
for (const pattern of patterns) {
34+
const found = content.match(pattern)
35+
if (found) {
36+
matches.push(...found)
37+
}
38+
}
39+
40+
return {
41+
hasIssue: matches.length > 0,
42+
matches,
43+
}
44+
}
45+
46+
/**
47+
* Check if content is missing external dependencies (they should be import/require statements).
48+
* External dependencies should NOT be bundled inline.
49+
*/
50+
function checkExternalDependencies(content: string): {
51+
missingImports: string[]
52+
hasAllImports: boolean
53+
} {
54+
// Dependencies that should be external (as import/require statements).
55+
const externalDeps = ['@socketsecurity/lib']
56+
57+
const missingImports: string[] = []
58+
59+
for (const dep of externalDeps) {
60+
// Check if the bundle has import or require() statements for this dependency.
61+
// ESM: import { foo } from "@socketsecurity/lib"
62+
// CJS: require("@socketsecurity/lib")
63+
const importPattern = new RegExp(
64+
`(?:import\\s+.*?from\\s+["']${dep.replace('/', '\\/')}|require\\(["']${dep.replace('/', '\\/')}["']\\))`,
65+
)
66+
const hasImport = importPattern.test(content)
67+
68+
if (!hasImport) {
69+
missingImports.push(dep)
70+
}
71+
}
72+
73+
return {
74+
missingImports,
75+
hasAllImports: missingImports.length === 0,
76+
}
77+
}
78+
79+
describe('Bundle validation', () => {
80+
it('should not contain absolute paths in dist/index.mjs', async () => {
81+
const indexPath = path.join(distPath, 'index.mjs')
82+
const content = await fs.readFile(indexPath, 'utf8')
83+
84+
const result = hasAbsolutePaths(content)
85+
86+
if (result.hasIssue) {
87+
console.error('Found absolute paths in bundle:')
88+
for (const match of result.matches) {
89+
console.error(` - ${match}`)
90+
}
91+
}
92+
93+
expect(result.hasIssue, 'Bundle should not contain absolute paths').toBe(
94+
false,
95+
)
96+
})
97+
98+
it('should have external dependencies as import/require statements', async () => {
99+
const indexPath = path.join(distPath, 'index.mjs')
100+
const content = await fs.readFile(indexPath, 'utf8')
101+
102+
const result = checkExternalDependencies(content)
103+
104+
if (!result.hasAllImports) {
105+
console.error(
106+
'Missing import/require statements for external dependencies:',
107+
)
108+
for (const dep of result.missingImports) {
109+
console.error(` - ${dep}`)
110+
}
111+
}
112+
113+
expect(
114+
result.hasAllImports,
115+
'All external dependencies should be import/require statements, not bundled inline',
116+
).toBe(true)
117+
})
118+
})

0 commit comments

Comments
 (0)