diff --git a/docs/build-guide.md b/docs/build-guide.md index 8a104d709..5a13a0cde 100644 --- a/docs/build-guide.md +++ b/docs/build-guide.md @@ -284,23 +284,25 @@ pnpm clean Located in `packages/cli/.config/`: -| Config | Output | Description | -| -------------------------- | --------------- | ------------------ | -| `esbuild.cli.build.mjs` | `build/cli.js` | Main CLI bundle | -| `esbuild.index.config.mjs` | `dist/index.js` | Entry point loader | +| Config | Output | Description | +| -------------------- | --------------- | ------------------------------------------------ | +| `esbuild.cli.mjs` | `build/cli.js` | Main CLI bundle — bundles all source into one JS | +| `esbuild.index.mjs` | `dist/index.js` | Entry point loader — thin shim that loads cli.js | +| `esbuild.build.mjs` | (orchestrator) | Runs both cli and index builds in parallel | ### Build Variants -The unified esbuild config (`esbuild.config.mjs`) orchestrates all variants: +The orchestrator (`esbuild.build.mjs`) accepts an optional variant argument: ```bash -# Build all variants -node .config/esbuild.config.mjs all +# Build all variants (default) +node .config/esbuild.build.mjs -# Build specific variant -node .config/esbuild.config.mjs cli -node .config/esbuild.config.mjs index -node .config/esbuild.config.mjs inject +# Build only the CLI bundle +node .config/esbuild.build.mjs cli + +# Build only the entry point loader +node .config/esbuild.build.mjs index ``` --- diff --git a/packages/build-infra/README.md b/packages/build-infra/README.md index ec830a275..e14184def 100644 --- a/packages/build-infra/README.md +++ b/packages/build-infra/README.md @@ -14,10 +14,10 @@ Shared build infrastructure utilities for Socket CLI. Provides esbuild plugins, │ │ Unicode │ │ API Client │ │ SHA256 │ │ │ │ Transform │ │ + Download │ │ Content │ │ │ │ │ │ │ │ Hashing │ │ -│ ├───────────────┤ ├──────────────┤ └──────────┤ │ -│ │ Dead Code │ │ Asset Cache │ │ Skip │ │ -│ │ Elimination │ │ (1hr TTL) │ │ Regen │ │ -│ └───────────────┘ └──────────────┘ └──────────┘ │ +│ └───────────────┘ ├──────────────┤ └──────────┤ │ +│ │ Asset Cache │ │ Skip │ │ +│ │ (1hr TTL) │ │ Regen │ │ +│ └──────────────┘ └──────────┘ │ │ │ │ Helpers │ │ ┌───────────────────────────────────────────────────────┐ │ @@ -72,30 +72,6 @@ export default { - Replaces unsupported patterns with `/(?:)/` (no-op) - Removes `/u` and `/v` flags after transformation -#### `deadCodeEliminationPlugin()` - -Removes unreachable code branches based on constant boolean conditions. Simplifies bundled output by eliminating dead paths. - -```javascript -import { deadCodeEliminationPlugin } from 'build-infra/lib/esbuild-plugin-dead-code-elimination' - -export default { - plugins: [deadCodeEliminationPlugin()], -} -``` - -**Transformations:** - -- `if (false) { deadCode() }` → `` (removed) -- `if (true) { liveCode() } else { deadCode() }` → `liveCode()` (unwrapped) -- `if (false) { } else { liveCode() }` → `liveCode()` (unwrapped) - -**Implementation:** - -- Uses Babel parser + MagicString for safe AST transformations -- Only processes `.js` files in esbuild output -- Applies transformations in reverse order to maintain positions - ### esbuild Helpers #### `IMPORT_META_URL_BANNER` @@ -296,10 +272,9 @@ ensureOutputDir('/path/to/output/file.js') ### esbuild Configuration ```javascript -// .config/esbuild.config.mjs +// .config/esbuild.cli.mjs import { IMPORT_META_URL_BANNER } from 'build-infra/lib/esbuild-helpers' import { unicodeTransformPlugin } from 'build-infra/lib/esbuild-plugin-unicode-transform' -import { deadCodeEliminationPlugin } from 'build-infra/lib/esbuild-plugin-dead-code-elimination' export default { entryPoints: ['src/cli.mts'], @@ -317,7 +292,7 @@ export default { 'import.meta.url': '__importMetaUrl', }, - plugins: [unicodeTransformPlugin(), deadCodeEliminationPlugin()], + plugins: [unicodeTransformPlugin()], } ``` @@ -438,8 +413,7 @@ Assets are cached per tag to avoid re-downloading across builds. **Consumers:** -- `packages/cli/.config/esbuild.cli.build.mjs` - Main CLI bundle config -- `packages/cli/.config/esbuild.inject.config.mjs` - Shadow npm inject config +- `packages/cli/.config/esbuild.cli.mjs` - Main CLI bundle config - `packages/cli/scripts/download-assets.mjs` - Unified asset downloader - `packages/cli/scripts/sea-build-utils/builder.mjs` - SEA binary builder diff --git a/packages/build-infra/lib/esbuild-plugin-dead-code-elimination.mjs b/packages/build-infra/lib/esbuild-plugin-dead-code-elimination.mjs deleted file mode 100644 index ac2a58101..000000000 --- a/packages/build-infra/lib/esbuild-plugin-dead-code-elimination.mjs +++ /dev/null @@ -1,156 +0,0 @@ -/** - * @fileoverview esbuild plugin for dead code elimination. - * - * Removes unreachable code branches like `if (false) { ... }` and `if (true) { } else { ... }`. - * Uses Babel parser + magic-string for safe AST-based transformations. - * - * @example - * import { deadCodeEliminationPlugin } from 'build-infra/lib/esbuild-plugin-dead-code-elimination' - * - * export default { - * plugins: [deadCodeEliminationPlugin()], - * } - */ - -import { parse } from '@babel/parser' -import { default as traverseImport } from '@babel/traverse' -import MagicString from 'magic-string' - -const traverse = - typeof traverseImport === 'function' ? traverseImport : traverseImport.default - -/** - * Evaluate a test expression to determine if it's a constant boolean. - * - * @param {import('@babel/types').Node} test - Test expression node - * @returns {boolean | null} true/false if constant, null if dynamic - */ -function evaluateTest(test) { - if (test.type === 'BooleanLiteral') { - return test.value - } - if (test.type === 'UnaryExpression' && test.operator === '!') { - const argValue = evaluateTest(test.argument) - return argValue !== null ? !argValue : null - } - return null -} - -/** - * Remove dead code branches from JavaScript code. - * - * @param {string} code - JavaScript code to transform - * @returns {string} Transformed code with dead branches removed - */ -function removeDeadCode(code) { - const ast = parse(code, { - sourceType: 'module', - plugins: [], - }) - - const s = new MagicString(code) - const nodesToRemove = [] - - traverse(ast, { - IfStatement(path) { - const testValue = evaluateTest(path.node.test) - - if (testValue === false) { - // if (false) { ... } [else { ... }]. - // Remove entire if statement, keep else block if present. - if (path.node.alternate) { - // Replace if statement with else block content. - const { alternate } = path.node - if (alternate.type === 'BlockStatement') { - // Remove braces from else block. - const start = alternate.start + 1 - const end = alternate.end - 1 - const elseContent = code.slice(start, end) - nodesToRemove.push({ - start: path.node.start, - end: path.node.end, - replacement: elseContent, - }) - } else { - // Single statement else. - nodesToRemove.push({ - start: path.node.start, - end: path.node.end, - replacement: code.slice(alternate.start, alternate.end), - }) - } - } else { - // No else block, remove entire if statement. - nodesToRemove.push({ - start: path.node.start, - end: path.node.end, - replacement: '', - }) - } - } else if (testValue === true) { - // if (true) { ... } [else { ... }]. - // Keep consequent, remove else block. - const { consequent } = path.node - if (consequent.type === 'BlockStatement') { - // Remove braces from consequent block. - const start = consequent.start + 1 - const end = consequent.end - 1 - const consequentContent = code.slice(start, end) - nodesToRemove.push({ - start: path.node.start, - end: path.node.end, - replacement: consequentContent, - }) - } else { - // Single statement consequent. - nodesToRemove.push({ - start: path.node.start, - end: path.node.end, - replacement: code.slice(consequent.start, consequent.end), - }) - } - } - }, - }) - - // Apply replacements in reverse order to maintain correct positions. - for (const node of nodesToRemove.reverse()) { - s.overwrite(node.start, node.end, node.replacement) - } - - return s.toString() -} - -/** - * Create esbuild plugin for dead code elimination. - * - * @returns {import('esbuild').Plugin} esbuild plugin - */ -export function deadCodeEliminationPlugin() { - return { - name: 'dead-code-elimination', - setup(build) { - build.onEnd(result => { - const outputs = result.outputFiles - if (!outputs || !outputs.length) { - return - } - - for (const output of outputs) { - // Only process JavaScript files. - if (!output.path.endsWith('.js')) { - continue - } - - let content = output.text - - // Remove dead code branches. - content = removeDeadCode(content) - - // Update the output content. - output.contents = Buffer.from(content, 'utf8') - } - }) - }, - } -} diff --git a/packages/build-infra/package.json b/packages/build-infra/package.json index 36c3380bc..fbcf59474 100644 --- a/packages/build-infra/package.json +++ b/packages/build-infra/package.json @@ -6,7 +6,6 @@ "type": "module", "exports": { "./lib/esbuild-helpers": "./lib/esbuild-helpers.mjs", - "./lib/esbuild-plugin-dead-code-elimination": "./lib/esbuild-plugin-dead-code-elimination.mjs", "./lib/esbuild-plugin-unicode-transform": "./lib/esbuild-plugin-unicode-transform.mjs", "./lib/extraction-cache": "./lib/extraction-cache.mjs", "./lib/github-error-utils": "./lib/github-error-utils.mjs", diff --git a/packages/cli/.config/esbuild.build.mjs b/packages/cli/.config/esbuild.build.mjs new file mode 100644 index 000000000..b662f3ee6 --- /dev/null +++ b/packages/cli/.config/esbuild.build.mjs @@ -0,0 +1,58 @@ +/** + * esbuild build orchestrator for Socket CLI. + * Builds all variants (CLI bundle + entry point) in parallel. + * + * Usage: + * node .config/esbuild.build.mjs # Build all variants + * node .config/esbuild.build.mjs cli # Build CLI bundle + * node .config/esbuild.build.mjs index # Build entry point + */ + +import { fileURLToPath } from 'node:url' + +import { getDefaultLogger } from '@socketsecurity/lib/logger' + +import { runBuild } from '../scripts/esbuild-utils.mjs' +import cliConfig from './esbuild.cli.mjs' +import indexConfig from './esbuild.index.mjs' + +const logger = getDefaultLogger() + +export const CONFIGS = { + __proto__: null, + cli: cliConfig, + index: indexConfig, +} + +async function main() { + const variant = process.argv[2] || 'all' + + if (variant !== 'all' && !(variant in CONFIGS)) { + logger.error(`Unknown variant: ${variant}`) + logger.error(`Available variants: all, ${Object.keys(CONFIGS).join(', ')}`) + process.exitCode = 1 + return + } + + const targets = + variant === 'all' + ? Object.entries(CONFIGS) + : [[variant, CONFIGS[variant]]] + + const results = await Promise.allSettled( + targets.map(({ 0: name, 1: config }) => runBuild(config, name)), + ) + const failed = results.filter(r => r.status === 'rejected') + if (failed.length > 0) { + process.exitCode = 1 + } +} + +if (fileURLToPath(import.meta.url) === process.argv[1]) { + main().catch(error => { + logger.error('Build failed:', error) + process.exitCode = 1 + }) +} + +export default CONFIGS diff --git a/packages/cli/.config/esbuild.cli.build.mjs b/packages/cli/.config/esbuild.cli.build.mjs deleted file mode 100644 index 254283947..000000000 --- a/packages/cli/.config/esbuild.cli.build.mjs +++ /dev/null @@ -1,408 +0,0 @@ -/** - * esbuild configuration for building Socket CLI as a SINGLE unified file. - * - * esbuild is much faster than Rollup and doesn't have template literal corruption issues. - */ - -import { existsSync, readFileSync } from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { IMPORT_META_URL_BANNER } from 'build-infra/lib/esbuild-helpers' -import { unicodeTransformPlugin } from 'build-infra/lib/esbuild-plugin-unicode-transform' - -import { - createBuildRunner, - createDefineEntries, - envVarReplacementPlugin, - getInlinedEnvVars, -} from '../scripts/esbuild-shared.mjs' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const rootPath = path.join(__dirname, '..') - -// Get all inlined environment variables from shared utility. -const inlinedEnvVars = getInlinedEnvVars() - -// Regex pattern for matching relative paths to socket-lib's external/ directory. -// Matches ./external/, ../external/, ../../external/, etc. -// Supports both forward slashes (Unix/Mac) and backslashes (Windows). -const socketLibExternalPathRegExp = /^(?:\.[/\\]|(?:\.\.[/\\])+)external[/\\]/ - -// Helper to find socket-lib directory (either local sibling or node_modules). -function findSocketLibPath(importerPath) { - // Try to extract socket-lib base path from the importer. - const match = importerPath.match(/^(.*\/@socketsecurity\/lib)\b/) - if (match) { - return match[1] - } - - // Fallback to local sibling directory. - const localPath = path.join(rootPath, '..', '..', '..', 'socket-lib') - if (existsSync(localPath)) { - return localPath - } - - return null -} - -// CLI build must use published packages only - no local sibling directories. -// This ensures the CLI is properly isolated and doesn't depend on local dev setup. -const socketPackages = {} - -// Resolve subpath from package.json exports. -function resolvePackageSubpath(packagePath, subpath) { - try { - const pkgJsonPath = path.join(packagePath, 'package.json') - const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) - const exports = pkgJson.exports || {} - - // Try exact export match. - const exportKey = subpath === '.' ? '.' : `./${subpath}` - if (exports[exportKey]) { - const exportValue = exports[exportKey] - // Handle conditional exports. - if (typeof exportValue === 'object' && exportValue.default) { - return path.join(packagePath, exportValue.default) - } - // Handle simple string exports. - if (typeof exportValue === 'string') { - return path.join(packagePath, exportValue) - } - } - - // Fallback: try conventional paths. - const distPath = path.join(packagePath, 'dist', subpath) - if (existsSync(`${distPath}.js`)) { - return `${distPath}.js` - } - if (existsSync(`${distPath}.mjs`)) { - return `${distPath}.mjs` - } - if (existsSync(path.join(distPath, 'index.js'))) { - return path.join(distPath, 'index.js') - } - if (existsSync(path.join(distPath, 'index.mjs'))) { - return path.join(distPath, 'index.mjs') - } - } catch {} - - return null -} - -const config = { - entryPoints: [path.join(rootPath, 'src/cli-dispatch.mts')], - bundle: true, - outfile: path.join(rootPath, 'build/cli.js'), - // Target Node.js environment (not browser). - platform: 'node', - // Target Node.js 18+ features. - target: 'node18', - format: 'cjs', - - // With platform: 'node', esbuild automatically externalizes all Node.js built-ins. - external: [], - - // Suppress warnings for intentional CommonJS compatibility code. - logOverride: { - 'commonjs-variable-in-esm': 'silent', - // Suppress warnings about require.resolve for node-gyp (it's external). - 'require-resolve-not-external': 'silent', - }, - - // Add loader for .cs files (node-gyp on Windows). - loader: { - '.cs': 'empty', - }, - - // Source maps off for production. - sourcemap: false, - - // Don't minify (keep readable for debugging). - minify: false, - - // Keep names for better stack traces. - keepNames: true, - - // Plugin needs to transform output. - write: false, - - // Generate metafile for debugging. - metafile: true, - - // Define environment variables and import.meta. - define: { - 'process.env.NODE_ENV': '"production"', - 'import.meta.url': '__importMetaUrl', - // Inject build metadata using shared utility. - ...createDefineEntries(inlinedEnvVars), - }, - - // Add shebang and import.meta.url polyfill at top of bundle. - banner: { - js: `#!/usr/bin/env node\n"use strict";\n${IMPORT_META_URL_BANNER.js}`, - }, - - // Handle special cases with plugins. - plugins: [ - // Environment variable replacement must run AFTER unicode transform. - envVarReplacementPlugin(inlinedEnvVars), - unicodeTransformPlugin(), - { - name: 'resolve-socket-packages', - setup(build) { - // Resolve local Socket packages with subpath exports. - for (const [packageName, packagePath] of Object.entries( - socketPackages, - )) { - // Handle package root imports. - build.onResolve( - { filter: new RegExp(`^${packageName.replace('/', '\\/')}$`) }, - () => { - if (!existsSync(packagePath)) { - return null - } - const resolved = resolvePackageSubpath(packagePath, '.') - if (resolved) { - return { path: resolved } - } - return null - }, - ) - - // Handle subpath imports. - build.onResolve( - { filter: new RegExp(`^${packageName.replace('/', '\\/')}\\/`) }, - args => { - if (!existsSync(packagePath)) { - return null - } - const subpath = args.path.slice(packageName.length + 1) - const resolved = resolvePackageSubpath(packagePath, subpath) - if (resolved) { - return { path: resolved } - } - return null - }, - ) - } - }, - }, - - { - name: 'resolve-socket-lib-internals', - setup(build) { - build.onResolve({ filter: /^\.\.\/constants\// }, args => { - // Only handle imports from socket-lib's dist directory. - if (!args.importer.includes('/socket-lib/dist/')) { - return null - } - - const socketLibPath = findSocketLibPath(args.importer) - if (!socketLibPath) { - return null - } - - const constantName = args.path.replace(/^\.\.\/constants\//, '') - const resolvedPath = path.join( - socketLibPath, - 'dist', - 'constants', - `${constantName}.js`, - ) - if (existsSync(resolvedPath)) { - return { path: resolvedPath } - } - return null - }) - - build.onResolve({ filter: /^\.\.\/\.\.\/constants\// }, args => { - // Handle ../../constants/ imports. - if (!args.importer.includes('/socket-lib/dist/')) { - return null - } - - const socketLibPath = findSocketLibPath(args.importer) - if (!socketLibPath) { - return null - } - - const constantName = args.path.replace(/^\.\.\/\.\.\/constants\//, '') - const resolvedPath = path.join( - socketLibPath, - 'dist', - 'constants', - `${constantName}.js`, - ) - if (existsSync(resolvedPath)) { - return { path: resolvedPath } - } - return null - }) - - // Resolve relative paths to socket-lib's external/ directory. - // Handles ./external/, ../external/, ../../external/, etc. - // Supports both forward slashes and backslashes for cross-platform compatibility. - // This supports any nesting depth in socket-lib's dist/ directory structure. - build.onResolve({ filter: socketLibExternalPathRegExp }, args => { - // Only handle imports from socket-lib's dist directory. - if (!args.importer.includes('@socketsecurity/lib/dist/')) { - return null - } - - const socketLibPath = findSocketLibPath(args.importer) - if (!socketLibPath) { - return null - } - - // Extract the package path after the relative prefix and external/, and remove .js extension. - // Handles both forward slashes and backslashes. - const externalPath = args.path - .replace(socketLibExternalPathRegExp, '') - .replace(/\.js$/, '') - - // Build the resolved path to socket-lib's bundled external. - let resolvedPath = null - if (externalPath.startsWith('@')) { - // Scoped package like @npmcli/arborist. - const [scope, name] = externalPath.split('/') - const scopedPath = path.join( - socketLibPath, - 'dist', - 'external', - scope, - `${name}.js`, - ) - if (existsSync(scopedPath)) { - resolvedPath = scopedPath - } - } else { - // Regular package. - const packageName = externalPath.split('/')[0] - const regularPath = path.join( - socketLibPath, - 'dist', - 'external', - `${packageName}.js`, - ) - if (existsSync(regularPath)) { - resolvedPath = regularPath - } - } - - if (resolvedPath) { - return { path: resolvedPath } - } - - return null - }) - - // Resolve external dependencies that socket-lib bundles in dist/external/. - // Automatically handles any bundled dependency (e.g., @inquirer/*, zod, semver). - build.onResolve({ filter: /^(@[^/]+\/[^/]+|[^./][^/]*)/ }, args => { - if (!args.importer.includes('/socket-lib/dist/')) { - return null - } - - const socketLibPath = findSocketLibPath(args.importer) - if (!socketLibPath) { - return null - } - - // Extract package name (handle scoped packages). - const packageName = args.path.startsWith('@') - ? args.path.split('/').slice(0, 2).join('/') - : args.path.split('/')[0] - - // Check if this package has a bundled version in dist/external/. - let resolvedPath = null - if (packageName.startsWith('@')) { - // Scoped package like @inquirer/confirm. - const [scope, name] = packageName.split('/') - const scopedPath = path.join( - socketLibPath, - 'dist', - 'external', - scope, - `${name}.js`, - ) - if (existsSync(scopedPath)) { - resolvedPath = scopedPath - } - } else { - // Regular package like zod, semver, etc. - const regularPath = path.join( - socketLibPath, - 'dist', - 'external', - `${packageName}.js`, - ) - if (existsSync(regularPath)) { - resolvedPath = regularPath - } - } - - if (resolvedPath) { - return { path: resolvedPath } - } - - return null - }) - }, - }, - - { - name: 'yoga-wasm-alias', - setup(build) { - // Redirect yoga-layout to our custom synchronous implementation. - build.onResolve({ filter: /^yoga-layout$/ }, () => { - return { - path: path.join(rootPath, 'build/yoga-sync.mjs'), - } - }) - }, - }, - - { - name: 'stub-problematic-packages', - setup(build) { - // Stub iconv-lite and encoding to avoid bundling issues. - build.onResolve({ filter: /^(iconv-lite|encoding)(\/|$)/ }, args => { - return { - path: args.path, - namespace: 'stub', - } - }) - - build.onLoad({ filter: /.*/, namespace: 'stub' }, () => { - return { - contents: 'module.exports = {}', - loader: 'js', - } - }) - }, - }, - - { - name: 'ignore-unsupported-files', - setup(build) { - // Prevent bundling @npmcli/arborist from workspace node_modules. - // This includes the main package and all subpaths like /lib/edge.js. - build.onResolve({ filter: /@npmcli\/arborist/ }, args => { - // Only redirect if it's not already coming from socket-lib's external bundle. - if (args.importer.includes('/socket-lib/dist/')) { - return null - } - return { path: args.path, external: true } - }) - - // Mark node-gyp as external (used by arborist but optionally resolved). - build.onResolve({ filter: /node-gyp/ }, args => { - return { path: args.path, external: true } - }) - }, - }, - ], -} - -export default createBuildRunner(config, 'CLI bundle', import.meta) diff --git a/packages/cli/.config/esbuild.cli.mjs b/packages/cli/.config/esbuild.cli.mjs new file mode 100644 index 000000000..b558518eb --- /dev/null +++ b/packages/cli/.config/esbuild.cli.mjs @@ -0,0 +1,206 @@ +/** + * esbuild configuration for building Socket CLI as a SINGLE unified file. + * + * esbuild is much faster than Rollup and doesn't have template literal corruption issues. + */ + +import { existsSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { IMPORT_META_URL_BANNER } from 'build-infra/lib/esbuild-helpers' +import { unicodeTransformPlugin } from 'build-infra/lib/esbuild-plugin-unicode-transform' + +import { + createDefineEntries, + envVarReplacementPlugin, + getInlinedEnvVars, + runBuild, +} from '../scripts/esbuild-utils.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') + +const inlinedEnvVars = getInlinedEnvVars() + +// Matches ./external/, ../external/, ../../external/, etc. (forward and back slashes). +const socketLibExternalPathRegExp = /^(?:\.[/\\]|(?:\.\.[/\\])+)external[/\\]/ + +function findSocketLibPath(importerPath) { + const match = importerPath.match(/^(.*\/@socketsecurity\/lib)\b/) + if (match) { + return match[1] + } + const localPath = path.join(rootPath, '..', '..', '..', 'socket-lib') + if (existsSync(localPath)) { + return localPath + } + return null +} + +function resolveSocketLibExternal(socketLibPath, packageName) { + if (packageName.startsWith('@')) { + const [scope, name] = packageName.split('/') + const p = path.join(socketLibPath, 'dist', 'external', scope, `${name}.js`) + return existsSync(p) ? p : null + } + const p = path.join( + socketLibPath, + 'dist', + 'external', + `${packageName.split('/')[0]}.js`, + ) + return existsSync(p) ? p : null +} + + +const config = { + entryPoints: [path.join(rootPath, 'src/cli-dispatch.mts')], + bundle: true, + outfile: path.join(rootPath, 'build/cli.js'), + platform: 'node', + target: 'node18', + format: 'cjs', + logOverride: { + 'commonjs-variable-in-esm': 'silent', + 'require-resolve-not-external': 'silent', + }, + // .cs files used by node-gyp on Windows. + loader: { '.cs': 'empty' }, + sourcemap: false, + minify: false, + keepNames: true, + write: false, + metafile: true, + define: { + 'process.env.NODE_ENV': '"production"', + 'import.meta.url': '__importMetaUrl', + ...createDefineEntries(inlinedEnvVars), + }, + banner: { + js: `#!/usr/bin/env node\n"use strict";\n${IMPORT_META_URL_BANNER.js}`, + }, + + plugins: [ + // Environment variable replacement must run AFTER unicode transform. + envVarReplacementPlugin(inlinedEnvVars), + unicodeTransformPlugin(), + { + name: 'resolve-socket-lib-internals', + setup(build) { + function resolveConstant(args, strip) { + if (!args.importer.includes('/socket-lib/dist/')) { + return null + } + const socketLibPath = findSocketLibPath(args.importer) + if (!socketLibPath) { + return null + } + const p = path.join( + socketLibPath, + 'dist', + 'constants', + `${args.path.replace(strip, '')}.js`, + ) + return existsSync(p) ? { path: p } : null + } + + build.onResolve({ filter: /^\.\.\/constants\// }, args => + resolveConstant(args, /^\.\.\/constants\//), + ) + + build.onResolve({ filter: /^\.\.\/\.\.\/constants\// }, args => + resolveConstant(args, /^\.\.\/\.\.\/constants\//), + ) + + build.onResolve({ filter: socketLibExternalPathRegExp }, args => { + if (!args.importer.includes('@socketsecurity/lib/dist/')) { + return null + } + const socketLibPath = findSocketLibPath(args.importer) + if (!socketLibPath) { + return null + } + const externalPath = args.path + .replace(socketLibExternalPathRegExp, '') + .replace(/\.js$/, '') + const p = resolveSocketLibExternal(socketLibPath, externalPath) + return p ? { path: p } : null + }) + + build.onResolve({ filter: /^(@[^/]+\/[^/]+|[^./][^/]*)/ }, args => { + if (!args.importer.includes('/socket-lib/dist/')) { + return null + } + const socketLibPath = findSocketLibPath(args.importer) + if (!socketLibPath) { + return null + } + const packageName = args.path.startsWith('@') + ? args.path.split('/').slice(0, 2).join('/') + : args.path.split('/')[0] + const p = resolveSocketLibExternal(socketLibPath, packageName) + return p ? { path: p } : null + }) + }, + }, + + { + name: 'yoga-wasm-alias', + setup(build) { + // Redirect yoga-layout to our custom synchronous implementation. + build.onResolve({ filter: /^yoga-layout$/ }, () => { + return { + path: path.join(rootPath, 'build/yoga-sync.mjs'), + } + }) + }, + }, + + { + name: 'stub-problematic-packages', + setup(build) { + // Stub iconv-lite and encoding to avoid bundling issues. + build.onResolve({ filter: /^(iconv-lite|encoding)(\/|$)/ }, args => { + return { + path: args.path, + namespace: 'stub', + } + }) + + build.onLoad({ filter: /.*/, namespace: 'stub' }, () => { + return { + contents: 'module.exports = {}', + loader: 'js', + } + }) + }, + }, + + { + name: 'ignore-unsupported-files', + setup(build) { + // Prevent bundling @npmcli/arborist from workspace node_modules. + // This includes the main package and all subpaths like /lib/edge.js. + build.onResolve({ filter: /@npmcli\/arborist/ }, args => { + // Only redirect if it's not already coming from socket-lib's external bundle. + if (args.importer.includes('/socket-lib/dist/')) { + return null + } + return { path: args.path, external: true } + }) + + // Mark node-gyp as external (used by arborist but optionally resolved). + build.onResolve({ filter: /node-gyp/ }, args => { + return { path: args.path, external: true } + }) + }, + }, + ], +} + +if (fileURLToPath(import.meta.url) === process.argv[1]) { + runBuild(config, 'CLI bundle') +} + +export default config diff --git a/packages/cli/.config/esbuild.config.mjs b/packages/cli/.config/esbuild.config.mjs deleted file mode 100644 index 84dceefa5..000000000 --- a/packages/cli/.config/esbuild.config.mjs +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Unified esbuild configuration orchestrator for Socket CLI. - * Supports building all variants by delegating to individual config files. - * - * Usage: - * node .config/esbuild.config.mjs [variant] - * node .config/esbuild.config.mjs cli # Build CLI bundle - * node .config/esbuild.config.mjs index # Build entry point - * node .config/esbuild.config.mjs all # Build all variants - */ - -import { spawn } from 'node:child_process' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import cliConfig from './esbuild.cli.build.mjs' -import indexConfig from './esbuild.index.config.mjs' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -/** - * Config mapping for each build variant (exports for programmatic use). - */ -export const CONFIGS = { - __proto__: null, - cli: cliConfig, - index: indexConfig, -} - -/** - * Config file paths for each build variant. - */ -const VARIANT_FILES = { - __proto__: null, - all: null, // Special variant to build all. - cli: path.join(__dirname, 'esbuild.cli.build.mjs'), - index: path.join(__dirname, 'esbuild.index.config.mjs'), -} - -/** - * Build a single variant by executing its config file. - */ -async function buildVariant(name, configPath) { - return new Promise(resolve => { - const child = spawn('node', [configPath], { stdio: 'inherit' }) - - child.on('close', code => { - if (code === 0) { - resolve({ name, ok: true }) - } else { - resolve({ name, ok: false }) - } - }) - }) -} - -/** - * Build all variants in parallel. - */ -async function buildAll() { - const variants = ['cli', 'index'] - const results = await Promise.all( - variants.map(name => buildVariant(name, VARIANT_FILES[name])), - ) - - const failed = results.filter(r => !r.ok) - if (failed.length > 0) { - console.error(`\n${failed.length} build(s) failed:`) - for (const { name } of failed) { - console.error(` - ${name}`) - } - process.exitCode = 1 - } else { - console.log(`\n✔ All ${results.length} builds succeeded`) - } -} - -/** - * Main entry point. - */ -async function main() { - const variant = process.argv[2] || 'all' - - if (!(variant in VARIANT_FILES)) { - console.error(`Unknown variant: ${variant}`) - console.error( - `Available variants: ${Object.keys(VARIANT_FILES).join(', ')}`, - ) - process.exitCode = 1 - return - } - - if (variant === 'all') { - await buildAll() - } else { - const result = await buildVariant(variant, VARIANT_FILES[variant]) - if (!result.ok) { - process.exitCode = 1 - } - } -} - -// Run if invoked directly. -if (fileURLToPath(import.meta.url) === process.argv[1]) { - main().catch(error => { - console.error('Build failed:', error) - process.exitCode = 1 - }) -} - -export default CONFIGS diff --git a/packages/cli/.config/esbuild.index.config.mjs b/packages/cli/.config/esbuild.index.mjs similarity index 73% rename from packages/cli/.config/esbuild.index.config.mjs rename to packages/cli/.config/esbuild.index.mjs index 9eac12a2e..87f5bec84 100644 --- a/packages/cli/.config/esbuild.index.config.mjs +++ b/packages/cli/.config/esbuild.index.mjs @@ -7,9 +7,9 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import { - createBuildRunner, createIndexConfig, -} from '../scripts/esbuild-shared.mjs' + runBuild, +} from '../scripts/esbuild-utils.mjs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.resolve(__dirname, '..') @@ -19,4 +19,8 @@ const config = createIndexConfig({ outfile: path.join(rootPath, 'dist', 'index.js'), }) -export default createBuildRunner(config, 'Entry point', import.meta) +if (fileURLToPath(import.meta.url) === process.argv[1]) { + runBuild(config, 'Entry point') +} + +export default config diff --git a/packages/cli/package.json b/packages/cli/package.json index 44158ce9c..4c1adbcd3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -23,10 +23,8 @@ "build:watch": "node --max-old-space-size=8192 --import=./scripts/load.mjs scripts/build.mjs --watch", "restore-cache": "node --import=./scripts/load.mjs scripts/restore-cache.mjs", "build:sea": "node --max-old-space-size=8192 --import=./scripts/load.mjs scripts/build-sea.mjs", - "build:sea:internal:bootstrap": "node --max-old-space-size=8192 .config/esbuild.sea-bootstrap.build.mjs", "build:js": "node scripts/build-js.mjs", "dev:watch": "pnpm run build:watch", - "publish:sea": "node --import=./scripts/load.mjs scripts/publish-sea.mjs", "check": "node ../../scripts/check.mjs", "check-ci": "pnpm run check", "lint": "oxlint -c ../../.oxlintrc.json", @@ -52,8 +50,7 @@ "dev:npx": "cross-env SOCKET_CLI_MODE=npx node --experimental-strip-types src/cli-dispatch.mts", "e2e-tests": "dotenvx -q run -f .env.test -- vitest run --config vitest.e2e.config.mts", "e2e:js": "node scripts/e2e.mjs --js", - "e2e:smol": "node scripts/e2e.mjs --smol", - "e2e:sea": "node scripts/e2e.mjs --sea", +"e2e:sea": "node scripts/e2e.mjs --sea", "e2e:all": "node scripts/e2e.mjs --all", "test": "run-s check test:*", "test:prepare": "dotenvx -q run -f .env.test -- pnpm build && del-cli 'test/**/node_modules'", diff --git a/packages/cli/scripts/build-js.mjs b/packages/cli/scripts/build-js.mjs index 4a9368ae4..ef7899381 100644 --- a/packages/cli/scripts/build-js.mjs +++ b/packages/cli/scripts/build-js.mjs @@ -35,9 +35,14 @@ async function main() { logger.step('Building CLI bundle') const buildResult = await spawn( 'node', - ['--max-old-space-size=8192', '.config/esbuild.config.mjs', 'cli'], + ['--max-old-space-size=8192', '.config/esbuild.build.mjs', 'cli'], { stdio: 'inherit' }, ) + if (!buildResult) { + logger.error('Failed to start CLI build') + process.exitCode = 1 + return + } if (buildResult.code !== 0) { process.exitCode = buildResult.code return diff --git a/packages/cli/scripts/build.mjs b/packages/cli/scripts/build.mjs index ce6c0f9b5..696ba081f 100644 --- a/packages/cli/scripts/build.mjs +++ b/packages/cli/scripts/build.mjs @@ -65,7 +65,7 @@ async function fixNodeGypStrings(dir, options = {}) { await fixNodeGypStrings(filePath, options) } else if (file.name.endsWith('.js')) { // Read file contents. - const contents = await fs.readFile(filePath, 'utf8') + const contents = await fs.readFile(filePath, 'utf-8') // Check if file contains the problematic pattern. if (contents.includes('node-gyp/bin/node-gyp.js')) { @@ -75,7 +75,7 @@ async function fixNodeGypStrings(dir, options = {}) { '"node-" + "gyp/bin/node-gyp.js"', ) - await fs.writeFile(filePath, fixed, 'utf8') + await fs.writeFile(filePath, fixed, 'utf-8') if (!quiet && verbose) { logger.info( @@ -134,16 +134,16 @@ async function main() { // Then start esbuild in watch mode. const watchResult = await spawn( 'node', - [...NODE_MEMORY_FLAGS, '.config/esbuild.cli.build.mjs', '--watch'], + [...NODE_MEMORY_FLAGS, '.config/esbuild.cli.mjs', '--watch'], { shell: WIN32, stdio: 'inherit', }, ) - if (watchResult.code !== 0) { - process.exitCode = watchResult.code - throw new Error(`Watch mode failed with exit code ${watchResult.code}`) + if (!watchResult || watchResult.code !== 0) { + process.exitCode = watchResult?.code ?? 1 + throw new Error(`Watch mode failed with exit code ${watchResult?.code}`) } return } @@ -240,7 +240,7 @@ async function main() { const buildResult = await spawn( 'node', - [...NODE_MEMORY_FLAGS, '.config/esbuild.config.mjs', 'all'], + [...NODE_MEMORY_FLAGS, '.config/esbuild.build.mjs', 'all'], { shell: WIN32, stdio: 'inherit', @@ -265,7 +265,7 @@ async function main() { logger.step('Phase 4: Post-processing (parallel)...') } - await Promise.all([ + const postResults = await Promise.allSettled([ // Copy CLI bundle to dist (required for dist/index.js to work). (async () => { copyFileSync('build/cli.js', 'dist/cli.js') @@ -297,6 +297,14 @@ async function main() { })(), ]) + const postFailed = postResults.filter(r => r.status === 'rejected') + if (postFailed.length > 0) { + for (const r of postFailed) { + logger.error(`Post-processing failed: ${r.reason?.message ?? r.reason}`) + } + throw new Error('Post-processing step(s) failed') + } + if (!quiet) { printSuccess('Build completed') printFooter() diff --git a/packages/cli/scripts/constants/env.mjs b/packages/cli/scripts/constants/env.mjs index e1a20e1ab..59d47e8c8 100644 --- a/packages/cli/scripts/constants/env.mjs +++ b/packages/cli/scripts/constants/env.mjs @@ -6,7 +6,6 @@ export const INLINED_COANA_VERSION = export const INLINED_CYCLONEDX_CDXGEN_VERSION = 'INLINED_CYCLONEDX_CDXGEN_VERSION' export const INLINED_HOMEPAGE = 'INLINED_HOMEPAGE' -export const INLINED_LEGACY_BUILD = 'INLINED_LEGACY_BUILD' export const INLINED_NAME = 'INLINED_NAME' export const INLINED_PUBLISHED_BUILD = 'INLINED_PUBLISHED_BUILD' diff --git a/packages/cli/scripts/cover.mjs b/packages/cli/scripts/cover.mjs index 532fc2c07..3e5630da9 100644 --- a/packages/cli/scripts/cover.mjs +++ b/packages/cli/scripts/cover.mjs @@ -22,25 +22,16 @@ import { spawn } from '@socketsecurity/lib/spawn' const logger = getDefaultLogger() -/** - * Print a header message. - */ function printHeader(message) { logger.error('\n═══════════════════════════════════════════════════════') logger.error(` ${message}`) logger.error('═══════════════════════════════════════════════════════\n') } -/** - * Print a success message. - */ function printSuccess(message) { logger.log(`✔ ${message}`) } -/** - * Print an error message. - */ function printError(message) { logger.error(`✖ ${message}`) } diff --git a/packages/cli/scripts/download-assets.mjs b/packages/cli/scripts/download-assets.mjs index d578f51a1..ce249b6c6 100644 --- a/packages/cli/scripts/download-assets.mjs +++ b/packages/cli/scripts/download-assets.mjs @@ -331,15 +331,17 @@ ${content} */ async function downloadAssets(assetNames, parallel = true) { if (parallel) { - const results = await Promise.all( + const settled = await Promise.allSettled( assetNames.map(name => downloadAsset(ASSETS[name])), ) - const failed = results.filter(r => !r.ok) + const failed = settled.filter( + r => r.status === 'fulfilled' && !r.value.ok, + ) if (failed.length > 0) { logger.error(`\n${failed.length} asset(s) failed:`) - for (const { name } of failed) { - logger.error(` - ${name}`) + for (const r of failed) { + logger.error(` - ${r.value.name}`) } process.exitCode = 1 } diff --git a/packages/cli/scripts/environment-variables.mjs b/packages/cli/scripts/environment-variables.mjs index 4b03cba04..6e7a859dc 100644 --- a/packages/cli/scripts/environment-variables.mjs +++ b/packages/cli/scripts/environment-variables.mjs @@ -3,7 +3,7 @@ * Single source of truth for all inlined environment variables. * * This module consolidates environment variable loading that was previously duplicated between: - * - esbuild-shared.mjs (full build-time inlining with 18 variables) + * - esbuild-utils.mjs (full build-time inlining with 18 variables) * - test-wrapper.mjs (partial test environment with 4 variables) * * Usage: @@ -158,7 +158,7 @@ export class EnvironmentVariables { static loadSafe() { try { const externalTools = JSON.parse( - readFileSync(path.join(rootPath, 'external-tools.json'), 'utf8'), + readFileSync(path.join(rootPath, 'external-tools.json'), 'utf-8'), ) return { INLINED_COANA_VERSION: diff --git a/packages/cli/scripts/esbuild-shared.mjs b/packages/cli/scripts/esbuild-utils.mjs similarity index 63% rename from packages/cli/scripts/esbuild-shared.mjs rename to packages/cli/scripts/esbuild-utils.mjs index 751a0ab99..99c70ff8e 100644 --- a/packages/cli/scripts/esbuild-shared.mjs +++ b/packages/cli/scripts/esbuild-utils.mjs @@ -3,14 +3,18 @@ * Contains helpers for environment variable inlining and build metadata. */ -import { execSync } from 'node:child_process' -import { randomUUID } from 'node:crypto' -import { readFileSync } from 'node:fs' +import { mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { build } from 'esbuild' + +import { getDefaultLogger } from '@socketsecurity/lib/logger' + import { EnvironmentVariables } from './environment-variables.mjs' +const logger = getDefaultLogger() + const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.join(__dirname, '..') @@ -32,12 +36,12 @@ export function createIndexConfig({ entryPoint, minify = false, outfile }) { }, bundle: true, entryPoints: [entryPoint], - external: [], format: 'cjs', outfile, platform: 'node', + // Source maps off for entry point production build. + sourcemap: false, target: 'node18', - treeShaking: true, // Define environment variables for inlining. define: { 'process.env.NODE_ENV': '"production"', @@ -54,7 +58,6 @@ export function createIndexConfig({ entryPoint, minify = false, outfile }) { } else { config.minifyWhitespace = true config.minifyIdentifiers = true - config.minifySyntax = false } return config @@ -127,65 +130,33 @@ export function getInlinedEnvVars() { } /** - * Create a build runner function that executes esbuild config when run as main module. - * This eliminates boilerplate code repeated across all esbuild config files. + * Run an esbuild config, writing output files if write: false. * * @param {Object} config - esbuild configuration object - * @param {string} [description] - Optional description of what this build does - * @param {ImportMeta} importMeta - The import.meta from the calling config file - * @returns {Object} The same config object (for chaining) - * - * @example - * ```javascript - * import { build } from 'esbuild' - * import { createBuildRunner } from './esbuild-shared.mjs' - * - * const config = { ... } - * export default createBuildRunner(config, 'CLI bundle', import.meta) - * ``` + * @param {string} [description] - Description logged before/after build */ -export function createBuildRunner(config, description = 'Build', importMeta) { - // Only run if the caller's file is the main module (executed directly). - // This allows configs to be imported without side effects. - if ( - importMeta && - fileURLToPath(importMeta.url) === process.argv[1]?.replace(/\\/g, '/') - ) { - ;(async () => { - try { - // Import esbuild dynamically to avoid loading it during imports. - const { build } = await import('esbuild') - - if (description) { - console.log(`Building: ${description}`) - } - - const result = await build(config) - - // If write: false, manually write outputFiles. - if (result.outputFiles && result.outputFiles.length > 0) { - const { writeFileSync } = await import('node:fs') - const { dirname } = await import('node:path') - const { mkdirSync } = await import('node:fs') - - for (const output of result.outputFiles) { - // Ensure directory exists. - mkdirSync(dirname(output.path), { recursive: true }) - // Write output file. - writeFileSync(output.path, output.contents) - } +export async function runBuild(config, description = 'Build') { + try { + if (description) { + logger.info(`Building: ${description}`) + } + + const result = await build(config) + + // If write: false, manually write outputFiles. + if (result.outputFiles && result.outputFiles.length > 0) { + for (const output of result.outputFiles) { + mkdirSync(path.dirname(output.path), { recursive: true }) + writeFileSync(output.path, output.contents) + } - if (description) { - console.log(`✓ ${description} complete`) - } - } - } catch (error) { - console.error(`Build failed: ${description || 'Unknown'}`) - console.error(error) - process.exitCode = 1 + if (description) { + logger.success(`${description} complete`) } - })() + } + } catch (e) { + logger.error(`Build failed: ${description || 'Unknown'}`) + logger.error(e) + process.exitCode = 1 } - - return config } diff --git a/packages/cli/scripts/esbuild.config.mjs b/packages/cli/scripts/esbuild.config.mjs deleted file mode 100644 index ef6277dc9..000000000 --- a/packages/cli/scripts/esbuild.config.mjs +++ /dev/null @@ -1,57 +0,0 @@ -/** - * esbuild build script for Socket CLI. - */ - -import { readFileSync, writeFileSync } from 'node:fs' -import { brotliCompressSync } from 'node:zlib' - -import { build } from 'esbuild' - -import { getDefaultLogger } from '@socketsecurity/lib/logger' - -import config from './esbuild.cli.config.mjs' - -const logger = getDefaultLogger() -logger.log('Building Socket CLI with esbuild...\n') - -try { - const result = await build(config) - - logger.log('✓ Build completed successfully') - logger.log(`✓ Output: ${config.outfile}`) - - if (result.metafile) { - const outputSize = Object.values(result.metafile.outputs)[0]?.bytes - if (outputSize) { - logger.log(`✓ Bundle size: ${(outputSize / 1024 / 1024).toFixed(2)} MB`) - } - } - - // Compress with brotli. - logger.log('\n🗜️ Compressing with brotli...') - const jsCode = readFileSync(config.outfile) - const compressed = brotliCompressSync(jsCode, { - params: { - [require('node:zlib').constants.BROTLI_PARAM_QUALITY]: 11, - - [require('node:zlib').constants.BROTLI_PARAM_SIZE_HINT]: jsCode.length, - }, - }) - - const bzPath = `${config.outfile}.bz` - writeFileSync(bzPath, compressed) - - const originalSize = jsCode.length / 1024 / 1024 - const compressedSize = compressed.length / 1024 / 1024 - const compressionRatio = ((compressed.length / jsCode.length) * 100).toFixed( - 1, - ) - - logger.log(`✓ Compressed: ${bzPath}`) - logger.log(`✓ Original size: ${originalSize.toFixed(2)} MB`) - logger.log(`✓ Compressed size: ${compressedSize.toFixed(2)} MB`) - logger.log(`✓ Compression ratio: ${compressionRatio}%`) -} catch (error) { - logger.error('Build failed:', error) - process.exitCode = 1 -} diff --git a/packages/cli/scripts/test-wrapper.mjs b/packages/cli/scripts/test-wrapper.mjs index 75bdcb249..0db2d5a39 100644 --- a/packages/cli/scripts/test-wrapper.mjs +++ b/packages/cli/scripts/test-wrapper.mjs @@ -154,17 +154,12 @@ async function main() { ...(WIN32 ? { shell: true } : {}), } - const child = spawn(dotenvxPath, dotenvxArgs, spawnOptions) - - child.on('exit', code => { - process.exitCode = code || 0 - }) - - child.on('error', e => { - logger.error('Failed to spawn test process:', e) - process.exitCode = 1 - }) - } catch {} + const result = await spawn(dotenvxPath, dotenvxArgs, spawnOptions) + process.exitCode = result?.code || 0 + } catch (e) { + logger.error('Failed to spawn test process:', e) + process.exitCode = 1 + } } main().catch(e => { diff --git a/packages/cli/scripts/utils/patches.mjs b/packages/cli/scripts/utils/patches.mjs index 1da7fa936..c73a3bc73 100644 --- a/packages/cli/scripts/utils/patches.mjs +++ b/packages/cli/scripts/utils/patches.mjs @@ -13,6 +13,8 @@ import { WIN32 } from '@socketsecurity/lib/constants/platform' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' +const logger = getDefaultLogger() + /** * Parse JavaScript/TypeScript code into a Babel AST. * @@ -90,7 +92,6 @@ async function promptYesNo(question, defaultAnswer = false) { * @returns {Promise} Path to temporary patch directory. */ export async function startPatch(packageSpec) { - const logger = getDefaultLogger() logger.log(`Starting patch for ${packageSpec}...`) // First, try to run pnpm patch to see if directory already exists. diff --git a/packages/cli/scripts/validate-bundle.mjs b/packages/cli/scripts/validate-bundle.mjs index 2e3338f15..6f3189143 100644 --- a/packages/cli/scripts/validate-bundle.mjs +++ b/packages/cli/scripts/validate-bundle.mjs @@ -23,10 +23,10 @@ const buildPath = path.join(__dirname, '..', 'build', 'cli.js') function validateBundle() { let content try { - content = readFileSync(buildPath, 'utf8') - } catch (error) { - logger.fail(`Failed to read bundle: ${error.message}`) - return false + content = readFileSync(buildPath, 'utf-8') + } catch (e) { + logger.fail(`Failed to read bundle: ${e.message}`) + return null } const violations = [] @@ -49,6 +49,11 @@ async function main() { try { const violations = validateBundle() + if (!violations) { + process.exitCode = 1 + return + } + if (violations.length === 0) { logger.success('Bundle validation passed') process.exitCode = 0 @@ -82,7 +87,7 @@ async function main() { } } -main().catch(error => { - console.error(`Validation failed: ${error}`) +main().catch(e => { + logger.error(`Validation failed: ${e}`) process.exitCode = 1 }) diff --git a/packages/cli/scripts/verify-package.mjs b/packages/cli/scripts/verify-package.mjs index be0ac7232..a7935bea3 100644 --- a/packages/cli/scripts/verify-package.mjs +++ b/packages/cli/scripts/verify-package.mjs @@ -1,33 +1,17 @@ -import { promises as fs } from 'node:fs' +import { existsSync, promises as fs } from 'node:fs' import path from 'node:path' -import process from 'node:process' import { fileURLToPath } from 'node:url' import colors from 'yoctocolors-cjs' import { getDefaultLogger } from '@socketsecurity/lib/logger' -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) +const __dirname = path.dirname(fileURLToPath(import.meta.url)) const packageRoot = path.resolve(__dirname, '..') -/** - * Check if a file exists and is readable. - */ -async function fileExists(filePath) { - try { - await fs.access(filePath) - return true - } catch { - return false - } -} +const logger = getDefaultLogger() -/** - * Main validation function. - */ async function validate() { - const logger = getDefaultLogger() logger.log('') logger.log('='.repeat(60)) logger.log(`${colors.blue('CLI Package Validation')}`) @@ -39,7 +23,7 @@ async function validate() { // Check package.json exists and has correct files array. logger.info('Checking package.json...') const pkgPath = path.join(packageRoot, 'package.json') - if (!(await fileExists(pkgPath))) { + if (!(existsSync(pkgPath))) { errors.push('package.json does not exist') } else { logger.success('package.json exists') @@ -75,7 +59,7 @@ async function validate() { for (const file of rootFiles) { logger.info(`Checking ${file}...`) const filePath = path.join(packageRoot, file) - if (!(await fileExists(filePath))) { + if (!(existsSync(filePath))) { errors.push(`${file} does not exist`) } else { logger.success(`${file} exists`) @@ -87,7 +71,7 @@ async function validate() { for (const file of distFiles) { logger.info(`Checking dist/${file}...`) const filePath = path.join(packageRoot, 'dist', file) - if (!(await fileExists(filePath))) { + if (!(existsSync(filePath))) { errors.push(`dist/${file} does not exist`) } else { logger.success(`dist/${file} exists`) @@ -97,7 +81,7 @@ async function validate() { // Check data directory exists. logger.info('Checking data directory...') const dataPath = path.join(packageRoot, 'data') - if (!(await fileExists(dataPath))) { + if (!(existsSync(dataPath))) { errors.push('data directory does not exist') } else { logger.success('data directory exists') @@ -110,7 +94,7 @@ async function validate() { for (const file of dataFiles) { logger.info(`Checking data/${file}...`) const filePath = path.join(dataPath, file) - if (!(await fileExists(filePath))) { + if (!(existsSync(filePath))) { errors.push(`data/${file} does not exist`) } else { logger.success(`data/${file} exists`) @@ -128,7 +112,7 @@ async function validate() { if (errors.length > 0) { logger.log(`${colors.red('Errors:')}`) for (const err of errors) { - logger.log(` ${error(err)}`) + logger.log(` ${err}`) } logger.log('') logger.fail('Package validation FAILED') diff --git a/packages/cli/scripts/wasm.mjs b/packages/cli/scripts/wasm.mjs index e5816a0d0..8386278fe 100644 --- a/packages/cli/scripts/wasm.mjs +++ b/packages/cli/scripts/wasm.mjs @@ -24,6 +24,8 @@ import { fileURLToPath } from 'node:url' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' +const logger = getDefaultLogger() + const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.join(__dirname, '..') const externalDir = path.join(rootPath, 'external') @@ -40,7 +42,6 @@ function checkNodeVersion() { const major = Number.parseInt(nodeVersion.split('.')[0], 10) if (major < 18) { - const logger = getDefaultLogger() logger.error(' Node.js version 18 or higher is required') logger.error(`Current version: ${nodeVersion}`) logger.error('Please upgrade: https://nodejs.org/') diff --git a/packages/package-builder/templates/cli-package/.config/esbuild.cli.build.mjs b/packages/package-builder/templates/cli-package/.config/esbuild.cli.build.mjs deleted file mode 100644 index 99382576a..000000000 --- a/packages/package-builder/templates/cli-package/.config/esbuild.cli.build.mjs +++ /dev/null @@ -1,48 +0,0 @@ -/** - * esbuild configuration for building Socket CLI. - * Extends the base CLI build configuration. - */ - -import { mkdirSync, writeFileSync } from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { build } from 'esbuild' - -import { getDefaultLogger } from '@socketsecurity/lib/logger' - -import baseConfig from '../../cli/.config/esbuild.cli.build.mjs' - -const logger = getDefaultLogger() - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const rootPath = path.join(__dirname, '..') - -// Create standard CLI configuration. -const config = { - ...baseConfig, - // Use the standard entry point. - entryPoints: [path.join(rootPath, '..', 'cli', 'src', 'cli-dispatch.mts')], - // Output to build directory. - outfile: path.join(rootPath, 'build/cli.js'), -} - -// Run build if invoked directly. -if (fileURLToPath(import.meta.url) === process.argv[1]) { - build(config) - .then(result => { - // Write the transformed output (build had write: false). - if (result.outputFiles && result.outputFiles.length > 0) { - mkdirSync(path.dirname(config.outfile), { recursive: true }) - for (const output of result.outputFiles) { - writeFileSync(output.path, output.contents) - } - } - }) - .catch(error => { - logger.error('Build failed:', error) - process.exitCode = 1 - }) -} - -export default config diff --git a/packages/package-builder/templates/cli-package/.config/esbuild.cli.mjs b/packages/package-builder/templates/cli-package/.config/esbuild.cli.mjs new file mode 100644 index 000000000..638d25d6b --- /dev/null +++ b/packages/package-builder/templates/cli-package/.config/esbuild.cli.mjs @@ -0,0 +1,26 @@ +/** + * esbuild configuration for building Socket CLI. + * Extends the base CLI build configuration. + */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { runBuild } from '../../cli/scripts/esbuild-utils.mjs' + +import baseConfig from '../../cli/.config/esbuild.cli.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') + +const config = { + ...baseConfig, + entryPoints: [path.join(rootPath, '..', 'cli', 'src', 'cli-dispatch.mts')], + outfile: path.join(rootPath, 'build/cli.js'), +} + +if (fileURLToPath(import.meta.url) === process.argv[1]) { + runBuild(config, 'CLI bundle') +} + +export default config diff --git a/packages/package-builder/templates/cli-package/.config/esbuild.config.mjs b/packages/package-builder/templates/cli-package/.config/esbuild.config.mjs deleted file mode 100644 index 5e69e8e7a..000000000 --- a/packages/package-builder/templates/cli-package/.config/esbuild.config.mjs +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @fileoverview esbuild configuration for Socket CLI with Sentry telemetry. - * Builds a Sentry-enabled version of the CLI with error reporting. - */ - -import { mkdirSync, writeFileSync } from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { build } from 'esbuild' - -import { getDefaultLogger } from '@socketsecurity/lib/logger' - -// Import base esbuild config from main CLI. -import baseConfig from '../../cli/.config/esbuild.cli.build.mjs' - -const logger = getDefaultLogger() - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const rootPath = path.join(__dirname, '..') -const cliPath = path.join(__dirname, '..', '..', 'cli') - -// Override entry point to use CLI dispatch with Sentry telemetry. -const config = { - ...baseConfig, - entryPoints: [path.join(cliPath, 'src/cli-dispatch-with-sentry.mts')], - outfile: path.join(rootPath, 'build/cli.js'), - - // Override define to enable Sentry build. - define: { - ...baseConfig.define, - 'process.env.INLINED_SENTRY_BUILD': JSON.stringify('1'), - }, - - // Make @sentry/node external (not bundled). - external: [...(baseConfig.external || []), '@sentry/node'], -} - -// Run build if invoked directly. -if (import.meta.url === `file://${process.argv[1]}`) { - build(config) - .then(result => { - // Write the transformed output (build had write: false). - if (result.outputFiles && result.outputFiles.length > 0) { - mkdirSync(path.dirname(config.outfile), { recursive: true }) - for (const output of result.outputFiles) { - writeFileSync(output.path, output.contents) - } - } - }) - .catch(error => { - logger.error('Build failed:', error) - process.exitCode = 1 - }) -} - -export default config diff --git a/packages/package-builder/templates/cli-package/.config/esbuild.index.config.mjs b/packages/package-builder/templates/cli-package/.config/esbuild.index.mjs similarity index 56% rename from packages/package-builder/templates/cli-package/.config/esbuild.index.config.mjs rename to packages/package-builder/templates/cli-package/.config/esbuild.index.mjs index 759dd4692..1c3973c24 100644 --- a/packages/package-builder/templates/cli-package/.config/esbuild.index.config.mjs +++ b/packages/package-builder/templates/cli-package/.config/esbuild.index.mjs @@ -1,18 +1,15 @@ /** - * esbuild configuration for Socket CLI with Sentry index loader. + * esbuild configuration for Socket CLI index loader. * Builds the index loader that executes the CLI. */ import path from 'node:path' import { fileURLToPath } from 'node:url' -import { build } from 'esbuild' - -import { getDefaultLogger } from '@socketsecurity/lib/logger' - -import { createIndexConfig } from '../../cli/scripts/esbuild-shared.mjs' - -const logger = getDefaultLogger() +import { + createIndexConfig, + runBuild, +} from '../../cli/scripts/esbuild-utils.mjs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.resolve(__dirname, '..') @@ -24,12 +21,8 @@ const config = createIndexConfig({ minify: true, }) -// Run build if invoked directly. if (fileURLToPath(import.meta.url) === process.argv[1]) { - build(config).catch(error => { - logger.error('Index loader build failed:', error) - process.exitCode = 1 - }) + runBuild(config, 'Entry point') } export default config diff --git a/packages/package-builder/templates/cli-package/scripts/build.mjs b/packages/package-builder/templates/cli-package/scripts/build.mjs index aa02c1e1a..b8934ed59 100644 --- a/packages/package-builder/templates/cli-package/scripts/build.mjs +++ b/packages/package-builder/templates/cli-package/scripts/build.mjs @@ -24,7 +24,7 @@ async function main() { // Build CLI bundle. logger.info('Building CLI bundle...') - let result = await spawn('node', ['.config/esbuild.cli.build.mjs'], { + let result = await spawn('node', ['.config/esbuild.cli.mjs'], { shell: WIN32, stdio: 'inherit', cwd: rootPath, @@ -41,7 +41,7 @@ async function main() { // Build index loader. logger.info('Building index loader...') - result = await spawn('node', ['.config/esbuild.index.config.mjs'], { + result = await spawn('node', ['.config/esbuild.index.mjs'], { shell: WIN32, stdio: 'inherit', cwd: rootPath, diff --git a/packages/package-builder/templates/cli-package/test/package.test.mjs b/packages/package-builder/templates/cli-package/test/package.test.mjs index 2115466be..9b4f50523 100644 --- a/packages/package-builder/templates/cli-package/test/package.test.mjs +++ b/packages/package-builder/templates/cli-package/test/package.test.mjs @@ -125,7 +125,7 @@ describe('@socketsecurity/cli-with-sentry package', () => { const content = await fs.readFile(esbuildPath, 'utf-8') expect(content).toContain( - "import baseConfig from '../../cli/.config/esbuild.cli.build.mjs'", + "import baseConfig from '../../cli/.config/esbuild.cli.mjs'", ) }) diff --git a/packages/package-builder/templates/cli-sentry-package/.config/esbuild.cli-sentry.build.mjs b/packages/package-builder/templates/cli-sentry-package/.config/esbuild.cli-sentry.build.mjs index 3cf673699..d1eca59d6 100644 --- a/packages/package-builder/templates/cli-sentry-package/.config/esbuild.cli-sentry.build.mjs +++ b/packages/package-builder/templates/cli-sentry-package/.config/esbuild.cli-sentry.build.mjs @@ -3,50 +3,31 @@ * Extends the base CLI build configuration with Sentry-specific settings. */ -import { mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { build } from 'esbuild' +import { runBuild } from '../../cli/scripts/esbuild-utils.mjs' -import baseConfig from '../../cli/.config/esbuild.cli.build.mjs' +import baseConfig from '../../cli/.config/esbuild.cli.mjs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.join(__dirname, '..') +const cliPath = path.join(__dirname, '..', '..', 'cli') -// Create Sentry-enabled configuration. const config = { ...baseConfig, - // Use the Sentry-enabled entry point. - entryPoints: [ - path.join(rootPath, '..', 'cli', 'src', 'cli-dispatch-with-sentry.mts'), - ], - // Output to build directory. + entryPoints: [path.join(cliPath, 'src/cli-dispatch-with-sentry.mts')], outfile: path.join(rootPath, 'build/cli.js'), - // Override define to enable Sentry build flag. define: { ...baseConfig.define, 'process.env.INLINED_SENTRY_BUILD': JSON.stringify('1'), 'process.env["INLINED_SENTRY_BUILD"]': JSON.stringify('1'), }, + external: [...(baseConfig.external || []), '@sentry/node'], } -// Run build if invoked directly. if (fileURLToPath(import.meta.url) === process.argv[1]) { - build(config) - .then(result => { - // Write the transformed output (build had write: false). - if (result.outputFiles && result.outputFiles.length > 0) { - mkdirSync(path.dirname(config.outfile), { recursive: true }) - for (const output of result.outputFiles) { - writeFileSync(output.path, output.contents) - } - } - }) - .catch(error => { - logger.error('Build failed:', error) - process.exitCode = 1 - }) + runBuild(config, 'CLI bundle (Sentry)') } export default config diff --git a/packages/package-builder/templates/cli-sentry-package/.config/esbuild.config.mjs b/packages/package-builder/templates/cli-sentry-package/.config/esbuild.config.mjs deleted file mode 100644 index 5e69e8e7a..000000000 --- a/packages/package-builder/templates/cli-sentry-package/.config/esbuild.config.mjs +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @fileoverview esbuild configuration for Socket CLI with Sentry telemetry. - * Builds a Sentry-enabled version of the CLI with error reporting. - */ - -import { mkdirSync, writeFileSync } from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { build } from 'esbuild' - -import { getDefaultLogger } from '@socketsecurity/lib/logger' - -// Import base esbuild config from main CLI. -import baseConfig from '../../cli/.config/esbuild.cli.build.mjs' - -const logger = getDefaultLogger() - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const rootPath = path.join(__dirname, '..') -const cliPath = path.join(__dirname, '..', '..', 'cli') - -// Override entry point to use CLI dispatch with Sentry telemetry. -const config = { - ...baseConfig, - entryPoints: [path.join(cliPath, 'src/cli-dispatch-with-sentry.mts')], - outfile: path.join(rootPath, 'build/cli.js'), - - // Override define to enable Sentry build. - define: { - ...baseConfig.define, - 'process.env.INLINED_SENTRY_BUILD': JSON.stringify('1'), - }, - - // Make @sentry/node external (not bundled). - external: [...(baseConfig.external || []), '@sentry/node'], -} - -// Run build if invoked directly. -if (import.meta.url === `file://${process.argv[1]}`) { - build(config) - .then(result => { - // Write the transformed output (build had write: false). - if (result.outputFiles && result.outputFiles.length > 0) { - mkdirSync(path.dirname(config.outfile), { recursive: true }) - for (const output of result.outputFiles) { - writeFileSync(output.path, output.contents) - } - } - }) - .catch(error => { - logger.error('Build failed:', error) - process.exitCode = 1 - }) -} - -export default config diff --git a/packages/package-builder/templates/cli-sentry-package/.config/esbuild.index.config.mjs b/packages/package-builder/templates/cli-sentry-package/.config/esbuild.index.mjs similarity index 70% rename from packages/package-builder/templates/cli-sentry-package/.config/esbuild.index.config.mjs rename to packages/package-builder/templates/cli-sentry-package/.config/esbuild.index.mjs index 6cf1525ef..4be36622e 100644 --- a/packages/package-builder/templates/cli-sentry-package/.config/esbuild.index.config.mjs +++ b/packages/package-builder/templates/cli-sentry-package/.config/esbuild.index.mjs @@ -6,9 +6,10 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' -import { build } from 'esbuild' - -import { createIndexConfig } from '../../cli/scripts/esbuild-shared.mjs' +import { + createIndexConfig, + runBuild, +} from '../../cli/scripts/esbuild-utils.mjs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.resolve(__dirname, '..') @@ -20,12 +21,8 @@ const config = createIndexConfig({ minify: true, }) -// Run build if invoked directly. if (fileURLToPath(import.meta.url) === process.argv[1]) { - build(config).catch(error => { - logger.error('Index loader build failed:', error) - process.exitCode = 1 - }) + runBuild(config, 'Entry point') } export default config diff --git a/packages/package-builder/templates/cli-sentry-package/scripts/build.mjs b/packages/package-builder/templates/cli-sentry-package/scripts/build.mjs index 4bd47ba82..53ede83a6 100644 --- a/packages/package-builder/templates/cli-sentry-package/scripts/build.mjs +++ b/packages/package-builder/templates/cli-sentry-package/scripts/build.mjs @@ -29,6 +29,9 @@ async function main() { stdio: 'inherit', cwd: rootPath, }) + if (!result) { + throw new Error('Failed to start CLI bundle build') + } if (result.code !== 0) { throw new Error(`CLI bundle build failed with exit code ${result.code}`) } @@ -36,11 +39,14 @@ async function main() { // Build index loader. logger.info('Building index loader...') - result = await spawn('node', ['.config/esbuild.index.config.mjs'], { + result = await spawn('node', ['.config/esbuild.index.mjs'], { shell: WIN32, stdio: 'inherit', cwd: rootPath, }) + if (!result) { + throw new Error('Failed to start index loader build') + } if (result.code !== 0) { throw new Error(`Index loader build failed with exit code ${result.code}`) } diff --git a/packages/package-builder/templates/cli-sentry-package/test/package.test.mjs b/packages/package-builder/templates/cli-sentry-package/test/package.test.mjs index b24020278..e23dd6dc0 100644 --- a/packages/package-builder/templates/cli-sentry-package/test/package.test.mjs +++ b/packages/package-builder/templates/cli-sentry-package/test/package.test.mjs @@ -125,7 +125,7 @@ describe('@socketsecurity/cli-with-sentry package', () => { const content = await fs.readFile(esbuildPath, 'utf-8') expect(content).toContain( - "import baseConfig from '../../cli/.config/esbuild.cli.build.mjs'", + "import baseConfig from '../../cli/.config/esbuild.cli.mjs'", ) }) diff --git a/scripts/download-iocraft-binaries.mjs b/scripts/download-iocraft-binaries.mjs index 0136d2ca1..9c69df338 100755 --- a/scripts/download-iocraft-binaries.mjs +++ b/scripts/download-iocraft-binaries.mjs @@ -154,15 +154,17 @@ async function downloadBinaries(platformFilter = null) { `Downloading iocraft binaries for ${configs.length} platform(s)...`, ) - const results = await Promise.all( + const settled = await Promise.allSettled( configs.map(config => downloadIocraftBinary(config)), ) - const failed = results.filter(r => !r.ok) + const failed = settled.filter( + r => r.status === 'fulfilled' && !r.value.ok, + ) if (failed.length > 0) { logger.error(`\n${failed.length} platform(s) failed:`) - for (const { target } of failed) { - logger.error(` - ${target}`) + for (const r of failed) { + logger.error(` - ${r.value.target}`) } return false }