@dbx-tools/projen 0.9.5 → 0.9.6

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 (47) hide show
  1. package/README.md +64 -16
  2. package/index.ts +7 -5
  3. package/package.json +9 -6
  4. package/src/_rust-project.ts +356 -0
  5. package/src/_rust-release-workflow.ts +901 -0
  6. package/src/_rust-release.ts +605 -0
  7. package/src/_task-command.ts +89 -0
  8. package/src/barrels.ts +9 -7
  9. package/src/engine-root.ts +5 -5
  10. package/src/openapi.ts +1 -1
  11. package/src/packages.ts +9 -9
  12. package/src/pnpm-workspace.ts +1 -1
  13. package/src/project-js.ts +19 -25
  14. package/src/project-py.ts +6 -6
  15. package/src/project-rs.ts +100 -1098
  16. package/src/projen-version.ts +9 -0
  17. package/src/publish.ts +3 -4
  18. package/src/release-catalog.ts +1 -1
  19. package/src/release.ts +11 -12
  20. package/src/scaffold.ts +1 -1
  21. package/src/uniffi.ts +7 -0
  22. package/src/watch.ts +2 -2
  23. package/src/workspace-version.ts +20 -15
  24. package/tasks/barrels.ts +4 -4
  25. package/tasks/build-rust-release.ts +68 -0
  26. package/tasks/bump.ts +3 -2
  27. package/tasks/clean.ts +3 -3
  28. package/tasks/local-publish.ts +21 -3
  29. package/tasks/openapi.ts +2 -2
  30. package/tasks/publish-npm.ts +12 -6
  31. package/tasks/publish-python.ts +2 -2
  32. package/tasks/publish-uniffi-local.ts +11 -15
  33. package/tasks/publish.ts +88 -88
  34. package/tasks/python-registry.ts +1 -1
  35. package/tasks/release-bootstrap.ts +2 -2
  36. package/tasks/release-plan.ts +1 -1
  37. package/tasks/release-please.ts +1 -1
  38. package/tasks/release-pr.ts +60 -115
  39. package/tasks/release-request.ts +8 -29
  40. package/tasks/release-summary-units.ts +2 -9
  41. package/tasks/release-summary.ts +35 -24
  42. package/tasks/release-version.ts +44 -0
  43. package/tasks/rust-release.d.ts +41 -0
  44. package/tasks/rust-release.mjs +1717 -0
  45. package/tasks/uniffi-python.js +1 -1
  46. package/tasks/uniffi.ts +7 -1
  47. package/tasks/version-check.ts +6 -5
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @dbx-tools/projen
2
2
 
3
- Projen engine for dbx-tools pnpm workspaces.
3
+ Projen engine for Bun-first dbx-tools workspaces.
4
4
 
5
5
  Import this package from `.projenrc.ts` when a repository should discover
6
6
  packages from the filesystem and generate manifests, tsconfigs,
@@ -11,9 +11,10 @@ Key features:
11
11
  - Filesystem package discovery: every `src`-bearing folder under configured
12
12
  workspace roots becomes a TypeScript package.
13
13
  - Tag-driven runtime defaults for shared libraries, Node packages, CLIs,
14
- servers, OpenAPI clients, and React/Vite UI packages.
15
- - Generated package manifests, tsconfigs, package-root barrels, Vite configs,
16
- pnpm workspace/catalog files, and VS Code settings.
14
+ servers, OpenAPI clients, and React/browser UI packages.
15
+ - Generated package manifests, tsconfigs, package-root barrels, Bun app configs,
16
+ VS Code settings, and a committed `pnpm-workspace.yaml` retained for
17
+ Databricks Apps deployment.
17
18
  - Extensible mixin system so repositories can add deps, tasks, or generated
18
19
  files based on package predicates.
19
20
  - OpenAPI client generation from tsoa controllers and zod schema generation from
@@ -23,15 +24,15 @@ Key features:
23
24
  ## Define A Workspace Root
24
25
 
25
26
  ```ts
26
- import { project as projenProject } from "@dbx-tools/projen";
27
+ import { project } from "@dbx-tools/projen";
27
28
 
28
- const project = new projenProject.DBXToolsNodeProject({
29
+ const rootProject = new project.DBXToolsNodeProject({
29
30
  name: "my-apps",
30
31
  scope: "my-apps",
31
32
  packageRoots: ["packages", "examples"],
32
33
  });
33
34
 
34
- project.synth();
35
+ rootProject.synth();
35
36
  ```
36
37
 
37
38
  Every `src`-bearing folder under the configured roots becomes a
@@ -130,8 +131,8 @@ derives the crate name and repository from the parent project. A crate containin
130
131
  `uniffi::setup_scaffolding!()`
131
132
  automatically wires matching public Node and Python binding packages using the
132
133
  `<name>-rs` folder suffix. Node packages are named `@<scope>/<name>-rs`;
133
- Python distributions are named `<scope>-<name>-rs` and import generated values
134
- from `<scope>.<name>_rs.bindings`. Binding packages are always dedicated and
134
+ Python distributions are named `<scope>-<name>-rs` and export generated values
135
+ from `<scope>.<name>_rs`. Binding packages are always dedicated and
135
136
  never merge generated code into handwritten Node or Python packages.
136
137
  Repository-specific dependencies and features remain
137
138
  declarative options in `.projenrc.ts`; generated bindings are built separately
@@ -141,9 +142,38 @@ facades compile to `lib/` and publish JavaScript entry points that plain Node
141
142
  can load from `node_modules`. A complete `bindings.ts` / `_bindings.ts` /
142
143
  `_bindings-ffi.ts` triplet is exported directly from the generated package
143
144
  barrel. Python keeps `bindings.py` as the generated implementation and leaves
144
- `__init__.py` empty. Node generation fails when direct binding names conflict.
145
+ `__init__.py` as its generated package-root export. Node generation fails when
146
+ direct binding names conflict.
145
147
  Do not create a `nodeExports` binding subpath or a handwritten type facade.
146
148
 
149
+ Each discovered member is a native Projen `Project`, exposed as
150
+ `DBXToolsRustProject`. The same class also owns standalone Cargo projects with
151
+ the flat object-style options used by the Node and Python project classes:
152
+
153
+ ```ts
154
+ import { project } from "@dbx-tools/projen";
155
+
156
+ new project.DBXToolsRustProject({
157
+ name: "my-apps-core",
158
+ outdir: "native/core",
159
+ version: "0.1.0",
160
+ description: "Shared native runtime",
161
+ repository: "https://github.com/example/my-apps",
162
+ });
163
+ ```
164
+
165
+ The project emits concrete standalone Cargo metadata plus `compile`, `test`,
166
+ `package`, `lint`, `format`, and `format:check` tasks. A nested standalone
167
+ project declares its own empty Cargo workspace so a surrounding repository
168
+ workspace does not absorb it accidentally. The `examples` option emits explicit
169
+ Cargo example targets and `required-features`, so an optional code generator is
170
+ excluded from ordinary workspace tests until its feature is enabled.
171
+ `DBXToolsRustWorkspace` constructs the same class with workspace-owned metadata
172
+ and keeps aggregate binding and release coordination.
173
+ `DBXToolsRustWorkspaceOptions.private` supplies the default Cargo publication
174
+ policy for every discovered crate; a package-level `private` value overrides it.
175
+ Cargo projects use the flat object-style constructor exclusively.
176
+
147
177
  Rust dependencies between binding-enabled workspace crates become Node
148
178
  `workspace:*` and Python `internalDependencies` automatically. Python generation
149
179
  selects the owning crate and supplies `external_packages` imports. UBRN currently
@@ -159,7 +189,8 @@ build has no colliding binary outputs. Packaging runs that prebuilt executable.
159
189
  Cargo manifests, target config, and UniFFI config are generated from structured
160
190
  Projen `TomlFile` objects. Local and release Python generation share one
161
191
  dependency-free helper for generator arguments, target-specific executable
162
- names, generated headers, empty `__init__.py`, and native-library placement.
192
+ names, generated headers, generated package-root exports, and native-library
193
+ placement.
163
194
 
164
195
  `sync --watch` runs a focused Rust watcher beside the OpenAPI watcher. Changes
165
196
  inside an existing UniFFI crate regenerate that crate and its dependent bindings
@@ -173,6 +204,12 @@ a host Rust build. Regenerate bindings through the focused watcher or explicit
173
204
  task while changing a UniFFI API. Release preparation runs Cargo workspace tests
174
205
  without invoking UBRN.
175
206
 
207
+ Rust fingerprinting and binary version stamping use the dependency-free Node
208
+ helper generated at `.projen/rust-release.mjs`. The implementation ships with
209
+ `@dbx-tools/projen`, so consumers do not need a repository-specific helper
210
+ crate, a crates.io publication, or an extra Cargo build before release
211
+ orchestration can start.
212
+
176
213
  The workspace generates one `release.yml` workflow for every ecosystem. One
177
214
  root `VERSION` drives every Node, Python, Cargo, native, and GitHub artifact.
178
215
  `bun run release` prepares one reviewed PR into `main`; it enables automatic
@@ -253,18 +290,28 @@ from the same workflow. When a conventional Node binding path already belongs
253
290
  to a root subproject, Rust mapping reuses that project and adds binding
254
291
  dependencies and metadata to its existing manifest.
255
292
 
293
+ The workspace npm publisher owns compilation for normal release publication. It
294
+ selects every publishable package with compiled entry points, invokes one
295
+ root-level filtered compile, then packs each package exactly once with lifecycle
296
+ scripts disabled. It validates the archive identity, configured access,
297
+ integrity, and repository metadata before publishing those same bytes. Local
298
+ release preparation passes `--skip-compile` only after its immediately preceding
299
+ validation compile and verifies every expected output exists before reuse.
300
+ Package `prepack` tasks remain available for standalone publishes without
301
+ multiplying `tsc --build` across the monorepo release flow.
302
+
256
303
  ## Customize Packages With Mixins
257
304
 
258
305
  ```ts
259
- import { project as projenProject } from "@dbx-tools/projen";
306
+ import { project } from "@dbx-tools/projen";
260
307
 
261
- const project = new projenProject.DBXToolsNodeProject();
308
+ const rootProject = new project.DBXToolsNodeProject();
262
309
 
263
- projenProject.applyToProjects(project, { tags: "shared" }, (pkg) => {
310
+ project.applyToProjects(rootProject, { tags: "shared" }, (pkg) => {
264
311
  pkg.addDeps("zod@catalog:");
265
312
  });
266
313
 
267
- project.synth();
314
+ rootProject.synth();
268
315
  ```
269
316
 
270
317
  Use `projectJs.addOptionalPeer(pkg, specifier)` for an optional peer that must
@@ -482,7 +529,8 @@ invokes, so there is no second place to run the same thing:
482
529
  edit/compile loop to one package.
483
530
  - `build` / `package` - a complete compile/test/pack lifecycle when invoked in
484
531
  one package. Release preparation validates through the root's filtered tasks,
485
- and publication uses concurrent `bun publish --ignore-scripts`. The package phase also packs
532
+ and publication concurrently uploads archives that were each packed and
533
+ validated once with lifecycle scripts disabled. The package phase also packs
486
534
  with `--ignore-scripts` because its build already compiled; `prepack` remains
487
535
  available for a standalone publish that did not run `build` first.
488
536
  - `install` / `install:ci` / `default` / `pre-compile` / `post-compile` -
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.5";
6
+ export const PACKAGE_VERSION = "0.9.6";
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";
@@ -22,6 +22,7 @@ export * as projectJs from "./src/project-js.ts";
22
22
  export * as projectPredicate from "./src/project-predicate.ts";
23
23
  export * as projectPy from "./src/project-py.ts";
24
24
  export * as projectRs from "./src/project-rs.ts";
25
+ export * as projenVersion from "./src/projen-version.ts";
25
26
  export * as publish from "./src/publish.ts";
26
27
  export * as release from "./src/release.ts";
27
28
  export * as releaseCatalog from "./src/release-catalog.ts";
@@ -50,12 +51,13 @@ export type { RecordedPackage } from "./src/packages.ts";
50
51
  export { PnpmWorkspaceState } from "./src/pnpm-workspace.ts";
51
52
  export type { Catalog, AllowBuilds, DBXToolsPNPMWorkspaceOptions } from "./src/pnpm-workspace.ts";
52
53
  export type { DBXToolsProjectLanguage, DBXToolsProjectOptions, DBXToolsProject, ApplyToProjectsOptions } from "./src/project.ts";
53
- export { PackageIdentifier, PROJEN_VERSION, DBX_TOOLS_LICENSE, DBXToolsNodeProject, ROOT_INSTALL_ONLY_MIXIN, DBXToolsTypeScriptProject } from "./src/project-js.ts";
54
+ export { PackageIdentifier, DBX_TOOLS_LICENSE, DBXToolsNodeProject, ROOT_INSTALL_ONLY_MIXIN, DBXToolsTypeScriptProject } from "./src/project-js.ts";
54
55
  export type { DBXToolsJavaScriptProject, DBXToolsReleaseMode, PullRequestTitlePolicyOptions, DBXToolsJavaScriptProjectOptions, DBXToolsTypeScriptProjectOptions } from "./src/project-js.ts";
55
56
  export { DBXToolsPythonProject, DBXToolsPythonWorkspace } from "./src/project-py.ts";
56
57
  export type { PythonRepositoryOptions, PythonPackageOptions, PythonTrustedPublisherOptions, DBXToolsPythonProjectOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
57
- export { RustReleaseOs, RustReleaseCpu, UNIFFI_RELEASE_TARGETS, DBXToolsRustProject, DBXToolsRustWorkspace } from "./src/project-rs.ts";
58
- export type { CargoDependencyOptions, CargoDependency, RustCliOptions, RustPackageOptions, DBXToolsRustWorkspaceOptions, RustReleasePlatform, UniFFIReleaseTarget, RustBindingMapping, RustWorkspaceMapping, RustReleaseBinaryAssetMapping, RustReleaseBinaryMapping } from "./src/project-rs.ts";
58
+ export { DBXToolsRustProject, RustReleaseOs, RustReleaseCpu, UNIFFI_RELEASE_TARGETS, DBXToolsRustWorkspace } from "./src/project-rs.ts";
59
+ export type { CargoDependency, CargoDependencyOptions, CargoExampleOptions, DBXToolsRustProjectOptions, RustCrateOptions, RustCliOptions, DBXToolsRustWorkspaceOptions, RustReleasePlatform, UniFFIReleaseTarget, RustBindingMapping, RustWorkspaceMapping, RustReleaseBinaryAssetMapping, RustReleaseBinaryMapping } from "./src/project-rs.ts";
60
+ export { PROJEN_VERSION } from "./src/projen-version.ts";
59
61
  export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
60
62
  export { DBXToolsRelease } from "./src/release.ts";
61
63
  export type { ReleaseStage, ReleaseDocsOptions, ReleaseSummaryOptions, DBXToolsReleaseOptions } from "./src/release.ts";
@@ -70,7 +72,7 @@ export type { ReleaseUnitWorkspacePluginOptions } from "./src/release-please.ts"
70
72
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
71
73
  export type { PackageTag } from "./src/tags.ts";
72
74
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
73
- export { makeDefaultedInterfaceParametersOptional, addTypeScriptExtensionsToBindingImports, addExplicitInterfaceReexports } from "./src/uniffi.ts";
75
+ export { makeDefaultedInterfaceParametersOptional, removeObsoleteInterfaceAliases, addTypeScriptExtensionsToBindingImports, addExplicitInterfaceReexports } from "./src/uniffi.ts";
74
76
  export type { TypeScriptBindingModule } from "./src/uniffi.ts";
75
77
  export { DBXToolsVsCode } from "./src/vscode.ts";
76
78
  export type { IgnoreGroupOptions } from "./src/watch.ts";
package/package.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "name": "@dbx-tools/projen",
3
3
  "scripts": {
4
4
  "build": "projen build",
5
+ "bundle:rust-release": "projen bundle:rust-release",
5
6
  "clobber": "projen clobber",
6
7
  "compile": "projen compile",
7
8
  "default": "projen default",
@@ -20,22 +21,24 @@
20
21
  "@types/node": "^24.6.0",
21
22
  "@types/semver": "^7.7.1",
22
23
  "constructs": "^10.0.0",
23
- "projen": "^0.101.6",
24
+ "projen": "0.103.27",
24
25
  "tsoa": "^6.6.0",
25
26
  "typescript": "^5.9.3"
26
27
  },
28
+ "peerDependencies": {
29
+ "projen": "^0.103.27"
30
+ },
27
31
  "dependencies": {
28
32
  "@clack/prompts": "^1.7.0",
29
- "@dbx-tools/core": "^0.9.5",
30
- "@dbx-tools/path": "^0.9.5",
31
- "@dbx-tools/shared-core": "^0.9.5",
33
+ "@dbx-tools/core": "^0.9.6",
34
+ "@dbx-tools/path": "^0.9.6",
35
+ "@dbx-tools/shared-core": "^0.9.6",
32
36
  "commander": "^15.0.0",
33
37
  "concurrently": "^10.0.3",
34
38
  "constructs": "^10.6.0",
35
39
  "is-identifier": "^1",
36
40
  "openapi-typescript": "^7.13.0",
37
41
  "oxc-parser": "^0.90.0",
38
- "projen": "^0.101.16",
39
42
  "release-please": "17.11.2",
40
43
  "semver": "^7.7.3",
41
44
  "smol-toml": "1.8.0",
@@ -50,7 +53,7 @@
50
53
  },
51
54
  "main": "index.ts",
52
55
  "license": "Apache-2.0",
53
- "version": "0.9.5",
56
+ "version": "0.9.6",
54
57
  "types": "index.ts",
55
58
  "type": "module",
56
59
  "exports": {
@@ -0,0 +1,356 @@
1
+ /** Projen-native standalone and workspace-owned Rust project implementation. */
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { basename, join, resolve } from "node:path";
4
+ import * as projectUtils from "@dbx-tools/core/project-utils";
5
+ import { stringUtils } from "@dbx-tools/shared-core";
6
+ import { License, Project, TextFile, TomlFile, javascript, type Task } from "projen";
7
+ import { DBX_TOOLS_LICENSE, projectRepositoryUrl } from "./project-js.ts";
8
+ import type { RustReleaseOs } from "./project-rs.ts";
9
+ import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
10
+ import { readWorkspaceVersion } from "./workspace-version.ts";
11
+
12
+ export interface CargoDependencyOptions {
13
+ readonly version?: string;
14
+ readonly workspace?: boolean;
15
+ readonly path?: string;
16
+ readonly optional?: boolean;
17
+ readonly package?: string;
18
+ readonly defaultFeatures?: boolean;
19
+ readonly features?: readonly string[];
20
+ }
21
+
22
+ export type CargoDependency = string | CargoDependencyOptions;
23
+
24
+ export interface RustCliOptions {
25
+ /** `dbx` subcommand. Defaults to the Rust package directory. */
26
+ readonly command?: string;
27
+ /** Root CLI help text. Defaults to the Rust package description. */
28
+ readonly description?: string;
29
+ }
30
+
31
+ /** Explicit Cargo example target and the feature gate required to compile it. */
32
+ export interface CargoExampleOptions {
33
+ readonly name: string;
34
+ /** Source path relative to the crate root. Defaults to Cargo's conventional path. */
35
+ readonly path?: string;
36
+ /** Features Cargo must enable before compiling this example. */
37
+ readonly requiredFeatures?: readonly string[];
38
+ }
39
+
40
+ /** Cargo package, target, binding, and release behavior shared by every Rust project. */
41
+ export interface RustCrateOptions {
42
+ readonly description?: string;
43
+ /** Keep this crate unpublished and out of public docs. */
44
+ readonly private?: boolean;
45
+ /** Build this crate's binary for each target and attach it to the GitHub release. */
46
+ readonly release?: boolean;
47
+ /** Omit this crate and any release binary artifact from these operating systems. */
48
+ readonly releaseExcludeOs?: readonly RustReleaseOs[];
49
+ readonly dependencies?: Readonly<Record<string, CargoDependency>>;
50
+ readonly devDependencies?: Readonly<Record<string, CargoDependency>>;
51
+ readonly features?: Readonly<Record<string, readonly string[]>>;
52
+ readonly defaultFeatures?: readonly string[];
53
+ /** Explicit example targets, including optional Cargo feature gates. */
54
+ readonly examples?: readonly CargoExampleOptions[];
55
+ /** Cargo and release executable name. Defaults to the generated package name. */
56
+ readonly binaryName?: string;
57
+ /** Publish this release binary through the generated `dbx` command registry. */
58
+ readonly cli?: boolean | RustCliOptions;
59
+ readonly bindings?: readonly ("node" | "python")[];
60
+ readonly uniffiConfig?: Readonly<Record<string, unknown>>;
61
+ }
62
+
63
+ /** Options for a standalone or workspace-owned Projen-native Cargo project. */
64
+ export interface DBXToolsRustProjectOptions extends DBXToolsProjectOptions, RustCrateOptions {
65
+ /** Cargo package name. */
66
+ readonly name: string;
67
+ readonly parent?: Project;
68
+ /** Package directory relative to its workspace root; inferred from `outdir` when omitted. */
69
+ readonly directory?: string;
70
+ /** Cargo workspace root used for generated workspace mappings. Defaults to `.`. */
71
+ readonly workspaceRoot?: string;
72
+ /** Inherit unspecified Cargo package metadata from `[workspace.package]`. */
73
+ readonly workspace?: boolean;
74
+ /** Crate-name scope retained for workspace package and binding naming. */
75
+ readonly scope?: string;
76
+ /** Concrete standalone package version. Defaults to `0.1.0`. */
77
+ readonly version?: string;
78
+ /** Rust edition. Defaults to `2021` for standalone projects. */
79
+ readonly edition?: string;
80
+ /** Minimum supported Rust version. Defaults to `1.82` for standalone projects. */
81
+ readonly rustVersion?: string;
82
+ /** SPDX license identifier. Defaults to Apache-2.0 for standalone projects. */
83
+ readonly license?: string;
84
+ /** Copyright owner written to a standalone license file. Defaults to the package name. */
85
+ readonly copyrightOwner?: string;
86
+ /** Source repository URL. Auto-detected for standalone projects when possible. */
87
+ readonly repository?: string;
88
+ }
89
+
90
+ function rustSources(directory: string): string[] {
91
+ if (!existsSync(directory)) return [];
92
+ const files: string[] = [];
93
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
94
+ const path = join(directory, entry.name);
95
+ if (entry.isDirectory()) files.push(...rustSources(path));
96
+ else if (entry.isFile() && entry.name.endsWith(".rs")) files.push(path);
97
+ }
98
+ return files;
99
+ }
100
+
101
+ /** Whether a Rust crate embeds the UniFFI proc-macro scaffolding marker. */
102
+ export function hasUniFFIBindings(directory: string): boolean {
103
+ return rustSources(join(directory, "src")).some((path) =>
104
+ /\buniffi\s*::\s*setup_scaffolding\s*!\s*\(/.test(readFileSync(path, "utf8")),
105
+ );
106
+ }
107
+
108
+ export function discoverRustCrates(root: string): string[] {
109
+ if (!existsSync(root)) return [];
110
+ return readdirSync(root, { withFileTypes: true })
111
+ .filter((entry) => entry.isDirectory())
112
+ .filter((entry) => rustSources(join(root, entry.name, "src")).length > 0)
113
+ .map((entry) => entry.name)
114
+ .sort();
115
+ }
116
+
117
+ export function cargoDependency(
118
+ value: CargoDependency,
119
+ workspaceVersion?: string,
120
+ ): string | Record<string, unknown> {
121
+ if (typeof value === "string") return value;
122
+ return {
123
+ ...(value.version ? { version: value.version } : {}),
124
+ ...(!value.version && value.path && workspaceVersion ? { version: workspaceVersion } : {}),
125
+ ...(value.workspace ? { workspace: true } : {}),
126
+ ...(value.path ? { path: value.path } : {}),
127
+ ...(value.package ? { package: value.package } : {}),
128
+ ...(value.optional ? { optional: true } : {}),
129
+ ...(value.defaultFeatures === false ? { "default-features": false } : {}),
130
+ ...(value.features?.length ? { features: [...value.features] } : {}),
131
+ };
132
+ }
133
+
134
+ export function structuredTomlSections(
135
+ value: Readonly<Record<string, unknown>>,
136
+ ): Record<string, unknown> {
137
+ const result: Record<string, unknown> = {};
138
+ for (const [section, contents] of Object.entries(value)) {
139
+ const path = section.split(".");
140
+ let target = result;
141
+ for (const part of path.slice(0, -1)) {
142
+ const current = target[part];
143
+ if (current === undefined) {
144
+ target[part] = {};
145
+ } else if (!current || typeof current !== "object" || Array.isArray(current)) {
146
+ throw new Error(`Conflicting TOML section ${section}`);
147
+ }
148
+ target = target[part] as Record<string, unknown>;
149
+ }
150
+ target[path.at(-1)!] = contents;
151
+ }
152
+ return result;
153
+ }
154
+
155
+ interface ResolvedRustProjectOptions extends DBXToolsRustProjectOptions {
156
+ readonly directory: string;
157
+ readonly outdir: string;
158
+ readonly workspaceRoot: string;
159
+ }
160
+
161
+ function resolveRustProjectOptions(
162
+ options: DBXToolsRustProjectOptions,
163
+ ): ResolvedRustProjectOptions {
164
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_-]*$/.test(options.name)) {
165
+ throw new Error(`Invalid Cargo package name: ${options.name}`);
166
+ }
167
+ const directory =
168
+ options.directory ??
169
+ (options.outdir && options.outdir !== "."
170
+ ? basename(options.outdir)
171
+ : stringUtils.toSlug(options.name));
172
+ return {
173
+ ...options,
174
+ directory,
175
+ outdir: options.outdir ?? (options.parent ? directory : "."),
176
+ workspaceRoot: options.workspaceRoot ?? ".",
177
+ };
178
+ }
179
+
180
+ function packageOptions(
181
+ options: ResolvedRustProjectOptions,
182
+ ): RustCrateOptions & { readonly directory: string } {
183
+ return {
184
+ directory: options.directory,
185
+ ...(options.description !== undefined ? { description: options.description } : {}),
186
+ ...(options.private !== undefined ? { private: options.private } : {}),
187
+ ...(options.release !== undefined ? { release: options.release } : {}),
188
+ ...(options.releaseExcludeOs ? { releaseExcludeOs: options.releaseExcludeOs } : {}),
189
+ ...(options.dependencies ? { dependencies: options.dependencies } : {}),
190
+ ...(options.devDependencies ? { devDependencies: options.devDependencies } : {}),
191
+ ...(options.features ? { features: options.features } : {}),
192
+ ...(options.defaultFeatures ? { defaultFeatures: options.defaultFeatures } : {}),
193
+ ...(options.binaryName ? { binaryName: options.binaryName } : {}),
194
+ ...(options.cli !== undefined ? { cli: options.cli } : {}),
195
+ ...(options.bindings ? { bindings: options.bindings } : {}),
196
+ ...(options.uniffiConfig ? { uniffiConfig: options.uniffiConfig } : {}),
197
+ };
198
+ }
199
+
200
+ function inheritedCargoValue(
201
+ workspace: boolean,
202
+ configured: string | undefined,
203
+ fallback: string,
204
+ ): string | { workspace: true } {
205
+ return workspace && configured === undefined ? { workspace: true } : (configured ?? fallback);
206
+ }
207
+
208
+ /** One standalone or workspace-owned Cargo project. */
209
+ export class RustProject extends Project implements DBXToolsProject {
210
+ readonly language = "rust" as const;
211
+ readonly crateName: string;
212
+ readonly workspaceRoot: string;
213
+ readonly scope: string;
214
+ readonly packageOptions: RustCrateOptions & { readonly directory: string };
215
+ readonly uniffi: boolean;
216
+ readonly manifestFile: TomlFile;
217
+ readonly lintTask: Task;
218
+ readonly formatTask: Task;
219
+ readonly formatCheckTask: Task;
220
+
221
+ constructor(projectOptions: DBXToolsRustProjectOptions) {
222
+ const options = resolveRustProjectOptions(projectOptions);
223
+ super({ parent: options.parent, outdir: options.outdir, name: options.name });
224
+ this.crateName = options.name;
225
+ this.workspaceRoot = options.workspaceRoot;
226
+ this.scope = stringUtils.toSlug(options.scope ?? options.name.split("-")[0]!);
227
+ this.packageOptions = packageOptions(options);
228
+ this.uniffi = hasUniFFIBindings(this.outdir);
229
+ const library = existsSync(join(this.outdir, "src/lib.rs"));
230
+ const binary = existsSync(join(this.outdir, "src/main.rs"));
231
+ const binaryName = options.binaryName ?? this.crateName;
232
+ const workspace = options.workspace === true;
233
+ const projectDirectory = options.parent
234
+ ? resolve(options.parent.outdir, options.outdir)
235
+ : resolve(options.outdir);
236
+ const repository =
237
+ options.repository ??
238
+ (options.parent instanceof javascript.NodeProject
239
+ ? projectRepositoryUrl(options.parent)
240
+ : projectUtils.repositoryUrl(projectDirectory));
241
+ const dependencyVersion =
242
+ options.version ?? (options.parent ? readWorkspaceVersion(options.parent.outdir) : "0.1.0");
243
+ const manifest: Record<string, unknown> = {
244
+ ...(!workspace ? { workspace: {} } : {}),
245
+ package: {
246
+ name: this.crateName,
247
+ version: inheritedCargoValue(workspace, options.version, "0.1.0"),
248
+ edition: inheritedCargoValue(workspace, options.edition, "2021"),
249
+ "rust-version": inheritedCargoValue(workspace, options.rustVersion, "1.82"),
250
+ description: options.description ?? this.crateName,
251
+ license: inheritedCargoValue(workspace, options.license, DBX_TOOLS_LICENSE),
252
+ ...(workspace && options.repository === undefined
253
+ ? { repository: { workspace: true } }
254
+ : repository
255
+ ? { repository }
256
+ : {}),
257
+ ...(options.private ? { publish: false } : {}),
258
+ },
259
+ ...(library
260
+ ? {
261
+ lib: {
262
+ name: this.crateName.replaceAll("-", "_"),
263
+ path: "src/lib.rs",
264
+ ...(this.uniffi ? { "crate-type": ["lib", "cdylib"] } : {}),
265
+ },
266
+ }
267
+ : {}),
268
+ ...(this.uniffi || binary
269
+ ? {
270
+ bin: [
271
+ ...(this.uniffi
272
+ ? [{ name: `${this.crateName}-uniffi-bindgen`, path: "uniffi-bindgen.rs" }]
273
+ : []),
274
+ ...(binary ? [{ name: binaryName, path: "src/main.rs" }] : []),
275
+ ],
276
+ }
277
+ : {}),
278
+ ...(options.features || options.defaultFeatures
279
+ ? {
280
+ features: {
281
+ ...(options.defaultFeatures ? { default: [...options.defaultFeatures] } : {}),
282
+ ...options.features,
283
+ },
284
+ }
285
+ : {}),
286
+ ...(options.examples?.length
287
+ ? {
288
+ example: options.examples.map((example) => ({
289
+ name: example.name,
290
+ ...(example.path ? { path: example.path } : {}),
291
+ ...(example.requiredFeatures?.length
292
+ ? { "required-features": [...example.requiredFeatures] }
293
+ : {}),
294
+ })),
295
+ }
296
+ : {}),
297
+ ...(options.dependencies
298
+ ? {
299
+ dependencies: Object.fromEntries(
300
+ Object.entries(options.dependencies).map(([name, value]) => [
301
+ name,
302
+ cargoDependency(value, dependencyVersion),
303
+ ]),
304
+ ),
305
+ }
306
+ : {}),
307
+ ...(options.devDependencies
308
+ ? {
309
+ "dev-dependencies": Object.fromEntries(
310
+ Object.entries(options.devDependencies).map(([name, value]) => [
311
+ name,
312
+ cargoDependency(value, dependencyVersion),
313
+ ]),
314
+ ),
315
+ }
316
+ : {}),
317
+ };
318
+ this.manifestFile = new TomlFile(this, "Cargo.toml", { marker: false, obj: manifest });
319
+ if (this.uniffi) {
320
+ new TextFile(this, "uniffi-bindgen.rs", {
321
+ lines: ["fn main() {", " uniffi::uniffi_bindgen_main();", "}", ""],
322
+ });
323
+ new TomlFile(this, "uniffi.toml", {
324
+ marker: false,
325
+ obj: structuredTomlSections(
326
+ options.uniffiConfig ?? {
327
+ "bindings.python": { cdylib_name: this.crateName.replaceAll("-", "_") },
328
+ "bindings.typescript": { strictTypeChecking: true },
329
+ },
330
+ ),
331
+ });
332
+ }
333
+ this.compileTask.reset("cargo build");
334
+ this.testTask.reset("cargo test");
335
+ this.packageTask.reset("cargo package");
336
+ this.lintTask = this.addTask("lint", {
337
+ description: "Lint every Rust target and feature",
338
+ exec: "cargo clippy --all-targets --all-features",
339
+ });
340
+ this.formatTask = this.addTask("format", {
341
+ description: "Format Rust sources",
342
+ exec: "cargo fmt",
343
+ });
344
+ this.formatCheckTask = this.addTask("format:check", {
345
+ description: "Check Rust source formatting",
346
+ exec: "cargo fmt -- --check",
347
+ });
348
+ if (!workspace) {
349
+ new License(this, {
350
+ spdx: options.license ?? DBX_TOOLS_LICENSE,
351
+ copyrightOwner: options.copyrightOwner ?? options.name,
352
+ });
353
+ this.gitignore.addPatterns("target/");
354
+ }
355
+ }
356
+ }