@dbx-tools/projen 0.6.76 → 0.6.78

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/src/project.ts CHANGED
@@ -1,1312 +1,65 @@
1
1
  /**
2
- * The dbx-tools project surface plus package tooling: the single
3
- * {@link DBXToolsProject} interface, the projen Node/TypeScript project classes,
4
- * naming, guards, manifest fields, and the shared root init.
5
- *
6
- * {@link DBXToolsNodeProject} (monorepo root) and {@link DBXToolsTypeScriptProject}
7
- * (a package, or a standalone compiling root) both implement {@link DBXToolsProject}.
2
+ * Language-agnostic project selection helpers and the compatibility facade for
3
+ * dbx-tools project implementations.
8
4
  */
9
- import { existsSync, readdirSync } from "node:fs";
10
- import { dirname, join, relative, resolve } from "node:path";
11
- import { project as coreProject } from "@dbx-tools/core";
12
- import { ignore, match, PathMatchInput } from "@dbx-tools/path";
13
- import { object, string, type OneOrMany } from "@dbx-tools/shared-core";
14
- import { IConstruct } from "constructs";
15
- import { Component, IgnoreFile, Project, type TaskOptions, javascript, typescript } from "projen";
16
- import { ReleaseTrigger } from "projen/lib/release";
17
- import { mixin, projectPredicate } from "..";
18
- import { generateBarrels } from "./barrels.ts";
19
- import {
20
- BUN_APP_OVERRIDES,
21
- BunBuildFile,
22
- BunDevServerFile,
23
- BunfigFile,
24
- RootBunfigFile,
25
- } from "./bun-app.ts";
26
- import { codegenModulePaths, generateCodegen } from "./codegen.ts";
27
- import { DBXToolsConfig, type DBXToolsConfigOptions } from "./dbx-tools-config.ts";
28
- import { resolvePkgRoot } from "./engine-root.ts";
29
- import {
30
- DEFAULT_PACKAGE_ROOTS,
31
- type DiscoveredPackage,
32
- projectName,
33
- readPackageManifest,
34
- repoRoot,
35
- scanPackages,
36
- toPosix,
37
- } from "./packages.ts";
38
- import { PnpmWorkspaceState, type DBXToolsPNPMWorkspaceOptions } from "./pnpm-workspace.ts";
39
- import { applyCompiledPublish } from "./publish.ts";
40
- import { DBXToolsRelease, type StandaloneRelease } from "./release.ts";
41
- import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
42
- import { DBXToolsRootTsconfig } from "./tsconfig.ts";
43
- import { DBXToolsVsCode } from "./vscode.ts";
5
+ import { type PathMatchInput } from "@dbx-tools/path";
6
+ import { object, type OneOrMany } from "@dbx-tools/shared-core";
7
+ import { type IConstruct } from "constructs";
8
+ import { Project, type ProjectOptions } from "projen";
9
+ import * as mixin from "./mixin.ts";
10
+ import * as projectPredicate from "./project-predicate.ts";
11
+ import type { DBXToolsJavaScriptProject } from "./project-js.ts";
44
12
 
45
- /**
46
- * The dbx-tools project surface, backed by projen's Node toolchain. A single
47
- * interface for both the monorepo root and each package: it carries the
48
- * `dbxToolsConfig` component plus the npm-naming and root-only file components.
49
- */
50
- export interface DBXToolsProject extends javascript.NodeProject {
51
- /** The package's `dbxToolsConfig` component (tags + `package.json` config). */
52
- readonly dbxToolsConfig: DBXToolsConfig;
53
- /** npm scope (the `@scope` in `@scope/pkg`), without the leading `@`. */
54
- readonly scope: string;
55
-
56
- /**
57
- * The `pnpm-workspace.yaml` catalog / member / build-allowance state - only a
58
- * tree ROOT has one. The FILE itself is owned by projen's native
59
- * `javascript.PnpmWorkspaceYaml`; this is the state it renders.
60
- */
61
- pnpmWorkspace?: PnpmWorkspaceState;
62
- /** Root projenrc tsconfigs - only a tree ROOT has one. */
63
- rootTsconfig?: DBXToolsRootTsconfig;
64
- /** Root `.vscode/*` - only a tree ROOT has one. */
65
- vsCode?: DBXToolsVsCode;
66
- }
67
-
68
- /** Parsed npm package identifier: optional scope plus the unscoped package name. */
69
- export class PackageIdentifier {
70
- public scope?: string;
71
-
72
- public name: string;
73
-
74
- constructor(scope: string | null | undefined, name: string) {
75
- this.scope = scope || undefined;
76
- this.name = name;
77
- }
78
-
79
- /** Full npm name (`@scope/name` or bare `name`). */
80
- public get packageName(): string {
81
- return this.scope ? `@${this.scope}/${this.name}` : this.name;
82
- }
83
-
84
- /**
85
- * Parse an npm package name into scope and unscoped segments without rewriting them.
86
- */
87
- static parse(value: string): PackageIdentifier | undefined {
88
- const trimmed = value?.trim();
89
- if (!trimmed) return undefined;
90
-
91
- if (trimmed.startsWith("@")) {
92
- const slash = trimmed.indexOf("/", 1);
93
- if (slash === -1) return new PackageIdentifier(trimmed.slice(1), "");
94
- return new PackageIdentifier(trimmed.slice(1, slash), trimmed.slice(slash + 1));
95
- }
96
-
97
- const slash = trimmed.indexOf("/");
98
- if (slash === -1) return new PackageIdentifier(undefined, trimmed);
99
- return new PackageIdentifier(trimmed.slice(0, slash), trimmed.slice(slash + 1));
100
- }
101
-
102
- /**
103
- * Build from ordered path parts. One segment stays bare; multiple become
104
- * `@<first>/<rest joined by ->`.
105
- *
106
- * The leading segment is the npm `@scope`, kebab-cased with
107
- * {@link string.toSlug} so a multi-word scope survives intact
108
- * (`dbx-tools` -> `dbx-tools`, not `dbx`/`tools`). Every later path
109
- * segment is tokenized with {@link string.tokenize}, so nested folders
110
- * split into their own dash-joined name parts.
111
- */
112
- static of(...names: OneOrMany<string>): PackageIdentifier {
113
- const segments = names.flatMap((part) => part.split("/")).filter(Boolean);
114
- const scope = segments.length ? string.toSlug(segments[0]!) : "";
115
- const nameParts = [
116
- scope,
117
- ...segments.slice(1).flatMap((segment) => [...string.tokenize(segment)]),
118
- ].filter(Boolean);
119
- if (!nameParts.length) throw new Error(`Invalid name: ${names.join(", ")}`);
120
- if (nameParts.length === 1) return new PackageIdentifier(undefined, nameParts[0]!);
121
- return new PackageIdentifier(nameParts[0], nameParts.slice(1).join("-"));
122
- }
123
- }
124
-
125
- /** Parsed `package.json` `name` for a projen `NodeProject`. */
126
- export function identifier(project: Project): PackageIdentifier {
127
- return PackageIdentifier.parse(project.name) ?? new PackageIdentifier(undefined, project.name);
128
- }
129
-
130
- /** Root-only `package.json` fields. */
131
- function configureRootPackage(project: javascript.NodeProject): void {
132
- project.package.addField("type", "module");
133
- project.package.addField("private", true);
134
- }
135
-
136
- /**
137
- * Stamp `repository` on a package's manifest so npm provenance can validate the
138
- * published source (without it, publish fails with E422). A child also carries the
139
- * monorepo `directory` subpath (its path relative to the root); the root omits it.
140
- * No-op when no git remote is detected and no `repository` override was supplied.
141
- * The URL is auto-detected + cached by {@link coreProject.repositoryUrl} (gh, then
142
- * a normalized git remote), in npm's `git+https://.../repo.git` form.
143
- */
144
- function applyRepository(project: javascript.NodeProject, override?: string): void {
145
- const url = override && override.length ? override : coreProject.repositoryUrl(repoRoot, "npm");
146
- if (!url) return;
147
- const root = project.parent ?? project;
148
- const directory = toPosix(relative(resolve(root.outdir), resolve(project.outdir)));
149
- project.package.addField("repository", {
150
- type: "git",
151
- url,
152
- ...(directory ? { directory } : {}),
153
- });
154
- }
155
-
156
- /** Inherit a parent's package manager, else bun. */
157
- function inheritedPackageManager(
158
- parent: javascript.NodeProject | undefined,
159
- ): javascript.NodePackageManager {
160
- return parent?.package.packageManager ?? javascript.NodePackageManager.BUN;
161
- }
162
-
163
- /** Override a package's generated tsconfig `compilerOptions` (later-wins per key). */
164
- export function applyCompilerOptions(
165
- pkg: javascript.NodeProject,
166
- compilerOptions: javascript.TypeScriptCompilerOptions,
167
- ): void {
168
- if (!(pkg instanceof typescript.TypeScriptProject)) return;
169
- const file = pkg.tsconfig?.file;
170
- if (!file) return;
171
- for (const [key, value] of Object.entries(compilerOptions)) {
172
- if (value === undefined) continue;
173
- file.addOverride(`compilerOptions.${key}`, value);
174
- }
175
- }
176
-
177
- /**
178
- * Add `include` globs to a package's generated tsconfig. The tag defaults cover
179
- * `src/**` only, so a package that compiles code OUTSIDE `src/` (its root
180
- * `index.ts` barrel, a `bin/` or `tasks/` tree) needs the extra entries - pair
181
- * this with a `rootDir: "."` in {@link applyCompilerOptions}.
182
- */
183
- export function applyIncludes(pkg: javascript.NodeProject, ...includes: string[]): void {
184
- if (!(pkg instanceof typescript.TypeScriptProject)) return;
185
- for (const include of includes) pkg.tsconfig?.addInclude(include);
186
- }
187
-
188
- /** Apply a tag's `tasks` through projen's task system. */
189
- export function applyTasks(pkg: javascript.NodeProject, tasks?: Record<string, TaskOptions>): void {
190
- if (!tasks) return;
191
- for (const [name, options] of Object.entries(tasks)) {
192
- const owned = name === "build" ? pkg.compileTask : pkg.tasks.tryFind(name);
193
- if (owned) owned.reset(options.exec, options);
194
- else pkg.addTask(name, options);
195
- }
196
- }
197
-
198
- /**
199
- * Set a package's `exports` subpath map (whole-field replace, so a later mixin
200
- * that supplies a fuller surface wins over a tag default). Lets the `cli` / `ui`
201
- * / `app` tags carry their standard export layout and a package only re-declare
202
- * `exports` when it deviates.
203
- */
204
- export function applyExports(pkg: javascript.NodeProject, exports: Record<string, string>): void {
205
- pkg.package.addField("exports", exports);
206
- // Keep the `main`/`types` entry points consistent with the map. A subpath-only
207
- // surface (the `ui` tag's `./react` + `./styles.css`) has no
208
- // `.` export, so the constructor's `main`/`types` would keep advertising a
209
- // root entry that every exports-aware resolver ignores - the contradiction
210
- // publint reports as "exports is missing the root entrypoint".
211
- if (!exports["."]) {
212
- pkg.package.addField("main", undefined);
213
- pkg.package.addField("types", undefined);
214
- }
215
- }
216
-
217
- /**
218
- * MERGE extra subpaths onto a package's existing `exports` map (later keys win),
219
- * preserving whatever a tag default already set. Use when a package just ADDS a
220
- * subpath - e.g. the CLI tag's `.` + `./package.json` default plus dbx-tools'
221
- * `./pnpm` - so the two common entries need not be re-listed. Contrast with
222
- * {@link applyExports}, which replaces the whole field.
223
- *
224
- * New subpaths are inserted before the conventional trailing `./package.json`
225
- * entry when present, so ordering stays `.` -> subpaths -> `./package.json`.
226
- */
227
- export function addExports(pkg: javascript.NodeProject, exports: Record<string, string>): void {
228
- const current = (pkg.package.manifest.exports ?? {}) as Record<string, string>;
229
- const { "./package.json": packageJson, ...rest } = current;
230
- pkg.package.addField("exports", {
231
- ...rest,
232
- ...exports,
233
- ...(packageJson !== undefined ? { "./package.json": packageJson } : {}),
234
- });
235
- }
236
-
237
- /**
238
- * MERGE entries onto a package's npm `files` allowlist - the only paths that
239
- * ship in the published tarball. npm always includes `package.json`, `README`,
240
- * and `LICENSE` on top of whatever is listed, so those are never declared here.
241
- *
242
- * The baseline (`index.ts` + `src`, set at construction) is the source-first
243
- * entry surface the workspace's own `exports` map resolves to. A tag adds what
244
- * its layout ships outside `src` - the `cli` tag its `bin/` launchers - and
245
- * {@link applyCompiledPublish} adds `lib/`, which is what the PUBLISHED
246
- * `exports` resolves to. Source ships alongside the compiled output rather than
247
- * instead of it: it costs little, and it keeps stack traces and go-to-definition
248
- * landing on real code for consumers that want it.
249
- *
250
- * Everything else the build leaves behind (`test/`, `.projen/`, `tsconfig*`) is
251
- * unreachable through either map and is deliberately withheld.
252
- */
253
- export function addPackageFiles(pkg: javascript.NodeProject, ...entries: string[]): void {
254
- const current = (pkg.package.manifest.files ?? []) as string[];
255
- pkg.package.addField("files", [...new Set([...current, ...entries])]);
256
- }
257
-
258
- /**
259
- * The `./<name>` -> `./src/<name>.ts` subpath map for a package's top-level `src`
260
- * modules, skipping `_`-prefixed private modules and declaration files.
261
- *
262
- * This widens no API surface: the root `index.ts` barrel already re-exports every
263
- * non-`_` module, so those names are public through `.` either way - the subpaths
264
- * just add a narrower import path. Deriving the map is what lets a tag carry the
265
- * whole export layout, instead of each package hand-listing its own modules.
266
- */
267
- export function srcModuleExports(pkg: javascript.NodeProject): Record<string, string> {
268
- const srcDir = join(pkg.outdir, "src");
269
- if (!existsSync(srcDir)) return {};
270
-
271
- const exports: Record<string, string> = {};
272
- for (const file of readdirSync(srcDir).sort()) {
273
- if (file.startsWith("_") || !file.endsWith(".ts") || file.endsWith(".d.ts")) continue;
274
- exports[`./${file.slice(0, -".ts".length)}`] = `./src/${file}`;
275
- }
276
- return exports;
277
- }
278
-
279
- /**
280
- * ESM compiler options every package shares regardless of tag.
281
- *
282
- * Relative imports in this repo carry their REAL extension (`./http.ts`), which
283
- * is what lets `tsc` rewrite them to `./http.js` on emit
284
- * (`rewriteRelativeImportExtensions`) instead of a post-processing pass fixing up
285
- * the emitted tree. Both flags belong here rather than on the publishing packages
286
- * only: the specifier style is a property of the SOURCE, so a `ui` package (which
287
- * publishes source and is excluded from the compiled surface) still has to accept
288
- * and rewrite it.
289
- *
290
- * `jsx` is here for the same reason, and it is NOT a per-tag concern even though
291
- * only React packages author `.tsx`. Packages resolve each other to SOURCE
292
- * (`main: index.ts`), so a consumer type-checks its dependency's files under its
293
- * OWN tsconfig: the moment any package re-exports a `.tsx` module, every package
294
- * that imports it - however far down the graph, whatever its tag - fails with
295
- * `TS6142: ... but '--jsx' is not set`. Setting it per consumer is the wrong fix
296
- * (the consumer does not author JSX and has no way to know a transitive dependency
297
- * started to), so the floor carries it. The option is inert for a package with no
298
- * `.tsx` in its graph: it selects how JSX syntax COMPILES and adds no lib, no
299
- * global, and no type dependency on its own.
300
- */
301
- const SHARED_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions & {
302
- rewriteRelativeImportExtensions: boolean;
303
- } = {
304
- module: "ESNext",
305
- moduleResolution: javascript.TypeScriptModuleResolution.BUNDLER,
306
- skipLibCheck: true,
307
- allowImportingTsExtensions: true,
308
- rewriteRelativeImportExtensions: true,
309
- jsx: javascript.TypeScriptJsxMode.REACT_JSX,
310
- };
311
-
312
- /** Shared formatting rules, applied by projen's Prettier on whichever project is root. */
313
- const PRETTIER_SETTINGS: javascript.PrettierSettings = {
314
- printWidth: 100,
315
- tabWidth: 2,
316
- useTabs: false,
317
- semi: true,
318
- singleQuote: false,
319
- quoteProps: javascript.QuoteProps.ASNEEDED,
320
- jsxSingleQuote: false,
321
- trailingComma: javascript.TrailingComma.ALL,
322
- bracketSpacing: true,
323
- bracketSameLine: false,
324
- arrowParens: javascript.ArrowParens.ALWAYS,
325
- endOfLine: javascript.EndOfLine.LF,
326
- };
327
-
328
- /**
329
- * The `projen` version every generated manifest pins.
330
- *
331
- * Kept as one constant so the root's devDependency and this engine's own
332
- * dependency can never drift apart - a synth run loads the engine from one copy
333
- * of projen and the tasks execute against another otherwise.
334
- */
335
- export const PROJEN_VERSION = "^0.101.16";
336
-
337
- /**
338
- * The engine's opinionated `NodeProject` defaults. A caller's own options override
339
- * these (they are spread AFTER this). Root-only concerns key off `options.parent`,
340
- * NOT the class: only the tree ROOT (no parent) turns on projen's built-in Prettier
341
- * (the `prettier` devDep + `.prettierrc.json` + `.prettierignore`), so a child package
342
- * inherits the root's config rather than emitting its own. `name`/`defaultReleaseBranch`
343
- * are resolved/applied by the caller.
344
- */
345
- function defaultProjectOptions(options: DBXToolsProjectOptions): DBXToolsProjectOptions {
346
- const isRoot = options.parent === undefined;
347
- return {
348
- // Bun owns install/run/build/test locally and in CI. projen renders
349
- // `bun install`/`bunx` and a native `trustedDependencies` field from this.
350
- // The engine still emits `pnpm-workspace.yaml` itself (see
351
- // {@link PnpmWorkspaceState}) for the Databricks Apps platform, whose build
352
- // phase installs with pnpm - so a deployed app keeps its catalog + build
353
- // allowances even though the local/CI manager is bun.
354
- packageManager: javascript.NodePackageManager.BUN,
355
- // Pinned rather than left to projen's "latest": 0.101.16 is the first release
356
- // whose `NodePackage` renders bun's `trustedDependencies` natively. Under bun,
357
- // projen does NOT create the `pnpm-workspace.yaml` component itself (that call
358
- // site is gated to pnpm), so the engine constructs it directly ({@link
359
- // PnpmWorkspaceState}). Floating would let an install cross that boundary
360
- // silently, so the co-tested version is stated here and bumped deliberately.
361
- projenVersion: PROJEN_VERSION,
362
- defaultReleaseBranch: "main",
363
- projenrcJs: false,
364
- // Every CHILD is a publishable package, so it needs
365
- // `publishConfig.access: public` - projen renders that from `npmAccess`
366
- // whenever the value differs from the name's default, and every child here is
367
- // scoped (`@dbx-tools/*`), whose default is RESTRICTED. Root-only exclusion is
368
- // deliberate: a root's name is unscoped, so PUBLIC *is* its default and projen
369
- // would omit the key - except that `npmProvenance` then defaults on and forces
370
- // the block to render, giving the root a `publishConfig` it does not have
371
- // today. Provenance is never written to a manifest here (projen only reads it
372
- // in its own `Publisher`, and `release: false` means none exists); the
373
- // tag-driven `release` workflow opts in per-run via `npm_config_provenance`
374
- // instead, so LOCAL publishes to a verdaccio still work with no CI OIDC
375
- // provider. See {@link DBXToolsRelease}.
376
- ...(isRoot ? {} : { npmAccess: javascript.NpmAccess.PUBLIC }),
377
- buildWorkflow: false,
378
- release: false,
379
- // No `npm pack` step on any project. projen wires `package` into `build`, so
380
- // `bunx projen build` would tarball all 36 manifests (root included) into
381
- // gitignored `dist/js` on every CI run and never read them: publishing here
382
- // is `bun publish` driving each package's own `prepack` (see
383
- // {@link applyCompiledPublish} and the `publish` task), and `release: false`
384
- // means no projen Publisher exists to consume the artifacts either.
385
- package: false,
386
- jest: false,
387
- github: false,
388
- npmignoreEnabled: false,
389
- licensed: false,
390
- entrypoint: "",
391
- depsUpgrade: false,
392
- // Bins are declared explicitly via `p.package.addBin(...)`. projen's default
393
- // auto-detection scans the `bin/` dir and adds every EXECUTABLE file keyed by
394
- // its filename, so an executable `bin/dbx-tools.ts` becomes a spurious second
395
- // bin named `dbx-tools.ts` (breaking `pnpm dlx` with ERR_PNPM_DLX_MULTIPLE_BINS).
396
- autoDetectBin: false,
397
- peerDependencyOptions: { pinnedDevDependency: false },
398
- addPackageManagerToDevEngines: false,
399
- devDeps: ["@types/node@^24.6.0"],
400
- ...(isRoot
401
- ? {
402
- prettier: true,
403
- prettierOptions: {
404
- settings: PRETTIER_SETTINGS,
405
- ignoreFile: true,
406
- ignoreFileOptions: { ignorePatterns: [...ignore.ignorePatterns({ test: false })] },
407
- },
408
- }
409
- : {}),
410
- ...options,
411
- ...copiedGitIgnoreOptions(options),
412
- };
413
- }
414
-
415
- /**
416
- * `gitIgnoreOptions` with its `ignorePatterns` array CLONED, for handing to a
417
- * projen `Project` constructor: projen's IgnoreFile ALIASES the array it is given
418
- * (every later addPatterns call mutates it), so the throwaway default-laden
419
- * `.gitignore` gets a copy - {@link swapChildGitignore} re-reads the caller's
420
- * pristine array to seed a child's fresh one. Spread AFTER `...options`.
421
- */
422
- function copiedGitIgnoreOptions(
423
- options: DBXToolsProjectOptions,
424
- ): Pick<javascript.NodeProjectOptions, "gitIgnoreOptions"> {
425
- if (!options.gitIgnoreOptions?.ignorePatterns) return {};
426
- return {
427
- gitIgnoreOptions: {
428
- ...options.gitIgnoreOptions,
429
- ignorePatterns: [...options.gitIgnoreOptions.ignorePatterns],
430
- },
431
- };
432
- }
433
-
434
- /**
435
- * The engine's `TypeScriptProject` defaults - a superset of {@link defaultProjectOptions}.
436
- * A DBXTools TS project can itself be the ROOT (a standalone compiling root), so the
437
- * same parent-based root/child logic applies; this just layers on typescript +
438
- * bun types and disables sample code. No `tsx`: bun runs `.ts` directly.
439
- */
440
- function defaultTypeScriptProjectOptions(
441
- options: DBXToolsTypeScriptProjectOptions,
442
- ): DBXToolsTypeScriptProjectOptions {
443
- const base = defaultProjectOptions(options);
444
- return {
445
- ...base,
446
- sampleCode: false,
447
- entrypoint: undefined,
448
- // ESLint is configured once on the ROOT (see initProject) and lints the whole
449
- // tree, so packages don't emit their own config. A caller can still override.
450
- eslint: false,
451
- devDeps: [...(base.devDeps ?? []), "typescript@^5.9.3", "@types/bun@^1.3.14"],
452
- ...options,
453
- ...copiedGitIgnoreOptions(options),
454
- };
455
- }
456
-
457
- // Pinned to match the subproject defaults so bun resolves a single typescript
458
- // across the workspace (a bare name -> `*` could pull a second, newer major).
459
- // `@types/bun` gives the `Bun.*` globals the server/app tags now use.
460
- const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", "@types/bun@^1.3.14"];
461
-
462
- /** Options for {@link DBXToolsNodeProject} (the monorepo root). */
463
- export interface DBXToolsProjectOptions
464
- extends
465
- Partial<javascript.NodeProjectOptions>,
466
- DBXToolsConfigOptions,
467
- DBXToolsPNPMWorkspaceOptions {
468
- /**
469
- * The npm scope for generated package names (`@<scope>/<seg-...>`). Defaults to
470
- * the (resolved) project name; a leading `@` is optional.
471
- */
472
- readonly scope?: string;
473
- /**
474
- * Roots scanned for packages (each `src`-bearing folder under a root is one).
475
- * Only a ROOT scans. Defaults to {@link DEFAULT_PACKAGE_ROOTS}.
476
- */
477
- readonly packageRoots?: readonly string[];
478
- /**
479
- * Leading path segment(s) dropped from a discovered package's relative path
480
- * before its npm name is derived, so a tier folder doesn't become a name
481
- * prefix. E.g. with the default `"node"`, `packages/node/path` names as
482
- * `@<scope>/path` instead of `@<scope>/node-path` (its `node` TAG still
483
- * derives from the path). One or many segment names; a segment is only
484
- * stripped when it is the FIRST segment of the relative path. Pass `[]` to
485
- * disable. Defaults to `"node"`.
486
- */
487
- readonly omitRelativePrefix?: OneOrMany<string>;
488
- /**
489
- * Maps a path token / relPath / glob to tag(s), unioned into a package's
490
- * path-derived tags. Defaults to an identity map over the known tag names; a
491
- * `""`/`"."` key tags the root.
492
- */
493
- readonly packageTagPaths?: Record<string, string[]>;
494
- /**
495
- * Which built-in {@link PACKAGE_TAG_MIXINS} to apply and seed
496
- * `packageTagPaths` identity entries for. Omitted = all; `false` = none;
497
- * a list = only those tags.
498
- */
499
- readonly defaultTagMixins?: false | PackageTag[];
500
- /**
501
- * Extra repo-root paths that trigger a full re-synth during `sync --watch`
502
- * (alongside `.projenrc.ts`). Repo-relative, e.g. `".example.projenrc.ts"`.
503
- */
504
- readonly syncResynthPaths?: readonly string[];
505
- /**
506
- * Standalone in-repo projects (NOT workspace members) that each get their own
507
- * tag-driven release workflow authored alongside the root's `release`
508
- * workflow - see {@link StandaloneRelease}. Use for a project that lives in a
509
- * repo subdirectory but releases on its own tag prefix (e.g. the
510
- * `@dbx-tools/projen` engine in `projen/`, tagged `projen-v*`).
511
- */
512
- readonly standaloneReleases?: readonly StandaloneRelease[];
513
- /**
514
- * Extra workspace member paths (repo-relative, POSIX) to list in the workspace
515
- * config ALONGSIDE the discovered `packageRoots` members - for a package that
516
- * is synthesized by its OWN `.projenrc.ts` (so it isn't a root subproject) but
517
- * should still resolve as a workspace sibling. The `@dbx-tools/projen` engine in
518
- * `projen/` is the case: it synthesizes itself (avoiding a dogfooding cycle) yet
519
- * is a member of the single bun workspace, so the root links it from source.
520
- */
521
- readonly extraWorkspaceMembers?: readonly string[];
522
- /**
523
- * Install workspace dependencies once from the custom root instead of once
524
- * per child project during post-synthesis. Defaults to `true`; set `false` to
525
- * preserve projen's native per-project install tasks.
526
- */
527
- readonly rootInstallOnly?: boolean;
528
- }
529
-
530
- /** Options for {@link DBXToolsTypeScriptProject} (a package, or a compiling root). */
531
- export interface DBXToolsTypeScriptProjectOptions
532
- extends Partial<typescript.TypeScriptProjectOptions>, DBXToolsProjectOptions {
533
- /** Emit the projen-owned bun app scaffolding (`bunfig.toml`/`dev.ts`/`build.ts`). */
534
- readonly bunApp?: boolean;
535
- }
536
-
537
- /**
538
- * A monorepo root. Scans `packageRoots` and appends a
539
- * {@link DBXToolsTypeScriptProject} per `src`-bearing folder, then emits the
540
- * shared config, tasks, `pnpm-workspace.yaml`, and barrels-on-synth.
541
- */
542
- export class DBXToolsNodeProject extends javascript.NodeProject implements DBXToolsProject {
543
- readonly scope: string;
544
- readonly dbxToolsConfig: DBXToolsConfig;
545
- pnpmWorkspace?: PnpmWorkspaceState;
546
- rootTsconfig?: DBXToolsRootTsconfig;
547
- vsCode?: DBXToolsVsCode;
548
- private readonly extraWorkspaceMembers: readonly string[];
549
- private readonly rootInstallOnly: boolean;
550
-
551
- constructor(options: DBXToolsProjectOptions = {}) {
552
- const { name, scope } = resolveIdentity(options);
553
- const releaseDefaults =
554
- options.release && options.releaseTrigger === undefined
555
- ? { releaseTrigger: ReleaseTrigger.tagged({ tags: ["v*"] }) }
556
- : {};
557
- // Holds the workspace state (members/catalog/allowBuilds/overrides). Under
558
- // bun, projen's base constructor does NOT create the `PnpmWorkspaceYaml`
559
- // component (its `configurePnpm` call site is gated to pnpm), so this state's
560
- // options are wired into a directly-constructed component below - AND mirrored
561
- // into `package.json` (`workspaces`/`catalog`) for bun to read. The
562
- // `pnpm-workspace.yaml` is still emitted for the Databricks Apps pnpm install.
563
- const pnpmWorkspace = new PnpmWorkspaceState(options);
564
- super({
565
- ...defaultProjectOptions(options),
566
- ...releaseDefaults,
567
- pnpmOptions: {
568
- ...options.pnpmOptions,
569
- workspaceYamlOptions: pnpmWorkspace.options,
570
- },
571
- name,
572
- });
573
-
574
- this.pnpmWorkspace = pnpmWorkspace;
575
- // Emit `pnpm-workspace.yaml` ourselves: under bun projen skips the native
576
- // component, but the file is still required by the Databricks Apps platform
577
- // (its build phase installs with pnpm and reads catalog + `allowBuilds`).
578
- pnpmWorkspace.attachWorkspaceFile(this);
579
- this.scope = scope;
580
- this.extraWorkspaceMembers = options.extraWorkspaceMembers ?? [];
581
- this.rootInstallOnly = options.rootInstallOnly !== false;
582
- this.dbxToolsConfig = new DBXToolsConfig(this, options);
583
- initProject(this, options);
584
- }
585
-
586
- public override preSynthesize(): void {
587
- if (this.rootInstallOnly) this.with(ROOT_INSTALL_ONLY_MIXIN);
588
- super.preSynthesize();
589
- // Members come from the attached subprojects, which the root's scan appends
590
- // after construction - so the list is filled here, not in the constructor.
591
- // `extraWorkspaceMembers` adds self-synthesizing siblings (e.g. `projen/`).
592
- this.pnpmWorkspace?.resolveMembers(this, this.extraWorkspaceMembers);
593
- preSynthesizeProject(this);
594
- }
595
- }
596
-
597
- /**
598
- * Root-owned workspace install policy.
599
- *
600
- * Every projen child has its own `NodePackage` post-synth hook, which otherwise
601
- * runs `bun install` against the same root workspace once per package. Clear the
602
- * child install tasks while leaving the root's real install/install:ci tasks
603
- * intact. Applied in root `preSynthesize` so manually attached late children are
604
- * included and repeated synths remain idempotent.
605
- */
606
- export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
607
- (construct: IConstruct): construct is DBXToolsNodeProject | DBXToolsTypeScriptProject =>
608
- (construct instanceof DBXToolsNodeProject || construct instanceof DBXToolsTypeScriptProject) &&
609
- construct.parent !== undefined,
610
- (child) => {
611
- child.package.installTask.reset();
612
- child.package.installCiTask.reset();
613
- },
614
- );
615
-
616
- /**
617
- * A single package (usually created by a root's scan), or a standalone
618
- * compiling root. The agnostic tsconfig floor is applied at construction; the
619
- * source-first package fields (`main`/`types`/`exports` -> `index.ts`) and optional
620
- * Bun app scaffolding are applied after. Per-tag deps/tsconfig arrive later
621
- * via the {@link PACKAGE_TAG_MIXINS} the root applies.
622
- */
623
- export class DBXToolsTypeScriptProject
624
- extends typescript.TypeScriptProject
625
- implements DBXToolsProject
626
- {
627
- readonly scope: string;
628
- readonly dbxToolsConfig: DBXToolsConfig;
629
- pnpmWorkspace?: PnpmWorkspaceState;
630
- rootTsconfig?: DBXToolsRootTsconfig;
631
- vsCode?: DBXToolsVsCode;
632
-
633
- constructor(options: DBXToolsTypeScriptProjectOptions) {
634
- const { name, scope } = resolveIdentity(options);
635
- const parent = options?.parent;
636
- const packageManager =
637
- options.packageManager ??
638
- inheritedPackageManager(parent instanceof javascript.NodeProject ? parent : undefined);
639
-
640
- super({
641
- ...defaultTypeScriptProjectOptions(options),
642
- name: options.name ?? name,
643
- packageManager,
644
- tsconfig: {
645
- ...options.tsconfig,
646
- include: options.tsconfig?.include,
647
- // Every package starts from the agnostic floor (ES2022, no DOM/node); a tag
648
- // mixin layers its `lib`/`jsx`/`types` on top afterward via `project.with`.
649
- compilerOptions: {
650
- ...SHARED_COMPILER_OPTIONS,
651
- ...AGNOSTIC_COMPILER_OPTIONS,
652
- ...options.tsconfig?.compilerOptions,
653
- },
654
- },
655
- });
656
- this.scope = scope;
657
- // Pairs with `jsx` in SHARED_COMPILER_OPTIONS: projen's default `include` is
658
- // `src/**/*.ts` only, which silently omits a `.tsx` file from the program
659
- // instead of failing, so authoring a React component would otherwise need
660
- // per-package tsconfig config to be compiled at all.
661
- this.tsconfig?.addInclude("src/**/*.tsx");
662
- this.dbxToolsConfig = new DBXToolsConfig(this, options);
663
- // Source-first entry: point the package at its package-ROOT `index.ts` barrel
664
- // so packages resolve each other's `@scope/pkg` imports to source.
665
- this.package.addField("type", "module");
666
- this.package.addField("main", "index.ts");
667
- this.package.addField("types", "index.ts");
668
- this.package.addField("exports", {
669
- ".": "./index.ts",
670
- "./package.json": "./package.json",
671
- });
672
- addPackageFiles(this, "index.ts", "src");
673
- // `bun test` intercepts `node:test` (the suites keep using node:test) and
674
- // runs it with bun's own fast runner. Args are FILTERS, not globs; a bare
675
- // directory auto-discovers `*.test.ts` recursively. But `bun test` EXITS 1
676
- // when it matches no files, so guard it: only invoke when a `*.test.ts`
677
- // exists, else succeed.
678
- this.testTask.exec("bun test test", {
679
- condition: 'find test -name "*.test.ts" 2>/dev/null | grep -q .',
680
- });
681
- if (options.bunApp ?? false) {
682
- new BunfigFile(this);
683
- new BunDevServerFile(this);
684
- new BunBuildFile(this);
685
- }
686
- initProject(this, options);
687
- }
688
-
689
- public override preSynthesize(): void {
690
- super.preSynthesize();
691
- preSynthesizeProject(this);
692
- }
693
- }
694
-
695
- /**
696
- * Regenerates the repo's generated source after synth: first the codegen
697
- * modules (ts-to-zod schemas from each `codegen`-declaring package's upstream
698
- * `.d.ts`), then every package's root `index.ts` barrel - so a freshly
699
- * generated module is namespaced into its barrel in the same pass. This is the
700
- * "generate on resynth" path for plain `projen`; codegen inputs (SDK `.d.ts`)
701
- * change rarely, so a synth-time regen is enough and there's no separate watch.
702
- *
703
- * projen only runs `postSynthesize` when `PROJEN_DISABLE_POST` is unset, so this
704
- * is skipped during the watcher's fast `runSynth` (which sets it); there barrels
705
- * are rebuilt explicitly. It also runs after `NodeProject`'s own post-synth
706
- * install, so codegen's `node_modules/...` inputs resolve.
707
- */
708
- class GeneratedSource extends Component {
709
- public override postSynthesize(): void {
710
- generateCodegen();
711
- generateBarrels();
712
- }
713
- }
714
-
715
- /**
716
- * Make the ROOT `compile` / `test` tasks actually validate the workspace.
717
- *
718
- * projen gives a monorepo root empty `compile`/`test` tasks - a child's tasks
719
- * are the child's business - so `bun run build` at the root type-checked nothing
720
- * and ran no package tests. The fan-out is delegated to bun's own workspace
721
- * filter rather than one `exec` per member, which matters three ways: bun runs
722
- * the members in PARALLEL (measured ~2.5x faster across this repo than the
723
- * sequential per-`cwd` form), a member that does not define the script is
724
- * skipped instead of needing a guard, and the filter reads the workspace from
725
- * `package.json` - so it stays correct when a package is added without a
726
- * re-synth. A non-zero member exit still fails the run.
727
- *
728
- * `*` matches every workspace MEMBER and never the root itself, so the root
729
- * task delegating to it cannot recurse. Members declared outside the scanned
730
- * package roots (`extraWorkspaceMembers`) are workspace members too, so they are
731
- * covered by the same filter.
732
- */
733
- class WorkspaceValidationTasks extends Component {
734
- private configured = false;
735
-
736
- public override preSynthesize(): void {
737
- if (this.configured) return;
738
- this.configured = true;
739
- const project = this.project as javascript.NodeProject;
740
- for (const task of [project.compileTask, project.testTask]) {
741
- task.exec(`bun run --filter '*' ${task.name}`);
742
- }
743
- }
744
- }
745
-
746
- /**
747
- * Bound the default validation workflows when a root opts into them.
748
- *
749
- * Projen otherwise leaves jobs at GitHub's six-hour ceiling. Missing workflows
750
- * are a no-op, so roots that keep the engine defaults (`github`/build workflow
751
- * off) do not gain new files.
752
- */
753
- class WorkflowTimeouts extends Component {
754
- public override preSynthesize(): void {
755
- const build = this.project.tryFindObjectFile(".github/workflows/build.yml");
756
- for (const job of ["build", "self-mutation"]) {
757
- build?.addOverride(`jobs.${job}.timeout-minutes`, 30);
758
- }
759
- this.project
760
- .tryFindObjectFile(".github/workflows/pull-request-lint.yml")
761
- ?.addOverride("jobs.validate.timeout-minutes", 10);
762
- }
763
- }
764
-
765
- /**
766
- * Ignore each `codegen`-declaring package's `src/` from the root ESLint config.
767
- * Those modules are read-only (ts-to-zod); lint `--fix` otherwise EACCES-crashes
768
- * on them. Runs in `preSynthesize` so mixin-added `codegen.inputs` are visible.
769
- */
770
- class EslintIgnoreCodegen extends Component {
771
- public override preSynthesize(): void {
772
- const eslint = javascript.Eslint.of(this.project);
773
- if (!eslint) return;
774
- const rootAbs = resolve(this.project.outdir);
775
- for (const sub of this.project.subprojects) {
776
- if (!(sub instanceof javascript.NodeProject)) continue;
777
- const codegen = sub.package.manifest.codegen as { inputs?: string[] } | undefined;
778
- if (!codegen?.inputs?.length) continue;
779
- const rel = toPosix(relative(rootAbs, sub.outdir));
780
- // Ignore the generated MODULES, not the package's whole `src/`. A codegen
781
- // package may hold hand-written modules next to its generated ones
782
- // (shared-genie generates `dashboards.ts` beside a hand-written
783
- // `genie-model.ts`), and a blanket `src/**` would silently stop linting
784
- // them - the failure mode being invisible, since ESLint just reports less.
785
- for (const module of codegenModulePaths(codegen.inputs)) {
786
- eslint.addIgnorePattern(`${rel}/${module}`);
787
- }
788
- }
789
- }
790
- }
791
-
792
- /**
793
- * Keep generated package barrels and codegen modules out of root formatting.
794
- *
795
- * Their generators own the layout and may mark outputs read-only. Package paths
796
- * are derived from attached subprojects so custom `packageRoots` need no manual
797
- * Prettier patterns.
798
- */
799
- class PrettierIgnoreGenerated extends Component {
800
- public override preSynthesize(): void {
801
- const prettier = javascript.Prettier.of(this.project);
802
- if (!prettier) return;
803
- const rootAbs = resolve(this.project.outdir);
804
- for (const sub of this.project.subprojects) {
805
- if (!(sub instanceof javascript.NodeProject)) continue;
806
- const rel = toPosix(relative(rootAbs, sub.outdir));
807
- prettier.addIgnorePattern(`${rel}/index.ts`);
808
- const codegen = sub.package.manifest.codegen as { inputs?: string[] } | undefined;
809
- for (const module of codegenModulePaths(codegen?.inputs ?? [])) {
810
- prettier.addIgnorePattern(`${rel}/${module}`);
811
- }
812
- }
813
- }
814
- }
815
-
816
- /** Default leading path segment stripped from a package's name (not its tag). */
817
- const DEFAULT_OMIT_RELATIVE_PREFIX = ["node"];
818
-
819
- /** Normalize the {@link DBXToolsProjectOptions.omitRelativePrefix} option to a slug list. */
820
- function resolveOmitRelativePrefix(option: OneOrMany<string> | undefined): string[] {
821
- const raw = option === undefined ? DEFAULT_OMIT_RELATIVE_PREFIX : option;
822
- const list = Array.isArray(raw) ? raw : [raw];
823
- return list.map((segment) => string.toSlug(segment)).filter(Boolean);
824
- }
825
-
826
- /**
827
- * Derive a package's npm name from its scope + relative path, dropping a leading
828
- * `omitPrefixes` segment first (so a tier folder like `node/` doesn't become a
829
- * name prefix). The full `relPath` is still used elsewhere for tags.
830
- */
831
- function packageNameFor(scope: string, relPath: string, omitPrefixes: string[]): string {
832
- const segments = relPath.split("/").filter(Boolean);
833
- if (segments.length > 1 && omitPrefixes.includes(string.toSlug(segments[0]!))) {
834
- segments.shift();
835
- }
836
- return PackageIdentifier.of(scope, segments.join("/")).packageName;
837
- }
838
-
839
- /**
840
- * Resolve `{ name, scope }` from options. `name` is `options.name`, else
841
- * auto-detected (git remote/folder). `scope` is `options.scope`, else the name;
842
- * either way it is parsed through {@link PackageIdentifier} so a scoped value
843
- * (`@dbx-tools` or a full `@dbx-tools/root` name) yields the bare scope `dbx-tools`.
844
- */
845
- function resolveIdentity(options: { name?: string; scope?: string }): {
846
- name: string;
847
- scope: string;
848
- } {
849
- const name = options.name && options.name.length ? options.name : projectName();
850
- const rawScope = options.scope && options.scope.length ? options.scope : name;
851
- const identifier = PackageIdentifier.parse(rawScope);
852
- return { name, scope: identifier?.scope ?? identifier?.name ?? rawScope };
853
- }
13
+ export * from "./project-js.ts";
14
+ export * from "./project-py.ts";
854
15
 
855
- /**
856
- * A devDep entry that keeps the engine itself resolvable for the *next* synth (a
857
- * consumer's `.projenrc.ts` imports the classes from it). Resolved from the
858
- * engine's OWN nearby `package.json`; `undefined` when running as plain in-repo
859
- * SOURCE (not under a `node_modules` segment). Reuses whatever specifier the
860
- * consumer already has for it rather than computing one.
861
- */
862
- function engineSelfDependency(project: javascript.NodeProject): string | undefined {
863
- const enginePkgJson = join(resolvePkgRoot(), "package.json");
864
- if (!toPosix(enginePkgJson).includes("/node_modules/")) return undefined;
865
- const engine = readPackageManifest(dirname(enginePkgJson));
866
- const name = string.trimToNull(engine?.name);
867
- if (!name) return undefined;
868
- const version = string.trimToNull(engine?.version);
869
-
870
- // No existing consumer manifest (or no entry) falls through to a computed pin.
871
- const consumer = readPackageManifest(resolve(project.outdir));
872
- const dependencyOf = (field: unknown): string | undefined =>
873
- object.isRecord(field) ? (string.trimToNull(field[name]) ?? undefined) : undefined;
874
- const existing = dependencyOf(consumer?.devDependencies) ?? dependencyOf(consumer?.dependencies);
875
- if (existing) return `${name}@${existing}`;
876
- return `${name}@^${version}`;
877
- }
16
+ /** Runtime family implemented by a dbx-tools project. */
17
+ export type DBXToolsProjectLanguage = "javascript" | "python";
878
18
 
879
- /** Resolve which {@link PACKAGE_TAG_MIXINS} keys to apply from `defaultTagMixins`. */
880
- function resolveEnabledTagMixins(selection: false | PackageTag[] | undefined): PackageTag[] {
881
- if (selection === false) return [];
882
- if (selection === undefined) {
883
- return Object.keys(PACKAGE_TAG_MIXINS) as PackageTag[];
884
- }
885
- return selection;
886
- }
19
+ /** Options shared by every dbx-tools project implementation. */
20
+ export interface DBXToolsProjectOptions extends Partial<
21
+ Pick<ProjectOptions, "name" | "parent" | "outdir">
22
+ > {}
887
23
 
888
- /** True if `key` matches a discovered package by candidate / relPath / memberPath / glob. */
889
- function tagPathMatches(key: string, p: DiscoveredPackage): boolean {
890
- // Fast path: an exact tag candidate or the package's rel/member path.
891
- if (p.tagCandidates.includes(key) || key === p.relPath || key === p.memberPath) {
892
- return true;
893
- }
894
- // Otherwise treat the key as a glob against the same targets.
895
- const isMatch = match.toPathMatcher(key);
896
- return isMatch(p.relPath) || isMatch(p.memberPath) || p.tagCandidates.some((c) => isMatch(c));
24
+ /** Minimal language-agnostic project contract. */
25
+ export interface DBXToolsProject extends Project {
26
+ readonly language: DBXToolsProjectLanguage;
897
27
  }
898
28
 
899
- /** Resolve a discovered package's tags from the `tagPaths` map (union of matches). */
900
- function resolveTags(p: DiscoveredPackage, tagPaths: Record<string, string[]>): string[] {
901
- const tags: string[] = [];
902
- for (const [key, value] of Object.entries(tagPaths)) {
903
- if (tagPathMatches(key, p)) {
904
- for (const tag of value) if (!tags.includes(tag)) tags.push(tag);
905
- }
906
- }
907
- return tags;
908
- }
909
-
910
- /** Register the native projen tasks on the monorepo root. */
911
- function registerRootTasks(project: javascript.NodeProject): void {
912
- applyTasks(project, {
913
- barrels: { exec: taskScript(project, "barrels.ts") },
914
- openapi: { exec: taskScript(project, "openapi.ts") },
915
- clean: { exec: taskScript(project, "clean.ts"), receiveArgs: true },
916
- // `receiveArgs` forwards `--watch`, so `bun run sync -- --watch` syncs once
917
- // then starts the single node-path watcher loop.
918
- sync: { exec: taskScript(project, "sync.ts"), receiveArgs: true },
919
- });
920
- }
921
-
922
- /**
923
- * `bun node_modules/@dbx-tools/projen/tasks/<script>` command for a projen task.
924
- *
925
- * Use the stable package symlink, never `require.resolve()`'s physical store
926
- * path. A later install can change the peer-hash directory while leaving the
927
- * package symlink valid; persisting the physical path made every generated task
928
- * fail with ERR_MODULE_NOT_FOUND after such an update. bun runs the `.ts`
929
- * directly (no tsx, no build step).
930
- */
931
- export function taskScript(_project: javascript.NodeProject, script: string, args = ""): string {
932
- const scriptPath = toPosix(join("node_modules", "@dbx-tools", "projen", "tasks", script));
933
- return args ? `bun ${scriptPath} ${args}` : `bun ${scriptPath}`;
934
- }
935
-
936
- /**
937
- * Shared init both classes call at the end of their constructor. Only the tree
938
- * ROOT does anything: it attaches the projenrc runner, root devDeps/fields,
939
- * `pnpm-workspace.yaml`, shared config, tasks, gitignore/`annotateGenerated`,
940
- * scans + appends children, applies the built-in tag mixins across the subtree
941
- * (via `project.with`), and adds the barrels-on-synth component. Non-root projects
942
- * only swap in a fresh custom-patterns-only `.gitignore` and return.
943
- */
944
- function initProject(
945
- project: DBXToolsNodeProject | DBXToolsTypeScriptProject,
946
- options: DBXToolsProjectOptions,
947
- ): void {
948
- // projen's GithubProject seeds a `# replace this` SampleReadme on every
949
- // project. READMEs are hand-written and owned outside projen, so drop the
950
- // generated one (and never mark it read-only) - both root and child.
951
- project.tryRemoveFile("README.md");
952
-
953
- if (project.parent) {
954
- project.package.file.readonly = true;
955
- // Stamp `repository` (with this package's `directory` subpath) so a published
956
- // package passes npm provenance validation.
957
- applyRepository(project, options.repository);
958
- // Only a ROOT configures the workspace; a child just swaps its default-laden
959
- // `.gitignore` for a fresh one that carries package-specific patterns only.
960
- swapChildGitignore(project, options);
961
- return;
962
- }
963
- project.package.file.readonly = false;
964
-
965
- // NodeProject has no built-in TS projenrc support (unlike TypeScriptProject), so
966
- // wire `.projenrc.ts` through a runner - this also populates the `default` task
967
- // that `bunx projen` runs (and that the `sync` watcher invokes to re-synth).
968
- // The runner choice is immaterial since the exec is reset to plain `bun` below;
969
- // `nodejs()` avoids declaring a `ts-node`/`tsx` dependency.
970
- new typescript.ProjenrcTs(project, {
971
- runner: typescript.TypeScriptRunner.nodejs(),
972
- });
973
- // bun runs `.projenrc.ts` directly (native TS, no loader to register). Reset to
974
- // a plain `bun` exec rather than any wrapper: the default task is spawned by
975
- // nested installs/synths, and a wrapper that exported `npm_config_*` broke them.
976
- project.defaultTask?.reset("bun .projenrc.ts");
977
-
978
- // Pin bun's hoisted linker workspace-wide (see RootBunfigFile) so a peer dep
979
- // resolves to one copy and singletons/types stay coherent.
980
- new RootBunfigFile(project);
981
-
982
- // Only reached on a ROOT (early-returned above otherwise), so the root devDeps
983
- // always apply; the self-dep is added only when the engine is an installed pkg.
984
- const selfDep = engineSelfDependency(project);
985
- if (selfDep) project.addDevDeps(selfDep);
986
- project.addDevDeps(...DEV_DEPS_ROOT);
987
- configureRootPackage(project);
988
- // Root carries the bare `repository` (no `directory`); children add their subpath.
989
- applyRepository(project, options.repository);
990
-
991
- if (options.syncResynthPaths?.length) {
992
- project.dbxToolsConfig.syncResynthPaths = [...options.syncResynthPaths];
993
- }
994
-
995
- project.rootTsconfig = new DBXToolsRootTsconfig(project);
996
- project.vsCode = new DBXToolsVsCode(project);
997
-
998
- registerRootTasks(project);
999
- if (options.prettier || project.prettier) {
1000
- const formatTask = project.tasks.tryFind("format") ?? project.addTask("format");
1001
- formatTask.prependExec("prettier . --write", { receiveArgs: true });
1002
- }
1003
-
1004
- // `dot: false` for the same reason as `test: false`: the dot group is a
1005
- // SCANNING concern (skip `.git` and caches when walking the tree), and a
1006
- // blanket `**/.*` in a `.gitignore` is both wrong and actively harmful. A repo
1007
- // legitimately commits `.github/`, `.projen/tasks.json`, `.vscode/settings.json`,
1008
- // `.editorconfig`. Worse, `**/.*` excludes those DIRECTORIES, and git refuses
1009
- // to re-include a file whose parent directory is excluded - so every per-file
1010
- // `!/.github/...` negation projen emits for its own generated files silently
1011
- // does nothing, and the file cannot be added at all.
1012
- project.gitignore.addPatterns(...[...ignore.ignorePatterns({ test: false, dot: false })]);
1013
- // What the dot group was actually earning here, named explicitly: secrets and
1014
- // local editor state. Both ignore CONTENTS (`.idea/*`) rather than the
1015
- // directory, so a later `!` negation can still reach a file inside.
1016
- project.gitignore.addPatterns(".env", ".env.*", "!.env.example", "!.env.sample", ".idea/*");
1017
- const roots = options.packageRoots ?? DEFAULT_PACKAGE_ROOTS;
1018
- for (const root of roots) {
1019
- project.annotateGenerated(`/${root}/**/index.ts`);
1020
- project.annotateGenerated(`/${root}/openapi/**`);
1021
- }
1022
-
1023
- // ESLint lives ONLY on the root and lints every package. `projectService` resolves
1024
- // each file to its own package tsconfig (so type-aware rules work tree-wide), and
1025
- // `import/no-extraneous-dependencies` still checks each file against its nearest
1026
- // package.json. Formatting defers to the root Prettier to avoid rule/formatter
1027
- // conflicts (e.g. quote style). The normal task is check-only so CI never
1028
- // repairs the worktree it is meant to validate; `eslint:fix` is the explicit
1029
- // local mutation path.
1030
- const eslint = new javascript.Eslint(project, {
1031
- dirs: [...roots, "projen"],
1032
- fileExtensions: [".ts", ".tsx"],
1033
- projectService: true,
1034
- prettier: Boolean(project.prettier),
1035
- tsconfigPath: "./tsconfig.json",
1036
- commandOptions: { fix: false },
1037
- });
1038
- project.addTask("eslint:fix", {
1039
- description: "Fix ESLint issues across the codebase",
1040
- exec: "bun run eslint -- --fix",
1041
- });
1042
- // Generated read-only outputs (barrels, openapi clients, app scripts, codegen).
1043
- // ESLint --fix cannot rewrite them; they are stamped by the barrel generator /
1044
- // openapi / codegen / projen.
1045
- for (const root of roots) {
1046
- eslint.addIgnorePattern(`${root}/openapi/**`);
1047
- eslint.addIgnorePattern(`${root}/**/index.ts`);
1048
- }
1049
- eslint.addIgnorePattern("projen/index.ts");
1050
- // The generated bun app scripts + unmanaged overrides live at the package root,
1051
- // outside any `src/**` tsconfig include, so the type-aware parser cannot resolve
1052
- // them to a project. ESLint still cannot parse them.
1053
- eslint.addIgnorePattern("**/dev.ts");
1054
- eslint.addIgnorePattern("**/build.ts");
1055
- // A deploy-staging helper that lives at a package root (outside any `src/**`
1056
- // tsconfig), same parse-resolution problem as the bun app scripts above.
1057
- eslint.addIgnorePattern("**/stage-deploy.ts");
1058
- for (const override of BUN_APP_OVERRIDES) {
1059
- eslint.addIgnorePattern(`**/${override}`);
1060
- }
1061
- // Codegen packages declare `codegen.inputs` via mixins after construction; ignore
1062
- // their `src/` once manifests are known (preSynthesize), same reason as openapi.
1063
- new EslintIgnoreCodegen(project);
1064
- eslint.addRules({
1065
- "import/no-relative-packages": "error",
1066
- // Monorepo tooling legitimately uses devDeps (typescript, tsx, projen) in src.
1067
- "import/no-extraneous-dependencies": [
1068
- "error",
1069
- { devDependencies: true, optionalDependencies: false, peerDependencies: true },
1070
- ],
1071
- "@typescript-eslint/no-shadow": "off",
1072
- "no-bitwise": "off",
1073
- "@typescript-eslint/member-ordering": "off",
1074
- });
1075
- eslint.addOverride({
1076
- files: ["**/test/**/*.ts", "**/test/**/*.tsx"],
1077
- // node:test `describe`/`it` return promises by design.
1078
- rules: { "@typescript-eslint/no-floating-promises": "off" },
1079
- });
1080
- if (eslint.config?.settings) {
1081
- // eslint-plugin-import knows Node built-ins but not Bun's test module.
1082
- eslint.config.settings["import/core-modules"] = ["bun:test"];
1083
- }
1084
- // Point the TS import resolver at every package tsconfig, not just the root's
1085
- // (which only includes `.projenrc.ts`), so `import/no-unresolved` resolves
1086
- // cross-package imports.
1087
- const tsResolver = eslint.config?.settings?.["import/resolver"]?.typescript;
1088
- if (tsResolver) {
1089
- tsResolver.project = ["tsconfig.json", ...roots.map((r) => `${r}/**/tsconfig.json`)];
1090
- }
1091
-
1092
- const enabledTagMixins = resolveEnabledTagMixins(options.defaultTagMixins);
1093
- const omitPrefixes = resolveOmitRelativePrefix(options.omitRelativePrefix);
1094
-
1095
- // path token/relPath/glob -> tag(s). Default: identity over the enabled tag names;
1096
- // any packageTagPaths entries AUGMENT that. A `""`/`"."` key tags the root.
1097
- const tagPaths: Record<string, string[]> = {
1098
- ...Object.fromEntries(enabledTagMixins.map((k) => [k, [k]])),
1099
- ...(options.packageTagPaths ?? {}),
1100
- };
1101
-
1102
- // Already-attached subprojects, keyed by repo-relative member path.
1103
- const rootAbs = resolve(project.outdir);
1104
- const existing = new Map<string, DBXToolsProject>();
1105
- for (const sub of project.subprojects) {
1106
- if (sub instanceof DBXToolsNodeProject || sub instanceof DBXToolsTypeScriptProject) {
1107
- existing.set(toPosix(relative(rootAbs, sub.outdir)), sub);
1108
- }
1109
- }
1110
-
1111
- // Discover + append a child per src-bearing folder. A root encapsulating an
1112
- // already-attached project doesn't re-create it, it just unions the tags in. The
1113
- // agnostic floor is set in the child's constructor; per-tag deps/tsconfig come from
1114
- // the PACKAGE_TAG_MIXINS applied across the subtree below.
1115
- for (const p of scanPackages(rootAbs, roots)) {
1116
- const tags = [...new Set([...p.tagCandidates, ...resolveTags(p, tagPaths)])];
1117
- const found = existing.get(p.memberPath);
1118
- if (found) {
1119
- found.dbxToolsConfig.tags.push(...tags);
1120
- continue;
1121
- }
1122
- new DBXToolsTypeScriptProject({
1123
- parent: project,
1124
- outdir: p.memberPath,
1125
- name: packageNameFor(project.scope, p.relPath, omitPrefixes),
1126
- tags,
1127
- });
1128
- }
1129
-
1130
- // The root project may itself carry tags (via a `""`/`"."` tag-path key).
1131
- const rootTags = [...new Set([...(tagPaths[""] ?? []), ...(tagPaths["."] ?? [])])];
1132
- if (rootTags.length) project.dbxToolsConfig.tags.push(...rootTags);
1133
-
1134
- // Apply per-tag mixins across the whole subtree now that every child exists
1135
- // (`construct.with` captures the tree at call time). User mixins run afterward
1136
- // via the caller's own `project.with(...)`.
1137
- if (enabledTagMixins.length) {
1138
- project.with(...enabledTagMixins.map((t) => PACKAGE_TAG_MIXINS[t]));
1139
- }
1140
-
1141
- new WorkspaceValidationTasks(project);
1142
- new WorkflowTimeouts(project);
1143
- new PrettierIgnoreGenerated(project);
1144
-
1145
- new GeneratedSource(project);
1146
- // The `bump` task (compute next version + commit + tag + push) is useful on
1147
- // any root; the actual publish is a tag-triggered GitHub workflow the caller
1148
- // authors. Independent of projen's own `release` component.
1149
- new DBXToolsRelease(project as DBXToolsNodeProject, {
1150
- tagPrefix: options.releaseTagPrefix,
1151
- standaloneReleases: options.standaloneReleases,
1152
- });
1153
- }
1154
-
1155
- /**
1156
- * A child's `.gitignore`, tracking whether any pattern was ever added so an
1157
- * untouched (empty) file can be dropped at presynth. `exclude`/`include` and
1158
- * constructor `ignorePatterns` all funnel through {@link addPatterns}, so the flag
1159
- * sees every route - but seed patterns must be added AFTER construction (see
1160
- * {@link swapChildGitignore}) because class fields initialize after `super()`.
1161
- */
1162
- class ChildGitignore extends IgnoreFile {
1163
- /** True once any pattern landed (custom patterns => the file is emitted). */
1164
- public hasPatterns = false;
1165
-
1166
- public override addPatterns(...patterns: string[]): void {
1167
- if (patterns.length) this.hasPatterns = true;
1168
- super.addPatterns(...patterns);
1169
- }
1170
- }
1171
-
1172
- /**
1173
- * Swap a CHILD's default `.gitignore` - pre-populated by `NodeProject` with the
1174
- * same defaults the root already carries (git applies the root's file to the whole
1175
- * tree) - for a FRESH {@link ChildGitignore}. Caller-supplied patterns
1176
- * (`gitignore` / `gitIgnoreOptions.ignorePatterns`) are re-seeded, and later
1177
- * `project.gitignore.addPatterns(...)` calls (tag/user mixins) land here too, so a
1178
- * package CAN carry package-specific ignores without inheriting the root noise.
1179
- * Left empty, the file is dropped by {@link preSynthesizeProject}. Safe because
1180
- * projen only writes gitignore defaults at construction time (`addDefaultGitIgnore`,
1181
- * yarn-berry config), never during synth.
1182
- */
1183
- function swapChildGitignore(
1184
- project: javascript.NodeProject,
1185
- options: DBXToolsProjectOptions,
1186
- ): void {
1187
- project.tryRemoveFile(".gitignore");
1188
- const fresh = new ChildGitignore(project, ".gitignore", {
1189
- ...options.gitIgnoreOptions,
1190
- // Re-added below so the custom-pattern flag sees them (not clobbered by the
1191
- // subclass field initializer running after super()).
1192
- ignorePatterns: undefined,
1193
- });
1194
- const seeds = [...(options.gitignore ?? []), ...(options.gitIgnoreOptions?.ignorePatterns ?? [])];
1195
- if (seeds.length) fresh.addPatterns(...seeds);
1196
- // `Project.gitignore` is readonly only at compile time; rebind it so every
1197
- // subsequent `project.gitignore.*` call reaches the fresh file.
1198
- (project as { gitignore: IgnoreFile }).gitignore = fresh;
1199
- }
1200
-
1201
- function preSynthesizeProject(project: javascript.NodeProject): void {
1202
- // `Project.files` is OWN-project only (its `components` getter filters on the
1203
- // project's own node path), so reaching a child's files means walking the tree.
1204
- // `node.findAll()` is projen/constructs' native preorder walk - self first, then
1205
- // descendants - which is the order the subproject recursion produced.
1206
- const subtree = project.node.findAll().filter(Project.isProject);
1207
- if (project.prettier) {
1208
- const ignorePatterns = new Set<string>();
1209
- for (const p of subtree) {
1210
- p.files.forEach((file) => {
1211
- if (file.readonly) ignorePatterns.add(file.path);
1212
- });
1213
- }
1214
- ignorePatterns.forEach((pattern) => project.prettier!.addIgnorePattern(pattern));
1215
- }
1216
- for (const p of subtree) {
1217
- if (!p.parent) continue;
1218
- // Swap the source entry points for compiled ones in the PUBLISHED manifest
1219
- // only. Runs here rather than in the constructor so the tags have already
1220
- // installed their `exports` layouts for it to mirror.
1221
- if (p instanceof javascript.NodeProject) applyCompiledPublish(p);
1222
- // A child's `.gitignore` survives ONLY when it carries custom patterns (see
1223
- // swapChildGitignore). `.gitattributes` is always dropped - the root's
1224
- // annotateGenerated globs cover the children. Runs once from the root's
1225
- // preSynthesize and again from each child's own; both passes agree, so the
1226
- // second is a no-op.
1227
- const keepGitignore = p.gitignore instanceof ChildGitignore && p.gitignore.hasPatterns;
1228
- for (const path of keepGitignore ? [".gitattributes"] : [".gitignore", ".gitattributes"]) {
1229
- if (p.tryRemoveFile(path)) {
1230
- const rootPath = resolve(p.outdir, path);
1231
- if (existsSync(rootPath)) {
1232
- console.log(`Removed ${rootPath} from ${p.name}`);
1233
- }
1234
- }
1235
- }
1236
- }
1237
- }
1238
-
1239
- /**
1240
- * Filters selecting which projects an {@link applyToProjects} call runs its
1241
- * callback(s) on. All provided filters are AND-ed; every string value is a glob
1242
- * (or list of globs) matched by the corresponding {@link projectPredicate}
1243
- * helper - prefix a glob with `!` to negate it. Omitted filters impose no
1244
- * constraint.
1245
- *
1246
- * By default the selection is DBXTools CHILD projects, so the callback receives
1247
- * the richer {@link DBXToolsProject} type; `includeNonDBXToolsProjects` and
1248
- * `includeRoots` widen it.
1249
- */
29
+ /** Filters selecting which projects an {@link applyToProjects} call runs against. */
1250
30
  export interface ApplyToProjectsOptions {
1251
- /**
1252
- * Include non-DBXTools projects (plain projen `Project`s) in the selection.
1253
- * Defaults to `false` - only {@link DBXToolsProject}s match, so the callback
1254
- * receives the richer type.
1255
- */
31
+ /** Include plain projen projects. Defaults to DBXTools projects only. */
1256
32
  includeNonDBXToolsProjects?: boolean;
1257
- /** Include tree ROOT projects (those with no parent). Defaults to `false` (children only). */
33
+ /** Include tree roots. Defaults to child projects only. */
1258
34
  includeRoots?: boolean;
1259
- /** Match the raw projen {@link Project.name} verbatim ({@link projectPredicate.hasName}). */
35
+ /** Match the raw projen project name. */
1260
36
  name?: PathMatchInput | OneOrMany<PathMatchInput>;
1261
- /** Match the parsed full npm name `@scope/name` ({@link projectPredicate.hasIdentifierPackageName}). */
37
+ /** Match the parsed full package name. */
1262
38
  identifierPackageName?: PathMatchInput | OneOrMany<PathMatchInput>;
1263
- /** Match the parsed npm scope ({@link projectPredicate.hasIdentifierScope}). */
39
+ /** Match the parsed package scope. */
1264
40
  identifierScope?: PathMatchInput | OneOrMany<PathMatchInput>;
1265
- /** Match the parsed unscoped name ({@link projectPredicate.hasIdentifierName}). */
41
+ /** Match the parsed unscoped package name. */
1266
42
  identifierName?: PathMatchInput | OneOrMany<PathMatchInput>;
1267
- /** Match every listed tag on `dbxToolsConfig.tags` ({@link projectPredicate.hasTag}). */
43
+ /** Match every listed dbx-tools tag. */
1268
44
  tags?: PathMatchInput | OneOrMany<PathMatchInput>;
1269
- /** Match the folder path relative to the tree root ({@link projectPredicate.hasPath}). */
45
+ /** Match the path relative to the tree root. */
1270
46
  path?: PathMatchInput | OneOrMany<PathMatchInput>;
1271
47
  }
1272
48
 
1273
- /** {@link ApplyToProjectsOptions} for the default DBXTools-only selection (callback gets {@link DBXToolsProject}). */
1274
49
  type ApplyToDBXToolsProjectsOptions = Omit<ApplyToProjectsOptions, "includeNonDBXToolsProjects"> & {
1275
50
  includeNonDBXToolsProjects?: false;
1276
51
  };
1277
52
 
1278
- /** {@link ApplyToProjectsOptions} opting into all projen projects (callback gets the base {@link Project}). */
1279
53
  type ApplyToAllProjectsOptions = Omit<ApplyToProjectsOptions, "includeNonDBXToolsProjects"> & {
1280
54
  includeNonDBXToolsProjects: true;
1281
55
  };
1282
56
 
1283
- /**
1284
- * Run one or more callbacks against every project in `construct`'s subtree that
1285
- * matches the given {@link ApplyToProjectsOptions} filters - the ergonomic
1286
- * front-end to authoring a {@link mixin} by hand. Internally builds one AND-ed
1287
- * predicate from the options and applies it via `construct.with(...)`.
1288
- *
1289
- * Call with just callback(s) to match every DBXTools child project, or pass an
1290
- * options object first to narrow by name/scope/tag/path. With
1291
- * `includeNonDBXToolsProjects: true` the callbacks receive the base
1292
- * {@link Project}; otherwise they receive the narrowed {@link DBXToolsProject}.
1293
- *
1294
- * @example
1295
- * // Add a dep to one package selected by unscoped name + tag:
1296
- * applyToProjects(root, { identifierName: "ui-mastra", tags: "ui" }, (p) => {
1297
- * p.addDeps("echarts@catalog:");
1298
- * });
1299
- * @example
1300
- * // Every child package except shared-core (negated glob):
1301
- * applyToProjects(root, { path: "packages/**", identifierName: "!shared-core" }, (p) => {
1302
- * p.addDeps("@dbx-tools/shared-core@workspace:*");
1303
- * });
1304
- */
57
+ /** Apply callbacks to projects matching all supplied filters. */
1305
58
  export function applyToProjects(
1306
59
  construct: IConstruct,
1307
60
  ...args:
1308
- | [ApplyToDBXToolsProjectsOptions, ...OneOrMany<(project: DBXToolsProject) => void>]
1309
- | OneOrMany<(project: DBXToolsProject) => void>
61
+ | [ApplyToDBXToolsProjectsOptions, ...OneOrMany<(project: DBXToolsJavaScriptProject) => void>]
62
+ | OneOrMany<(project: DBXToolsJavaScriptProject) => void>
1310
63
  ): void;
1311
64
 
1312
65
  export function applyToProjects(
@@ -1314,6 +67,12 @@ export function applyToProjects(
1314
67
  ...args: [ApplyToAllProjectsOptions, ...OneOrMany<(project: Project) => void>]
1315
68
  ): void;
1316
69
 
70
+ export function applyToProjects<P extends Project>(
71
+ construct: IConstruct,
72
+ ...args:
73
+ [ApplyToProjectsOptions, ...OneOrMany<(project: P) => void>] | OneOrMany<(project: P) => void>
74
+ ): void;
75
+
1317
76
  export function applyToProjects<P extends Project>(
1318
77
  construct: IConstruct,
1319
78
  ...args:
@@ -1323,31 +82,39 @@ export function applyToProjects<P extends Project>(
1323
82
  const hasOptions = typeof first !== "function";
1324
83
  const options = hasOptions ? (first as ApplyToProjectsOptions) : undefined;
1325
84
  const callbacks = (hasOptions ? rest : args) as OneOrMany<(project: Project) => void>;
1326
- let pred = projectPredicate.isProject();
1327
- if (!options?.includeNonDBXToolsProjects) pred = pred.and(projectPredicate.isDBXToolsProject());
1328
- if (!options?.includeRoots) pred = pred.and((p) => p.parent != null);
85
+ let predicate = projectPredicate.isProject();
86
+ if (!options?.includeNonDBXToolsProjects) {
87
+ predicate = predicate.and(projectPredicate.isDBXToolsJavaScriptProject());
88
+ }
89
+ if (!options?.includeRoots) predicate = predicate.and((project) => project.parent != null);
1329
90
  if (options?.identifierPackageName) {
1330
- pred = pred.and(
91
+ predicate = predicate.and(
1331
92
  projectPredicate.hasIdentifierPackageName(
1332
93
  ...object.toOneOrMany(options.identifierPackageName),
1333
94
  ),
1334
95
  );
1335
96
  }
1336
- if (options?.name) pred = pred.and(projectPredicate.hasName(...object.toOneOrMany(options.name)));
97
+ if (options?.name) {
98
+ predicate = predicate.and(projectPredicate.hasName(...object.toOneOrMany(options.name)));
99
+ }
1337
100
  if (options?.identifierScope) {
1338
- pred = pred.and(
101
+ predicate = predicate.and(
1339
102
  projectPredicate.hasIdentifierScope(...object.toOneOrMany(options.identifierScope)),
1340
103
  );
1341
104
  }
1342
105
  if (options?.identifierName) {
1343
- pred = pred.and(
106
+ predicate = predicate.and(
1344
107
  projectPredicate.hasIdentifierName(...object.toOneOrMany(options.identifierName)),
1345
108
  );
1346
109
  }
1347
- if (options?.tags) pred = pred.and(projectPredicate.hasTag(...object.toOneOrMany(options.tags)));
1348
- if (options?.path) pred = pred.and(projectPredicate.hasPath(...object.toOneOrMany(options.path)));
1349
- const projectMixin = mixin.create(pred, (p) => {
1350
- callbacks.forEach((callback) => callback(p as Project));
110
+ if (options?.tags) {
111
+ predicate = predicate.and(projectPredicate.hasTag(...object.toOneOrMany(options.tags)));
112
+ }
113
+ if (options?.path) {
114
+ predicate = predicate.and(projectPredicate.hasPath(...object.toOneOrMany(options.path)));
115
+ }
116
+ const projectMixin = mixin.create(predicate, (project) => {
117
+ callbacks.forEach((callback) => callback(project as Project));
1351
118
  });
1352
119
  construct.with(projectMixin);
1353
120
  }