170 lines · 6.3 KB · TypeScript
RawDownload
#!/usr/bin/env node import * as NodeRuntime from "@effect/platform-node/NodeRuntime";import * as NodeServices from "@effect/platform-node/NodeServices";import { Data, Effect, FileSystem, Logger, Option, Path } from "effect";import { Command, Flag } from "effect/unstable/cli";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts";import rootPackageJson from "../../../package.json" with { type: "json" };import serverPackageJson from "../package.json" with { type: "json" }; class CliError extends Data.TaggedError("CliError")<{  readonly message: string;  readonly cause?: unknown;}> {} const RepoRoot = Effect.service(Path.Path).pipe(  Effect.flatMap((path) => path.fromFileUrl(new URL("../../..", import.meta.url))),); const runCommand = Effect.fn("runCommand")(function* (command: ChildProcess.Command) {  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;  const child = yield* spawner.spawn(command);  const exitCode = yield* child.exitCode;   if (exitCode !== 0) {    return yield* new CliError({      message: `Command exited with non-zero exit code (${exitCode})`,    });  }}); // ---------------------------------------------------------------------------// build subcommand// --------------------------------------------------------------------------- const buildCmd = Command.make(  "build",  {    verbose: Flag.boolean("verbose").pipe(Flag.withDefault(false)),  },  (config) =>    Effect.gen(function* () {      const path = yield* Path.Path;      const fs = yield* FileSystem.FileSystem;      const repoRoot = yield* RepoRoot;      const serverDir = path.join(repoRoot, "apps/server");       yield* Effect.log("[cli] Running tsdown...");      yield* runCommand(        ChildProcess.make({          cwd: serverDir,          stdout: config.verbose ? "inherit" : "ignore",          stderr: "inherit",        })`bun tsdown`,      );       const webDist = path.join(repoRoot, "apps/web/dist");      const clientTarget = path.join(serverDir, "dist/client");       if (yield* fs.exists(webDist)) {        yield* fs.copy(webDist, clientTarget);        yield* Effect.log("[cli] Bundled web app into dist/client");      } else {        yield* Effect.logWarning("[cli] Web dist not found — skipping client bundle.");      }    }),).pipe(Command.withDescription("Build the server package (tsdown + bundle web client).")); // ---------------------------------------------------------------------------// publish subcommand// --------------------------------------------------------------------------- const publishCmd = Command.make(  "publish",  {    tag: Flag.string("tag").pipe(Flag.withDefault("latest")),    access: Flag.string("access").pipe(Flag.withDefault("public")),    appVersion: Flag.string("app-version").pipe(Flag.optional),    provenance: Flag.boolean("provenance").pipe(Flag.withDefault(false)),    dryRun: Flag.boolean("dry-run").pipe(Flag.withDefault(false)),    verbose: Flag.boolean("verbose").pipe(Flag.withDefault(false)),  },  (config) =>    Effect.gen(function* () {      const path = yield* Path.Path;      const fs = yield* FileSystem.FileSystem;      const repoRoot = yield* RepoRoot;      const serverDir = path.join(repoRoot, "apps/server");      const packageJsonPath = path.join(serverDir, "package.json");      const backupPath = `${packageJsonPath}.bak`;       // Assert build assets exist      for (const relPath of ["dist/index.mjs", "dist/client/index.html"]) {        const abs = path.join(serverDir, relPath);        if (!(yield* fs.exists(abs))) {          return yield* new CliError({            message: `Missing build asset: ${abs}. Run the build subcommand first.`,          });        }      }       yield* Effect.acquireUseRelease(        // Acquire: backup package.json, resolve catalog: deps, strip devDependencies/scripts        Effect.gen(function* () {          const original = yield* fs.readFileString(packageJsonPath);          yield* fs.writeFileString(backupPath, original);           // Build package.json for publish          const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);          const pkg = {            name: serverPackageJson.name,            type: serverPackageJson.type,            version,            engines: serverPackageJson.engines,            files: serverPackageJson.files,            dependencies: serverPackageJson.dependencies as Record<string, unknown>,          };           // Resolve catalog: entries in production dependencies          pkg.dependencies = resolveCatalogDependencies(            pkg.dependencies,            rootPackageJson.workspaces.catalog,            "apps/server dependencies",          );           yield* fs.writeFileString(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);          yield* Effect.log("[cli] Resolved package.json for publish");        }),        // Use: npm publish        () =>          Effect.gen(function* () {            const args = ["publish", "--access", config.access, "--tag", config.tag];            if (config.provenance) args.push("--provenance");            if (config.dryRun) args.push("--dry-run");             yield* Effect.log(`[cli] Running: npm ${args.join(" ")}`);            yield* runCommand(              ChildProcess.make("npm", [...args], {                cwd: serverDir,                stdout: config.verbose ? "inherit" : "ignore",                stderr: "inherit",              }),            );          }),        // Release: restore        () =>          Effect.gen(function* () {            yield* fs.rename(backupPath, packageJsonPath);            if (config.verbose) yield* Effect.log("[cli] Restored original package.json");          }),      );    }),).pipe(Command.withDescription("Publish the server package to npm.")); // ---------------------------------------------------------------------------// root command// --------------------------------------------------------------------------- const cli = Command.make("cli").pipe(  Command.withDescription("T3 server build & publish CLI."),  Command.withSubcommands([buildCmd, publishCmd]),); Command.run(cli, { version: "0.0.0" }).pipe(  Effect.scoped,  Effect.provide([Logger.layer([Logger.consolePretty()]), NodeServices.layer]),  NodeRuntime.runMain,);