Skip to content

Commit d59bc84

Browse files
committed
feat(build): add path shortening and bundle @socketsecurity/lib
- Add createPathShorteningPlugin to shorten module identifiers - Remove @socketsecurity/lib from external array to bundle it - Update alias plugin to skip externalizing @socketsecurity/lib This reduces consumer dependencies by bundling @socketsecurity/lib and makes module identifiers more readable in build output.
1 parent 4548d00 commit d59bc84

File tree

1 file changed

+178
-7
lines changed

1 file changed

+178
-7
lines changed

.config/esbuild.config.mjs

Lines changed: 178 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,178 @@ const rootPath = path.join(__dirname, '..')
1313
const srcPath = path.join(rootPath, 'src')
1414
const distPath = path.join(rootPath, 'dist')
1515

16+
/**
17+
* Plugin to shorten module paths in bundled output with conflict detection.
18+
* Uses @babel/parser and magic-string for precise AST-based modifications.
19+
*/
20+
function createPathShorteningPlugin() {
21+
return {
22+
name: 'shorten-module-paths',
23+
setup(build) {
24+
build.onEnd(async result => {
25+
if (!result.outputFiles && result.metafile) {
26+
// Dynamic imports to avoid adding to production dependencies
27+
const fs = await import('node:fs/promises')
28+
const { parse } = await import('@babel/parser')
29+
const MagicString = (await import('magic-string')).default
30+
31+
const outputs = Object.keys(result.metafile.outputs).filter(f =>
32+
f.endsWith('.js'),
33+
)
34+
35+
for (const outputPath of outputs) {
36+
const content = await fs.readFile(outputPath, 'utf8')
37+
const magicString = new MagicString(content)
38+
39+
// Track module paths and their shortened versions
40+
// Map<originalPath, shortenedPath>
41+
const pathMap = new Map()
42+
// Track shortened paths to detect conflicts
43+
// Map<shortenedPath, originalPath>
44+
const conflictDetector = new Map()
45+
46+
/**
47+
* Shorten a module path and detect conflicts.
48+
*/
49+
function shortenPath(longPath) {
50+
if (pathMap.has(longPath)) {
51+
return pathMap.get(longPath)
52+
}
53+
54+
let shortPath = longPath
55+
56+
// Handle pnpm scoped packages
57+
// node_modules/.pnpm/@scope+pkg@version/node_modules/@scope/pkg/dist/file.js
58+
// -> @scope/pkg/dist/file.js
59+
const scopedPnpmMatch = longPath.match(
60+
/node_modules\/\.pnpm\/@([^+/]+)\+([^@/]+)@[^/]+\/node_modules\/(@[^/]+\/[^/]+)\/(.+)/,
61+
)
62+
if (scopedPnpmMatch) {
63+
const [, _scope, _pkg, packageName, subpath] = scopedPnpmMatch
64+
shortPath = `${packageName}/${subpath}`
65+
} else {
66+
// Handle pnpm non-scoped packages
67+
// node_modules/.pnpm/pkg@version/node_modules/pkg/dist/file.js
68+
// -> pkg/dist/file.js
69+
const pnpmMatch = longPath.match(
70+
/node_modules\/\.pnpm\/([^@/]+)@[^/]+\/node_modules\/([^/]+)\/(.+)/,
71+
)
72+
if (pnpmMatch) {
73+
const [, _pkgName, packageName, subpath] = pnpmMatch
74+
shortPath = `${packageName}/${subpath}`
75+
}
76+
}
77+
78+
// Detect conflicts
79+
if (conflictDetector.has(shortPath)) {
80+
const existingPath = conflictDetector.get(shortPath)
81+
if (existingPath !== longPath) {
82+
// Conflict detected - keep original path
83+
console.warn(
84+
`⚠ Path conflict detected:\n "${shortPath}"\n Maps to: "${existingPath}"\n Also from: "${longPath}"\n Keeping original paths to avoid conflict.`,
85+
)
86+
shortPath = longPath
87+
}
88+
} else {
89+
conflictDetector.set(shortPath, longPath)
90+
}
91+
92+
pathMap.set(longPath, shortPath)
93+
return shortPath
94+
}
95+
96+
// Parse AST to find all string literals containing module paths
97+
try {
98+
const ast = parse(content, {
99+
sourceType: 'module',
100+
plugins: [],
101+
})
102+
103+
// Walk through all comments (esbuild puts module paths in comments)
104+
for (const comment of ast.comments || []) {
105+
if (
106+
comment.type === 'CommentLine' &&
107+
comment.value.includes('node_modules')
108+
) {
109+
const originalPath = comment.value.trim()
110+
const shortPath = shortenPath(originalPath)
111+
112+
if (shortPath !== originalPath) {
113+
// Replace in comment
114+
const commentStart = comment.start
115+
const commentEnd = comment.end
116+
magicString.overwrite(
117+
commentStart,
118+
commentEnd,
119+
`// ${shortPath}`,
120+
)
121+
}
122+
}
123+
}
124+
125+
// Walk through all string literals in __commonJS calls
126+
function walk(node) {
127+
if (!node || typeof node !== 'object') {
128+
return
129+
}
130+
131+
// Check for string literals containing node_modules paths
132+
if (
133+
node.type === 'StringLiteral' &&
134+
node.value &&
135+
node.value.includes('node_modules')
136+
) {
137+
const originalPath = node.value
138+
const shortPath = shortenPath(originalPath)
139+
140+
if (shortPath !== originalPath) {
141+
// Replace the string content (keep quotes)
142+
magicString.overwrite(
143+
node.start + 1,
144+
node.end - 1,
145+
shortPath,
146+
)
147+
}
148+
}
149+
150+
// Recursively walk all properties
151+
for (const key of Object.keys(node)) {
152+
if (key === 'start' || key === 'end' || key === 'loc') {
153+
continue
154+
}
155+
const value = node[key]
156+
if (Array.isArray(value)) {
157+
for (const item of value) {
158+
walk(item)
159+
}
160+
} else {
161+
walk(value)
162+
}
163+
}
164+
}
165+
166+
walk(ast.program)
167+
168+
// Write the modified content
169+
await fs.writeFile(outputPath, magicString.toString(), 'utf8')
170+
} catch (error) {
171+
console.error(
172+
`Failed to shorten paths in ${outputPath}:`,
173+
error.message,
174+
)
175+
// Continue without failing the build
176+
}
177+
}
178+
}
179+
})
180+
},
181+
}
182+
}
183+
16184
/**
17185
* Plugin to handle local package aliases.
18186
* Provides consistent alias resolution across all Socket repos.
187+
* Note: Does not externalize @socketsecurity/lib - that should be bundled.
19188
*/
20189
function createAliasPlugin() {
21190
const aliases = getLocalPackageAliases(rootPath)
@@ -28,8 +197,13 @@ function createAliasPlugin() {
28197
return {
29198
name: 'local-package-aliases',
30199
setup(build) {
31-
// Intercept imports for aliased packages
200+
// Intercept imports for aliased packages (except @socketsecurity/lib which should be bundled)
32201
for (const [packageName, _aliasPath] of Object.entries(aliases)) {
202+
// Skip @socketsecurity/lib - it should be bundled, not externalized
203+
if (packageName === '@socketsecurity/lib') {
204+
continue
205+
}
206+
33207
// Match both exact package name and subpath imports
34208
build.onResolve(
35209
{ filter: new RegExp(`^${packageName}(/|$)`) },
@@ -48,6 +222,7 @@ function createAliasPlugin() {
48222
export const buildConfig = {
49223
entryPoints: [`${srcPath}/index.ts`],
50224
outdir: distPath,
225+
outbase: srcPath,
51226
bundle: true,
52227
format: 'cjs',
53228
platform: 'node',
@@ -63,18 +238,14 @@ export const buildConfig = {
63238
// Preserve module structure for better tree-shaking
64239
splitting: false,
65240

66-
// Use plugin for local package aliases (consistent across all Socket repos)
67-
plugins: [createAliasPlugin()].filter(Boolean),
241+
// Use plugins for local package aliases and path shortening
242+
plugins: [createPathShorteningPlugin(), createAliasPlugin()].filter(Boolean),
68243

69244
// External dependencies
70245
external: [
71246
// Node.js built-ins
72247
...builtinModules,
73248
...builtinModules.map(m => `node:${m}`),
74-
// Package dependencies (should be resolved by consumers)
75-
'@socketsecurity/lib',
76-
'@socketsecurity/registry',
77-
'@socketregistry/packageurl-js',
78249
],
79250

80251
// Banner for generated code

0 commit comments

Comments
 (0)