@dbx-tools/projen 0.6.195 → 0.6.196

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,9 +5,12 @@ import { arch, platform } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { parseArgs } from "node:util";
8
+ import { log } from "@dbx-tools/shared-core";
8
9
  import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
9
10
  import type { RustBindingMapping, RustWorkspaceMapping } from "../src/project-rs.ts";
10
11
 
12
+ const logger = log.logger("projen:publish-uniffi-local");
13
+
11
14
  const parsed = parseArgs({
12
15
  options: {
13
16
  version: { type: "string" },
@@ -85,7 +88,16 @@ function artifacts(directory: string, suffix: string): string[] {
85
88
  .map((name) => join(directory, name));
86
89
  }
87
90
 
88
- function publishCargo(config: RustWorkspaceMapping, registry: string): void {
91
+ function cargoVersionExists(crateName: string, version: string, registry: string): boolean {
92
+ return (
93
+ spawnSync("cargo", ["info", `${crateName}@${version}`, "--registry", registry], {
94
+ cwd: repoRoot,
95
+ stdio: "ignore",
96
+ }).status === 0
97
+ );
98
+ }
99
+
100
+ function publishCargo(config: RustWorkspaceMapping, registry: string, version: string): void {
89
101
  const manifests = config.crates
90
102
  .map((crate) => resolve(repoRoot, crate, "Cargo.toml"))
91
103
  .filter((manifest) => !/^publish = false$/m.test(readFileSync(manifest, "utf8")));
@@ -115,6 +127,10 @@ function publishCargo(config: RustWorkspaceMapping, registry: string): void {
115
127
  }
116
128
  for (const crateName of crateNames) {
117
129
  if (!crateName) continue;
130
+ if (cargoVersionExists(crateName, version, registry)) {
131
+ logger.info(`skip published ${crateName} @ ${version}`);
132
+ continue;
133
+ }
118
134
  run("cargo", [
119
135
  "publish",
120
136
  "--package",
@@ -180,12 +196,18 @@ function buildAndPublish(binding: RustBindingMapping, version: string): void {
180
196
  ]);
181
197
 
182
198
  if (includeNode) {
183
- const packages = [
184
- ...artifacts(join(output, "npm"), ".tgz"),
185
- ...artifacts(join(output, "npm-facade"), ".tgz"),
186
- ];
187
- for (const packageFile of packages) {
188
- run("npm", ["publish", packageFile, "--registry", registry!]);
199
+ const publishNpmScript = resolve(dirname(fileURLToPath(import.meta.url)), "publish-npm.ts");
200
+ for (const directory of [join(output, "npm"), join(output, "npm-facade")]) {
201
+ if (artifacts(directory, ".tgz").length === 0) continue;
202
+ run(process.execPath, [
203
+ publishNpmScript,
204
+ "--directory",
205
+ directory,
206
+ "--version",
207
+ version,
208
+ "--registry",
209
+ registry!,
210
+ ]);
189
211
  }
190
212
  }
191
213
  if (includePython) {
@@ -215,5 +237,5 @@ if (config?.bindings.length) {
215
237
  }
216
238
  const cargoRegistry = parsed.values["cargo-registry"];
217
239
  if (cargoRegistry && config?.crates.length) {
218
- publishCargo(config, cargoRegistry);
240
+ publishCargo(config, cargoRegistry, version);
219
241
  }
package/tasks/publish.ts CHANGED
@@ -1,27 +1,22 @@
1
1
  #!/usr/bin/env -S bun
2
2
  /**
3
3
  * `bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run]`
4
- * - ensure every workspace member and the Bun lock carry the release version,
4
+ * - verify every workspace member and the Bun lock carry the release version,
5
5
  * then publish each package owned by the standard Node release with `bun publish`.
6
6
  *
7
7
  * Bun has no `pnpm -r publish`, so this loop is the recursive-publish stand-in.
8
8
  * It leans on native bun for everything bun already does:
9
9
  *
10
- * - **version stamping**, when needed, is `bun pm pkg set version=<version>`
11
- * per member (bun's native package.json editor - idempotent, edits only that
12
- * dir's manifest, no git side effects; `bun pm version` is for bumping and
13
- * errors on an unchanged version, so `pkg set` is the right tool for an exact
14
- * release value);
10
+ * - **version validation** fails when any member differs from the reviewed
11
+ * `VERSION`; publication never repairs committed release metadata;
15
12
  * - **`workspace:` / `catalog:` rewriting** is NOT done here - `bun publish`
16
13
  * strips both protocols in the PACKED tarball, resolving `workspace:*` to the
17
14
  * sibling's version and `catalog:` to the root catalog entry. (Verified: a
18
15
  * packed manifest shows `"@scope/x": "<version>"` and the real catalog range,
19
16
  * while the on-disk manifest keeps the protocols.) Setting each member's
20
17
  * version first is the only prerequisite, so a sibling resolves the release
21
- * version; the disk manifest already carries the workspace `VERSION`, and
22
- * this makes doubly sure it matches the value being published. When every
23
- * manifest and Bun's workspace lock already carry the version, both this
24
- * stamp and the lockfile refresh are skipped;
18
+ * version. When Bun's workspace lock already carries the version, its
19
+ * refresh is skipped;
25
20
  * - **`publishConfig` substitution** (compiled `lib/` entry points) is done
26
21
  * HERE, by {@link applyPublishConfig}, NOT by bun: unlike pnpm/npm, `bun
27
22
  * publish`/`bun pm pack` do NOT fold `publishConfig`'s `main`/`types`/`bin`/
@@ -46,14 +41,12 @@
46
41
  * non-default registry (a local verdaccio); `--exclude <dir>` (repeatable,
47
42
  * repo-relative) skips a member owned by another publication flow.
48
43
  *
49
- * `--no-restore` keeps the edits on disk instead of undoing them at exit. Only
50
- * needed when a LATER process must still see them - `--stamp-only` implies it,
51
- * since its whole job is to stamp the workspace for a `bun publish` that runs
52
- * afterwards from another directory.
44
+ * `--no-restore` keeps the transient publishConfig edits on disk instead of
45
+ * undoing them at exit.
53
46
  *
54
47
  * The disk manifests carry the workspace `VERSION` (projen owns them, read-only);
55
48
  * this unlocks each only long enough to fold in the `publishConfig` entry points
56
- * (and re-affirm the version) + publish, then RESTORES every one it touched
49
+ * and publish, then RESTORES every one it touched
57
50
  * byte-for-byte (and re-locks the mode) on the way out - see
58
51
  * {@link restoreManifests}. Restore returns each manifest to its committed
59
52
  * content, which already equals the release version, so the worktree is never
@@ -92,17 +85,9 @@ interface ManifestBackup {
92
85
  /**
93
86
  * Original state of every manifest this task has unlocked, keyed by path.
94
87
  *
95
- * Publishing has to mutate manifests projen owns (the version stamp, so a
96
- * `workspace:*` sibling resolves to the release version, and the `publishConfig`
97
- * entry-point substitution bun won't do itself). Those edits are only ever meant
98
- * to reach the packed TARBALL, so every one is undone by {@link restoreManifests}
99
- * before the process ends.
100
- *
101
- * Leaving them on disk is what used to strand a local `bun run bump` with ~34
102
- * modified `package.json` files - version stamps and `lib/` entry points - after
103
- * the release commit was already made, so the tree diverged from the commit that
104
- * was just pushed and every subsequent `git status` needed a manual revert. CI
105
- * never noticed because a fresh checkout is discarded.
88
+ * Bun does not apply `publishConfig` while packing, so publication temporarily
89
+ * projects those entry points onto projen-owned manifests. The projection is
90
+ * only meant for the packed tarball and is restored before the process exits.
106
91
  */
107
92
  const manifestBackups = new Map<string, ManifestBackup>();
108
93
 
@@ -201,11 +186,9 @@ async function runConcurrent<T>(
201
186
  }
202
187
 
203
188
  /**
204
- * Make a projen-readonly manifest writable so `bun pm pkg set` / `bun publish` can
205
- * edit it, recording its original bytes + mode for {@link restoreManifests} the
206
- * first time it is seen. Idempotent, so a manifest unlocked twice (version stamp,
207
- * then `publishConfig`) keeps its PRE-EDIT backup rather than overwriting it with
208
- * the already-stamped content.
189
+ * Make a projen-readonly manifest writable so the publishConfig projection can
190
+ * edit it, recording its original bytes and mode for {@link restoreManifests}
191
+ * the first time it is seen.
209
192
  */
210
193
  function unlockManifest(pkgPath: string): void {
211
194
  const mode = statSync(pkgPath).mode;
@@ -220,11 +203,9 @@ function unlockManifest(pkgPath: string): void {
220
203
  *
221
204
  * Runs from an `exit` handler so a clean finish and a failure partway through the
222
205
  * publish loop are covered alike - a `bun publish` that dies on package 12 of 34
223
- * must not leave the first 11 stamped. Best-effort per file: one unwritable
206
+ * must not leave the first 11 projected. Best-effort per file: one unwritable
224
207
  * manifest is logged and the rest are still restored, since a partial restore
225
208
  * beats none.
226
- *
227
- * Not registered under `--stamp-only`, where the stamps ARE the deliverable.
228
209
  */
229
210
  function restoreManifests(): void {
230
211
  for (const [pkgPath, backup] of manifestBackups) {
@@ -248,8 +229,7 @@ const PUBLISH_CONFIG_ENTRY_FIELDS = ["main", "types", "bin", "exports"] as const
248
229
  * doc). Idempotent, writes only when something changes, and leaves `publishConfig`
249
230
  * in place (npm ignores it once the top-level fields already point at `lib/`). The
250
231
  * manifest must already be unlocked. {@link restoreManifests} puts the `.ts` entry
251
- * points back at exit, so this only ever affects the packed tarball - like the
252
- * version stamp.
232
+ * points back at exit, so this only ever affects the packed tarball.
253
233
  */
254
234
  function applyPublishConfig(pkgPath: string): void {
255
235
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
@@ -292,13 +272,8 @@ if (!Number.isInteger(parsedConcurrency) || parsedConcurrency < 1) {
292
272
  throw new Error(`--concurrency must be a positive integer, got ${String(parsedConcurrency)}`);
293
273
  }
294
274
  const concurrency = parsedConcurrency;
295
- // `--stamp-only`: set versions + refresh the lockfile, then STOP (no publish).
296
- // A separate package command can use this when its own publish process needs
297
- // workspace sibling versions resolved before it starts.
298
- const stampOnly = rest.includes("--stamp-only");
299
- // Undo the manifest edits at exit unless a later process still needs them. Implied
300
- // off by `--stamp-only`, whose stamps exist precisely for a subsequent `bun publish`.
301
- const restore = !stampOnly && !rest.includes("--no-restore");
275
+ // Undo transient publishConfig edits at exit.
276
+ const restore = !rest.includes("--no-restore");
302
277
  const excluded = new Set(
303
278
  rest.reduce<string[]>((acc, arg, i) => (arg === "--exclude" ? [...acc, rest[i + 1]] : acc), []),
304
279
  );
@@ -313,18 +288,11 @@ const members = workspaceMembers(root)
313
288
  .filter((dir) => existsSync(join(dir, "package.json")))
314
289
  .filter((dir) => !excluded.has(resolve(root, dir).replace(`${resolve(root)}/`, "")));
315
290
 
316
- // Ensure every member carries the release version before packing. A normal bump
317
- // already synthesized it everywhere; dry runs and standalone invocations may not
318
- // have, so retain the native `bun pm pkg set` fallback for those paths.
319
- if (manifestsMatchVersion(members, version)) {
320
- logger.info(`all ${members.length} member manifests already carry ${version}`);
321
- } else {
322
- logger.info(`setting ${version} across ${members.length} members`);
323
- await runConcurrent(members, concurrency, async (dir) => {
324
- unlockManifest(join(dir, "package.json"));
325
- await runAsync(dir, "bun", ["pm", "pkg", "set", `version=${version}`], path);
326
- });
291
+ // Publication consumes the reviewed version state and never repairs it.
292
+ if (!manifestsMatchVersion(members, version)) {
293
+ throw new Error(`workspace manifests do not all match release version ${version}; run projen`);
327
294
  }
295
+ logger.info(`all ${members.length} member manifests carry ${version}`);
328
296
 
329
297
  // Ensure the lockfile resolves each `workspace:*` to the release version.
330
298
  // `bun publish`/`pm pack` reads workspace versions from the LOCKFILE, not just
@@ -339,11 +307,6 @@ if (lockfileMatchesVersion(root, members, version)) {
339
307
  run(root, "bun", ["install"], path);
340
308
  }
341
309
 
342
- if (stampOnly) {
343
- logger.success(`stamped ${members.length} members @ ${version} (no publish)`);
344
- process.exit(0);
345
- }
346
-
347
310
  const publishArgs = [
348
311
  "--access",
349
312
  "public",
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env -S bun
2
+ /**
3
+ * Prepare a reviewed release pull request from the current branch.
4
+ *
5
+ * Pending work is committed and the current branch is pushed first. A dedicated
6
+ * release branch then receives the VERSION change, generated files, validation,
7
+ * and local registry preflight. Public publication remains owned by the
8
+ * main-branch release workflow.
9
+ */
10
+ import { existsSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { exec, project } from "@dbx-tools/core";
14
+ import { log } from "@dbx-tools/shared-core";
15
+ import { Command, Option } from "commander";
16
+ import { publishLocalRelease } from "./local-publish.ts";
17
+ import {
18
+ type VersionLevel,
19
+ readWorkspaceVersion,
20
+ resolveNextVersion,
21
+ } from "../src/workspace-version.ts";
22
+
23
+ const logger = log.logger("projen:release");
24
+ const LEVELS = ["patch", "minor", "major"] as const;
25
+ const RELEASE_OSES = ["darwin", "linux", "win32"] as const;
26
+ const RELEASE_ARCHES = ["arm64", "x64"] as const;
27
+ type ReleaseOs = (typeof RELEASE_OSES)[number];
28
+ type ReleaseArch = (typeof RELEASE_ARCHES)[number];
29
+
30
+ function collectValue<T extends string>(value: T, previous: T[]): T[] {
31
+ return [...previous, value];
32
+ }
33
+
34
+ function git(
35
+ root: string,
36
+ args: string[],
37
+ { capture = false, check = true }: { capture?: boolean; check?: boolean } = {},
38
+ ): string {
39
+ const result = exec.spawnSync("git", args, {
40
+ cwd: root,
41
+ stdout: capture ? "capture" : "inherit",
42
+ stderr: capture ? "ignore" : "inherit",
43
+ stdin: "ignore",
44
+ check,
45
+ });
46
+ return result.stdout?.trim() ?? "";
47
+ }
48
+
49
+ function pushCurrentBranch(root: string, branch: string): void {
50
+ const upstream = git(root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], {
51
+ capture: true,
52
+ check: false,
53
+ });
54
+ git(root, upstream ? ["push"] : ["push", "--set-upstream", "origin", branch]);
55
+ }
56
+
57
+ function gitSucceeds(root: string, args: string[]): boolean {
58
+ return (
59
+ exec.spawnSync("git", args, {
60
+ cwd: root,
61
+ stdout: "ignore",
62
+ stderr: "ignore",
63
+ stdin: "ignore",
64
+ check: false,
65
+ }).exitCode === 0
66
+ );
67
+ }
68
+
69
+ function run(root: string, command: string, args: string[], env?: NodeJS.ProcessEnv): void {
70
+ exec.spawnSync(command, args, {
71
+ cwd: root,
72
+ env,
73
+ stdout: "inherit",
74
+ stderr: "inherit",
75
+ stdin: "ignore",
76
+ check: true,
77
+ });
78
+ }
79
+
80
+ function githubAccount(root: string): { owner: string; token: string } {
81
+ const repository = project.repositoryUrl(root);
82
+ if (!repository) throw new Error("Release preparation requires a GitHub repository");
83
+ const owner = new URL(repository).pathname.split("/").filter(Boolean)[0];
84
+ if (!owner) throw new Error(`Cannot determine GitHub owner from ${repository}`);
85
+ const token = exec
86
+ .spawnSync("gh", ["auth", "token", "--user", owner], {
87
+ cwd: root,
88
+ stdout: "capture",
89
+ stderr: "ignore",
90
+ stdin: "ignore",
91
+ check: true,
92
+ })
93
+ .stdout?.trim();
94
+ if (!token) throw new Error(`No GitHub CLI authentication found for ${owner}`);
95
+ return { owner, token };
96
+ }
97
+
98
+ const program = new Command();
99
+ program
100
+ .description("Prepare, validate, locally publish, and open a reviewed release PR")
101
+ .addOption(
102
+ new Option("-l, --level <level>", "semver increment").choices([...LEVELS]).default("patch"),
103
+ )
104
+ .option("--prefix <prefix>", "release tag prefix", "v")
105
+ .option("--base <branch>", "release pull request base branch", "main")
106
+ .option("--message <message>", "commit message for pending source work", "chore: prepare release")
107
+ .addOption(
108
+ new Option("--os <os>", "release operating system, repeatable; crossed with every --arch")
109
+ .choices([...RELEASE_OSES])
110
+ .argParser((value, previous: ReleaseOs[]) => collectValue(value as ReleaseOs, previous))
111
+ .default([] as ReleaseOs[]),
112
+ )
113
+ .addOption(
114
+ new Option("--arch <arch>", "release CPU architecture, repeatable; crossed with every --os")
115
+ .choices([...RELEASE_ARCHES])
116
+ .argParser((value, previous: ReleaseArch[]) => collectValue(value as ReleaseArch, previous))
117
+ .default([] as ReleaseArch[]),
118
+ )
119
+ .option("--local-registry <value>", "local npm registry: auto, false, or an explicit URL", "auto")
120
+ .option("--local-pypi <value>", "local PyPI index: auto, false, or an explicit URL", "auto")
121
+ .option("--python-root <path>", "Python workspace package root", "packages/py")
122
+ .option("--no-local-cargo", "skip local Cargo publication")
123
+ .action(
124
+ async (opts: {
125
+ level: VersionLevel;
126
+ prefix: string;
127
+ base: string;
128
+ message: string;
129
+ os: ReleaseOs[];
130
+ arch: ReleaseArch[];
131
+ localRegistry: string;
132
+ localPypi: string;
133
+ pythonRoot: string;
134
+ localCargo: boolean;
135
+ }) => {
136
+ const root = project.root() ?? process.cwd();
137
+ const currentBranch = git(root, ["branch", "--show-current"], { capture: true });
138
+ if (!currentBranch) throw new Error("Release preparation requires a local branch");
139
+ const account = githubAccount(root);
140
+ git(root, ["fetch", "--tags", "origin", opts.base]);
141
+ const next = resolveNextVersion(root, [opts.prefix], opts.level, { fetch: false });
142
+ const releaseTag = `${opts.prefix}${next.version}`;
143
+ const releaseBranch = `release/${releaseTag}`;
144
+ const resuming = currentBranch === releaseBranch;
145
+ if (currentBranch.startsWith("release/") && !resuming) {
146
+ throw new Error(`Expected release branch ${releaseBranch}, got ${currentBranch}`);
147
+ }
148
+ if (
149
+ git(root, ["ls-remote", "--tags", "origin", `refs/tags/${releaseTag}`], {
150
+ capture: true,
151
+ check: false,
152
+ })
153
+ ) {
154
+ throw new Error(`Release tag already exists: ${releaseTag}`);
155
+ }
156
+
157
+ if (!resuming) {
158
+ if (!gitSucceeds(root, ["merge-base", "--is-ancestor", `origin/${opts.base}`, "HEAD"])) {
159
+ throw new Error(`Current branch must contain origin/${opts.base}`);
160
+ }
161
+ const status = git(root, ["status", "--porcelain=v1", "--untracked-files=all"], {
162
+ capture: true,
163
+ });
164
+ if (status) {
165
+ git(root, ["add", "-A"]);
166
+ git(root, ["commit", "-m", opts.message]);
167
+ }
168
+ pushCurrentBranch(root, currentBranch);
169
+
170
+ const localBranch = git(root, ["branch", "--list", releaseBranch], { capture: true });
171
+ const remoteBranch = git(
172
+ root,
173
+ ["ls-remote", "--heads", "origin", `refs/heads/${releaseBranch}`],
174
+ { capture: true, check: false },
175
+ );
176
+ if (localBranch || remoteBranch) {
177
+ throw new Error(`Release branch already exists: ${releaseBranch}`);
178
+ }
179
+ git(root, ["switch", "--create", releaseBranch]);
180
+ } else {
181
+ logger.info(`resuming ${releaseBranch}`);
182
+ }
183
+
184
+ const bumpScript = fileURLToPath(new URL("./bump.ts", import.meta.url));
185
+ const versionCheckScript = fileURLToPath(new URL("./version-check.ts", import.meta.url));
186
+ run(root, process.execPath, [
187
+ bumpScript,
188
+ "--level",
189
+ opts.level,
190
+ "--prefix",
191
+ opts.prefix,
192
+ ...opts.os.flatMap((value) => ["--os", value]),
193
+ ...opts.arch.flatMap((value) => ["--arch", value]),
194
+ ]);
195
+ if (readWorkspaceVersion(root) !== next.version) {
196
+ throw new Error(`Release preparation did not produce ${next.version}`);
197
+ }
198
+
199
+ run(root, process.execPath, [versionCheckScript]);
200
+ if (existsSync(join(root, "Cargo.toml"))) {
201
+ run(root, "cargo", [
202
+ "test",
203
+ "--workspace",
204
+ ...(existsSync(join(root, "Cargo.lock")) ? ["--locked"] : []),
205
+ ]);
206
+ run(root, process.execPath, ["run", "rs:bindings"]);
207
+ }
208
+ run(root, process.execPath, ["run", "compile"]);
209
+ run(root, process.execPath, ["run", "test"]);
210
+ await publishLocalRelease({
211
+ root,
212
+ version: next.version,
213
+ localRegistry: opts.localRegistry,
214
+ localPypi: opts.localPypi,
215
+ pythonRoot: opts.pythonRoot,
216
+ localCargo: opts.localCargo,
217
+ });
218
+ run(root, process.execPath, [versionCheckScript]);
219
+
220
+ git(root, ["add", "-A"]);
221
+ const staged = git(root, ["diff", "--cached", "--name-only"], { capture: true });
222
+ if (staged) {
223
+ git(root, ["commit", "-m", `chore(release): ${next.version}`]);
224
+ } else if (!resuming) {
225
+ throw new Error("Release preparation produced no changes");
226
+ }
227
+ git(root, ["push", "--set-upstream", "origin", releaseBranch]);
228
+
229
+ const title = `chore(release): ${next.version}`;
230
+ const body = [
231
+ `Release ${releaseTag}.`,
232
+ "",
233
+ `Source commit: ${git(root, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
234
+ "",
235
+ "Merging this PR updates VERSION on main and starts the public release workflow.",
236
+ ].join("\n");
237
+ run(
238
+ root,
239
+ "gh",
240
+ [
241
+ "pr",
242
+ "create",
243
+ "--base",
244
+ opts.base,
245
+ "--head",
246
+ releaseBranch,
247
+ "--title",
248
+ title,
249
+ "--body",
250
+ body,
251
+ ],
252
+ { ...process.env, GH_TOKEN: account.token },
253
+ );
254
+ if (!resuming) git(root, ["switch", currentBranch]);
255
+ logger.success(`opened ${releaseBranch} for ${releaseTag}`);
256
+ },
257
+ );
258
+
259
+ await program.parseAsync();
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env -S bun
2
+ /** Verify that every committed package version matches the workspace VERSION. */
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { exec, project } from "@dbx-tools/core";
6
+ import { find } from "@dbx-tools/path";
7
+ import { json, log, object } from "@dbx-tools/shared-core";
8
+ import { parse } from "smol-toml";
9
+ import { recordedPackages } from "../src/packages.ts";
10
+ import { readWorkspaceVersion } from "../src/workspace-version.ts";
11
+
12
+ const logger = log.logger("projen:version-check");
13
+
14
+ function manifestVersion(path: string): string | undefined {
15
+ const manifest = json.parseRecord(readFileSync(path, "utf8"));
16
+ return typeof manifest?.version === "string" ? manifest.version : undefined;
17
+ }
18
+
19
+ function pythonVersion(path: string): string | undefined {
20
+ const manifest = parse(readFileSync(path, "utf8")) as {
21
+ project?: { version?: unknown };
22
+ };
23
+ return typeof manifest.project?.version === "string" ? manifest.project.version : undefined;
24
+ }
25
+
26
+ function cargoVersions(root: string): Array<{ name: string; version: string }> {
27
+ if (!existsSync(join(root, "Cargo.toml"))) return [];
28
+ const locked = existsSync(join(root, "Cargo.lock"));
29
+ const result = exec.spawnSync(
30
+ "cargo",
31
+ ["metadata", ...(locked ? ["--locked"] : []), "--format-version", "1", "--no-deps"],
32
+ {
33
+ cwd: root,
34
+ stdout: "capture",
35
+ stderr: "inherit",
36
+ stdin: "ignore",
37
+ check: true,
38
+ },
39
+ );
40
+ const metadata = json.parseRecord(result.stdout ?? "");
41
+ if (!Array.isArray(metadata?.packages)) return [];
42
+ return metadata.packages.flatMap((candidate) => {
43
+ if (
44
+ !object.isRecord(candidate) ||
45
+ typeof candidate.name !== "string" ||
46
+ typeof candidate.version !== "string"
47
+ ) {
48
+ return [];
49
+ }
50
+ return [{ name: candidate.name, version: candidate.version }];
51
+ });
52
+ }
53
+
54
+ function main(): void {
55
+ const root = project.root() ?? process.cwd();
56
+ const version = readWorkspaceVersion(root);
57
+ const mismatches: string[] = [];
58
+ const check = (name: string, actual: string | undefined): void => {
59
+ if (actual !== version) mismatches.push(`${name}: ${actual ?? "missing"} != ${version}`);
60
+ };
61
+
62
+ check("package.json", manifestVersion(join(root, "package.json")));
63
+ for (const pkg of recordedPackages(root)) {
64
+ check(`${pkg.path}/package.json`, manifestVersion(join(pkg.dir, "package.json")));
65
+ const barrel = join(pkg.dir, "index.ts");
66
+ if (existsSync(barrel)) {
67
+ const actual = /export const PACKAGE_VERSION = "([^"]+)";/.exec(
68
+ readFileSync(barrel, "utf8"),
69
+ )?.[1];
70
+ check(`${pkg.path}/index.ts`, actual);
71
+ }
72
+ }
73
+
74
+ for (const path of find.findFiles("**/pyproject.toml", { cwd: root })) {
75
+ check(path, pythonVersion(join(root, path)));
76
+ }
77
+ for (const pkg of cargoVersions(root)) check(`Cargo package ${pkg.name}`, pkg.version);
78
+
79
+ if (mismatches.length > 0) {
80
+ throw new Error(`Workspace version mismatch:\n${mismatches.join("\n")}`);
81
+ }
82
+ logger.success(`all workspace versions match ${version}`);
83
+ }
84
+
85
+ main();