Skip to content

Commit 11606fd

Browse files
cli for pub
1 parent a935db0 commit 11606fd

File tree

10 files changed

+225
-53
lines changed

10 files changed

+225
-53
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,11 @@ jobs:
273273
run: bun run build --filter=@t3tools/web --filter=t3
274274

275275
- name: Publish CLI package
276-
run: bun publish --access public --tag alpha --ignore-scripts
277-
working-directory: apps/server
276+
run: node apps/server/scripts/cli.ts publish --tag alpha --provenance
278277

279278
release:
280279
name: Publish GitHub Release
281-
needs: [preflight, build]
282-
# needs: [preflight, build, publish_cli]
280+
needs: [preflight, build, publish_cli]
283281
runs-on: ubuntu-24.04
284282
steps:
285283
- name: Download all desktop artifacts

apps/server/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88
"dist"
99
],
1010
"type": "module",
11-
"main": "./dist/index.mjs",
1211
"scripts": {
1312
"dev": "VITE_DEV_SERVER_URL=${VITE_DEV_SERVER_URL:-http://localhost:5733} bun run src/index.ts",
14-
"build": "tsdown && node scripts/bundle-client.ts",
13+
"build": "node scripts/cli.ts build",
1514
"start": "node dist/index.mjs",
1615
"prepare": "effect-language-service patch",
1716
"typecheck": "tsc --noEmit",
@@ -38,5 +37,8 @@
3837
"tsdown": "^0.20.3",
3938
"typescript": "^5.7.3",
4039
"vitest": "^4.0.0"
40+
},
41+
"engines": {
42+
"node": "^22.13 || ^23.4 || >=24.10"
4143
}
4244
}

apps/server/scripts/bundle-client.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

apps/server/scripts/cli.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env node
2+
3+
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4+
import * as NodeServices from "@effect/platform-node/NodeServices";
5+
import { Data, Effect, FileSystem, Logger, Path } from "effect";
6+
import { Command, Flag } from "effect/unstable/cli";
7+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
8+
9+
import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts";
10+
import rootPackageJson from "../../../package.json" with { type: "json" };
11+
import serverPackageJson from "../package.json" with { type: "json" };
12+
13+
class CliError extends Data.TaggedError("CliError")<{
14+
readonly message: string;
15+
readonly cause?: unknown;
16+
}> {}
17+
18+
const RepoRoot = Effect.service(Path.Path).pipe(
19+
Effect.flatMap((path) => path.fromFileUrl(new URL("../../..", import.meta.url))),
20+
);
21+
22+
const runCommand = Effect.fn(function* (command: ChildProcess.Command) {
23+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
24+
const child = yield* spawner.spawn(command);
25+
const exitCode = yield* child.exitCode;
26+
27+
if (exitCode !== 0) {
28+
return yield* new CliError({
29+
message: `Command exited with non-zero exit code (${exitCode})`,
30+
});
31+
}
32+
});
33+
34+
// ---------------------------------------------------------------------------
35+
// build subcommand
36+
// ---------------------------------------------------------------------------
37+
38+
const buildCmd = Command.make(
39+
"build",
40+
{
41+
verbose: Flag.boolean("verbose").pipe(Flag.withDefault(false)),
42+
},
43+
(config) =>
44+
Effect.gen(function* () {
45+
const path = yield* Path.Path;
46+
const fs = yield* FileSystem.FileSystem;
47+
const repoRoot = yield* RepoRoot;
48+
const serverDir = path.join(repoRoot, "apps/server");
49+
50+
yield* Effect.log("[cli] Running tsdown...");
51+
yield* runCommand(
52+
ChildProcess.make({
53+
cwd: serverDir,
54+
stdout: config.verbose ? "inherit" : "ignore",
55+
stderr: "inherit",
56+
})`tsdown`,
57+
);
58+
59+
const webDist = path.join(repoRoot, "apps/web/dist");
60+
const clientTarget = path.join(serverDir, "dist/client");
61+
62+
if (yield* fs.exists(webDist)) {
63+
yield* fs.copy(webDist, clientTarget);
64+
yield* Effect.log("[cli] Bundled web app into dist/client");
65+
} else {
66+
yield* Effect.log("[cli] Web dist not found — skipping client bundle.");
67+
}
68+
}),
69+
).pipe(Command.withDescription("Build the server package (tsdown + bundle web client)."));
70+
71+
// ---------------------------------------------------------------------------
72+
// publish subcommand
73+
// ---------------------------------------------------------------------------
74+
75+
const publishCmd = Command.make(
76+
"publish",
77+
{
78+
tag: Flag.string("tag").pipe(Flag.withDefault("latest")),
79+
access: Flag.string("access").pipe(Flag.withDefault("public")),
80+
provenance: Flag.boolean("provenance").pipe(Flag.withDefault(false)),
81+
dryRun: Flag.boolean("dry-run").pipe(Flag.withDefault(false)),
82+
verbose: Flag.boolean("verbose").pipe(Flag.withDefault(false)),
83+
},
84+
(config) =>
85+
Effect.gen(function* () {
86+
const path = yield* Path.Path;
87+
const fs = yield* FileSystem.FileSystem;
88+
const repoRoot = yield* RepoRoot;
89+
const serverDir = path.join(repoRoot, "apps/server");
90+
const packageJsonPath = path.join(serverDir, "package.json");
91+
const backupPath = `${packageJsonPath}.bak`;
92+
93+
// Assert build assets exist
94+
for (const relPath of ["dist/index.mjs", "dist/client/index.html"]) {
95+
const abs = path.join(serverDir, relPath);
96+
if (!(yield* fs.exists(abs))) {
97+
return yield* new CliError({
98+
message: `Missing build asset: ${abs}. Run the build subcommand first.`,
99+
});
100+
}
101+
}
102+
103+
yield* Effect.acquireUseRelease(
104+
// Acquire: backup package.json, resolve catalog: deps, strip devDependencies/scripts
105+
Effect.gen(function* () {
106+
const original = yield* fs.readFileString(packageJsonPath);
107+
yield* fs.writeFileString(backupPath, original);
108+
109+
// Build package.json for publish
110+
const pkg = {
111+
name: serverPackageJson.name,
112+
type: serverPackageJson.type,
113+
version: serverPackageJson.version,
114+
engines: serverPackageJson.engines,
115+
files: serverPackageJson.files,
116+
dependencies: serverPackageJson.dependencies as Record<string, unknown>,
117+
};
118+
119+
// Resolve catalog: entries in production dependencies
120+
pkg.dependencies = resolveCatalogDependencies(
121+
pkg.dependencies,
122+
rootPackageJson.workspaces.catalog,
123+
"apps/server dependencies",
124+
);
125+
126+
yield* fs.writeFileString(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
127+
yield* Effect.log("[cli] Resolved package.json for publish");
128+
}),
129+
// Use: npm publish
130+
() =>
131+
Effect.gen(function* () {
132+
const args = ["publish", "--access", config.access, "--tag", config.tag];
133+
if (config.provenance) args.push("--provenance");
134+
if (config.dryRun) args.push("--dry-run");
135+
136+
yield* Effect.log(`[cli] Running: npm ${args.join(" ")}`);
137+
yield* runCommand(
138+
ChildProcess.make("npm", args, {
139+
cwd: serverDir,
140+
stdout: config.verbose ? "inherit" : "ignore",
141+
stderr: "inherit",
142+
}),
143+
);
144+
}),
145+
// Release: restore
146+
() =>
147+
Effect.gen(function* () {
148+
yield* fs.rename(backupPath, packageJsonPath);
149+
if (config.verbose) yield* Effect.log("[cli] Restored original package.json");
150+
}),
151+
);
152+
}),
153+
).pipe(Command.withDescription("Publish the server package to npm."));
154+
155+
// ---------------------------------------------------------------------------
156+
// root command
157+
// ---------------------------------------------------------------------------
158+
159+
const cli = Command.make("cli").pipe(
160+
Command.withDescription("T3 server build & publish CLI."),
161+
Command.withSubcommands([buildCmd, publishCmd]),
162+
);
163+
164+
Command.run(cli, { version: "0.0.0" }).pipe(
165+
Effect.scoped,
166+
Effect.provide([Logger.layer([Logger.consolePretty()]), NodeServices.layer]),
167+
NodeRuntime.runMain,
168+
);

apps/server/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@
2121
}
2222
]
2323
},
24-
"include": ["src", "tsdown.config.ts", "scripts", "integration"]
24+
"include": ["src", "tsdown.config.ts", "scripts", "integration", "../../scripts/lib"]
2525
}

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"scripts": {
77
"dev": "vite",
88
"build": "vite build",
9+
"prepare": "effect-language-service patch",
910
"preview": "vite preview",
1011
"typecheck": "tsc --noEmit",
1112
"test": "vitest run --passWithNoTests"

bun.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/shared/package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,17 @@
1010
}
1111
},
1212
"scripts": {
13-
"typecheck": "tsc --noEmit"
13+
"prepare": "effect-language-service patch",
14+
"typecheck": "tsc --noEmit",
15+
"test": "vitest run --passWithNoTests"
16+
},
17+
"dependencies": {
18+
"effect": "catalog:"
1419
},
1520
"devDependencies": {
16-
"typescript": "^5.7.3"
21+
"@effect/language-service": "0.75.1",
22+
"@effect/vitest": "catalog:",
23+
"typescript": "^5.7.3",
24+
"vitest": "^4.0.0"
1725
}
1826
}

scripts/build-desktop-artifact.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import rootPackageJson from "../package.json" with { type: "json" };
66
import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };
77
import serverPackageJson from "../apps/server/package.json" with { type: "json" };
88

9+
import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
10+
911
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
1012
import * as NodeServices from "@effect/platform-node/NodeServices";
1113
import { Config, Data, Effect, FileSystem, Logger, Option, Path, Schema } from "effect";
@@ -319,31 +321,6 @@ function validateBundledClientAssets(clientDir: string) {
319321
});
320322
}
321323

322-
function resolveCatalogDependencies(
323-
dependencies: Record<string, unknown>,
324-
catalog: Record<string, unknown>,
325-
dependencySourceLabel: string,
326-
): Record<string, unknown> {
327-
return Object.fromEntries(
328-
Object.entries(dependencies).map(([dependencyName, spec]) => {
329-
if (typeof spec !== "string" || !spec.startsWith("catalog:")) {
330-
return [dependencyName, spec];
331-
}
332-
333-
const catalogKey = spec.slice("catalog:".length).trim();
334-
const lookupKey = catalogKey.length > 0 ? catalogKey : dependencyName;
335-
const resolvedSpec = catalog[lookupKey];
336-
if (typeof resolvedSpec !== "string" || resolvedSpec.length === 0) {
337-
throw new BuildScriptError({
338-
message: `Unable to resolve '${spec}' for ${dependencySourceLabel} dependency '${dependencyName}'. Expected key '${lookupKey}' in root workspace catalog.`,
339-
});
340-
}
341-
342-
return [dependencyName, resolvedSpec];
343-
}),
344-
);
345-
}
346-
347324
const createBuildConfig = Effect.fn(function* (
348325
platform: typeof BuildPlatform.Type,
349326
target: string,

scripts/lib/resolve-catalog.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Resolve `catalog:` dependency specs using the workspace catalog.
3+
*
4+
* Pure function: returns a new record with every `catalog:…` value replaced by
5+
* the concrete version string found in `catalog`. Throws on missing entries.
6+
*/
7+
export function resolveCatalogDependencies(
8+
dependencies: Record<string, unknown>,
9+
catalog: Record<string, unknown>,
10+
label: string,
11+
): Record<string, unknown> {
12+
return Object.fromEntries(
13+
Object.entries(dependencies).map(([name, spec]) => {
14+
if (typeof spec !== "string" || !spec.startsWith("catalog:")) {
15+
return [name, spec];
16+
}
17+
18+
const catalogKey = spec.slice("catalog:".length).trim();
19+
const lookupKey = catalogKey.length > 0 ? catalogKey : name;
20+
const resolved = catalog[lookupKey];
21+
22+
if (typeof resolved !== "string" || resolved.length === 0) {
23+
throw new Error(
24+
`Unable to resolve '${spec}' for ${label} dependency '${name}'. Expected key '${lookupKey}' in root workspace catalog.`,
25+
);
26+
}
27+
28+
return [name, resolved];
29+
}),
30+
);
31+
}

0 commit comments

Comments
 (0)