Created
June 17, 2026 09:36
-
-
Save rksm/736f5af900a95d7ae076c672b97d4188 to your computer and use it in GitHub Desktop.
postinstall.mjs Effect and simplified JS/Bash variants
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { BunRuntime, BunServices } from "@effect/platform-bun"; | |
| import { Clock, Effect, Inspectable, Path, Schema as S, Stream } from "effect"; | |
| import { ChildProcess } from "effect/unstable/process"; | |
| const STATUS_STREAM = process.stdout; | |
| const IS_TTY = STATUS_STREAM.isTTY === true; | |
| const GREEN = IS_TTY ? "\x1b[32m" : ""; | |
| const RED = IS_TTY ? "\x1b[31m" : ""; | |
| const DIM = IS_TTY ? "\x1b[90m" : ""; | |
| const RESET = IS_TTY ? "\x1b[0m" : ""; | |
| const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; | |
| class CommandError extends S.TaggedErrorClass()("CommandError", { | |
| command: S.String, | |
| exitCode: S.Int, | |
| output: S.String, | |
| }) { | |
| get message() { | |
| return this.output.length > 0 | |
| ? `${this.command} exited with code ${this.exitCode}: ${this.output}` | |
| : `${this.command} exited with code ${this.exitCode}`; | |
| } | |
| } | |
| const formatUnknown = (value) => { | |
| if (value instanceof Error) { | |
| return value.message || value.name || Inspectable.toStringUnknown(value); | |
| } | |
| return Inspectable.toStringUnknown(value); | |
| }; | |
| const formatTiming = (elapsedMs) => { | |
| const seconds = elapsedMs / 1000; | |
| const value = seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(2); | |
| return `${DIM}[${value}s]${RESET}`; | |
| }; | |
| const renderRow = (label, status, frame) => { | |
| switch (status.kind) { | |
| case "running": | |
| return `${DIM}${SPINNER[frame % SPINNER.length]}${RESET} ${label}`; | |
| case "ok": | |
| return `${GREEN}✓${RESET} ${label} ${status.timing}`; | |
| case "fail": | |
| return `${RED}✗${RESET} ${label} failed ${status.timing}`; | |
| } | |
| }; | |
| const startProgress = (labels) => { | |
| const statuses = labels.map(() => ({ kind: "running" })); | |
| let frame = 0; | |
| if (IS_TTY) { | |
| for (let i = 0; i < labels.length; i++) { | |
| STATUS_STREAM.write(`${renderRow(labels[i], statuses[i], 0)}\n`); | |
| } | |
| } | |
| const repaint = () => { | |
| if (!IS_TTY) { | |
| return; | |
| } | |
| STATUS_STREAM.write(`\x1b[${labels.length}A`); | |
| for (let i = 0; i < labels.length; i++) { | |
| STATUS_STREAM.write(`\r\x1b[2K${renderRow(labels[i], statuses[i], frame)}\n`); | |
| } | |
| }; | |
| const timer = IS_TTY | |
| ? setInterval(() => { | |
| frame++; | |
| repaint(); | |
| }, 80) | |
| : null; | |
| return { | |
| setStatus(index, status) { | |
| statuses[index] = status; | |
| if (IS_TTY) { | |
| repaint(); | |
| return; | |
| } | |
| if (status.kind !== "running") { | |
| STATUS_STREAM.write(`${renderRow(labels[index], status, 0)}\n`); | |
| } | |
| }, | |
| stop() { | |
| if (timer) { | |
| clearInterval(timer); | |
| } | |
| if (IS_TTY) { | |
| repaint(); | |
| } | |
| for (let i = 0; i < statuses.length; i++) { | |
| const status = statuses[i]; | |
| if (status.kind === "fail") { | |
| STATUS_STREAM.write(`${RED}error${RESET} (${labels[i]}): ${status.reason}\n`); | |
| } | |
| } | |
| }, | |
| }; | |
| }; | |
| const resolvePostinstall = Effect.fn("resolvePostinstall")(function* () { | |
| const path = yield* Path.Path; | |
| const scriptPath = yield* path.fromFileUrl(new URL(import.meta.url)); | |
| const rootDir = path.resolve(path.dirname(scriptPath), ".."); | |
| const binDir = path.join(rootDir, "node_modules", ".bin"); | |
| return { | |
| rootDir, | |
| steps: [ | |
| ...(process.env.CI === "true" | |
| ? [] | |
| : [ | |
| { | |
| args: [path.join(rootDir, "scripts", "sync-effect-submodule.ts")], | |
| command: process.execPath, | |
| label: "Effect submodule", | |
| timeout: "5 minutes", | |
| }, | |
| ]), | |
| { | |
| args: ["config", "--hooks-dir", ".githooks"], | |
| command: path.join(binDir, "vp"), | |
| label: "Vite+ config", | |
| timeout: "60 seconds", | |
| }, | |
| { | |
| args: ["patch"], | |
| command: path.join(binDir, "effect-tsgo"), | |
| label: "Effect tsgo patch", | |
| timeout: "60 seconds", | |
| }, | |
| ], | |
| }; | |
| }); | |
| const runCommand = Effect.fn("runCommand")(function* (cwd, command, args) { | |
| const formatted = [command, ...args].join(" "); | |
| const child = yield* ChildProcess.make(command, args, { cwd, stderr: "pipe", stdout: "pipe" }); | |
| const [output, exitCode] = yield* Effect.all([ | |
| Stream.mkString(Stream.decodeText(child.all)), | |
| child.exitCode, | |
| ]); | |
| const trimmed = output.trim(); | |
| if (exitCode !== 0) { | |
| return yield* new CommandError({ | |
| command: formatted, | |
| exitCode, | |
| output: trimmed, | |
| }); | |
| } | |
| }); | |
| const postinstall = Effect.gen(function* () { | |
| const { rootDir, steps } = yield* resolvePostinstall(); | |
| yield* Effect.acquireUseRelease( | |
| Effect.sync(() => startProgress(steps.map((step) => step.label))), | |
| (progress) => | |
| Effect.gen(function* () { | |
| const failures = []; | |
| yield* Effect.all( | |
| steps.map((step, index) => | |
| Effect.gen(function* () { | |
| const startedAt = yield* Clock.currentTimeMillis; | |
| const result = yield* Effect.result( | |
| runCommand(rootDir, step.command, step.args).pipe(Effect.timeout(step.timeout)), | |
| ); | |
| const finishedAt = yield* Clock.currentTimeMillis; | |
| const timing = formatTiming(finishedAt - startedAt); | |
| if (result._tag === "Failure") { | |
| const reason = formatUnknown(result.failure); | |
| progress.setStatus(index, { kind: "fail", reason, timing }); | |
| failures.push(result.failure); | |
| return; | |
| } | |
| progress.setStatus(index, { kind: "ok", timing }); | |
| }), | |
| ), | |
| { concurrency: "unbounded" }, | |
| ); | |
| if (failures.length > 0) { | |
| return yield* Effect.fail(failures[0]); | |
| } | |
| }), | |
| (progress) => Effect.sync(() => progress.stop()), | |
| ); | |
| }); | |
| const program = postinstall.pipe(Effect.scoped, Effect.provide(BunServices.layer)); | |
| BunRuntime.runMain(program, { disableErrorReporting: true }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { spawn } from "node:child_process"; | |
| import path from "node:path"; | |
| import { fileURLToPath } from "node:url"; | |
| const isTty = process.stdout.isTTY === true; | |
| const green = isTty ? "\x1b[32m" : ""; | |
| const red = isTty ? "\x1b[31m" : ""; | |
| const dim = isTty ? "\x1b[90m" : ""; | |
| const reset = isTty ? "\x1b[0m" : ""; | |
| const scriptPath = fileURLToPath(import.meta.url); | |
| const rootDir = path.resolve(path.dirname(scriptPath), ".."); | |
| const binDir = path.join(rootDir, "node_modules", ".bin"); | |
| const steps = [ | |
| ...(process.env.CI === "true" | |
| ? [] | |
| : [ | |
| { | |
| label: "Effect submodule", | |
| command: process.execPath, | |
| args: [path.join(rootDir, "scripts", "sync-effect-submodule.ts")], | |
| timeoutMs: 5 * 60 * 1000, | |
| }, | |
| ]), | |
| { | |
| label: "Vite+ config", | |
| command: path.join(binDir, "vp"), | |
| args: ["config", "--hooks-dir", ".githooks"], | |
| timeoutMs: 60 * 1000, | |
| }, | |
| { | |
| label: "Effect tsgo patch", | |
| command: path.join(binDir, "effect-tsgo"), | |
| args: ["patch"], | |
| timeoutMs: 60 * 1000, | |
| }, | |
| ]; | |
| function formatTiming(startedAt) { | |
| const seconds = (Date.now() - startedAt) / 1000; | |
| const value = seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(2); | |
| return `${dim}[${value}s]${reset}`; | |
| } | |
| function runCommand({ command, args, timeoutMs }) { | |
| return new Promise((resolve, reject) => { | |
| const child = spawn(command, args, { | |
| cwd: rootDir, | |
| stdio: ["ignore", "pipe", "pipe"], | |
| }); | |
| let output = ""; | |
| child.stdout.on("data", (chunk) => { | |
| output += chunk; | |
| }); | |
| child.stderr.on("data", (chunk) => { | |
| output += chunk; | |
| }); | |
| const timer = setTimeout(() => { | |
| child.kill("SIGTERM"); | |
| reject(new Error(`${command} ${args.join(" ")} timed out`)); | |
| }, timeoutMs); | |
| child.on("error", (error) => { | |
| clearTimeout(timer); | |
| reject(error); | |
| }); | |
| child.on("close", (exitCode) => { | |
| clearTimeout(timer); | |
| if (exitCode === 0) { | |
| resolve(); | |
| return; | |
| } | |
| const trimmed = output.trim(); | |
| reject( | |
| new Error( | |
| trimmed.length > 0 | |
| ? `${command} ${args.join(" ")} exited with code ${exitCode}: ${trimmed}` | |
| : `${command} ${args.join(" ")} exited with code ${exitCode}`, | |
| ), | |
| ); | |
| }); | |
| }); | |
| } | |
| async function runStep(step) { | |
| const startedAt = Date.now(); | |
| try { | |
| await runCommand(step); | |
| console.log(`${green}✓${reset} ${step.label} ${formatTiming(startedAt)}`); | |
| return null; | |
| } catch (error) { | |
| console.error(`${red}✗${reset} ${step.label} failed ${formatTiming(startedAt)}`); | |
| console.error(`${red}error${reset} (${step.label}): ${error.message}`); | |
| return error; | |
| } | |
| } | |
| const failures = (await Promise.all(steps.map(runStep))).filter(Boolean); | |
| if (failures.length > 0) { | |
| process.exitCode = 1; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | |
| BIN_DIR="$ROOT_DIR/node_modules/.bin" | |
| cd "$ROOT_DIR" | |
| if [ "${CI:-}" != "true" ]; then | |
| bun "$ROOT_DIR/scripts/sync-effect-submodule.ts" | |
| fi | |
| "$BIN_DIR/vp" config --hooks-dir .githooks | |
| "$BIN_DIR/effect-tsgo" patch |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment