@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
package/tasks/barrels.ts CHANGED
@@ -7,14 +7,6 @@ import { recordedPackages } from "../src/packages.ts";
7
7
  import { watchLoop, watchRoots } from "../src/watch.ts";
8
8
 
9
9
  const logger = log.logger("projen:barrels");
10
- const { values } = parseArgs({
11
- args: process.argv.slice(2),
12
- options: {
13
- watch: { type: "boolean" },
14
- dir: { type: "string", multiple: true },
15
- },
16
- strict: false,
17
- });
18
10
 
19
11
  /** The recorded package dir that owns `abs`, if any (for a targeted barrel rebuild). */
20
12
  function ownerPackageDir(
@@ -45,36 +37,49 @@ function warnUnownedChanges(changed: readonly string[]): void {
45
37
  }
46
38
  }
47
39
 
48
- if (values.watch) {
49
- // Watch the package roots; a source edit inside a package rebuilds just that
50
- // package's `index.ts` barrel (no re-synth - the projenrc watcher owns that).
51
- // watchLoop already drops generated paths, so a barrel write never re-triggers us.
52
- watchLoop(
53
- "barrels",
54
- watchRoots(),
55
- (changed) => {
56
- const n = generateBarrels({ dirs: [...changedBarrelTargets(changed)] });
57
- if (n) logger.success(`rebuilt ${stringUtils.pluralize(n, "barrel")}`);
40
+ export function main(args: string[] = process.argv.slice(2)): void {
41
+ const { values } = parseArgs({
42
+ args,
43
+ options: {
44
+ watch: { type: "boolean" },
45
+ dir: { type: "string", multiple: true },
58
46
  },
59
- {
60
- check: (changed) => {
61
- const targets = changedBarrelTargets(changed);
62
- // A change under a package root that no recorded package owns is a new
63
- // folder: only a re-synth can create its barrel, so this watcher skips.
64
- if (!targets.some(() => true)) {
65
- warnUnownedChanges(changed);
66
- return false;
67
- }
68
- return true;
47
+ strict: false,
48
+ });
49
+
50
+ if (values.watch) {
51
+ // Watch the package roots; a source edit inside a package rebuilds just that
52
+ // package's `index.ts` barrel (no re-synth - the projenrc watcher owns that).
53
+ // watchLoop already drops generated paths, so a barrel write never re-triggers us.
54
+ watchLoop(
55
+ "barrels",
56
+ watchRoots(),
57
+ (changed) => {
58
+ const n = generateBarrels({ dirs: [...changedBarrelTargets(changed)] });
59
+ if (n) logger.success(`rebuilt ${stringUtils.pluralize(n, "barrel")}`);
69
60
  },
70
- },
71
- );
72
- } else {
73
- const dirs = Array.isArray(values.dir)
74
- ? values.dir.filter((value): value is string => typeof value === "string")
75
- : [];
76
- const n = generateBarrels(dirs.length ? { dirs } : undefined);
77
- logger.success(
78
- n === 0 ? "barrels already up to date" : `updated ${stringUtils.pluralize(n, "barrel")}`,
79
- );
61
+ {
62
+ check: (changed) => {
63
+ const targets = changedBarrelTargets(changed);
64
+ // A change under a package root that no recorded package owns is a new
65
+ // folder: only a re-synth can create its barrel, so this watcher skips.
66
+ if (!targets.some(() => true)) {
67
+ warnUnownedChanges(changed);
68
+ return false;
69
+ }
70
+ return true;
71
+ },
72
+ },
73
+ );
74
+ } else {
75
+ const dirs = Array.isArray(values.dir)
76
+ ? values.dir.filter((value): value is string => typeof value === "string")
77
+ : [];
78
+ const n = generateBarrels(dirs.length ? { dirs } : undefined);
79
+ logger.success(
80
+ n === 0 ? "barrels already up to date" : `updated ${stringUtils.pluralize(n, "barrel")}`,
81
+ );
82
+ }
80
83
  }
84
+
85
+ if (import.meta.main) main();
package/tasks/bump.ts CHANGED
@@ -16,26 +16,30 @@ import {
16
16
 
17
17
  const logger = log.logger("projen:bump");
18
18
 
19
- const program = new Command();
20
- program
21
- .description("Increment VERSION and synchronize generated workspace versions")
22
- .addOption(
23
- new Option("-l, --level <level>", "semver increment")
24
- .choices(["patch", "minor", "major"])
25
- .default("patch"),
26
- )
27
- .option("--no-synth", "write VERSION without synchronizing generated files")
28
- .action((opts: { level: VersionLevel; synth: boolean }) => {
29
- const root = projectUtils.root() ?? process.cwd();
30
- const next = resolveNextVersion(root, opts.level);
19
+ export async function main(): Promise<void> {
20
+ const program = new Command();
21
+ program
22
+ .description("Increment VERSION and synchronize generated workspace versions")
23
+ .addOption(
24
+ new Option("-l, --level <level>", "semver increment")
25
+ .choices(["patch", "minor", "major"])
26
+ .default("patch"),
27
+ )
28
+ .option("--no-synth", "write VERSION without synchronizing generated files")
29
+ .action((opts: { level: VersionLevel; synth: boolean }) => {
30
+ const root = projectUtils.root() ?? process.cwd();
31
+ const next = resolveNextVersion(root, opts.level);
31
32
 
32
- logger.info(`bump ${next.base} -> ${next.version} (${opts.level})`);
33
- writeWorkspaceVersion(root, next.version);
33
+ logger.info(`bump ${next.base} -> ${next.version} (${opts.level})`);
34
+ writeWorkspaceVersion(root, next.version);
34
35
 
35
- if (opts.synth) {
36
- runTaskCommand(root, process.execPath, [".projenrc.ts"]);
37
- }
38
- logger.success(`workspace version synchronized at ${next.version}`);
39
- });
36
+ if (opts.synth) {
37
+ runTaskCommand(root, process.execPath, [".projenrc.ts"]);
38
+ }
39
+ logger.success(`workspace version synchronized at ${next.version}`);
40
+ });
40
41
 
41
- await program.parseAsync();
42
+ await program.parseAsync();
43
+ }
44
+
45
+ if (import.meta.main) await main();
package/tasks/clean.ts CHANGED
@@ -5,63 +5,67 @@ import { listGeneratedFiles, listNodeModulesDirs, removePaths } from "../src/cle
5
5
  import { repoRoot, toPosix } from "../src/packages.ts";
6
6
 
7
7
  const logger = log.logger("projen:clean");
8
- const yes = process.argv.includes("-y") || process.argv.includes("--yes");
8
+ export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
9
+ const yes = args.includes("-y") || args.includes("--yes");
9
10
 
10
- const files = listGeneratedFiles();
11
- const nodeModules = listNodeModulesDirs();
12
- const targets = [...files, ...nodeModules];
11
+ const files = listGeneratedFiles();
12
+ const nodeModules = listNodeModulesDirs();
13
+ const targets = [...files, ...nodeModules];
13
14
 
14
- if (targets.length === 0) {
15
- logger.success("nothing to remove (no generated files or node_modules)");
16
- process.exit(0);
17
- }
15
+ if (targets.length === 0) {
16
+ logger.success("nothing to remove (no generated files or node_modules)");
17
+ process.exit(0);
18
+ }
18
19
 
19
- const regenHint = (removedNodeModules: boolean): string =>
20
- removedNodeModules
21
- ? "reinstall with `bun install`, then `bun run default`"
22
- : "regenerate with `bun run default`";
20
+ const regenHint = (removedNodeModules: boolean): string =>
21
+ removedNodeModules
22
+ ? "reinstall with `bun install`, then `bun run default`"
23
+ : "regenerate with `bun run default`";
23
24
 
24
- if (yes) {
25
- const n = removePaths(targets);
26
- logger.success(
27
- `removed ${stringUtils.pluralize(n, "path")} (${files.length} generated + ${nodeModules.length} node_modules) - ${regenHint(nodeModules.length > 0)}`,
28
- );
29
- process.exit(0);
30
- }
25
+ if (yes) {
26
+ const n = removePaths(targets);
27
+ logger.success(
28
+ `removed ${stringUtils.pluralize(n, "path")} (${files.length} generated + ${nodeModules.length} node_modules) - ${regenHint(nodeModules.length > 0)}`,
29
+ );
30
+ process.exit(0);
31
+ }
31
32
 
32
- if (!process.stdin.isTTY) {
33
- logger.warn(
34
- `non-interactive shell: re-run with -y to remove all ${targets.length} paths (${files.length} generated + ${nodeModules.length} node_modules), or run in a terminal to pick`,
35
- );
36
- process.exit(1);
37
- }
33
+ if (!process.stdin.isTTY) {
34
+ logger.warn(
35
+ `non-interactive shell: re-run with -y to remove all ${targets.length} paths (${files.length} generated + ${nodeModules.length} node_modules), or run in a terminal to pick`,
36
+ );
37
+ process.exit(1);
38
+ }
38
39
 
39
- const clack = await import("@clack/prompts");
40
- clack.intro("projen clean");
41
- const label = (f: string): string => toPosix(relative(repoRoot, f));
42
- const picked = await clack.multiselect<string>({
43
- message: `Select paths to remove (${files.length} generated + ${nodeModules.length} node_modules, all preselected)`,
44
- options: [
45
- ...files.map((f) => ({ value: f, label: label(f) })),
46
- ...nodeModules.map((d) => ({
47
- value: d,
48
- label: `${label(d)} (directory)`,
49
- })),
50
- ],
51
- initialValues: [...targets],
52
- required: false,
53
- });
40
+ const clack = await import("@clack/prompts");
41
+ clack.intro("projen clean");
42
+ const label = (f: string): string => toPosix(relative(repoRoot, f));
43
+ const picked = await clack.multiselect<string>({
44
+ message: `Select paths to remove (${files.length} generated + ${nodeModules.length} node_modules, all preselected)`,
45
+ options: [
46
+ ...files.map((f) => ({ value: f, label: label(f) })),
47
+ ...nodeModules.map((d) => ({
48
+ value: d,
49
+ label: `${label(d)} (directory)`,
50
+ })),
51
+ ],
52
+ initialValues: [...targets],
53
+ required: false,
54
+ });
54
55
 
55
- if (typeof picked === "symbol") {
56
- clack.cancel("clean cancelled - nothing removed");
57
- process.exit(0);
58
- }
56
+ if (typeof picked === "symbol") {
57
+ clack.cancel("clean cancelled - nothing removed");
58
+ process.exit(0);
59
+ }
60
+
61
+ if (picked.length === 0) {
62
+ clack.outro("nothing selected - nothing removed");
63
+ process.exit(0);
64
+ }
59
65
 
60
- if (picked.length === 0) {
61
- clack.outro("nothing selected - nothing removed");
62
- process.exit(0);
66
+ const removedNodeModules = picked.some((p) => nodeModules.includes(p));
67
+ const n = removePaths(picked);
68
+ clack.outro(`removed ${stringUtils.pluralize(n, "path")} - ${regenHint(removedNodeModules)}`);
63
69
  }
64
70
 
65
- const removedNodeModules = picked.some((p) => nodeModules.includes(p));
66
- const n = removePaths(picked);
67
- clack.outro(`removed ${stringUtils.pluralize(n, "path")} - ${regenHint(removedNodeModules)}`);
71
+ if (import.meta.main) await main();
@@ -1,121 +1,6 @@
1
1
  #!/usr/bin/env -S bun
2
- /** Compile source-first workspace packages in a few TypeScript processes. */
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { availableParallelism } from "node:os";
5
- import { join, resolve } from "node:path";
6
- import * as exec from "@dbx-tools/core/exec";
2
+ import { main } from "./lib/compile-workspace.ts";
7
3
 
8
- interface PackageManifest {
9
- readonly name?: string;
10
- readonly scripts?: Record<string, string>;
11
- readonly workspaces?: string[];
12
- }
13
-
14
- interface TaskManifest {
15
- readonly tasks?: {
16
- readonly compile?: {
17
- readonly steps?: Array<{ readonly execArgs?: string[] }>;
18
- };
19
- };
20
- }
21
-
22
- export interface CompilePlan {
23
- readonly typescriptConfigs: string[];
24
- readonly customPackages: Array<{ readonly name: string; readonly directory: string }>;
25
- }
26
-
27
- function readJson<T>(path: string): T {
28
- return JSON.parse(readFileSync(path, "utf8")) as T;
29
- }
30
-
31
- function plainTypeScriptBuild(steps: unknown): boolean {
32
- if (!Array.isArray(steps) || steps.length !== 1) return false;
33
- const step = steps[0] as { execArgs?: string[] };
34
- if (!step || typeof step !== "object") return false;
35
- return (
36
- Object.keys(step).length === 1 &&
37
- step.execArgs?.length === 2 &&
38
- step.execArgs[0] === "tsc" &&
39
- step.execArgs[1] === "--build"
40
- );
41
- }
42
-
43
- /** Read the root's concrete workspace paths on every run, including newly added members. */
44
- export function compilePlan(root: string): CompilePlan {
45
- const workspace = readJson<PackageManifest>(join(root, "package.json"));
46
- const typescriptConfigs: string[] = [];
47
- const customPackages: CompilePlan["customPackages"] = [];
48
- for (const member of workspace.workspaces ?? []) {
49
- const directory = resolve(root, member);
50
- const manifest = readJson<PackageManifest>(join(directory, "package.json"));
51
- const script = manifest.scripts?.compile;
52
- if (!script) continue;
53
- const taskPath = join(directory, ".projen", "tasks.json");
54
- const steps = existsSync(taskPath)
55
- ? readJson<TaskManifest>(taskPath).tasks?.compile?.steps
56
- : undefined;
57
- if (
58
- script === "projen compile" &&
59
- !manifest.scripts?.precompile &&
60
- !manifest.scripts?.postcompile &&
61
- plainTypeScriptBuild(steps)
62
- ) {
63
- typescriptConfigs.push(join(directory, "tsconfig.json"));
64
- } else {
65
- customPackages.push({ name: manifest.name ?? member, directory });
66
- }
67
- }
68
- return { typescriptConfigs, customPackages };
69
- }
70
-
71
- const emoji = /[\p{Extended_Pictographic}\uFE0F\u200D]/gu;
72
-
73
- async function main(): Promise<void> {
74
- const root = process.cwd();
75
- const plan = compilePlan(root);
76
- const jobs: Array<{ name: string; result: Promise<exec.ExecResult> }> = [];
77
- if (plan.typescriptConfigs.length > 0) {
78
- const batchCount = Math.min(4, availableParallelism(), plan.typescriptConfigs.length);
79
- const batches = Array.from({ length: batchCount }, () => [] as string[]);
80
- plan.typescriptConfigs.forEach((config, index) => batches[index % batchCount]!.push(config));
81
- console.log(
82
- `Type-checking ${plan.typescriptConfigs.length} workspaces in ${batchCount} batches`,
83
- );
84
- for (const [index, batch] of batches.entries()) {
85
- jobs.push({
86
- name: `TypeScript batch ${index + 1}`,
87
- result: exec.spawn("bunx", ["tsc", "--build", ...batch], {
88
- cwd: root,
89
- stdin: "ignore",
90
- stdout: "inherit",
91
- stderr: "inherit",
92
- }),
93
- });
94
- }
95
- }
96
- for (const pkg of plan.customPackages) {
97
- jobs.push({
98
- name: pkg.name,
99
- result: exec.spawn("bun", ["run", "compile"], {
100
- cwd: pkg.directory,
101
- stdin: "ignore",
102
- stdout: (line) => console.log(`${pkg.name}: ${line.replace(emoji, "")}`),
103
- stderr: (line) => console.error(`${pkg.name}: ${line.replace(emoji, "")}`),
104
- }),
105
- });
106
- }
107
- const results = await Promise.allSettled(jobs.map((job) => job.result));
108
- let failed = false;
109
- for (const [index, result] of results.entries()) {
110
- if (result.status === "rejected") {
111
- console.error(`${jobs[index]!.name} compile failed:`, result.reason);
112
- failed = true;
113
- } else if (result.value.exitCode !== 0) {
114
- console.error(`${jobs[index]!.name} compile exited with ${result.value.exitCode}`);
115
- failed = true;
116
- }
117
- }
118
- if (failed) process.exitCode = 1;
119
- }
4
+ export { compilePlan, main } from "./lib/compile-workspace.ts";
120
5
 
121
6
  if (import.meta.main) await main();
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env -S bun
2
+ /** Compile source-first workspace packages in a few TypeScript processes. */
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { availableParallelism } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import * as exec from "@dbx-tools/core/exec";
7
+
8
+ interface PackageManifest {
9
+ readonly name?: string;
10
+ readonly scripts?: Record<string, string>;
11
+ readonly workspaces?: string[];
12
+ }
13
+
14
+ interface TaskManifest {
15
+ readonly tasks?: {
16
+ readonly compile?: {
17
+ readonly steps?: Array<{ readonly execArgs?: string[] }>;
18
+ };
19
+ };
20
+ }
21
+
22
+ export interface CompilePlan {
23
+ readonly typescriptConfigs: string[];
24
+ readonly customPackages: Array<{ readonly name: string; readonly directory: string }>;
25
+ }
26
+
27
+ function readJson<T>(path: string): T {
28
+ return JSON.parse(readFileSync(path, "utf8")) as T;
29
+ }
30
+
31
+ function plainTypeScriptBuild(steps: unknown): boolean {
32
+ if (!Array.isArray(steps) || steps.length !== 1) return false;
33
+ const step = steps[0] as { execArgs?: string[] };
34
+ if (!step || typeof step !== "object") return false;
35
+ return (
36
+ Object.keys(step).length === 1 &&
37
+ step.execArgs?.length === 2 &&
38
+ step.execArgs[0] === "tsc" &&
39
+ step.execArgs[1] === "--build"
40
+ );
41
+ }
42
+
43
+ /** Read the root's concrete workspace paths on every run, including newly added members. */
44
+ export function compilePlan(root: string): CompilePlan {
45
+ const workspace = readJson<PackageManifest>(join(root, "package.json"));
46
+ const typescriptConfigs: string[] = [];
47
+ const customPackages: CompilePlan["customPackages"] = [];
48
+ for (const member of workspace.workspaces ?? []) {
49
+ const directory = resolve(root, member);
50
+ const manifest = readJson<PackageManifest>(join(directory, "package.json"));
51
+ const script = manifest.scripts?.compile;
52
+ if (!script) continue;
53
+ const taskPath = join(directory, ".projen", "tasks.json");
54
+ const steps = existsSync(taskPath)
55
+ ? readJson<TaskManifest>(taskPath).tasks?.compile?.steps
56
+ : undefined;
57
+ if (
58
+ script === "projen compile" &&
59
+ !manifest.scripts?.precompile &&
60
+ !manifest.scripts?.postcompile &&
61
+ plainTypeScriptBuild(steps)
62
+ ) {
63
+ typescriptConfigs.push(join(directory, "tsconfig.json"));
64
+ } else {
65
+ customPackages.push({ name: manifest.name ?? member, directory });
66
+ }
67
+ }
68
+ return { typescriptConfigs, customPackages };
69
+ }
70
+
71
+ const emoji = /[\p{Extended_Pictographic}\uFE0F\u200D]/gu;
72
+
73
+ export async function main(): Promise<void> {
74
+ const root = process.cwd();
75
+ const plan = compilePlan(root);
76
+ const jobs: Array<{ name: string; result: Promise<exec.ExecResult> }> = [];
77
+ if (plan.typescriptConfigs.length > 0) {
78
+ const batchCount = Math.min(4, availableParallelism(), plan.typescriptConfigs.length);
79
+ const batches = Array.from({ length: batchCount }, () => [] as string[]);
80
+ plan.typescriptConfigs.forEach((config, index) => batches[index % batchCount]!.push(config));
81
+ console.log(
82
+ `Type-checking ${plan.typescriptConfigs.length} workspaces in ${batchCount} batches`,
83
+ );
84
+ for (const [index, batch] of batches.entries()) {
85
+ jobs.push({
86
+ name: `TypeScript batch ${index + 1}`,
87
+ result: exec.spawn("bunx", ["tsc", "--build", ...batch], {
88
+ cwd: root,
89
+ stdin: "ignore",
90
+ stdout: "inherit",
91
+ stderr: "inherit",
92
+ }),
93
+ });
94
+ }
95
+ }
96
+ for (const pkg of plan.customPackages) {
97
+ jobs.push({
98
+ name: pkg.name,
99
+ result: exec.spawn("bun", ["run", "compile"], {
100
+ cwd: pkg.directory,
101
+ stdin: "ignore",
102
+ stdout: (line) => console.log(`${pkg.name}: ${line.replace(emoji, "")}`),
103
+ stderr: (line) => console.error(`${pkg.name}: ${line.replace(emoji, "")}`),
104
+ }),
105
+ });
106
+ }
107
+ const results = await Promise.allSettled(jobs.map((job) => job.result));
108
+ let failed = false;
109
+ for (const [index, result] of results.entries()) {
110
+ if (result.status === "rejected") {
111
+ console.error(`${jobs[index]!.name} compile failed:`, result.reason);
112
+ failed = true;
113
+ } else if (result.value.exitCode !== 0) {
114
+ console.error(`${jobs[index]!.name} compile exited with ${result.value.exitCode}`);
115
+ failed = true;
116
+ }
117
+ }
118
+ if (failed) process.exitCode = 1;
119
+ }
@@ -0,0 +1,122 @@
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";
9
+
10
+ const logger = log.logger("projen:local-publish");
11
+
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(
39
+ options: LocalPublishOptions,
40
+ ): Promise<LocalPublishResult> {
41
+ const root = resolve(options.root);
42
+ const localRegistry = resolveLocalRegistry(options.localRegistry);
43
+ const activeIndexes = activePythonIndexes();
44
+ const localPypi = resolveLocalPypi(options.localPypi, activeIndexes);
45
+ const publishes: Promise<unknown>[] = [];
46
+
47
+ if (
48
+ options.localPypi.toLowerCase() === "auto" &&
49
+ activeIndexes.some((index) => net.isLoopbackHost(index)) &&
50
+ !localPypi
51
+ ) {
52
+ logger.info(
53
+ `skipped local Python publish: no active index (${activeIndexes.join(", ")}) is a devpi +simple index`,
54
+ );
55
+ }
56
+
57
+ if (localRegistry) {
58
+ const publishNpmScript = fileURLToPath(new URL("../publish.ts", import.meta.url));
59
+ logger.info(`building and publishing npm workspace to ${localRegistry}`);
60
+ publishes.push(
61
+ runTaskCommandAsync(root, process.execPath, [
62
+ publishNpmScript,
63
+ options.version,
64
+ "--registry",
65
+ localRegistry,
66
+ ]),
67
+ );
68
+ }
69
+
70
+ if (localPypi) {
71
+ const publishPythonScript = fileURLToPath(new URL("../publish-python.ts", import.meta.url));
72
+ logger.info(`building and publishing Python workspace to ${localPypi.publishUrl}`);
73
+ publishes.push(
74
+ runTaskCommandAsync(root, process.execPath, [
75
+ publishPythonScript,
76
+ options.version,
77
+ "--root",
78
+ options.pythonRoot,
79
+ "--index-url",
80
+ localPypi.indexUrl,
81
+ "--publish-url",
82
+ localPypi.publishUrl,
83
+ ]),
84
+ );
85
+ }
86
+
87
+ if (publishes.length === 0) {
88
+ logger.info("no local registries configured; skipping local publish");
89
+ return { npm: false, python: false };
90
+ }
91
+
92
+ await Promise.all(publishes);
93
+ logger.success(`published local workspace ${options.version}`);
94
+ return { npm: Boolean(localRegistry), python: Boolean(localPypi) };
95
+ }
96
+
97
+ export async function main(): Promise<void> {
98
+ await new Command()
99
+ .requiredOption("--version <version>", "release version")
100
+ .option("--root <path>", "repository root")
101
+ .option("--python-root <path>", "Python package root", "packages/py")
102
+ .option("--local-registry <url>", "local npm registry", "auto")
103
+ .option("--local-pypi <url>", "local devpi index", "auto")
104
+ .action(
105
+ async (options: {
106
+ version: string;
107
+ root?: string;
108
+ pythonRoot: string;
109
+ localRegistry: string;
110
+ localPypi: string;
111
+ }) => {
112
+ await publishLocalRelease({
113
+ localPypi: options.localPypi,
114
+ localRegistry: options.localRegistry,
115
+ pythonRoot: options.pythonRoot,
116
+ root: options.root ?? projectUtils.root() ?? process.cwd(),
117
+ version: options.version,
118
+ });
119
+ },
120
+ )
121
+ .parseAsync();
122
+ }