@dbx-tools/projen 0.9.32 → 0.9.37

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.
Files changed (55) hide show
  1. package/README.md +71 -65
  2. package/index.ts +5 -8
  3. package/package.json +8 -7
  4. package/src/_task-command.ts +0 -75
  5. package/src/barrels.ts +15 -23
  6. package/src/bun-app.ts +5 -6
  7. package/src/clean.ts +41 -15
  8. package/src/codegen.ts +2 -3
  9. package/src/dbx-tools-config.ts +23 -64
  10. package/src/generated.ts +7 -1
  11. package/src/module-exports.ts +35 -3
  12. package/src/openapi.ts +34 -38
  13. package/src/packages.ts +3 -30
  14. package/src/pnpm-workspace.ts +21 -26
  15. package/src/project-js.ts +152 -365
  16. package/src/project-predicate.ts +4 -5
  17. package/src/project-py.ts +55 -144
  18. package/src/publish.ts +1 -1
  19. package/src/python-node-bindings.ts +140 -67
  20. package/src/python-node-bundle.ts +6 -42
  21. package/src/release-context.ts +4 -4
  22. package/src/release.ts +47 -85
  23. package/src/tags.ts +20 -18
  24. package/src/tsconfig.ts +9 -5
  25. package/src/vscode.ts +47 -58
  26. package/src/watch.ts +2 -8
  27. package/src/workspace-version.ts +1 -6
  28. package/tasks/barrels.ts +43 -38
  29. package/tasks/bump.ts +24 -20
  30. package/tasks/clean.ts +54 -50
  31. package/tasks/compile-workspace.ts +2 -117
  32. package/tasks/lib/compile-workspace.ts +119 -0
  33. package/tasks/lib/local-publish.ts +122 -0
  34. package/tasks/lib/publish-npm.ts +458 -0
  35. package/tasks/lib/publish-python.ts +195 -0
  36. package/tasks/lib/publish.ts +264 -0
  37. package/tasks/lib/python-node-bindings.ts +1262 -0
  38. package/tasks/{python-registry.ts → lib/python-registry.ts} +21 -30
  39. package/tasks/{python-release.ts → lib/python-release.ts} +1 -0
  40. package/tasks/lib/release-version.ts +66 -0
  41. package/tasks/lib/release.ts +212 -0
  42. package/tasks/local-publish.ts +4 -118
  43. package/tasks/openapi.ts +25 -21
  44. package/tasks/projenrc.ts +14 -12
  45. package/tasks/publish-npm.ts +3 -364
  46. package/tasks/publish-python.ts +3 -184
  47. package/tasks/publish.ts +3 -279
  48. package/tasks/python-node-bindings.ts +3 -1014
  49. package/tasks/release-version.ts +3 -62
  50. package/tasks/release.ts +3 -121
  51. package/tasks/sync.ts +71 -70
  52. package/tasks/version-check.ts +3 -10
  53. package/src/release-catalog.ts +0 -684
  54. package/tasks/databricks-cli-assets.ts +0 -129
  55. package/tasks/python-node-bindings-watch.ts +0 -47
@@ -1,65 +1,6 @@
1
1
  #!/usr/bin/env -S bun
2
- /** Validate one stable release version against the canonical workspace policy. */
3
- import { resolve } from "node:path";
4
- import { parseArgs } from "node:util";
5
- import * as projectUtils from "@dbx-tools/core/project-utils";
6
- import { captureGitTaskCommand } from "../src/_task-command.ts";
7
- import { compareSemver, parseSemver, type Semver } from "../src/workspace-version.ts";
2
+ import { main } from "./lib/release-version.ts";
8
3
 
9
- function publishedVersion(root: string, prefixes: readonly string[]): string | undefined {
10
- let best: Semver | undefined;
11
- for (const prefix of prefixes) {
12
- const tags = captureGitTaskCommand(root, [
13
- "-c",
14
- "versionsort.suffix=-",
15
- "tag",
16
- "--sort=-version:refname",
17
- "--list",
18
- `${prefix}*`,
19
- ]);
20
- for (const tag of tags.split("\n")) {
21
- if (!tag.startsWith(prefix)) continue;
22
- const version = parseSemver(tag.slice(prefix.length));
23
- if (version && (!best || compareSemver(version, best) > 0)) best = version;
24
- }
25
- }
26
- return best?.join(".");
27
- }
4
+ export * from "./lib/release-version.ts";
28
5
 
29
- export function assertReleaseVersion(
30
- version: string,
31
- options: { root?: string; prefixes?: readonly string[]; assertNext?: boolean } = {},
32
- ): void {
33
- const parsed = parseSemver(version);
34
- if (!parsed) {
35
- throw new Error(
36
- `release version must be an exact stable x.y.z, got ${JSON.stringify(version)}`,
37
- );
38
- }
39
- if (!options.assertNext) return;
40
- const root = resolve(options.root ?? projectUtils.root() ?? process.cwd());
41
- const prefixes = options.prefixes?.length ? options.prefixes : ["v"];
42
- const current = publishedVersion(root, prefixes);
43
- const currentParsed = current ? parseSemver(current) : undefined;
44
- if (currentParsed && compareSemver(parsed, currentParsed) <= 0) {
45
- throw new Error(`release version ${version} must be greater than published ${current}`);
46
- }
47
- }
48
-
49
- if (import.meta.main) {
50
- const { values } = parseArgs({
51
- options: {
52
- version: { type: "string" },
53
- root: { type: "string" },
54
- prefix: { type: "string", multiple: true },
55
- "assert-next": { type: "boolean", default: false },
56
- },
57
- strict: true,
58
- });
59
- if (!values.version) throw new Error("--version is required");
60
- assertReleaseVersion(values.version, {
61
- ...(values.root ? { root: values.root } : {}),
62
- ...(values.prefix ? { prefixes: values.prefix } : {}),
63
- assertNext: values["assert-next"],
64
- });
65
- }
6
+ if (import.meta.main) main();
package/tasks/release.ts CHANGED
@@ -1,124 +1,6 @@
1
1
  #!/usr/bin/env -S bun
2
- /** Run the configured release transaction. */
3
- import * as projectUtils from "@dbx-tools/core/project-utils";
4
- import { log } from "@dbx-tools/shared-core";
5
- import { Command } from "commander";
6
- import {
7
- captureGitTaskCommand,
8
- gitTaskCommandSucceeds,
9
- runGitTaskCommand,
10
- runTaskCommand,
11
- } from "../src/_task-command.ts";
12
- import { assertReleaseVersion } from "./release-version.ts";
13
- import { publishLocalRelease } from "./local-publish.ts";
14
- import { readWorkspaceVersion } from "../src/workspace-version.ts";
2
+ import { main } from "./lib/release.ts";
15
3
 
16
- const logger = log.logger("projen:release");
4
+ export { main, runRelease } from "./lib/release.ts";
17
5
 
18
- export async function runRelease(options: {
19
- readonly root: string;
20
- readonly branch: string;
21
- readonly bump?: boolean;
22
- readonly prefix: string;
23
- readonly remote: string;
24
- readonly localPublish?: boolean;
25
- readonly pythonRoot?: string;
26
- }): Promise<string> {
27
- const { branch, prefix, remote, root } = options;
28
-
29
- // Release runs from any branch: whatever is checked out is fast-forwarded onto
30
- // `branch`. The working tree is committed into the release commit unless it is
31
- // already clean - the clean tree is a convenience, not a precondition.
32
- if (options.bump ?? true) {
33
- runTaskCommand(root, "bun", ["run", "bump"]);
34
- }
35
-
36
- const version = readWorkspaceVersion(root);
37
- const tag = `${prefix}${version}`;
38
- runGitTaskCommand(root, ["fetch", remote, branch, "--tags"]);
39
- assertReleaseVersion(version, { root, prefixes: [prefix], assertNext: true });
40
-
41
- if (gitTaskCommandSucceeds(root, ["rev-parse", "--verify", `refs/tags/${tag}`])) {
42
- throw new Error(`release tag ${tag} already exists`);
43
- }
44
-
45
- runTaskCommand(root, "bun", ["run", "version:check"]);
46
- const status = captureGitTaskCommand(
47
- root,
48
- ["status", "--porcelain=v1", "--untracked-files=all"],
49
- { check: true },
50
- );
51
- if (status) {
52
- if (gitTaskCommandSucceeds(root, ["diff", "--quiet", "HEAD", "--", "VERSION"])) {
53
- throw new Error("release changes must include the local VERSION bump");
54
- }
55
- runGitTaskCommand(root, ["add", "--all"]);
56
- runGitTaskCommand(root, ["commit", "-m", `chore(release): ${version}`]);
57
- }
58
-
59
- // Fast-forward only: HEAD must already contain the remote branch tip, otherwise
60
- // the push would be a non-fast-forward (an unclean merge) and we fail instead
61
- // of force-pushing. This holds regardless of which branch is checked out.
62
- const remoteHead = captureGitTaskCommand(root, ["rev-parse", `${remote}/${branch}`], {
63
- check: true,
64
- });
65
- const head = captureGitTaskCommand(root, ["rev-parse", "HEAD"], { check: true });
66
- if (!gitTaskCommandSucceeds(root, ["merge-base", "--is-ancestor", remoteHead, head])) {
67
- throw new Error(
68
- `cannot fast-forward ${remote}/${branch} to HEAD; ${branch} has diverged (rebase before release)`,
69
- );
70
- }
71
-
72
- runGitTaskCommand(root, ["push", remote, `HEAD:${branch}`]);
73
- runGitTaskCommand(root, ["tag", "--annotate", tag, "--message", tag]);
74
- runGitTaskCommand(root, ["push", remote, `refs/tags/${tag}`]);
75
- logger.success(`released ${tag}`, { sha: head });
76
-
77
- // Restore local deploys: when a non-standard (loopback) npm or uv registry is
78
- // configured, publish the freshly tagged version to it directly. Detection and
79
- // publishing are owned by local-publish.ts; this no-ops on standard registries.
80
- if (options.localPublish ?? true) {
81
- await publishLocalRelease({
82
- localPypi: "auto",
83
- localRegistry: "auto",
84
- pythonRoot: options.pythonRoot ?? "packages/py",
85
- root,
86
- version,
87
- });
88
- }
89
-
90
- return tag;
91
- }
92
-
93
- if (import.meta.main) {
94
- await new Command()
95
- .option("--root <path>", "repository root")
96
- .option("--branch <name>", "release branch", "main")
97
- .option("--prefix <prefix>", "release tag prefix", "v")
98
- .option("--remote <name>", "git remote", "origin")
99
- .option("--python-root <path>", "Python package root for local publish", "packages/py")
100
- .option("--no-bump", "use an existing synchronized local version bump")
101
- .option("--no-local-publish", "skip publishing to configured local registries")
102
- .action(
103
- async (options: {
104
- root?: string;
105
- branch: string;
106
- bump: boolean;
107
- prefix: string;
108
- remote: string;
109
- pythonRoot: string;
110
- localPublish: boolean;
111
- }) => {
112
- await runRelease({
113
- root: options.root ?? projectUtils.root() ?? process.cwd(),
114
- branch: options.branch,
115
- bump: options.bump,
116
- prefix: options.prefix,
117
- remote: options.remote,
118
- pythonRoot: options.pythonRoot,
119
- localPublish: options.localPublish,
120
- });
121
- },
122
- )
123
- .parseAsync();
124
- }
6
+ if (import.meta.main) await main();
package/tasks/sync.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { log } from "@dbx-tools/shared-core";
4
4
  import concurrently from "concurrently";
5
- import { pythonNodeBindingProjects, repoRoot } from "../src/packages.ts";
5
+ import { repoRoot } from "../src/packages.ts";
6
6
  import { runSynth } from "../src/scaffold.ts";
7
7
  import { withWorkspaceMutationLock } from "../src/workspace-lock.ts";
8
8
 
@@ -35,79 +35,80 @@ function watcher(script: string, name: string, prefixColor: string, ...args: str
35
35
  };
36
36
  }
37
37
 
38
- if (!process.argv.includes("--watch")) {
39
- // One-shot: full synth (+install + barrels via the post-synth component). This is
40
- // the scriptable path, so a failed synth stays a failed exit code.
41
- logger.start("synthesizing");
42
- await withWorkspaceMutationLock(repoRoot, () => runSynth({ post: true }));
43
- logger.success("synced");
44
- } else {
45
- // Watch: one initial full synth to bring the tree up to date, then focused
46
- // watchers under `concurrently`. The projenrc watcher is the intelligent stand-in
47
- // for stock `projen --watch` - it re-synths (+install) ONLY when `.projenrc.ts` or
48
- // a configured `syncResynthPaths` entry changes, while barrels/openapi keep generated
49
- // OUTPUT fresh on source edits with no full synth.
50
- //
51
- // VS Code auto-runs this on folder open, so from here down nothing is allowed to be
52
- // fatal: errors are logged and retried, and only a stop signal ends the task.
53
- logger.start("initial sync");
54
- try {
38
+ export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
39
+ if (!args.includes("--watch")) {
40
+ // One-shot: full synth (+install + barrels via the post-synth component). This is
41
+ // the scriptable path, so a failed synth stays a failed exit code.
42
+ logger.start("synthesizing");
55
43
  await withWorkspaceMutationLock(repoRoot, () => runSynth({ post: true }));
56
- logger.success("synced - watching (Ctrl-C to stop)");
57
- } catch (err) {
58
- // A tree that doesn't synth is precisely when the watcher is most useful: the edit
59
- // that repairs it is the one the projenrc watcher is sitting there waiting for.
60
- logger.error(
61
- "initial sync failed - watching anyway:",
62
- err instanceof Error ? err.message : err,
63
- );
64
- }
44
+ logger.success("synced");
45
+ } else {
46
+ // Watch: one initial full synth to bring the tree up to date, then focused
47
+ // watchers under `concurrently`. The projenrc watcher is the intelligent stand-in
48
+ // for stock `projen --watch` - it re-synths (+install) ONLY when `.projenrc.ts` or
49
+ // a configured `syncResynthPaths` entry changes, while barrels/openapi keep generated
50
+ // OUTPUT fresh on source edits with no full synth.
51
+ //
52
+ // VS Code auto-runs this on folder open, so from here down nothing is allowed to be
53
+ // fatal: errors are logged and retried, and only a stop signal ends the task.
54
+ logger.start("initial sync");
55
+ try {
56
+ await withWorkspaceMutationLock(repoRoot, () => runSynth({ post: true }));
57
+ logger.success("synced - watching (Ctrl-C to stop)");
58
+ } catch (err) {
59
+ // A tree that doesn't synth is precisely when the watcher is most useful: the edit
60
+ // that repairs it is the one the projenrc watcher is sitting there waiting for.
61
+ logger.error(
62
+ "initial sync failed - watching anyway:",
63
+ err instanceof Error ? err.message : err,
64
+ );
65
+ }
65
66
 
66
- const watchers = [
67
- watcher("projenrc.ts", "projenrc", "magenta"),
68
- watcher("barrels.ts", "barrels", "cyan", "--watch"),
69
- watcher("openapi.ts", "openapi", "green", "--watch"),
70
- ];
71
- if (pythonNodeBindingProjects(repoRoot).length > 0) {
72
- watchers.push(watcher("python-node-bindings-watch.ts", "node-bindings", "blue"));
73
- }
74
- const { result } = concurrently(watchers, {
75
- prefix: "name",
76
- // No `killOthersOn`: one watcher falling over is no reason to tear the other two
77
- // down. `-1` is concurrently's spelling for "restart forever", so a crashed
78
- // watcher comes back instead of silently leaving its outputs stale.
79
- restartTries: -1,
80
- restartDelay: RESTART_DELAY_MS,
81
- });
67
+ const watchers = [
68
+ watcher("projenrc.ts", "projenrc", "magenta"),
69
+ watcher("barrels.ts", "barrels", "cyan", "--watch"),
70
+ watcher("openapi.ts", "openapi", "green", "--watch"),
71
+ ];
72
+ const { result } = concurrently(watchers, {
73
+ prefix: "name",
74
+ // No `killOthersOn`: one watcher falling over is no reason to tear the other two
75
+ // down. `-1` is concurrently's spelling for "restart forever", so a crashed
76
+ // watcher comes back instead of silently leaving its outputs stale.
77
+ restartTries: -1,
78
+ restartDelay: RESTART_DELAY_MS,
79
+ });
82
80
 
83
- let stopping = false;
81
+ let stopping = false;
84
82
 
85
- /**
86
- * Wind the watchers down for good.
87
- *
88
- * Restarting forever means an ordinary SIGTERM would be answered by respawning every
89
- * watcher, so the task has to opt out explicitly. SIGINT is the one signal concurrently
90
- * neutralizes - it rewrites that exit to 0 before the restart controller sees it - so
91
- * re-emitting it is how we say "stop" in a language the supervisor understands.
92
- *
93
- * Killing is asynchronous (concurrently shells out to `ps` to walk each process tree)
94
- * and can outlive `result`. The timer keeps the process open until the kills
95
- * land, which prevents orphaned watchers after SIGTERM. It also stops waiting
96
- * if one of them wedges.
97
- */
98
- function stop(): void {
99
- if (stopping) return;
100
- stopping = true;
101
- process.emit("SIGINT", "SIGINT");
102
- setTimeout(() => process.exit(0), STOP_GRACE_MS);
103
- }
83
+ /**
84
+ * Wind the watchers down for good.
85
+ *
86
+ * Restarting forever means an ordinary SIGTERM would be answered by respawning every
87
+ * watcher, so the task has to opt out explicitly. SIGINT is the one signal concurrently
88
+ * neutralizes - it rewrites that exit to 0 before the restart controller sees it - so
89
+ * re-emitting it is how we say "stop" in a language the supervisor understands.
90
+ *
91
+ * Killing is asynchronous (concurrently shells out to `ps` to walk each process tree)
92
+ * and can outlive `result`. The timer keeps the process open until the kills
93
+ * land, which prevents orphaned watchers after SIGTERM. It also stops waiting
94
+ * if one of them wedges.
95
+ */
96
+ function stop(): void {
97
+ if (stopping) return;
98
+ stopping = true;
99
+ process.emit("SIGINT", "SIGINT");
100
+ setTimeout(() => process.exit(0), STOP_GRACE_MS);
101
+ }
104
102
 
105
- // Registered after `concurrently()` so its own signal handler - the one that actually
106
- // kills the children - is already in place by the time ours can fire.
107
- for (const signal of ["SIGTERM", "SIGHUP"] as const) process.on(signal, stop);
103
+ // Registered after `concurrently()` so its own signal handler - the one that actually
104
+ // kills the children - is already in place by the time ours can fire.
105
+ for (const signal of ["SIGTERM", "SIGHUP"] as const) process.on(signal, stop);
108
106
 
109
- // With infinite restarts this settles only once a stop signal has wound the watchers
110
- // down, so there is no failure left to report. When `stop()` is driving, the pending
111
- // grace timer keeps us alive past this point and owns the exit.
112
- await result.catch(() => {});
107
+ // With infinite restarts this settles only once a stop signal has wound the watchers
108
+ // down, so there is no failure left to report. When `stop()` is driving, the pending
109
+ // grace timer keeps us alive past this point and owns the exit.
110
+ await result.catch(() => {});
111
+ }
113
112
  }
113
+
114
+ if (import.meta.main) await main();
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env -S bun
2
2
  /** Verify that every committed package version matches the root VERSION file. */
3
- import { existsSync, readFileSync } from "node:fs";
3
+ import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import * as projectUtils from "@dbx-tools/core/project-utils";
6
6
  import { find } from "@dbx-tools/path";
@@ -23,7 +23,7 @@ function pythonVersion(path: string): string | undefined {
23
23
  return typeof manifest.project?.version === "string" ? manifest.project.version : undefined;
24
24
  }
25
25
 
26
- function main(): void {
26
+ export function main(): void {
27
27
  const root = projectUtils.root() ?? process.cwd();
28
28
  const version = readWorkspaceVersion(root);
29
29
  const mismatches: string[] = [];
@@ -34,13 +34,6 @@ function main(): void {
34
34
  check("package.json", manifestVersion(join(root, "package.json")));
35
35
  for (const pkg of recordedPackages(root)) {
36
36
  check(`${pkg.path}/package.json`, manifestVersion(join(pkg.dir, "package.json")));
37
- const barrel = join(pkg.dir, "index.ts");
38
- if (existsSync(barrel)) {
39
- const actual = /export const PACKAGE_VERSION = "([^"]+)";/.exec(
40
- readFileSync(barrel, "utf8"),
41
- )?.[1];
42
- check(`${pkg.path}/index.ts`, actual);
43
- }
44
37
  }
45
38
 
46
39
  for (const path of find.findFiles("**/pyproject.toml", { cwd: root })) {
@@ -52,4 +45,4 @@ function main(): void {
52
45
  logger.success(`all workspace package versions match ${version}`);
53
46
  }
54
47
 
55
- main();
48
+ if (import.meta.main) main();