diff --git a/.config/babel.config.js b/.config/babel.config.js index ee78b19c5..e443f512d 100644 --- a/.config/babel.config.js +++ b/.config/babel.config.js @@ -7,7 +7,7 @@ const scriptsPath = path.join(rootPath, 'scripts') const babelPluginsPath = path.join(scriptsPath, 'babel') module.exports = { - presets: ['@babel/preset-typescript'], + presets: ['@babel/preset-react', '@babel/preset-typescript'], plugins: [ '@babel/plugin-proposal-export-default-from', '@babel/plugin-transform-export-namespace-from', @@ -21,7 +21,12 @@ module.exports = { version: '^7.27.1', }, ], - path.join(babelPluginsPath, 'transform-set-proto-plugin.js'), - path.join(babelPluginsPath, 'transform-url-parse-plugin.js'), + // Run strict-mode transformations first to fix loose-mode code + path.join(babelPluginsPath, 'babel-plugin-strict-mode.mjs'), + path.join(babelPluginsPath, 'babel-plugin-inline-require-calls.js'), + path.join(babelPluginsPath, 'transform-set-proto-plugin.mjs'), + path.join(babelPluginsPath, 'transform-url-parse-plugin.mjs'), + // Run ICU removal last to transform Intl/locale APIs + path.join(babelPluginsPath, 'babel-plugin-remove-icu.mjs'), ], } diff --git a/biome.json b/.config/biome.json similarity index 83% rename from biome.json rename to .config/biome.json index 72e2cc852..0ff68b262 100644 --- a/biome.json +++ b/.config/biome.json @@ -1,21 +1,23 @@ { - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "$schema": "../node_modules/@biomejs/biome/configuration_schema.json", "files": { "includes": [ "**", + "!**/.cache", "!**/.DS_Store", "!**/._.DS_Store", "!**/.env", "!**/.git", "!**/.github", "!**/.husky", - "!**/.nvm", - "!**/.rollup.cache", "!**/.type-coverage", "!**/.vscode", "!**/coverage", + "!**/dist", "!**/package.json", - "!**/package-lock.json" + "!**/pnpm-lock.yaml", + "!test/**/fixtures", + "!test/**/packages" ], "maxSize": 8388608 }, @@ -64,8 +66,9 @@ "useSingleVarDeclarator": "error", "noUnusedTemplateLiteral": "error", "useNumberNamespace": "error", - "noInferrableTypes": "error", - "noUselessElse": "error" + "noInferrableTypes": "off", + "noUselessElse": "error", + "useNumericSeparators": "error" } } } diff --git a/.config/build-config.json5 b/.config/build-config.json5 new file mode 100644 index 000000000..eac385404 --- /dev/null +++ b/.config/build-config.json5 @@ -0,0 +1,238 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "version": "1.0.0", + + // Configuration for building custom Node.js binaries + // Size optimizations: ~24.5MB saved from configure flags + "node": { + "defaultVersion": "v22.19.0", + "currentVersion": "v24.9.0", + + "build": { + // Configure flags for custom Node.js build + // These remove unnecessary features to reduce binary size + "configureArgs": [ + "--without-intl", // Remove ICU/Internationalization (saves ~12MB) + "--without-npm", // Remove npm package manager (saves ~2MB) + "--without-corepack", // Remove corepack (yarn/pnpm) (saves ~1MB) + "--without-inspector", // Remove Chrome DevTools debugger (saves ~1MB) + "--without-amaro", // Remove TypeScript support (saves ~0.5MB) + "--without-sqlite", // Remove SQLite database (saves ~1MB) + "--without-node-snapshot", // Disable V8 snapshot (saves ~4MB) + "--without-node-code-cache", // Disable V8 code cache (saves ~3MB) + "--without-node-options", // Remove NODE_OPTIONS support (saves ~0.1MB, security) + // Security patches: + // - disable-eval-flag-v24.patch: ALWAYS removes -e/--eval (no exceptions) + // - restrict-require-flag-v24.patch: Removes -r/--require unless IPC subprocess + // - make-issea-detect-pkg-v24.patch: Makes isSea() return true for pkg binaries + "--disable-single-executable-application", // Disable SEA support (saves ~0.5MB) + "--openssl-no-asm", // Disable OpenSSL assembly (saves ~0.5MB, but slower crypto) + "--enable-lto", // Link Time Optimization (saves ~2-3MB, slower build) + "--v8-lite-mode" // V8 lite mode for embedded systems (saves ~5MB) + + // Size reduction options + // "--without-ssl", // Remove OpenSSL/crypto (saves ~3MB) - WARNING: Breaks HTTPS + // "--without-dtrace", // INVALID in v24 - DTrace support is auto-detected + // "--without-etw", // INVALID in v24 - ETW is Windows-specific, auto-detected + // "--without-perfctr", // INVALID in v24 - Windows performance counters (saves ~0.1MB) + + // Feature flags + // "--shared", // Build shared library instead of executable + // "--fully-static", // INVALID in v24 - Use --enable-static + // "--partly-static", // INVALID in v24 - No longer supported + // "--enable-pgo-generate", // Profile Guided Optimization - generate + // "--enable-pgo-use", // Profile Guided Optimization - use profile + + // V8 options + // "--v8-enable-hugepage", // INVALID in v24 - Use runtime flag instead + // "--without-v8-platform-macos", // INVALID in v24 - Use --without-v8-platform + // "--without-bundled-v8", // Use system V8 instead of bundled + + // OpenSSL options + // "--shared-openssl", // Use system OpenSSL instead of bundled + // "--openssl-fips", // Enable FIPS 140-2 compliance + + // Build options + // "--ninja", // Use Ninja build system instead of Make + // "--debug", // Debug build (much larger) + // "--gdb", // Add GDB debugging support + // "--coverage", // Add code coverage support + // "--asan", // AddressSanitizer (memory error detector) + // "--ubsan", // UndefinedBehaviorSanitizer + + // Target options + // "--dest-cpu=x64", // Target CPU architecture + // "--dest-os=linux", // Target operating system + // "--cross-compiling", // Enable cross-compilation + // "--without-siphash", // Remove SipHash (hash flooding protection) + // "--with-ltcg", // Link-time code generation (Windows) + + // Experimental + // "--experimental-http-parser", // Use experimental HTTP parser + // "--experimental-sea-config" // Single Executable Application config + ] + }, + + // V8 runtime flags - these generate patches for V8 behavior + "v8Flags": [ + // "--harmony-import-assertions", // REMOVED in v24 - replaced by harmony-import-attributes + "--harmony-import-attributes" // Enable import attributes (for @yao-pkg/pkg) - Already default in v24 + + // Memory management + // "--max-old-space-size=4096", // Set max heap memory (MB) + // "--max-semi-space-size=16", // Set semi-space size (MB) + // "--expose-gc", // Expose global.gc() for manual GC + // "--trace-gc", // Trace garbage collection + // "--trace-gc-verbose", // Verbose GC tracing + + // Debugging & tracing + // "--trace-deprecation", // Trace deprecation warnings + // "--throw-deprecation", // Throw on deprecation + // "--pending-deprecation", // Show pending deprecations + // "--trace-warnings", // Trace warning origins + // "--trace-sync-io", // Trace synchronous I/O + // "--abort-on-uncaught-exception", // Abort on uncaught exceptions + + // Module system + // "--preserve-symlinks", // Don't resolve symlinks for modules + // "--preserve-symlinks-main", // Don't resolve symlinks for main module + // "--experimental-modules", // Enable experimental ESM features + // "--experimental-wasm-modules", // Enable WASM modules + // "--experimental-vm-modules", // Enable VM modules support + + // Security & policy + // "--experimental-policy", // Enable policy feature + // "--zero-fill-buffers", // Zero-fill Buffer/Uint8Array allocations + // "--disable-proto=throw", // Disable __proto__ (security) + // "--no-expose-wasm", // Don't expose WASM (security) - Can't use: We need WASM for yoga-layout (base64-encoded WASM module) + + // Performance & profiling + // "--no-force-async-hooks-checks", // Disable async hooks checks (faster) + // "--track-heap-objects", // Track heap object allocations + // "--heap-prof", // Enable heap profiling + // "--prof", // Generate V8 profiler output + + // Reporting + // "--diagnostic-dir=./reports", // Set diagnostics directory + // "--report-uncaught-exception", // Report on uncaught exceptions + // "--report-on-signal", // Generate report on signal (SIGUSR2) + // "--report-on-fatalerror" // Generate report on fatal error + ], + + // Node.js process flags (separate from V8 flags) + "nodeFlags": [ + "--no-deprecation", // Disable deprecation warnings + "--no-warnings" // Disable process warnings + + // Available Node.js flags + // "--enable-source-maps", // Enable source map support + // "--preserve-symlinks", // Preserve symbolic links + // "--preserve-symlinks-main", // Preserve symlinks for main module + // "--inspect", // Enable inspector (requires --without-inspector not set) + // "--inspect-brk", // Enable inspector with break (requires --without-inspector not set) + // "--napi-modules", // REMOVED - N-API is always enabled + // "--trace-event-categories", // Trace event categories + // "--trace-event-file-pattern", // Trace event file pattern + // "--trace-exit", // Trace exit + // "--trace-sigint", // Trace SIGINT + // "--trace-tls", // Trace TLS + // "--tls-min-v1.0", // Allow TLS 1.0 + // "--tls-min-v1.1", // Allow TLS 1.1 + // "--tls-min-v1.2", // Minimum TLS 1.2 + // "--tls-min-v1.3", // Minimum TLS 1.3 + // "--use-openssl-ca", // Use OpenSSL CA store + // "--use-bundled-ca", // Use bundled CA store + // "--enable-fips", // Enable FIPS crypto + // "--force-fips", // Force FIPS crypto + // "--redirect-warnings", // Redirect warnings to file + // "--throw-deprecation", // Throw on deprecation + // "--pending-deprecation", // Show pending deprecations + // "--input-type", // Set input type (commonjs/module) + // "--experimental-loader", // Custom ESM loader hooks + // "--experimental-modules", // REMOVED - ESM is stable + // "--experimental-wasm-modules", // REMOVED - Use --experimental-wasm-modules in V8 flags + // "--experimental-json-modules", // REMOVED - JSON modules are stable + // "--experimental-top-level-await" // REMOVED - Top-level await is stable + ] + }, + + // Node.js SEA (Single Executable Application) configuration + // Used for injecting empty SEA blob into socket-node binary + // Makes isSea() return true for both yao-pkg and native SEA builds + // See: https://nodejs.org/api/single-executable-applications.html + "sea": { + "main": "// Empty SEA main - actual code injected via other mechanism\n" + }, + + // @yao-pkg/pkg configuration for creating standalone executables + // This follows the @yao-pkg/pkg configuration format + // See: https://github.com/yao-pkg/pkg#config + // Size optimizations: bytecode disabled, Brotli compression (up to 60% reduction) + "yao": { + "name": "socket", + "bytecode": false, // Disabled to reduce binary size + "compress": "Brotli", // Use Brotli compression to reduce size of JavaScript bundle (up to 60% reduction) + // Dictionaries map native .node files and dynamic requires that pkg can't detect at build time + // Typically includes: native C/C++ modules (bcrypt, sqlite3, canvas), WASM files, binary assets + // We use empty {} because Socket CLI is pure JS/TS without native dependencies - reduces binary size + "dictionary": {}, + + // Entry points for different CLI commands + "binaries": { + "socket": "dist/cli.js", + "socket-npm": "dist/npm-cli.js", + "socket-npx": "dist/npx-cli.js", + "socket-pnpm": "dist/pnpm-cli.js", + "socket-yarn": "dist/yarn-cli.js" + }, + + // Build targets for all platforms + // Linux targets work for both glibc and musl (Alpine) due to static compilation + // linuxstatic targets create fully static binaries (best for Docker containers) + // Note: linuxstatic cannot load native .node modules + "targets": [ + "node24-macos-arm64", + "node24-macos-x64", + "node24-linux-arm64", + "node24-linux-x64", + "node24-linuxstatic-arm64", // Fully static for Docker/Alpine ARM64 + "node24-linuxstatic-x64", // Fully static for Docker/Alpine x64 + "node24-win-arm64", + "node24-win-x64" + ], + + // Files to include in the packaged binary + "assets": [ + "dist/**/*", + "requirements.json", + "translations.json", + "shadow-bin/**/*" + ] + }, + + // Build-related paths (all relative to repo root) + "paths": { + "buildDir": "build/socket-node", + "outputDir": "build/output", + "patchesDir": "build/patches", + "distDir": "dist", + // @yao-pkg/pkg's cache directory - hardcoded format, not configurable + // Must be: ~/.pkg-cache/{version}/built-{node_version}-{platform}-{arch} + "yaoCache": "~/.pkg-cache/{yao_version}", + + // Node.js build artifacts and their locations + "socketNode": { + // Where Node.js build system outputs (hardcoded in GYP/GN, not configurable) + "buildOutput": "build/socket-node/{node_version}/out/Release/node", + + // Processed versions (following Node's out/ structure) + "strippedOutput": "build/socket-node/{node_version}/out/Stripped/node", + "signedOutput": "build/socket-node/{node_version}/out/Signed/node" + } + }, + + // Source download configuration + "source": { + "baseUrl": "https://github.com/nodejs/node/archive/refs/tags" + } +} \ No newline at end of file diff --git a/.config/esbuild.config.mjs b/.config/esbuild.config.mjs new file mode 100644 index 000000000..652f1b7d7 --- /dev/null +++ b/.config/esbuild.config.mjs @@ -0,0 +1,180 @@ +/** @fileoverview esbuild configuration for Socket CLI - faster builds and smaller bundles */ + +import { promises as fs } from 'node:fs' +import path from 'node:path' + +import { analyzeMetafile, build } from 'esbuild' + +import constants from '../scripts/constants.mjs' + +const { + INLINED_SOCKET_CLI_HOMEPAGE, + INLINED_SOCKET_CLI_VERSION, + INLINED_SOCKET_CLI_VERSION_HASH, + distPath, + srcPath, +} = constants + +// Entry points for the CLI +const entryPoints = [ + path.join(srcPath, 'cli.mts'), + path.join(srcPath, 'npm-cli.mts'), + path.join(srcPath, 'npx-cli.mts'), + path.join(srcPath, 'pnpm-cli.mts'), + path.join(srcPath, 'constants.mts'), +] + +// External packages that should not be bundled +const external = [ + '@socketsecurity/registry', + '@socketsecurity/sdk', + // Keep React/Ink external for lazy loading + 'react', + 'react-dom', + 'ink', + 'ink-table', + '@pppp606/ink-chart', + 'yoga-layout', +] + +async function buildCli() { + console.log('🚀 Building Socket CLI with esbuild...\n') + + try { + // Main build + const result = await build({ + entryPoints, + bundle: true, + platform: 'node', + target: 'node18', + format: 'esm', + outdir: distPath, + + // Code splitting for dynamic imports + splitting: true, + chunkNames: 'chunks/[name]-[hash]', + + // External dependencies + external, + + // Optimizations + minify: process.env.NODE_ENV === 'production', + treeShaking: true, + + // Source maps for debugging + sourcemap: process.env.NODE_ENV !== 'production', + + // Define global constants + define: { + 'process.env.INLINED_SOCKET_CLI_VERSION': JSON.stringify(INLINED_SOCKET_CLI_VERSION), + 'process.env.INLINED_SOCKET_CLI_VERSION_HASH': JSON.stringify(INLINED_SOCKET_CLI_VERSION_HASH), + 'process.env.INLINED_SOCKET_CLI_HOMEPAGE': JSON.stringify(INLINED_SOCKET_CLI_HOMEPAGE), + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + }, + + // Handle .mts extensions + resolveExtensions: ['.mts', '.ts', '.mjs', '.js', '.json'], + + // Generate metafile for bundle analysis + metafile: true, + + // Preserve legal comments + legalComments: 'linked', + + // Loader for different file types + loader: { + '.mts': 'ts', + '.ts': 'ts', + '.json': 'json', + }, + + // Banner for CLI files + banner: { + js: '#!/usr/bin/env node\n', + }, + }) + + // Analyze bundle if requested + if (process.env.ANALYZE) { + const analysis = await analyzeMetafile(result.metafile, { + verbose: false, + }) + console.log('📊 Bundle Analysis:\n', analysis) + } + + // Write metafile for further analysis + await fs.writeFile( + path.join(distPath, 'metafile.json'), + JSON.stringify(result.metafile, null, 2) + ) + + console.log('✅ Build completed successfully!\n') + + // Report sizes + const stats = await fs.stat(path.join(distPath, 'cli.js')) + console.log(`Main CLI bundle size: ${(stats.size / 1024).toFixed(2)} KB`) + + } catch (error) { + console.error('❌ Build failed:', error) + throw error + } +} + +// Build React/Ink components separately +async function buildReactComponents() { + console.log('📦 Building React/Ink components...\n') + + const reactEntryPoints = [ + path.join(srcPath, 'commands/analytics/AnalyticsApp.js'), + path.join(srcPath, 'commands/audit-log/AuditLogApp.js'), + path.join(srcPath, 'commands/threat-feed/ThreatFeedApp.js'), + ].filter(async (file) => { + try { + await fs.access(file) + return true + } catch { + return false + } + }) + + if (reactEntryPoints.length === 0) { + console.log('No React components to build.') + return + } + + await build({ + entryPoints: reactEntryPoints, + bundle: true, + platform: 'node', + target: 'node18', + format: 'esm', + outdir: path.join(distPath, 'components'), + + // Don't bundle React - it will be provided by the runtime + external: ['react', 'react-dom', 'ink', 'ink-table'], + + minify: process.env.NODE_ENV === 'production', + treeShaking: true, + + resolveExtensions: ['.jsx', '.js', '.tsx', '.ts'], + }) + + console.log('✅ React components built successfully!\n') +} + +// Run builds +async function main() { + // Clean dist directory + await fs.rm(distPath, { recursive: true, force: true }) + await fs.mkdir(distPath, { recursive: true }) + await fs.mkdir(path.join(distPath, 'chunks'), { recursive: true }) + await fs.mkdir(path.join(distPath, 'components'), { recursive: true }) + + // Run builds in parallel + await Promise.all([ + buildCli(), + buildReactComponents(), + ]) +} + +main().catch(console.error) \ No newline at end of file diff --git a/eslint.config.js b/.config/eslint.config.mjs similarity index 68% rename from eslint.config.js rename to .config/eslint.config.mjs index 4c0dbed84..c530d0d13 100644 --- a/eslint.config.js +++ b/.config/eslint.config.mjs @@ -1,46 +1,50 @@ -'use strict' +import { createRequire } from 'node:module' +import path from 'node:path' +import { fileURLToPath } from 'node:url' -const path = require('node:path') - -const { +import { convertIgnorePatternToMinimatch, includeIgnoreFile, -} = require('@eslint/compat') -const js = require('@eslint/js') -const tsParser = require('@typescript-eslint/parser') -const { - createTypeScriptImportResolver, -} = require('eslint-import-resolver-typescript') -const importXPlugin = require('eslint-plugin-import-x') -const nodePlugin = require('eslint-plugin-n') -const sortDestructureKeysPlugin = require('eslint-plugin-sort-destructure-keys') -const unicornPlugin = require('eslint-plugin-unicorn') -const globals = require('globals') -const tsEslint = require('typescript-eslint') +} from '@eslint/compat' +import js from '@eslint/js' +import tsParser from '@typescript-eslint/parser' +import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript' +import importXPlugin from 'eslint-plugin-import-x' +import nodePlugin from 'eslint-plugin-n' +import sortDestructureKeysPlugin from 'eslint-plugin-sort-destructure-keys' +import unicornPlugin from 'eslint-plugin-unicorn' +import globals from 'globals' +import tsEslint from 'typescript-eslint' + +import maintainedNodeVersions from '@socketsecurity/registry/lib/constants/maintained-node-versions' -const constants = require('@socketsecurity/registry/lib/constants') -const { BIOME_JSON, GITIGNORE, LATEST, TSCONFIG_JSON } = constants +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const require = createRequire(import.meta.url) const { flatConfigs: origImportXFlatConfigs } = importXPlugin -const rootPath = __dirname -const rootTsConfigPath = path.join(rootPath, TSCONFIG_JSON) +const rootPath = path.dirname(__dirname) +const rootTsConfigPath = path.join(rootPath, 'tsconfig.json') const nodeGlobalsConfig = Object.fromEntries( Object.entries(globals.node).map(([k]) => [k, 'readonly']), ) -const biomeConfigPath = path.join(rootPath, BIOME_JSON) +const biomeConfigPath = path.join(__dirname, 'biome.json') const biomeConfig = require(biomeConfigPath) const biomeIgnores = { - name: 'Imported biome.json ignore patterns', + name: `Imported .config/biome.json ignore patterns`, ignores: biomeConfig.files.includes .filter(p => p.startsWith('!')) .map(p => convertIgnorePatternToMinimatch(p.slice(1))), } -const gitignorePath = path.join(rootPath, GITIGNORE) -const gitIgnores = includeIgnoreFile(gitignorePath) +const gitignorePath = path.join(rootPath, '.gitignore') +const gitIgnores = { + ...includeIgnoreFile(gitignorePath), + name: `Imported .gitignore ignore patterns`, +} if (process.env.LINT_DIST) { const isNotDistGlobPattern = p => !/(?:^|[\\/])dist/.test(p) @@ -62,6 +66,7 @@ const sharedPlugins = { const sharedRules = { 'unicorn/consistent-function-scoping': 'error', curly: 'error', + 'line-comment-position': ['error', { position: 'above' }], 'no-await-in-loop': 'error', 'no-control-regex': 'error', 'no-empty': ['error', { allowEmptyCatch: true }], @@ -142,7 +147,7 @@ const sharedRulesForNode = { 'test', 'test.describe', ], - version: constants.maintainedNodeVersions.current, + version: maintainedNodeVersions.current, }, ], 'n/prefer-node-protocol': 'error', @@ -154,7 +159,7 @@ function getImportXFlatConfigs(isEsm) { ...origImportXFlatConfigs.recommended, languageOptions: { ...origImportXFlatConfigs.recommended.languageOptions, - ecmaVersion: LATEST, + ecmaVersion: 'latest', sourceType: isEsm ? 'module' : 'script', }, rules: { @@ -188,11 +193,32 @@ function getImportXFlatConfigs(isEsm) { const importFlatConfigsForScript = getImportXFlatConfigs(false) const importFlatConfigsForModule = getImportXFlatConfigs(true) -module.exports = [ +export default [ gitIgnores, biomeIgnores, + { + name: 'Ignore test fixture node_modules', + ignores: ['**/test/fixtures/**/node_modules/**'], + }, + { + name: 'Ignore build data files and package.json', + ignores: [ + 'build/patches/**/*.json', + 'build/patches/**/*.md', + 'scripts/build/**/*.json', + 'scripts/build/**/*.json5', + 'package.json', + ], + }, { files: ['**/*.{cts,mts,ts}'], + ignores: [ + '**/*.test.{cts,mts,ts}', + 'test/**/*.{cts,mts,ts}', + 'src/test/**/*.{cts,mts,ts}', + 'src/utils/test-mocks.mts', + '**/*.d.{cts,mts,ts}', + ], ...js.configs.recommended, ...importFlatConfigsForModule.typescript, languageOptions: { @@ -210,23 +236,7 @@ module.exports = [ parserOptions: { ...js.configs.recommended.languageOptions?.parserOptions, ...importFlatConfigsForModule.typescript.languageOptions?.parserOptions, - projectService: { - ...importFlatConfigsForModule.typescript.languageOptions - ?.parserOptions?.projectService, - allowDefaultProject: [ - // Allow configs. - '*.config.mts', - // Allow paths like src/utils/*.test.mts. - 'src/*/*.test.mts', - // Allow paths like src/commands/optimize/*.test.mts. - 'src/*/*/*.test.mts', - 'test/*.mts', - ], - defaultProject: 'tsconfig.json', - tsconfigRootDir: rootPath, - // Need this to glob the test files in /src. Otherwise it won't work. - maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 1_000_000, - }, + project: rootTsConfigPath, }, }, linterOptions: { @@ -274,6 +284,72 @@ module.exports = [ 'no-unused-vars': 'off', }, }, + { + files: [ + '**/*.test.{cts,mts,ts}', + 'test/**/*.{cts,mts,ts}', + 'src/test/**/*.{cts,mts,ts}', + 'src/utils/test-mocks.mts', + 'scripts/**/*.d.{cts,mts,ts}', + ], + ...js.configs.recommended, + ...importFlatConfigsForModule.typescript, + languageOptions: { + ...js.configs.recommended.languageOptions, + ...importFlatConfigsForModule.typescript.languageOptions, + globals: { + ...js.configs.recommended.languageOptions?.globals, + ...importFlatConfigsForModule.typescript.languageOptions?.globals, + ...nodeGlobalsConfig, + BufferConstructor: 'readonly', + BufferEncoding: 'readonly', + NodeJS: 'readonly', + }, + parser: tsParser, + parserOptions: { + ...js.configs.recommended.languageOptions?.parserOptions, + ...importFlatConfigsForModule.typescript.languageOptions?.parserOptions, + // No project specified for test files since they're excluded from tsconfig + }, + }, + linterOptions: { + ...js.configs.recommended.linterOptions, + ...importFlatConfigsForModule.typescript.linterOptions, + reportUnusedDisableDirectives: 'off', + }, + plugins: { + ...js.configs.recommended.plugins, + ...importFlatConfigsForModule.typescript.plugins, + ...nodePlugin.configs['flat/recommended-module'].plugins, + ...sharedPlugins, + '@typescript-eslint': tsEslint.plugin, + }, + rules: { + ...js.configs.recommended.rules, + ...importFlatConfigsForModule.typescript.rules, + ...nodePlugin.configs['flat/recommended-module'].rules, + ...sharedRulesForNode, + ...sharedRules, + '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], + '@typescript-eslint/consistent-type-assertions': [ + 'error', + { assertionStyle: 'as' }, + ], + '@typescript-eslint/no-misused-new': 'error', + '@typescript-eslint/no-this-alias': [ + 'error', + { allowDestructuring: true }, + ], + // Disable TypeScript rules that require type information for test files + '@typescript-eslint/return-await': 'off', + // Disable the following rules because they don't play well with TypeScript. + 'n/hashbang': 'off', + 'n/no-extraneous-import': 'off', + 'n/no-missing-import': 'off', + 'no-redeclare': 'off', + 'no-unused-vars': 'off', + }, + }, { files: ['**/*.{cjs,js}'], ...js.configs.recommended, diff --git a/.config/packages/package.cli-legacy.json b/.config/packages/package.cli-legacy.json new file mode 100644 index 000000000..e7f460464 --- /dev/null +++ b/.config/packages/package.cli-legacy.json @@ -0,0 +1,51 @@ +{ + "name": "@socketsecurity/cli", + "version": "1.1.22", + "description": "CLI for Socket.dev", + "homepage": "https://github.com/SocketDev/socket-cli", + "license": "MIT AND OFL-1.1", + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-cli.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "bin": { + "cli": "bin/cli.js", + "socket": "bin/cli.js", + "socket-npm": "bin/npm-cli.js", + "socket-npx": "bin/npx-cli.js", + "socket-pnpm": "bin/pnpm-cli.js", + "socket-yarn": "bin/yarn-cli.js" + }, + "types": "./dist/types/src/cli.d.ts", + "exports": { + "./bin/cli.js": "./dist/cli.js", + "./bin/npm-cli.js": "./dist/npm-cli.js", + "./bin/npx-cli.js": "./dist/npx-cli.js", + "./bin/pnpm-cli.js": "./dist/pnpm-cli.js", + "./bin/yarn-cli.js": "./dist/yarn-cli.js", + "./package.json": "./package.json", + "./requirements.json": "./requirements.json", + "./translations.json": "./translations.json" + }, + "files": ["dist", "bin", "requirements.json", "translations.json"], + "keywords": [ + "socket", + "socket.dev", + "security", + "supply chain", + "vulnerability", + "cli", + "npm", + "yarn", + "pnpm" + ], + "engines": { + "node": ">=18.18.0" + }, + "preferGlobal": true +} diff --git a/.config/packages/package.cli-with-sentry.json b/.config/packages/package.cli-with-sentry.json new file mode 100644 index 000000000..28eeb3c42 --- /dev/null +++ b/.config/packages/package.cli-with-sentry.json @@ -0,0 +1,54 @@ +{ + "name": "@socketsecurity/cli-with-sentry", + "version": "1.1.22", + "description": "CLI for Socket.dev with error monitoring", + "homepage": "https://github.com/SocketDev/socket-cli", + "license": "MIT AND OFL-1.1", + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-cli.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "bin": { + "socket-with-sentry": "bin/cli.js", + "socket-sentry": "bin/cli.js", + "socket-sentry-npm": "bin/npm-cli.js", + "socket-sentry-npx": "bin/npx-cli.js", + "socket-sentry-pnpm": "bin/pnpm-cli.js", + "socket-sentry-yarn": "bin/yarn-cli.js" + }, + "types": "./dist/types/src/cli.d.ts", + "exports": { + "./bin/cli.js": "./dist/cli.js", + "./bin/npm-cli.js": "./dist/npm-cli.js", + "./bin/npx-cli.js": "./dist/npx-cli.js", + "./bin/pnpm-cli.js": "./dist/pnpm-cli.js", + "./bin/yarn-cli.js": "./dist/yarn-cli.js", + "./package.json": "./package.json", + "./requirements.json": "./requirements.json", + "./translations.json": "./translations.json" + }, + "files": ["dist", "bin", "requirements.json", "translations.json"], + "dependencies": { + "@sentry/node": "8.42.0" + }, + "keywords": [ + "socket", + "socket.dev", + "security", + "supply chain", + "vulnerability", + "cli", + "npm", + "yarn", + "pnpm" + ], + "engines": { + "node": ">=18.18.0" + }, + "preferGlobal": true +} diff --git a/.config/packages/package.cli.json b/.config/packages/package.cli.json new file mode 100644 index 000000000..b48bbc7a0 --- /dev/null +++ b/.config/packages/package.cli.json @@ -0,0 +1,50 @@ +{ + "name": "@socketsecurity/cli", + "version": "1.1.22", + "description": "CLI for Socket.dev", + "homepage": "https://github.com/SocketDev/socket-cli", + "license": "MIT AND OFL-1.1", + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-cli.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "bin": { + "socket": "bin/cli.js", + "socket-npm": "bin/npm-cli.js", + "socket-npx": "bin/npx-cli.js", + "socket-pnpm": "bin/pnpm-cli.js", + "socket-yarn": "bin/yarn-cli.js" + }, + "types": "./dist/types/src/cli.d.ts", + "exports": { + "./bin/cli.js": "./dist/cli.js", + "./bin/npm-cli.js": "./dist/npm-cli.js", + "./bin/npx-cli.js": "./dist/npx-cli.js", + "./bin/pnpm-cli.js": "./dist/pnpm-cli.js", + "./bin/yarn-cli.js": "./dist/yarn-cli.js", + "./package.json": "./package.json", + "./requirements.json": "./requirements.json", + "./translations.json": "./translations.json" + }, + "files": ["dist", "bin", "requirements.json", "translations.json"], + "keywords": [ + "socket", + "socket.dev", + "security", + "supply chain", + "vulnerability", + "cli", + "npm", + "yarn", + "pnpm" + ], + "engines": { + "node": ">=18.18.0" + }, + "preferGlobal": true +} diff --git a/.config/rollup.base.config.mjs b/.config/rollup.base.config.mjs index f48a4f139..03fbcb315 100644 --- a/.config/rollup.base.config.mjs +++ b/.config/rollup.base.config.mjs @@ -14,13 +14,15 @@ import { escapeRegExp } from '@socketsecurity/registry/lib/regexps' import { spawnSync } from '@socketsecurity/registry/lib/spawn' import { stripAnsi } from '@socketsecurity/registry/lib/strings' -import constants from '../scripts/constants.js' -import socketModifyPlugin from '../scripts/rollup/socket-modify-plugin.js' +import constants from '../scripts/constants.mjs' +import socketModifyPlugin from '../scripts/rollup/socket-modify-plugin.mjs' +// Transform Ink to remove DEV mode block with top-level await +import transformInkPlugin from '../scripts/rollup/transform-ink-plugin.mjs' import { getPackageName, isBuiltin, normalizeId, -} from '../scripts/utils/packages.js' +} from '../scripts/utils/packages.mjs' const { INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION, @@ -29,6 +31,8 @@ const { INLINED_SOCKET_CLI_LEGACY_BUILD, INLINED_SOCKET_CLI_NAME, INLINED_SOCKET_CLI_PUBLISHED_BUILD, + INLINED_SOCKET_CLI_PYTHON_BUILD_TAG, + INLINED_SOCKET_CLI_PYTHON_VERSION, INLINED_SOCKET_CLI_SENTRY_BUILD, INLINED_SOCKET_CLI_SYNP_VERSION, INLINED_SOCKET_CLI_VERSION, @@ -41,8 +45,12 @@ const { export const EXTERNAL_PACKAGES = [ '@socketsecurity/registry', - 'blessed', - 'blessed-contrib', + '@socketsecurity/sdk', + 'react', + 'ink', + 'ink-table', + '@pppp606/ink-chart', + 'yoga-layout', ] const builtinAliases = builtinModules.reduce((o, n) => { @@ -109,7 +117,7 @@ export default function baseConfig(extendConfig = {}) { ? extendConfig.plugins.slice() : [] - const extractedPlugins = { __proto__: null } + const extractedPlugins = Object.create(null) if (extendPlugins.length) { for (const pluginName of [ 'babel', @@ -132,20 +140,71 @@ export default function baseConfig(extendConfig = {}) { } return { + // Enable caching for faster rebuilds + cache: { + dir: path.join(rootPath, '.cache', 'rollup'), + }, + + // Maximum parallelization + maxParallelFileOps: 10, + + // Enable safe tree-shaking that preserves side effects + treeshake: { + moduleSideEffects: true, + propertyReadSideEffects: true, + tryCatchDeoptimization: false, + unknownGlobalSideEffects: true, + correctVarValueBeforeDeclaration: true, + preset: 'safest' + }, external(rawId) { + // Order checks by likelihood for better performance. + // Externalize Node.js built-ins (most common case). + if (isBuiltin(rawId)) { + return true + } + // Externalize special rollup external suffix. + if (rawId.endsWith(ROLLUP_EXTERNAL_SUFFIX)) { + return true + } const id = normalizeId(rawId) + // Externalize anything from the external directory, except entry points. + if ( + id.includes('/external/') && + !id.endsWith('/external/ink-table.mjs') && + !id.endsWith('/external/yoga-layout.mjs') + ) { + return true + } + // Externalize TypeScript declaration files. + if ( + id.endsWith('.d.ts') || + id.endsWith('.d.mts') || + id.endsWith('.d.cts') + ) { + return true + } const pkgName = getPackageName( id, path.isAbsolute(id) ? nmPath.length + 1 : 0, ) - return ( - id.endsWith('.d.cts') || - id.endsWith('.d.mts') || - id.endsWith('.d.ts') || - EXTERNAL_PACKAGES.includes(pkgName) || - rawId.endsWith(ROLLUP_EXTERNAL_SUFFIX) || - isBuiltin(rawId) - ) + // Externalize @socketsecurity/registry and all its internal paths. + if ( + pkgName === '@socketsecurity/registry' || + id.includes('@socketsecurity/registry/external/') || + id.includes('/@socketsecurity+registry@') + ) { + return true + } + // Externalize @socketsecurity/sdk and all its internal paths. + if ( + pkgName === '@socketsecurity/sdk' || + id.includes('/@socketsecurity+sdk@') + ) { + return true + } + // Externalize other specific external packages. + return EXTERNAL_PACKAGES.includes(pkgName) }, onwarn(warning, warn) { // Suppress warnings. @@ -160,10 +219,21 @@ export default function baseConfig(extendConfig = {}) { }, ...extendConfig, plugins: [ + // Resolve yoga-layout to our external wrapper + { + name: 'resolve-yoga-layout', + resolveId(id) { + if (id === 'yoga-layout') { + return path.join(rootPath, 'src/external/yoga-layout.mjs') + } + }, + }, + // Transform Ink to remove DEV mode block with top-level await + transformInkPlugin(), extractedPlugins['node-resolve'] ?? nodeResolve({ exportConditions: ['node'], - extensions: ['.mjs', '.js', '.json', '.ts', '.mts'], + extensions: ['.mjs', '.mts', '.js', '.ts', '.tsx', '.json'], preferBuiltins: true, }), extractedPlugins['json'] ?? jsonPlugin(), @@ -181,7 +251,17 @@ export default function baseConfig(extendConfig = {}) { babelHelpers: 'runtime', babelrc: false, configFile: path.join(configPath, 'babel.config.js'), - extensions: ['.mjs', '.js', '.ts', '.mts'], + extensions: ['.mjs', '.mts', '.js', '.ts', '.tsx'], + // Skip files that don't need transformation + exclude: [ + 'node_modules/**', + '**/*.min.js', + '**/vendor/**' + ], + // Only include what we need + include: [ + 'src/**', + ], }), extractedPlugins['unplugin-purge-polyfills'] ?? purgePolyfills.rollup({ @@ -206,6 +286,11 @@ export default function baseConfig(extendConfig = {}) { getRootPkgJsonSync().devDependencies['@cyclonedx/cdxgen'], ), ], + [INLINED_SOCKET_CLI_PYTHON_VERSION, () => JSON.stringify('3.10.18')], + [ + INLINED_SOCKET_CLI_PYTHON_BUILD_TAG, + () => JSON.stringify('20250918'), + ], [ INLINED_SOCKET_CLI_HOMEPAGE, () => JSON.stringify(getRootPkgJsonSync().homepage), @@ -243,7 +328,7 @@ export default function baseConfig(extendConfig = {}) { INLINED_SOCKET_CLI_VERSION_HASH, () => JSON.stringify(getSocketCliVersionHash()), ], - [VITEST, () => !!constants.ENV[VITEST]], + [VITEST, () => JSON.stringify(!!constants.ENV[VITEST])], ].reduce((obj, { 0: name, 1: value }) => { obj[`process.env.${name}`] = value obj[`process.env['${name}']`] = value diff --git a/.config/rollup.dist.config.mjs b/.config/rollup.dist.config.mjs index 7857466ad..87154b11e 100644 --- a/.config/rollup.dist.config.mjs +++ b/.config/rollup.dist.config.mjs @@ -1,74 +1,59 @@ -import assert from 'node:assert' -import { existsSync, promises as fs } from 'node:fs' +import { promises as fs } from 'node:fs' import os from 'node:os' import path from 'node:path' -import util from 'node:util' -import { babel as babelPlugin } from '@rollup/plugin-babel' -import commonjsPlugin from '@rollup/plugin-commonjs' -import jsonPlugin from '@rollup/plugin-json' -import { nodeResolve } from '@rollup/plugin-node-resolve' +import { deleteAsync } from 'del' import fastGlob from 'fast-glob' -import trash from 'trash' import { isDirEmptySync } from '@socketsecurity/registry/lib/fs' -import { hasKeys } from '@socketsecurity/registry/lib/objects' -import { - fetchPackageManifest, - readPackageJson, -} from '@socketsecurity/registry/lib/packages' +// import { hasKeys } from '@socketsecurity/registry/lib/objects' +// import { +// fetchPackageManifest, +// readPackageJson, +// } from '@socketsecurity/registry/lib/packages' import { normalizePath } from '@socketsecurity/registry/lib/path' import { escapeRegExp } from '@socketsecurity/registry/lib/regexps' -import { naturalCompare } from '@socketsecurity/registry/lib/sorts' -import { spawn } from '@socketsecurity/registry/lib/spawn' +// import { naturalCompare } from '@socketsecurity/registry/lib/sorts' +// import { spawn } from '@socketsecurity/registry/lib/spawn' import baseConfig, { EXTERNAL_PACKAGES } from './rollup.base.config.mjs' -import constants from '../scripts/constants.js' -import socketModifyPlugin from '../scripts/rollup/socket-modify-plugin.js' -import { - getPackageName, - isBuiltin, - normalizeId, -} from '../scripts/utils/packages.js' +import constants from '../scripts/constants.mjs' +import socketModifyPlugin from '../scripts/rollup/socket-modify-plugin.mjs' +import { isBuiltin, normalizeId } from '../scripts/utils/packages.mjs' const { CONSTANTS, INLINED_SOCKET_CLI_LEGACY_BUILD, INLINED_SOCKET_CLI_SENTRY_BUILD, - INSTRUMENT_WITH_SENTRY, NODE_MODULES, NODE_MODULES_GLOB_RECURSIVE, - ROLLUP_EXTERNAL_SUFFIX, + PRELOAD_SENTRY, SHADOW_NPM_BIN, - SHADOW_NPM_INJECT, + SHADOW_NPM_PRELOAD_ARBORIST, SHADOW_NPX_BIN, SHADOW_PNPM_BIN, - SHADOW_YARN_BIN, SLASH_NODE_MODULES_SLASH, - SOCKET_CLI_BIN_NAME, - SOCKET_CLI_BIN_NAME_ALIAS, - SOCKET_CLI_LEGACY_PACKAGE_NAME, - SOCKET_CLI_NPM_BIN_NAME, - SOCKET_CLI_NPX_BIN_NAME, - SOCKET_CLI_PACKAGE_NAME, - SOCKET_CLI_PNPM_BIN_NAME, - SOCKET_CLI_SENTRY_BIN_NAME, - SOCKET_CLI_SENTRY_BIN_NAME_ALIAS, - SOCKET_CLI_SENTRY_NPM_BIN_NAME, - SOCKET_CLI_SENTRY_NPX_BIN_NAME, - SOCKET_CLI_SENTRY_PACKAGE_NAME, - SOCKET_CLI_SENTRY_PNPM_BIN_NAME, - SOCKET_CLI_SENTRY_YARN_BIN_NAME, - SOCKET_CLI_YARN_BIN_NAME, + // SOCKET_CLI_BIN_NAME, + // SOCKET_CLI_BIN_NAME_ALIAS, + // SOCKET_CLI_LEGACY_PACKAGE_NAME, + // SOCKET_CLI_NPM_BIN_NAME, + // SOCKET_CLI_NPX_BIN_NAME, + // SOCKET_CLI_PACKAGE_NAME, + // SOCKET_CLI_PNPM_BIN_NAME, + // SOCKET_CLI_SENTRY_BIN_NAME, + // SOCKET_CLI_SENTRY_BIN_NAME_ALIAS, + // SOCKET_CLI_SENTRY_NPM_BIN_NAME, + // SOCKET_CLI_SENTRY_NPX_BIN_NAME, + // SOCKET_CLI_SENTRY_PACKAGE_NAME, + // SOCKET_CLI_SENTRY_PNPM_BIN_NAME, + // SOCKET_CLI_SENTRY_YARN_BIN_NAME, + // SOCKET_CLI_YARN_BIN_NAME, } = constants -const BLESSED = 'blessed' -const BLESSED_CONTRIB = 'blessed-contrib' const FLAGS = 'flags' -const LICENSE_MD = `LICENSE.md` -const SENTRY_NODE = '@sentry/node' -const SOCKET_DESCRIPTION = 'CLI for Socket.dev' -const SOCKET_DESCRIPTION_WITH_SENTRY = `${SOCKET_DESCRIPTION}, includes Sentry error handling, otherwise identical to the regular \`${SOCKET_CLI_BIN_NAME}\` package` +// const SENTRY_NODE = '@sentry/node' +// const SOCKET_DESCRIPTION = 'CLI for Socket.dev' +// const SOCKET_DESCRIPTION_WITH_SENTRY = `${SOCKET_DESCRIPTION}, includes Sentry error handling, otherwise identical to the regular \`${SOCKET_CLI_BIN_NAME}\` package` const SOCKET_SECURITY_REGISTRY = '@socketsecurity/registry' const UTILS = 'utils' const VENDOR = 'vendor' @@ -89,46 +74,30 @@ async function copyBashCompletion() { } async function copyExternalPackages() { - const { blessedContribPath, blessedPath, socketRegistryPath } = constants - const nmPath = path.join(constants.rootPath, NODE_MODULES) - const blessedContribNmPath = path.join(nmPath, BLESSED_CONTRIB) + const { socketRegistryPath } = constants // Copy package folders. - await Promise.all([ - ...EXTERNAL_PACKAGES - // Skip copying 'blessed-contrib' over because we already - // have it bundled as ./external/blessed-contrib. - .filter(n => n !== BLESSED_CONTRIB) - // Copy the other packages over to ./external/. - .map(n => - copyPackage(n, { - strict: - // Skip adding 'use strict' directives to Socket packages. - n !== SOCKET_SECURITY_REGISTRY, - }), - ), - // Copy 'blessed-contrib' license over to - // ./external/blessed-contrib/LICENSE.md. - await fs.cp( - `${blessedContribNmPath}/${LICENSE_MD}`, - `${blessedContribPath}/${LICENSE_MD}`, - { dereference: true }, + await Promise.all( + EXTERNAL_PACKAGES.map(n => + copyPackage(n, { + strict: + // Skip adding 'use strict' directives to Socket packages. + n !== SOCKET_SECURITY_REGISTRY, + }), ), - ]) + ) const alwaysIgnoredPatterns = ['LICENSE*', 'README*'] // Cleanup package files. await Promise.all( [ - [blessedPath, ['lib/**/*.js', 'usr/**/**', 'vendor/**/*.js']], - [blessedContribPath, ['lib/**/*.js', 'index.js']], [ socketRegistryPath, [ 'external/**/*.js', - 'lib/**/*.js', 'index.js', + 'lib/**/*.js', 'extensions.json', 'manifest.json', ], @@ -140,19 +109,31 @@ async function copyExternalPackages() { await removeEmptyDirs(thePath) }), ) - // Rewire 'blessed' inside 'blessed-contrib'. + // Remove all source map files from external packages. + await removeFiles(constants.externalPath, { + exclude: [ + ...alwaysIgnoredPatterns, + '**/*.js', + '**/*.mjs', + '**/*.cjs', + '**/*.json', + '**/*.d.ts', + ], + }) + // Rewire '@socketsecurity/registry' inside '@socketsecurity/sdk'. + const sdkPath = path.join(constants.externalPath, '@socketsecurity/sdk') await Promise.all( ( await fastGlob.glob(['**/*.js'], { absolute: true, - cwd: blessedContribPath, + cwd: sdkPath, ignore: [NODE_MODULES_GLOB_RECURSIVE], }) ).map(async p => { - const relPath = path.relative(path.dirname(p), blessedPath) + const relPath = path.relative(path.dirname(p), socketRegistryPath) const content = await fs.readFile(p, 'utf8') const modded = content.replace( - /(?<=require\(["'])blessed(?=(?:\/[^"']+)?["']\))/g, + /(?<=require\(["'])@socketsecurity\/registry(?=(?:\/[^"']+)?["']\))/g, () => relPath, ) await fs.writeFile(p, modded, 'utf8') @@ -193,80 +174,62 @@ async function copyPackage(pkgName, options) { } let _sentryManifest -async function getSentryManifest() { - if (_sentryManifest === undefined) { - _sentryManifest = await fetchPackageManifest(`${SENTRY_NODE}@latest`) - } - return _sentryManifest -} +// async function getSentryManifest() { +// if (_sentryManifest === undefined) { +// _sentryManifest = await fetchPackageManifest(`${SENTRY_NODE}@latest`) +// } +// return _sentryManifest +// } -async function updatePackageJson() { - const editablePkgJson = await readPackageJson(constants.rootPath, { - editable: true, - normalize: true, - }) - const bin = resetBin(editablePkgJson.content.bin) - const dependencies = resetDependencies(editablePkgJson.content.dependencies) - editablePkgJson.update({ - name: SOCKET_CLI_PACKAGE_NAME, - description: SOCKET_DESCRIPTION, - bin, - dependencies: hasKeys(dependencies) ? dependencies : undefined, - }) +async function copyPublishFiles() { + // Determine which package.json to use based on build variant. + let packageJsonSource if (constants.ENV[INLINED_SOCKET_CLI_LEGACY_BUILD]) { - editablePkgJson.update({ - name: SOCKET_CLI_LEGACY_PACKAGE_NAME, - bin: { - [SOCKET_CLI_BIN_NAME_ALIAS]: bin[SOCKET_CLI_BIN_NAME], - ...bin, - }, - }) + packageJsonSource = path.join( + constants.rootPath, + '.config/packages/package.cli-legacy.json', + ) } else if (constants.ENV[INLINED_SOCKET_CLI_SENTRY_BUILD]) { - editablePkgJson.update({ - name: SOCKET_CLI_SENTRY_PACKAGE_NAME, - description: SOCKET_DESCRIPTION_WITH_SENTRY, - bin: { - [SOCKET_CLI_SENTRY_BIN_NAME_ALIAS]: bin[SOCKET_CLI_BIN_NAME], - [SOCKET_CLI_SENTRY_BIN_NAME]: bin[SOCKET_CLI_BIN_NAME], - [SOCKET_CLI_SENTRY_NPM_BIN_NAME]: bin[SOCKET_CLI_NPM_BIN_NAME], - [SOCKET_CLI_SENTRY_NPX_BIN_NAME]: bin[SOCKET_CLI_NPX_BIN_NAME], - [SOCKET_CLI_SENTRY_PNPM_BIN_NAME]: bin[SOCKET_CLI_PNPM_BIN_NAME], - [SOCKET_CLI_SENTRY_YARN_BIN_NAME]: bin[SOCKET_CLI_YARN_BIN_NAME], - }, - dependencies: { - ...dependencies, - [SENTRY_NODE]: (await getSentryManifest()).version, - }, - }) + packageJsonSource = path.join( + constants.rootPath, + '.config/packages/package.cli-with-sentry.json', + ) + } else { + packageJsonSource = path.join( + constants.rootPath, + '.config/packages/package.cli.json', + ) } - await editablePkgJson.save() -} -async function updatePackageLockFile() { - const { rootPackageLockPath } = constants - if (!existsSync(rootPackageLockPath)) { - return - } - try { - await spawn( - 'pnpm', - [ - 'install', - '--frozen-lockfile=false', - '--config.confirmModulesPurge=false', - ], - { - cwd: constants.rootPath, - stdio: 'inherit', - }, - ) - } catch (e) { - console.warn('Failed to update pnpm lockfile:', e?.message) + // Read the source package.json directly as JSON. + const sourcePkgJson = JSON.parse(await fs.readFile(packageJsonSource, 'utf8')) + + // Write package.json to dist (version already set in package variant files). + const distPackageJsonPath = path.join(constants.distPath, 'package.json') + const distPkgJson = { + ...sourcePkgJson, } + await fs.writeFile( + distPackageJsonPath, + JSON.stringify(distPkgJson, null, 2) + '\n', + ) + + // JSON files are now inlined during build via @rollup/plugin-json + + // Copy bin directory to dist. + const binDir = path.join(constants.rootPath, 'bin') + const distBinDir = path.join(constants.distPath, 'bin') + await fs.mkdir(distBinDir, { recursive: true }) + const binFiles = await fs.readdir(binDir) + await Promise.all( + binFiles.map(file => + fs.copyFile(path.join(binDir, file), path.join(distBinDir, file)), + ), + ) } async function removeEmptyDirs(thePath) { - await trash( + await deleteAsync( ( await fastGlob.glob(['**/'], { ignore: [NODE_MODULES_GLOB_RECURSIVE], @@ -283,80 +246,28 @@ async function removeEmptyDirs(thePath) { async function removeFiles(thePath, options) { const { exclude } = { __proto__: null, ...options } - const ignore = Array.isArray(exclude) ? exclude : exclude ? [exclude] : [] - return await trash( + return await deleteAsync( await fastGlob.glob(['**/*'], { absolute: true, onlyFiles: true, cwd: thePath, dot: true, - ignore, + ignore: Array.isArray(exclude) + ? exclude + : (exclude ? [exclude] : []), }), ) } -function resetBin(bin) { - const tmpBin = { - [SOCKET_CLI_BIN_NAME]: - bin?.[SOCKET_CLI_BIN_NAME] ?? bin?.[SOCKET_CLI_SENTRY_BIN_NAME], - [SOCKET_CLI_NPM_BIN_NAME]: - bin?.[SOCKET_CLI_NPM_BIN_NAME] ?? bin?.[SOCKET_CLI_SENTRY_NPM_BIN_NAME], - [SOCKET_CLI_NPX_BIN_NAME]: - bin?.[SOCKET_CLI_NPX_BIN_NAME] ?? bin?.[SOCKET_CLI_SENTRY_NPX_BIN_NAME], - [SOCKET_CLI_PNPM_BIN_NAME]: - bin?.[SOCKET_CLI_PNPM_BIN_NAME] ?? bin?.[SOCKET_CLI_SENTRY_PNPM_BIN_NAME], - [SOCKET_CLI_YARN_BIN_NAME]: - bin?.[SOCKET_CLI_YARN_BIN_NAME] ?? bin?.[SOCKET_CLI_SENTRY_YARN_BIN_NAME], - } - const newBin = { - ...(tmpBin[SOCKET_CLI_BIN_NAME] - ? { [SOCKET_CLI_BIN_NAME]: tmpBin.socket } - : {}), - ...(tmpBin[SOCKET_CLI_NPM_BIN_NAME] - ? { [SOCKET_CLI_NPM_BIN_NAME]: tmpBin[SOCKET_CLI_NPM_BIN_NAME] } - : {}), - ...(tmpBin[SOCKET_CLI_NPX_BIN_NAME] - ? { [SOCKET_CLI_NPX_BIN_NAME]: tmpBin[SOCKET_CLI_NPX_BIN_NAME] } - : {}), - ...(tmpBin[SOCKET_CLI_PNPM_BIN_NAME] - ? { [SOCKET_CLI_PNPM_BIN_NAME]: tmpBin[SOCKET_CLI_PNPM_BIN_NAME] } - : {}), - ...(tmpBin[SOCKET_CLI_YARN_BIN_NAME] - ? { [SOCKET_CLI_YARN_BIN_NAME]: tmpBin[SOCKET_CLI_YARN_BIN_NAME] } - : {}), - } - assert( - util.isDeepStrictEqual(Object.keys(newBin).sort(naturalCompare), [ - SOCKET_CLI_BIN_NAME, - SOCKET_CLI_NPM_BIN_NAME, - SOCKET_CLI_NPX_BIN_NAME, - SOCKET_CLI_PNPM_BIN_NAME, - SOCKET_CLI_YARN_BIN_NAME, - ]), - "Update the rollup Legacy and Sentry build's .bin to match the default build.", - ) - return newBin -} - -function resetDependencies(deps) { - const { [SENTRY_NODE]: _ignored, ...newDeps } = { ...deps } - return newDeps -} - export default async () => { - const { configPath, distPath, rootPath, srcPath } = constants - const nmPath = normalizePath(path.join(rootPath, NODE_MODULES)) + const { distPath, rootPath, srcPath } = constants const constantsSrcPath = normalizePath(path.join(srcPath, 'constants.mts')) - const externalSrcPath = normalizePath(path.join(srcPath, 'external')) - const blessedContribSrcPath = normalizePath( - path.join(externalSrcPath, BLESSED_CONTRIB), - ) const flagsSrcPath = normalizePath(path.join(srcPath, 'flags.mts')) const shadowNpmBinSrcPath = normalizePath( path.join(srcPath, 'shadow/npm/bin.mts'), ) - const shadowNpmInjectSrcPath = normalizePath( - path.join(srcPath, 'shadow/npm/inject.mts'), + const shadowNpmPreloadArboristSrcPath = normalizePath( + path.join(srcPath, 'shadow/npm/preload-arborist.mts'), ) const shadowNpxBinSrcPath = normalizePath( path.join(srcPath, 'shadow/npx/bin.mts'), @@ -364,9 +275,6 @@ export default async () => { const shadowPnpmBinSrcPath = normalizePath( path.join(srcPath, 'shadow/pnpm/bin.mts'), ) - const shadowYarnBinSrcPath = normalizePath( - path.join(srcPath, 'shadow/yarn/bin.mts'), - ) const utilsSrcPath = normalizePath(path.join(srcPath, UTILS)) return [ @@ -377,16 +285,14 @@ export default async () => { 'npm-cli': `${srcPath}/npm-cli.mts`, 'npx-cli': `${srcPath}/npx-cli.mts`, 'pnpm-cli': `${srcPath}/pnpm-cli.mts`, - 'yarn-cli': `${srcPath}/yarn-cli.mts`, [CONSTANTS]: `${srcPath}/constants.mts`, [SHADOW_NPM_BIN]: `${srcPath}/shadow/npm/bin.mts`, - [SHADOW_NPM_INJECT]: `${srcPath}/shadow/npm/inject.mts`, + [SHADOW_NPM_PRELOAD_ARBORIST]: `${srcPath}/shadow/npm/preload-arborist.mts`, [SHADOW_NPX_BIN]: `${srcPath}/shadow/npx/bin.mts`, [SHADOW_PNPM_BIN]: `${srcPath}/shadow/pnpm/bin.mts`, - [SHADOW_YARN_BIN]: `${srcPath}/shadow/yarn/bin.mts`, ...(constants.ENV[INLINED_SOCKET_CLI_SENTRY_BUILD] ? { - [INSTRUMENT_WITH_SENTRY]: `${srcPath}/${INSTRUMENT_WITH_SENTRY}.mts`, + [PRELOAD_SENTRY]: `${srcPath}/${PRELOAD_SENTRY}.mts`, } : {}), }, @@ -398,6 +304,14 @@ export default async () => { exports: 'auto', externalLiveBindings: false, format: 'cjs', + experimentalMinChunkSize: 10000, + generatedCode: { + preset: 'es2015', + arrowFunctions: true, + constBindings: true, + objectShorthand: true + }, + compact: true, manualChunks(id_) { const id = normalizeId(id_) switch (id) { @@ -407,14 +321,12 @@ export default async () => { return FLAGS case shadowNpmBinSrcPath: return SHADOW_NPM_BIN - case shadowNpmInjectSrcPath: - return SHADOW_NPM_INJECT + case shadowNpmPreloadArboristSrcPath: + return SHADOW_NPM_PRELOAD_ARBORIST case shadowNpxBinSrcPath: return SHADOW_NPX_BIN case shadowPnpmBinSrcPath: return SHADOW_PNPM_BIN - case shadowYarnBinSrcPath: - return SHADOW_YARN_BIN default: if (id.startsWith(`${utilsSrcPath}/`)) { return UTILS @@ -425,22 +337,41 @@ export default async () => { return null } }, - sourcemap: true, - sourcemapDebugIds: true, + sourcemap: false, }, ], plugins: [ - // Replace require() and require.resolve() calls like - // require('blessed/lib/widgets/screen') with - // require('../external/blessed/lib/widgets/screen') - ...EXTERNAL_PACKAGES.map(n => - socketModifyPlugin({ - find: new RegExp( - `(?<=require[$\\w]*(?:\\.resolve)?\\(["'])${escapeRegExp(n)}(?=(?:\\/[^"']+)?["']\\))`, - 'g', - ), - replace: id => `../external/${id}`, - }), + // Replace require() and require.resolve() calls for @socketsecurity/registry + // require('@socketsecurity/registry/lib/path') with + // require('./external/@socketsecurity/registry/dist/lib/path') + socketModifyPlugin({ + find: new RegExp( + `(?<=require[$\\w]*(?:\\.resolve)?\\(["'])${escapeRegExp(SOCKET_SECURITY_REGISTRY)}(?=(?:\\/[^"']+)?["']\\))`, + 'g', + ), + replace: () => `./external/${SOCKET_SECURITY_REGISTRY}/dist`, + }), + // Replace individual registry constant requires with index file require. + // require('./external/@socketsecurity/registry/dist/lib/constants/socket-public-api-token') with + // require('./external/@socketsecurity/registry/dist/lib/constants/index') + socketModifyPlugin({ + find: new RegExp( + `(?<=require[$\\w]*(?:\\.resolve)?\\(["'])(\\.+/external/${escapeRegExp(SOCKET_SECURITY_REGISTRY)}/dist/lib/constants)/[^"']+(?=["']\\))`, + 'g', + ), + replace: (match, prefix) => `${prefix}/index`, + }), + // Replace require() and require.resolve() calls for other external packages + // (currently just @socketsecurity/sdk). + ...EXTERNAL_PACKAGES.filter(n => n !== SOCKET_SECURITY_REGISTRY).map( + n => + socketModifyPlugin({ + find: new RegExp( + `(?<=require[$\\w]*(?:\\.resolve)?\\(["'])${escapeRegExp(n)}(?=(?:\\/[^"']+)?["']\\))`, + 'g', + ), + replace: id => `./external/${id}`, + }), ), // Replace require.resolve('node-gyp/bin/node-gyp.js') with // require('./constants.js').npmNmNodeGypPath. @@ -459,74 +390,85 @@ export default async () => { await Promise.all([ copyInitGradle(), copyBashCompletion(), - updatePackageJson(), - // Remove dist/vendor.js.map file. - trash([path.join(distPath, `${VENDOR}.js.map`)]), + copyPublishFiles(), ]) // Copy external packages AFTER other operations to avoid conflicts. await copyExternalPackages() - // Update package-lock.json AFTER package.json. - await updatePackageLockFile() }, }, ], }), - // Bundle /src/external/blessed-contrib/ files and output to - // /external/blessed-contrib/. - ...( - await fastGlob.glob(['**/*.mjs'], { - absolute: true, - cwd: blessedContribSrcPath, - }) - ).map(filepath => { - const relPath = `${path.relative(srcPath, filepath).slice(0, -4 /*.mjs*/)}.js` - return { - input: filepath, - output: [ - { - file: path.join(rootPath, relPath), - exports: 'auto', - externalLiveBindings: false, - format: 'cjs', - inlineDynamicImports: true, - sourcemap: false, - }, - ], - external(rawId) { - const id = normalizeId(rawId) - const pkgName = getPackageName( - id, - path.isAbsolute(id) ? nmPath.length + 1 : 0, - ) - return ( - pkgName === BLESSED || - rawId.endsWith(ROLLUP_EXTERNAL_SUFFIX) || - isBuiltin(rawId) - ) + // Bundle external wrapper modules separately + // Each external module gets its own self-contained bundle + // Bundle each one individually to avoid code splitting + baseConfig({ + input: `${srcPath}/external/ink.mjs`, + output: { + file: path.join(path.relative(rootPath, distPath), 'external/ink.js'), + format: 'cjs', + exports: 'auto', + externalLiveBindings: false, + generatedCode: { + preset: 'es2015', + arrowFunctions: true, + constBindings: true, + objectShorthand: true }, - plugins: [ - nodeResolve({ - exportConditions: ['node'], - extensions: ['.mjs', '.js', '.json'], - preferBuiltins: true, - }), - jsonPlugin(), - commonjsPlugin({ - defaultIsModuleExports: true, - extensions: ['.cjs', '.js'], - ignoreDynamicRequires: true, - ignoreGlobal: true, - ignoreTryCatch: true, - strictRequires: true, - }), - babelPlugin({ - babelHelpers: 'runtime', - babelrc: false, - configFile: path.join(configPath, 'babel.config.js'), - extensions: ['.js', '.cjs', '.mjs'], - }), - ], - } + compact: true, + sourcemap: false, + inlineDynamicImports: true, + }, + // Override external to bundle dependencies for these modules + external(id) { + // Only externalize Node.js built-ins + return isBuiltin(id) + }, + }), + baseConfig({ + input: `${srcPath}/external/ink-table.mjs`, + output: { + file: path.join(path.relative(rootPath, distPath), 'external/ink-table.js'), + format: 'cjs', + exports: 'auto', + externalLiveBindings: false, + generatedCode: { + preset: 'es2015', + arrowFunctions: true, + constBindings: true, + objectShorthand: true + }, + compact: true, + sourcemap: false, + inlineDynamicImports: true, + }, + // Override external to bundle dependencies for these modules + external(id) { + // Only externalize Node.js built-ins + return isBuiltin(id) + }, + }), + baseConfig({ + input: `${srcPath}/external/yoga-layout.mjs`, + output: { + file: path.join(path.relative(rootPath, distPath), 'external/yoga-layout.js'), + format: 'cjs', + exports: 'auto', + externalLiveBindings: false, + generatedCode: { + preset: 'es2015', + arrowFunctions: true, + constBindings: true, + objectShorthand: true + }, + compact: true, + sourcemap: false, + inlineDynamicImports: true, + }, + // Override external to bundle dependencies for these modules + external(id) { + // Only externalize Node.js built-ins + return isBuiltin(id) + }, }), ] } diff --git a/.config/rollup.dist.optimized.config.mjs b/.config/rollup.dist.optimized.config.mjs new file mode 100644 index 000000000..7dbff10b9 --- /dev/null +++ b/.config/rollup.dist.optimized.config.mjs @@ -0,0 +1,57 @@ +/** @fileoverview Optimized Rollup configuration that splits React/Ink into a separate chunk */ + +import baseConfig from './rollup.dist.config.mjs' + +export default { + ...baseConfig, + + output: { + ...baseConfig.output, + // Enable code splitting for dynamic imports + preserveModules: false, + // Create separate chunks for better caching + manualChunks: (id) => { + // Put React and Ink in a separate chunk + if (id.includes('node_modules/react') || + id.includes('node_modules/react-dom') || + id.includes('node_modules/ink') || + id.includes('node_modules/ink-table') || + id.includes('node_modules/yoga-layout') || + id.includes('node_modules/@pppp606/ink-chart')) { + return 'vendor-react-ink' + } + + // Put Socket SDK in its own chunk + if (id.includes('@socketsecurity/sdk')) { + return 'vendor-socket-sdk' + } + + // Put other large dependencies in vendor chunk + if (id.includes('node_modules') && + !id.includes('@socketsecurity/registry')) { + return 'vendor' + } + }, + + // Optimize chunk loading + chunkFileNames: 'chunks/[name]-[hash].js', + + // Enable optimizations + compact: true, + generatedCode: { + constBindings: true, + objectShorthand: true, + }, + }, + + // Optimize tree-shaking + treeshake: { + ...baseConfig.treeshake, + // More aggressive tree-shaking for production + moduleSideEffects: false, + propertyReadSideEffects: false, + + // Remove unused React code + preset: 'recommended', + }, +} \ No newline at end of file diff --git a/.config/rollup.fast.config.mjs b/.config/rollup.fast.config.mjs new file mode 100644 index 000000000..576ca79b2 --- /dev/null +++ b/.config/rollup.fast.config.mjs @@ -0,0 +1,154 @@ +/** + * @fileoverview Ultra-fast Rollup configuration using SWC instead of Babel + */ + +import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' +import { builtinModules } from 'node:module' +import path from 'node:path' + +import commonjsPlugin from '@rollup/plugin-commonjs' +import jsonPlugin from '@rollup/plugin-json' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import replacePlugin from '@rollup/plugin-replace' +// import swc from '@rollup/plugin-swc' + +const rootPath = path.join(import.meta.dirname, '..') +const srcPath = path.join(rootPath, 'src') +const distPath = path.join(rootPath, 'dist') +const cachePath = path.join(rootPath, '.cache', 'rollup') + +// Generate build hash for cache busting +function getBuildHash() { + const packageJson = readFileSync(path.join(rootPath, 'package.json'), 'utf8') + return createHash('md5').update(packageJson).digest('hex').slice(0, 8) +} + +export default { + input: { + cli: `${srcPath}/cli.mts`, + 'npm-cli': `${srcPath}/npm-cli.mts`, + 'npx-cli': `${srcPath}/npx-cli.mts`, + 'pnpm-cli': `${srcPath}/pnpm-cli.mts`, + constants: `${srcPath}/constants.mts`, + 'shadow/npm/bin': `${srcPath}/shadow/npm/bin.mts`, + 'shadow/npm/inject': `${srcPath}/shadow/npm/inject.mts`, + 'shadow/npx/bin': `${srcPath}/shadow/npx/bin.mts`, + 'shadow/pnpm/bin': `${srcPath}/shadow/pnpm/bin.mts`, + 'external/ink-table': `${srcPath}/external/ink-table.mjs`, + 'external/yoga-layout': `${srcPath}/external/yoga-layout.mjs`, + }, + + output: { + dir: distPath, + format: 'cjs', + exports: 'auto', + compact: true, + minifyInternalExports: true, + generatedCode: { + preset: 'es2015', + arrowFunctions: true, + constBindings: true, + objectShorthand: true + }, + experimentalMinChunkSize: 20000, + }, + + cache: { + dir: cachePath, + // Use build hash to invalidate cache when dependencies change + key: getBuildHash() + }, + + // Maximum parallelization + maxParallelFileOps: 10, + + treeshake: { + moduleSideEffects: false, + propertyReadSideEffects: false, + preset: 'recommended' + }, + + external(id) { + return ( + builtinModules.includes(id) || + builtinModules.includes(id.replace(/^node:/, '')) || + id.includes('@socketsecurity/registry') || + id.includes('@socketsecurity/sdk') + ) + }, + + plugins: [ + nodeResolve({ + exportConditions: ['node'], + extensions: ['.mjs', '.mts', '.js', '.ts', '.tsx', '.json'], + preferBuiltins: true, + // Use cache + moduleDirectories: ['node_modules'], + }), + + // SWC - MUCH faster than Babel! + // TODO: Enable when @rollup/plugin-swc is installed + // swc({ + // swc: { + // jsc: { + // parser: { + // syntax: 'typescript', + // tsx: true, + // decorators: false, + // dynamicImport: true, + // }, + // target: 'es2018', + // transform: { + // react: { + // runtime: 'automatic', + // development: false, + // }, + // }, + // // Minification handled by SWC + // minify: { + // compress: { + // drop_console: false, + // drop_debugger: true, + // dead_code: true, + // unused: true, + // }, + // mangle: false, + // }, + // }, + // // We'll handle this separately + // minify: false, + // }, + // include: /\.(m?[jt]sx?)$/, + // exclude: /node_modules/, + // }), + + jsonPlugin(), + + commonjsPlugin({ + defaultIsModuleExports: true, + // Speed optimizations + requireReturnsDefault: 'auto', + esmExternals: true, + // Cache transformation results + transformMixedEsModules: true, + }), + + replacePlugin({ + preventAssignment: true, + 'process.env.NODE_ENV': JSON.stringify('production'), + }), + ], + + onwarn(warning, warn) { + // Suppress known warnings for speed + if ( + warning.code === 'CIRCULAR_DEPENDENCY' || + warning.code === 'INVALID_ANNOTATION' || + warning.code === 'THIS_IS_UNDEFINED' + ) { + return + } + warn(warning) + }, +} \ No newline at end of file diff --git a/.config/rollup.sea.config.mjs b/.config/rollup.sea.config.mjs index b22fe8e32..d18311633 100644 --- a/.config/rollup.sea.config.mjs +++ b/.config/rollup.sea.config.mjs @@ -1,6 +1,6 @@ /** - * Rollup configuration for building SEA bootstrap thin wrapper. - * Compiles TypeScript bootstrap to CommonJS for Node.js SEA compatibility. + * Rollup configuration for building SEA stub thin wrapper. + * Compiles TypeScript stub to CommonJS for Node.js SEA compatibility. */ import path from 'node:path' @@ -9,21 +9,32 @@ import url from 'node:url' import { babel as babelPlugin } from '@rollup/plugin-babel' import commonjsPlugin from '@rollup/plugin-commonjs' import { nodeResolve } from '@rollup/plugin-node-resolve' +import replacePlugin from '@rollup/plugin-replace' +import semver from 'semver' +import UnpluginOxc from 'unplugin-oxc/rollup' + +import maintainedNodeVersions from '@socketsecurity/registry/lib/constants/maintained-node-versions' const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) const rootDir = path.join(__dirname, '..') +const isProduction = + process.env.NODE_ENV === 'production' || process.env.MINIFY === '1' + +// Get the major version of the current maintained Node.js version (22.x.x -> 22) +const MIN_NODE_VERSION = semver.major(maintainedNodeVersions[2]) export default { input: - process.env.SEA_BOOTSTRAP || path.join(rootDir, 'src/sea/bootstrap.mts'), + process.env.SEA_STUB || path.join(rootDir, 'src/sea/stub.mts'), output: { file: - process.env.SEA_OUTPUT || path.join(rootDir, 'dist/sea/bootstrap.cjs'), + process.env.SEA_OUTPUT || path.join(rootDir, 'dist/sea/stub.cjs'), format: 'cjs', interop: 'auto', }, external: [ // Only externalize Node.js built-ins for the thin wrapper. + // nanotar package will be inlined (not external). /^node:/, ], plugins: [ @@ -31,6 +42,14 @@ export default { preferBuiltins: true, exportConditions: ['node'], }), + // Inline process.env.MIN_NODE_VERSION at build time from maintainedNodeVersions + replacePlugin({ + preventAssignment: true, + values: { + 'process.env.MIN_NODE_VERSION': JSON.stringify(MIN_NODE_VERSION), + "process.env['MIN_NODE_VERSION']": JSON.stringify(MIN_NODE_VERSION), + }, + }), babelPlugin({ babelHelpers: 'runtime', babelrc: false, @@ -38,5 +57,13 @@ export default { extensions: ['.mjs', '.js', '.ts', '.mts'], }), commonjsPlugin(), - ], + // Minify in production builds using oxc (faster than terser) + isProduction && + UnpluginOxc({ + minify: { + compress: {}, + mangle: true, + }, + }), + ].filter(Boolean), } diff --git a/taze.config.mts b/.config/taze.config.mts similarity index 100% rename from taze.config.mts rename to .config/taze.config.mts diff --git a/.config/tsconfig.base.json b/.config/tsconfig.base.json index ae0573e79..c58372b11 100644 --- a/.config/tsconfig.base.json +++ b/.config/tsconfig.base.json @@ -3,17 +3,18 @@ // The following options are not supported by @typescript/native-preview. // They are either ignored or throw an unknown option error: //"importsNotUsedAsValues": "remove", - //"incremental": true, "allowImportingTsExtensions": false, "allowJs": false, - "composite": true, + "composite": false, "declaration": true, "declarationMap": true, "erasableSyntaxOnly": true, "esModuleInterop": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, + "incremental": false, "isolatedModules": true, + "jsx": "react-jsx", "lib": ["esnext"], "module": "nodenext", "noEmit": true, diff --git a/.config/tsconfig.check.json b/.config/tsconfig.check.json new file mode 100644 index 000000000..465b71e1d --- /dev/null +++ b/.config/tsconfig.check.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "typeRoots": ["../node_modules/@types"] + }, + "include": ["../src/**/*.mts", "../*.config.mts", "./*.mts"], + "exclude": [ + "../**/*.tsx", + "../**/*.d.mts", + "../src/commands/analytics/output-analytics.mts", + "../src/commands/audit-log/output-audit-log.mts", + "../src/commands/threat-feed/output-threat-feed.mts", + "../src/**/*.test.mts", + "../src/test/**/*.mts", + "../src/utils/test-mocks.mts", + "../test/**/*.mts" + ] +} \ No newline at end of file diff --git a/.config/vitest.config.mts b/.config/vitest.config.mts new file mode 100644 index 000000000..32ad20115 --- /dev/null +++ b/.config/vitest.config.mts @@ -0,0 +1,75 @@ +import { defineConfig } from 'vitest/config' + +// Check if coverage is enabled via CLI flags or environment. +const isCoverageEnabled = + process.env['COVERAGE'] === 'true' || + process.env['npm_lifecycle_event']?.includes('coverage') || + process.argv.some(arg => arg.includes('coverage')) + +export default defineConfig({ + resolve: { + preserveSymlinks: false, + }, + test: { + globals: false, + environment: 'node', + setupFiles: ['./test/vitest-setup.mts'], + include: [ + 'test/**/*.test.{js,ts,mjs,cjs,mts}', + 'src/**/*.test.{js,ts,mjs,cjs,mts}', + ], + reporters: ['default'], + // Improve memory usage by running tests sequentially in CI. + pool: 'forks', + poolOptions: { + forks: { + // Use single fork for coverage to reduce memory, parallel otherwise. + singleFork: isCoverageEnabled, + ...(isCoverageEnabled ? { maxForks: 1 } : {}), + // Isolate tests to prevent memory leaks between test files. + isolate: true, + }, + threads: { + // Use single thread for coverage to reduce memory, parallel otherwise. + singleThread: isCoverageEnabled, + ...(isCoverageEnabled ? { maxThreads: 1 } : {}), + }, + }, + testTimeout: 60_000, + hookTimeout: 60_000, + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html', 'lcov', 'clover'], + exclude: [ + '**/*.config.*', + '**/node_modules/**', + '**/[.]**', + '**/*.d.mts', + '**/*.d.ts', + '**/virtual:*', + 'bin/**', + 'coverage/**', + 'dist/**', + 'pnpmfile.*', + 'scripts/**', + 'src/**/types.mts', + 'test/**', + 'perf/**', + // Explicit root-level exclusions + '/scripts/**', + '/test/**', + ], + include: ['src/**/*.mts', 'src/**/*.ts'], + all: true, + clean: true, + skipFull: false, + ignoreClassMethods: ['constructor'], + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, + }, + }, +}) diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 1597c187e..000000000 --- a/.editorconfig +++ /dev/null @@ -1,10 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 2 -indent_style = space -insert_final_newline = true -max_line_length = 80 -trim_trailing_whitespace = true diff --git a/.env.dist b/.env.dist index 71bb97822..65e6340f4 100644 --- a/.env.dist +++ b/.env.dist @@ -1,2 +1,3 @@ LINT_DIST=1 NODE_COMPILE_CACHE="./.cache" +NODE_OPTIONS="--max-old-space-size=4096 --max-semi-space-size=512" diff --git a/.env.local b/.env.local deleted file mode 100644 index c775cb167..000000000 --- a/.env.local +++ /dev/null @@ -1 +0,0 @@ -NODE_COMPILE_CACHE="./.cache" diff --git a/.env.test b/.env.test index e00209738..caf63610f 100644 --- a/.env.test +++ b/.env.test @@ -1,2 +1,6 @@ +DEBUG=0 NODE_COMPILE_CACHE="./.cache" +NODE_OPTIONS="--max-old-space-size=2048" +NODE_ENV=test +SOCKET_CLI_DEBUG=0 VITEST=1 diff --git a/.env.testu b/.env.testu deleted file mode 100644 index f3a357e03..000000000 --- a/.env.testu +++ /dev/null @@ -1,3 +0,0 @@ -NODE_COMPILE_CACHE="./.cache" -SOCKET_CLI_NO_API_TOKEN=1 -VITEST=1 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e218639c1..6840dc6d1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,8 +5,12 @@ updates: schedule: interval: 'weekly' day: 'monday' + cooldown: + default-days: 7 - package-ecosystem: 'npm' directory: '/' schedule: interval: 'weekly' day: 'monday' + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..7071cd1df --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: 🚀 CI Pipeline + +# Dependencies: +# - SocketDev/socket-registry/.github/workflows/ci.yml + +on: + push: + branches: [main] + tags: ['*'] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + ci: + name: Run CI Pipeline + uses: SocketDev/socket-registry/.github/workflows/ci.yml@17c8e09407f67149512e95e082a9c77dfe8e27a4 # main + with: + coverage-script: 'pnpm run cover' + coverage-report-script: 'pnpm run cover --percent' + fail-fast: false + lint-script: 'pnpm run lint-ci' + node-versions: '[20, 22, 24]' + os-versions: '["ubuntu-latest", "windows-latest"]' + test-script: 'pnpm run test-ci' + test-setup-script: 'pnpm run build' + type-check-script: 'pnpm run type-ci' + type-check-setup-script: 'pnpm run build' diff --git a/.github/workflows/claude-auto-review.yml b/.github/workflows/claude-auto-review.yml index 3c9208efe..eb7199175 100644 --- a/.github/workflows/claude-auto-review.yml +++ b/.github/workflows/claude-auto-review.yml @@ -12,6 +12,6 @@ permissions: jobs: auto-review: - uses: SocketDev/socket-registry/.github/workflows/claude-auto-review.yml@main + uses: SocketDev/socket-registry/.github/workflows/claude-auto-review.yml@17c8e09407f67149512e95e082a9c77dfe8e27a4 # main secrets: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index ebec7a215..5e50b1765 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,6 +19,6 @@ permissions: jobs: claude: - uses: SocketDev/socket-registry/.github/workflows/claude.yml@main + uses: SocketDev/socket-registry/.github/workflows/claude.yml@17c8e09407f67149512e95e082a9c77dfe8e27a4 # main secrets: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0b9175738..a87de87af 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,11 +1,6 @@ name: Linting on: - push: - branches: [main] - tags: ['*'] - pull_request: - branches: [main] workflow_dispatch: permissions: @@ -13,4 +8,4 @@ permissions: jobs: lint-check: - uses: SocketDev/socket-registry/.github/workflows/lint.yml@main + uses: SocketDev/socket-registry/.github/workflows/lint.yml@17c8e09407f67149512e95e082a9c77dfe8e27a4 # main diff --git a/.github/workflows/provenance.yml b/.github/workflows/provenance.yml index 6a9605f39..0dc6807ba 100644 --- a/.github/workflows/provenance.yml +++ b/.github/workflows/provenance.yml @@ -11,6 +11,7 @@ on: options: - '0' - '1' + jobs: build: runs-on: ubuntu-latest @@ -21,29 +22,53 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: SocketDev/socket-registry/.github/actions/setup@main + - uses: SocketDev/socket-registry/.github/actions/setup@797e90f4f82ac089a308acdc434d2027c2cd7d5d with: scope: '@socketsecurity' - run: pnpm install - - run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 pnpm run build:dist - - run: pnpm publish --provenance --access public --no-git-checks + + - name: Ensure npm version 11.5.1+ for trusted publishing + run: | + NPM_VERSION=$(npm --version) + echo "Current npm version: $NPM_VERSION" + # Check if npm version is >= 11.5.1 + if ! npx --yes semver "$NPM_VERSION" -r ">=11.5.1"; then + echo "Installing npm 11.5.1+ for trusted publishing..." + npm install -g npm@latest + echo "Updated npm version: $(npm --version)" + else + echo "npm version $NPM_VERSION meets the 11.5.1+ requirement for trusted publishing" + fi + + # Build and publish 'socket' package (default). + - name: Build socket package + run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 pnpm run build:dist + env: + SOCKET_CLI_DEBUG: ${{ inputs.debug }} + - name: Publish socket package + run: cd dist && npm publish --access public --no-git-checks continue-on-error: true env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} SOCKET_CLI_DEBUG: ${{ inputs.debug }} - - run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 INLINED_SOCKET_CLI_LEGACY_BUILD=1 pnpm run build:dist + + # Build and publish '@socketsecurity/cli' package (legacy). + - name: Build @socketsecurity/cli package + run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 INLINED_SOCKET_CLI_LEGACY_BUILD=1 pnpm run build:dist env: SOCKET_CLI_DEBUG: ${{ inputs.debug }} - - run: pnpm publish --provenance --access public --no-git-checks + - name: Publish @socketsecurity/cli package + run: cd dist && npm publish --access public --no-git-checks continue-on-error: true env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} SOCKET_CLI_DEBUG: ${{ inputs.debug }} - - run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 INLINED_SOCKET_CLI_SENTRY_BUILD=1 pnpm run build:dist + + # Build and publish '@socketsecurity/cli-with-sentry' package. + - name: Build @socketsecurity/cli-with-sentry package + run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 INLINED_SOCKET_CLI_SENTRY_BUILD=1 pnpm run build:dist env: SOCKET_CLI_DEBUG: ${{ inputs.debug }} - - run: pnpm publish --provenance --access public --no-git-checks + - name: Publish @socketsecurity/cli-with-sentry package + run: cd dist && npm publish --access public --no-git-checks continue-on-error: true env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} SOCKET_CLI_DEBUG: ${{ inputs.debug }} diff --git a/.github/workflows/publish-socketbin.yml b/.github/workflows/publish-socketbin.yml new file mode 100644 index 000000000..160d21162 --- /dev/null +++ b/.github/workflows/publish-socketbin.yml @@ -0,0 +1,309 @@ +name: Publish @socketbin Packages + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to publish (without v prefix)' + required: true + type: string + dry-run: + description: 'Dry run (build but do not publish)' + required: false + type: boolean + default: false + +jobs: + build-binaries: + name: Build ${{ matrix.platform }}-${{ matrix.arch }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # Linux builds + - os: ubuntu-latest + platform: linux + arch: x64 + - os: ubuntu-latest + platform: linux + arch: arm64 + + # macOS builds + - os: macos-latest + platform: darwin + arch: x64 + - os: macos-latest + platform: darwin + arch: arm64 + + # Windows builds + - os: windows-latest + platform: win32 + arch: x64 + - os: windows-latest + platform: win32 + arch: arm64 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: 'https://registry.npmjs.org' + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build stub + run: pnpm run build:sea:stub + + - name: Setup UPX (Linux/Windows) + if: matrix.platform != 'darwin' + uses: crazy-max/ghaction-upx@v3 + with: + install-only: true + + - name: Build binary + run: | + pnpm run build:sea -- \ + --platform=${{ matrix.platform }} \ + --arch=${{ matrix.arch }} + + - name: Compress binary with UPX (Linux/Windows) + if: matrix.platform != 'darwin' + run: | + # Find the binary file (different extensions for different platforms) + BINARY_FILE=$(find dist/sea -name "socket-*" -type f | head -1) + if [ -f "$BINARY_FILE" ]; then + echo "Compressing $BINARY_FILE with UPX..." + upx --best --lzma "$BINARY_FILE" || echo "UPX compression failed, continuing anyway" + ls -lh "$BINARY_FILE" + fi + + - name: Sign binary (macOS) + if: matrix.platform == 'darwin' + run: | + # Sign the macOS binary with ad-hoc signature for distribution + BINARY_FILE=$(find dist/sea -name "socket-*" -type f | head -1) + if [ -f "$BINARY_FILE" ]; then + echo "Signing macOS binary: $BINARY_FILE" + codesign --sign - --force "$BINARY_FILE" + codesign -dv "$BINARY_FILE" + fi + + - name: Verify binary + run: | + ls -la dist/sea/socket-* + file dist/sea/socket-* || true + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: binary-${{ matrix.platform }}-${{ matrix.arch }} + path: dist/sea/socket-* + retention-days: 1 + + publish-packages: + name: Publish to npm + needs: build-binaries + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # For provenance + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: 'https://registry.npmjs.org' + + - name: Determine version + id: version + run: | + VERSION="${{ inputs.version }}" + # Remove 'v' prefix if present + VERSION="${VERSION#v}" + echo "version=${VERSION}" >> $GITHUB_OUTPUT + + - name: Check version consistency + run: | + echo "🔍 Checking version consistency for v${{ steps.version.outputs.version }}..." + node scripts/check-version-consistency.mjs ${{ steps.version.outputs.version }} + + - name: Download all binaries + uses: actions/download-artifact@v4 + with: + path: dist/sea + pattern: binary-* + + - name: Organize binaries + run: | + # Flatten directory structure from artifacts + for dir in dist/sea/binary-*; do + if [ -d "$dir" ]; then + mv "$dir"/* dist/sea/ + rmdir "$dir" + fi + done + + # List all binaries + echo "Downloaded binaries:" + ls -la dist/sea/ + + - name: Generate and publish Linux x64 + if: ${{ !inputs.dry-run }} + run: | + node scripts/generate-binary-package.mjs \ + --platform=linux --arch=x64 \ + --version=${{ steps.version.outputs.version }} + + cd packages/binaries/cli-linux-x64 + npm publish --provenance --access public + + - name: Generate and publish Linux ARM64 + if: ${{ !inputs.dry-run }} + run: | + node scripts/generate-binary-package.mjs \ + --platform=linux --arch=arm64 \ + --version=${{ steps.version.outputs.version }} + + cd packages/binaries/cli-linux-arm64 + npm publish --provenance --access public + + - name: Generate and publish macOS x64 + if: ${{ !inputs.dry-run }} + run: | + node scripts/generate-binary-package.mjs \ + --platform=darwin --arch=x64 \ + --version=${{ steps.version.outputs.version }} + + cd packages/binaries/cli-darwin-x64 + npm publish --provenance --access public + + - name: Generate and publish macOS ARM64 + if: ${{ !inputs.dry-run }} + run: | + node scripts/generate-binary-package.mjs \ + --platform=darwin --arch=arm64 \ + --version=${{ steps.version.outputs.version }} + + cd packages/binaries/cli-darwin-arm64 + npm publish --provenance --access public + + - name: Generate and publish Windows x64 + if: ${{ !inputs.dry-run }} + run: | + node scripts/generate-binary-package.mjs \ + --platform=win32 --arch=x64 \ + --version=${{ steps.version.outputs.version }} + + cd packages/binaries/cli-win32-x64 + npm publish --provenance --access public + + - name: Generate and publish Windows ARM64 + if: ${{ !inputs.dry-run }} + run: | + node scripts/generate-binary-package.mjs \ + --platform=win32 --arch=arm64 \ + --version=${{ steps.version.outputs.version }} + + cd packages/binaries/cli-win32-arm64 + npm publish --provenance --access public + + - name: Dry run summary + if: ${{ inputs.dry-run }} + run: | + echo "🚫 Dry run mode - packages were NOT published" + echo "" + echo "Generated packages:" + find packages/binaries -name package.json -exec echo {} \; -exec jq -r '.name + "@" + .version' {} \; + + publish-main: + name: Publish main socket package + needs: publish-packages + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: 'https://registry.npmjs.org' + + - name: Determine version + id: version + run: | + VERSION="${{ inputs.version }}" + # Remove 'v' prefix if present + VERSION="${VERSION#v}" + echo "version=${VERSION}" >> $GITHUB_OUTPUT + + - name: Update package.json for @socketbin + run: | + cd src/sea/npm-package + + # Update version + npm version ${{ steps.version.outputs.version }} --no-git-tag-version + + # Update package.json to use optionalDependencies + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + + // Remove old postinstall script + delete pkg.scripts?.postinstall; + + // Add dispatcher script to bin + pkg.bin = { socket: 'bin/socket.js' }; + + // Add optionalDependencies + pkg.optionalDependencies = { + '@socketbin/cli-darwin-arm64': '${{ steps.version.outputs.version }}', + '@socketbin/cli-darwin-x64': '${{ steps.version.outputs.version }}', + '@socketbin/cli-linux-arm64': '${{ steps.version.outputs.version }}', + '@socketbin/cli-linux-x64': '${{ steps.version.outputs.version }}', + '@socketbin/cli-win32-arm64': '${{ steps.version.outputs.version }}', + '@socketbin/cli-win32-x64': '${{ steps.version.outputs.version }}' + }; + + // Update files list + pkg.files = ['bin', 'README.md']; + + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); + " + + # Show the updated package.json + cat package.json + + - name: Publish main package + if: ${{ !inputs.dry-run }} + working-directory: src/sea/npm-package + run: npm publish --provenance --access public + + - name: Dry run summary + if: ${{ inputs.dry-run }} + run: | + echo "🚫 Dry run mode - main package was NOT published" + echo "" + echo "Would have published:" + cd src/sea/npm-package + echo "socket@$(jq -r .version package.json)" \ No newline at end of file diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml new file mode 100644 index 000000000..a59713e02 --- /dev/null +++ b/.github/workflows/release-binaries.yml @@ -0,0 +1,317 @@ +name: Build and Release Binaries + +on: + workflow_dispatch: + inputs: + build_mode: + description: 'Build mode' + required: true + type: choice + options: + - yao-pkg + - node-sea + - both + default: yao-pkg + version: + description: 'Version to release (leave empty to use package.json version)' + required: false + type: string + node_version: + description: 'Node.js version for yao-pkg (leave empty for auto-detect)' + required: false + type: string + release: + types: [created] + +env: + # Auto-detect latest supported Node version if not specified + NODE_VERSION: ${{ github.event.inputs.node_version || '' }} + +jobs: + detect-node-version: + runs-on: ubuntu-latest + outputs: + node_version: ${{ steps.detect.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + scripts/build/build-binary.mjs + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Detect latest yao-pkg Node version + id: detect + run: | + if [ -n "${{ env.NODE_VERSION }}" ]; then + echo "version=${{ env.NODE_VERSION }}" >> $GITHUB_OUTPUT + else + # Fetch latest supported version from yao-pkg + VERSION=$(node -e " + fetch('https://api.github.com/repos/yao-pkg/pkg-fetch/contents/patches') + .then(r => r.json()) + .then(data => { + const versions = data + .filter(f => f.name.startsWith('node.v')) + .map(f => f.name.match(/node\.v(\d+\.\d+\.\d+)/)?.[1]) + .filter(Boolean) + .sort((a, b) => { + const [aMajor, aMinor, aPatch] = a.split('.').map(Number); + const [bMajor, bMinor, bPatch] = b.split('.').map(Number); + if (aMajor !== bMajor) return bMajor - aMajor; + if (aMinor !== bMinor) return bMinor - aMinor; + return bPatch - aPatch; + }); + const v24 = versions.find(v => v.startsWith('24.')); + const v22 = versions.find(v => v.startsWith('22.')); + console.log(v24 || v22 || '24.9.0'); + }) + .catch(() => console.log('24.9.0')); + ") + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "Detected Node version: ${VERSION}" + fi + + build-yao-pkg: + needs: detect-node-version + if: github.event.inputs.build_mode == 'yao-pkg' || github.event.inputs.build_mode == 'both' || github.event_name == 'release' + strategy: + fail-fast: false + matrix: + include: + # Linux builds + - os: ubuntu-latest + platform: linux + arch: x64 + - os: ubuntu-latest + platform: linux + arch: arm64 + + # macOS builds + - os: macos-latest + platform: darwin + arch: x64 + - os: macos-latest + platform: darwin + arch: arm64 + + # Windows builds + - os: windows-latest + platform: win32 + arch: x64 + - os: windows-latest + platform: win32 + arch: arm64 + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - run: pnpm install --frozen-lockfile + + - name: Build distribution + run: pnpm run build + + - name: Build binary with yao-pkg + run: | + node scripts/build/build-binary.mjs \ + --mode=yao-pkg \ + --platform=${{ matrix.platform }} \ + --arch=${{ matrix.arch }} \ + --node-version=${{ needs.detect-node-version.outputs.node_version }} \ + --output=dist/binaries/socket-${{ matrix.platform }}-${{ matrix.arch }}${{ matrix.platform == 'win32' && '.exe' || '' }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: yao-pkg-${{ matrix.platform }}-${{ matrix.arch }} + path: dist/binaries/socket-* + retention-days: 7 + + build-node-sea: + if: github.event.inputs.build_mode == 'node-sea' || github.event.inputs.build_mode == 'both' + strategy: + fail-fast: false + matrix: + include: + # Node SEA requires Node 20.12+ or 22+ + - os: ubuntu-latest + platform: linux + arch: x64 + node: '22' + - os: ubuntu-latest + platform: linux + arch: arm64 + node: '22' + + - os: macos-latest + platform: darwin + arch: x64 + node: '22' + - os: macos-latest + platform: darwin + arch: arm64 + node: '22' + + - os: windows-latest + platform: win32 + arch: x64 + node: '22' + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - run: pnpm install --frozen-lockfile + + - name: Build distribution + run: pnpm run build + + - name: Install postject + run: npm install -g postject + + - name: Build SEA binary + run: | + node scripts/build/build-binary.mjs \ + --mode=node-sea \ + --platform=${{ matrix.platform }} \ + --arch=${{ matrix.arch }} \ + --output=dist/binaries/socket-sea-${{ matrix.platform }}-${{ matrix.arch }}${{ matrix.platform == 'win32' && '.exe' || '' }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: node-sea-${{ matrix.platform }}-${{ matrix.arch }} + path: dist/binaries/socket-sea-* + retention-days: 7 + + upload-release: + needs: [build-yao-pkg, build-node-sea] + # Only run if at least one build job succeeded + if: always() && (needs.build-yao-pkg.result == 'success' || needs.build-node-sea.result == 'success') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist/binaries + pattern: '*-*-*' + + - name: Flatten directory structure + run: | + cd dist/binaries + find . -type f -name "socket-*" -exec mv {} . \; + find . -type d -empty -delete + ls -la + + - name: Get version + id: version + run: | + if [ -n "${{ github.event.inputs.version }}" ]; then + echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + elif [ "${{ github.event_name }}" = "release" ]; then + echo "version=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT + else + VERSION=$(node -p "require('./package.json').version") + echo "version=v${VERSION}" >> $GITHUB_OUTPUT + fi + + - name: Create checksums + run: | + cd dist/binaries + sha256sum socket-* > checksums.txt + cat checksums.txt + + - name: Create or update release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.version.outputs.version }}" + + # Check if release exists + if gh release view "${VERSION}" > /dev/null 2>&1; then + echo "Release ${VERSION} exists, uploading binaries..." + else + echo "Creating release ${VERSION}..." + gh release create "${VERSION}" \ + --title "Socket CLI ${VERSION}" \ + --notes "Socket CLI ${VERSION} + +## Binaries + +This release includes binaries built with: +- **yao-pkg**: Maximum compatibility, Node ${{ needs.detect-node-version.outputs.node_version || '24.9.0' }} +- **node-sea**: Native Node.js Single Executable Applications (experimental) + +### Supported Platforms +- Linux (x64, arm64) +- macOS (x64, arm64) +- Windows (x64, arm64) + +### Installation + +Download the appropriate binary for your platform and make it executable: + +\`\`\`bash +# Linux/macOS +chmod +x socket-linux-x64 +./socket-linux-x64 --version + +# Windows +socket-win-x64.exe --version +\`\`\` + +### Checksums + +See \`checksums.txt\` for SHA-256 checksums of all binaries. + +### Changelog + +See [CHANGELOG.md](https://github.com/SocketDev/socket-cli/blob/main/CHANGELOG.md) for details. +" \ + --draft + fi + + # Upload binaries and checksums + cd dist/binaries + for file in socket-* checksums.txt; do + if [ -f "$file" ]; then + echo "Uploading $file..." + gh release upload "${VERSION}" "$file" --clobber + fi + done + + - name: Summary + run: | + echo "## Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "**Build Mode:** ${{ github.event.inputs.build_mode || 'yao-pkg' }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Binaries Built" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + ls -lh dist/binaries/socket-* | awk '{print $NF, $5}' >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/release-sea.yml b/.github/workflows/release-sea.yml new file mode 100644 index 000000000..96e2a6c0a --- /dev/null +++ b/.github/workflows/release-sea.yml @@ -0,0 +1,152 @@ +name: Build and Release SEA Binaries + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (leave empty to use package.json version)' + required: false + type: string + release: + types: [created] + +jobs: + build-sea: + strategy: + fail-fast: false # Continue building other platforms if one fails + matrix: + include: + # Linux builds - fully supported + - os: ubuntu-latest + platform: linux + arch: x64 + - os: ubuntu-latest + platform: linux + arch: arm64 # Cross-compilation, generally stable + # macOS builds - fully supported (native compilation) + - os: macos-latest + platform: darwin + arch: x64 + - os: macos-latest + platform: darwin + arch: arm64 + # Windows builds - x64 is stable + - os: windows-latest + platform: win32 + arch: x64 + # Windows ARM64 - native ARM64 runner (GitHub Actions 2025) + - os: windows-2022-arm64 + platform: win32 + arch: arm64 + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - uses: SocketDev/socket-registry/.github/actions/setup@17c8e09407f67149512e95e082a9c77dfe8e27a4 # main + with: + scope: '@socketsecurity' + + - run: pnpm install + + - name: Build SEA binary + run: pnpm run build:sea -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }} + + - name: Upload artifact + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + with: + name: socket-${{ matrix.platform }}-${{ matrix.arch }} + path: | + dist/sea/socket-* + retention-days: 7 + + upload-release: + needs: build-sea + runs-on: ubuntu-latest + if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' + + permissions: + contents: write + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Download all artifacts + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 + with: + path: dist/sea + + - name: Flatten directory structure + run: | + cd dist/sea + find . -name "socket-*" -type f -exec mv {} . \; + find . -type d -empty -delete + ls -la + + - name: Get version + id: version + run: | + if [ -n "${{ github.event.inputs.version }}" ]; then + echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + elif [ "${{ github.event_name }}" = "release" ]; then + echo "version=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT + else + VERSION=$(node -p "require('./src/sea/npm-package/package.json').version") + echo "version=v${VERSION}" >> $GITHUB_OUTPUT + fi + + - name: Upload binaries to release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.version.outputs.version }}" + + # Create release if it doesn't exist + if ! gh release view "$VERSION" > /dev/null 2>&1; then + gh release create "$VERSION" \ + --title "$VERSION" \ + --notes "Socket CLI $VERSION - See [CHANGELOG.md](https://github.com/SocketDev/socket-cli/blob/main/CHANGELOG.md) for details." \ + --draft + fi + + # Upload binaries (skip if none exist) + if ls dist/sea/socket-* 1> /dev/null 2>&1; then + for file in dist/sea/socket-*; do + echo "Uploading $file..." + gh release upload "$VERSION" "$file" --clobber + done + else + echo "⚠️ No binaries found to upload" + echo "This may be expected if some platform builds failed" + echo "See docs/SEA_PLATFORM_SUPPORT.md for supported platforms" + fi + + publish-npm: + needs: upload-release + runs-on: ubuntu-latest + if: github.event_name == 'release' + + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - uses: SocketDev/socket-registry/.github/actions/setup@17c8e09407f67149512e95e082a9c77dfe8e27a4 # main + with: + scope: '@socketsecurity' + + - name: Update npm package version + run: | + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" # Remove 'v' prefix if present + cd src/sea/npm-package + npm version "$VERSION" --no-git-tag-version + + - name: Publish to npm + working-directory: src/sea/npm-package + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/socket-auto-pr.yml b/.github/workflows/socket-auto-pr.yml index ed775a058..31e63ff01 100644 --- a/.github/workflows/socket-auto-pr.yml +++ b/.github/workflows/socket-auto-pr.yml @@ -21,7 +21,7 @@ permissions: jobs: socket-auto-pr: - uses: SocketDev/socket-registry/.github/workflows/socket-auto-pr.yml@main + uses: SocketDev/socket-registry/.github/workflows/socket-auto-pr.yml@17c8e09407f67149512e95e082a9c77dfe8e27a4 # main with: debug: ${{ inputs.debug }} autopilot: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb17467b8..bd227d2ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,11 +1,6 @@ name: Tests on: - push: - branches: [main] - tags: ['*'] - pull_request: - branches: [main] workflow_dispatch: permissions: @@ -13,4 +8,4 @@ permissions: jobs: test: - uses: SocketDev/socket-registry/.github/workflows/test.yml@main + uses: SocketDev/socket-registry/.github/workflows/test.yml@17c8e09407f67149512e95e082a9c77dfe8e27a4 # main diff --git a/.github/workflows/types.yml b/.github/workflows/types.yml index 1f1646df0..653ad3538 100644 --- a/.github/workflows/types.yml +++ b/.github/workflows/types.yml @@ -1,11 +1,6 @@ name: Type Checks on: - push: - branches: [main] - tags: ['*'] - pull_request: - branches: [main] workflow_dispatch: permissions: @@ -13,4 +8,4 @@ permissions: jobs: type-check: - uses: SocketDev/socket-registry/.github/workflows/types.yml@main + uses: SocketDev/socket-registry/.github/workflows/types.yml@17c8e09407f67149512e95e082a9c77dfe8e27a4 # main diff --git a/.gitignore b/.gitignore index a3d4c8c32..4f8672a71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,13 @@ .DS_Store ._.DS_Store Thumbs.db +/.claude /.env /.nvm /.rollup.cache /.type-coverage /.vscode /coverage -/external /npm-debug.log **/.cache **/dist @@ -16,4 +16,15 @@ Thumbs.db *.d.ts.map *.tsbuildinfo +# Yarn PnP files (tests may create these) +/.yarn +/.pnp.cjs +/.pnp.loader.mjs +/yarn.lock + !/.vscode/extensions.json + +# Build outputs (keep build/patches in version control) +/build/output +/build/socket-node +/.build diff --git a/.husky/pre-commit b/.husky/pre-commit index 67a066ea0..bce1d9a1b 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,11 +1,11 @@ if [ -z "${DISABLE_PRECOMMIT_LINT}" ]; then - pnpm run lint-staged + pnpm run precommit else echo "Skipping lint due to DISABLE_PRECOMMIT_LINT env var" fi if [ -z "${DISABLE_PRECOMMIT_TEST}" ]; then - pnpm run test-pre-commit + dotenvx -q run -f .env.precommit -- pnpm test --staged else echo "Skipping testing due to DISABLE_PRECOMMIT_TEST env var" fi diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..a45fd52cc --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/.oxlintignore b/.oxlintignore deleted file mode 100644 index d8b83df9c..000000000 --- a/.oxlintignore +++ /dev/null @@ -1 +0,0 @@ -package-lock.json diff --git a/.oxlintrc.json b/.oxlintrc.json deleted file mode 100644 index 09a399951..000000000 --- a/.oxlintrc.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["import", "promise", "typescript", "unicorn"], - "categories": { - "correctness": "warn", - "perf": "warn", - "suspicious": "warn" - }, - "settings": {}, - "rules": { - "@typescript-eslint/array-type": ["error", { "default": "array-simple" }], - "@typescript-eslint/no-misused-new": "error", - "@typescript-eslint/no-this-alias": [ - "error", - { "allowDestructuring": true } - ], - "@typescript-eslint/return-await": ["error", "always"], - "curly": "error", - "no-control-regex": "off", - "no-new": "off", - "no-self-assign": "off", - "no-undef": "off", - "no-unused-vars": "off", - "no-var": "error", - "unicorn/no-empty-file": "off", - "unicorn/no-new-array": "off", - "unicorn/prefer-string-starts-ends-with": "off" - } -} diff --git a/.pnpmrc b/.pnpmrc index 66cedf68c..f687c3bb7 100644 --- a/.pnpmrc +++ b/.pnpmrc @@ -1,14 +1,14 @@ -# Delayed dependency updates - wait 7 days (10080 minutes) before allowing new packages. -minimumReleaseAge=10080 +# Security: Allowlist for native binaries +only-built-dependencies[]=@appthreat/sqlite3 +only-built-dependencies[]=esbuild +only-built-dependencies[]=oxc-resolver +only-built-dependencies[]=unrs-resolver -# Auto-install peers. -auto-install-peers=true +# Enable pre/post scripts for the main project (e.g., prepare -> husky) +enable-pre-post-scripts=true -# Strict peer dependencies. +# Dependency management +minimumReleaseAge=10080 +auto-install-peers=true strict-peer-dependencies=false - -# Use node-linker to ensure better compatibility. -node-linker=hoisted - -# Save exact versions (like npm --save-exact). save-exact=true \ No newline at end of file diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 000000000..af0e972a8 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,2 @@ +nodeLinker: node-modules +enableGlobalCache: false diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 000000000..7cbaa89f4 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,172 @@ +# Socket CLI Architecture + +## Overview + +The Socket CLI is a TypeScript-based command-line tool for security analysis and package management. It's designed with modularity and maintainability in mind, using a clear separation of concerns. + +## Directory Structure + +``` +socket-cli/ +├── src/ # Source code +│ ├── cli.mts # Main CLI entry point +│ ├── commands.mts # Command registry +│ ├── constants.mts # Shared constants +│ ├── types.mts # Type definitions +│ ├── commands/ # Command implementations +│ ├── utils/ # Shared utilities +│ ├── shadow/ # Package manager shadows +│ ├── sea/ # Single Executable Application +│ └── external/ # External dependencies +├── bin/ # Binary entry points +├── dist/ # Compiled output +├── scripts/ # Build and utility scripts +├── test/ # Test files +└── .config/ # Configuration files +``` + +## Core Concepts + +### 1. Command Pattern +Each command follows a consistent pattern: +- `cmd-*.mts` - CLI interface and flag parsing +- `handle-*.mts` - Business logic implementation +- `output-*.mts` - Output formatting +- `fetch-*.mts` - API data fetching + +### 2. Shadow Binaries +The CLI can act as a shadow for npm, npx, and pnpm, intercepting and enhancing their functionality: +- Located in `src/shadow/` +- Provides security scanning during package operations +- Transparent to the end user + +### 3. Utils Organization +Utilities are grouped by function to reduce cognitive load: +- **API & Network** - API client and HTTP utilities +- **Error Handling** - Centralized error types and handlers +- **Output & Formatting** - Consistent output formatting +- **File System** - File operations and path handling +- **Package Management** - Package manager detection and operations + +### 4. Constants Management +Constants are imported from `@socketsecurity/registry` and extended with CLI-specific values: +- Shared constants from registry +- CLI-specific constants +- Environment variables +- Configuration keys + +## Key Design Patterns + +### Result/Either Pattern +Used throughout for error handling: +```typescript +type CResult = + | { ok: true; data: T; message?: string } + | { ok: false; message: string; code?: number } +``` + +### Lazy Loading +Heavy dependencies are loaded only when needed to improve startup time. + +### Configuration Cascade +Configuration is resolved in order: +1. Command-line flags +2. Environment variables +3. Local config file (.socketrc) +4. Global config file +5. Default values + +## Build System + +### Rollup Configuration +- Main build uses Rollup for tree-shaking and optimization +- Separate configs for different build targets +- JSON files are inlined during build + +### TypeScript Compilation +- Uses `.mts` extensions for ES modules +- Compiled with `tsgo` for better performance +- Type definitions generated separately + +## Testing Strategy + +### Unit Tests +- Located alongside source files as `*.test.mts` +- Focus on individual function behavior +- Run with Vitest + +### Integration Tests +- Located in `test/integration/` +- Test command flows end-to-end +- Mock external API calls + +## Performance Considerations + +### Startup Time +- Minimal dependencies loaded at startup +- Commands lazy-loaded on demand +- Constants use lazy getters + +### Memory Usage +- Configurable memory limits via flags +- Streaming for large data sets +- Efficient caching strategies + +## Security + +### API Token Management +- Tokens never logged or displayed +- Stored securely in system keychain when possible +- Validated before use + +### Package Scanning +- Real-time scanning during installs +- Comprehensive vulnerability database +- Configurable risk policies + +## Extension Points + +### Custom Commands +Commands can be added by: +1. Creating files in `src/commands/` +2. Following the command pattern +3. Registering in `src/commands.mts` + +### Plugins +Future support planned for plugins via: +- Standard plugin interface +- Dynamic loading +- Isolated execution context + +## Best Practices + +1. **Keep files focused** - Single responsibility per file +2. **Use TypeScript strictly** - No `any` types without good reason +3. **Document exports** - JSDoc for all public APIs +4. **Test thoroughly** - Aim for high coverage +5. **Handle errors gracefully** - Use Result pattern +6. **Optimize imports** - Import only what's needed +7. **Follow patterns** - Consistency reduces cognitive load + +## Common Tasks + +### Adding a New Command +1. Create `src/commands/[name]/cmd-[name].mts` +2. Implement command logic in `handle-[name].mts` +3. Add output formatting in `output-[name].mts` +4. Register in `src/commands.mts` +5. Add tests + +### Adding a Utility +1. Choose appropriate category in `src/utils/` +2. Create focused utility file +3. Export from category index if applicable +4. Document with JSDoc +5. Add unit tests + +### Updating Constants +1. Check if constant exists in `@socketsecurity/registry` +2. If shared, add to registry +3. If CLI-specific, add to `src/constants.mts` +4. Use descriptive names +5. Group related constants \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dc6decca..dd2c27d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [1.1.24](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.24) - 2025-10-05 + +### Added +- Optional API response caching to speed up repeated queries +- `cacheEnabled` configuration option for enabling response caching (default: false) +- `cacheTtl` configuration option for customizing cache duration in milliseconds (default: 300000 = 5 minutes) +- `SOCKET_CLI_CACHE_ENABLED` environment variable for opt-in caching +- `SOCKET_CLI_CACHE_TTL` environment variable for configuring cache TTL + ## [1.1.23](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.23) - 2025-09-22 ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 73ef2bc90..512892659 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,286 +1,115 @@ # CLAUDE.md -🚨 **CRITICAL**: This file contains MANDATORY guidelines for Claude Code (claude.ai/code). You MUST follow these guidelines EXACTLY as specified. Act as a principal-level software engineer with deep expertise in TypeScript, Node.js, and CLI development. +🚨 **MANDATORY**: Act as principal-level engineer with deep expertise in TypeScript, Node.js, and CLI development. -## 🎯 Your Role -You are a **Principal Software Engineer** responsible for: -- Writing production-quality, maintainable code -- Making architectural decisions with long-term impact in mind -- Ensuring code follows established patterns and conventions -- Mentoring through code examples and best practices -- Prioritizing system reliability, performance, and developer experience -- Taking ownership of technical decisions and their consequences +## 📚 SHARED STANDARDS -## Commands +**See canonical reference:** `../socket-registry/CLAUDE.md` -### Development Commands -- **Build**: `npm run build` (alias for `npm run build:dist`) -- **Build source**: `npm run build:dist:src` or `pnpm build:dist:src` -- **Build types**: `npm run build:dist:types` -- **Test**: `npm run test` (runs check + all tests) -- **Test unit only**: `npm run test:unit` or `pnpm test:unit` -- **Lint**: `npm run check:lint` (uses eslint) -- **Type check**: `npm run check:tsc` (uses tsgo) -- **Check all**: `npm run check` (lint + typecheck) -- **Fix linting**: `npm run lint:fix` -- **Commit without tests**: `git commit --no-verify` (skips pre-commit hooks including tests) +For all shared Socket standards (git workflow, testing, code style, imports, sorting, error handling, cross-platform, CI, etc.), refer to socket-registry/CLAUDE.md. -### Testing Best Practices - CRITICAL: NO -- FOR FILE PATHS -- **🚨 NEVER USE `--` BEFORE TEST FILE PATHS** - This runs ALL tests, not just your specified files! -- **Always build before testing**: Run `pnpm build:dist:src` before running tests to ensure dist files are up to date -- **Test single file**: ✅ CORRECT: `pnpm test:unit src/commands/specific/cmd-file.test.mts` - - ❌ WRONG: `pnpm test:unit -- src/commands/specific/cmd-file.test.mts` (runs ALL tests!) -- **Test multiple files**: ✅ CORRECT: `pnpm test:unit file1.test.mts file2.test.mts` -- **Test with pattern**: ✅ CORRECT: `pnpm test:unit src/commands/specific/cmd-file.test.mts -t "pattern"` - - ❌ WRONG: `pnpm test:unit -- src/commands/specific/cmd-file.test.mts -t "pattern"` -- **Update snapshots**: - - All tests: `pnpm testu` (builds first, then updates all snapshots) - - Single file: ✅ CORRECT: `pnpm testu src/commands/specific/cmd-file.test.mts` - - ❌ WRONG: `pnpm testu -- src/commands/specific/cmd-file.test.mts` (updates ALL snapshots!) -- **Update with --update flag**: `pnpm test:unit src/commands/specific/cmd-file.test.mts --update` -- **Timeout for long tests**: Use `timeout` command or specify in test file +**Git Workflow Reminder**: When user says "commit changes" → create actual commits, use small atomic commits, follow all CLAUDE.md rules (NO AI attribution). -### Git Commit Guidelines -- **🚨 FORBIDDEN**: NEVER add Claude co-authorship or Claude signatures to commits -- **🚨 FORBIDDEN**: Do NOT include "Generated with Claude Code" or similar AI attribution in commit messages -- **Commit messages**: Should be written as if by a human developer, focusing on the what and why of changes -- **Professional commits**: Write clear, concise commit messages that describe the actual changes made - -### Running the CLI locally -- **Build and run**: `npm run build && npm exec socket` or `pnpm build && pnpm exec socket` -- **Quick build + run**: `npm run bs` or `pnpm bs` (builds source only, then runs socket) -- **Run without build**: `npm run s` or `pnpm s` (runs socket directly) -- **Native TypeScript**: `./sd` (runs the CLI without building using Node.js native TypeScript support on Node 22+) - -### Package Management -- **Package Manager**: This project uses pnpm (v10.16.0+) -- **Install dependencies**: `pnpm install` -- **Add dependency**: `pnpm add ` -- **Add dev dependency**: `pnpm add -D ` -- **Update dependencies**: `pnpm update` -- **Override behavior**: pnpm.overrides in package.json controls dependency versions across the entire project -- **Using $ syntax**: `"$package-name"` in overrides means "use the version specified in dependencies" +--- -## Architecture +## 🏗️ CLI-SPECIFIC -This is a CLI tool for Socket.dev security analysis, built with TypeScript using .mts extensions. +### Architecture +CLI tool for Socket.dev security analysis - TypeScript with .mts extensions -### Core Structure -- **Entry point**: `src/cli.mts` - Main CLI entry with meow subcommands +**Core Structure**: +- **Entry**: `src/cli.mts` - Main CLI with meow subcommands - **Commands**: `src/commands.mts` - Exports all command definitions -- **Command modules**: `src/commands/*/` - Each feature has its own directory with cmd-*, handle-*, and output-* files -- **Utilities**: `src/utils/` - Shared utilities for API, config, formatting, etc. -- **Constants**: `src/constants.mts` - Application constants -- **Types**: `src/types.mts` - TypeScript type definitions +- **Modules**: `src/commands/*/` - Each feature: `cmd-*`, `handle-*`, `output-*`, `fetch-*` +- **Utils**: `src/utils/` - Shared utilities +- **Constants**: `src/constants.mts` +- **Types**: `src/types.mts` -### Command Architecture Pattern -Each command follows a consistent pattern: -- `cmd-*.mts` - Command definition and CLI interface -- `handle-*.mts` - Business logic and processing -- `output-*.mts` - Output formatting (JSON, markdown, etc.) -- `fetch-*.mts` - API calls (where applicable) +**Command Pattern**: `cmd-*.mts` (CLI interface), `handle-*.mts` (business logic), `output-*.mts` (formatting), `fetch-*.mts` (API calls) -### Key Command Categories -- **npm/npx wrapping**: `socket npm`, `socket npx` - Wraps npm/npx with security scanning -- **Scanning**: `socket scan` - Create and manage security scans -- **Organization management**: `socket organization` - Manage org settings and policies -- **Package analysis**: `socket package` - Analyze package scores -- **Optimization**: `socket optimize` - Apply Socket registry overrides -- **Configuration**: `socket config` - Manage CLI configuration +**Categories**: npm/npx wrapping, scanning, org management, package analysis, optimization, configuration + +**Shadow Binaries**: `shadow-bin/npm`, `shadow-bin/npx` - Wrappers for `socket npm`, `socket npx` ### Build System -- Uses Rollup for building distribution files -- TypeScript compilation with tsgo -- Multiple environment configs (.env.local, .env.test, .env.dist) -- Dual linting with oxlint and eslint -- Formatting with Biome +- Rollup for distribution +- TypeScript with tsgo (preferred) or tsc +- Individual file compilation +- Env configs: `.env.local`, `.env.test`, `.env.dist` + +### Commands +- **Build**: `pnpm run build` (alias for `build:dist`) +- **Build source**: `pnpm run build:dist:src` +- **Build types**: `pnpm run build:dist:types` +- **Test**: `pnpm run test`, `pnpm run test:unit` +- **Lint**: `pnpm run check:lint` +- **Type check**: `pnpm run check:tsc` (uses tsgo) +- **Check all**: `pnpm run check` + +**Run locally**: +- `pnpm run build && pnpm exec socket` +- `pnpm run bs` (builds source, runs socket) +- `pnpm run s` (runs socket directly) +- `./sd` (native TS on Node 22+) + +### CLI-Specific Patterns + +#### Command Structure +🚨 MANDATORY - Each command: `cmd-*.mts`, `handle-*.mts`, `output-*.mts` + +#### File Structure +- **Extensions**: `.mts` +- **Naming**: kebab-case (`cmd-scan-create.mts`, `handle-create-new-scan.mts`) +- **Module headers**: 🚨 MANDATORY `@fileoverview` headers + +#### CLI Patterns +- **Flags**: Use `MeowFlags` with descriptive help +- **GitHub API**: Use Octokit from `src/utils/github.mts`, not raw fetch +- **Null-prototype**: `{ __proto__: null, key: val }` or `Object.create(null)` for empty + +#### Error Handling +- **Input validation**: Use `InputError` from `src/utils/errors.mts` +- **Authentication**: Use `AuthError` from `src/utils/errors.mts` +- **Result pattern**: Use `CResult` for fallible functions +- **Messages**: NO periods at end (see canonical socket-registry/CLAUDE.md) +- Examples: + - ✅ `throw new InputError('No .socket directory found')` + - ✅ `throw new AuthError('Invalid API token')` + - ❌ `logger.error('Error occurred'); return` ### Testing -- Vitest for unit testing -- Test files use `.test.mts` extension -- Fixtures in `test/fixtures/` -- Coverage reporting available - -### External Dependencies -- Bundles external dependencies in `external/` directory -- Uses Socket registry overrides for security -- Custom patches applied to dependencies in `patches/` - -## Environment and Configuration - -### Environment Files -- **`.env.local`** - Local development environment -- **`.env.test`** - Test environment configuration -- **`.env.testu`** - Test update environment -- **`.env.dist`** - Distribution build environment -- **`.env.external`** - External dependencies environment - -### Configuration Files -- **`biome.json`** - Biome formatter and linter configuration -- **`vitest.config.mts`** - Vitest test runner configuration -- **`eslint.config.js`** - ESLint configuration -- **`tsconfig.json`** - Main TypeScript configuration -- **`tsconfig.dts.json`** - TypeScript configuration for type definitions -- **`knip.json`** - Knip unused code detection configuration - -### Shadow Binaries -- **`shadow-bin/`** - Contains wrapper scripts for npm/npx commands - - `shadow-bin/npm` - Wraps npm with Socket security scanning - - `shadow-bin/npx` - Wraps npx with Socket security scanning - - These enable `socket npm` and `socket npx` functionality +- **🚨 NEVER USE `--` before test paths** - runs ALL tests +- **Build first**: `pnpm run build:dist:src` +- **Test single file**: ✅ `pnpm run test:unit src/commands/specific/cmd-file.test.mts` +- **Update snapshots**: ✅ `pnpm run testu src/commands/specific/cmd-file.test.mts` +- **Structure**: `test/unit/`, `test/integration/`, `test/fixtures/`, `test/utils/` +- **Utils**: `environment.mts`, `fixtures.mts`, `mock-helpers.mts`, `constants.mts` + +### CI Testing +- **🚨 MANDATORY**: `SocketDev/socket-registry/.github/workflows/ci.yml@` with full SHA +- **Format**: `@662bbcab1b7533e24ba8e3446cffd8a7e5f7617e # main` +- **Docs**: `docs/CI_TESTING.md`, `socket-registry/docs/CI_TESTING_TOOLS.md` ### Package Structure -- **Binary entries**: `socket`, `socket-npm`, `socket-npx` (in `bin/` directory) -- **Distribution**: Built files go to `dist/` directory -- **External dependencies**: Bundled in `external/` directory -- **Test fixtures**: Located in `test/fixtures/` - -### Dependency Management -- Uses Socket registry overrides for enhanced alternatives -- Custom patches applied to dependencies via `custompatch` -- Overrides specified in package.json for enhanced alternatives - -## Changelog Management - -When updating the changelog (`CHANGELOG.md`): -- Version headers should be formatted as markdown links to GitHub releases -- Use the format: `## [version](https://github.com/SocketDev/socket-cli/releases/tag/vversion) - date` -- Example: `## [1.0.80](https://github.com/SocketDev/socket-cli/releases/tag/v1.0.80) - 2025-07-29` -- This allows users to click version numbers to view the corresponding GitHub release - -### Keep a Changelog Compliance -Follow the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format: -- Use standard sections: Added, Changed, Fixed, Removed (Security if applicable) -- Maintain chronological order with latest version first -- Include release dates in YYYY-MM-DD format -- Make entries human-readable, not machine diffs -- Focus on notable changes that impact users - -**Exclude** internal changes like: -- Dependency updates (unless they fix security issues or add user features) -- Code refactoring and cleanup -- Internal constant reorganization -- Test snapshot updates -- Build system improvements -- Developer tooling changes -- Minor nits and formatting tweaks -- GitHub workflow and CI/CD changes -- Third-party integration updates (unless they add user-visible features) - - -### Content Guidelines -Focus on **user-facing changes** only. Include: -- **Added**: New features, commands, flags, or capabilities users can access -- **Changed**: Modifications to existing behavior that users will notice -- **Fixed**: Bug fixes that resolve user-reported issues or improve functionality -- **Removed**: Features, flags, or commands that are no longer available - -### Writing Style -Use a **marketing voice** that emphasizes user benefits while staying **concise**: -- Focus on what users can accomplish rather than technical implementation -- Highlight improvements in user experience and productivity -- Use active, positive language that showcases value -- Keep entries brief - users need to find information quickly -- Example: Instead of "Added flag X", write "Enhanced security scanning with new X option" - -### Third-Party Integrations - -Socket CLI integrates with various third-party tools and services: -- **@coana-tech/cli**: Static analysis tool for reachability analysis and vulnerability detection -- **cdxgen**: CycloneDX BOM generator for creating software bill of materials -- **synp**: Tool for converting between yarn.lock and package-lock.json formats - -## 🔧 Code Style (MANDATORY) - -### 📁 File Organization -- **File extensions**: Use `.mts` for TypeScript module files -- **Import order**: Node.js built-ins first, then third-party packages, then local imports -- **Import grouping**: Group imports by source (Node.js, external packages, local modules) -- **Type imports**: 🚨 ALWAYS use separate `import type` statements for TypeScript types, NEVER mix runtime imports with type imports in the same statement - - ✅ CORRECT: `import { readPackageJson } from '@socketsecurity/registry/lib/packages'` followed by `import type { PackageJson } from '@socketsecurity/registry/lib/packages'` - - ❌ FORBIDDEN: `import { readPackageJson, type PackageJson } from '@socketsecurity/registry/lib/packages'` - -### Naming Conventions -- **Constants**: Use `UPPER_SNAKE_CASE` for constants (e.g., `CMD_NAME`, `REPORT_LEVEL`) -- **Files**: Use kebab-case for filenames (e.g., `cmd-scan-create.mts`, `handle-create-new-scan.mts`) -- **Variables**: Use camelCase for variables and functions - -### 🏗️ Code Structure (CRITICAL PATTERNS) -- **Command pattern**: 🚨 MANDATORY - Each command MUST have `cmd-*.mts`, `handle-*.mts`, and `output-*.mts` files -- **Type definitions**: 🚨 ALWAYS use `import type` for better tree-shaking -- **Flags**: 🚨 MUST use `MeowFlags` type with descriptive help text -- **Error handling**: 🚨 REQUIRED - Use custom error types `AuthError` and `InputError` -- **Array destructuring**: Use object notation `{ 0: key, 1: data }` instead of array destructuring `[key, data]` -- **Dynamic imports**: 🚨 FORBIDDEN - Never use dynamic imports (`await import()`). Always use static imports at the top of the file -- **Sorting**: 🚨 MANDATORY - Always sort lists, exports, and items in documentation headers alphabetically/alphanumerically for consistency -- **Comment periods**: 🚨 MANDATORY - ALL comments MUST end with periods. This includes single-line comments, multi-line comments, and inline comments. No exceptions -- **Comment placement**: Place comments on their own line, not to the right of code -- **Comment formatting**: Use fewer hyphens/dashes and prefer commas, colons, or semicolons for better readability -- **Await in loops**: When using `await` inside for-loops, add `// eslint-disable-next-line no-await-in-loop` to suppress the ESLint warning when sequential processing is intentional -- **If statement returns**: Never use single-line return if statements; always use proper block syntax with braces -- **List formatting**: Use `-` for bullet points in text output, not `•` or other Unicode characters, for better terminal compatibility -- **Existence checks**: Perform simple existence checks first before complex operations -- **Destructuring order**: Sort destructured properties alphabetically in const declarations -- **Function ordering**: Place functions in alphabetical order, with private functions first, then exported functions -- **GitHub API calls**: Use Octokit instances from `src/utils/github.mts` (`getOctokit()`, `getOctokitGraphql()`) instead of raw fetch calls for GitHub API interactions -- **Object mappings**: Use objects with `__proto__: null` (not `undefined`) for static string-to-string mappings and lookup tables to prevent prototype pollution; use `Map` for dynamic collections that will be mutated -- **Mapping constants**: Move static mapping objects outside functions as module-level constants with descriptive UPPER_SNAKE_CASE names -- **Array length checks**: Use `!array.length` instead of `array.length === 0`. For `array.length > 0`, use `!!array.length` when function must return boolean, or `array.length` when used in conditional contexts -- **Catch parameter naming**: Use `catch (e)` instead of `catch (error)` for consistency across the codebase -- **Node.js fs imports**: 🚨 MANDATORY pattern - `import { someSyncThing, promises as fs } from 'node:fs'` -- **Process spawning**: 🚨 FORBIDDEN to use Node.js built-in `child_process.spawn` - MUST use `spawn` from `@socketsecurity/registry/lib/spawn` -- **Number formatting**: 🚨 REQUIRED - Use underscore separators (e.g., `20_000`) for large numeric literals. 🚨 FORBIDDEN - Do NOT modify number values inside strings - -### Error Handling -- **Input validation errors**: Use `InputError` from `src/utils/errors.mts` for user input validation failures (missing files, invalid arguments, etc.) -- **Authentication errors**: Use `AuthError` from `src/utils/errors.mts` for API authentication issues -- **CResult pattern**: Use `CResult` type for functions that can fail, following the Result/Either pattern with `ok: true/false` -- **Process exit**: Avoid `process.exit(1)` unless absolutely necessary; prefer throwing appropriate error types that the CLI framework handles -- **Error messages**: Write clear, actionable error messages that help users understand what went wrong and how to fix it -- **Examples**: - - ✅ `throw new InputError('No .socket directory found in current directory')` - - ✅ `throw new AuthError('Invalid API token')` - - ❌ `logger.error('Error occurred'); return` (doesn't set proper exit code) - - ❌ `process.exit(1)` (bypasses error handling framework) - -### 🗑️ Safe File Operations (SECURITY CRITICAL) -- **File deletion**: 🚨 ABSOLUTELY FORBIDDEN - NEVER use `rm -rf`. 🚨 MANDATORY - ALWAYS use `pnpm dlx trash-cli` -- **Examples**: - - ❌ CATASTROPHIC: `rm -rf directory` (permanent deletion - DATA LOSS RISK) - - ❌ REPOSITORY DESTROYER: `rm -rf "$(pwd)"` (deletes entire repository) - - ✅ SAFE: `pnpm dlx trash-cli directory` (recoverable deletion) -- **Why this matters**: trash-cli enables recovery from accidental deletions via system trash/recycle bin - -### Debugging and Troubleshooting -- **CI vs Local Differences**: CI uses published npm packages, not local versions. Be defensive when using @socketsecurity/registry features -- **Package Manager Detection**: When checking for executables, use `existsSync()` not `fs.access()` for consistency - -### Formatting -- **Linting**: Uses ESLint with TypeScript support and import/export rules -- **Formatting**: Uses Biome for code formatting with 2-space indentation -- **Line length**: Target 80 character line width where practical - ---- - -# 🚨 CRITICAL BEHAVIORAL REQUIREMENTS - -## 🎯 Principal Engineer Mindset -- Act with the authority and expertise of a principal-level software engineer -- Make decisions that prioritize long-term maintainability over short-term convenience -- Anticipate edge cases and potential issues before they occur -- Write code that other senior engineers would be proud to review -- Take ownership of technical decisions and their consequences - -## 🛡️ ABSOLUTE RULES (NEVER BREAK THESE) -- 🚨 **NEVER** create files unless absolutely necessary for the goal -- 🚨 **ALWAYS** prefer editing existing files over creating new ones -- 🚨 **FORBIDDEN** to proactively create documentation files (*.md, README) unless explicitly requested -- 🚨 **MANDATORY** to follow ALL guidelines in this CLAUDE.md file without exception -- 🚨 **REQUIRED** to do exactly what was asked - nothing more, nothing less - -## 🎯 Quality Standards -- Code MUST pass all existing lints and type checks -- Changes MUST maintain backward compatibility unless explicitly breaking changes are requested -- All patterns MUST follow established codebase conventions -- Error handling MUST be robust and user-friendly -- Performance considerations MUST be evaluated for any changes +- **Binary entries**: `socket`, `socket-npm`, `socket-npx` (in `bin/`) +- **Distribution**: `dist/` directory +- **External deps**: Bundled in `dist/external/` +- **Test fixtures**: `test/fixtures/` +- **Custom patches**: `patches/` + +### Changelog Management +**Content**: Focus on user-facing changes only +- **Include**: New features/commands/flags, behavior changes, bug fixes, removed features +- **Exclude**: Deps (unless security/features), refactoring, internal constants, test snapshots, build system, dev tooling, CI/CD, third-party integration updates + +**Style**: Marketing voice emphasizing user benefits, concise +- Focus on what users can accomplish +- Highlight UX/productivity improvements +- Active, positive language +- Keep brief + +**Third-party integrations**: @coana-tech/cli, cdxgen, synp + +### Debugging +- **CI vs Local**: CI uses published packages, not local +- **Package detection**: Use `existsSync()` not `fs.access()` diff --git a/README.md b/README.md index b490963ff..77fcc840f 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,58 @@ # Socket CLI [![Socket Badge](https://socket.dev/api/badge/npm/package/socket)](https://socket.dev/npm/package/socket) +[![CI](https://github.com/SocketDev/socket-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/SocketDev/socket-cli/actions/workflows/ci.yml) + [![Follow @SocketSecurity](https://img.shields.io/twitter/follow/SocketSecurity?style=social)](https://twitter.com/SocketSecurity) +[![Follow @socket.dev on Bluesky](https://img.shields.io/badge/Follow-@socket.dev-1DA1F2?style=social&logo=bluesky)](https://bsky.app/profile/socket.dev) CLI for [Socket.dev] security analysis -## Usage +## Quick Start ```bash -npm install -g socket +pnpm install -g socket socket --help ``` -## Commands - -- `socket npm [args...]` and `socket npx [args...]` - Wraps npm/npx with Socket security scanning +## Core Commands +- `socket npm [args...]` / `socket npx [args...]` - Wrap npm/npx with security scanning +- `socket pnpm [args...]` / `socket yarn [args...]` - Wrap pnpm/yarn with security scanning +- `socket pip [args...]` - Wrap pip with security scanning +- `socket scan` - Create and manage security scans +- `socket package ` - Analyze package security scores - `socket fix` - Fix CVEs in dependencies - - `socket optimize` - Optimize dependencies with [`@socketregistry`](https://github.com/SocketDev/socket-registry) overrides - - `socket cdxgen [command]` - Run [cdxgen](https://cyclonedx.github.io/cdxgen/#/?id=getting-started) for SBOM generation +## Organization & Repository Management + +- `socket organization` (alias: `org`) - Manage organization settings +- `socket repository` (alias: `repo`) - Manage repositories +- `socket dependencies` (alias: `deps`) - View organization dependencies +- `socket audit-log` (alias: `audit`) - View audit logs +- `socket analytics` - View organization analytics +- `socket threat-feed` (alias: `feed`) - View threat intelligence + +## Authentication & Configuration + +- `socket login` - Authenticate with Socket.dev +- `socket logout` - Remove authentication +- `socket whoami` - Show authenticated user +- `socket config` - Manage CLI configuration + ## Aliases All aliases support the flags and arguments of the commands they alias. - `socket ci` - Alias for `socket scan create --report` (creates report and exits with error if unhealthy) +- `socket org` - Alias for `socket organization` +- `socket repo` - Alias for `socket repository` +- `socket pkg` - Alias for `socket package` +- `socket deps` - Alias for `socket dependencies` +- `socket audit` - Alias for `socket audit-log` +- `socket feed` - Alias for `socket threat-feed` ## Flags @@ -54,8 +80,8 @@ Supports version 2 format with `projectIgnorePaths` for excluding files from rep - `SOCKET_CLI_API_TOKEN` - Socket API token - `SOCKET_CLI_CONFIG` - JSON configuration object - `SOCKET_CLI_GITHUB_API_URL` - GitHub API base URL -- `SOCKET_CLI_GIT_USER_EMAIL` - Git user email (default: `github-actions[bot]@users.noreply.github.com`) -- `SOCKET_CLI_GIT_USER_NAME` - Git user name (default: `github-actions[bot]`) +- `SOCKET_CLI_GIT_USER_EMAIL` - Git user email (default: `94589996+socket-bot@users.noreply.github.com`) +- `SOCKET_CLI_GIT_USER_NAME` - Git user name (default: `Socket Bot`) - `SOCKET_CLI_GITHUB_TOKEN` - GitHub token with repo access (alias: `GITHUB_TOKEN`) - `SOCKET_CLI_NO_API_TOKEN` - Disable default API token - `SOCKET_CLI_NPM_PATH` - Path to npm directory @@ -67,10 +93,10 @@ Supports version 2 format with `projectIgnorePaths` for excluding files from rep Run locally: -``` -npm install -npm run build -npm exec socket +```bash +pnpm install +pnpm run build +pnpm exec socket ``` ### Development environment variables @@ -78,9 +104,190 @@ npm exec socket - `SOCKET_CLI_API_BASE_URL` - API base URL (default: `https://api.socket.dev/v0/`) - `SOCKET_CLI_API_PROXY` - Proxy for API requests (aliases: `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`, `http_proxy`) - `SOCKET_CLI_API_TIMEOUT` - API request timeout in milliseconds +- `SOCKET_CLI_CACHE_ENABLED` - Enable API response caching (default: `false`) +- `SOCKET_CLI_CACHE_TTL` - Cache TTL in milliseconds (default: `300000` = 5 minutes) - `SOCKET_CLI_DEBUG` - Enable debug logging - `DEBUG` - Enable [`debug`](https://socket.dev/npm/package/debug) package logging +### Debug logging categories + +The CLI supports granular debug logging via the `DEBUG` environment variable: + +**Default categories** (shown with `SOCKET_CLI_DEBUG=1`): +- `error` - Critical errors that prevent operation +- `warn` - Important warnings that may affect behavior +- `notice` - Notable events and state changes +- `silly` - Very verbose debugging info + +**Opt-in categories** (require explicit `DEBUG='category'`): +- `cache` - Cache hit/miss operations +- `network` - HTTP requests with timing +- `command` - External command execution +- `auth` - Authentication flow +- `perf` - Performance timing +- `spinner` - Spinner state changes +- `inspect` - Detailed object inspection +- `stdio` - Command execution logs + +**Examples:** +```bash +DEBUG=cache socket scan # Cache debugging only +DEBUG=network,cache socket scan # Multiple categories +DEBUG=* socket scan # All categories +SOCKET_CLI_DEBUG=1 socket scan # Default categories +``` + +## Developer API + +### Progress indicators + +Track long-running operations with visual progress bars: + +```typescript +import { startSpinner, updateSpinnerProgress } from './src/utils/spinner.mts' + +const stop = startSpinner('Processing files') +for (let i = 0; i < files.length; i++) { + updateSpinnerProgress(i + 1, files.length, 'files') + await processFile(files[i]) +} +stop() +// Output: ⠋ Processing files ████████████░░░░░░░░ 60% (12/20 files) +``` + +### Table formatting + +Display structured data with professional table formatting: + +```typescript +import { formatTable, formatSimpleTable } from './src/utils/output-formatting.mts' +import colors from 'yoctocolors-cjs' + +// Bordered table with box-drawing characters +const data = [ + { name: 'lodash', version: '4.17.21', issues: 0 }, + { name: 'react', version: '18.2.0', issues: 2 } +] +const columns = [ + { key: 'name', header: 'Package' }, + { key: 'version', header: 'Version', align: 'center' }, + { + key: 'issues', + header: 'Issues', + align: 'right', + color: (v) => v === '0' ? colors.green(v) : colors.red(v) + } +] +console.log(formatTable(data, columns)) +// Output: +// ┌─────────┬─────────┬────────┐ +// │ Package │ Version │ Issues │ +// ├─────────┼─────────┼────────┤ +// │ lodash │ 4.17.21 │ 0 │ +// │ react │ 18.2.0 │ 2 │ +// └─────────┴─────────┴────────┘ + +// Simple table without borders +console.log(formatSimpleTable(data, columns)) +// Output: +// Package Version Issues +// ─────── ─────── ────── +// lodash 4.17.21 0 +// react 18.2.0 2 +``` + +### Performance monitoring + +Track and optimize CLI performance with comprehensive monitoring utilities: + +```typescript +import { perfTimer, measure, perfCheckpoint, printPerformanceSummary } from './src/utils/performance.mts' + +// Simple operation timing +const stop = perfTimer('fetch-packages') +await fetchPackages() +stop({ count: 50 }) + +// Function measurement +const { result, duration } = await measure('parse-manifest', async () => { + return parseManifest(file) +}) +console.log(`Parsed in ${duration}ms`) + +// Track complex operation progress +perfCheckpoint('start-scan') +perfCheckpoint('analyze-dependencies', { count: 100 }) +perfCheckpoint('detect-issues', { issueCount: 5 }) +perfCheckpoint('end-scan') + +// Print performance summary +printPerformanceSummary() +// Performance Summary: +// fetch-packages: 1 calls, avg 234ms (min 234ms, max 234ms, total 234ms) +// parse-manifest: 5 calls, avg 12ms (min 8ms, max 20ms, total 60ms) +``` + +**Enable with:** `DEBUG=perf socket ` + +### Intelligent caching strategies + +Optimize API performance with smart caching based on data volatility: + +```typescript +import { getCacheStrategy, getRecommendedTtl, warmCaches } from './src/utils/cache-strategies.mts' + +// Get recommended TTL for an endpoint +const ttl = getRecommendedTtl('/npm/lodash/4.17.21/score') +// Returns: 900000 (15 minutes for stable package info) + +// Check cache strategy +const strategy = getCacheStrategy('/scans/abc123') +// Returns: { ttl: 120000, volatile: true } (2 minutes for active scans) + +// Warm critical caches on startup +await warmCaches(sdk, [ + '/users/me', + '/organizations/my-org/settings' +]) +``` + +**Built-in strategies:** +- Package info: 15min (stable data) +- Package issues: 5min (moderate volatility) +- Scan results: 2min (high volatility) +- Org settings: 30min (very stable) +- User info: 1hr (most stable) + +### Enhanced error handling + +Handle errors with actionable recovery suggestions: + +```typescript +import { InputError, AuthError, getRecoverySuggestions } from './src/utils/errors.mts' + +// Throw errors with recovery suggestions +throw new InputError('Invalid package name', 'Must be in format: @scope/name', [ + 'Use npm package naming conventions', + 'Check for typos in the package name' +]) + +throw new AuthError('Token expired', [ + 'Run `socket login` to re-authenticate', + 'Generate a new token at https://socket.dev/dashboard' +]) + +// Extract and display recovery suggestions +try { + await operation() +} catch (error) { + const suggestions = getRecoverySuggestions(error) + if (suggestions.length > 0) { + console.error('How to fix:') + suggestions.forEach(s => console.error(` - ${s}`)) + } +} +``` + ## See also - [Socket API Reference](https://docs.socket.dev/reference) diff --git a/bin/bootstrap.js b/bin/bootstrap.js new file mode 100644 index 000000000..cccd88ac9 --- /dev/null +++ b/bin/bootstrap.js @@ -0,0 +1,45 @@ +/** + * @fileoverview Bootstrap loader for Socket CLI + * + * Checks if ~/.socket/_socket exists, delegates to it. + * Otherwise downloads and installs Socket CLI there. + */ + +/* eslint-disable n/no-process-exit */ +// process.exit() is acceptable in CLI bootstrap scripts + +'use strict' + +const { spawnSync } = require('node:child_process') +const { existsSync } = require('node:fs') +const { homedir } = require('node:os') +const { join } = require('node:path') + +const SOCKET_CLI_DIR = join(homedir(), '.socket', '_socket') +const CLI_ENTRY = join(SOCKET_CLI_DIR, 'index.js') + +// Check if CLI exists +if (existsSync(CLI_ENTRY)) { + // Delegate to ~/.socket/_socket + const result = spawnSync( + process.execPath, + [CLI_ENTRY, ...process.argv.slice(2)], + { + stdio: 'inherit', + env: { + ...process.env, + PKG_EXECPATH: process.env.PKG_EXECPATH || 'PKG_INVOKE_NODEJS', + }, + }, + ) + process.exit(result.status || 0) +} else { + // Download and install + console.error('📦 Socket CLI not found, installing...') + console.error(` Directory: ${SOCKET_CLI_DIR}`) + console.error() + console.error('❌ Not implemented yet') + console.error(' TODO: Download tarball from npm registry') + console.error(' TODO: Extract to ~/.socket/_socket') + process.exit(1) +} diff --git a/bin/cli.js b/bin/cli.js index f2066d267..84e440321 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict' -void (async () => { +async function main() { const Module = require('node:module') const path = require('node:path') const rootPath = path.join(__dirname, '..') @@ -10,43 +10,65 @@ void (async () => { const { default: constants } = require( path.join(rootPath, 'dist/constants.js'), ) - const { spawn } = require( - path.join(rootPath, 'external/@socketsecurity/registry/lib/spawn.js'), - ) - process.exitCode = 1 - - const spawnPromise = spawn( - constants.execPath, - [ - ...constants.nodeNoWarningsFlags, - ...constants.nodeDebugFlags, - ...constants.nodeHardenFlags, - ...constants.nodeMemoryFlags, - ...(constants.ENV.INLINED_SOCKET_CLI_SENTRY_BUILD - ? ['--require', constants.instrumentWithSentryPath] - : []), - constants.distCliPath, - ...process.argv.slice(2), - ], - { - env: { - ...process.env, - ...constants.processEnv, + // Detect if running as pkg/yao-pkg binary + const isPkg = typeof process.pkg !== 'undefined' + + if (isPkg) { + // Running as pkg binary - directly execute CLI without spawning + process.exitCode = 1 + + // Set environment variables + Object.assign(process.env, constants.processEnv) + + // Directly require and execute the CLI + require(constants.distCliPath) + } else { + // Running as normal Node - use spawn with flags + const { spawn } = require( + path.join( + rootPath, + 'dist/external/@socketsecurity/registry/dist/lib/spawn.js', + ), + ) + + process.exitCode = 1 + + const spawnPromise = spawn( + constants.execPath, + [ + ...constants.nodeNoWarningsFlags, + ...constants.nodeDebugFlags, + ...constants.nodeHardenFlags, + ...constants.nodeMemoryFlags, + // Preload Sentry instrumentation in @socketsecurity/cli-with-sentry builds. + ...(constants.ENV.INLINED_SOCKET_CLI_SENTRY_BUILD + ? ['--require', constants.preloadSentryPath] + : []), + constants.distCliPath, + ...process.argv.slice(2), + ], + { + env: { + ...process.env, + ...constants.processEnv, + }, + stdio: 'inherit', }, - stdio: 'inherit', - }, - ) + ) + + // See https://nodejs.org/api/child_process.html#event-exit. + spawnPromise.process.on('exit', (code, signalName) => { + if (signalName) { + process.kill(process.pid, signalName) + } else if (typeof code === 'number') { + // eslint-disable-next-line n/no-process-exit + process.exit(code) + } + }) + + await spawnPromise + } +} - // See https://nodejs.org/api/child_process.html#event-exit. - spawnPromise.process.on('exit', (code, signalName) => { - if (signalName) { - process.kill(process.pid, signalName) - } else if (typeof code === 'number') { - // eslint-disable-next-line n/no-process-exit - process.exit(code) - } - }) - - await spawnPromise -})() +main().catch(console.error) diff --git a/bin/npm-cli.js b/bin/npm-cli.js index dcb56ec85..e4565ea44 100755 --- a/bin/npm-cli.js +++ b/bin/npm-cli.js @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict' -void (async () => { +async function main() { const Module = require('node:module') const path = require('node:path') const rootPath = path.join(__dirname, '..') @@ -26,4 +26,6 @@ void (async () => { }) await spawnPromise -})() +} + +main().catch(console.error) diff --git a/bin/npx-cli.js b/bin/npx-cli.js index 4fd74e224..9a7ab83da 100755 --- a/bin/npx-cli.js +++ b/bin/npx-cli.js @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict' -void (async () => { +async function main() { const Module = require('node:module') const path = require('node:path') const rootPath = path.join(__dirname, '..') @@ -26,4 +26,6 @@ void (async () => { }) await spawnPromise -})() +} + +main().catch(console.error) diff --git a/bin/pnpm-cli.js b/bin/pnpm-cli.js index ea93dde6e..3def44d3c 100755 --- a/bin/pnpm-cli.js +++ b/bin/pnpm-cli.js @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict' -void (async () => { +async function main() { const Module = require('node:module') const path = require('node:path') const rootPath = path.join(__dirname, '..') @@ -26,4 +26,6 @@ void (async () => { }) await spawnPromise -})() +} + +main().catch(console.error) diff --git a/bin/yarn-cli.js b/bin/yarn-cli.js index 300e68708..0baf2c368 100755 --- a/bin/yarn-cli.js +++ b/bin/yarn-cli.js @@ -1,29 +1,17 @@ #!/usr/bin/env node 'use strict' -void (async () => { +/** @fileoverview Yarn CLI wrapper entry point. Forwards to Socket Firewall (sfw) for security scanning. */ + +async function main() { const Module = require('node:module') const path = require('node:path') const rootPath = path.join(__dirname, '..') Module.enableCompileCache?.(path.join(rootPath, '.cache')) - const shadowYarnBin = require(path.join(rootPath, 'dist/shadow-yarn-bin.js')) - - process.exitCode = 1 - - const { spawnPromise } = await shadowYarnBin(process.argv.slice(2), { - stdio: 'inherit', - }) + require(path.join(rootPath, 'dist/yarn-cli.js')) - // See https://nodejs.org/api/child_process.html#event-exit. - spawnPromise.process.on('exit', (code, signalName) => { - if (signalName) { - process.kill(process.pid, signalName) - } else if (typeof code === 'number') { - // eslint-disable-next-line n/no-process-exit - process.exit(code) - } - }) + // The yarn-cli module handles exit codes internally +} - await spawnPromise -})() +main().catch(console.error) diff --git a/build/patches/README.md b/build/patches/README.md new file mode 100644 index 000000000..a3c51eba1 --- /dev/null +++ b/build/patches/README.md @@ -0,0 +1,71 @@ +# Stub Build Patches + +This directory contains patches used when building custom Node.js binaries for @yao-pkg/pkg packaging. + +## Structure + +``` +stub/ +├── patches/ +│ ├── yao/ # Patches from @yao-pkg/pkg (cached in version control) +│ └── socket/ # Socket custom patches +└── README.md +``` + +## Patches Organization + +### Yao Patches (`patches/yao/`) +- Downloaded from: https://github.com/yao-pkg/pkg-fetch/tree/main/patches +- Cached in version control for reproducible builds +- Contains official @yao-pkg/pkg compatibility patches +- Last synced: See `.sync-cache.json` + +### Socket Patches (`patches/socket/`) +- Custom Socket-specific modifications: + - `001-v8-flags-harmony-dynamic-import.patch` - V8 flag modifications + - `002-node-gyp-static-linking.patch` - Force static linking + - `003-fix-v8-include-paths-v24.patch` - Fix V8 include paths for v24 + +## Patch Management + +### Syncing Yao Patches +```bash +# Force sync from upstream (built into build-stub) +node scripts/build/build-stub.mjs --sync-yao-patches + +# Or use the standalone script +node scripts/build/fetch-yao-patches.mjs +``` + +## Building + +See the main build scripts in `scripts/build/`: +- `build-stub.mjs` - Build standalone executables +- `build-socket-node.mjs` - Build custom Node.js binaries +- `fetch-yao-patches.mjs` - Update yao patches from upstream + +## Creating Custom Patches + +To create a patch for Node.js modifications: + +1. Make your changes in the Node source directory (`build/socket-node/node-*`) +2. Generate a patch: + ```bash + cd build/socket-node/node-v24.9.0-custom + git diff > ../../scripts/build/stub/patches/socket/003-my-custom-change.patch + ``` + +## Patch Application Flow + +1. Node.js source is downloaded +2. Yao-pkg patches are applied from `patches/yao/` +3. Socket patches are applied from `patches/socket/` +4. Node.js is configured and built with flags from `.config/build-config.json5` + +## Configuration + +All build configuration is centralized in `.config/build-config.json5`: +- Node.js versions and their patches +- Configure flags for size optimization +- @yao-pkg/pkg settings +- Build paths and directories \ No newline at end of file diff --git a/build/patches/socket/enable-sea-for-pkg-binaries-v24.patch b/build/patches/socket/enable-sea-for-pkg-binaries-v24.patch new file mode 100644 index 000000000..c7d809fad --- /dev/null +++ b/build/patches/socket/enable-sea-for-pkg-binaries-v24.patch @@ -0,0 +1,17 @@ +# Patch: Make isSea() return true for pkg binaries +# +# Overrides the isSea binding to always return true, making pkg binaries +# report as Single Executable Applications for consistency. + +--- a/lib/sea.js ++++ b/lib/sea.js +@@ -16,7 +16,8 @@ const { + ERR_UNKNOWN_BUILTIN_MODULE, + } = require('internal/errors').codes; + +-const { isSea, getAsset: getAssetInternal, getAssetKeys: getAssetKeysInternal } = internalBinding('sea'); ++const isSea = () => true; ++const { getAsset: getAssetInternal, getAssetKeys: getAssetKeysInternal } = internalBinding('sea'); + + const { + setOwnProperty, \ No newline at end of file diff --git a/build/patches/socket/fix-v8-include-paths-v24.patch b/build/patches/socket/fix-v8-include-paths-v24.patch new file mode 100644 index 000000000..65eefff31 --- /dev/null +++ b/build/patches/socket/fix-v8-include-paths-v24.patch @@ -0,0 +1,68 @@ +# Fix V8 include paths for Node.js v24.9.0 +# +# Node.js v24.9.0 source has incorrect include paths in V8 code +# Files are looking for 'src/base/hashmap.h' when it should be 'base/hashmap.h' +# This patch removes the incorrect 'src/' prefix from V8 internal includes +# +# This issue causes build failures with errors like: +# fatal error: 'src/base/hashmap.h' file not found +# +# Author: Socket CLI +# Date: 2024-10-09 +# Node versions affected: v24.9.0 + +--- deps/v8/src/ast/ast-value-factory.h ++++ deps/v8/src/ast/ast-value-factory.h +@@ -30,7 +30,7 @@ + + #include + +-#include "src/base/hashmap.h" ++#include "base/hashmap.h" + #include "src/base/logging.h" + #include "src/common/globals.h" + #include "src/execution/isolate.h" +--- deps/v8/src/heap/new-spaces-inl.h ++++ deps/v8/src/heap/new-spaces-inl.h +@@ -12,7 +12,7 @@ + #include "src/heap/incremental-marking.h" + #include "src/heap/new-spaces.h" + #include "src/heap/paged-spaces-inl.h" +-#include "src/heap/spaces-inl.h" ++#include "heap/spaces-inl.h" + #include "src/objects/objects-inl.h" + #include "src/objects/tagged-impl.h" + +--- deps/v8/src/heap/factory-inl.h ++++ deps/v8/src/heap/factory-inl.h +@@ -14,7 +14,7 @@ + #include "src/handles/handles-inl.h" + #include "src/heap/factory-base-inl.h" + #include "src/heap/factory.h" +-#include "src/heap/factory-base-inl.h" ++#include "heap/factory-base-inl.h" + #include "src/heap/heap-inl.h" + #include "src/numbers/conversions.h" + #include "src/objects/feedback-cell.h" +--- deps/v8/src/objects/js-objects-inl.h ++++ deps/v8/src/objects/js-objects-inl.h +@@ -19,7 +19,7 @@ + #include "src/objects/feedback-cell-inl.h" + #include "src/objects/field-index-inl.h" + #include "src/objects/fixed-array-inl.h" +-#include "src/objects/hash-table-inl.h" ++#include "objects/hash-table-inl.h" + #include "src/objects/heap-number-inl.h" + #include "src/objects/js-array-inl.h" + #include "src/objects/js-objects.h" +--- deps/v8/src/heap/cppgc/heap-page.h ++++ deps/v8/src/heap/cppgc/heap-page.h +@@ -9,7 +9,7 @@ + #include + #include + +-#include "src/base/iterator.h" ++#include "base/iterator.h" + #include "src/base/macros.h" + #include "src/heap/cppgc/globals.h" + #include "src/heap/cppgc/heap-object-header.h" \ No newline at end of file diff --git a/build/patches/yao/.sync-cache.json b/build/patches/yao/.sync-cache.json new file mode 100644 index 000000000..c38dba1f2 --- /dev/null +++ b/build/patches/yao/.sync-cache.json @@ -0,0 +1,9 @@ +{ + "lastSync": "2025-10-09T12:32:10.950Z", + "versions": [ + "v24.9.0", + "v22.19.0", + "v20.19.5" + ], + "source": "https://github.com/yao-pkg/pkg-fetch/tree/main/patches" +} \ No newline at end of file diff --git a/build/patches/yao/node.v20.19.5.cpp.patch b/build/patches/yao/node.v20.19.5.cpp.patch new file mode 100644 index 000000000..dc174e973 --- /dev/null +++ b/build/patches/yao/node.v20.19.5.cpp.patch @@ -0,0 +1,602 @@ +diff --git node/common.gypi node/common.gypi +index 20e81dea95..3580f7f2e4 100644 +--- node/common.gypi ++++ node/common.gypi +@@ -187,7 +187,7 @@ + ['clang==1', { + 'lto': ' -flto ', # Clang + }, { +- 'lto': ' -flto=4 -fuse-linker-plugin -ffat-lto-objects ', # GCC ++ 'lto': ' -flto=4 -ffat-lto-objects ', # GCC + }], + ], + }, +diff --git node/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.c node/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.c +index 5e7775f1a5..eeebf67796 100644 +--- node/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.c ++++ node/deps/ngtcp2/nghttp3/lib/nghttp3_ringbuf.c +@@ -33,16 +33,6 @@ + + #include "nghttp3_macro.h" + +-#if defined(_MSC_VER) && !defined(__clang__) && (defined(_M_ARM) || defined(_M_ARM64)) +-unsigned int __popcnt(unsigned int x) { +- unsigned int c = 0; +- for (; x; ++c) { +- x &= x - 1; +- } +- return c; +-} +-#endif +- + int nghttp3_ringbuf_init(nghttp3_ringbuf *rb, size_t nmemb, size_t size, + const nghttp3_mem *mem) { + if (nmemb) { +diff --git node/deps/ngtcp2/ngtcp2/lib/ngtcp2_ringbuf.c node/deps/ngtcp2/ngtcp2/lib/ngtcp2_ringbuf.c +index 74e488bce7..36ca05e80e 100644 +--- node/deps/ngtcp2/ngtcp2/lib/ngtcp2_ringbuf.c ++++ node/deps/ngtcp2/ngtcp2/lib/ngtcp2_ringbuf.c +@@ -31,16 +31,6 @@ + + #include "ngtcp2_macro.h" + +-#if defined(_MSC_VER) && !defined(__clang__) && (defined(_M_ARM) || defined(_M_ARM64)) +-unsigned int __popcnt(unsigned int x) { +- unsigned int c = 0; +- for (; x; ++c) { +- x &= x - 1; +- } +- return c; +-} +-#endif +- + int ngtcp2_ringbuf_init(ngtcp2_ringbuf *rb, size_t nmemb, size_t size, + const ngtcp2_mem *mem) { + uint8_t *buf = ngtcp2_mem_malloc(mem, nmemb * size); +diff --git node/deps/v8/include/v8-initialization.h node/deps/v8/include/v8-initialization.h +index d3e35d6ec5..6e9bbe3849 100644 +--- node/deps/v8/include/v8-initialization.h ++++ node/deps/v8/include/v8-initialization.h +@@ -89,6 +89,10 @@ class V8_EXPORT V8 { + static void SetFlagsFromCommandLine(int* argc, char** argv, + bool remove_flags); + ++ static void EnableCompilationForSourcelessUse(); ++ static void DisableCompilationForSourcelessUse(); ++ static void FixSourcelessScript(Isolate* v8_isolate, Local script); ++ + /** Get the version string. */ + static const char* GetVersion(); + +diff --git node/deps/v8/src/api/api.cc node/deps/v8/src/api/api.cc +index a06394e6c1..154b7a82a8 100644 +--- node/deps/v8/src/api/api.cc ++++ node/deps/v8/src/api/api.cc +@@ -806,6 +806,28 @@ void V8::SetFlagsFromCommandLine(int* argc, char** argv, bool remove_flags) { + HelpOptions(HelpOptions::kDontExit)); + } + ++bool save_lazy; ++bool save_predictable; ++ ++void V8::EnableCompilationForSourcelessUse() { ++ save_lazy = i::v8_flags.lazy; ++ i::v8_flags.lazy = false; ++ save_predictable = i::v8_flags.predictable; ++ i::v8_flags.predictable = true; ++} ++ ++void V8::DisableCompilationForSourcelessUse() { ++ i::v8_flags.lazy = save_lazy; ++ i::v8_flags.predictable = save_predictable; ++} ++ ++void V8::FixSourcelessScript(Isolate* v8_isolate, Local unbound_script) { ++ auto isolate = reinterpret_cast(v8_isolate); ++ auto function_info = i::Handle::cast(Utils::OpenHandle(*unbound_script)); ++ i::Handle script(i::Script::cast(function_info->script()), isolate); ++ script->set_source(i::ReadOnlyRoots(isolate).undefined_value()); ++} ++ + RegisteredExtension* RegisteredExtension::first_extension_ = nullptr; + + RegisteredExtension::RegisteredExtension(std::unique_ptr extension) +diff --git node/deps/v8/src/codegen/compiler.cc node/deps/v8/src/codegen/compiler.cc +index 31c5acceeb..56cad8671f 100644 +--- node/deps/v8/src/codegen/compiler.cc ++++ node/deps/v8/src/codegen/compiler.cc +@@ -3475,7 +3475,7 @@ MaybeHandle GetSharedFunctionInfoForScriptImpl( + maybe_script = lookup_result.script(); + maybe_result = lookup_result.toplevel_sfi(); + is_compiled_scope = lookup_result.is_compiled_scope(); +- if (!maybe_result.is_null()) { ++ if (!maybe_result.is_null() && source->length()) { + compile_timer.set_hit_isolate_cache(); + } else if (can_consume_code_cache) { + compile_timer.set_consuming_code_cache(); +diff --git node/deps/v8/src/objects/js-function.cc node/deps/v8/src/objects/js-function.cc +index 94f7a672a7..57bb3fc7f1 100644 +--- node/deps/v8/src/objects/js-function.cc ++++ node/deps/v8/src/objects/js-function.cc +@@ -1280,6 +1280,9 @@ Handle JSFunction::ToString(Handle function) { + Handle maybe_class_positions = JSReceiver::GetDataProperty( + isolate, function, isolate->factory()->class_positions_symbol()); + if (maybe_class_positions->IsClassPositions()) { ++ if (String::cast(Script::cast(shared_info->script()).source()).IsUndefined(isolate)) { ++ return isolate->factory()->NewStringFromAsciiChecked("class {}"); ++ } + ClassPositions class_positions = + ClassPositions::cast(*maybe_class_positions); + int start_position = class_positions.start(); +diff --git node/deps/v8/src/objects/shared-function-info-inl.h node/deps/v8/src/objects/shared-function-info-inl.h +index 5621b15d98..722e1d18cb 100644 +--- node/deps/v8/src/objects/shared-function-info-inl.h ++++ node/deps/v8/src/objects/shared-function-info-inl.h +@@ -635,6 +635,14 @@ bool SharedFunctionInfo::ShouldFlushCode( + } + if (!data.IsBytecodeArray()) return false; + ++ Object script_obj = script(); ++ if (!script_obj.IsUndefined()) { ++ Script script = Script::cast(script_obj); ++ if (script.source().IsUndefined()) { ++ return false; ++ } ++ } ++ + if (IsStressFlushingEnabled(code_flush_mode)) return true; + + BytecodeArray bytecode = BytecodeArray::cast(data); +diff --git node/deps/v8/src/parsing/parsing.cc node/deps/v8/src/parsing/parsing.cc +index 8c55a6fb6e..70bf82a57d 100644 +--- node/deps/v8/src/parsing/parsing.cc ++++ node/deps/v8/src/parsing/parsing.cc +@@ -42,6 +42,7 @@ bool ParseProgram(ParseInfo* info, Handle