Skip to content

Commit bb4c6f0

Browse files
committed
refactor(cli-with-sentry): complete build system with compression
- Rename esbuild.cli-sentry.build.mjs to esbuild.config.mjs - Add esbuild.index.config.mjs for index loader - Add compress-cli.mjs for brotli compression - Update build.mjs to include all build steps: - Build CLI bundle (outputs to build/cli.js) - Build index loader (outputs to dist/index.js) - Compress CLI (outputs to dist/cli.js.bz with SHA256) - Copy data/ from cli package - Copy logo images from repo root - Use spawn from @socketsecurity/lib/spawn with WIN32 shell - Change esbuild output from dist/cli.js to build/cli.js - Achieves 87.2% compression (13.67MB → 1.75MB) - Package validation now passes
1 parent cdab32b commit bb4c6f0

File tree

4 files changed

+135
-31
lines changed

4 files changed

+135
-31
lines changed

packages/cli-with-sentry/.config/esbuild.cli-sentry.build.mjs renamed to packages/cli-with-sentry/.config/esbuild.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const cliPath = path.join(__dirname, '..', '..', 'cli')
1919
const config = {
2020
...baseConfig,
2121
entryPoints: [path.join(cliPath, 'src/cli-dispatch-with-sentry.mts')],
22-
outfile: path.join(rootPath, 'dist/cli.js'),
22+
outfile: path.join(rootPath, 'build/cli.js'),
2323

2424
// Override define to enable Sentry build.
2525
define: {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* esbuild configuration for Socket CLI with Sentry index loader.
3+
* Builds the brotli decompression loader that executes the compressed CLI.
4+
*/
5+
6+
import path from 'node:path'
7+
import { fileURLToPath } from 'node:url'
8+
9+
import { build } from 'esbuild'
10+
11+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
12+
const rootPath = path.resolve(__dirname, '..')
13+
const cliPath = path.resolve(__dirname, '../../cli')
14+
15+
const config = {
16+
banner: {
17+
js: '#!/usr/bin/env node',
18+
},
19+
bundle: true,
20+
entryPoints: [path.join(cliPath, 'src', 'index.mts')],
21+
external: [],
22+
format: 'cjs',
23+
minify: true,
24+
outfile: path.join(rootPath, 'dist', 'index.js'),
25+
platform: 'node',
26+
target: 'node18',
27+
treeShaking: true,
28+
}
29+
30+
// Run build if invoked directly.
31+
if (fileURLToPath(import.meta.url) === process.argv[1]) {
32+
build(config).catch(error => {
33+
console.error('Index loader build failed:', error)
34+
process.exitCode = 1
35+
})
36+
}
37+
38+
export default config

packages/cli-with-sentry/scripts/build.mjs

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,55 +5,61 @@
55
*/
66

77
import { promises as fs } from 'node:fs'
8-
import { spawn } from 'node:child_process'
98
import path from 'node:path'
109
import { fileURLToPath } from 'node:url'
10+
11+
import { WIN32 } from '@socketsecurity/lib/constants/platform'
1112
import { logger } from '@socketsecurity/lib/logger'
13+
import { spawn } from '@socketsecurity/lib/spawn'
1214
import colors from 'yoctocolors-cjs'
1315

1416
const __dirname = path.dirname(fileURLToPath(import.meta.url))
1517
const rootPath = path.join(__dirname, '..')
1618
const repoRoot = path.join(__dirname, '../../..')
1719

18-
/**
19-
* Runs a command and returns a promise.
20-
*/
21-
function runCommand(command, args, options = {}) {
22-
return new Promise((resolve, reject) => {
23-
const child = spawn(command, args, {
24-
stdio: 'inherit',
25-
...options,
26-
})
27-
28-
child.on('error', reject)
29-
child.on('exit', code => {
30-
if (code === 0) {
31-
resolve()
32-
} else {
33-
reject(
34-
new Error(`Command failed: ${command} ${args.join(' ')} (exit ${code})`),
35-
)
36-
}
37-
})
38-
})
39-
}
40-
4120
async function main() {
4221
try {
43-
const esbuildConfig = path.join(
44-
rootPath,
45-
'.config/esbuild.cli-sentry.build.mjs',
46-
)
4722
const cliPath = path.join(rootPath, '..', 'cli')
4823

49-
// Run esbuild config directly.
50-
await runCommand('node', [esbuildConfig], {
24+
// Build CLI bundle.
25+
logger.log(`${colors.blue('ℹ')} Building CLI bundle...`)
26+
let result = await spawn('node', ['.config/esbuild.config.mjs'], {
27+
shell: WIN32,
28+
stdio: 'inherit',
5129
cwd: rootPath,
5230
env: {
5331
...process.env,
5432
INLINED_SOCKET_CLI_SENTRY_BUILD: '1',
5533
},
5634
})
35+
if (result.code !== 0) {
36+
throw new Error(`CLI bundle build failed with exit code ${result.code}`)
37+
}
38+
logger.log(`${colors.green('✓')} Built CLI bundle`)
39+
40+
// Build index loader.
41+
logger.log(`${colors.blue('ℹ')} Building index loader...`)
42+
result = await spawn('node', ['.config/esbuild.index.config.mjs'], {
43+
shell: WIN32,
44+
stdio: 'inherit',
45+
cwd: rootPath,
46+
})
47+
if (result.code !== 0) {
48+
throw new Error(`Index loader build failed with exit code ${result.code}`)
49+
}
50+
logger.log(`${colors.green('✓')} Built index loader`)
51+
52+
// Compress CLI.
53+
logger.log(`${colors.blue('ℹ')} Compressing CLI...`)
54+
result = await spawn('node', ['scripts/compress-cli.mjs'], {
55+
shell: WIN32,
56+
stdio: 'inherit',
57+
cwd: rootPath,
58+
})
59+
if (result.code !== 0) {
60+
throw new Error(`CLI compression failed with exit code ${result.code}`)
61+
}
62+
logger.log(`${colors.green('✓')} Compressed CLI`)
5763

5864
// Copy data directory from packages/cli.
5965
logger.log(`${colors.blue('ℹ')} Copying data/ from packages/cli...`)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* @fileoverview Compress build/cli.js with brotli to dist/cli.js.bz.
3+
*
4+
* This script compresses the CLI bundle to reduce npm package size
5+
* from ~13MB to ~1.7MB (87% reduction).
6+
*
7+
* The compressed file is decompressed at runtime by dist/index.js.
8+
*/
9+
10+
import crypto from 'node:crypto'
11+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
12+
import path from 'node:path'
13+
import { fileURLToPath } from 'node:url'
14+
import { brotliCompressSync } from 'node:zlib'
15+
16+
import { logger } from '@socketsecurity/lib/logger'
17+
18+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
19+
const rootPath = path.join(__dirname, '..')
20+
const buildPath = path.join(rootPath, 'build')
21+
const distPath = path.join(rootPath, 'dist')
22+
23+
const cliPath = path.join(buildPath, 'cli.js')
24+
const cliBzPath = path.join(distPath, 'cli.js.bz')
25+
26+
logger.log('')
27+
logger.step('Compressing CLI with brotli...')
28+
29+
// Ensure dist/ directory exists.
30+
mkdirSync(distPath, { recursive: true })
31+
32+
// Read the uncompressed CLI from build/.
33+
const cliCode = readFileSync(cliPath)
34+
const originalSize = cliCode.length
35+
36+
// Compress with brotli (max quality for best compression).
37+
const compressed = brotliCompressSync(cliCode, {
38+
params: {
39+
[0]: 11, // BROTLI_PARAM_QUALITY: 11 (max quality).
40+
},
41+
})
42+
const compressedSize = compressed.length
43+
44+
// Write compressed file to dist/.
45+
writeFileSync(cliBzPath, compressed)
46+
47+
const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(1)
48+
logger.success(
49+
`Compressed: ${(originalSize / 1024 / 1024).toFixed(2)} MB → ${(compressedSize / 1024 / 1024).toFixed(2)} MB (${compressionRatio}% reduction)`,
50+
)
51+
52+
// Generate SHA256 checksum for integrity validation.
53+
const sha256 = crypto.createHash('sha256').update(compressed).digest('hex')
54+
const checksumPath = path.join(distPath, 'cli.js.bz.sha256')
55+
writeFileSync(checksumPath, `${sha256} cli.js.bz\n`)
56+
57+
logger.success(`SHA256: ${sha256}`)
58+
logger.log(`Checksum written to: ${path.relative(rootPath, checksumPath)}`)
59+
60+
logger.log('')

0 commit comments

Comments
 (0)