@dbx-tools/projen 0.9.32 → 0.9.36

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 +88 -86
  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
@@ -2,24 +2,25 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { resolve } from "node:path";
4
4
  import { net } from "@dbx-tools/shared-core";
5
- import { captureTaskCommand } from "../src/_task-command.ts";
5
+ import { parse } from "smol-toml";
6
+ import { captureTaskCommand } from "../../src/_task-command.ts";
6
7
 
7
8
  export interface LocalPythonRegistry {
8
9
  readonly indexUrl: string;
9
10
  readonly publishUrl: string;
10
11
  }
11
12
 
12
- /** Read the default index URL from uv's TOML configuration. */
13
- export function parseUvDefaultIndex(source: string): string | undefined {
14
- const blocks = source.split(/(?=^\[\[index\]\]\s*$)/m);
15
- for (const block of blocks) {
16
- if (!/^\[\[index\]\]\s*$/m.test(block) || !/^\s*default\s*=\s*true\s*$/m.test(block)) {
17
- continue;
18
- }
19
- const url = /^\s*url\s*=\s*["']([^"']+)["']\s*$/m.exec(block)?.[1];
20
- if (url) return url;
21
- }
22
- return undefined;
13
+ interface UvIndex {
14
+ readonly url?: unknown;
15
+ readonly default?: unknown;
16
+ readonly "publish-url"?: unknown;
17
+ }
18
+
19
+ function uvIndexes(source: string): UvIndex[] {
20
+ const document = parse(source) as { index?: unknown };
21
+ return Array.isArray(document.index)
22
+ ? document.index.filter((value): value is UvIndex => Boolean(value && typeof value === "object"))
23
+ : [];
23
24
  }
24
25
 
25
26
  /**
@@ -30,15 +31,12 @@ export function parseUvDefaultIndex(source: string): string | undefined {
30
31
  * a non-default block is discoverable here.
31
32
  */
32
33
  export function parseUvIndexes(source: string): string[] {
33
- const blocks = source.split(/(?=^\[\[index\]\]\s*$)/m);
34
34
  let primary: string | undefined;
35
35
  const extras: string[] = [];
36
- for (const block of blocks) {
37
- if (!/^\[\[index\]\]\s*$/m.test(block)) continue;
38
- const url = /^\s*url\s*=\s*["']([^"']+)["']\s*$/m.exec(block)?.[1];
39
- if (!url) continue;
40
- if (/^\s*default\s*=\s*true\s*$/m.test(block)) primary = url;
41
- else extras.push(url);
36
+ for (const index of uvIndexes(source)) {
37
+ if (typeof index.url !== "string") continue;
38
+ if (index.default === true) primary = index.url;
39
+ else extras.push(index.url);
42
40
  }
43
41
  return primary ? [primary, ...extras] : extras;
44
42
  }
@@ -52,12 +50,10 @@ export function parseUvIndexes(source: string): string[] {
52
50
  */
53
51
  export function parseUvPublishUrls(source: string): Map<string, string> {
54
52
  const map = new Map<string, string>();
55
- const blocks = source.split(/(?=^\[\[index\]\]\s*$)/m);
56
- for (const block of blocks) {
57
- if (!/^\[\[index\]\]\s*$/m.test(block)) continue;
58
- const url = /^\s*url\s*=\s*["']([^"']+)["']\s*$/m.exec(block)?.[1];
59
- const publishUrl = /^\s*publish-url\s*=\s*["']([^"']+)["']\s*$/m.exec(block)?.[1];
60
- if (url && publishUrl) map.set(url, publishUrl);
53
+ for (const index of uvIndexes(source)) {
54
+ if (typeof index.url === "string" && typeof index["publish-url"] === "string") {
55
+ map.set(index.url, index["publish-url"]);
56
+ }
61
57
  }
62
58
  return map;
63
59
  }
@@ -167,11 +163,6 @@ export function activePythonIndexes(): string[] {
167
163
  return out;
168
164
  }
169
165
 
170
- /** The primary (first) active Python index, or `undefined` when none is set. */
171
- export function activePythonIndex(): string | undefined {
172
- return activePythonIndexes()[0];
173
- }
174
-
175
166
  /**
176
167
  * Resolve `auto`, `false`, or an explicit devpi index/publish URL.
177
168
  *
@@ -12,6 +12,7 @@ const directSibling = (
12
12
  dependency: string,
13
13
  sibling: { readonly directory: string; readonly name: string },
14
14
  ): boolean => {
15
+ if (dependency === sibling.name) return true;
15
16
  const prefix = `${sibling.name} @ git+`;
16
17
  if (!dependency.startsWith(prefix)) return false;
17
18
  const marker = "#subdirectory=";
@@ -0,0 +1,66 @@
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 { captureTaskCommand } from "../../src/_task-command.ts";
7
+ import { compareSemver, parseSemver, type Semver } from "../../src/workspace-version.ts";
8
+
9
+ function publishedVersion(root: string, prefixes: readonly string[]): string | undefined {
10
+ let best: Semver | undefined;
11
+ for (const prefix of prefixes) {
12
+ const tags = captureTaskCommand(root, "git", [
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
+ }
28
+
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
+ export function main(args: string[] = process.argv.slice(2)): void {
50
+ const { values } = parseArgs({
51
+ args,
52
+ options: {
53
+ version: { type: "string" },
54
+ root: { type: "string" },
55
+ prefix: { type: "string", multiple: true },
56
+ "assert-next": { type: "boolean", default: false },
57
+ },
58
+ strict: true,
59
+ });
60
+ if (!values.version) throw new Error("--version is required");
61
+ assertReleaseVersion(values.version, {
62
+ ...(values.root ? { root: values.root } : {}),
63
+ ...(values.prefix ? { prefixes: values.prefix } : {}),
64
+ assertNext: values["assert-next"],
65
+ });
66
+ }
@@ -0,0 +1,212 @@
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 { publishLocalRelease } from "./local-publish.ts";
7
+ import { assertReleaseVersion } from "./release-version.ts";
8
+ import {
9
+ captureTaskCommand,
10
+ runTaskCommand,
11
+ taskCommandSucceeds,
12
+ } from "../../src/_task-command.ts";
13
+ import { readWorkspaceVersion } from "../../src/workspace-version.ts";
14
+
15
+ const logger = log.logger("projen:release");
16
+
17
+ function gitHead(root: string, ref: string): string | undefined {
18
+ if (!taskCommandSucceeds(root, "git", ["rev-parse", "--verify", ref])) return undefined;
19
+ return captureTaskCommand(root, "git", ["rev-parse", ref], { check: true });
20
+ }
21
+
22
+ function assertAncestor(root: string, ancestor: string, descendant: string, message: string): void {
23
+ if (!taskCommandSucceeds(root, "git", ["merge-base", "--is-ancestor", ancestor, descendant])) {
24
+ throw new Error(message);
25
+ }
26
+ }
27
+
28
+ function prepareReleaseBranch(options: {
29
+ readonly branch: string;
30
+ readonly remote: string;
31
+ readonly root: string;
32
+ }): void {
33
+ const { branch, remote, root } = options;
34
+ const sourceBranch = captureTaskCommand(root, "git", ["branch", "--show-current"], {
35
+ check: true,
36
+ });
37
+ if (!sourceBranch) throw new Error("release cannot run from a detached HEAD");
38
+
39
+ const status = captureTaskCommand(
40
+ root,
41
+ "git",
42
+ ["status", "--porcelain=v1", "--untracked-files=all"],
43
+ { check: true },
44
+ );
45
+ if (status) {
46
+ runTaskCommand(root, "git", ["add", "--all"]);
47
+ runTaskCommand(root, "git", ["commit", "-m", "chore: prepare release"]);
48
+ }
49
+
50
+ runTaskCommand(root, "git", ["fetch", "--prune", remote, "--tags"]);
51
+ const sourceHead = captureTaskCommand(root, "git", ["rev-parse", "HEAD"], { check: true });
52
+ const remoteSourceHead = gitHead(root, `refs/remotes/${remote}/${sourceBranch}`);
53
+ if (remoteSourceHead) {
54
+ assertAncestor(
55
+ root,
56
+ remoteSourceHead,
57
+ sourceHead,
58
+ `cannot fast-forward ${remote}/${sourceBranch} to ${sourceBranch}`,
59
+ );
60
+ }
61
+ runTaskCommand(root, "git", ["push", remote, `HEAD:${sourceBranch}`]);
62
+
63
+ if (sourceBranch === branch) return;
64
+
65
+ const remoteMain = gitHead(root, `refs/remotes/${remote}/${branch}`);
66
+ if (!remoteMain) throw new Error(`release branch ${remote}/${branch} does not exist`);
67
+ assertAncestor(
68
+ root,
69
+ remoteMain,
70
+ sourceHead,
71
+ `cannot safely merge ${sourceBranch} into ${branch}; ${branch} is not an ancestor`,
72
+ );
73
+
74
+ const localMain = gitHead(root, `refs/heads/${branch}`);
75
+ if (localMain) {
76
+ assertAncestor(
77
+ root,
78
+ localMain,
79
+ sourceHead,
80
+ `cannot safely fast-forward local ${branch} to ${sourceBranch}`,
81
+ );
82
+ } else {
83
+ runTaskCommand(root, "git", ["branch", branch, `${remote}/${branch}`]);
84
+ }
85
+ runTaskCommand(root, "git", ["switch", branch]);
86
+ runTaskCommand(root, "git", ["merge", "--ff-only", sourceBranch]);
87
+ runTaskCommand(root, "git", ["push", remote, `HEAD:${branch}`]);
88
+ }
89
+
90
+ export async function runRelease(options: {
91
+ readonly root: string;
92
+ readonly branch: string;
93
+ readonly bump?: boolean;
94
+ readonly prefix: string;
95
+ readonly remote: string;
96
+ readonly localPublish?: boolean;
97
+ readonly pythonRoot?: string;
98
+ readonly validationTasks?: readonly string[];
99
+ }): Promise<string> {
100
+ const { branch, prefix, remote, root } = options;
101
+
102
+ prepareReleaseBranch({ branch, remote, root });
103
+
104
+ runTaskCommand(root, "git", ["fetch", remote, branch, "--tags"]);
105
+ if (options.bump ?? true) {
106
+ runTaskCommand(root, "bun", ["run", "bump"]);
107
+ }
108
+
109
+ const version = readWorkspaceVersion(root);
110
+ const tag = `${prefix}${version}`;
111
+ assertReleaseVersion(version, { root, prefixes: [prefix], assertNext: true });
112
+
113
+ if (taskCommandSucceeds(root, "git", ["rev-parse", "--verify", `refs/tags/${tag}`])) {
114
+ throw new Error(`release tag ${tag} already exists`);
115
+ }
116
+
117
+ runTaskCommand(root, "bun", ["run", "version:check"]);
118
+ for (const task of options.validationTasks ?? []) {
119
+ runTaskCommand(root, "bun", ["run", task]);
120
+ }
121
+ const status = captureTaskCommand(
122
+ root,
123
+ "git",
124
+ ["status", "--porcelain=v1", "--untracked-files=all"],
125
+ { check: true },
126
+ );
127
+ if (status) {
128
+ if (!(options.bump ?? true)) {
129
+ throw new Error("--no-bump requires an already committed synchronized version");
130
+ }
131
+ if (taskCommandSucceeds(root, "git", ["diff", "--quiet", "HEAD", "--", "VERSION"])) {
132
+ throw new Error("release changes must include the local VERSION bump");
133
+ }
134
+ runTaskCommand(root, "git", ["add", "--all"]);
135
+ runTaskCommand(root, "git", ["commit", "-m", `chore(release): ${version}`]);
136
+ }
137
+
138
+ const remoteHead = captureTaskCommand(root, "git", ["rev-parse", `${remote}/${branch}`], {
139
+ check: true,
140
+ });
141
+ const head = captureTaskCommand(root, "git", ["rev-parse", "HEAD"], { check: true });
142
+ if (!taskCommandSucceeds(root, "git", ["merge-base", "--is-ancestor", remoteHead, head])) {
143
+ throw new Error(
144
+ `cannot fast-forward ${remote}/${branch} to HEAD; ${branch} has diverged (rebase before release)`,
145
+ );
146
+ }
147
+
148
+ runTaskCommand(root, "git", ["tag", "--annotate", tag, "--message", tag]);
149
+ try {
150
+ runTaskCommand(root, "git", ["push", "--atomic", remote, `HEAD:${branch}`, `refs/tags/${tag}`]);
151
+ } catch (error) {
152
+ runTaskCommand(root, "git", ["tag", "--delete", tag]);
153
+ throw error;
154
+ }
155
+ logger.success(`released ${tag}`, { sha: head });
156
+
157
+ // Restore local deploys: when a non-standard (loopback) npm or uv registry is
158
+ // configured, publish the freshly tagged version to it directly. Detection and
159
+ // publishing are owned by local-publish.ts; this no-ops on standard registries.
160
+ if (options.localPublish ?? true) {
161
+ await publishLocalRelease({
162
+ localPypi: "auto",
163
+ localRegistry: "auto",
164
+ pythonRoot: options.pythonRoot ?? "packages/py",
165
+ root,
166
+ version,
167
+ });
168
+ }
169
+
170
+ return tag;
171
+ }
172
+
173
+ export async function main(): Promise<void> {
174
+ await new Command()
175
+ .option("--root <path>", "repository root")
176
+ .option("--branch <name>", "release branch", "main")
177
+ .option("--prefix <prefix>", "release tag prefix", "v")
178
+ .option("--remote <name>", "git remote", "origin")
179
+ .option("--python-root <path>", "Python package root for local publish", "packages/py")
180
+ .option(
181
+ "--validate <task>",
182
+ "task to run before pushing",
183
+ (task, tasks: string[]) => [...tasks, task],
184
+ [],
185
+ )
186
+ .option("--no-bump", "use an existing synchronized local version bump")
187
+ .option("--no-local-publish", "skip publishing to configured local registries")
188
+ .action(
189
+ async (options: {
190
+ root?: string;
191
+ branch: string;
192
+ bump: boolean;
193
+ prefix: string;
194
+ remote: string;
195
+ pythonRoot: string;
196
+ localPublish: boolean;
197
+ validate: string[];
198
+ }) => {
199
+ await runRelease({
200
+ root: options.root ?? projectUtils.root() ?? process.cwd(),
201
+ branch: options.branch,
202
+ bump: options.bump,
203
+ prefix: options.prefix,
204
+ remote: options.remote,
205
+ pythonRoot: options.pythonRoot,
206
+ localPublish: options.localPublish,
207
+ validationTasks: options.validate,
208
+ });
209
+ },
210
+ )
211
+ .parseAsync();
212
+ }
@@ -1,120 +1,6 @@
1
- /** Build and publish the current workspace to configured loopback registries. */
2
- import { resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import * as projectUtils from "@dbx-tools/core/project-utils";
5
- import { log, net } from "@dbx-tools/shared-core";
6
- import { Command } from "commander";
7
- import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
8
- import { runTaskCommandAsync } from "../src/_task-command.ts";
1
+ #!/usr/bin/env -S bun
2
+ import { main } from "./lib/local-publish.ts";
9
3
 
10
- const logger = log.logger("projen:local-publish");
4
+ export * from "./lib/local-publish.ts";
11
5
 
12
- /** Options for publishing the current workspace to local registries. */
13
- export interface LocalPublishOptions {
14
- readonly localPypi: string;
15
- readonly localRegistry: string;
16
- readonly pythonRoot: string;
17
- readonly root: string;
18
- readonly version: string;
19
- }
20
-
21
- export function resolveLocalRegistry(value: string): string | undefined {
22
- const trimmed = value.trim();
23
- if (!trimmed || trimmed.toLowerCase() === "false") return undefined;
24
- if (trimmed.toLowerCase() === "auto") {
25
- const registry = projectUtils.npmRegistry();
26
- return registry && net.isLoopbackHost(registry) ? registry.href : undefined;
27
- }
28
- return trimmed;
29
- }
30
-
31
- /** Outcome of a local publish: which ecosystems were deployed to a local registry. */
32
- export interface LocalPublishResult {
33
- readonly npm: boolean;
34
- readonly python: boolean;
35
- }
36
-
37
- /** Build and publish the current workspace directly to local registries. */
38
- export async function publishLocalRelease(options: LocalPublishOptions): Promise<LocalPublishResult> {
39
- const root = resolve(options.root);
40
- const localRegistry = resolveLocalRegistry(options.localRegistry);
41
- const activeIndexes = activePythonIndexes();
42
- const localPypi = resolveLocalPypi(options.localPypi, activeIndexes);
43
- const publishes: Promise<unknown>[] = [];
44
-
45
- if (
46
- options.localPypi.toLowerCase() === "auto" &&
47
- activeIndexes.some((index) => net.isLoopbackHost(index)) &&
48
- !localPypi
49
- ) {
50
- logger.info(
51
- `skipped local Python publish: no active index (${activeIndexes.join(", ")}) is a devpi +simple index`,
52
- );
53
- }
54
-
55
- if (localRegistry) {
56
- const publishNpmScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
57
- logger.info(`building and publishing npm workspace to ${localRegistry}`);
58
- publishes.push(
59
- runTaskCommandAsync(root, process.execPath, [
60
- publishNpmScript,
61
- options.version,
62
- "--registry",
63
- localRegistry,
64
- ]),
65
- );
66
- }
67
-
68
- if (localPypi) {
69
- const publishPythonScript = fileURLToPath(new URL("./publish-python.ts", import.meta.url));
70
- logger.info(`building and publishing Python workspace to ${localPypi.publishUrl}`);
71
- publishes.push(
72
- runTaskCommandAsync(root, process.execPath, [
73
- publishPythonScript,
74
- options.version,
75
- "--root",
76
- options.pythonRoot,
77
- "--index-url",
78
- localPypi.indexUrl,
79
- "--publish-url",
80
- localPypi.publishUrl,
81
- ]),
82
- );
83
- }
84
-
85
- if (publishes.length === 0) {
86
- logger.info("no local registries configured; skipping local publish");
87
- return { npm: false, python: false };
88
- }
89
-
90
- await Promise.all(publishes);
91
- logger.success(`published local workspace ${options.version}`);
92
- return { npm: Boolean(localRegistry), python: Boolean(localPypi) };
93
- }
94
-
95
- if (import.meta.main) {
96
- await new Command()
97
- .requiredOption("--version <version>", "release version")
98
- .option("--root <path>", "repository root")
99
- .option("--python-root <path>", "Python package root", "packages/py")
100
- .option("--local-registry <url>", "local npm registry", "auto")
101
- .option("--local-pypi <url>", "local devpi index", "auto")
102
- .action(
103
- async (options: {
104
- version: string;
105
- root?: string;
106
- pythonRoot: string;
107
- localRegistry: string;
108
- localPypi: string;
109
- }) => {
110
- await publishLocalRelease({
111
- localPypi: options.localPypi,
112
- localRegistry: options.localRegistry,
113
- pythonRoot: options.pythonRoot,
114
- root: options.root ?? projectUtils.root() ?? process.cwd(),
115
- version: options.version,
116
- });
117
- },
118
- )
119
- .parseAsync();
120
- }
6
+ if (import.meta.main) await main();
package/tasks/openapi.ts CHANGED
@@ -7,25 +7,29 @@ import { watchLoop } from "../src/watch.ts";
7
7
 
8
8
  const logger = log.logger("projen:openapi");
9
9
 
10
- if (process.argv.includes("--watch")) {
11
- // Watch the package roots; a changed tsoa controller regenerates the openapi
12
- // packages (spec + client) and rebuilds just their barrels. The projenrc watcher
13
- // (alongside under `concurrently`) owns full re-synth, so no re-synth here.
14
- watchLoop(
15
- "openapi",
16
- openapiWatchRoots(),
17
- async () => {
18
- const dirs = await generateOpenapi();
19
- if (dirs.length) {
20
- generateBarrels({ dirs });
21
- logger.success(`regenerated openapi (${stringUtils.pluralize(dirs.length, "package")})`);
22
- }
23
- },
24
- { check: (changed) => object.sequence(changed).some(isOpenapiSource) },
25
- );
26
- } else {
27
- // One-shot: regenerate, then re-synth so any newly created openapi folder becomes a
28
- // workspace member (a new package needs install + linking, which the watch path skips).
29
- const dirs = await generateOpenapi();
30
- if (dirs.length > 0) runSynth({ post: true });
10
+ export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
11
+ if (args.includes("--watch")) {
12
+ // Watch the package roots; a changed tsoa controller regenerates the openapi
13
+ // packages (spec + client) and rebuilds just their barrels. The projenrc watcher
14
+ // (alongside under `concurrently`) owns full re-synth, so no re-synth here.
15
+ watchLoop(
16
+ "openapi",
17
+ openapiWatchRoots(),
18
+ async () => {
19
+ const dirs = await generateOpenapi();
20
+ if (dirs.length) {
21
+ generateBarrels({ dirs });
22
+ logger.success(`regenerated openapi (${stringUtils.pluralize(dirs.length, "package")})`);
23
+ }
24
+ },
25
+ { check: (changed) => object.sequence(changed).some(isOpenapiSource) },
26
+ );
27
+ } else {
28
+ // One-shot: regenerate, then re-synth so any newly created openapi folder becomes a
29
+ // workspace member (a new package needs install + linking, which the watch path skips).
30
+ const dirs = await generateOpenapi();
31
+ if (dirs.length > 0) runSynth({ post: true });
32
+ }
31
33
  }
34
+
35
+ if (import.meta.main) await main();
package/tasks/projenrc.ts CHANGED
@@ -17,8 +17,6 @@ function resynthWatchPaths(): string[] {
17
17
  return paths;
18
18
  }
19
19
 
20
- const WATCH_PATHS = resynthWatchPaths();
21
-
22
20
  // Watch-only: `sync --watch` runs this under `concurrently` as the intelligent
23
21
  // re-synth trigger. It watches `.projenrc.ts` plus any `dbxToolsConfig.syncResynthPaths`
24
22
  // entries (from the root's `syncResynthPaths` option) and re-synths (+install) on edit -
@@ -26,13 +24,17 @@ const WATCH_PATHS = resynthWatchPaths();
26
24
  // makes the watch intelligent vs stock `projen --watch`, which re-synths on any tree
27
25
  // change. `{ dot: false }` keeps the default dotfile ignore group from pruning dotfile
28
26
  // targets. (A one-shot "projenrc" is just a synth, i.e. `bun run default`.)
29
- watchLoop(
30
- "projenrc",
31
- WATCH_PATHS,
32
- () => {
33
- logger.start("projenrc changed - re-synthesizing (+install)");
34
- runSynth({ post: true });
35
- logger.success("re-synth complete");
36
- },
37
- { dot: false },
38
- );
27
+ export function main(): void {
28
+ watchLoop(
29
+ "projenrc",
30
+ resynthWatchPaths(),
31
+ () => {
32
+ logger.start("projenrc changed - re-synthesizing (+install)");
33
+ runSynth({ post: true });
34
+ logger.success("re-synth complete");
35
+ },
36
+ { ignoreOptions: { dot: false } },
37
+ );
38
+ }
39
+
40
+ if (import.meta.main) main();