22 * Build script for creating self-executable Socket CLI applications.
33 * Uses Node.js Single Executable Application (SEA) feature.
44 *
5- * IMPORTANT: This builds a FULL SEA with the complete CLI embedded .
5+ * IMPORTANT: This builds a SEA with minimal bootstrap (like smol-node) .
66 * The binary contains:
77 * - Node.js runtime (~50MB)
8- * - Full Socket CLI from packages/cli/dist/cli.js.bz (~1.7MB compressed, decompressed at build time)
9- * - All dependencies and node_modules
8+ * - Minimal bootstrap (~50KB) that downloads @socketsecurity/cli from npm on first run
109 *
11- * Expected binary size: ~110MB+ per platform.
10+ * Expected binary size: ~50MB per platform (Node.js + tiny bootstrap) .
1211 *
1312 * Prerequisites:
14- * - Run `pnpm run build:cli` first to create packages/cli/dist/cli .js.bz
13+ * - Run `pnpm --filter @socketsecurity/bootstrap run build` to create bootstrap-sea .js
1514 *
1615 * Supported platforms:
1716 * - Windows (x64, arm64)
2423 * - Build specific platform: node scripts/build.mjs --platform=darwin --arch=x64
2524 */
2625
27- import crypto from 'node:crypto'
2826import { existsSync , promises as fs } from 'node:fs'
2927import os from 'node:os'
3028import path from 'node:path'
3129import url from 'node:url'
32- import zlib from 'node:zlib'
3330
3431import { parseArgs } from '@socketsecurity/lib/argv/parse'
3532import { WIN32 } from '@socketsecurity/lib/constants/platform'
@@ -503,88 +500,24 @@ async function buildTarget(target, options) {
503500 const fuseSentinel = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2'
504501
505502 logger . log (
506- `\nBuilding full SEA for ${ target . platform } -${ target . arch } ...` ,
503+ `\nBuilding minimal SEA for ${ target . platform } -${ target . arch } ...` ,
507504 )
508- logger . log ( '(Embedding full CLI from packages/cli/dist/cli.js.bz )' )
505+ logger . log ( '(Node.js + minimal bootstrap that downloads CLI from npm )' )
509506
510- // Use the compressed CLI from packages/cli /dist/cli .js.bz and decompress it .
511- const cliBzPath = normalizePath (
512- path . join ( __dirname , '../../..' , 'packages' , 'cli ' , 'dist' , 'cli .js.bz ' ) ,
507+ // Use the bootstrap from packages/bootstrap /dist/bootstrap-sea .js.
508+ const bootstrapPath = normalizePath (
509+ path . join ( __dirname , '../../..' , 'packages' , 'bootstrap ' , 'dist' , 'bootstrap-sea .js' ) ,
513510 )
514511
515- // Check if CLI needs to be built or validated.
516- const checksumPath = normalizePath (
517- path . join ( __dirname , '../../..' , 'packages' , 'cli' , 'dist' , 'cli.js.bz.sha256' ) ,
518- )
519-
520- let needsBuild = false
521-
522- if ( ! existsSync ( cliBzPath ) ) {
523- needsBuild = true
512+ // Check if bootstrap needs to be built.
513+ if ( ! existsSync ( bootstrapPath ) ) {
524514 logger . log ( '' )
525- logger . log ( `${ colors . blue ( 'ℹ' ) } CLI not found, building @socketsecurity/cli package...` )
526- } else if ( ! existsSync ( checksumPath ) ) {
527- needsBuild = true
528- logger . log ( '' )
529- logger . log ( `${ colors . yellow ( '⚠' ) } CLI checksum not found, rebuilding @socketsecurity/cli package...` )
530- } else {
531- // Validate checksum.
532- try {
533- const checksumContent = ( await fs . readFile ( checksumPath , 'utf8' ) ) . trim ( )
534- const expectedHash = checksumContent . split ( / \s + / ) [ 0 ]
535- const cliData = await fs . readFile ( cliBzPath )
536- const actualHash = crypto . createHash ( 'sha256' ) . update ( cliData ) . digest ( 'hex' )
537-
538- if ( expectedHash !== actualHash ) {
539- needsBuild = true
540- logger . log ( '' )
541- logger . log ( `${ colors . yellow ( '⚠' ) } CLI checksum mismatch, rebuilding @socketsecurity/cli package...` )
542- logger . log ( ` Expected: ${ expectedHash } ` )
543- logger . log ( ` Actual: ${ actualHash } ` )
544- } else {
545- // Check if source files are newer than the compressed artifact (freshness check).
546- const cliSrcDir = normalizePath (
547- path . join ( __dirname , '../../..' , 'packages' , 'cli' , 'src' ) ,
548- )
549- const cliBzStat = await fs . stat ( cliBzPath )
550-
551- // Check if any source file is newer than the compressed artifact.
552- let hasNewerSources = false
553- try {
554- const srcFiles = await fs . readdir ( cliSrcDir , { recursive : true } )
555- for ( const file of srcFiles ) {
556- const filePath = path . join ( cliSrcDir , file )
557- const stat = await fs . stat ( filePath )
558-
559- if ( stat . isFile ( ) && stat . mtime > cliBzStat . mtime ) {
560- hasNewerSources = true
561- break
562- }
563- }
564- } catch {
565- // Ignore stat errors - validation succeeded so we're OK to continue.
566- }
567-
568- if ( hasNewerSources ) {
569- needsBuild = true
570- logger . log ( '' )
571- logger . log ( `${ colors . yellow ( '⚠' ) } CLI source files modified, rebuilding @socketsecurity/cli package...` )
572- }
573- }
574- } catch ( e ) {
575- needsBuild = true
576- logger . log ( '' )
577- logger . log ( `${ colors . yellow ( '⚠' ) } CLI validation failed, rebuilding @socketsecurity/cli package...` )
578- logger . log ( ` Error: ${ e . message } ` )
579- }
580- }
581-
582- if ( needsBuild ) {
515+ logger . log ( `${ colors . blue ( 'ℹ' ) } Bootstrap not found, building @socketsecurity/bootstrap package...` )
583516 logger . log ( '' )
584517
585518 const result = await spawn (
586519 'pnpm' ,
587- [ '--filter' , '@socketsecurity/cli ' , 'run' , 'build' ] ,
520+ [ '--filter' , '@socketsecurity/bootstrap ' , 'run' , 'build' ] ,
588521 {
589522 cwd : path . join ( __dirname , '../../..' ) ,
590523 shell : WIN32 ,
@@ -594,32 +527,24 @@ async function buildTarget(target, options) {
594527
595528 if ( result . code !== 0 ) {
596529 throw new Error (
597- `Failed to build @socketsecurity/cli . Exit code: ${ result . code } ` ,
530+ `Failed to build @socketsecurity/bootstrap . Exit code: ${ result . code } ` ,
598531 )
599532 }
600533
601- // Verify CLI was built.
602- if ( ! existsSync ( cliBzPath ) ) {
534+ // Verify bootstrap was built.
535+ if ( ! existsSync ( bootstrapPath ) ) {
603536 throw new Error (
604- `CLI build succeeded but compressed file not found at ${ cliBzPath } ` ,
605- )
606- }
607-
608- // Verify checksum was created.
609- if ( ! existsSync ( checksumPath ) ) {
610- throw new Error (
611- `CLI build succeeded but checksum file not found at ${ checksumPath } ` ,
537+ `Bootstrap build succeeded but file not found at ${ bootstrapPath } ` ,
612538 )
613539 }
614540
615541 logger . log ( '' )
616542 }
617543
618- logger . log ( `Decompressing CLI from: ${ cliBzPath } ` )
544+ logger . log ( `Using bootstrap from: ${ bootstrapPath } ` )
619545
620- // Read and decompress the brotli-compressed CLI.
621- const compressedCli = await fs . readFile ( cliBzPath )
622- const cliCode = zlib . brotliDecompressSync ( compressedCli ) . toString ( 'utf8' )
546+ // Read the bootstrap code.
547+ const bootstrapCode = await fs . readFile ( bootstrapPath , 'utf8' )
623548
624549 // Ensure output directory exists.
625550 await fs . mkdir ( outputDir , { recursive : true } )
@@ -628,8 +553,8 @@ async function buildTarget(target, options) {
628553 const outputPath = normalizePath ( path . join ( outputDir , target . outputName ) )
629554
630555 // Check if we can use cached SEA build.
631- // Hash the CLI bundle and build script since those are the inputs.
632- const sourcePaths = [ cliBzPath , url . fileURLToPath ( import . meta. url ) ]
556+ // Hash the bootstrap and build script since those are the inputs.
557+ const sourcePaths = [ bootstrapPath , url . fileURLToPath ( import . meta. url ) ]
633558
634559 // Store hash in centralized build/.cache/ directory.
635560 const cacheDir = normalizePath ( path . join ( __dirname , '../build/.cache' ) )
@@ -649,77 +574,15 @@ async function buildTarget(target, options) {
649574 // Cache hit! SEA binary is up to date.
650575 logger . log ( '' )
651576 logger . log ( `${ colors . green ( '✓' ) } Using cached SEA binary` )
652- logger . log ( 'CLI bundle unchanged since last build.' )
577+ logger . log ( 'Bootstrap unchanged since last build.' )
653578 logger . log ( '' )
654579 logger . log ( `Binary: ${ outputPath } ` )
655580 logger . log ( '' )
656581 return
657582 }
658583
659- // Create a modified copy of the CLI with SEA compatibility fixes.
660- const modifiedCliPath = normalizePath ( path . join ( outputDir , 'cli-modified.js' ) )
661- let cliContent = cliCode
662-
663- // Fix 1: Replace the sentinel constant with a split string that won't match the sentinel search.
664- cliContent = cliContent . replace (
665- / " N O D E _ S E A _ F U S E _ f c e 6 8 0 a b 2 c c 4 6 7 b 6 e 0 7 2 b 8 b 5 d f 1 9 9 6 b 2 " / g,
666- '"NODE_SEA" + "_FUSE_fce680ab2cc467b6e072b8b5df1996b2"' ,
667- )
668-
669- // Fix 2: Add defensive checks for require.resolve in SEA context.
670- // Replace: if (require.resolve.paths)
671- // With: if (require.resolve && require.resolve.paths)
672- cliContent = cliContent . replace (
673- / i f \s * \( \s * r e q u i r e \. r e s o l v e \. p a t h s \s * \) / g,
674- 'if (require.resolve && require.resolve.paths)' ,
675- )
676-
677- // Fix 3: Add SEA compatibility polyfill after shebang.
678- const seaPolyfill = `
679- // SEA Compatibility Polyfill
680- if (typeof require !== 'undefined' && (!require.resolve || !require.resolve.paths)) {
681- if (!require.resolve) {
682- require.resolve = function(id, options) {
683- // Basic resolve that returns the id for SEA context.
684- return id;
685- };
686- }
687- if (!require.resolve.paths) {
688- require.resolve.paths = function(request) {
689- // Return empty array for SEA context where path resolution isn't available.
690- return [];
691- };
692- }
693- }
694- `
695-
696- // Insert polyfill after shebang (if present) or at the beginning.
697- let modifiedContent
698- if ( cliContent . startsWith ( '#!' ) ) {
699- const firstNewline = cliContent . indexOf ( '\n' )
700- const shebang = cliContent . substring ( 0 , firstNewline + 1 )
701- const rest = cliContent . substring ( firstNewline + 1 )
702- modifiedContent = shebang + seaPolyfill + rest
703- } else {
704- modifiedContent = seaPolyfill + cliContent
705- }
706-
707- await fs . writeFile ( modifiedCliPath , modifiedContent )
708-
709- // Verify transformations were applied correctly.
710- logger . log ( 'Verifying SEA compatibility transformations...' )
711- const verifyScript = normalizePath ( path . join ( __dirname , 'verify-sea-transforms.mjs' ) )
712- try {
713- await spawn ( 'node' , [ verifyScript ] , { stdio : 'pipe' } )
714- logger . log ( `${ colors . green ( '✓' ) } All transformations verified` )
715- } catch ( error ) {
716- logger . error ( `${ colors . yellow ( '⚠' ) } Transformation verification failed` )
717- logger . error ( ' The build will continue, but runtime issues may occur' )
718- // Don't fail the build - polyfill provides runtime safety net.
719- }
720-
721- // Use the modified CLI as the entry point.
722- const entryPoint = modifiedCliPath
584+ // Use the bootstrap directly as the entry point (no modifications needed).
585+ const entryPoint = bootstrapPath
723586
724587 // Download Node.js binary for target platform.
725588 const nodeBinary = await downloadNodeBinary (
@@ -749,18 +612,8 @@ if (typeof require !== 'undefined' && (!require.resolve || !require.resolve.path
749612 const sourceHashComment = await generateHashComment ( sourcePaths )
750613 await fs . writeFile ( hashFilePath , sourceHashComment , 'utf-8' )
751614
752- // Clean up temporary files using trash.
753- const filesToClean = [
754- blobPath ,
755- entryPoint . endsWith ( '.compiled.mjs' ) ? entryPoint : null ,
756- entryPoint . endsWith ( '.mjs' ) && ! entryPoint . endsWith ( '.compiled.mjs' )
757- ? entryPoint
758- : null ,
759- ] . filter ( Boolean )
760-
761- if ( filesToClean . length > 0 ) {
762- await safeDelete ( filesToClean ) . catch ( ( ) => { } )
763- }
615+ // Clean up temporary files (just the blob, bootstrap is preserved).
616+ await safeDelete ( blobPath ) . catch ( ( ) => { } )
764617 } finally {
765618 // Clean up config.
766619 await safeDelete ( configPath ) . catch ( ( ) => { } )
@@ -787,10 +640,10 @@ async function main() {
787640 strict : false ,
788641 } )
789642
790- logger . log ( 'Socket CLI Self-Executable Builder' )
791- logger . log ( '==================================== ' )
643+ logger . log ( 'Socket CLI SEA Builder' )
644+ logger . log ( '======================' )
792645 logger . log (
793- 'Building THIN WRAPPER that downloads @socketsecurity/cli on first use' ,
646+ 'Building SEA with minimal bootstrap ( downloads @socketsecurity/cli on first use) ' ,
794647 )
795648
796649 // Generate and filter targets based on options.
@@ -890,8 +743,8 @@ async function main() {
890743 logger . log ( `\n${ colors . green ( '✓' ) } Build complete!` )
891744 logger . log ( `Output directory: ${ options . outputDir || 'dist/sea' } ` )
892745 logger . log ( `Variant directory: ${ binsDir } ` )
893- logger . log ( '\nNOTE: These binaries are thin wrappers that will download ' )
894- logger . log ( '@socketsecurity/cli from npm on first run.' )
746+ logger . log ( '\nNOTE: These are minimal SEA binaries (Node.js + bootstrap) ' )
747+ logger . log ( 'that download @socketsecurity/cli from npm on first run.' )
895748}
896749
897750// Run if executed directly.
0 commit comments