@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/README.md CHANGED
@@ -125,63 +125,71 @@ VS Code component; dbx-tools reuses it rather than constructing a second
125
125
 
126
126
  ### Bundle Node Code For PythonMonkey
127
127
 
128
- Add `nodeBindings` to any `DBXToolsPythonWorkspace` package to generate a
129
- committed CommonJS runtime and async Python wrappers from a public Node package.
130
- The configuration is written to `pyproject.toml`, so the generation task also
131
- works in consuming repositories after `@dbx-tools/projen` is installed.
128
+ Add `nodeBindings` to any `DBXToolsPythonWorkspace` package to generate
129
+ committed CommonJS runtimes and Python wrappers from public Node packages. Pass
130
+ one binding object or an array. Projen writes one
131
+ `[tool.dbx_tools.node_bindings]` table or multiple
132
+ `[[tool.dbx_tools.node_bindings]]` tables accordingly, so generation also works
133
+ in consuming repositories after `@dbx-tools/projen` is installed.
132
134
 
133
135
  ```ts
134
136
  new project.DBXToolsPythonWorkspace(root, {
135
137
  root: "packages/py",
136
138
  packages: [
137
139
  {
138
- directory: "auth",
139
- description: "Python bindings for example auth",
140
- nodeBindings: {
141
- package: "@example/auth",
142
- layout: "package",
143
- private: false,
144
- shimRoot: "projen/shims/python-node",
145
- functionOverrides: [
146
- {
147
- module: "@example/core/file-lock",
148
- export: "acquireFileLock",
149
- handler: "projen/shims/python-node/file-lock.ts",
150
- },
151
- ],
152
- },
140
+ directory: "postgres",
141
+ description: "Python bindings for example Postgres helpers",
142
+ nodeBindings: [
143
+ {
144
+ package: "@example/postgres",
145
+ modules: ["identity", "config"],
146
+ },
147
+ {
148
+ package: "@example/lakebase",
149
+ modules: ["address"],
150
+ },
151
+ ],
153
152
  },
154
153
  ],
155
154
  });
156
155
  ```
157
156
 
158
- `layout: "package"` owns the complete Python package under `generated-src`.
159
- Use the default `layout: "submodule"` to place generated bindings under
160
- `<module>._generated` beside handwritten source. Generated wrappers convert
161
- public function names to `snake_case`, await promises, recursively convert plain
162
- records and arrays, and proxy returned JavaScript class instances with
163
- `snake_case` async methods.
164
-
165
- Set `private: true` to keep every generated symbol out of the generated
166
- `__init__.py`. The runtime and `node_bindings.py` remain available for a
167
- handwritten consuming package to import selectively. The default is `false`.
157
+ Bindings always live under
158
+ `<python-module>/_generated/node/`. Each Python package receives one shared
159
+ `_runtime.js`; typed wrappers live under `<node-package>/<module>.py`. This
160
+ preserves Node module singleton identity across all bindings in the Python
161
+ process. The intermediate directories remain implicit namespace packages with
162
+ no generated `__init__.py`. Omit `modules` to discover every exported package
163
+ namespace that contains plain functions, or supply one module string or an array
164
+ explicitly.
165
+ Generated wrappers convert public function names to `snake_case`, preserve
166
+ synchronous functions, await promises, recursively convert plain records and
167
+ arrays, and proxy returned JavaScript class instances with `snake_case` async
168
+ methods.
168
169
 
169
170
  Every function parameter is resolved through the TypeScript compiler API and
170
171
  rendered with its Python-equivalent name and type. Supported record types become
171
172
  keyword-only Python dataclasses, nested records become nested dataclasses, and a
172
173
  same-named exported companion's `defaults()` method supplies field defaults. A
173
- trailing optional record can be passed as the dataclass, a dictionary, or direct
174
- snake-case keyword fields. Generation fails with the property path when a type
175
- cannot be represented safely in Python.
174
+ record parameter can be passed as the generated dataclass or a dictionary.
175
+ Generation fails with the property path when a type cannot be represented safely
176
+ in Python.
176
177
 
177
178
  Return types use the same compiler model. Plain records become generated
178
179
  `TypedDict` responses, returned clients and class instances become `Protocol`
179
180
  types with typed async methods, and primitives, arrays, maps, promises, and
180
181
  optional values retain their corresponding Python annotations.
181
182
 
182
- The workspace creates `<name>:python-runtime`,
183
- `<name>:python-runtime:check`, and, for workspace Node dependencies,
184
- `<name>:python-runtime:watch`. The root sync watcher includes the watch task.
183
+ The generator resolves portable Node built-ins through standard browser
184
+ polyfills. Python-host-backed adapters for process execution, files, crypto,
185
+ OS values, and process state are included automatically, so packages do not
186
+ configure a shim directory.
187
+
188
+ The workspace creates `<name>:python-runtime` and
189
+ `<name>:python-runtime:check`. Full synthesis regenerates bindings and removes
190
+ stale `_generated/node` trees by reconciling them with the current
191
+ `pyproject.toml` files, including the resynthesis triggered by a `.projenrc.ts`
192
+ change.
185
193
  Built-in shims can make ordinary Node imports work under PythonMonkey.
186
194
  `functionOverrides` replaces named exports only in the generated runtime, so the
187
195
  Node package does not gain Python callbacks or alternate source files. A
@@ -264,15 +272,14 @@ barrels.generateBarrels();
264
272
 
265
273
  `generateCodegen()` reads `package.json` `codegen.inputs` and writes generated
266
274
  schema modules. They are written read-only, and the root ESLint task runs with
267
- `--fix` (which fails on a read-only file), so each generated module is added to
275
+ an explicit separate fix task, so each generated module is added to
268
276
  `ignorePatterns` at synth - named individually via `codegen.codegenModulePaths()`,
269
277
  never as a blanket `<package>/src/**`. A codegen package may hold hand-written
270
278
  modules beside its generated ones, and those must stay linted.
271
279
 
272
280
  `generateBarrels()` writes package-root `index.ts` barrels with module
273
- namespaces, flat unique type exports, `PACKAGE_IDENTIFIER`, and
274
- `PACKAGE_VERSION`, returning the number that actually changed. A name two
275
- modules both declare is ambiguous and stays namespace-only —
281
+ namespaces, flat unique type exports, and `PACKAGE_IDENTIFIER`, returning the
282
+ number that actually changed. A name two modules both declare is ambiguous and stays namespace-only,
276
283
  except when one of them is generated: the hand-written module is the curated view
277
284
  of the generated shape (`shared-genie`'s `genie-model.ts` extends its own
278
285
  codegen'd `dashboards.ts`), so it owns the name and stays hoisted. A barrel whose export surface is unchanged is left untouched,
@@ -325,10 +332,7 @@ tree ROOT only; a member package never gets a nested one.
325
332
  import { clean, watch } from "@dbx-tools/projen";
326
333
 
327
334
  const generated = clean.listGeneratedFiles();
328
- watch.watchLoop({
329
- roots: watch.watchRoots(),
330
- onChange: async (files) => console.log(files),
331
- });
335
+ watch.watchLoop("workspace", watch.watchRoots(), async (files) => refresh(files));
332
336
  ```
333
337
 
334
338
  Use these modules for maintenance tasks that should follow the same generated
@@ -346,7 +350,6 @@ file contract as the CLI.
346
350
  - `barrels` / `moduleExports` - public entrypoint generation.
347
351
  - `codegen` - `.d.ts` to zod schema generation.
348
352
  - `openapi` - tsoa/OpenAPI package generation.
349
- - `releaseCatalog` - cross-language package ownership and publication order.
350
353
  - `bunApp` / `tsconfig` / `vscode` - generated support files/components.
351
354
  - `generated` / `clean` / `watch` / `scaffold` - read-only file ownership,
352
355
  cleanup, watchers, and synth orchestration.
@@ -366,24 +369,27 @@ packages in up to four `tsc --build` processes and runs custom compile tasks alo
366
369
  `test` delegates with `bun run --filter '*'`. Both read the current workspace
367
370
  list, so a new package is covered without a re-synth. Work from the root:
368
371
 
369
- | Task | What it does |
370
- | ------------------------------ | ---------------------------------------------------- |
371
- | `bun run build` | synth + workspace compile and tests |
372
- | `bun run compile` | Batched `tsc --build` plus custom member compiles |
373
- | `bun run test` | `eslint` once, then each member's tests |
374
- | `bun run sync` | re-synth (`--watch` to keep synthing) |
375
- | `bun run barrels` | regenerate the read-only `index.ts` barrels |
376
- | `bun run bump` | increment `VERSION` and regenerate version surfaces |
377
- | `bun run version:check` | verify every generated version against `VERSION` |
378
- | `bun run release` | run the configured release transaction |
379
-
380
- Run exactly `bun run release` from a clean `main`. It calls the existing `bump`
381
- task, commits the generated version changes, pushes `main`, and pushes the
382
- matching annotated `vX.Y.Z` tag. Pass `--no-bump` only to release an existing
383
- synchronized local bump. The tag workflow verifies that the tag points at the
384
- exact `origin/main` commit, publishes npm and PyPI directly from that checkout,
385
- then builds and deploys documentation. Local npm and Python registry publication
386
- remains available through the direct local publication tooling.
372
+ | Task | What it does |
373
+ | ----------------------- | --------------------------------------------------- |
374
+ | `bun run build` | synth + workspace compile and tests |
375
+ | `bun run compile` | Batched `tsc --build` plus custom member compiles |
376
+ | `bun run test` | `eslint` once, then each member's tests |
377
+ | `bun run sync` | re-synth (`--watch` to keep synthing) |
378
+ | `bun run barrels` | regenerate the read-only `index.ts` barrels |
379
+ | `bun run bump` | increment `VERSION` and regenerate version surfaces |
380
+ | `bun run version:check` | verify every generated version against `VERSION` |
381
+ | `bun run release` | run the configured release transaction |
382
+
383
+ Run exactly `bun run release` from any branch. Pending changes are committed and
384
+ the current branch is pushed first. A non-`main` branch then fast-forwards
385
+ `main`; the command fails without creating a merge commit if either branch has
386
+ diverged. From `main`, it calls the existing `bump` task, commits the generated
387
+ version changes, pushes `main`, and pushes the matching annotated `vX.Y.Z` tag.
388
+ Pass `--no-bump` only to release an existing synchronized local bump. The tag
389
+ workflow verifies that the tag points at the exact `origin/main` commit,
390
+ publishes npm and PyPI directly from that checkout, then builds and deploys
391
+ documentation. Local npm and Python registry publication remains available
392
+ through the direct local publication tooling.
387
393
 
388
394
  Members intentionally keep only the tasks that something OTHER than a human
389
395
  invokes, so there is no second place to run the same thing:
@@ -412,6 +418,6 @@ from the root instead.
412
418
  ## Versioning
413
419
 
414
420
  `VERSION` is the language-neutral version seam. Real package manifests remain
415
- Projen-owned and reproduce it exactly across JavaScript and Python packages. Package and
416
- artifact discovery still supplies publication order and target inventory; it
417
- does not create independent version ownership.
421
+ Projen-owned and reproduce it exactly across JavaScript and Python packages.
422
+ Release jobs publish the generated workspace inventory in dependency-safe
423
+ workflow order without introducing another version owner.
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
- export const PACKAGE_VERSION = "0.9.32";
6
+ export const PACKAGE_VERSION = "0.9.37";
7
7
  export * as barrels from "./src/barrels.ts";
8
8
  export * as bunApp from "./src/bun-app.ts";
9
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
@@ -26,7 +26,6 @@ export * as publish from "./src/publish.ts";
26
26
  export * as pythonNodeBindings from "./src/python-node-bindings.ts";
27
27
  export * as pythonNodeBundle from "./src/python-node-bundle.ts";
28
28
  export * as release from "./src/release.ts";
29
- export * as releaseCatalog from "./src/release-catalog.ts";
30
29
  export * as releaseContext from "./src/release-context.ts";
31
30
  export * as scaffold from "./src/scaffold.ts";
32
31
  export * as tags from "./src/tags.ts";
@@ -50,23 +49,21 @@ export { PnpmWorkspaceState } from "./src/pnpm-workspace.ts";
50
49
  export type { Catalog, AllowBuilds, DBXToolsPNPMWorkspaceOptions } from "./src/pnpm-workspace.ts";
51
50
  export type { DBXToolsProjectLanguage, DBXToolsProjectOptions, DBXToolsProject, ApplyToProjectsOptions } from "./src/project.ts";
52
51
  export { PackageIdentifier, DBX_TOOLS_LICENSE, DBXToolsNodeProject, ROOT_INSTALL_ONLY_MIXIN, DBXToolsTypeScriptProject } from "./src/project-js.ts";
53
- export type { DBXToolsJavaScriptProject, DBXToolsReleaseMode, PullRequestTitlePolicyOptions, DBXToolsJavaScriptProjectOptions, DBXToolsTypeScriptProjectOptions } from "./src/project-js.ts";
52
+ export type { DBXToolsJavaScriptProject, DBXToolsReleaseMode, DBXToolsJavaScriptProjectOptions, DBXToolsTypeScriptProjectOptions } from "./src/project-js.ts";
54
53
  export { DBXToolsPythonProject, DBXToolsPythonWorkspace } from "./src/project-py.ts";
55
- export type { PythonRepositoryOptions, PythonPackageOptions, PythonTrustedPublisherOptions, DBXToolsPythonProjectOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
54
+ export type { PythonRepositoryOptions, PythonPackageOptions, DBXToolsPythonProjectOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
56
55
  export { PROJEN_VERSION } from "./src/projen-version.ts";
57
56
  export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
57
+ export { PYTHON_NODE_SHIM_ROOT } from "./src/python-node-bindings.ts";
58
58
  export type { ResolvedPythonNodeFunctionOverride, ResolvedPythonNodeBindings } from "./src/python-node-bindings.ts";
59
59
  export { PythonNodeBundle } from "./src/python-node-bundle.ts";
60
60
  export type { PythonNodeFunctionOverride, PythonNodeBindingsOptions, PythonNodeBundleOptions } from "./src/python-node-bundle.ts";
61
61
  export { DBXToolsRelease } from "./src/release.ts";
62
- export type { ReleaseStage, ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
63
- export { DBXToolsReleaseCatalog } from "./src/release-catalog.ts";
64
- export type { ReleaseProjectLanguage, ReleaseArtifactKind, ReleaseEdgeKind, ReleasePropagation, ReleaseUnitRule, DBXToolsReleaseCatalogOptions, ReleaseDependencyInput, ReleaseProjectRegistration, ExternalReleaseProjectRegistration, ReleaseArtifactRegistration, ReleaseProjectNode, ReleaseArtifact, ReleaseDependencyEdge, ReleaseUnit, ReleaseUnitGraph } from "./src/release-catalog.ts";
62
+ export type { ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
65
63
  export { RELEASE_TAG, RELEASE_SHA, RELEASE_VERSION } from "./src/release-context.ts";
66
64
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
67
65
  export type { PackageTag } from "./src/tags.ts";
68
66
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
69
- export { DBXToolsVsCode } from "./src/vscode.ts";
70
67
  export type { IgnoreGroupOptions, WatchLoopOptions } from "./src/watch.ts";
71
68
  export type { WorkspaceMutationLockOptions } from "./src/workspace-lock.ts";
72
69
  export { VERSION_FILE, DEFAULT_VERSION } from "./src/workspace-version.ts";
package/package.json CHANGED
@@ -24,28 +24,28 @@
24
24
  "devDependencies": {
25
25
  "@types/node": "^24.6.0",
26
26
  "@types/semver": "^7.7.1",
27
- "constructs": "^10.0.0",
28
27
  "projen": "0.103.27",
29
- "tsoa": "^6.6.0",
30
- "typescript": "^5.9.3"
28
+ "tsoa": "^6.6.0"
31
29
  },
32
30
  "peerDependencies": {
33
31
  "projen": "^0.103.27"
34
32
  },
35
33
  "dependencies": {
36
34
  "@clack/prompts": "^1.7.0",
37
- "@dbx-tools/core": "^0.9.32",
38
- "@dbx-tools/path": "^0.9.32",
39
- "@dbx-tools/shared-core": "^0.9.32",
35
+ "@dbx-tools/core": "^0.9.37",
36
+ "@dbx-tools/path": "^0.9.37",
37
+ "@dbx-tools/shared-core": "^0.9.37",
40
38
  "commander": "^15.0.0",
41
39
  "concurrently": "^10.0.3",
42
40
  "constructs": "^10.6.0",
43
41
  "is-identifier": "^1",
42
+ "node-stdlib-browser": "^1.3.1",
44
43
  "openapi-typescript": "^7.13.0",
45
44
  "oxc-parser": "^0.90.0",
46
45
  "semver": "^7.7.3",
47
46
  "smol-toml": "1.8.0",
48
47
  "ts-to-zod": "^5.1.0",
48
+ "typescript": "^5.9.3",
49
49
  "yaml": "^2.9.0"
50
50
  },
51
51
  "devEngines": {
@@ -56,11 +56,12 @@
56
56
  },
57
57
  "main": "index.ts",
58
58
  "license": "Apache-2.0",
59
- "version": "0.9.32",
59
+ "version": "0.9.37",
60
60
  "types": "index.ts",
61
61
  "type": "module",
62
62
  "exports": {
63
63
  ".": "./index.ts",
64
+ "./release-packaging": "./tasks/lib/publish-npm.ts",
64
65
  "./package.json": "./package.json"
65
66
  },
66
67
  "files": [
@@ -12,13 +12,6 @@ export interface CaptureTaskCommandOptions extends TaskCommandOptions {
12
12
  readonly check?: boolean;
13
13
  }
14
14
 
15
- export type ProbeTaskCommandOptions = TaskCommandOptions;
16
-
17
- export interface LoggedTaskCommandOptions extends TaskCommandOptions {
18
- /** Receive normalized stdout and stderr lines after the command exits. */
19
- readonly onLine: (line: string) => void;
20
- }
21
-
22
15
  function executable(command: string): string {
23
16
  return command === "bun" && process.versions.bun ? process.execPath : command;
24
17
  }
@@ -76,47 +69,6 @@ export function captureTaskCommand(
76
69
  return result.exitCode === 0 ? (result.stdout?.trim() ?? "") : "";
77
70
  }
78
71
 
79
- /** Capture trimmed stdout while preserving command failure as `undefined`. */
80
- export function probeTaskCommand(
81
- cwd: string,
82
- command: string,
83
- args: readonly string[],
84
- options: ProbeTaskCommandOptions = {},
85
- ): string | undefined {
86
- const result = exec.spawnSync(executable(command), [...args], {
87
- cwd,
88
- env: options.env,
89
- stdout: "capture",
90
- stderr: options.stderr ?? "ignore",
91
- stdin: "ignore",
92
- check: false,
93
- });
94
- return result.exitCode === 0 ? (result.stdout?.trim() ?? "") : undefined;
95
- }
96
-
97
- /** Run a checked command while routing captured output through one line policy. */
98
- export function runLoggedTaskCommand(
99
- cwd: string,
100
- command: string,
101
- args: readonly string[],
102
- options: LoggedTaskCommandOptions,
103
- ): void {
104
- const result = exec.spawnSync(executable(command), [...args], {
105
- cwd,
106
- env: options.env,
107
- stdout: "capture",
108
- stderr: "capture",
109
- stdin: "ignore",
110
- check: false,
111
- });
112
- for (const line of [...result.stdoutLines, ...result.stderrLines]) {
113
- if (line) options.onLine(line);
114
- }
115
- if (result.exitCode !== 0) {
116
- throw new Error(`${command} exited with ${result.exitCode}`);
117
- }
118
- }
119
-
120
72
  /** Whether a quiet, non-interactive command exits successfully. */
121
73
  export function taskCommandSucceeds(
122
74
  cwd: string,
@@ -135,30 +87,3 @@ export function taskCommandSucceeds(
135
87
  }).exitCode === 0
136
88
  );
137
89
  }
138
-
139
- /** Run Git with the shared non-interactive checked command policy. */
140
- export function runGitTaskCommand(
141
- cwd: string,
142
- args: readonly string[],
143
- options: TaskCommandOptions = {},
144
- ): void {
145
- runTaskCommand(cwd, "git", args, options);
146
- }
147
-
148
- /** Capture Git output, returning an empty string for optional offline probes. */
149
- export function captureGitTaskCommand(
150
- cwd: string,
151
- args: readonly string[],
152
- options: CaptureTaskCommandOptions = {},
153
- ): string {
154
- return captureTaskCommand(cwd, "git", args, options);
155
- }
156
-
157
- /** Whether a quiet Git command succeeds. */
158
- export function gitTaskCommandSucceeds(
159
- cwd: string,
160
- args: readonly string[],
161
- options: Pick<TaskCommandOptions, "env"> = {},
162
- ): boolean {
163
- return taskCommandSucceeds(cwd, "git", args, options);
164
- }
package/src/barrels.ts CHANGED
@@ -29,10 +29,8 @@
29
29
  * `export { ... }`. The module namespaces stay either way, so a namespaced call
30
30
  * site keeps working.
31
31
  *
32
- * Every generated barrel also exports `PACKAGE_IDENTIFIER` and
33
- * `PACKAGE_VERSION` from the package's own `package.json`. Runtime helpers can
34
- * retain package identity without trying to recover it from an ESM namespace
35
- * object or loader function.
32
+ * Every generated barrel also exports `PACKAGE_IDENTIFIER` and `PACKAGE_VERSION`
33
+ * from the package's own `package.json`.
36
34
  *
37
35
  * Uniqueness is tallied over types and values TOGETHER: a name carried by two
38
36
  * modules is ambiguous whichever kind it is, and hoisting one module's value
@@ -111,10 +109,10 @@ const BARREL_HEADER: HeaderOpts = {
111
109
 
112
110
  /** Generated package metadata exports, reserved against source-module hoisting. */
113
111
  const PACKAGE_IDENTIFIER_EXPORT = "PACKAGE_IDENTIFIER";
114
- const PACKAGE_VERSION_EXPORT = "PACKAGE_VERSION";
115
112
  const PACKAGE_IDENTIFIER_LINE = `export const ${PACKAGE_IDENTIFIER_EXPORT} = "";`;
116
- const PACKAGE_VERSION_LINE = `export const ${PACKAGE_VERSION_EXPORT} = "";`;
117
113
  const PACKAGE_IDENTIFIER_LINE_RE = /^export const PACKAGE_IDENTIFIER = .*;$/m;
114
+ const PACKAGE_VERSION_EXPORT = "PACKAGE_VERSION";
115
+ const PACKAGE_VERSION_LINE = `export const ${PACKAGE_VERSION_EXPORT} = "";`;
118
116
  const PACKAGE_VERSION_LINE_RE = /^export const PACKAGE_VERSION = .*;$/m;
119
117
 
120
118
  /** `config-tools` / `config_tools` -> `configTools`; `local-fs` -> `localFS`. */
@@ -123,7 +121,8 @@ function moduleSegmentToCamel(segment: string): string {
123
121
  ...stringUtils.tokenizeWithOptions({ lowerCase: true, capitalize: true }, segment),
124
122
  ];
125
123
  if (tokens.length === 0) return segment;
126
- const [first, ...rest] = tokens;
124
+ const first = tokens[0]!;
125
+ const rest = tokens.slice(1);
127
126
  // Standalone acronym modules (`fs`, `ai`) stay lowercase so they match
128
127
  // Node-style namespaces (`import * as fs`). Trailing / mid acronyms keep
129
128
  // their override casing (`localFS`).
@@ -274,7 +273,7 @@ function customExportNames(file: string): Set<string> {
274
273
  */
275
274
  function mergeCustomExports(content: string, pkgDir: string): string {
276
275
  const customPath = join(pkgDir, CUSTOM_EXPORTS_FILE);
277
- if (!existsSync(customPath) || isLegacyBindingsExport(customPath)) return content;
276
+ if (!existsSync(customPath)) return content;
278
277
  const overridden = customExportNames(customPath);
279
278
  const kept = content.split("\n").filter((line) => {
280
279
  const ns = /^export \* as (\w+) from /.exec(line)?.[1];
@@ -283,12 +282,11 @@ function mergeCustomExports(content: string, pkgDir: string): string {
283
282
  return `${kept.join("\n").replace(/\n+$/, "")}\nexport * from "./exports.ts";\n`;
284
283
  }
285
284
 
286
- function isLegacyBindingsExport(file: string): boolean {
287
- return readFileSync(file, "utf8").trim() === 'export * from "./src/bindings.ts";';
288
- }
289
-
290
- /** Read the authoritative npm package name and version emitted by the package project. */
291
- function packageMetadata(pkgDir: string): { identifier: string; version: string } {
285
+ /** Read the authoritative npm package metadata emitted by the package project. */
286
+ function packageMetadata(pkgDir: string): {
287
+ identifier: string;
288
+ version: string;
289
+ } {
292
290
  const manifestPath = join(pkgDir, "package.json");
293
291
  const manifest = existsSync(manifestPath)
294
292
  ? json.parseRecord(readFileSync(manifestPath, "utf8"))
@@ -404,10 +402,7 @@ function generateForPackage(pkgDir: string): number {
404
402
  // Hoist package-unique named exports to the top level. Names a hand-authored
405
403
  // `exports.ts` declares are suppressed so that file stays authoritative.
406
404
  const customPath = join(pkgDir, CUSTOM_EXPORTS_FILE);
407
- const suppress =
408
- existsSync(customPath) && !isLegacyBindingsExport(customPath)
409
- ? customExportNames(customPath)
410
- : new Set<string>();
405
+ const suppress = existsSync(customPath) ? customExportNames(customPath) : new Set<string>();
411
406
  suppress.add(PACKAGE_IDENTIFIER_EXPORT);
412
407
  suppress.add(PACKAGE_VERSION_EXPORT);
413
408
  content = hoistUniqueExports(content, pkgDir, suppress);
@@ -415,11 +410,8 @@ function generateForPackage(pkgDir: string): number {
415
410
  content = mergeCustomExports(content, pkgDir);
416
411
 
417
412
  // Package metadata is blanked for the structural comparison, then restored
418
- // from package.json so a version-only change still rewrites the barrel.
419
- // If the structural result and package metadata match what's on disk, leave
420
- // the file and its read-only bit untouched and report no change (0). This
421
- // keeps the watcher quiet on ordinary in-file edits while allowing a version
422
- // bump to update PACKAGE_VERSION.
413
+ // from package.json. If the result matches what's on disk, leave the file and
414
+ // its read-only bit untouched and report no change.
423
415
  content = `${content.replace(/\n+$/, "")}\n`;
424
416
  const template = `${header(BARREL_HEADER)}\n${content}`;
425
417
  const next = withPackageMetadata(template, pkgDir);
package/src/bun-app.ts CHANGED
@@ -14,10 +14,9 @@
14
14
  * JS API, NOT the `bun build` CLI, which does not load it), plus Vite-compatible
15
15
  * staging of an optional `public/` directory.
16
16
  *
17
- * Each file supports an unmanaged OVERRIDE beside it (`bunfig.override.toml`,
18
- * `bun-dev.override.ts`, `bun-build.override.ts`): the dev/build scripts import
19
- * the override's default export and merge it over the generated options, so a
20
- * package tweaks its server/bundle WITHOUT editing the projen-owned file.
17
+ * The dev and build scripts support unmanaged `bun-dev.override.ts` and
18
+ * `bun-build.override.ts` modules beside them. Each script imports the override's
19
+ * default export and merges it over the generated options.
21
20
  *
22
21
  * bun runs these `.ts` files directly. Being package-ROOT files (not under
23
22
  * `src/`), they are outside the package's `tsconfig` include, so their `Bun.*`
@@ -30,8 +29,8 @@ import { type Project, TextFile } from "projen";
30
29
  export const BUN_DEV_OVERRIDE = "bun-dev.override.ts";
31
30
  /** Unmanaged build override module, merged over the generated options if present. */
32
31
  export const BUN_BUILD_OVERRIDE = "bun-build.override.ts";
33
- /** Unmanaged bunfig override (documented; merging TOML is left to the user). */
34
- export const BUN_APP_OVERRIDES = ["bunfig.override.toml", BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE];
32
+ /** Unmanaged generated-script overrides excluded from formatting and linting. */
33
+ export const BUN_APP_OVERRIDES = [BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE];
35
34
 
36
35
  /**
37
36
  * Root `bunfig.toml`: pin bun's HOISTED linker workspace-wide.
package/src/clean.ts CHANGED
@@ -5,13 +5,9 @@
5
5
  * `@clack/prompts` multiselect picker with all preselected, the TTY guard) lives in
6
6
  * `tasks/clean.ts`, which forwards to these functions.
7
7
  *
8
- * "Generated" is detected structurally, not by a hardcoded list: every file this
9
- * toolchain writes is set READ-ONLY (projen's own config + the generated barrels; see
10
- * {@link isReadonly}), while every hand-authored source stays writable. So a read-only
11
- * file under the repo is a clean target - EXCEPT anything inside a dot-prefixed folder
12
- * (`.projen`, `.vscode`, `.git`, ...) and `.gitignore` itself, which clean always leaves
13
- * alone (projen re-syncs `.projen`/`.vscode` on the next synth). `node_modules` is
14
- * enumerated separately (see {@link listNodeModulesDirs}) as whole directories.
8
+ * Generated files come from Projen's `.projen/files.json` inventories plus the
9
+ * durable header used by dynamic dbx-tools generators. File mode is edit
10
+ * protection only and is never treated as ownership evidence.
15
11
  *
16
12
  * Deleting only generated files is never destructive to the ability to regenerate:
17
13
  * `.projenrc.ts` imports the engine by SOURCE path (relative into the repo, e.g.
@@ -21,10 +17,11 @@
21
17
  * `bun install` first - the engine's runtime deps live there - so a clean that takes
22
18
  * `node_modules` must be followed by reinstall, then re-synth.
23
19
  */
24
- import { existsSync, rmSync, statSync } from "node:fs";
25
- import { basename, join, relative } from "node:path";
20
+ import { existsSync, readFileSync, rmSync, statSync } from "node:fs";
21
+ import { basename, dirname, join, relative, resolve } from "node:path";
26
22
  import { find } from "@dbx-tools/path";
27
- import { isReadonly, makeWritable } from "./generated.ts";
23
+ import { json } from "@dbx-tools/shared-core";
24
+ import { isGenerated, makeWritable } from "./generated.ts";
28
25
  import { resolveRepoRoot, toPosix } from "./packages.ts";
29
26
 
30
27
  /**
@@ -35,16 +32,41 @@ import { resolveRepoRoot, toPosix } from "./packages.ts";
35
32
  const CLEAN_SKIP_FILES: ReadonlySet<string> = new Set([".gitignore"]);
36
33
 
37
34
  /**
38
- * Every generated (read-only) file in the workspace, as absolute paths sorted by
35
+ * Every generated file in the workspace, as absolute paths sorted by
39
36
  * repo-relative posix path. Skips vendor/build/VCS dirs via node-path's built-in
40
37
  * ignores AND every dot-prefixed folder (`.projen`, `.vscode`, `.github`, ...), and
41
38
  * {@link CLEAN_SKIP_FILES} entry (`.gitignore`).
42
39
  */
40
+ function nativeGeneratedFiles(root: string): string[] {
41
+ const files = new Set<string>();
42
+ for (const manifestPath of find.findFiles("**/.projen/files.json", {
43
+ cwd: root,
44
+ ignoreOptions: { dot: false },
45
+ })) {
46
+ const manifest = json.parseRecord(readFileSync(resolve(root, manifestPath), "utf8"));
47
+ const projectRoot = dirname(dirname(resolve(root, manifestPath)));
48
+ const entries = manifest?.files;
49
+ if (!Array.isArray(entries)) continue;
50
+ for (const entry of entries) {
51
+ if (typeof entry === "string") files.add(resolve(projectRoot, entry));
52
+ }
53
+ }
54
+ return [...files];
55
+ }
56
+
43
57
  export function listGeneratedFiles(root: string = resolveRepoRoot()): string[] {
44
58
  const rel = (f: string): string => toPosix(relative(root, f));
45
- return [...find.findFiles("**/*", { cwd: root })]
59
+ const custom = [...find.findFiles("**/*", { cwd: root })]
46
60
  .map((f) => join(root, f))
47
- .filter(isReadonly)
61
+ .filter(isGenerated);
62
+ return [...new Set([...nativeGeneratedFiles(root), ...custom])]
63
+ .filter(existsSync)
64
+ .filter(
65
+ (file) =>
66
+ !rel(file)
67
+ .split("/")
68
+ .some((part) => part.startsWith(".")),
69
+ )
48
70
  .filter((f) => !CLEAN_SKIP_FILES.has(basename(f)))
49
71
  .sort((a, b) => rel(a).localeCompare(rel(b)));
50
72
  }
@@ -74,15 +96,19 @@ export function listNodeModulesDirs(root: string = resolveRepoRoot()): string[]
74
96
  */
75
97
  export function removePaths(paths: readonly string[]): number {
76
98
  let removed = 0;
99
+ const failures: unknown[] = [];
77
100
  for (const path of paths) {
78
101
  try {
79
102
  if (!existsSync(path)) continue;
80
103
  if (statSync(path).isFile()) makeWritable(path);
81
104
  rmSync(path, { recursive: true, force: true });
82
105
  removed++;
83
- } catch {
84
- // Already gone, or racing the watcher - nothing to do.
106
+ } catch (error) {
107
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") failures.push(error);
85
108
  }
86
109
  }
110
+ if (failures.length > 0) {
111
+ throw new AggregateError(failures, `Could not remove ${failures.length} selected path(s)`);
112
+ }
87
113
  return removed;
88
114
  }
package/src/codegen.ts CHANGED
@@ -49,7 +49,7 @@ import { basename, dirname, join, resolve } from "node:path";
49
49
  import { log, object } from "@dbx-tools/shared-core";
50
50
  import type * as ts from "typescript";
51
51
  import { lazyRequire } from "./_lazy-require.ts";
52
- import { header, isReadonly, makeReadonly, makeWritable } from "./generated.ts";
52
+ import { header, makeReadonly, makeWritable } from "./generated.ts";
53
53
  import { readPackageManifest, recordedPackages, resolveRepoRoot } from "./packages.ts";
54
54
 
55
55
  const logger = log.logger("projen:codegen");
@@ -266,11 +266,10 @@ function inlineInferredTypes(inferredFile: string): string {
266
266
  .trim();
267
267
  }
268
268
 
269
- /** True if `<srcDir>/<file>` is a codegen-generated module (read-only + our header). */
269
+ /** True if `<srcDir>/<file>` carries the codegen ownership header. */
270
270
  function isGeneratedModule(srcDir: string, file: string): boolean {
271
271
  if (!file.endsWith(".ts")) return false;
272
272
  const path = join(srcDir, file);
273
- if (!isReadonly(path)) return false;
274
273
  try {
275
274
  return readFileSync(path, "utf8").startsWith("// GENERATED by projen synth (codegen:");
276
275
  } catch {