import type { TypePatchCommandRequest } from "../frontend/type-patches/command.js"; import { rmSync, statSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { parseArgs } from "node:util"; import { isExactExternalTypeSpecifier } from "../frontend/program.js"; import { renderDiagnostics } from "../diagnostics/render.js"; import { COVERAGE_FAIL_ON, coverageEnvelope, coveragePasses, renderCoverage, renderExplainDynamic, renderExplainIsland, type CoverageFailOn, } from "../coverage/report.js"; import { NODE_ISLANDS_MODES, type NodeIslandOptions, type NodeIslandsMode, } from "../node-islands/policy.js"; import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js"; import { buildEnvelope } from "../diagnostics/envelope.js"; import { renderConfigWarnings, renderTypePatchWarnings, renderWarnings, } from "../diagnostics/render.js"; import { isTypePatchCode } from "../diagnostics/diagnostic.js"; import { isPerformanceCode, renderPerformanceWarnings } from "../diagnostics/dynamic-tier.js"; import { isNativeOptimization } from "../backend/optimization.js"; import { renderNativeProfileMatch } from "../backend/native-profile.js"; import { hostTargetCpu, isTargetCpuOption, TARGET_CPU_OPTIONS, targetCpuField, type TargetCpu, } from "../backend/target-cpu.js"; import { setProvenanceSources } from "../frontend/provenance-registry.js"; import type { CliHost, NativeCacheWarmProfile } from "./host.js"; import { resolveOutputOptions } from "./output-options.js"; import { selectOutputPaths } from "./paths.js"; import { CLI_OPTIONS, USAGE } from "./usage.js"; /* The exit discipline: NEVER process.exit() after writing output. stdout/ * stderr to a PIPE are async streams — process.exit() drops whatever libuv * hasn't flushed yet, which truncates large diagnostic renders at the pipe * buffer (observed: 64KB cut mid-code-frame). Every path sets * process.exitCode and returns instead; Node exits naturally once the * streams drain. */ class CliExit extends Error { constructor(readonly code: number) { super(`exit ${code}`); this.name = "CliExit"; } } function fail(msg: string): never { process.stderr.write(msg + "\n"); throw new CliExit(1); } /** parseArgs, with its throw turned into the CLI's own one-line error. * Unparseable arguments are a USER error — an unknown flag or a missing * value used to reach the top level as an uncaught ERR_PARSE_ARGS_* and * print a Node stack trace over the user's terminal. */ function parseCli( args: string[], ): ReturnType< typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }> > { // `--explain-dynamic` takes an optional value: alone it lists every unit. const normalized = args.map((arg) => (arg === "--explain-dynamic" ? "--explain-dynamic=" : arg)); try { return parseArgs({ args: normalized, options: CLI_OPTIONS, allowPositionals: true, allowNegative: true, }); } catch (err) { // parseArgs appends a paragraph about `--` and positionals to the // unknown-option message; the first sentence is the part that names // what was wrong, and USAGE below already covers what was meant. const raw = err instanceof Error ? err.message : String(err); const msg = raw.split("\n")[0]!.split(". ")[0]!; fail(`scriptc: ${msg}\n\n${USAGE}`); } } async function main(args: string[], host: CliHost): Promise { const { values, positionals } = parseCli(args); const externalTypeArgs = values["external-types"] ?? []; if (values.version) { process.stdout.write(`${host.version()}\n`); return 0; } if (values.help || positionals.length === 0) { process.stdout.write(USAGE); return values.help ? 0 : 1; } const [command, inputArg] = positionals; if (command === "cache") { if (inputArg !== "warm") fail(`unknown cache command "${inputArg ?? ""}" (supported: warm)\n\n${USAGE}`); if ( values.lib || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values["fail-on"] !== undefined || values.ffi !== undefined || values.profile !== undefined || values.strip || values.static || values["windows-subsystem"] !== undefined || values["detached-exit"] || values["large-pages"] || values["target-cpu"] !== undefined || values["cpu-dispatch"] !== undefined || values["profile-generate"] !== undefined || values["profile-use"] !== undefined || values.sparse || (values["npm-static"] ?? []).length > 0 || values["provenance-sources"] || externalTypeArgs.length > 0 || values.out !== undefined || values["emit-ir"] || values.tier !== undefined || values["explain-dynamic"] !== undefined || hasNodeIslandFlags(values) || !values["keep-llvm"] ) { fail( `scriptc cache warm takes only native optimization/sanitizer options and profile names\n\n${USAGE}`, ); } const optimization = values.optimization; if (optimization !== undefined && !isNativeOptimization(optimization)) { fail(`unknown optimization "${optimization}" (supported: release, dev, speed)\n\n${USAGE}`); } const profileArgs = positionals.slice(2); const knownProfiles = new Set(["runtime", "tls"]); for (const profile of profileArgs) { if (!knownProfiles.has(profile as NativeCacheWarmProfile)) { fail(`unknown cache warm profile "${profile}" (supported: runtime, tls)`); } } let result; try { result = await host.warmNativeCaches({ ...(optimization === undefined ? {} : { optimization }), sanitize: values.sanitize, ...(profileArgs.length === 0 ? {} : { profiles: profileArgs as NativeCacheWarmProfile[] }), }); } catch (error) { fail(`scriptc: ${error instanceof Error ? error.message : String(error)}`); } process.stdout.write(`${result.cacheRoot}\n`); for (const profile of result.profiles) { process.stdout.write(`${profile.profile}\t${Math.round(profile.elapsedMs)}ms\n`); } return 0; } if (command === "merge-profiles") { const inputs = positionals.slice(1); const others = values.lib || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values["fail-on"] !== undefined || values.optimization !== undefined || values.ffi !== undefined || values.profile !== undefined || values.strip || values.static || values.sanitize || values["windows-subsystem"] !== undefined || values["detached-exit"] || values["large-pages"] || values["target-cpu"] !== undefined || values["cpu-dispatch"] !== undefined || values["profile-generate"] !== undefined || values["profile-use"] !== undefined || (values["npm-static"] ?? []).length > 0 || values["provenance-sources"] || externalTypeArgs.length > 0 || (values.wasm ?? []).length > 0 || values["emit-ir"] || values.tier !== undefined || values["explain-dynamic"] !== undefined || hasNodeIslandFlags(values) || !values["keep-llvm"]; if (values.out === undefined || inputs.length === 0 || others) { fail( `scriptc merge-profiles takes -o , optionally --sparse, and one or more profiles\n\n${USAGE}`, ); } const outputPath = resolve(values.out); try { await host.mergeProfiles( outputPath, inputs.map((input) => resolve(input)), values.sparse, ); } catch (error) { fail(`scriptc: ${error instanceof Error ? error.message : String(error)}`); } process.stdout.write(`${outputPath}\n`); return 0; } if (command === "types") { const sub = positionals[1]; const rest = positionals.slice(2); let request: TypePatchCommandRequest; if (sub === "patch" && rest.length === 3) { request = { action: "patch", cwd: process.cwd(), pkg: rest[0]!, selector: rest[1]!, type: rest[2]!, range: values.range ?? null, dryRun: values["dry-run"], }; } else if ( sub === "list" && rest.length === 0 && values.range === undefined && !values["dry-run"] ) { request = { action: "list", cwd: process.cwd() }; } else { fail( `scriptc types takes "patch " (with --range or --dry-run) or "list"\n\n${USAGE}`, ); } const result = await host.typePatches(request); process.stdout.write(result.stdout); process.stderr.write(result.stderr); return result.exitCode; } if (values.range !== undefined || values["dry-run"]) fail(`--range and --dry-run are scriptc types patch options\n\n${USAGE}`); if (values.sparse) fail(`--sparse is a merge-profiles option\n\n${USAGE}`); if (command !== "build" && command !== "run" && command !== "coverage") { fail(`unknown command "${command}"\n\n${USAGE}`); } if (values.lib) { // LIBRARY mode: the profile names the entry module and pins the // emission; the executable lane's mode flags have no meaning here // (library artifacts are static-tier only, and there is no fallback // concept — bare npm specifiers are static-or-refuse: the npm-static // eligibility bar runs automatically, eligible packages compile into // the graph, ineligible ones refuse with SC4013). if (command !== "build") fail(`--lib is a build mode (scriptc build --lib --profile )\n\n${USAGE}`); const profileArg = values.profile; if (!profileArg) fail(`scriptc build --lib needs --profile \n\n${USAGE}`); if (inputArg) { fail("scriptc build --lib takes no input positional: the profile names the entry module"); } if ( values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values["fail-on"] !== undefined || values.optimization !== undefined || values.strip || values.static || values.ffi !== undefined || values["windows-subsystem"] !== undefined || values["detached-exit"] || values["large-pages"] || values["target-cpu"] !== undefined || values["cpu-dispatch"] !== undefined || values["profile-generate"] !== undefined || values["profile-use"] !== undefined || (values["npm-static"] ?? []).length > 0 || externalTypeArgs.length > 0 || values.tier !== undefined || values["explain-dynamic"] !== undefined || hasNodeIslandFlags(values) ) { fail( "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--static/--windows-subsystem/--detached-exit/--large-pages/--target-cpu/--cpu-dispatch/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only", ); } const profilePath = resolve(profileArg); const libOutDir = values.out ? dirname(resolve(values.out)) : join(dirname(profilePath), ".scriptc"); const result = await host.compileLibrary({ profilePath, outDir: libOutDir, ...(values.out ? { outPath: resolve(values.out) } : {}), emitIr: values["emit-ir"], sanitize: values.sanitize, }); if (!result.ok) { const color = process.stderr.isTTY ?? false; process.stderr.write( renderDiagnostics(result.diagnostics, result.sourceTexts, { color }) + "\n", ); const n = result.diagnostics.length; process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`); return 1; } if (!values["keep-llvm"]) rmSync(result.llvmPath, { force: true }); process.stdout.write(`${result.archivePath}\n`); // The contract sidecar rides the same invocation when the profile // declares one — name it so the embedder's tooling knows where to look. if (result.sidecarPath !== undefined) process.stdout.write(`${result.sidecarPath}\n`); return 0; } if (values["emit-ir"] && (command === "build" || command === "run")) { process.stderr.write( "scriptc: warning: --emit-ir is deprecated; use --emit=ir for IR as the primary output\n", ); } if (!inputArg) fail(`missing input file\n\n${USAGE}`); const input = resolve(inputArg); if (command === "coverage" && values.emit !== undefined) { fail(`--emit is a build/run option\n\n${USAGE}`); } if (command === "coverage" && values.strip) { fail(`--strip is a build/run option\n\n${USAGE}`); } if (command === "coverage" && values.static) { fail(`--static is a build/run option\n\n${USAGE}`); } if (command === "coverage" && values["target-cpu"] !== undefined) { fail(`--target-cpu is a build/run option\n\n${USAGE}`); } if (command === "coverage" && values["cpu-dispatch"] !== undefined) { fail(`--cpu-dispatch is a build/run option\n\n${USAGE}`); } if (command === "coverage" && values["detached-exit"]) { fail(`--detached-exit is a build/run option\n\n${USAGE}`); } if (command === "coverage" && values["large-pages"]) { fail(`--large-pages is a build/run option\n\n${USAGE}`); } if (command === "coverage" && values["profile-generate"] !== undefined) { fail(`--profile-generate is a build/run option\n\n${USAGE}`); } if (command === "coverage" && values["profile-use"] !== undefined) { fail(`--profile-use is a build/run option\n\n${USAGE}`); } if ( values.print !== undefined && values.print !== "native-link-info" && values.print !== "diagnostics" ) { fail( `unknown print kind "${values.print}" (supported: native-link-info, diagnostics)\n\n${USAGE}`, ); } const printNativeLinkInfo = values.print === "native-link-info"; const printDiagnostics = values.print === "diagnostics"; if (printDiagnostics && command === "run") { fail(`--print=diagnostics is a build/coverage option\n\n${USAGE}`); } const failOnRaw = values["fail-on"]; if (failOnRaw !== undefined && !(COVERAGE_FAIL_ON as readonly string[]).includes(failOnRaw)) { fail( `unknown --fail-on value "${failOnRaw}" (supported: ${COVERAGE_FAIL_ON.join(", ")})\n\n${USAGE}`, ); } // --no-dynamic-tier-warning leaves out SC6101 (diagnostics/dynamic-tier.ts). const dynamicTierWarning = values["dynamic-tier-warning"] !== false; if (failOnRaw === "dynamic" && !dynamicTierWarning) { fail(`--fail-on=dynamic checks the warning --no-dynamic-tier-warning leaves out\n\n${USAGE}`); } if (failOnRaw !== undefined && command !== "coverage") { fail(`--fail-on is a coverage option (a build already fails on blockers)\n\n${USAGE}`); } const failOn = failOnRaw as CoverageFailOn | undefined; const tierRaw = values.tier; if (tierRaw !== undefined && !(TIER_MODES as readonly string[]).includes(tierRaw)) { fail(`unknown --tier value "${tierRaw}" (supported: ${TIER_MODES.join(", ")})\n\n${USAGE}`); } const tier = tierRaw as TierMode | undefined; const explainDynamic = values["explain-dynamic"]; if (explainDynamic !== undefined && command !== "coverage") { fail(`--explain-dynamic is a coverage option\n\n${USAGE}`); } const explainIsland = values["explain-island"]; if (explainIsland !== undefined && command !== "coverage") { fail(`--explain-island is a coverage option\n\n${USAGE}`); } if (explainIsland === "") fail(`--explain-island needs a package name\n\n${USAGE}`); if (values["strict-user-js"] && command !== "coverage") { fail(`--strict-user-js is a coverage option\n\n${USAGE}`); } // The Node island policy's command-line half. Report-only: build and run // validate it, coverage reports the islands it would choose. const islandMode = values["node-islands"]; if (islandMode !== undefined && !(NODE_ISLANDS_MODES as readonly string[]).includes(islandMode)) { fail( `unknown --node-islands value "${islandMode}" (supported: ${NODE_ISLANDS_MODES.join(", ")})\n\n${USAGE}`, ); } const listValues = (raw: string[] | undefined): string[] => (raw ?? []) .flatMap((v) => v.split(",")) .map((v) => v.trim()) .filter((v) => v !== ""); const islandArgs = listValues(values["node-island"]); const nativeArgs = listValues(values.native); for (const name of nativeArgs) { if (islandArgs.includes(name)) fail(`--native=${name} and --node-island=${name} contradict each other\n\n${USAGE}`); } if (values.node === "") fail(`--node needs the path of a node executable\n\n${USAGE}`); const nodeIslands: NodeIslandOptions | undefined = islandMode === undefined && islandArgs.length === 0 && nativeArgs.length === 0 && values.node === undefined && !values["strict-user-js"] ? undefined : { ...(islandMode !== undefined ? { mode: islandMode as NodeIslandsMode } : {}), ...(islandArgs.length > 0 ? { islands: islandArgs } : {}), ...(nativeArgs.length > 0 ? { native: nativeArgs } : {}), ...(values.node !== undefined ? { node: resolve(values.node) } : {}), ...(values["strict-user-js"] ? { strictUserJs: true } : {}), }; if (printNativeLinkInfo && command !== "build") { fail(`--print=native-link-info is a build option\n\n${USAGE}`); } if (printNativeLinkInfo && values.emit !== undefined && values.emit !== "obj") { fail(`--print=native-link-info requires --emit=obj\n\n${USAGE}`); } if (externalTypeArgs.length > 0 && command !== "coverage") { fail(`--external-types is a coverage-only option\n\n${USAGE}`); } const externalTypes: Record = Object.create(null) as Record; for (const mapping of externalTypeArgs) { const equals = mapping.indexOf("="); if (equals <= 0 || equals === mapping.length - 1) { fail( `invalid --external-types mapping ${JSON.stringify(mapping)} (expected )`, ); } const specifier = mapping.slice(0, equals).trim(); const declarationArg = mapping.slice(equals + 1).trim(); if (!isExactExternalTypeSpecifier(specifier)) { fail( `invalid --external-types specifier ${JSON.stringify(specifier)} (expected an exact bare package specifier)`, ); } if (!/\.d\.(?:ts|mts|cts)$/.test(declarationArg)) { fail( `invalid --external-types declaration ${JSON.stringify(declarationArg)} (expected a .d.ts, .d.mts, or .d.cts file)`, ); } if (externalTypes[specifier] !== undefined) { fail(`duplicate --external-types mapping for ${JSON.stringify(specifier)}`); } const declarationPath = resolve(declarationArg); try { if (!statSync(declarationPath).isFile()) throw new Error("not a file"); } catch { fail(`--external-types declaration does not name a readable file: ${declarationPath}`); } externalTypes[specifier] = declarationPath; } const wasmArgs = values.wasm ?? []; if (wasmArgs.length > 0 && command !== "coverage") { fail(`--wasm is a coverage-only option\n\n${USAGE}`); } const wasmFiles = wasmArgs.map((file) => resolve(file)); const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined; if (values.backend !== undefined && values.backend !== "llvm") { fail(`unknown backend "${values.backend}" (supported: llvm)\n\n${USAGE}`); } const optimization = values.optimization; if (optimization !== undefined && !isNativeOptimization(optimization)) { fail(`unknown optimization "${optimization}" (supported: release, dev, speed)\n\n${USAGE}`); } const windowsSubsystem = values["windows-subsystem"]; if ( windowsSubsystem !== undefined && windowsSubsystem !== "console" && windowsSubsystem !== "gui" ) { fail(`unknown Windows subsystem "${windowsSubsystem}" (supported: console, gui)\n\n${USAGE}`); } if (windowsSubsystem !== undefined && command === "coverage") { fail(`--windows-subsystem is only supported for executable builds\n\n${USAGE}`); } const targetCpuOption = values["target-cpu"]; if (targetCpuOption !== undefined && !isTargetCpuOption(targetCpuOption)) { fail( `unknown target CPU "${targetCpuOption}" (supported: ${TARGET_CPU_OPTIONS.join(", ")})\n\n${USAGE}`, ); } let targetCpu: TargetCpu | undefined = targetCpuOption === "native" ? undefined : targetCpuOption; if (targetCpuOption === "native") { const level = hostTargetCpu(); if (level === null) fail(`--target-cpu=native needs an x86-64 Linux or macOS build host; name a level instead`); targetCpu = level; } const output = command === "coverage" ? null : resolveOutputOptions(command, { ...(values.emit === undefined && !printNativeLinkInfo ? {} : { emit: values.emit ?? "obj" }), emitIr: values["emit-ir"], ...(values.backend === undefined ? {} : { backend: values.backend }), keepLlvm: values["keep-llvm"], sanitize: values.sanitize, ...(values.optimization === undefined ? {} : { optimization: values.optimization }), strip: values.strip, staticLink: values.static, detachedExit: values["detached-exit"], largePages: values["large-pages"], ...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }), ...(values["profile-generate"] === undefined ? {} : { profileGenerate: values["profile-generate"] }), ...(values["profile-use"] === undefined ? {} : { profileUse: values["profile-use"] }), ...(windowsSubsystem === undefined ? {} : { windowsSubsystem }), ...(values.ffi === undefined ? {} : { ffi: values.ffi }), }); if (output !== null && !output.ok) fail(`${output.message}\n\n${USAGE}`); const backend = output === null ? undefined : output.backend; // --npm-static: repeatable and comma-splittable; the literal "auto" // switches to eligibility-based detection (mixing "auto" with names // is rejected — the shapes answer different questions). const npmStaticRaw = (values["npm-static"] ?? []) .flatMap((v) => v.split(",")) .map((v) => v.trim()) .filter((v) => v !== ""); let npmStatic: string[] | "auto" | undefined; if (npmStaticRaw.includes("auto")) { if (npmStaticRaw.length > 1) fail(`--npm-static auto cannot be combined with package names\n\n${USAGE}`); npmStatic = "auto"; } else if (npmStaticRaw.length > 0) { npmStatic = npmStaticRaw; } // --provenance-sources resolves BEFORE the program loads (tsgo needs the // source "paths" at creation): attestations and source trees fetch (or // ride the content-addressed cache / the offline manifest), the registry // installs, and every fallback prints as a note — never a failure. const provenance = values["provenance-sources"] ? await host.resolveProvenanceSources(input) : null; if (provenance !== null) { setProvenanceSources(provenance); for (const pkg of provenance.packages) { process.stderr.write( `provenance: ${pkg.name}@${pkg.version} ← ${pkg.repo.replace(/^git\+/, "")} @ ${pkg.commit.slice(0, 12)} (source compiles statically)\n`, ); } for (const note of provenance.notes) process.stderr.write(`provenance: ${note}\n`); } if (command === "coverage") { const { coverage, sourceTexts } = await host.analyze(input, { ...(npmStatic !== undefined ? { npmStatic } : {}), ...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}), ...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}), ...(tier !== undefined ? { tier } : {}), ...(explainDynamic !== undefined ? { explainDynamic: true } : {}), ...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}), ...(nodeIslands !== undefined ? { nodeIslands } : {}), }); if (!dynamicTierWarning) delete coverage.performance; if (printDiagnostics) { // Keep stdout pure JSON for tooling. const envelope = coverageEnvelope(coverage, { compilerVersion: host.version(), sourceTexts, ...(failOn === undefined ? {} : { failOn }), }); process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`); } else { const color = process.stdout.isTTY ?? false; process.stdout.write( renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n", ); if (explainIsland !== undefined) { process.stdout.write( "\n" + renderExplainIsland(coverage.nodeIslands, explainIsland, { color, sourceTexts, root: process.cwd(), }) + "\n", ); } if (explainDynamic !== undefined && coverage.tiers !== undefined) { process.stdout.write( "\n" + renderExplainDynamic(coverage.tiers, { color, sourceTexts, root: process.cwd(), query: explainDynamic, }) + "\n", ); } } return coveragePasses(coverage, failOn) ? 0 : 1; } if (output === null || !output.ok) throw new Error("internal output-option state"); if (windowsSubsystem !== undefined && host.sourceTargetPlatform() !== "win32") { fail(`--windows-subsystem requires a Windows executable target\n\n${USAGE}`); } const { outDir, outPath } = selectOutputPaths( input, output.cliOutputKind, values.out, output.cliOutputKind === "exe" ? host.sourceTargetPlatform() : undefined, ); let nativeLinkInfo: object | undefined; let diagnosticsEnvelope: object | undefined; const build = async (): Promise => { const result = await host.compile(input, { outPath, outDir, outputKind: output.outputKind, emitIr: output.emitIr, sanitize: values.sanitize, ...(backend !== undefined ? { backend } : {}), ...(optimization !== undefined ? { optimization } : {}), ...(values.strip ? { strip: true } : {}), ...(values.static ? { staticLink: true } : {}), ...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}), ...(values["detached-exit"] ? { detachedExit: true } : {}), ...(values.libnode !== undefined ? { libnode: values.libnode === "beside" || values.libnode.startsWith("$ORIGIN/") ? values.libnode : resolve(values.libnode), } : {}), ...(values["large-pages"] ? { largePages: true } : {}), ...targetCpuField(targetCpu), ...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }), // The instrumented program resolves a relative pattern against its own // working directory; resolve it here, where the user wrote it. ...(values["profile-generate"] === undefined ? {} : { profileGenerate: resolve(values["profile-generate"]) }), ...(values["profile-use"] === undefined ? {} : { profileUse: resolve(values["profile-use"]) }), ...(npmStatic !== undefined ? { npmStatic } : {}), ...(nodeIslands !== undefined ? { nodeIslands } : {}), ...(values["shape-overlay"] === false ? { shapeOverlay: false } : {}), ...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}), ...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}), ...(tier !== undefined ? { tier } : {}), }); if (!result.ok) { if (printDiagnostics) { const envelope = buildEnvelope({ compilerVersion: host.version(), entry: input, ok: false, diagnostics: result.diagnostics, sourceTexts: result.sourceTexts, }); process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`); throw new CliExit(1); } const color = process.stderr.isTTY ?? false; process.stderr.write( renderDiagnostics(result.diagnostics, result.sourceTexts, { color }) + "\n", ); const n = result.diagnostics.length; process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`); throw new CliExit(1); } const warnings = (result.warnings ?? []).filter( (d) => dynamicTierWarning || !isPerformanceCode(d.code), ); if (printDiagnostics) { diagnosticsEnvelope = buildEnvelope({ compilerVersion: host.version(), entry: input, ok: true, artifact: result.artifact.path, diagnostics: [], warnings, ...(result.sourceTexts === undefined ? {} : { sourceTexts: result.sourceTexts }), }); } else if (warnings.length > 0 && command === "build") { // Divergence warnings never fail a build; they explain where the // native program can behave differently from Node. The performance // warning names likely-hot functions that use untyped values. `run` // keeps stderr for the program's own output. const color = process.stderr.isTTY ?? false; const config = warnings.filter((d) => d.code === "SC8004"); const patches = warnings.filter((d) => isTypePatchCode(d.code)); const divergences = warnings.filter( (d) => !isPerformanceCode(d.code) && d.code !== "SC8004" && !isTypePatchCode(d.code), ); const performance = warnings.filter((d) => isPerformanceCode(d.code)); if (config.length > 0) process.stderr.write( renderConfigWarnings(config, result.sourceTexts ?? new Map(), { color }) + "\n", ); if (patches.length > 0) { if (config.length > 0) process.stderr.write("\n"); process.stderr.write( renderTypePatchWarnings(patches, result.sourceTexts ?? new Map(), { color }) + "\n", ); } if (divergences.length > 0) { if (config.length > 0 || patches.length > 0) process.stderr.write("\n"); process.stderr.write( renderWarnings(divergences, result.sourceTexts ?? new Map(), { color }) + "\n", ); } if (performance.length > 0) { if (divergences.length > 0 || config.length > 0 || patches.length > 0) process.stderr.write("\n"); process.stderr.write(renderPerformanceWarnings(performance) + "\n"); } } if (result.artifact.kind === "exe") { // How the profile matched the program's functions; `run` keeps stderr // for the program's own output. const profileUse = values["profile-use"]; if ( command === "build" && profileUse !== undefined && result.artifact.profileMatch !== undefined ) process.stderr.write(renderNativeProfileMatch(profileUse, result.artifact.profileMatch)); if (!values["keep-llvm"]) rmSync(result.artifact.translationUnitPath, { force: true }); } else if (result.artifact.kind === "obj") { nativeLinkInfo = result.artifact.nativeLinkInfo; } return result.artifact.path; }; const binary = await build(); if (command === "run") { return host.run(binary); } if (printDiagnostics) { process.stdout.write(`${JSON.stringify(diagnosticsEnvelope, null, 2)}\n`); } else if (printNativeLinkInfo) { if (nativeLinkInfo === undefined) throw new Error("internal native-link-info state"); // Keep stdout pure JSON for tooling; the ordinary artifact path is in // program.object inside the document. process.stdout.write(`${JSON.stringify(nativeLinkInfo, null, 2)}\n`); } else { process.stdout.write(`${binary}\n`); } return 0; } /** Whether any Node island flag is present (they belong to build, run and * coverage). */ function hasNodeIslandFlags(values: ReturnType["values"]): boolean { return ( values["node-islands"] !== undefined || (values["node-island"] ?? []).length > 0 || (values.native ?? []).length > 0 || values.node !== undefined || values["explain-island"] !== undefined || values["strict-user-js"] ); } /** Both installed and seed commands use this argument and diagnostic contract. */ export async function runCli(args: string[], host: CliHost): Promise { try { return await main(args, host); } catch (err) { if (err instanceof Error && err.name === "CliExit") return Number(err.message.slice(5)); throw err; } }