@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
@@ -1,11 +1,5 @@
1
- /**
2
- * In-memory `package.json` `dbxToolsConfig` record for a package.
3
- *
4
- * Values are read and written on the component's object; each write flushes the
5
- * record to the manifest through `project.package.addField`.
6
- */
7
- import { object } from "@dbx-tools/shared-core";
8
- import { Component, javascript } from "projen";
1
+ /** Typed `package.json` `dbxToolsConfig` record for a package. */
2
+ import { javascript } from "projen";
9
3
 
10
4
  /** `package.json` field name for the dbx-tools config object. */
11
5
  const DBX_TOOLS_CONFIG_KEY = "dbxToolsConfig";
@@ -16,70 +10,35 @@ export interface DBXToolsConfigOptions {
16
10
  readonly tags?: string[];
17
11
  }
18
12
 
19
- function readDBXToolsConfig(pkg: javascript.NodePackage): Record<string, unknown> {
20
- try {
21
- const manifest = pkg.manifest as unknown;
22
- if (object.isRecord(manifest)) {
23
- const config = manifest[DBX_TOOLS_CONFIG_KEY];
24
- if (object.isRecord(config)) {
25
- return config;
26
- }
27
- }
28
- } catch {}
29
- return {};
30
- }
31
-
32
- function loadConfig(dbxToolsConfig: DBXToolsConfig, pkg: javascript.NodePackage) {
33
- const { tags, ...config } = readDBXToolsConfig(pkg);
34
- if (Array.isArray(tags)) {
35
- object
36
- .sequence(tags)
37
- .filter((v: unknown) => typeof v === "string")
38
- .forEach((v: string) => dbxToolsConfig.tags.push(v));
39
- }
40
- Object.entries(config).forEach(([key, value]) => {
41
- dbxToolsConfig[key] = value;
42
- });
13
+ interface DBXToolsConfigData {
14
+ readonly tags?: string[];
15
+ readonly packageRoots?: string[];
16
+ readonly syncResynthPaths?: string[];
43
17
  }
44
18
 
45
- /**
46
- * Owns a package's in-memory `dbxToolsConfig` object. Callers mutate it
47
- * directly - push to {@link DBXToolsConfig.tags}, or assign any other key
48
- * through the index signature. Every own key (i.e. not inherited from
49
- * `Component`) is collected by {@link DBXToolsConfig.data} and written through
50
- * `project.package.addField` at synth, so nothing is cached on a field.
51
- */
52
- export class DBXToolsConfig extends Component {
53
- private readonly inheritedKeys: ReadonlySet<string>;
19
+ /** Owns the typed in-memory `dbxToolsConfig` written to `package.json`. */
20
+ export class DBXToolsConfig {
54
21
  readonly tags: string[];
55
- [key: string]: unknown;
22
+ packageRoots?: string[];
23
+ syncResynthPaths?: string[];
56
24
 
57
25
  constructor(
58
- override readonly project: javascript.NodeProject,
26
+ readonly project: javascript.NodeProject,
59
27
  options: DBXToolsConfigOptions = {},
60
28
  ) {
61
- super(project);
62
- this.inheritedKeys = new Set(Object.keys(this));
63
- this.tags = [];
64
- loadConfig(this, project.package);
65
- options.tags?.forEach((v: string) => this.tags.push(v));
66
- }
67
-
68
- data(): Record<string, unknown> {
69
- const dataRecord: Record<string, unknown> = {};
70
- for (const [key, value] of Object.entries(this)) {
71
- if (this.inheritedKeys.has(key)) continue;
72
- dataRecord[key] = value;
73
- }
74
- dataRecord.tags = object.sequence(this.tags).distinct().toArray();
75
- return dataRecord;
29
+ this.tags = [...(options.tags ?? [])];
30
+ project.package.addField(DBX_TOOLS_CONFIG_KEY, () => {
31
+ const data = this.data();
32
+ return Object.keys(data).length === 0 ? undefined : data;
33
+ });
76
34
  }
77
35
 
78
- preSynthesize(): void {
79
- let dataRecord: Record<string, unknown> | undefined = this.data();
80
- if (object.isEmpty(dataRecord, { recursive: true })) {
81
- dataRecord = undefined;
82
- }
83
- this.project.package.addField(DBX_TOOLS_CONFIG_KEY, dataRecord);
36
+ data(): DBXToolsConfigData {
37
+ const tags = [...new Set(this.tags)];
38
+ return {
39
+ ...(tags.length ? { tags } : {}),
40
+ ...(this.packageRoots?.length ? { packageRoots: this.packageRoots } : {}),
41
+ ...(this.syncResynthPaths?.length ? { syncResynthPaths: this.syncResynthPaths } : {}),
42
+ };
84
43
  }
85
44
  }
package/src/generated.ts CHANGED
@@ -66,7 +66,13 @@ export function header(opts: HeaderOpts): string {
66
66
  */
67
67
  export function isGenerated(file: string): boolean {
68
68
  try {
69
- return readFileSync(file, "utf8").startsWith(MARKER);
69
+ const source = readFileSync(file, "utf8");
70
+ return (
71
+ source.startsWith(MARKER) ||
72
+ source.startsWith("// ~~ Generated by projen") ||
73
+ source.startsWith("# ~~ Generated by projen") ||
74
+ /"\/\/"\s*:\s*"~~ Generated by projen/.test(source)
75
+ );
70
76
  } catch {
71
77
  return false;
72
78
  }
@@ -69,6 +69,7 @@ export interface PublicFunctionExport {
69
69
  readonly name: string;
70
70
  readonly sourceFile: string;
71
71
  readonly sourceName: string;
72
+ readonly async: boolean;
72
73
  }
73
74
 
74
75
  /** Declaration node types that are inherently type-only. */
@@ -149,10 +150,31 @@ export function moduleExports(file: string): ModuleExport[] {
149
150
  */
150
151
  export function publicFunctionExports(file: string): PublicFunctionExport[] {
151
152
  return [...collectPublicFunctions(resolve(file), new Set()).values()]
152
- .map(({ name, sourceFile, sourceName }) => ({ name, sourceFile, sourceName }))
153
+ .map(({ name, sourceFile, sourceName, async }) => ({
154
+ name,
155
+ sourceFile,
156
+ sourceName,
157
+ async,
158
+ }))
153
159
  .sort((left, right) => left.name.localeCompare(right.name));
154
160
  }
155
161
 
162
+ /** Names of namespace exports declared as `export * as name from "..."`. */
163
+ export function publicNamespaceExports(file: string): string[] {
164
+ return moduleStatements(file)
165
+ .flatMap((statement) => {
166
+ if (statement.type !== "ExportAllDeclaration") return [];
167
+ const exported = (
168
+ statement as {
169
+ exported?: { name?: string; value?: string } | null;
170
+ }
171
+ ).exported;
172
+ const name = exported?.name ?? exported?.value;
173
+ return name ? [name] : [];
174
+ })
175
+ .sort((left, right) => left.localeCompare(right));
176
+ }
177
+
156
178
  interface ResolvedFunctionExport extends PublicFunctionExport {
157
179
  readonly source: string;
158
180
  }
@@ -230,13 +252,15 @@ function collectPublicFunctions(
230
252
  const local = specifier.local?.name ?? specifier.local?.value;
231
253
  const exportedName = specifier.exported?.name ?? specifier.exported?.value;
232
254
  const target = local ? available.get(local) : undefined;
233
- if (target && exportedName)
255
+ if (target && exportedName) {
234
256
  add({
235
257
  name: exportedName,
236
258
  source: target.source,
237
259
  sourceFile: target.sourceFile,
238
260
  sourceName: target.sourceName,
261
+ async: target.async,
239
262
  });
263
+ }
240
264
  }
241
265
  continue;
242
266
  }
@@ -252,6 +276,7 @@ function collectPublicFunctions(
252
276
  source: parsed.source,
253
277
  sourceFile: parsed.sourceFile,
254
278
  sourceName: parsed.sourceName,
279
+ async: parsed.async,
255
280
  });
256
281
  }
257
282
  }
@@ -267,6 +292,7 @@ interface FunctionNode {
267
292
  readonly generator?: boolean;
268
293
  readonly declare?: boolean;
269
294
  readonly body?: unknown;
295
+ readonly async?: boolean;
270
296
  }
271
297
 
272
298
  function parseFunctionNode(file: string, node: FunctionNode): ResolvedFunctionExport {
@@ -276,7 +302,13 @@ function parseFunctionNode(file: string, node: FunctionNode): ResolvedFunctionEx
276
302
  if (node.type === "TSDeclareFunction" || node.declare || !node.body) {
277
303
  throw new Error(`Declaration-only function export ${name} is not supported in ${file}`);
278
304
  }
279
- return { name, source: `${file}#${name}`, sourceFile: file, sourceName: name };
305
+ return {
306
+ name,
307
+ source: `${file}#${name}`,
308
+ sourceFile: file,
309
+ sourceName: name,
310
+ async: node.async === true,
311
+ };
280
312
  }
281
313
 
282
314
  function resolveModuleSpecifier(importer: string, specifier: string): string {
package/src/openapi.ts CHANGED
@@ -20,20 +20,11 @@
20
20
  * consuming workspace, which is where they already live. Speakeasy's `openapi`
21
21
  * binary is installed lazily through `@dbx-tools/core`'s binary cache.
22
22
  */
23
- import { execFile } from "node:child_process";
24
- import {
25
- existsSync,
26
- mkdirSync,
27
- mkdtempSync,
28
- readFileSync,
29
- renameSync,
30
- rmSync,
31
- writeFileSync,
32
- } from "node:fs";
23
+ import { mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
33
24
  import { createRequire } from "node:module";
34
25
  import { isAbsolute, join, relative, resolve } from "node:path";
35
- import { promisify } from "node:util";
36
26
  import * as bin from "@dbx-tools/core/bin";
27
+ import * as exec from "@dbx-tools/core/exec";
37
28
  import { find } from "@dbx-tools/path";
38
29
  import { log, object, stringUtils } from "@dbx-tools/shared-core";
39
30
  import type * as ts from "typescript";
@@ -46,7 +37,6 @@ import {
46
37
  toPosix,
47
38
  recordedPackages,
48
39
  } from "./packages.ts";
49
- import type { ReleaseUnitGraph } from "./release-catalog.ts";
50
40
  import { readWorkspaceVersion } from "./workspace-version.ts";
51
41
 
52
42
  const logger = log.logger("projen:openapi");
@@ -57,7 +47,6 @@ const OPENAPI_TAG = "openapi";
57
47
  const TSOA_IMPORT = /from\s+['"](?:tsoa|@tsoa\/runtime)['"]/;
58
48
  const SPEAKEASY_OPENAPI_VERSION = "1.24.0";
59
49
  const SPEAKEASY_OPENAPI_RELEASE_URL = `https://github.com/speakeasy-api/openapi/releases/download/v${SPEAKEASY_OPENAPI_VERSION}`;
60
- const execFileAsync = promisify(execFile);
61
50
 
62
51
  // prettier-ignore
63
52
  const CLIENT_SRC =
@@ -102,12 +91,6 @@ function inside(path: string, directory: string): boolean {
102
91
  return child === "" || (!child.startsWith("..") && !isAbsolute(child));
103
92
  }
104
93
 
105
- /** Whether one changed source belongs to any producing package directory. */
106
- export function isOpenapiProducerSource(path: string, producers: readonly string[]): boolean {
107
- const absolute = resolve(path);
108
- return object.sequence(producers).some((producer) => inside(absolute, producer));
109
- }
110
-
111
94
  /** Source roots consumed by TypeScript OpenAPI producers. */
112
95
  export function openapiWatchRoots(): string[] {
113
96
  return [
@@ -168,7 +151,10 @@ async function speakeasyOpenapiPath(): Promise<string> {
168
151
  /** Deduplicate inline schemas into `components.schemas` with Speakeasy. */
169
152
  export async function optimizeOpenapiSpec(specPath: string, executable?: string): Promise<void> {
170
153
  const openapi = executable ?? (await speakeasyOpenapiPath());
171
- await execFileAsync(openapi, ["spec", "optimize", specPath, "--write", "--non-interactive"]);
154
+ await exec.spawn(openapi, ["spec", "optimize", specPath, "--write", "--non-interactive"], {
155
+ stdin: "ignore",
156
+ check: true,
157
+ });
172
158
  }
173
159
 
174
160
  type OpenApiTypeTools = {
@@ -234,28 +220,14 @@ export async function generateOpenapi(): Promise<string[]> {
234
220
  const runtime = lazyRequire<typeof ts>(require, "typescript", "openapi generation");
235
221
  return {
236
222
  generateSpec,
237
- compilerOptions: {
238
- experimentalDecorators: true,
239
- target: runtime.ScriptTarget.ES2022,
240
- module: runtime.ModuleKind.ESNext,
241
- moduleResolution: runtime.ModuleResolutionKind.Bundler,
242
- esModuleInterop: true,
243
- skipLibCheck: true,
244
- } satisfies ts.CompilerOptions,
223
+ runtime,
245
224
  };
246
225
  })()
247
226
  : undefined;
248
227
 
249
- const releaseGraphPath = join(repoRoot, ".projen/release-units.json");
250
- const releaseGraph = existsSync(releaseGraphPath)
251
- ? (JSON.parse(readFileSync(releaseGraphPath, "utf8")) as ReleaseUnitGraph)
252
- : undefined;
228
+ const version = readWorkspaceVersion(repoRoot);
253
229
  const written: string[] = [];
254
230
  for (const p of pkgs) {
255
- const releaseProject = releaseGraph?.projects.find((project) => project.path === p.path);
256
- const specVersion =
257
- releaseGraph?.units.find((unit) => unit.id === releaseProject?.unit)?.version ??
258
- readWorkspaceVersion(repoRoot);
259
231
  // The generated package's folder is the source's leaf folder name (`api`), not
260
232
  // its npm name - `p.name` is the (possibly-overridden) manifest name.
261
233
  const leaf = p.relPath.split("/").pop() ?? p.relPath;
@@ -268,6 +240,30 @@ export async function generateOpenapi(): Promise<string[]> {
268
240
  const tempDir = mkdtempSync(join(outDir, ".openapi-"));
269
241
  const tempSpecPath = join(tempDir, "openapi.json");
270
242
  try {
243
+ const tsconfigPath = join(p.dir, "tsconfig.json");
244
+ const config = typescript!.runtime.readConfigFile(
245
+ tsconfigPath,
246
+ typescript!.runtime.sys.readFile,
247
+ );
248
+ if (config.error) {
249
+ throw new Error(
250
+ typescript!.runtime.flattenDiagnosticMessageText(config.error.messageText, "\n"),
251
+ );
252
+ }
253
+ const parsed = typescript!.runtime.parseJsonConfigFileContent(
254
+ config.config,
255
+ typescript!.runtime.sys,
256
+ p.dir,
257
+ );
258
+ if (parsed.errors.length > 0) {
259
+ throw new Error(
260
+ parsed.errors
261
+ .map((error) =>
262
+ typescript!.runtime.flattenDiagnosticMessageText(error.messageText, "\n"),
263
+ )
264
+ .join("\n"),
265
+ );
266
+ }
271
267
  await typescript!.generateSpec(
272
268
  {
273
269
  entryFile: "",
@@ -277,9 +273,9 @@ export async function generateOpenapi(): Promise<string[]> {
277
273
  specFileBaseName: "openapi",
278
274
  specVersion: 3,
279
275
  name: `${p.relPath} API`,
280
- version: specVersion,
276
+ version,
281
277
  },
282
- typescript!.compilerOptions,
278
+ parsed.options,
283
279
  );
284
280
  await optimizeOpenapiSpec(tempSpecPath);
285
281
  makeWritable(specPath);
package/src/packages.ts CHANGED
@@ -29,6 +29,7 @@ import * as projectUtils from "@dbx-tools/core/project-utils";
29
29
  import { find } from "@dbx-tools/path";
30
30
  import { json, object, stringUtils } from "@dbx-tools/shared-core";
31
31
  import { parse } from "yaml";
32
+ import { isGenerated } from "./generated.ts";
32
33
 
33
34
  /** Resolve the nearest package/projenrc root at call time. */
34
35
  export function resolveRepoRoot(cwd: string = process.cwd()): string {
@@ -86,24 +87,9 @@ function nestingTagsFromSegments(segments: readonly string[]): string[] {
86
87
  /** Matches a barrel `index.<ext>` (as a basename or a posix path tail). */
87
88
  const BARREL_RE = /(^|\/)index\.(ts|tsx|js|jsx|mjs|cjs)$/;
88
89
 
89
- /** Basenames this toolchain generates (projen manifests/tsconfigs + bun app scaffolding). */
90
- const GENERATED_BASENAMES = new Set([
91
- "package.json",
92
- "tsconfig.json",
93
- "tsconfig.dev.json",
94
- "bunfig.toml",
95
- "dev.ts",
96
- "build.ts",
97
- ]);
98
-
99
- /**
100
- * True if the file matches the watcher's generated-file heuristic: projen manifest
101
- * basenames, package-root barrels (`index.ts`), bun app scaffolding, or declaration
102
- * files. Other read-only toolchain output (e.g. openapi artifacts) is not covered here.
103
- */
90
+ /** True when a durable generated-file marker identifies the file's owner. */
104
91
  export function isGeneratedFile(file: string): boolean {
105
- const base = file.split(sep).pop() ?? "";
106
- return GENERATED_BASENAMES.has(base) || BARREL_RE.test(base) || base.endsWith(".d.ts");
92
+ return isGenerated(file);
107
93
  }
108
94
 
109
95
  /** A re-exportable source module: ts/tsx/js/jsx/mjs/cjs, not a barrel/test/decl. */
@@ -291,19 +277,6 @@ export function syncResynthPaths(projectRoot: string = resolveRepoRoot()): strin
291
277
  return Array.isArray(paths) ? stringUtils.parseList(paths.map((p) => String(p))) : [];
292
278
  }
293
279
 
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
-
307
280
  /**
308
281
  * The recorded workspace members from `pnpm-workspace.yaml` (the source of truth),
309
282
  * each augmented with the `tags` read back from its `package.json`. This is what
@@ -14,13 +14,11 @@
14
14
  * `packages` straight through; nothing derives it from `project.subprojects`.
15
15
  * The root's scan attaches packages AFTER construction, so the list is
16
16
  * resolved in `preSynthesize`.
17
- * - **The catalog is accumulated.** pnpm's `catalog:` has no projen API at all,
18
- * and tag mixins plus a consumer's `.projenrc.ts` add pins after construction.
17
+ * - **The catalog is accumulated.** Tag mixins and a consumer's `.projenrc.ts`
18
+ * add pins after construction, after Projen has captured the options object.
19
19
  * - **Build allowances are the `allowBuilds` MAP.** projen's `allowScripts`
20
20
  * renders `onlyBuiltDependencies`, and the pnpm this repo installs with (10.33)
21
- * does not read that key at ALL - its only build gate is `allowBuilds`, which
22
- * projen's schema does not type. Rendering the list would install with every
23
- * build script silently skipped.
21
+ * does not read that key at ALL - its only build gate is `allowBuilds`.
24
22
  *
25
23
  * Late mutation works because projen renders lazily (its `YamlFile` takes
26
24
  * `obj: () => toJson_PnpmWorkspaceYamlSchema(options)`) and because
@@ -44,16 +42,12 @@ import { BUN_VERSION } from "./bun-workflow.ts";
44
42
  import { toPosix } from "./packages.ts";
45
43
 
46
44
  /**
47
- * The pnpm `catalog:` version registry: dependency name -> version range. A
48
- * pnpm-workspace feature (packages reference it via a `catalog:` specifier), so
49
- * there is no projen type for it - it's just a string map.
45
+ * The pnpm `catalog:` version registry typed by Projen's workspace schema.
50
46
  */
51
- export type Catalog = Record<string, string>;
47
+ export type Catalog = NonNullable<javascript.PnpmWorkspaceYamlOptions["catalog"]>;
52
48
 
53
49
  /**
54
- * pnpm's `allowBuilds`: dependency name -> may its install scripts run. Not in
55
- * projen's schema, whose build gate is the `onlyBuiltDependencies` LIST that
56
- * current pnpm ignores.
50
+ * Stronger local type for Projen's permissively typed `allowBuilds` field.
57
51
  */
58
52
  export type AllowBuilds = Record<string, boolean>;
59
53
 
@@ -80,13 +74,11 @@ const DEFAULT_CATALOG: Catalog = {
80
74
  // `@mastra/core`'s `^3.25.0 || ^4.0.0` peer range.
81
75
  zod: "4.3.6",
82
76
  typescript: "^5.9.3",
83
- tsx: "^4.23.0",
84
77
  commander: "^15.0.0",
85
78
  "@clack/prompts": "^1.7.0",
86
79
  "openapi-fetch": "^0.17.0",
87
80
  tsoa: "^6.6.0",
88
81
  concurrently: "^10.0.3",
89
- pnpm: "^11.0.6",
90
82
  "@databricks/appkit": "0.81.0",
91
83
  "@databricks/appkit-ui": "0.81.0",
92
84
  "@databricks/sdk-experimental": "^0.17.0",
@@ -125,7 +117,8 @@ const DEFAULT_OVERRIDES: Readonly<Record<string, string>> = {
125
117
  /**
126
118
  * pnpm settings this engine applies to every workspace, beyond members, catalog,
127
119
  * and allowances. Each is stated because pnpm's own default is the weaker choice
128
- * for a projen-managed monorepo; a caller's `workspaceYaml` overrides any.
120
+ * for a projen-managed monorepo; native `pnpmOptions.workspaceYamlOptions`
121
+ * override any.
129
122
  */
130
123
  const DEFAULT_WORKSPACE_YAML: javascript.PnpmWorkspaceYamlOptions = {
131
124
  // The catalog is GENERATED (`addCatalog` in `.projenrc.ts` / a tag mixin), so
@@ -150,8 +143,6 @@ export interface DBXToolsPNPMWorkspaceOptions {
150
143
  readonly catalog?: Catalog;
151
144
  /** Initial build allowances, merged over `{ esbuild: true }`. */
152
145
  readonly allowBuilds?: AllowBuilds;
153
- /** Any other pnpm-workspace setting, typed by projen's schema. */
154
- readonly workspaceYaml?: javascript.PnpmWorkspaceYamlOptions;
155
146
  }
156
147
 
157
148
  /**
@@ -167,25 +158,26 @@ export class PnpmWorkspaceState {
167
158
  * The options object handed to projen, whose members are mutated in place.
168
159
  * Held as the live reference the native component captured.
169
160
  *
170
- * Widened with `allowBuilds`, which projen's schema type does not declare;
171
- * projen renders unrecognized keys verbatim, so it reaches the file.
172
161
  */
173
- readonly options: javascript.PnpmWorkspaceYamlOptions & { allowBuilds: AllowBuilds };
162
+ readonly options: javascript.PnpmWorkspaceYamlOptions;
174
163
 
175
164
  private readonly packages: string[] = [];
176
165
  private readonly catalog: Catalog;
177
166
  private readonly allowBuilds: AllowBuilds;
178
167
  private readonly overrides: Record<string, string>;
179
168
 
180
- constructor(options: DBXToolsPNPMWorkspaceOptions = {}) {
169
+ constructor(
170
+ options: DBXToolsPNPMWorkspaceOptions & Pick<javascript.NodeProjectOptions, "pnpmOptions"> = {},
171
+ ) {
172
+ const workspaceOptions = options.pnpmOptions?.workspaceYamlOptions;
181
173
  this.catalog = { ...DEFAULT_CATALOG, ...options.catalog };
182
174
  this.allowBuilds = { ...DEFAULT_ALLOW_BUILDS, ...options.allowBuilds };
183
175
  // Seeded even when empty so `addOverride` has a reference projen already
184
176
  // captured; projen's `omitEmpty` drops the key while it stays empty.
185
- this.overrides = { ...DEFAULT_OVERRIDES, ...options.workspaceYaml?.overrides };
177
+ this.overrides = { ...DEFAULT_OVERRIDES, ...workspaceOptions?.overrides };
186
178
  this.options = {
187
179
  ...DEFAULT_WORKSPACE_YAML,
188
- ...options.workspaceYaml,
180
+ ...workspaceOptions,
189
181
  packages: this.packages,
190
182
  catalog: this.catalog,
191
183
  allowBuilds: this.allowBuilds,
@@ -238,8 +230,9 @@ export class PnpmWorkspaceState {
238
230
  * (e.g. a future projen that DOES create it under bun).
239
231
  */
240
232
  public attachWorkspaceFile(project: Project): void {
241
- const exists = project.files.some((file) => file.path === "pnpm-workspace.yaml");
242
- if (!exists) new javascript.PnpmWorkspaceYaml(project, this.options);
233
+ if (!javascript.PnpmWorkspaceYaml.of(project)) {
234
+ new javascript.PnpmWorkspaceYaml(project, this.options);
235
+ }
243
236
  }
244
237
 
245
238
  /**
@@ -280,7 +273,9 @@ export class PnpmWorkspaceState {
280
273
  pkg.addField("catalog", { ...this.catalog });
281
274
  }
282
275
  if (Object.keys(this.overrides).length > 0) {
283
- pkg.addField("overrides", { ...this.overrides });
276
+ pkg.addPackageResolutions(
277
+ ...Object.entries(this.overrides).map(([name, version]) => `${name}@${version}`),
278
+ );
284
279
  }
285
280
  // projen renders bun's `trustedDependencies` from `allowedScripts`.
286
281
  const allowed = Object.entries(this.allowBuilds)