Skip to content

Commit 15dd45f

Browse files
committed
feat(bootstrap): create shared bootstrap package for npm and smol builds
Create new @socketsecurity/bootstrap package with shared utilities: Structure: - src/bootstrap-npm.mts - npm wrapper bootstrap entry point - src/bootstrap-smol.mts - smol binary bootstrap entry point - src/shared/bootstrap-shared.mjs - shared download and execution logic Build outputs: - dist/bootstrap-npm.js - Standard Node.js with node:* requires - dist/bootstrap-smol.js - Transformed for smol with internal/* requires Features: - downloadCli() - Downloads @socketsecurity/cli to ~/.socket/_dlx/ - executeCli() - Executes CLI with proper environment setup - executeCompressedCli() - Handles brotli-compressed CLI execution - findAndExecuteCli() - Main orchestration logic Build system: - esbuild configs for npm and smol variants - smol transform plugin converts node:* to internal/* requires - Both outputs ~1080 KB each Dependencies added to catalog: - semver: 7.6.3 (for version comparison) - del-cli: 6.0.0 (for cleaning build artifacts) - esbuild: 0.24.0 (for building bundles) Cleanup: - Removed old bootstrap files from packages/socket/src/ - Bootstrap logic now centralized in shared package
1 parent 6292382 commit 15dd45f

File tree

12 files changed

+1079
-40
lines changed

12 files changed

+1079
-40
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* esbuild plugin to transform node:* requires to internal/* requires.
3+
*
4+
* This makes the bootstrap compatible with Node.js internal bootstrap context
5+
* for smol builds.
6+
*/
7+
8+
/**
9+
* Create smol transformation plugin.
10+
* @returns {import('esbuild').Plugin}
11+
*/
12+
export function smolTransformPlugin() {
13+
return {
14+
name: 'smol-transform',
15+
setup(build) {
16+
build.onEnd((result) => {
17+
// Get the output files.
18+
const outputs = result.outputFiles
19+
if (!outputs || outputs.length === 0) {
20+
return
21+
}
22+
23+
// Transform each output file.
24+
for (const output of outputs) {
25+
let content = output.text
26+
27+
// Map core module requires to their internal bootstrap equivalents.
28+
// Based on Module.builtinModules from Node.js v24.10.0.
29+
// Format: [moduleName, bootstrapPath]
30+
// Handles both 'node:x' and plain 'x' variants (except prefix-only modules).
31+
const requireMappings = new Map([
32+
// Core modules with internal equivalents.
33+
['child_process', 'internal/child_process'],
34+
['fs', 'fs'],
35+
['fs/promises', 'internal/fs/promises'],
36+
37+
// Stream internals.
38+
['stream', 'stream'],
39+
['stream/promises', 'internal/streams/promises'],
40+
['stream/web', 'internal/webstreams/readablestream'],
41+
42+
// Path variants.
43+
['path', 'path'],
44+
['path/posix', 'path'],
45+
['path/win32', 'path'],
46+
47+
// Core modules available at top level.
48+
['assert', 'assert'],
49+
['assert/strict', 'internal/assert/strict'],
50+
['async_hooks', 'async_hooks'],
51+
['buffer', 'buffer'],
52+
['cluster', 'cluster'],
53+
['console', 'console'],
54+
['constants', 'constants'],
55+
['crypto', 'crypto'],
56+
['dgram', 'dgram'],
57+
['diagnostics_channel', 'diagnostics_channel'],
58+
['dns', 'dns'],
59+
['dns/promises', 'dns/promises'],
60+
['domain', 'domain'],
61+
['events', 'events'],
62+
['http', 'http'],
63+
['http2', 'http2'],
64+
['https', 'https'],
65+
['inspector', 'inspector'],
66+
['inspector/promises', 'inspector/promises'],
67+
['module', 'module'],
68+
['net', 'net'],
69+
['os', 'os'],
70+
['perf_hooks', 'perf_hooks'],
71+
['process', 'process'],
72+
['punycode', 'punycode'],
73+
['querystring', 'querystring'],
74+
['readline', 'readline'],
75+
['readline/promises', 'readline/promises'],
76+
['repl', 'repl'],
77+
['string_decoder', 'string_decoder'],
78+
['sys', 'sys'],
79+
['timers', 'timers'],
80+
['timers/promises', 'timers/promises'],
81+
['tls', 'tls'],
82+
['trace_events', 'trace_events'],
83+
['tty', 'tty'],
84+
['url', 'url'],
85+
['util', 'util'],
86+
['util/types', 'internal/util/types'],
87+
['v8', 'v8'],
88+
['vm', 'vm'],
89+
['wasi', 'wasi'],
90+
['worker_threads', 'worker_threads'],
91+
['zlib', 'zlib'],
92+
])
93+
94+
// Prefix-only modules that have no unprefixed form.
95+
// These ONLY support node:* syntax.
96+
const prefixOnlyModules = new Set([
97+
'node:sea',
98+
'node:sqlite',
99+
'node:test',
100+
'node:test/reporters',
101+
])
102+
103+
// Replace require("node:X") and require("X") with correct bootstrap path.
104+
for (const [moduleName, bootstrapPath] of requireMappings) {
105+
// Handle node:x variant.
106+
content = content.replace(
107+
new RegExp(`require\\(["']node:${moduleName}["']\\)`, 'g'),
108+
`require("${bootstrapPath}")`,
109+
)
110+
// Handle plain x variant (if different from bootstrap path).
111+
// Skip if this is a prefix-only module.
112+
if (moduleName !== bootstrapPath && !prefixOnlyModules.has(`node:${moduleName}`)) {
113+
content = content.replace(
114+
new RegExp(`require\\(["']${moduleName}["']\\)`, 'g'),
115+
`require("${bootstrapPath}")`,
116+
)
117+
}
118+
}
119+
120+
// Update the output content.
121+
output.contents = Buffer.from(content, 'utf8')
122+
}
123+
})
124+
},
125+
}
126+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* esbuild configuration for npm bootstrap.
3+
* Standard Node.js modules (node:* requires).
4+
*/
5+
6+
import { build } from 'esbuild'
7+
import path from 'node:path'
8+
import { fileURLToPath } from 'node:url'
9+
10+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
11+
const rootPath = path.resolve(__dirname, '..')
12+
13+
const config = {
14+
banner: {
15+
js: '#!/usr/bin/env node',
16+
},
17+
bundle: true,
18+
entryPoints: [path.join(rootPath, 'src', 'bootstrap-npm.mts')],
19+
external: [],
20+
format: 'cjs',
21+
metafile: true,
22+
minify: true,
23+
outfile: path.join(rootPath, 'dist', 'bootstrap-npm.js'),
24+
platform: 'node',
25+
target: 'node18',
26+
treeShaking: true,
27+
}
28+
29+
// Run build if invoked directly.
30+
if (fileURLToPath(import.meta.url) === process.argv[1]) {
31+
build(config).catch(error => {
32+
console.error('npm bootstrap build failed:', error)
33+
process.exitCode = 1
34+
})
35+
}
36+
37+
export default config
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* esbuild configuration for smol bootstrap.
3+
* Transforms node:* requires to internal/* for Node.js internal bootstrap context.
4+
*/
5+
6+
import { build } from 'esbuild'
7+
import path from 'node:path'
8+
import { fileURLToPath } from 'node:url'
9+
10+
import { smolTransformPlugin } from './esbuild-plugin-smol-transform.mjs'
11+
12+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
13+
const rootPath = path.resolve(__dirname, '..')
14+
15+
const config = {
16+
banner: {
17+
js: '#!/usr/bin/env node',
18+
},
19+
bundle: true,
20+
entryPoints: [path.join(rootPath, 'src', 'bootstrap-smol.mts')],
21+
external: [],
22+
format: 'cjs',
23+
metafile: true,
24+
minify: true,
25+
outfile: path.join(rootPath, 'dist', 'bootstrap-smol.js'),
26+
platform: 'node',
27+
plugins: [smolTransformPlugin()],
28+
target: 'node24',
29+
treeShaking: true,
30+
write: false, // Plugin needs to transform output.
31+
}
32+
33+
// Run build if invoked directly.
34+
if (fileURLToPath(import.meta.url) === process.argv[1]) {
35+
build(config).catch(error => {
36+
console.error('smol bootstrap build failed:', error)
37+
process.exitCode = 1
38+
})
39+
}
40+
41+
export default config

packages/bootstrap/dist/bootstrap-npm.js

Lines changed: 224 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/bootstrap/dist/bootstrap-smol.js

Lines changed: 224 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/bootstrap/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@socketsecurity/bootstrap",
3+
"version": "1.0.0",
4+
"description": "Shared bootstrap for Socket CLI npm wrapper and smol binary",
5+
"private": true,
6+
"type": "module",
7+
"exports": {
8+
"./bootstrap-npm.js": "./dist/bootstrap-npm.js",
9+
"./bootstrap-smol.js": "./dist/bootstrap-smol.js"
10+
},
11+
"scripts": {
12+
"build": "node scripts/build.mjs",
13+
"clean": "del-cli dist"
14+
},
15+
"devDependencies": {
16+
"@socketsecurity/lib": "catalog:",
17+
"del-cli": "catalog:",
18+
"esbuild": "catalog:",
19+
"semver": "catalog:"
20+
}
21+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Build script for Socket bootstrap package.
4+
*
5+
* Builds two versions:
6+
* 1. bootstrap-npm.js - Standard version for npm wrapper
7+
* 2. bootstrap-smol.js - Transformed version for smol binary
8+
*/
9+
10+
import { mkdirSync, writeFileSync } from 'node:fs'
11+
import path from 'node:path'
12+
import { fileURLToPath } from 'node:url'
13+
14+
import { build } from 'esbuild'
15+
16+
import npmConfig from '../.config/esbuild.npm.config.mjs'
17+
import smolConfig from '../.config/esbuild.smol.config.mjs'
18+
19+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
20+
const packageRoot = path.resolve(__dirname, '..')
21+
22+
console.log('Building Socket bootstrap with esbuild...\\n')
23+
24+
try {
25+
// Create dist directory.
26+
mkdirSync(path.join(packageRoot, 'dist'), { recursive: true })
27+
28+
// Build npm version.
29+
console.log('→ Building npm bootstrap...')
30+
const npmResult = await build(npmConfig)
31+
32+
console.log(`✓ ${npmConfig.outfile}`)
33+
34+
if (npmResult.metafile) {
35+
const outputSize = Object.values(npmResult.metafile.outputs)[0]?.bytes
36+
if (outputSize) {
37+
console.log(` Size: ${(outputSize / 1024).toFixed(2)} KB`)
38+
}
39+
}
40+
41+
// Build smol version.
42+
console.log('\\n→ Building smol bootstrap...')
43+
const smolResult = await build(smolConfig)
44+
45+
// Write the transformed output (build had write: false).
46+
if (smolResult.outputFiles && smolResult.outputFiles.length > 0) {
47+
for (const output of smolResult.outputFiles) {
48+
writeFileSync(output.path, output.contents)
49+
}
50+
}
51+
52+
console.log(`✓ ${smolConfig.outfile}`)
53+
54+
if (smolResult.metafile) {
55+
const outputSize = Object.values(smolResult.metafile.outputs)[0]?.bytes
56+
if (outputSize) {
57+
console.log(` Size: ${(outputSize / 1024).toFixed(2)} KB`)
58+
}
59+
}
60+
61+
console.log('\\n✓ Build completed successfully')
62+
} catch (error) {
63+
console.error('\\n✗ Build failed:', error)
64+
process.exit(1)
65+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Bootstrap for Socket CLI npm wrapper.
3+
*
4+
* This runs when users execute `npx socket` or `npm install -g socket`.
5+
* It downloads @socketsecurity/cli from npm and executes it.
6+
*/
7+
8+
import { findAndExecuteCli, getArgs } from './shared/bootstrap-shared.mjs'
9+
10+
async function main() {
11+
const args = getArgs()
12+
await findAndExecuteCli(args)
13+
}
14+
15+
// Run the bootstrap.
16+
main().catch((e) => {
17+
process.stderr.write(`❌ Bootstrap error: ${e instanceof Error ? e.message : String(e)}\n`)
18+
process.exit(1)
19+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Bootstrap for Socket CLI smol binary.
3+
*
4+
* This runs inside the smol Node.js binary via internal bootstrap.
5+
* Uses Node.js internal/* requires (transformed by esbuild plugin).
6+
*
7+
* The smol binary loads this at startup via lib/internal/process/pre_execution.js.
8+
*/
9+
10+
import { findAndExecuteCli, getArgs } from './shared/bootstrap-shared.mjs'
11+
12+
async function main() {
13+
const args = getArgs()
14+
await findAndExecuteCli(args)
15+
}
16+
17+
// Run the bootstrap.
18+
main().catch((e) => {
19+
process.stderr.write(`❌ Bootstrap error: ${e instanceof Error ? e.message : String(e)}\n`)
20+
process.exit(1)
21+
})

0 commit comments

Comments
 (0)