@dbx-tools/projen 0.6.142 → 0.6.144

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.
package/README.md CHANGED
@@ -88,6 +88,43 @@ Git `#subdirectory=` URL. `root.vscode` is projen's existing VS Code component;
88
88
  dbx-tools reuses it rather than constructing a second `.vscode/settings.json`
89
89
  owner.
90
90
 
91
+ ## Add A Rust Workspace
92
+
93
+ `DBXToolsRustWorkspace` discovers every source-bearing folder under
94
+ `packages/rs`, generates its Cargo manifest, and derives the crate name from the
95
+ root scope plus folder name. A crate containing `uniffi::setup_scaffolding!()`
96
+ automatically wires matching private Node and Python binding packages using the
97
+ same capability name. Repository-specific dependencies and features remain
98
+ declarative options in `.projenrc.ts`; generated bindings are built separately
99
+ from projen synthesis.
100
+
101
+ `sync --watch` runs a focused Rust watcher beside the OpenAPI watcher. Changes
102
+ inside an existing UniFFI crate regenerate only that crate's bindings; adding or
103
+ removing a crate or `setup_scaffolding!()` marker triggers a full synth. Repos
104
+ without Rust crates start no Rust watcher. When Rust projects are detected,
105
+ Cargo is required and the focused task fails immediately if it is unavailable.
106
+
107
+ The workspace also generates a `rust-release` workflow from those discovered
108
+ UniFFI crates. It builds Linux x64/arm64, macOS x64/arm64, and Windows x64.
109
+ Node publishes a small facade plus optional OS/CPU packages, so npm installs
110
+ only the matching native library. Python publishes one platform-tagged wheel
111
+ per target, so pip follows the same thin-install model.
112
+
113
+ Set the repository variable `LOCAL_REPOSITORIES=true` to enable the generated
114
+ self-hosted mirror job. Configure `LOCAL_NPM_REGISTRY` and
115
+ `LOCAL_PYPI_PUBLISH_URL` with their matching `LOCAL_*` credentials. Set
116
+ `LOCAL_CARGO_REGISTRY` to a named Cargo registry such as a loopback
117
+ [Kellnr](https://kellnr.io/) instance and provide `LOCAL_CARGO_TOKEN`. The runner
118
+ detects its OS, architecture, libc, Rust target, and Python wheel tag and builds
119
+ only that native target. Public Cargo crates also publish directly to crates.io
120
+ with `CARGO_REGISTRY_TOKEN`. Override `releaseTargets` only when a consumer has
121
+ additional native runners; ordinary projects inherit the maintained matrix
122
+ automatically.
123
+
124
+ Private Python projects are marked with `[tool.dbx-tools] private = true`. They
125
+ are excluded from the uv workspace, documentation, and Python release workflow
126
+ until their native artifact publishing matrix is enabled.
127
+
91
128
  ## Customize Packages With Mixins
92
129
 
93
130
  ```ts
package/index.ts CHANGED
@@ -19,11 +19,13 @@ export * as project from "./src/project.ts";
19
19
  export * as projectJs from "./src/project-js.ts";
20
20
  export * as projectPredicate from "./src/project-predicate.ts";
21
21
  export * as projectPy from "./src/project-py.ts";
22
+ export * as projectRs from "./src/project-rs.ts";
22
23
  export * as publish from "./src/publish.ts";
23
24
  export * as release from "./src/release.ts";
24
25
  export * as scaffold from "./src/scaffold.ts";
25
26
  export * as tags from "./src/tags.ts";
26
27
  export * as tsconfig from "./src/tsconfig.ts";
28
+ export * as uniffi from "./src/uniffi.ts";
27
29
  export * as vscode from "./src/vscode.ts";
28
30
  export * as watch from "./src/watch.ts";
29
31
  export * as workspaceVersion from "./src/workspace-version.ts";
@@ -43,12 +45,16 @@ export { PackageIdentifier, PROJEN_VERSION, DBXToolsNodeProject, ROOT_INSTALL_ON
43
45
  export type { DBXToolsJavaScriptProject, DBXToolsJavaScriptProjectOptions, DBXToolsTypeScriptProjectOptions } from "./src/project-js.ts";
44
46
  export { DBXToolsPythonProject, DBXToolsPythonWorkspace } from "./src/project-py.ts";
45
47
  export type { PythonRepositoryOptions, PythonPackageOptions, DBXToolsPythonProjectOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
48
+ export { UNIFFI_RELEASE_TARGETS, DBXToolsRustProject, DBXToolsRustWorkspace } from "./src/project-rs.ts";
49
+ export type { CargoDependencyOptions, CargoDependency, RustPackageOptions, DBXToolsRustWorkspaceOptions, UniFFIReleaseTarget, RustBindingMapping, RustWorkspaceMapping } from "./src/project-rs.ts";
46
50
  export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
47
51
  export { DBXToolsRelease } from "./src/release.ts";
48
52
  export type { StandaloneRelease, DBXToolsReleaseOptions } from "./src/release.ts";
49
53
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
50
54
  export type { PackageTag } from "./src/tags.ts";
51
55
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
56
+ export { makeDefaultedInterfaceParametersOptional, addExplicitInterfaceReexports } from "./src/uniffi.ts";
57
+ export type { TypeScriptBindingModule } from "./src/uniffi.ts";
52
58
  export { DBXToolsVsCode } from "./src/vscode.ts";
53
59
  export type { IgnoreGroupOptions } from "./src/watch.ts";
54
60
  export { VERSION_FILE, DEFAULT_VERSION } from "./src/workspace-version.ts";
package/package.json CHANGED
@@ -26,9 +26,9 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@clack/prompts": "^1.7.0",
29
- "@dbx-tools/core": "0.6.142",
30
- "@dbx-tools/path": "0.6.142",
31
- "@dbx-tools/shared-core": "0.6.142",
29
+ "@dbx-tools/core": "0.6.144",
30
+ "@dbx-tools/path": "0.6.144",
31
+ "@dbx-tools/shared-core": "0.6.144",
32
32
  "commander": "^15.0.0",
33
33
  "concurrently": "^10.0.3",
34
34
  "constructs": "^10.6.0",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "main": "index.ts",
50
50
  "license": "Apache-2.0",
51
- "version": "0.6.142",
51
+ "version": "0.6.144",
52
52
  "types": "index.ts",
53
53
  "type": "module",
54
54
  "exports": {
package/src/barrels.ts CHANGED
@@ -6,7 +6,8 @@
6
6
  * rules (see {@link isExcluded}):
7
7
  * 1. a file/folder whose name starts with `_` is private and never barrelled;
8
8
  * 2. test / `.d.ts` files are skipped;
9
- * 3. a `src/**​/index.ts` is a hand-authored subpath entry, not a module;
9
+ * 3. a hand-authored `src/**​/index.ts` is a subpath entry, while a generated
10
+ * nested index is the facade for its generated folder;
10
11
  * 4. only files that actually contain an `export` are re-exported.
11
12
  *
12
13
  * A hand-authored `exports.ts` sitting next to the generated `index.ts` (a Vite-style
@@ -62,15 +63,16 @@ import { isModuleFile, toPosix, recordedPackages, repoRoot } from "./packages.ts
62
63
  * 1. any path segment starting with `_` (private module or folder);
63
64
  * 2. a test / spec file;
64
65
  * 3. a `.d.ts` declaration;
65
- * 4. a `src/**​/index.ts` - a hand-authored subpath entry (e.g. `src/react/index.ts`
66
+ * 4. a hand-authored `src/**​/index.ts` - a subpath entry (e.g. `src/react/index.ts`
66
67
  * behind a package's `./react` export), not a module to namespace into the barrel.
68
+ * Generated nested indexes are retained as the generated folder's facade.
67
69
  */
68
- function isExcluded(relPath: string): boolean {
70
+ function isExcluded(relPath: string, srcDir: string): boolean {
69
71
  return (
70
72
  /(^|\/)_/.test(relPath) ||
71
73
  /\.(test|spec)\./.test(relPath) ||
72
74
  /\.d\.ts$/.test(relPath) ||
73
- /(^|\/)index\.ts$/.test(relPath)
75
+ (/(^|\/)index\.ts$/.test(relPath) && !isGenerated(join(srcDir, relPath)))
74
76
  );
75
77
  }
76
78
 
@@ -128,12 +130,14 @@ function kebabToCamel(segment: string): string {
128
130
  /** Derive a valid namespace identifier from a relocated barrel module path. */
129
131
  function modulePathToNamespace(modulePath: string): string {
130
132
  const rel = modulePath.replace(/^\.\/src\//, "").replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
131
- const segments = rel.split("/").map(kebabToCamel);
133
+ const segments = rel.split("/");
134
+ if (segments.at(-1) === "index" && segments.length > 1) segments.pop();
135
+ const names = segments.map(kebabToCamel);
132
136
  let name =
133
- segments.length === 1
134
- ? segments[0]!
135
- : segments[0]! +
136
- segments
137
+ names.length === 1
138
+ ? names[0]!
139
+ : names[0]! +
140
+ names
137
141
  .slice(1)
138
142
  .map((s) => string.capitalize(s))
139
143
  .join("");
@@ -315,23 +319,36 @@ function generateForPackage(pkgDir: string): number {
315
319
  const before = existsSync(rootBarrel) ? readFileSync(rootBarrel, "utf8") : undefined;
316
320
 
317
321
  // The re-exportable module set under `src/`: every source file that actually
318
- // exports something, minus the private / test / declaration files and any
319
- // `src/**/index.ts` (those are hand-authored subpath entries behind a package's
320
- // `./sub` export, not modules to namespace into the root barrel). `findFiles`
322
+ // exports something, minus private / test / declaration files and hand-authored
323
+ // `src/**/index.ts` subpath entries. A generated nested index is retained as
324
+ // that generated folder's public facade. `findFiles`
321
325
  // yields posix paths relative to `srcDir`; `hasExport` parses each via
322
326
  // `moduleStatements` and needs the absolute path.
323
327
  const candidates = [...find.findFiles("**/*", { cwd: srcDir })]
324
328
  .map(toPosix)
325
- .filter(isModuleFile)
326
- .filter((f) => !isExcluded(f))
329
+ .filter(
330
+ (file) =>
331
+ isModuleFile(file) || (/(^|\/)index\.ts$/.test(file) && isGenerated(join(srcDir, file))),
332
+ )
333
+ .filter((f) => !isExcluded(f, srcDir))
327
334
  .filter((f) => hasExport(join(srcDir, f)));
328
335
 
336
+ const generatedIndexDirs = new Set(
337
+ candidates
338
+ .filter((file) => /(^|\/)index\.ts$/.test(file) && isGenerated(join(srcDir, file)))
339
+ .map((file) => file.replace(/(^|\/)index\.ts$/, "")),
340
+ );
341
+ const publicCandidates = candidates.filter((file) => {
342
+ if (/(^|\/)index\.ts$/.test(file)) return true;
343
+ return ![...generatedIndexDirs].some((dir) => dir && file.startsWith(`${dir}/`));
344
+ });
345
+
329
346
  // Collapse each extensionless module path to one entry, preferring a TypeScript
330
347
  // source over a sibling compiled artifact (`math.ts` wins over a committed
331
348
  // `math.js`), so a module is barrelled exactly once. Then sort by module path.
332
349
  const byModulePath = new Map<string, string>();
333
- for (const f of candidates) {
334
- const stem = f.replace(MODULE_EXT_RE, "");
350
+ for (const f of publicCandidates) {
351
+ const stem = f.replace(/(^|\/)index\.ts$/, "").replace(MODULE_EXT_RE, "");
335
352
  const existing = byModulePath.get(stem);
336
353
  if (!existing || (!isSourceExt(existing) && isSourceExt(f))) byModulePath.set(stem, f);
337
354
  }
package/src/packages.ts CHANGED
@@ -269,7 +269,7 @@ export function readPackageManifest(dir: string): Record<string, unknown> | unde
269
269
  }
270
270
 
271
271
  /** A package's `dbxToolsConfig` object, or `undefined` when absent. */
272
- function readDbxToolsConfig(dir: string): Record<string, unknown> | undefined {
272
+ export function readDbxToolsConfig(dir: string): Record<string, unknown> | undefined {
273
273
  const config = readPackageManifest(dir)?.dbxToolsConfig;
274
274
  return object.isRecord(config) ? config : undefined;
275
275
  }
@@ -33,7 +33,8 @@ 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",
36
+ (project as Partial<DBXToolsProject>).language === "python" ||
37
+ (project as Partial<DBXToolsProject>).language === "rust",
37
38
  );
38
39
  }
39
40
 
package/src/project-py.ts CHANGED
@@ -23,6 +23,8 @@ export interface PythonPackageOptions extends DBXToolsProjectOptions {
23
23
  readonly description: string;
24
24
  readonly dependencies?: readonly string[];
25
25
  readonly scripts?: Readonly<Record<string, string>>;
26
+ /** Keep this package unpublished and out of public docs and releases. */
27
+ readonly private?: boolean;
26
28
  }
27
29
 
28
30
  /** Options for one projen-native Python workspace member. */
@@ -58,6 +60,8 @@ export interface DBXToolsPythonWorkspaceOptions {
58
60
  readonly testPaths?: readonly string[];
59
61
  readonly lintPaths?: readonly string[];
60
62
  readonly ruffPerFileIgnores?: Readonly<Record<string, readonly string[]>>;
63
+ /** Generated Python implementation files Pyrefly should resolve but not type-check. */
64
+ readonly pyreflyProjectExcludes?: readonly string[];
61
65
  readonly interpreterPath?: string | false;
62
66
  readonly release?: boolean | PythonReleaseOptions;
63
67
  }
@@ -171,6 +175,9 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
171
175
  if (pkg.scripts) {
172
176
  this.uv.file.addOverride("project.scripts", pkg.scripts);
173
177
  }
178
+ if (pkg.private) {
179
+ this.uv.file.addOverride("tool.dbx-tools.private", true);
180
+ }
174
181
  formatPyproject(this.uv.file);
175
182
  this.uv.file.readonly = true;
176
183
 
@@ -263,7 +270,9 @@ export class DBXToolsPythonWorkspace extends Component {
263
270
  tool: {
264
271
  uv: python.uvConfig.toJson_UvConfiguration({
265
272
  package: false,
266
- workspace: { members: [`${this.repository.root}/*`] },
273
+ workspace: {
274
+ members: [`${this.repository.root}/*`],
275
+ },
267
276
  }),
268
277
  pytest: {
269
278
  ini_options: {
@@ -283,6 +292,10 @@ export class DBXToolsPythonWorkspace extends Component {
283
292
  if (options.indexStrategy) {
284
293
  file.addOverride("tool.uv.index-strategy", options.indexStrategy);
285
294
  }
295
+ file.addOverride("tool.pyrefly.ignore-errors-in-generated-code", true);
296
+ if (options.pyreflyProjectExcludes?.length) {
297
+ file.addOverride("tool.pyrefly.project-excludes", [...options.pyreflyProjectExcludes]);
298
+ }
286
299
  file.addOverride(
287
300
  "tool.uv.sources",
288
301
  Object.fromEntries(options.packages.map((pkg) => [pkg.name, { workspace: true }])),
@@ -318,6 +331,8 @@ export class DBXToolsPythonWorkspace extends Component {
318
331
 
319
332
  private addReleaseWorkflow(project: javascript.NodeProject, options: PythonReleaseOptions): void {
320
333
  if (!project.github) return;
334
+ const publishablePackages = this.packages.filter((pkg) => !pkg.packageOptions.private);
335
+ if (publishablePackages.length === 0) return;
321
336
  const workflow = new GithubWorkflow(project.github, options.workflowName ?? "python-release");
322
337
  workflow.file?.addOverride("permissions", { contents: "read" });
323
338
  workflow.on({ push: { tags: ["v*"] }, workflowDispatch: {} });
@@ -346,7 +361,7 @@ export class DBXToolsPythonWorkspace extends Component {
346
361
  },
347
362
  {
348
363
  name: "Build distributions",
349
- run: this.packages
364
+ run: publishablePackages
350
365
  .map(
351
366
  (pkg) =>
352
367
  `uv build --package ${pkg.packageOptions.name} --out-dir dist/${pkg.packageOptions.directory}`,
@@ -356,7 +371,7 @@ export class DBXToolsPythonWorkspace extends Component {
356
371
  {
357
372
  name: "Validate distributions",
358
373
  run: [
359
- `test "$(find dist -type f \\( -name '*.whl' -o -name '*.tar.gz' \\) | wc -l | tr -d ' ')" -eq ${this.packages.length * 2}`,
374
+ `test "$(find dist -type f \\( -name '*.whl' -o -name '*.tar.gz' \\) | wc -l | tr -d ' ')" -eq ${publishablePackages.length * 2}`,
360
375
  "uvx twine check dist/*/*.whl dist/*/*.tar.gz",
361
376
  ].join("\n"),
362
377
  },
@@ -367,7 +382,7 @@ export class DBXToolsPythonWorkspace extends Component {
367
382
  },
368
383
  ],
369
384
  });
370
- for (const pkg of this.packages) {
385
+ for (const pkg of publishablePackages) {
371
386
  workflow.addJob(`publish-${pkg.packageOptions.directory}`, {
372
387
  if: "${{ github.event_name == 'push' }}",
373
388
  needs: ["build"],