diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index 22f383701e8e..e3f0e5d016df 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -389,13 +389,18 @@ export class BootServiceCommandError extends Schema.TaggedErrorvitest": "-", + vite: "npm:@voidzero-dev/vite-plus-core@0.3.0", +}); + +const okResult = (stdout = "", stderr = "") => ({ + stdout, + stderr, + code: ChildProcessSpawner.ExitCode(0), + timedOut: false, + stdoutTruncated: false, + stderrTruncated: false, + stdoutInvalidUtf8: false, + stderrInvalidUtf8: false, +}); + +const failedResult = (stderr: string, stdout = "") => ({ + ...okResult(stdout, stderr), + code: ChildProcessSpawner.ExitCode(1), +}); + +const isNpmView = (input: ProcessRunner.ProcessRunInput) => + input.args[0] === "view" || (input.command === "pnpm" && input.args.includes("view")); + +const isNpmInstall = (input: ProcessRunner.ProcessRunInput) => + input.args[0] === "install" || (input.command === "pnpm" && input.args.includes("install")); + +const stagingPrefix = (input: ProcessRunner.ProcessRunInput) => { + const prefixIndex = input.args.indexOf("--prefix"); + return input.args[prefixIndex + 1]; +}; + const successfulRunner = (fs: FileSystem.FileSystem, path: Path.Path) => ProcessRunner.ProcessRunner.of({ run: (input) => Effect.gen(function* () { - const prefixIndex = input.args.indexOf("--prefix"); - const stagingDir = input.args[prefixIndex + 1]; + if (isNpmView(input)) { + return okResult(effectOverridesJson); + } + if (!isNpmInstall(input)) { + return yield* Effect.die(`unexpected command: ${input.command} ${input.args.join(" ")}`); + } + const stagingDir = stagingPrefix(input); if (stagingDir === undefined) return yield* Effect.die("missing npm --prefix"); + assert.isUndefined( + input.args.find((arg) => arg.startsWith("t3@")), + "install must use the staging manifest, not a positional t3@version", + ); const entry = path.join(stagingDir, "node_modules", "t3", "dist", "bin.mjs"); yield* fs.makeDirectory(path.dirname(entry), { recursive: true }).pipe(Effect.orDie); yield* fs.writeFileString(entry, "export {};\n").pipe(Effect.orDie); - return { - stdout: "", - stderr: "", - code: ChildProcessSpawner.ExitCode(0), - timedOut: false, - stdoutTruncated: false, - stderrTruncated: false, - stdoutInvalidUtf8: false, - stderrInvalidUtf8: false, - }; + return okResult(); }), }); it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { + it.effect("writes Effect overrides into the staging manifest before install", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-manifest-" }); + const commands: Array = []; + let manifestBeforeInstall: unknown; + + yield* ensurePinnedRuntimeInstalled({ + baseDir, + version: "1.2.3", + fs, + path, + runner: ProcessRunner.ProcessRunner.of({ + run: (input) => + Effect.gen(function* () { + commands.push(input); + if (isNpmView(input)) { + assert.deepEqual(input.args, ["view", "t3@1.2.3", "overrides", "--json"]); + return okResult(effectOverridesJson); + } + const stagingDir = stagingPrefix(input); + if (stagingDir === undefined) return yield* Effect.die("missing npm --prefix"); + manifestBeforeInstall = JSON.parse( + yield* fs.readFileString(path.join(stagingDir, "package.json")).pipe(Effect.orDie), + ); + return yield* successfulRunner(fs, path).run(input); + }), + }), + validate: () => Effect.void, + }); + + assert.equal(commands[0]?.args[0], "view"); + assert.equal(commands[1]?.args[0], "install"); + assert.deepEqual(commands[1]?.args, [ + "install", + "--prefix", + stagingPrefix(commands[1]!), + "--no-fund", + "--no-audit", + ]); + assert.deepEqual(manifestBeforeInstall, { + dependencies: { t3: "1.2.3" }, + overrides: { + effect: "4.0.0-rc.112", + "@effect/platform-node": "4.0.0-rc.112", + "@effect/platform-node-shared": "4.0.0-rc.112", + }, + }); + }), + ); + + it.effect("surfaces a truncated npm stderr tail when install fails", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-stderr-" }); + // npm puts the actionable failure near the end of stderr; the bounded + // tail must keep that reason when earlier noise is large. + const stderr = `${"x".repeat(2500)}\nnpm error code ERESOLVE\nnpm error Could not resolve dependency\n`; + + const error = yield* ensurePinnedRuntimeInstalled({ + baseDir, + version: "1.2.3", + fs, + path, + runner: ProcessRunner.ProcessRunner.of({ + run: (input) => + isNpmView(input) + ? Effect.succeed(okResult(effectOverridesJson)) + : Effect.succeed(failedResult(stderr)), + }), + validate: () => Effect.die("must not validate a failed install"), + }).pipe(Effect.flip); + + assert.equal(error._tag, "PinnedRuntimeInstallError"); + assert.equal(error.exitCode, 1); + assert.isTrue(error.message.includes("exit code 1")); + assert.isTrue(error.outputTail !== undefined && error.outputTail.includes("ERESOLVE")); + assert.equal(error.outputTail?.length, 2048); + assert.isTrue(error.message.endsWith(error.outputTail!)); + }), + ); + it.effect("installs through pnpm when its Node runtime has no npm executable", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -77,9 +196,10 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { }); assert.deepEqual( commands.map((command) => command.command), - ["npm", "pnpm"], + ["npm", "pnpm", "npm", "pnpm"], ); assert.deepEqual(commands[1]!.args, ["--package=npm@11", "dlx", "npm", ...commands[0]!.args]); + assert.deepEqual(commands[3]!.args, ["--package=npm@11", "dlx", "npm", ...commands[2]!.args]); assert.equal(yield* fs.readFileString(paths.sentinelPath), "1.2.3\n"); }), ); @@ -113,6 +233,7 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { }), validate: () => Effect.die("must not validate a failed install"), }).pipe(Effect.flip); + // Fails on the first npm (view) without falling back to pnpm. assert.deepEqual(commands, ["npm"]); }), ); diff --git a/apps/server/src/cloud/pinnedRuntime.ts b/apps/server/src/cloud/pinnedRuntime.ts index 534ed917218f..2c168cf0c025 100644 --- a/apps/server/src/cloud/pinnedRuntime.ts +++ b/apps/server/src/cloud/pinnedRuntime.ts @@ -19,6 +19,9 @@ import * as ProcessRunner from "../processRunner.ts"; const PINNED_RUNTIME_DIR = "runtime"; const PINNED_RUNTIME_INSTALL_TIMEOUT = Duration.minutes(10); +const PINNED_RUNTIME_VIEW_TIMEOUT = Duration.seconds(60); +/** Keep failures diagnosable without dumping an entire npm log into the CLI. */ +const PINNED_RUNTIME_OUTPUT_TAIL_CHARS = 2048; // Boot-service setup and remote update can construct separate layers. Serialize // the complete install transaction across every caller in this process. const pinnedRuntimeInstallLock = Semaphore.makeUnsafe(1); @@ -49,13 +52,18 @@ export class PinnedRuntimeInstallError extends Schema.TaggedErrordep`) and removal overrides (`-`) are publish-time + * monorepo policy and are rejected by npm when copied into this staging root. + */ +function selectEffectOverrides(overrides: Record): Record { + const selected: Record = {}; + for (const [key, value] of Object.entries(overrides)) { + if (typeof value !== "string" || value === "-" || value.length === 0) continue; + if (key === "effect" || /^@effect\/[^>]+$/.test(key)) { + selected[key] = value; + } + } + return selected; +} + +function stderrOutputTail(stderr: string): string | undefined { + const trimmed = stderr.trim(); + if (trimmed.length === 0) return undefined; + if (trimmed.length <= PINNED_RUNTIME_OUTPUT_TAIL_CHARS) return trimmed; + return trimmed.slice(trimmed.length - PINNED_RUNTIME_OUTPUT_TAIL_CHARS); +} + /** * Installs `t3@` into the pinned runtime directory unless a complete * install is already there, and returns its paths. The sentinel is written @@ -89,6 +123,85 @@ interface PinnedRuntimeInstallInput { ) => Effect.Effect; } +const runNpm = ( + runner: ProcessRunner.ProcessRunner["Service"], + args: ReadonlyArray, + timeout: Duration.Input, +) => + runner.run({ command: "npm", args, timeout }).pipe( + Effect.catchTags({ + ProcessSpawnError: (error) => + error.cause instanceof PlatformError.PlatformError && error.cause.reason._tag === "NotFound" + ? // pnpm-managed Node installations do not include npm. Keep npm + // installation semantics for the pinned runtime and native builds. + runner.run({ + command: "pnpm", + args: ["--package=npm@11", "dlx", "npm", ...args], + timeout, + }) + : Effect.fail(error), + }), + ); + +const resolveTargetEffectOverrides = Effect.fn("cloud.pinned_runtime.resolve_effect_overrides")( + function* (input: { + readonly version: string; + readonly runner: ProcessRunner.ProcessRunner["Service"]; + }) { + const viewStep = "resolving Effect overrides for the pinned t3 runtime"; + const viewArgs = ["view", `t3@${input.version}`, "overrides", "--json"]; + const result = yield* runNpm(input.runner, viewArgs, PINNED_RUNTIME_VIEW_TIMEOUT).pipe( + Effect.mapError((cause) => new PinnedRuntimeInstallError({ step: viewStep, cause })), + Effect.filterOrFail( + (output) => output.code === 0, + (output) => + new PinnedRuntimeInstallError({ + step: viewStep, + exitCode: Number(output.code), + stdoutLength: output.stdout.length, + stderrLength: output.stderr.length, + outputTail: stderrOutputTail(output.stderr), + }), + ), + ); + + let parsed: unknown; + try { + const trimmed = result.stdout.trim(); + parsed = trimmed.length === 0 ? {} : JSON.parse(trimmed); + } catch (cause) { + return yield* new PinnedRuntimeInstallError({ + step: "decoding Effect overrides for the pinned t3 runtime", + cause, + stdoutLength: result.stdout.length, + stderrLength: result.stderr.length, + outputTail: stderrOutputTail(result.stderr), + }); + } + + if (parsed === null || parsed === undefined) return {}; + if (typeof parsed !== "object" || Array.isArray(parsed)) { + return yield* new PinnedRuntimeInstallError({ + step: "decoding Effect overrides for the pinned t3 runtime", + stdoutLength: result.stdout.length, + stderrLength: result.stderr.length, + outputTail: stderrOutputTail(result.stderr), + }); + } + const record = parsed as Record; + // `npm view overrides --json` usually returns the map itself; some + // npm versions wrap it as `{ overrides: { … } }`. + const overrides = + record.overrides !== undefined && + typeof record.overrides === "object" && + record.overrides !== null && + !Array.isArray(record.overrides) + ? (record.overrides as Record) + : record; + return selectEffectOverrides(overrides); + }, +); + const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")(function* ( input: PinnedRuntimeInstallInput, ) { @@ -152,48 +265,45 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( }; return yield* Effect.gen(function* () { + const overrides = yield* resolveTargetEffectOverrides({ + version: input.version, + runner, + }); + // @effect-diagnostics-next-line preferSchemaOverJson:off - fixed npm install manifest. + const stagingManifest = `${JSON.stringify( + { + dependencies: { t3: input.version }, + overrides, + }, + null, + 2, + )}\n`; + yield* fs.writeFileString(input.path.join(stagingDir, "package.json"), stagingManifest).pipe( + Effect.mapError( + (cause) => + new PinnedRuntimeInstallError({ + step: "writing the pinned runtime install manifest", + cause, + }), + ), + ); + const installStep = "installing the pinned t3 runtime (this can take a few minutes)"; - const installArgs = [ - "install", - "--prefix", - stagingDir, - "--no-fund", - "--no-audit", - `t3@${input.version}`, - ]; - yield* runner - .run({ - command: "npm", - args: installArgs, - // Native dependencies may compile from source on slower machines. - timeout: PINNED_RUNTIME_INSTALL_TIMEOUT, - }) - .pipe( - Effect.catchTags({ - ProcessSpawnError: (error) => - error.cause instanceof PlatformError.PlatformError && - error.cause.reason._tag === "NotFound" - ? // pnpm-managed Node installations do not include npm. Keep npm - // installation semantics for the pinned runtime and native builds. - runner.run({ - command: "pnpm", - args: ["--package=npm@11", "dlx", "npm", ...installArgs], - timeout: PINNED_RUNTIME_INSTALL_TIMEOUT, - }) - : Effect.fail(error), - }), - Effect.mapError((cause) => new PinnedRuntimeInstallError({ step: installStep, cause })), - Effect.filterOrFail( - (result) => result.code === 0, - (result) => - new PinnedRuntimeInstallError({ - step: installStep, - exitCode: Number(result.code), - stdoutLength: result.stdout.length, - stderrLength: result.stderr.length, - }), - ), - ); + const installArgs = ["install", "--prefix", stagingDir, "--no-fund", "--no-audit"]; + yield* runNpm(runner, installArgs, PINNED_RUNTIME_INSTALL_TIMEOUT).pipe( + Effect.mapError((cause) => new PinnedRuntimeInstallError({ step: installStep, cause })), + Effect.filterOrFail( + (result) => result.code === 0, + (result) => + new PinnedRuntimeInstallError({ + step: installStep, + exitCode: Number(result.code), + stdoutLength: result.stdout.length, + stderrLength: result.stderr.length, + outputTail: stderrOutputTail(result.stderr), + }), + ), + ); yield* input.validate(stagingPaths); yield* fs