@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/README.md +38 -0
- package/index.ts +8 -21
- package/package.json +4 -4
- package/src/pnpm-workspace.ts +1 -0
- package/src/project-js.ts +1246 -0
- package/src/project-predicate.ts +17 -6
- package/src/project-py.ts +401 -0
- package/src/project.ts +57 -1290
- package/src/publish.ts +2 -2
- package/src/vscode.ts +3 -3
package/src/project.ts
CHANGED
|
@@ -1,1312 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
/**
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
/**
|
|
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
|
|
33
|
+
/** Include tree roots. Defaults to child projects only. */
|
|
1258
34
|
includeRoots?: boolean;
|
|
1259
|
-
/** Match the raw projen
|
|
35
|
+
/** Match the raw projen project name. */
|
|
1260
36
|
name?: PathMatchInput | OneOrMany<PathMatchInput>;
|
|
1261
|
-
/** Match the parsed full
|
|
37
|
+
/** Match the parsed full package name. */
|
|
1262
38
|
identifierPackageName?: PathMatchInput | OneOrMany<PathMatchInput>;
|
|
1263
|
-
/** Match the parsed
|
|
39
|
+
/** Match the parsed package scope. */
|
|
1264
40
|
identifierScope?: PathMatchInput | OneOrMany<PathMatchInput>;
|
|
1265
|
-
/** Match the parsed unscoped name
|
|
41
|
+
/** Match the parsed unscoped package name. */
|
|
1266
42
|
identifierName?: PathMatchInput | OneOrMany<PathMatchInput>;
|
|
1267
|
-
/** Match every listed tag
|
|
43
|
+
/** Match every listed dbx-tools tag. */
|
|
1268
44
|
tags?: PathMatchInput | OneOrMany<PathMatchInput>;
|
|
1269
|
-
/** Match the
|
|
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:
|
|
1309
|
-
| OneOrMany<(project:
|
|
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
|
|
1327
|
-
if (!options?.includeNonDBXToolsProjects)
|
|
1328
|
-
|
|
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
|
-
|
|
91
|
+
predicate = predicate.and(
|
|
1331
92
|
projectPredicate.hasIdentifierPackageName(
|
|
1332
93
|
...object.toOneOrMany(options.identifierPackageName),
|
|
1333
94
|
),
|
|
1334
95
|
);
|
|
1335
96
|
}
|
|
1336
|
-
if (options?.name)
|
|
97
|
+
if (options?.name) {
|
|
98
|
+
predicate = predicate.and(projectPredicate.hasName(...object.toOneOrMany(options.name)));
|
|
99
|
+
}
|
|
1337
100
|
if (options?.identifierScope) {
|
|
1338
|
-
|
|
101
|
+
predicate = predicate.and(
|
|
1339
102
|
projectPredicate.hasIdentifierScope(...object.toOneOrMany(options.identifierScope)),
|
|
1340
103
|
);
|
|
1341
104
|
}
|
|
1342
105
|
if (options?.identifierName) {
|
|
1343
|
-
|
|
106
|
+
predicate = predicate.and(
|
|
1344
107
|
projectPredicate.hasIdentifierName(...object.toOneOrMany(options.identifierName)),
|
|
1345
108
|
);
|
|
1346
109
|
}
|
|
1347
|
-
if (options?.tags)
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
}
|