Skip to content

Commit 3fda4fe

Browse files
committed
feat(socket): add esbuild-based bootstrap implementation
Add new bootstrap wrapper implementation for @socketbin/socket package: New files: - src/bootstrap.mts: Bootstrap entry point with IPC stub path passing - src/bootstrap/ipc.mts: Inter-process communication for stub path - scripts/build-bootstrap.mjs: Build script using esbuild - scripts/esbuild.bootstrap.config.mjs: esbuild configuration - dist/bootstrap.js: Built bootstrap bundle Package changes: - Update bin entry to use dist/bootstrap.js - Add build script for bootstrap - Simpler, more maintainable build process The bootstrap wrapper passes its own path via IPC to enable self-update detection for Socket-managed binaries.
1 parent 9a1edec commit 3fda4fe

File tree

5 files changed

+350
-2
lines changed

5 files changed

+350
-2
lines changed

packages/socket/dist/bootstrap.js

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

packages/socket/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@
1313
"email": "eng@socket.dev",
1414
"url": "https://socket.dev"
1515
},
16-
"devDependencies": {},
1716
"engines": {
1817
"node": ">=18"
1918
},
2019
"files": [
21-
"bin/",
2220
"dist/"
2321
],
2422
"optionalDependencies": {
@@ -32,8 +30,16 @@
3230
"@socketbin/cli-win32-x64": "0.0.0"
3331
},
3432
"scripts": {
33+
"build": "node scripts/build-bootstrap.mjs",
3534
"test": "vitest run",
3635
"test:coverage": "vitest run --coverage",
3736
"test:watch": "vitest"
37+
},
38+
"bin": {
39+
"socket": "./dist/bootstrap.js"
40+
},
41+
"devDependencies": {
42+
"@socketsecurity/lib": "^2.0.0",
43+
"esbuild": "^0.24.0"
3844
}
3945
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Build script for Socket npm wrapper bootstrap.
3+
*/
4+
5+
import { build } from 'esbuild'
6+
7+
import config from './esbuild.bootstrap.config.mjs'
8+
9+
console.log('Building Socket npm wrapper bootstrap with esbuild...\n')
10+
11+
try {
12+
const result = await build(config)
13+
14+
console.log('✓ Build completed successfully')
15+
console.log(`✓ Output: ${config.outfile}`)
16+
17+
if (result.metafile) {
18+
const outputSize = Object.values(result.metafile.outputs)[0]?.bytes
19+
if (outputSize) {
20+
console.log(`✓ Bundle size: ${(outputSize / 1024).toFixed(2)} KB`)
21+
}
22+
}
23+
} catch (error) {
24+
console.error('Build failed:', error)
25+
process.exit(1)
26+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* esbuild configuration for Socket npm wrapper bootstrap.
3+
*/
4+
5+
import path from 'node:path'
6+
import { fileURLToPath } from 'node:url'
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
9+
const packageRoot = path.resolve(__dirname, '..')
10+
11+
export default {
12+
banner: {
13+
js: '#!/usr/bin/env node',
14+
},
15+
bundle: true,
16+
entryPoints: [path.join(packageRoot, 'src', 'bootstrap.mts')],
17+
external: [],
18+
format: 'cjs',
19+
metafile: true,
20+
minify: true,
21+
outfile: path.join(packageRoot, 'dist', 'bootstrap.js'),
22+
platform: 'node',
23+
target: 'node18',
24+
treeShaking: true,
25+
}

packages/socket/src/bootstrap.mts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* Bootstrap loader for Socket CLI npm wrapper package.
3+
*
4+
* This script handles three scenarios:
5+
* 1. Brotli-compressed CLI exists -> decompress and execute
6+
* 2. Uncompressed CLI exists -> execute directly
7+
* 3. CLI not found -> download from npm using dlxPackage, then execute
8+
*/
9+
10+
import { spawnSync } from 'node:child_process'
11+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
12+
import { homedir, tmpdir } from 'node:os'
13+
import path from 'node:path'
14+
import { brotliDecompressSync } from 'node:zlib'
15+
16+
import type { DlxPackageResult } from '@socketsecurity/lib/dlx-package'
17+
18+
import { dlxPackage } from '@socketsecurity/lib/dlx-package'
19+
20+
const SOCKET_DLX_DIR = path.join(homedir(), '.socket', '_dlx')
21+
22+
// Note: CLI_PACKAGE_DIR will be dynamically determined by dlxPackage.
23+
let cliPackageDir: string | undefined
24+
let cliEntry: string | undefined
25+
let cliEntryBz: string | undefined
26+
27+
/**
28+
* Get paths for CLI package.
29+
*/
30+
function getCliPaths(): { cliEntry: string; cliEntryBz: string } {
31+
if (!cliPackageDir) {
32+
throw new Error('CLI package directory not initialized')
33+
}
34+
return {
35+
cliEntry: path.join(cliPackageDir, 'node_modules', '@socketsecurity', 'cli', 'dist', 'cli.js'),
36+
cliEntryBz: path.join(cliPackageDir, 'node_modules', '@socketsecurity', 'cli', 'dist', 'cli.js.bz'),
37+
}
38+
}
39+
40+
/**
41+
* Get command-line arguments.
42+
*/
43+
function getArgs(): string[] {
44+
return process.argv ? process.argv.slice(2) : []
45+
}
46+
47+
/**
48+
* Execute the CLI with the given arguments.
49+
*/
50+
function executeCli(cliPath: string, args: string[]): never {
51+
const result = spawnSync(process.execPath, [cliPath, ...args], {
52+
env: {
53+
...process.env,
54+
PKG_EXECPATH: process.env.PKG_EXECPATH || 'PKG_INVOKE_NODEJS',
55+
},
56+
stdio: 'inherit',
57+
})
58+
process.exit(result.status ?? 0)
59+
}
60+
61+
/**
62+
* Execute brotli-compressed CLI.
63+
*/
64+
function executeCompressedCli(bzPath: string, args: string[]): never {
65+
// Read compressed file.
66+
const compressed = readFileSync(bzPath)
67+
68+
// Decompress with brotli.
69+
const decompressed = brotliDecompressSync(compressed)
70+
71+
// Write to temporary file and execute.
72+
// Using a temp file allows us to maintain spawn behavior for proper stdio handling.
73+
const tempCliPath = path.join(tmpdir(), `socket-cli-${process.pid}.js`)
74+
writeFileSync(tempCliPath, decompressed)
75+
76+
try {
77+
executeCli(tempCliPath, args)
78+
} finally {
79+
// Clean up temp file.
80+
try {
81+
unlinkSync(tempCliPath)
82+
} catch {
83+
// Ignore cleanup errors.
84+
}
85+
}
86+
}
87+
88+
/**
89+
* Download and install CLI using dlxPackage.
90+
*/
91+
async function downloadCli(): Promise<DlxPackageResult> {
92+
process.stderr.write('📦 Socket CLI not found, downloading...\n')
93+
process.stderr.write('\n')
94+
95+
// Create directories.
96+
mkdirSync(SOCKET_DLX_DIR, { recursive: true })
97+
98+
try {
99+
// Use dlxPackage to download and install @socketsecurity/cli.
100+
const result = await dlxPackage(
101+
[], // Empty args - we don't want to execute anything.
102+
{
103+
force: false, // Use cached version if available.
104+
package: '@socketsecurity/cli',
105+
spawnOptions: {
106+
stdio: 'pipe', // Suppress output from the package execution.
107+
},
108+
},
109+
)
110+
111+
// Save the package directory for later use.
112+
cliPackageDir = result.packageDir
113+
114+
process.stderr.write(` Installed to: ${cliPackageDir}\n`)
115+
116+
// Wait for installation to complete (but the spawn will fail since we don't have a command).
117+
// That's okay - we just need the package installed.
118+
try {
119+
await result.spawnPromise
120+
} catch {
121+
// Ignore execution errors - we only care that the package was installed.
122+
}
123+
124+
process.stderr.write('✅ Socket CLI installed successfully\n')
125+
process.stderr.write('\n')
126+
127+
return result
128+
} catch (e) {
129+
process.stderr.write('❌ Failed to download Socket CLI\n')
130+
process.stderr.write(` Error: ${e instanceof Error ? e.message : String(e)}\n`)
131+
process.exit(1)
132+
}
133+
}
134+
135+
/**
136+
* Main bootstrap logic.
137+
*/
138+
async function main(): Promise<void> {
139+
const args = getArgs()
140+
141+
// Check if CLI is already installed by trying to locate it in dlx cache.
142+
// We need to download it first to find out where it is.
143+
await downloadCli()
144+
145+
// Now get the paths.
146+
const { cliEntry, cliEntryBz } = getCliPaths()
147+
148+
// Check for brotli-compressed CLI first.
149+
if (existsSync(cliEntryBz)) {
150+
executeCompressedCli(cliEntryBz, args)
151+
}
152+
153+
// Fallback to uncompressed CLI.
154+
if (existsSync(cliEntry)) {
155+
executeCli(cliEntry, args)
156+
}
157+
158+
// If we still can't find the CLI, exit with error.
159+
process.stderr.write('❌ Socket CLI installation failed\n')
160+
process.stderr.write(' CLI entry point not found after installation\n')
161+
process.stderr.write(` Looked in: ${cliEntry}\n`)
162+
process.exit(1)
163+
}
164+
165+
// Run the bootstrap.
166+
main().catch((e) => {
167+
process.stderr.write(`❌ Bootstrap error: ${e instanceof Error ? e.message : String(e)}\n`)
168+
process.exit(1)
169+
})

0 commit comments

Comments
 (0)