Skip to content
Source
Sign inSign up

build-npm-launcher.ts

131 lines5.3 KB

1#!/usr/bin/env node
2// GitHub archives avoid npm scanning delays; output <output-dir>/t3/ and publishable t3.tgz.
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";
8import * as Logger from "effect/Logger";
9import * as Path from "effect/Path";
10import * as Schema from "effect/Schema";
11import { Command, Flag } from "effect/cli";
12import { ChildProcess, ChildProcessSpawner } from "effect/process";
13​
14import * as HostProcess from "@t3tools/shared/HostProcess";
15import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
16import serverPackageJson from "../apps/server/package.json" with { type: "json" };
17​
18import { windowsSystemTar } from "./lib/windows-tar.ts";
19​
20export const NPM_LAUNCHER_PACKAGE_NAME = "t3";
21​
22const encodePackageJson = Schema.encodeEffect(fromJsonStringPretty(Schema.Unknown));
23​
24export class NpmLauncherCommandFailedError extends Schema.TaggedError<NpmLauncherCommandFailedError>()(
25 "NpmLauncherCommandFailedError",
26 { command: Schema.String, exitCode: Schema.Int },
27) {
28 override get message(): string {
29 return `${this.command} exited with code ${this.exitCode}.`;
30 }
32​
33/** engines requires import.meta.dirname and fetch. `t3code` is read back by npmReleaseDatabaseMigration. */
34export function npmLauncherPackageManifest(version: string, databaseMigration: number) {
35 return {
36 name: NPM_LAUNCHER_PACKAGE_NAME,
37 version,
38 description:
39 "T3 Code CLI. Downloads the self-contained executable for this platform from the GitHub Release.",
40 license: serverPackageJson.license,
41 repository: serverPackageJson.repository,
42 bin: { t3: "./bin/t3.mjs" },
43 files: ["bin", "dist"],
44 engines: { node: ">=20.11" },
45 t3code: { databaseMigration },
46 };
48​
49/** Highest server migration id, from the `NNN_Name.ts` files the server registers. */
50const latestServerMigrationId = Effect.fn("latestServerMigrationId")(function* () {
51 const fs = yield* FileSystem.FileSystem;
52 const path = yield* Path.Path;
53 const dir = yield* path.fromFileUrl(
54 new URL("../apps/server/src/persistence/Migrations/", import.meta.url),
55 );
56 const ids = (yield* fs.readDirectory(dir)).flatMap((name) => {
57 const id = /^(\d+)_\w+\.ts$/.exec(name)?.[1];
58 return id === undefined ? [] : [Number(id)];
59 });
60 return Math.max(0, ...ids);
61});
62​
63export const buildNpmLauncher = Effect.fn("buildNpmLauncher")(function* (input: {
64 readonly version: string;
65 readonly outputDir: string;
66}) {
67 const fs = yield* FileSystem.FileSystem;
68 const path = yield* Path.Path;
69 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
70 const sourceDir = yield* path.fromFileUrl(new URL("./npm-launcher/", import.meta.url));
71​
72 yield* fs.makeDirectory(input.outputDir, { recursive: true });
73 const scratch = yield* fs.makeTempDirectoryScoped({
74 directory: input.outputDir,
75 prefix: ".launcher-",
76 });
77 const stageDir = path.join(scratch, "package");
78 yield* fs.makeDirectory(path.join(stageDir, "bin"), { recursive: true });
79 yield* fs.makeDirectory(path.join(stageDir, "dist"));
80 yield* fs.writeFileString(
81 path.join(stageDir, "package.json"),
82 `${yield* encodePackageJson(
83 npmLauncherPackageManifest(input.version, yield* latestServerMigrationId()),
84 )}\n`,
85 );
86 yield* fs.copyFile(path.join(sourceDir, "t3.mjs"), path.join(stageDir, "bin/t3.mjs"));
87 yield* fs.chmod(path.join(stageDir, "bin/t3.mjs"), 0o755);
88 yield* fs.copyFile(path.join(sourceDir, "bin.mjs"), path.join(stageDir, "dist/bin.mjs"));
89 const readme = yield* path.fromFileUrl(new URL("../apps/server/README.md", import.meta.url));
90 if (yield* fs.exists(readme)) {
91 yield* fs.copyFile(readme, path.join(stageDir, "README.md"));
92 }
93​
94 const packageDir = path.join(input.outputDir, NPM_LAUNCHER_PACKAGE_NAME);
95 const tarball = path.join(input.outputDir, `${NPM_LAUNCHER_PACKAGE_NAME}.tgz`);
96 yield* fs.remove(tarball, { force: true });
97 const tar = (yield* HostProcess.Platform) === "win32" ? windowsSystemTar() : "tar";
98 // npm tarballs root everything under `package/`.
99 const child = yield* spawner.spawn(
100 ChildProcess.make(tar, ["-czf", tarball, "-C", scratch, "package"], {
101 stdout: "inherit",
102 stderr: "inherit",
103 }),
104 );
105 const exitCode = Number(yield* child.exitCode);
106 if (exitCode !== 0) {
107 return yield* new NpmLauncherCommandFailedError({ command: `tar (${tarball})`, exitCode });
108 }
109 yield* fs.remove(packageDir, { recursive: true, force: true });
110 yield* fs.rename(stageDir, packageDir);
111 yield* Effect.log(`[npm-launcher] Wrote ${packageDir} and ${tarball}`);
112 return { packageDir, tarball };
113}, Effect.scoped);
114​
115const command = Command.make(
116 "build-npm-launcher",
117 {
118 version: Flag.String("version").pipe(
119 Flag.withDescription("Exact release version; the launcher downloads this version's archive."),
120 ),
121 outputDir: Flag.String("output-dir").pipe(Flag.withDefault("npm-packages")),
122 },
123 buildNpmLauncher,
124).pipe(Command.withDescription("Build the t3 npm launcher package."));
125​
126if (import.meta.main) {
127 Command.run(command, { version: "0.0.0" }).pipe(
128 Effect.provide(Layer.mergeAll(Logger.layer([Logger.consolePretty()]), NodeServices.layer)),
129 NodeRuntime.runMain,
130 );