@dbx-tools/projen 0.9.19 → 0.9.28

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 (56) hide show
  1. package/README.md +83 -229
  2. package/index.ts +11 -18
  3. package/package.json +9 -5
  4. package/src/_task-command.ts +75 -0
  5. package/src/barrels.ts +1 -74
  6. package/src/module-exports.ts +175 -1
  7. package/src/openapi.ts +5 -72
  8. package/src/packages.ts +45 -0
  9. package/src/project-js.ts +5 -48
  10. package/src/project-predicate.ts +1 -2
  11. package/src/project-py.ts +87 -199
  12. package/src/project.ts +2 -2
  13. package/src/python-node-bindings.ts +138 -0
  14. package/src/python-node-bundle.ts +92 -0
  15. package/src/release-catalog.ts +40 -208
  16. package/src/{release-dispatch.ts → release-context.ts} +0 -11
  17. package/src/release.ts +74 -778
  18. package/src/scaffold.ts +2 -6
  19. package/src/workspace-version.ts +9 -121
  20. package/tasks/bump.ts +17 -34
  21. package/tasks/databricks-cli-assets.ts +129 -0
  22. package/tasks/local-publish.ts +61 -138
  23. package/tasks/publish-npm.ts +71 -30
  24. package/tasks/publish-python.ts +61 -148
  25. package/tasks/publish.ts +32 -182
  26. package/tasks/python-node-bindings-watch.ts +47 -0
  27. package/tasks/python-node-bindings.ts +1017 -0
  28. package/tasks/python-registry.ts +2 -9
  29. package/tasks/python-release.ts +74 -0
  30. package/tasks/release-version.ts +23 -2
  31. package/tasks/release.ts +99 -0
  32. package/tasks/sync.ts +13 -18
  33. package/tasks/version-check.ts +13 -72
  34. package/src/_rust-project.ts +0 -429
  35. package/src/_rust-release-workflow.ts +0 -698
  36. package/src/project-rs.ts +0 -1127
  37. package/src/release-plan.ts +0 -269
  38. package/src/release-please.ts +0 -242
  39. package/src/uniffi.ts +0 -65
  40. package/tasks/publish-uniffi-local.ts +0 -314
  41. package/tasks/release-assets.ts +0 -476
  42. package/tasks/release-bootstrap.ts +0 -59
  43. package/tasks/release-candidate.ts +0 -383
  44. package/tasks/release-manifest.ts +0 -273
  45. package/tasks/release-plan.ts +0 -89
  46. package/tasks/release-please.ts +0 -162
  47. package/tasks/release-pr.ts +0 -563
  48. package/tasks/release-request.ts +0 -166
  49. package/tasks/release-summary-units.ts +0 -75
  50. package/tasks/release-summary.ts +0 -381
  51. package/tasks/rust.ts +0 -178
  52. package/tasks/stamp-python.ts +0 -13
  53. package/tasks/uniffi-python.d.ts +0 -61
  54. package/tasks/uniffi-python.js +0 -191
  55. package/tasks/uniffi-release.mjs +0 -573
  56. package/tasks/uniffi.ts +0 -271
package/src/barrels.ts CHANGED
@@ -16,11 +16,6 @@
16
16
  * takes priority. This keeps the barrel auto-generated while letting you add or
17
17
  * override individual exports.
18
18
  *
19
- * A UniFFI source triplet (`bindings.ts`, `_bindings.ts`, and
20
- * `_bindings-ffi.ts`) is one special case: the internal underscore modules stay
21
- * private, while `bindings.ts` is re-exported directly instead of under a
22
- * `bindings` namespace. Generation fails when one of those direct binding names
23
- * conflicts with another top-level package export.
24
19
  *
25
20
  * Each eligible module becomes `export * as <name> from "./src/x.ts"` (camelCase
26
21
  * namespace from its path segments; invalid identifiers suffixed with `Module`),
@@ -121,9 +116,6 @@ const PACKAGE_IDENTIFIER_LINE = `export const ${PACKAGE_IDENTIFIER_EXPORT} = "";
121
116
  const PACKAGE_VERSION_LINE = `export const ${PACKAGE_VERSION_EXPORT} = "";`;
122
117
  const PACKAGE_IDENTIFIER_LINE_RE = /^export const PACKAGE_IDENTIFIER = .*;$/m;
123
118
  const PACKAGE_VERSION_LINE_RE = /^export const PACKAGE_VERSION = .*;$/m;
124
- const UNIFFI_BINDINGS_FILE = "bindings.ts";
125
- const UNIFFI_GENERATED_FILE = "_bindings.ts";
126
- const UNIFFI_FFI_FILE = "_bindings-ffi.ts";
127
119
 
128
120
  /** `config-tools` / `config_tools` -> `configTools`; `local-fs` -> `localFS`. */
129
121
  function moduleSegmentToCamel(segment: string): string {
@@ -295,61 +287,6 @@ function isLegacyBindingsExport(file: string): boolean {
295
287
  return readFileSync(file, "utf8").trim() === 'export * from "./src/bindings.ts";';
296
288
  }
297
289
 
298
- function hasUniFFIBindings(srcDir: string): boolean {
299
- return [UNIFFI_BINDINGS_FILE, UNIFFI_GENERATED_FILE, UNIFFI_FFI_FILE].every((file) =>
300
- existsSync(join(srcDir, file)),
301
- );
302
- }
303
-
304
- function bindingExportNames(srcDir: string): Set<string> {
305
- return new Set(
306
- [UNIFFI_BINDINGS_FILE, UNIFFI_GENERATED_FILE].flatMap((file) =>
307
- moduleExports(join(srcDir, file)).map((entry) => entry.name),
308
- ),
309
- );
310
- }
311
-
312
- function barrelExportNames(content: string, customPath: string): Set<string> {
313
- const names = new Set<string>([PACKAGE_IDENTIFIER_EXPORT, PACKAGE_VERSION_EXPORT]);
314
- for (const match of content.matchAll(/^export \* as (\w+) from /gm)) {
315
- names.add(match[1]!);
316
- }
317
- for (const match of content.matchAll(/^export(?: type)? \{([^}]+)\} from /gm)) {
318
- for (const specifier of match[1]!.split(",")) {
319
- const parts = specifier.trim().split(/\s+as\s+/);
320
- const name = parts.at(-1);
321
- if (name) names.add(name);
322
- }
323
- }
324
- if (existsSync(customPath) && !isLegacyBindingsExport(customPath)) {
325
- const statements = moduleStatements(customPath) as ReadonlyArray<{
326
- type: string;
327
- exported?: unknown;
328
- }>;
329
- if (
330
- statements.some(
331
- (statement) => statement.type === "ExportAllDeclaration" && !statement.exported,
332
- )
333
- ) {
334
- throw new Error(
335
- `Cannot verify UniFFI export conflicts through a bare export in ${customPath}`,
336
- );
337
- }
338
- for (const name of customExportNames(customPath)) names.add(name);
339
- }
340
- return names;
341
- }
342
-
343
- function assertNoBindingExportConflicts(srcDir: string, content: string, customPath: string): void {
344
- const other = barrelExportNames(content, customPath);
345
- const conflicts = [...bindingExportNames(srcDir)].filter((name) => other.has(name)).sort();
346
- if (conflicts.length > 0) {
347
- throw new Error(
348
- `UniFFI binding exports conflict with package exports in ${srcDir}: ${conflicts.join(", ")}`,
349
- );
350
- }
351
- }
352
-
353
290
  /** Read the authoritative npm package name and version emitted by the package project. */
354
291
  function packageMetadata(pkgDir: string): { identifier: string; version: string } {
355
292
  const manifestPath = join(pkgDir, "package.json");
@@ -441,10 +378,7 @@ function generateForPackage(pkgDir: string): number {
441
378
  if (!existing || (!isSourceExt(existing) && isSourceExt(f))) byModulePath.set(stem, f);
442
379
  }
443
380
  const modulePaths = [...byModulePath.keys()].sort((a, b) => a.localeCompare(b));
444
- const uniffiBindings = hasUniFFIBindings(srcDir);
445
- const namespacedModulePaths = uniffiBindings
446
- ? modulePaths.filter((modulePath) => modulePath !== "bindings")
447
- : modulePaths;
381
+ const namespacedModulePaths = modulePaths;
448
382
 
449
383
  // No eligible modules -> no barrel: drop any stale root barrel and bail.
450
384
  if (modulePaths.length === 0) {
@@ -479,13 +413,6 @@ function generateForPackage(pkgDir: string): number {
479
413
  content = hoistUniqueExports(content, pkgDir, suppress);
480
414
  // A sibling `exports.ts` overrides/extends the generated barrel and wins on conflict.
481
415
  content = mergeCustomExports(content, pkgDir);
482
- if (uniffiBindings) {
483
- assertNoBindingExportConflicts(srcDir, content, customPath);
484
- content = content.replace(
485
- PACKAGE_VERSION_LINE,
486
- `${PACKAGE_VERSION_LINE}\nexport * from "./src/bindings.ts";`,
487
- );
488
- }
489
416
 
490
417
  // Package metadata is blanked for the structural comparison, then restored
491
418
  // from package.json so a version-only change still rewrites the barrel.
@@ -19,8 +19,9 @@
19
19
  * `export { ... }` is a hard error (TS1205). `export function` names are marked
20
20
  * `isFunction` so the barrel leaves them namespace-only.
21
21
  */
22
- import { readFileSync } from "node:fs";
22
+ import { existsSync, readFileSync } from "node:fs";
23
23
  import { createRequire } from "node:module";
24
+ import { dirname, extname, resolve } from "node:path";
24
25
  import type { parseSync as OxcParseSync } from "oxc-parser";
25
26
 
26
27
  const require = createRequire(import.meta.url);
@@ -63,6 +64,13 @@ export interface ModuleExport {
63
64
  readonly isFunction: boolean;
64
65
  }
65
66
 
67
+ /** Plain public function reachable through a module's re-export graph. */
68
+ export interface PublicFunctionExport {
69
+ readonly name: string;
70
+ readonly sourceFile: string;
71
+ readonly sourceName: string;
72
+ }
73
+
66
74
  /** Declaration node types that are inherently type-only. */
67
75
  const TYPE_DECLARATIONS = new Set(["TSInterfaceDeclaration", "TSTypeAliasDeclaration"]);
68
76
 
@@ -132,3 +140,169 @@ export function moduleExports(file: string): ModuleExport[] {
132
140
  }
133
141
  return [...byName.values()];
134
142
  }
143
+
144
+ /**
145
+ * Resolve every plain named function exported by `file`, following relative
146
+ * named and star re-exports. Non-function values and types are ignored. A
147
+ * generator, declaration-only overload, or conflicting function definition is
148
+ * rejected because a generic runtime bridge cannot preserve those semantics.
149
+ */
150
+ export function publicFunctionExports(file: string): PublicFunctionExport[] {
151
+ return [...collectPublicFunctions(resolve(file), new Set()).values()]
152
+ .map(({ name, sourceFile, sourceName }) => ({ name, sourceFile, sourceName }))
153
+ .sort((left, right) => left.name.localeCompare(right.name));
154
+ }
155
+
156
+ interface ResolvedFunctionExport extends PublicFunctionExport {
157
+ readonly source: string;
158
+ }
159
+
160
+ function collectPublicFunctions(
161
+ file: string,
162
+ visiting: Set<string>,
163
+ ): Map<string, ResolvedFunctionExport> {
164
+ if (visiting.has(file)) return new Map();
165
+ visiting.add(file);
166
+ const source = readFileSync(file, "utf8");
167
+ const body = parseSync(file, source).program.body;
168
+ const functions = new Map<string, ResolvedFunctionExport>();
169
+ const localFunctions = new Map<string, FunctionNode>();
170
+
171
+ for (const statement of body) {
172
+ const node = statement as FunctionNode & { declaration?: FunctionNode | null };
173
+ const declaration =
174
+ node.type === "FunctionDeclaration" || node.type === "TSDeclareFunction"
175
+ ? node
176
+ : node.declaration;
177
+ if (declaration?.type === "FunctionDeclaration" || declaration?.type === "TSDeclareFunction") {
178
+ const name = declaration.id?.name;
179
+ if (name) localFunctions.set(name, declaration);
180
+ }
181
+ }
182
+
183
+ const add = (exported: ResolvedFunctionExport): void => {
184
+ const existing = functions.get(exported.name);
185
+ if (existing && existing.source !== exported.source) {
186
+ throw new Error(
187
+ `Conflicting function export ${exported.name} from ${existing.source} and ${exported.source}`,
188
+ );
189
+ }
190
+ functions.set(exported.name, exported);
191
+ };
192
+
193
+ for (const statement of body) {
194
+ if (statement.type === "ExportAllDeclaration") {
195
+ const node = statement as { exported?: unknown; source?: { value?: string } };
196
+ if (node.exported || !node.source?.value) continue;
197
+ for (const exported of collectPublicFunctions(
198
+ resolveModuleSpecifier(file, node.source.value),
199
+ visiting,
200
+ ).values()) {
201
+ add(exported);
202
+ }
203
+ continue;
204
+ }
205
+ if (statement.type !== "ExportNamedDeclaration") continue;
206
+ const node = statement as {
207
+ declaration?: FunctionNode | null;
208
+ source?: { value?: string } | null;
209
+ specifiers?: {
210
+ local?: { name?: string; value?: string };
211
+ exported?: { name?: string; value?: string };
212
+ exportKind?: "value" | "type";
213
+ }[];
214
+ exportKind?: "value" | "type";
215
+ };
216
+ if (
217
+ node.declaration?.type === "FunctionDeclaration" ||
218
+ node.declaration?.type === "TSDeclareFunction"
219
+ ) {
220
+ add(parseFunctionNode(file, node.declaration));
221
+ }
222
+ if (node.exportKind === "type") continue;
223
+ if (node.source?.value) {
224
+ const available = collectPublicFunctions(
225
+ resolveModuleSpecifier(file, node.source.value),
226
+ visiting,
227
+ );
228
+ for (const specifier of node.specifiers ?? []) {
229
+ if (specifier.exportKind === "type") continue;
230
+ const local = specifier.local?.name ?? specifier.local?.value;
231
+ const exportedName = specifier.exported?.name ?? specifier.exported?.value;
232
+ const target = local ? available.get(local) : undefined;
233
+ if (target && exportedName)
234
+ add({
235
+ name: exportedName,
236
+ source: target.source,
237
+ sourceFile: target.sourceFile,
238
+ sourceName: target.sourceName,
239
+ });
240
+ }
241
+ continue;
242
+ }
243
+ for (const specifier of node.specifiers ?? []) {
244
+ if (specifier.exportKind === "type") continue;
245
+ const local = specifier.local?.name ?? specifier.local?.value;
246
+ const exportedName = specifier.exported?.name ?? specifier.exported?.value;
247
+ const target = local ? localFunctions.get(local) : undefined;
248
+ if (target && exportedName) {
249
+ const parsed = parseFunctionNode(file, target);
250
+ add({
251
+ name: exportedName,
252
+ source: parsed.source,
253
+ sourceFile: parsed.sourceFile,
254
+ sourceName: parsed.sourceName,
255
+ });
256
+ }
257
+ }
258
+ }
259
+
260
+ visiting.delete(file);
261
+ return functions;
262
+ }
263
+
264
+ interface FunctionNode {
265
+ readonly type: string;
266
+ readonly id?: { readonly name?: string } | null;
267
+ readonly generator?: boolean;
268
+ readonly declare?: boolean;
269
+ readonly body?: unknown;
270
+ }
271
+
272
+ function parseFunctionNode(file: string, node: FunctionNode): ResolvedFunctionExport {
273
+ const name = node.id?.name;
274
+ if (!name) throw new Error(`Anonymous exported function is not supported in ${file}`);
275
+ if (node.generator) throw new Error(`Generator export ${name} is not supported in ${file}`);
276
+ if (node.type === "TSDeclareFunction" || node.declare || !node.body) {
277
+ throw new Error(`Declaration-only function export ${name} is not supported in ${file}`);
278
+ }
279
+ return { name, source: `${file}#${name}`, sourceFile: file, sourceName: name };
280
+ }
281
+
282
+ function resolveModuleSpecifier(importer: string, specifier: string): string {
283
+ if (!specifier.startsWith(".")) return createRequire(importer).resolve(specifier);
284
+ const base = resolve(dirname(importer), specifier);
285
+ const extension = extname(base);
286
+ const candidates = extension
287
+ ? [
288
+ base,
289
+ ...(extension === ".js" || extension === ".jsx"
290
+ ? [`${base.slice(0, -extension.length)}.ts`, `${base.slice(0, -extension.length)}.tsx`]
291
+ : []),
292
+ ]
293
+ : [
294
+ `${base}.ts`,
295
+ `${base}.tsx`,
296
+ `${base}.js`,
297
+ `${base}.jsx`,
298
+ `${base}.mjs`,
299
+ `${base}.cjs`,
300
+ resolve(base, "index.ts"),
301
+ resolve(base, "index.tsx"),
302
+ resolve(base, "index.js"),
303
+ ];
304
+ for (const candidate of candidates) {
305
+ if (existsSync(candidate)) return candidate;
306
+ }
307
+ throw new Error(`Could not resolve ${specifier} from ${importer}`);
308
+ }
package/src/openapi.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  /**
2
- * OpenAPI generator for tsoa controllers and code-first Rust producers.
2
+ * OpenAPI generator for tsoa controllers.
3
3
  *
4
- * TypeScript packages are discovered from tsoa imports. Rust producers are
5
- * recorded by `DBXToolsRustWorkspace`. Both feed the same optimized generated
6
- * package:
4
+ * TypeScript packages are discovered from tsoa imports and feed the optimized generated package:
7
5
  *
8
6
  * - `openapi.json` - the OpenAPI 3 spec (tsoa `generateSpec`, then Speakeasy
9
7
  * optimization to extract duplicate inline schemas into components).
@@ -48,7 +46,6 @@ import {
48
46
  toPosix,
49
47
  recordedPackages,
50
48
  } from "./packages.ts";
51
- import type { RustOpenApiMapping, RustWorkspaceMapping } from "./project-rs.ts";
52
49
  import type { ReleaseUnitGraph } from "./release-catalog.ts";
53
50
  import { readWorkspaceVersion } from "./workspace-version.ts";
54
51
 
@@ -100,15 +97,6 @@ function controllerPackages(): object.Sequence<RecordedPackage> {
100
97
  return object.sequence(recordedPackages()).filter(isTsoaCandidate).filter(hasTsoaControllers);
101
98
  }
102
99
 
103
- function rustOpenapiMappings(): RustOpenApiMapping[] {
104
- const manifest = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as unknown;
105
- if (!object.isRecord(manifest)) return [];
106
- const config = manifest.dbxToolsConfig;
107
- if (!object.isRecord(config) || !object.isRecord(config.rust)) return [];
108
- const mapping = config.rust as unknown as Partial<RustWorkspaceMapping>;
109
- return Array.isArray(mapping.openapi) ? [...mapping.openapi] : [];
110
- }
111
-
112
100
  function inside(path: string, directory: string): boolean {
113
101
  const child = relative(resolve(directory), resolve(path));
114
102
  return child === "" || (!child.startsWith("..") && !isAbsolute(child));
@@ -120,16 +108,11 @@ export function isOpenapiProducerSource(path: string, producers: readonly string
120
108
  return object.sequence(producers).some((producer) => inside(absolute, producer));
121
109
  }
122
110
 
123
- function rustOpenapiSources(mapping: RustOpenApiMapping): string[] {
124
- return [resolve(repoRoot, mapping.rust, "src"), resolve(repoRoot, mapping.rust, "Cargo.toml")];
125
- }
126
-
127
- /** Source roots consumed by TypeScript and Rust OpenAPI producers. */
111
+ /** Source roots consumed by TypeScript OpenAPI producers. */
128
112
  export function openapiWatchRoots(): string[] {
129
113
  return [
130
114
  ...controllerPackages()
131
115
  .map((pkg) => resolve(tsoaSource(pkg)))
132
- .join(object.sequence(rustOpenapiMappings()).flatMap(rustOpenapiSources))
133
116
  .distinct(),
134
117
  ];
135
118
  }
@@ -139,14 +122,6 @@ export function isOpenapiSource(path: string): boolean {
139
122
  const absolute = resolve(path);
140
123
  const segments = toPosix(relative(repoRoot, absolute)).split("/");
141
124
  if (segments.includes(OPENAPI_TAG)) return false;
142
- if (
143
- object
144
- .sequence(rustOpenapiMappings())
145
- .flatMap(rustOpenapiSources)
146
- .some((source) => inside(absolute, source))
147
- ) {
148
- return true;
149
- }
150
125
  return object
151
126
  .sequence(recordedPackages())
152
127
  .filter(isTsoaCandidate)
@@ -196,28 +171,6 @@ export async function optimizeOpenapiSpec(specPath: string, executable?: string)
196
171
  await execFileAsync(openapi, ["spec", "optimize", specPath, "--write", "--non-interactive"]);
197
172
  }
198
173
 
199
- /** Cargo arguments that export one Rust producer's aide document. */
200
- export function rustOpenapiArgs(
201
- mapping: RustOpenApiMapping,
202
- output: string,
203
- targetDir = join(repoRoot, "target/openapi"),
204
- ): string[] {
205
- return [
206
- "run",
207
- "--quiet",
208
- "--target-dir",
209
- targetDir,
210
- "--package",
211
- mapping.crate,
212
- ...(mapping.noDefaultFeatures ? ["--no-default-features"] : []),
213
- ...(mapping.features.length ? ["--features", mapping.features.join(",")] : []),
214
- ...(mapping.binary ? ["--bin", mapping.binary] : []),
215
- "--",
216
- "--generate-spec",
217
- output,
218
- ];
219
- }
220
-
221
174
  type OpenApiTypeTools = {
222
175
  openapiTS: typeof import("openapi-typescript").default;
223
176
  astToString: typeof import("openapi-typescript").astToString;
@@ -257,11 +210,10 @@ async function writeClientPackage(
257
210
  */
258
211
  export async function generateOpenapi(): Promise<string[]> {
259
212
  const pkgs = controllerPackages().toArray();
260
- const rustMappings = rustOpenapiMappings();
261
213
  // Same reasoning as codegen's empty case: a workspace with no tsoa controllers
262
214
  // is not a condition worth a line on every synth.
263
- if (pkgs.length === 0 && rustMappings.length === 0) {
264
- logger.debug("no TypeScript or Rust OpenAPI producers found");
215
+ if (pkgs.length === 0) {
216
+ logger.debug("no TypeScript OpenAPI producers found");
265
217
  return [];
266
218
  }
267
219
 
@@ -341,24 +293,5 @@ export async function generateOpenapi(): Promise<string[]> {
341
293
  written.push(outDir);
342
294
  logger.success(`openapi/${leaf} (from ${p.relPath})`);
343
295
  }
344
- for (const mapping of rustMappings) {
345
- const outDir = resolve(repoRoot, mapping.output);
346
- mkdirSync(outDir, { recursive: true });
347
- const specPath = join(outDir, "openapi.json");
348
- const tempDir = mkdtempSync(join(outDir, ".openapi-"));
349
- const tempSpecPath = join(tempDir, "openapi.json");
350
- try {
351
- await execFileAsync("cargo", rustOpenapiArgs(mapping, tempSpecPath), { cwd: repoRoot });
352
- await optimizeOpenapiSpec(tempSpecPath);
353
- makeWritable(specPath);
354
- renameSync(tempSpecPath, specPath);
355
- } finally {
356
- rmSync(tempDir, { recursive: true, force: true });
357
- }
358
- makeReadonly(specPath);
359
- await writeClientPackage(outDir, `the aide routes in ${mapping.rust}`, tools);
360
- written.push(outDir);
361
- logger.success(`${mapping.output} (from ${mapping.rust})`);
362
- }
363
296
  return written;
364
297
  }
package/src/packages.ts CHANGED
@@ -291,6 +291,19 @@ export function syncResynthPaths(projectRoot: string = resolveRepoRoot()): strin
291
291
  return Array.isArray(paths) ? stringUtils.parseList(paths.map((p) => String(p))) : [];
292
292
  }
293
293
 
294
+ /** Python projects with Node bindings registered for the root sync watcher. */
295
+ export function pythonNodeBindingProjects(projectRoot: string = resolveRepoRoot()): string[] {
296
+ const configured = readDbxToolsConfig(projectRoot)?.pythonNodeBindings;
297
+ if (!Array.isArray(configured)) return [];
298
+ return [
299
+ ...new Set(
300
+ configured.flatMap((value) =>
301
+ typeof value === "string" && value.trim() ? [value.trim()] : [],
302
+ ),
303
+ ),
304
+ ].sort();
305
+ }
306
+
294
307
  /**
295
308
  * The recorded workspace members from `pnpm-workspace.yaml` (the source of truth),
296
309
  * each augmented with the `tags` read back from its `package.json`. This is what
@@ -314,6 +327,38 @@ export function recordedPackages(projectRoot: string = resolveRepoRoot()): Recor
314
327
  return out.sort((a, b) => a.path.localeCompare(b.path));
315
328
  }
316
329
 
330
+ /**
331
+ * Absolute directories for a workspace package and its transitive runtime
332
+ * workspace dependencies. Dependency edges come from package manifests; external
333
+ * packages are ignored. Returns `[]` when `packageName` is not a workspace member.
334
+ */
335
+ export function workspaceDependencyDirectories(
336
+ packageName: string,
337
+ projectRoot: string = resolveRepoRoot(),
338
+ ): string[] {
339
+ const packages = new Map<string, { dir: string; manifest: Record<string, unknown> }>();
340
+ for (const pkg of recordedPackages(projectRoot)) {
341
+ const manifest = readPackageManifest(pkg.dir);
342
+ if (!manifest || typeof manifest.name !== "string") continue;
343
+ packages.set(manifest.name, { dir: pkg.dir, manifest });
344
+ }
345
+ if (!packages.has(packageName)) return [];
346
+
347
+ const directories = new Set<string>();
348
+ const visit = (name: string): void => {
349
+ const pkg = packages.get(name);
350
+ if (!pkg || directories.has(pkg.dir)) return;
351
+ directories.add(pkg.dir);
352
+ for (const field of ["dependencies", "optionalDependencies", "peerDependencies"] as const) {
353
+ const dependencies = pkg.manifest[field];
354
+ if (!object.isRecord(dependencies)) continue;
355
+ for (const dependency of Object.keys(dependencies)) visit(dependency);
356
+ }
357
+ };
358
+ visit(packageName);
359
+ return [...directories].sort();
360
+ }
361
+
317
362
  /**
318
363
  * The configured roots recorded in the root manifest. Older workspaces without
319
364
  * that field fall back to the distinct first segment of every recorded member,
package/src/project-js.ts CHANGED
@@ -15,7 +15,7 @@ import { type IConstruct } from "constructs";
15
15
  import { Component, IgnoreFile, Project, type TaskOptions, javascript, typescript } from "projen";
16
16
  import { BuildWorkflow } from "projen/lib/build";
17
17
  import { AutoMerge } from "projen/lib/github";
18
- import { JobPermission, type JobStep, type Triggers } from "projen/lib/github/workflows-model";
18
+ import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
19
19
  import type { ReleaseProjectOptions } from "projen/lib/release";
20
20
  import { mixin } from "..";
21
21
  import { generateBarrels } from "./barrels.ts";
@@ -44,12 +44,11 @@ import { PROJEN_VERSION } from "./projen-version.ts";
44
44
  import { applyCompiledPublish } from "./publish.ts";
45
45
  import {
46
46
  DBXToolsReleaseCatalog,
47
- type DBXToolsVersioningMode,
48
47
  type ExternalReleaseProjectRegistration,
49
48
  type ReleaseDependencyInput,
50
49
  type ReleaseUnitRule,
51
50
  } from "./release-catalog.ts";
52
- import { DBXToolsRelease, type ReleaseDocsOptions, type ReleaseSummaryOptions } from "./release.ts";
51
+ import { DBXToolsRelease, type ReleaseDocsOptions } from "./release.ts";
53
52
  import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
54
53
  import { DBXToolsRootTsconfig } from "./tsconfig.ts";
55
54
  import { DBXToolsVsCode } from "./vscode.ts";
@@ -385,7 +384,7 @@ const PRETTIER_SETTINGS: javascript.PrettierSettings = {
385
384
 
386
385
  export { PROJEN_VERSION } from "./projen-version.ts";
387
386
 
388
- /** SPDX license shared by generated JavaScript, Python, and Rust packages. */
387
+ /** SPDX license shared by generated JavaScript and Python packages. */
389
388
  export const DBX_TOOLS_LICENSE = "Apache-2.0";
390
389
 
391
390
  /**
@@ -626,20 +625,12 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
626
625
  readonly releasePythonRoot?: string;
627
626
  /** Repository task names run during local release preparation before publication. */
628
627
  readonly releaseValidationTasks?: readonly string[];
629
- /** Optional AI-generated release summary. Defaults to enabled. */
630
- readonly releaseSummary?: boolean | ReleaseSummaryOptions;
631
- /** Existing source branch safely fast-forwarded after an independent release. */
632
- readonly releaseSyncBranch?: string | false;
633
628
  /** Set to `false` to omit normal npm workspace publication. */
634
629
  readonly nodeRelease?: boolean;
635
630
  /** Unified dbx-tools release workflow, or no release surface. Defaults to `dbx-tools`. */
636
631
  readonly releaseMode?: DBXToolsReleaseMode;
637
- /** Package version ownership mode. Defaults to `fixed`. */
638
- readonly versioningMode?: DBXToolsVersioningMode;
639
632
  /** Explicit stable release-unit grouping rules. */
640
633
  readonly releaseUnits?: readonly ReleaseUnitRule[];
641
- /** Commit before the first independent Release Please history. */
642
- readonly releaseBootstrapSha?: string;
643
634
  /** Publishable workspace members synthesized outside the attached project tree. */
644
635
  readonly externalReleaseProjects?: readonly ExternalReleaseProjectRegistration[];
645
636
  /** Prefix for generated release tags. Defaults to `v`. */
@@ -723,10 +714,8 @@ export class DBXToolsNodeProject
723
714
  // (its build phase installs with pnpm and reads catalog + `allowBuilds`).
724
715
  pnpmWorkspace.attachWorkspaceFile(this);
725
716
  this.releaseCatalog = new DBXToolsReleaseCatalog(this, {
726
- mode: options.versioningMode,
727
717
  units: options.releaseUnits,
728
718
  externalProjects: options.externalReleaseProjects,
729
- bootstrapSha: options.releaseBootstrapSha,
730
719
  });
731
720
  registerJavaScriptReleaseProject(this);
732
721
  this.package.addField("version", () => this.releaseCatalog.versionFor(this));
@@ -862,10 +851,8 @@ export class DBXToolsTypeScriptProject
862
851
  (parent instanceof DBXToolsNodeProject || parent instanceof DBXToolsTypeScriptProject)
863
852
  ? parent.releaseCatalog
864
853
  : new DBXToolsReleaseCatalog(this, {
865
- mode: options.versioningMode,
866
854
  units: options.releaseUnits,
867
855
  externalProjects: options.externalReleaseProjects,
868
- bootstrapSha: options.releaseBootstrapSha,
869
856
  });
870
857
  // Pairs with `jsx` in SHARED_COMPILER_OPTIONS: projen's default `include` is
871
858
  // `src/**/*.ts` only, which silently omits a `.tsx` file from the program
@@ -937,35 +924,7 @@ function configureBuildWorkflow(
937
924
  });
938
925
  validation.exec("bunx projen default");
939
926
  validation.exec("bun run compile");
940
- const configured = {
941
- ...options.buildWorkflowOptions,
942
- ...(!options.buildWorkflowOptions?.workflowTriggers &&
943
- project.releaseCatalog.mode === "independent"
944
- ? {
945
- workflowTriggers: {
946
- pullRequest: {
947
- types: ["opened", "synchronize", "reopened", "closed"],
948
- paths: [
949
- "**",
950
- "!.projen/release-units.json",
951
- "!.release-please-manifest.json",
952
- "!.release-units/**",
953
- "!.release-notes/**",
954
- "!docs/releases/**",
955
- "!Cargo.lock",
956
- "!**/Cargo.toml",
957
- "!**/package.json",
958
- "!**/pyproject.toml",
959
- "!**/index.ts",
960
- "!packages/js/node/rust-binary/src/_release-binaries.ts",
961
- "!packages/js/node/appkit-graphiti/src/_python-runtime.ts",
962
- ],
963
- },
964
- workflowDispatch: {},
965
- } satisfies Triggers,
966
- }
967
- : {}),
968
- };
927
+ const configured = { ...options.buildWorkflowOptions };
969
928
  const compatibility = nodeWorkflowCompatibility(project);
970
929
  const configuredRunner = configured.runsOn !== undefined || configured.runsOnGroup !== undefined;
971
930
  const workflow = new BuildWorkflow(project, {
@@ -1363,7 +1322,7 @@ class PublishedPackageDescriptionValidation extends Component {
1363
1322
  * fail with ERR_MODULE_NOT_FOUND after such an update. bun runs the `.ts`
1364
1323
  * directly (no tsx, no build step).
1365
1324
  */
1366
- export function taskScript(_project: javascript.NodeProject, script: string, args = ""): string {
1325
+ export function taskScript(_project: Project, script: string, args = ""): string {
1367
1326
  const scriptPath = toPosix(join("node_modules", "@dbx-tools", "projen", "tasks", script));
1368
1327
  return args ? `bun ${scriptPath} ${args}` : `bun ${scriptPath}`;
1369
1328
  }
@@ -1619,8 +1578,6 @@ function initProject(
1619
1578
  docs: options.releaseDocs,
1620
1579
  pythonRoot: options.releasePythonRoot,
1621
1580
  validationTasks: options.releaseValidationTasks,
1622
- summary: options.releaseSummary,
1623
- syncBranch: options.releaseSyncBranch,
1624
1581
  });
1625
1582
  }
1626
1583
  }
@@ -33,8 +33,7 @@ export function isDBXToolsProject(): Predicate<IConstruct, DBXToolsProject> {
33
33
  return isProject().and(
34
34
  (project): project is DBXToolsProject =>
35
35
  (project as Partial<DBXToolsProject>).language === "javascript" ||
36
- (project as Partial<DBXToolsProject>).language === "python" ||
37
- (project as Partial<DBXToolsProject>).language === "rust",
36
+ (project as Partial<DBXToolsProject>).language === "python",
38
37
  );
39
38
  }
40
39