Skip to content

Commit 3696dd8

Browse files
committed
test(bundle): use Babel AST parser instead of regex
Replaces regex-based dependency detection with AST parsing for more accurate bundle validation. Prevents false positives from string literals containing package names (e.g., SOCKET_REGISTRY_PACKAGE_NAME).
1 parent ea6875a commit 3696dd8

1 file changed

Lines changed: 64 additions & 47 deletions

File tree

test/bundle-validation.test.mts

Lines changed: 64 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { promises as fs } from 'node:fs'
77
import path from 'node:path'
88
import { fileURLToPath } from 'node:url'
99

10+
import { parse } from '@babel/parser'
11+
import traverse from '@babel/traverse'
1012
import { describe, expect, it } from 'vitest'
1113

1214
const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -44,7 +46,7 @@ function hasAbsolutePaths(content: string): {
4446
}
4547

4648
/**
47-
* Check if bundle contains inlined dependencies.
49+
* Check if bundle contains inlined dependencies using AST analysis.
4850
* Reads package.json dependencies and ensures they are NOT bundled inline.
4951
*/
5052
async function checkBundledDependencies(content: string): Promise<{
@@ -58,62 +60,77 @@ async function checkBundledDependencies(content: string): Promise<{
5860

5961
const bundledDeps: string[] = []
6062

61-
// If we have NO dependencies, check that no external packages are bundled.
63+
// Parse the bundle into an AST.
64+
const ast = parse(content, {
65+
sourceType: 'module',
66+
plugins: ['typescript'],
67+
})
68+
69+
// Collect all import sources from the AST.
70+
const importSources = new Set<string>()
71+
// @ts-expect-error - traverse types are complex
72+
traverse(ast, {
73+
ImportDeclaration(path) {
74+
const source = path.node.source.value
75+
importSources.add(source)
76+
},
77+
})
78+
79+
// Packages that should always be external (never bundled).
80+
const socketPackagePatterns = [
81+
/@socketsecurity\/lib/,
82+
/@socketregistry\/packageurl-js/,
83+
/@socketsecurity\/sdk/,
84+
/@socketsecurity\/registry/,
85+
]
86+
87+
// Check if we have runtime dependencies.
6288
if (Object.keys(dependencies).length === 0) {
63-
// Look for signs of bundled npm packages in ESM format.
64-
// Bundled ESM packages have patterns like:
65-
// - import { x } from "inline-bundled-code"
66-
// - Functions from external packages inlined directly.
67-
const bundledPackagePatterns = [
68-
// Socket packages that should always be external.
69-
/@socketsecurity\/lib/,
70-
/@socketsecurity\/packageurl-js/,
71-
/@socketsecurity\/sdk/,
72-
/@socketsecurity\/registry/,
73-
]
74-
75-
for (const pattern of bundledPackagePatterns) {
76-
// For ESM bundles, check if imports are to external packages (good)
77-
// vs having the code inlined (bad).
78-
// Look for: import ... from "@socketsecurity/lib" (external - good)
79-
// vs: no imports but code from the package is present (bundled - bad).
80-
const hasExternalImport = new RegExp(
81-
`import\\s+.*?from\\s+["']${pattern.source}`,
82-
).test(content)
83-
84-
// If no external import found, check if package code might be bundled.
85-
// This is a heuristic - look for multiple characteristic functions.
86-
if (!hasExternalImport) {
87-
// Check for signs of bundled code from this package.
88-
// This would mean the package wasn't properly externalized.
89-
const bundlePattern = new RegExp(
90-
`(?:var|const|let)\\s+\\w+.*${pattern.source}`,
91-
)
89+
// No runtime dependencies - check that Socket packages aren't bundled.
90+
for (const pattern of socketPackagePatterns) {
91+
const hasExternalImport = Array.from(importSources).some(source =>
92+
pattern.test(source),
93+
)
9294

93-
if (bundlePattern.test(content)) {
95+
if (!hasExternalImport) {
96+
// Check if this package name appears in the content at all.
97+
// If it's just in string literals (like constants), that's fine.
98+
// Use AST to check if it appears in any meaningful way.
99+
let foundInCode = false
100+
// @ts-expect-error - traverse types are complex
101+
traverse(ast, {
102+
StringLiteral(path) {
103+
// Skip string literals - these are fine
104+
if (pattern.test(path.node.value)) {
105+
// It's in a string literal, which is fine
106+
}
107+
},
108+
Identifier(path) {
109+
// Check if the package name appears in identifiers or other code
110+
if (
111+
pattern.test(path.node.name) ||
112+
(path.node.name.includes('socketsecurity') &&
113+
pattern.test(path.node.name))
114+
) {
115+
foundInCode = true
116+
}
117+
},
118+
})
119+
120+
// Only flag if we found it in actual code, not just string literals
121+
if (foundInCode) {
94122
bundledDeps.push(pattern.source)
95123
}
96124
}
97125
}
98126
} else {
99-
// If we have dependencies, check that they remain external (not bundled).
127+
// We have runtime dependencies - check that they remain external.
100128
for (const dep of Object.keys(dependencies)) {
101-
const escapedDep = dep.replace(/[/\\^$*+?.()|[\]{}]/g, '\\$&')
102-
103-
// Check if dependency is properly external.
104-
const hasExternalImport = new RegExp(
105-
`import\\s+.*?from\\s+["']${escapedDep}`,
106-
).test(content)
129+
const hasExternalImport = importSources.has(dep)
107130

108-
// If no external import, it might be bundled.
109131
if (!hasExternalImport) {
110-
const bundlePattern = new RegExp(
111-
`(?:var|const|let)\\s+\\w+.*${escapedDep}`,
112-
)
113-
114-
if (bundlePattern.test(content)) {
115-
bundledDeps.push(dep)
116-
}
132+
// Dependency isn't imported externally - it might be bundled
133+
bundledDeps.push(dep)
117134
}
118135
}
119136
}

0 commit comments

Comments
 (0)