@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
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env -S bun
2
+ /**
3
+ * `bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run] [--skip-compile]`
4
+ * - verify every workspace member carries the release version, then pack and
5
+ * publish each package owned by the standard Node release.
6
+ *
7
+ * Bun has no `pnpm -r publish`, so this loop is the recursive-publish stand-in.
8
+ * It leans on native bun for everything bun already does:
9
+ *
10
+ * - **version validation** fails when any member differs from the reviewed
11
+ * `VERSION`; publication never repairs committed release metadata;
12
+ * - **`workspace:` / `catalog:` rewriting** is NOT done here - `bun pm pack`
13
+ * strips both protocols in the PACKED tarball, resolving `workspace:*` to the
14
+ * sibling's version and `catalog:` to the root catalog entry. (Verified: a
15
+ * packed manifest shows `"@scope/x": "<version>"` and the real catalog range,
16
+ * while the on-disk manifest keeps the protocols.) Setting each member's
17
+ * version first is the only prerequisite, so a sibling resolves the release
18
+ * - **`publishConfig` substitution** (compiled `lib/` entry points) is done
19
+ * inside the temporary archive, NOT in the checkout: unlike pnpm/npm, `bun
20
+ * publish`/`bun pm pack` do NOT fold `publishConfig`'s `main`/`types`/`bin`/
21
+ * `exports` into the packed manifest (verified: the packed manifest keeps the
22
+ * raw `.ts` source paths and an inert `publishConfig`). Left unsubstituted, a
23
+ * published CLI's `bin` points at `./bin/x.ts`, and because the bin runs via
24
+ * its `#!/usr/bin/env node` shebang, node chokes on the `.ts`
25
+ * (ERR_UNKNOWN_FILE_EXTENSION). We merge `publishConfig` onto the top-level
26
+ * manifest after packing so the tarball advertises the compiled `lib/` tree;
27
+ * - **compiled output** is emitted once, before packing, by one root-level
28
+ * filtered `bun run` that fans out to every publishable member in parallel.
29
+ * Every package is then packed once with lifecycle scripts disabled. The
30
+ * exact validated archive is passed to `npm publish`, so upload never
31
+ * repacks or repeats a member's `prepack`. Packages retain `prepack` for
32
+ * standalone publishes.
33
+ * - **release recovery** compares each packed archive's integrity and
34
+ * repository identity with registry metadata. A matching immutable version
35
+ * is skipped, while any mismatch fails the retry.
36
+ *
37
+ * `--dry-run` forwards to `npm publish`: it packs + validates
38
+ * but uploads nothing. `--registry` targets a non-default registry (a local
39
+ * verdaccio); `--exclude <dir>` (repeatable, repo-relative) skips a member owned
40
+ * by another publication flow.
41
+ *
42
+ * The checkout remains byte-for-byte unchanged. A stale manifest is a release
43
+ * error rather than something publication repairs. Local lockfiles are ignored
44
+ * because configured registries can leak into them.
45
+ */
46
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
47
+ import { tmpdir } from "node:os";
48
+ import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
49
+ import { asyncUtils, log } from "@dbx-tools/shared-core";
50
+ import {
51
+ applyPublishConfig,
52
+ npmReleaseMatches,
53
+ packNpmPackage,
54
+ publishedNpmRelease,
55
+ readNpmArchiveIdentity,
56
+ } from "./publish-npm.ts";
57
+ import { runTaskCommand, runTaskCommandAsync } from "../../src/_task-command.ts";
58
+ import { recordedPackages } from "../../src/packages.ts";
59
+
60
+ const logger = log.logger("projen:publish");
61
+
62
+ export { applyPublishConfig };
63
+
64
+ /** Whether selected workspace members carry the reviewed workspace version. */
65
+ function manifestsMatchVersion(members: readonly string[], version: string): boolean {
66
+ return members.every((dir) => {
67
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
68
+ version?: string;
69
+ };
70
+ return pkg.version === version;
71
+ });
72
+ }
73
+
74
+ /** Compiled files referenced by a publishConfig entry-point tree. */
75
+ function compiledPublishTargets(value: unknown): string[] {
76
+ if (typeof value === "string") return value.startsWith("./lib/") ? [value] : [];
77
+ if (Array.isArray(value)) return value.flatMap(compiledPublishTargets);
78
+ if (!value || typeof value !== "object") return [];
79
+ return Object.values(value).flatMap(compiledPublishTargets);
80
+ }
81
+
82
+ /**
83
+ * PATH with the workspace-root `node_modules/.bin` prepended. The root-level
84
+ * compile reaches each package's projen/dax task, which resolves `tsc` off PATH;
85
+ * under the hoisted linker `tsc` lives only in the root `.bin`.
86
+ */
87
+ function enrichedPath(root: string): string {
88
+ const binDir = join(root, "node_modules", ".bin");
89
+ const current = process.env.PATH ?? "";
90
+ return current.split(delimiter).includes(binDir) ? current : `${binDir}${delimiter}${current}`;
91
+ }
92
+
93
+ function outputDirectory(root: string, value: string | undefined): string | undefined {
94
+ if (!value) return undefined;
95
+ const output = resolve(root, value);
96
+ const path = relative(root, output);
97
+ if (!path || path.startsWith("..") || isAbsolute(path)) {
98
+ throw new Error("--output must be a non-root directory inside the workspace");
99
+ }
100
+ return output;
101
+ }
102
+
103
+ export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
104
+ const version = argv[0] && !argv[0].startsWith("--") ? argv[0] : undefined;
105
+ const rest = version ? argv.slice(1) : argv;
106
+ const root = process.cwd();
107
+ if (!version) {
108
+ logger.error(
109
+ "usage: bun tasks/publish.ts <version> [--registry <url>] [--output <dir>] [--exclude <dir>] [--dry-run] [--skip-compile]",
110
+ );
111
+ process.exit(1);
112
+ }
113
+ const registryIdx = rest.indexOf("--registry");
114
+ const registry = registryIdx >= 0 ? rest[registryIdx + 1] : undefined;
115
+ const outputIdx = rest.indexOf("--output");
116
+ const output = outputDirectory(root, outputIdx >= 0 ? rest[outputIdx + 1] : undefined);
117
+ const dryRun = rest.includes("--dry-run");
118
+ const skipCompile = rest.includes("--skip-compile");
119
+ const concurrencyIdx = rest.indexOf("--concurrency");
120
+ const parsedConcurrency = Number(concurrencyIdx >= 0 ? rest[concurrencyIdx + 1] : 4);
121
+ if (!Number.isInteger(parsedConcurrency) || parsedConcurrency < 1) {
122
+ throw new Error(`--concurrency must be a positive integer, got ${String(parsedConcurrency)}`);
123
+ }
124
+ const concurrency = parsedConcurrency;
125
+ const excluded = new Set(
126
+ rest.reduce<string[]>((acc, arg, i) => (arg === "--exclude" ? [...acc, rest[i + 1]] : acc), []),
127
+ );
128
+
129
+ const path = enrichedPath(root);
130
+ const allMembers = recordedPackages(root)
131
+ .map((pkg) => pkg.dir)
132
+ .filter((dir) => existsSync(join(dir, "package.json")))
133
+ .filter((dir) => !excluded.has(resolve(root, dir).replace(`${resolve(root)}/`, "")));
134
+ const members = allMembers;
135
+
136
+ if (!manifestsMatchVersion(members, version)) {
137
+ throw new Error(`workspace manifests do not match release ${version}; run projen`);
138
+ }
139
+ logger.info(`validated ${members.length} selected member manifests`);
140
+
141
+ const publishArgs = [
142
+ "--ignore-scripts",
143
+ ...(registry ? ["--registry", registry] : []),
144
+ ...(dryRun ? ["--dry-run"] : []),
145
+ ];
146
+ const publishable: Array<{
147
+ dir: string;
148
+ name: string;
149
+ version: string;
150
+ compile: boolean;
151
+ compiledTargets: string[];
152
+ access?: "public" | "restricted";
153
+ }> = [];
154
+ for (const dir of members) {
155
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
156
+ name?: string;
157
+ version?: string;
158
+ private?: boolean;
159
+ publishConfig?: { access?: unknown };
160
+ };
161
+ if (pkg.private) {
162
+ logger.info(`skip private ${pkg.name ?? dirname(dir)}`);
163
+ continue;
164
+ }
165
+ if (!pkg.version) throw new Error(`Missing package version for ${pkg.name ?? dirname(dir)}`);
166
+ const access = pkg.publishConfig?.access;
167
+ if (access !== undefined && access !== "public" && access !== "restricted") {
168
+ throw new Error(
169
+ `${pkg.name ?? dirname(dir)} has invalid publishConfig.access ${String(access)}`,
170
+ );
171
+ }
172
+ const compiledTargets = [...new Set(compiledPublishTargets(pkg.publishConfig))];
173
+ publishable.push({
174
+ dir,
175
+ name: pkg.name ?? dirname(dir),
176
+ version: pkg.version,
177
+ compile: compiledTargets.length > 0,
178
+ compiledTargets,
179
+ ...(access ? { access } : {}),
180
+ });
181
+ }
182
+
183
+ const compiled = publishable.filter((pkg) => pkg.compile);
184
+ if (compiled.length > 0) {
185
+ if (skipCompile) {
186
+ const missing = compiled.flatMap((pkg) =>
187
+ pkg.compiledTargets
188
+ .filter((target) => !existsSync(resolve(pkg.dir, target)))
189
+ .map((target) => `${pkg.name}:${target}`),
190
+ );
191
+ if (missing.length > 0) {
192
+ throw new Error(
193
+ `--skip-compile requires validated compiled output; missing ${missing.join(", ")}`,
194
+ );
195
+ }
196
+ logger.info(`reusing validated compiled output for ${compiled.length} publishable packages`);
197
+ } else {
198
+ logger.info(`compiling ${compiled.length} publishable packages from the workspace root`);
199
+ runTaskCommand(
200
+ root,
201
+ "bun",
202
+ ["run", ...compiled.flatMap((pkg) => ["--filter", pkg.name]), "compile"],
203
+ { env: { ...process.env, PATH: path } },
204
+ );
205
+ }
206
+ }
207
+
208
+ logger.info(
209
+ `${output ? "packing" : dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
210
+ );
211
+ if (output) {
212
+ rmSync(output, { recursive: true, force: true });
213
+ mkdirSync(output, { recursive: true });
214
+ }
215
+ await asyncUtils.mapConcurrent(
216
+ publishable,
217
+ async ({ dir, name, version: packageVersion, access }) => {
218
+ const packed = mkdtempSync(join(tmpdir(), "projen-npm-release-"));
219
+ try {
220
+ const archive = packNpmPackage(dir, packed, path, applyPublishConfig);
221
+ const local = readNpmArchiveIdentity(archive);
222
+ if (local.name !== name || local.version !== packageVersion) {
223
+ throw new Error(
224
+ `Packed npm identity ${local.name}@${local.version} does not match ${name}@${packageVersion}`,
225
+ );
226
+ }
227
+ if (local.access !== access) {
228
+ throw new Error(
229
+ `Packed npm access ${String(local.access)} does not match ${String(access)} for ${name}`,
230
+ );
231
+ }
232
+ if (output) {
233
+ const destination = join(output, archive.split(/[\\/]/).at(-1)!);
234
+ if (existsSync(destination)) {
235
+ throw new Error(`Duplicate npm archive name: ${destination}`);
236
+ }
237
+ copyFileSync(archive, destination);
238
+ logger.info(`packed ${name} @ ${packageVersion}`);
239
+ return;
240
+ }
241
+ if (!dryRun) {
242
+ const published = await publishedNpmRelease(local.name, local.version, registry);
243
+ if (npmReleaseMatches(local, published)) {
244
+ logger.info(`skip published ${name} @ ${packageVersion}`);
245
+ return;
246
+ }
247
+ }
248
+ logger.info(`${dryRun ? "dry-run publishing" : "publishing"} ${name} @ ${packageVersion}`);
249
+ await runTaskCommandAsync(
250
+ dir,
251
+ "npm",
252
+ ["publish", ...(access ? ["--access", access] : []), ...publishArgs, archive],
253
+ { env: { ...process.env, PATH: path } },
254
+ );
255
+ } finally {
256
+ rmSync(packed, { recursive: true, force: true });
257
+ }
258
+ },
259
+ { concurrency, errorMode: "settle" },
260
+ );
261
+ logger.success(
262
+ `${output || dryRun ? "packed" : "published"} ${publishable.length} packages${output ? ` to ${output}` : ""}`,
263
+ );
264
+ }