@dbx-tools/projen 0.1.1 → 0.3.43

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.
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Inputs for `pnpm-workspace.yaml`, which projen's NATIVE
3
+ * `javascript.PnpmWorkspaceYaml` owns.
4
+ *
5
+ * projen (>= 0.101.16) writes this file itself: every pnpm `NodeProject` gets a
6
+ * `PnpmWorkspaceYaml` component typed by `PnpmWorkspaceYamlSchema`, fed from
7
+ * `pnpmOptions.workspaceYamlOptions`. Nothing here writes a file; this only
8
+ * supplies the options object it renders, so the whole pnpm schema (`overrides`,
9
+ * `packageExtensions`, `catalogs`, ...) stays typed.
10
+ *
11
+ * Three things projen has no API for, and why this state object exists:
12
+ *
13
+ * - **Members are discovered, not declared.** `NodePackage.configurePnpm` passes
14
+ * `packages` straight through; nothing derives it from `project.subprojects`.
15
+ * The root's scan attaches packages AFTER construction, so the list is
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.
19
+ * - **Build allowances are the `allowBuilds` MAP.** projen's `allowScripts`
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.
24
+ *
25
+ * Late mutation works because projen renders lazily (its `YamlFile` takes
26
+ * `obj: () => toJson_PnpmWorkspaceYamlSchema(options)`) and because
27
+ * `configurePnpm` SPREADS the options into a new object, copying the `packages`
28
+ * array and the `catalog`/`allowBuilds` objects by REFERENCE. Mutating those same
29
+ * objects any time before synth lands in the rendered YAML; reassigning the
30
+ * fields would not.
31
+ *
32
+ * The obvious alternative - drop this and call projen's public
33
+ * `file.addOverride("catalog.<name>", ...)` - is WORSE here, for two measured
34
+ * reasons. `addOverride` SPLITS its path on `.`, so a dependency whose name
35
+ * contains a dot renders as a nested object (`socket.io` -> `socket: {io: ...}`),
36
+ * producing a catalog entry no `catalog:` specifier can resolve, silently. And an
37
+ * override for a key the schema did not already emit is appended LAST, which
38
+ * buries `packages` under the catalog. Keys set here are plain object keys, so
39
+ * neither applies.
40
+ */
41
+ import { relative } from "node:path";
42
+ import { javascript, type Project } from "projen";
43
+ import { toPosix } from "./packages";
44
+
45
+ /**
46
+ * The pnpm `catalog:` version registry: dependency name -> version range. A
47
+ * pnpm-workspace feature (packages reference it via a `catalog:` specifier), so
48
+ * there is no projen type for it - it's just a string map.
49
+ */
50
+ export type Catalog = Record<string, string>;
51
+
52
+ /**
53
+ * pnpm's `allowBuilds`: dependency name -> may its install scripts run. Not in
54
+ * projen's schema, whose build gate is the `onlyBuiltDependencies` LIST that
55
+ * current pnpm ignores.
56
+ */
57
+ export type AllowBuilds = Record<string, boolean>;
58
+
59
+ /**
60
+ * Default pnpm `catalog:` versions, pinned to match `databricks apps init`
61
+ * (AppKit). The `@databricks/*` packages are hardcoded engine defaults: this
62
+ * engine is steered toward Databricks, so AppKit + the experimental SDK are
63
+ * always available at `catalog:` without a per-repo override.
64
+ */
65
+ const DEFAULT_CATALOG: Catalog = {
66
+ react: "^19.2.4",
67
+ "react-dom": "^19.2.4",
68
+ "@types/react": "^19.2.2",
69
+ "@types/react-dom": "^19.2.2",
70
+ vite: "^7.1.14",
71
+ "@vitejs/plugin-react": "^5.0.4",
72
+ "@types/node": "^24.6.0",
73
+ "@types/express": "^5.0.5",
74
+ express: "^5.1.0",
75
+ // Exact, not a range: AppKit pins `zod` to a single version, and a schema
76
+ // built against a different copy is a structurally identical but nominally
77
+ // distinct type, so passing one to `defineTool` needs a cast. Matching the
78
+ // pin keeps one zod in the tree and the boundary cast-free. Still inside
79
+ // `@mastra/core`'s `^3.25.0 || ^4.0.0` peer range.
80
+ zod: "4.3.6",
81
+ typescript: "^5.9.3",
82
+ tsx: "^4.23.0",
83
+ commander: "^15.0.0",
84
+ "@clack/prompts": "^1.7.0",
85
+ "openapi-fetch": "^0.17.0",
86
+ tsoa: "^6.6.0",
87
+ concurrently: "^10.0.3",
88
+ pnpm: "^11.0.6",
89
+ // Optional logger: shared-core's `log` module lazy-imports it and degrades to
90
+ // a console fallback when it's absent, so consumers can leave it uninstalled.
91
+ consola: "^3.4.2",
92
+ "@databricks/appkit": "^0.43.0",
93
+ "@databricks/appkit-ui": "^0.43.1",
94
+ "@databricks/sdk-experimental": "^0.17.0",
95
+ };
96
+
97
+ /**
98
+ * Build allowances every workspace needs, because the engine itself is what
99
+ * drags each one in: `esbuild` arrives with tsx (so it has to be built for any
100
+ * task to run at all), and `unrs-resolver` is the native binding behind
101
+ * `eslint-import-resolver-typescript`, which projen's eslint component adds to
102
+ * every generated project. Leaving either unlisted greets a freshly bootstrapped
103
+ * workspace with pnpm's "Ignored build scripts" warning on its first install.
104
+ */
105
+ const DEFAULT_ALLOW_BUILDS: AllowBuilds = { esbuild: true, "unrs-resolver": true };
106
+
107
+ /**
108
+ * pnpm settings this engine applies to every workspace, beyond members, catalog,
109
+ * and allowances. Each is stated because pnpm's own default is the weaker choice
110
+ * for a projen-managed monorepo; a caller's `workspaceYaml` overrides any.
111
+ */
112
+ const DEFAULT_WORKSPACE_YAML: javascript.PnpmWorkspaceYamlOptions = {
113
+ // The catalog is GENERATED (`addCatalog` in `.projenrc.ts` / a tag mixin), so
114
+ // `pnpm add` must never write to it - `manual` keeps pnpm out of a file projen
115
+ // owns. Also pnpm's own default; stated so a future pnpm default flip cannot
116
+ // start editing generated content.
117
+ catalogMode: javascript.PnpmWorkspaceYamlSchemaCatalogMode.MANUAL,
118
+ // Every package resolves its siblings from source (`workspace:*` + an
119
+ // `index.ts` entrypoint), so a stale `node_modules` after a branch switch
120
+ // shows up as a warning on the next task rather than a confusing type error.
121
+ verifyDepsBeforeRun: "warn",
122
+ // `strictDepBuilds` is deliberately absent. It turns any dependency with an
123
+ // unreviewed install script into a hard install failure, which forces EVERY
124
+ // such package to be declared, including ones only ever declined. Left off,
125
+ // pnpm warns ("Ignored build scripts: ...") and installs, so `allowBuilds`
126
+ // holds real allowances only.
127
+ };
128
+
129
+ /** Options for the engine's `pnpm-workspace.yaml` contributions. */
130
+ export interface DBXToolsPNPMWorkspaceOptions {
131
+ /** Initial `catalog:` registry. Defaults to {@link DEFAULT_CATALOG}. */
132
+ readonly catalog?: Catalog;
133
+ /** Initial build allowances, merged over `{ esbuild: true }`. */
134
+ readonly allowBuilds?: AllowBuilds;
135
+ /** Any other pnpm-workspace setting, typed by projen's schema. */
136
+ readonly workspaceYaml?: javascript.PnpmWorkspaceYamlOptions;
137
+ }
138
+
139
+ /**
140
+ * The accumulated members, catalog, and build allowances behind the root's
141
+ * `pnpm-workspace.yaml`, exposed as `project.pnpmWorkspace`. ROOT-only, so the
142
+ * field is `undefined` on a child package (like projen's `project.eslint`).
143
+ *
144
+ * NOT a projen component and NOT a file: projen's native `PnpmWorkspaceYaml`
145
+ * owns both, and this owns only the options object it renders.
146
+ */
147
+ export class PnpmWorkspaceState {
148
+ /**
149
+ * The options object handed to projen, whose members are mutated in place.
150
+ * Held as the live reference the native component captured.
151
+ *
152
+ * Widened with `allowBuilds`, which projen's schema type does not declare;
153
+ * projen renders unrecognized keys verbatim, so it reaches the file.
154
+ */
155
+ readonly options: javascript.PnpmWorkspaceYamlOptions & { allowBuilds: AllowBuilds };
156
+
157
+ private readonly packages: string[] = [];
158
+ private readonly catalog: Catalog;
159
+ private readonly allowBuilds: AllowBuilds;
160
+
161
+ constructor(options: DBXToolsPNPMWorkspaceOptions = {}) {
162
+ this.catalog = { ...DEFAULT_CATALOG, ...options.catalog };
163
+ this.allowBuilds = { ...DEFAULT_ALLOW_BUILDS, ...options.allowBuilds };
164
+ this.options = {
165
+ ...DEFAULT_WORKSPACE_YAML,
166
+ ...options.workspaceYaml,
167
+ packages: this.packages,
168
+ catalog: this.catalog,
169
+ allowBuilds: this.allowBuilds,
170
+ };
171
+ }
172
+
173
+ /** Add or override a `catalog:` entry (dependency name -> version range). */
174
+ public addCatalog(name: string, version: string): void {
175
+ this.catalog[name] = version;
176
+ }
177
+
178
+ /**
179
+ * Let a dependency's install scripts run.
180
+ *
181
+ * Only allowances are declared. A dependency that is never allowed needs no
182
+ * entry: without `strictDepBuilds`, pnpm warns and skips it.
183
+ */
184
+ public allowBuild(name: string): void {
185
+ this.allowBuilds[name] = true;
186
+ }
187
+
188
+ /**
189
+ * Fill `packages` from the project's attached subprojects.
190
+ *
191
+ * Called from the root's `preSynthesize`, which is the earliest point every
192
+ * discovered package is attached - the list cannot be captured at
193
+ * construction, since the root's scan runs after it.
194
+ */
195
+ public resolveMembers(project: Project): void {
196
+ const members = project.subprojects
197
+ .map((sub) => toPosix(relative(project.outdir, sub.outdir)))
198
+ .filter(Boolean);
199
+ // Replaces the array's CONTENTS, keeping the reference projen captured.
200
+ this.packages.splice(0, this.packages.length, ...new Set(members.sort()));
201
+ }
202
+ }
@@ -0,0 +1,124 @@
1
+ import {
2
+ object,
3
+ predicate,
4
+ Sequence,
5
+ type OneOrMany,
6
+ type Predicate,
7
+ } from "@dbx-tools/shared-core";
8
+ import { IConstruct } from "constructs";
9
+ import { Project } from "projen";
10
+ import { DBXToolsProject, DBXToolsNodeProject, DBXToolsTypeScriptProject } from "./project";
11
+ import { toPosix } from "./packages";
12
+ import { relative } from "node:path";
13
+ import { match, PathMatchInput, PathMatchPredicate } from "@dbx-tools/path";
14
+ import { project } from "..";
15
+
16
+ /**
17
+ * Guard: the construct is a projen {@link Project} - the base every builder here
18
+ * starts from.
19
+ *
20
+ * Uses projen's own `Project.isProject` rather than `instanceof`. It tests for
21
+ * `Symbol.for("projen.Project")`, which every `Project` constructor stamps on
22
+ * itself, so it still matches when a construct came from a SECOND resolved copy
23
+ * of projen - the engine pins its own `projen` dependency separately from the
24
+ * consuming root's, which is exactly the case `instanceof` fails silently.
25
+ */
26
+ export function isProject(): Predicate<IConstruct, Project> {
27
+ return predicate.create((c: IConstruct): c is Project => Project.isProject(c));
28
+ }
29
+
30
+ /** Guard: the construct is a {@link DBXToolsProject} (a DBXTools Node or TypeScript project). */
31
+ export function isDBXToolsProject(): Predicate<IConstruct, DBXToolsProject> {
32
+ return isProject().and(
33
+ (project): project is DBXToolsProject =>
34
+ project instanceof DBXToolsNodeProject || project instanceof DBXToolsTypeScriptProject,
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Compile each glob/predicate input to a {@link PathMatchPredicate} once, cached
40
+ * so the returned {@link Sequence} is re-iterable across every project tested by
41
+ * the resulting predicate.
42
+ */
43
+ function projectMatchers(...inputs: OneOrMany<PathMatchInput>): Sequence<PathMatchPredicate> {
44
+ return object
45
+ .sequence(inputs)
46
+ .map((input) => match.toPathMatcher(input))
47
+ .cache();
48
+ }
49
+
50
+ /**
51
+ * Matches projects whose raw projen {@link Project.name} matches every glob in
52
+ * `patterns` (e.g. `@dbx-tools/ui-mastra`, `*-mastra`). Tests `project.name`
53
+ * verbatim, without normalizing through {@link PackageIdentifier} - use the
54
+ * `hasIdentifier*` variants to match the parsed scope/name instead.
55
+ */
56
+ export function hasName(...patterns: OneOrMany<PathMatchInput>): Predicate<IConstruct, Project> {
57
+ const matchers = projectMatchers(...patterns);
58
+ return isProject().and((p) => matchers.every((matcher) => matcher(p.name)));
59
+ }
60
+
61
+ /**
62
+ * Matches projects whose parsed npm name matches every glob in `patterns` (e.g.
63
+ * `*\/shared-core`, `@dbx-tools/*`): tested against the full `@scope/name` from
64
+ * {@link PackageIdentifier}.
65
+ */
66
+ export function hasIdentifierPackageName(
67
+ ...patterns: OneOrMany<PathMatchInput>
68
+ ): Predicate<IConstruct, Project> {
69
+ const matchers = projectMatchers(...patterns);
70
+ return isProject().and((p) => {
71
+ const packageName = project.identifier(p).packageName;
72
+ return matchers.every((matcher) => matcher(packageName));
73
+ });
74
+ }
75
+
76
+ /** Matches projects whose parsed unscoped name (from {@link PackageIdentifier}) matches every glob. */
77
+ export function hasIdentifierName(
78
+ ...patterns: OneOrMany<PathMatchInput>
79
+ ): Predicate<IConstruct, Project> {
80
+ const matchers = projectMatchers(...patterns);
81
+ return isProject().and((p) => {
82
+ const name = project.identifier(p).name;
83
+ return matchers.every((matcher) => matcher(name));
84
+ });
85
+ }
86
+
87
+ /** Matches projects whose parsed npm scope (from {@link PackageIdentifier}) matches every glob. */
88
+ export function hasIdentifierScope(
89
+ ...patterns: OneOrMany<PathMatchInput>
90
+ ): Predicate<IConstruct, Project> {
91
+ const matchers = projectMatchers(...patterns);
92
+ return isProject().and((p) => {
93
+ const scope = project.identifier(p).scope;
94
+ return scope && matchers.every((matcher) => matcher(scope));
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Matches DBXTools packages carrying every listed tag (`dbxToolsConfig.tags`), narrowing
100
+ * {@link Project} to {@link DBXToolsProject} (tags live only on DBXTools packages). Also the
101
+ * guard backing each built-in {@link PACKAGE_TAG_MIXINS} entry. Keep it in the SAME `.and(...)`
102
+ * as any name/path filter (or last when chaining) - a later non-tag `.and` re-widens to
103
+ * {@link Project} and drops the narrowing.
104
+ */
105
+ export function hasTag(...tags: OneOrMany<PathMatchInput>): Predicate<IConstruct, DBXToolsProject> {
106
+ const matchers = projectMatchers(...tags);
107
+ return isDBXToolsProject().and((project) =>
108
+ matchers.every((matcher) => project.dbxToolsConfig.tags.some((tag) => matcher(tag))),
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Matches projects whose folder (relative to the tree root) matches any glob in
114
+ * `pathPattern`. Globs are matched verbatim, so scope to a subtree with an
115
+ * explicit pattern - e.g. `hasPath("packages/**")` for every package under
116
+ * `packages/`.
117
+ */
118
+ export function hasPath(...pathPattern: OneOrMany<PathMatchInput>): Predicate<IConstruct, Project> {
119
+ const matchers = projectMatchers(...pathPattern);
120
+ return isProject().and((project) => {
121
+ const relativePath = toPosix(relative(project.root.outdir, project.outdir));
122
+ return matchers.some((matcher) => matcher(relativePath));
123
+ });
124
+ }