@dbx-tools/projen 0.1.1 → 0.3.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/release.ts ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Release wiring: registers the `bump` task on a project (compute next version +
3
+ * commit + tag + push), parameterized by the project's git tag prefix, and - when
4
+ * the project has a GitHub component - authors the tag-driven npm publish
5
+ * workflow that the pushed tag triggers.
6
+ */
7
+ import { Component } from "projen";
8
+ import { GithubWorkflow } from "projen/lib/github";
9
+ import { JobPermission } from "projen/lib/github/workflows-model";
10
+ import { applyTasks, taskScript, type DBXToolsNodeProject } from "./project";
11
+
12
+ /**
13
+ * A standalone project that lives in a repo SUBDIRECTORY but is NOT a member of
14
+ * this pnpm workspace (e.g. the `@dbx-tools/projen` engine in `projen/`), yet
15
+ * still needs a release workflow. GitHub Actions only runs workflows from the
16
+ * REPO-ROOT `.github/`, so the workflow for such a project is authored here,
17
+ * alongside the root's own `release` workflow, under a distinct name + tag
18
+ * prefix so the two never collide. Tag-driven and pack-based: push
19
+ * `<tagPrefix>1.2.3` and the single package in `directory` is published at 1.2.3
20
+ * via `npm pack` + `npm publish`.
21
+ *
22
+ * Declaring one also enlists it in the root's `bump`, which cuts BOTH tags at one
23
+ * shared version. The separate tag namespace still lets it be released alone
24
+ * (`cd <directory> && pnpm run bump`) for a consumer who wants only this package -
25
+ * but a routine root bump no longer leaves it behind, which is how the engine
26
+ * drifted to 0.1.24 while the packages reached 0.3.41.
27
+ */
28
+ export interface StandaloneRelease {
29
+ /** Workflow name (and `.github/workflows/<name>.yml` file). E.g. `projen-release`. */
30
+ readonly name: string;
31
+ /** Repo-relative directory of the standalone project. E.g. `projen`. */
32
+ readonly directory: string;
33
+ /**
34
+ * Git tag prefix that triggers this release, disjoint from the root's `v*`
35
+ * (e.g. `projen-v`). The pushed tag IS the published version.
36
+ */
37
+ readonly tagPrefix: string;
38
+ }
39
+
40
+ /** Options for {@link DBXToolsRelease}. */
41
+ export interface DBXToolsReleaseOptions {
42
+ /**
43
+ * Git tag prefix for this project's releases (e.g. `v` or `projen-v`). The
44
+ * `bump` task reads/writes `<prefix><version>` tags, keeping sibling projects
45
+ * in the same repo on disjoint tag namespaces. Defaults to `v`.
46
+ */
47
+ readonly tagPrefix?: string;
48
+ /**
49
+ * Standalone in-repo projects (NOT workspace members) that each get their own
50
+ * tag-driven release workflow authored alongside the root's - see
51
+ * {@link StandaloneRelease}. Authored only when the project has a GitHub
52
+ * component. Defaults to none.
53
+ */
54
+ readonly standaloneReleases?: readonly StandaloneRelease[];
55
+ }
56
+
57
+ /**
58
+ * Adds a `bump` task: compute the next release version (from the higher of the
59
+ * latest `<prefix>*` git tag and the local `package.json`), then commit, tag,
60
+ * and push it - pushing the tag is what triggers the release workflow. Each step
61
+ * is toggleable (`--no-version` / `--no-commit` / `--no-tag` / `--no-push` /
62
+ * `--no-publish`); see `tasks/bump.ts`.
63
+ *
64
+ * When the project has a GitHub component (`github: true`), also authors the
65
+ * `release` workflow: on a pushed `<prefix>*` tag it sets that version on every
66
+ * publishable package and runs `pnpm -r publish` (which skips
67
+ * `private` packages and honors each package's `publishConfig`).
68
+ *
69
+ * Provenance is opt-in. Each package's generated `publishConfig` omits
70
+ * `provenance`, so LOCAL publishes (e.g. to a verdaccio) never try to attest -
71
+ * npm has no CI OIDC provider off-CI and would fail with `provider: null`. This
72
+ * CI workflow turns it on with `npm_config_provenance=true`, backed by the
73
+ * `id-token: write` permission that lets npm mint the OIDC token.
74
+ *
75
+ * Registry AUTH is still `NPM_TOKEN`, deliberately. OIDC here mints the
76
+ * provenance attestation only; full npm trusted publishing would additionally
77
+ * replace the token, but it has to be registered per package on npmjs.com
78
+ * against an exact repo + workflow filename, and this repo publishes 26 of
79
+ * them. That is a considered deferral, not an oversight - do not "fix" it by
80
+ * dropping the token without doing the registrations first, or every publish
81
+ * fails.
82
+ */
83
+ export class DBXToolsRelease extends Component {
84
+ private readonly tagPrefix: string;
85
+ private readonly standaloneReleases: readonly StandaloneRelease[];
86
+
87
+ constructor(project: DBXToolsNodeProject, options: DBXToolsReleaseOptions = {}) {
88
+ super(project);
89
+ this.tagPrefix = options.tagPrefix ?? "v";
90
+ this.standaloneReleases = options.standaloneReleases ?? [];
91
+ }
92
+
93
+ public override preSynthesize(): void {
94
+ const project = this.project as DBXToolsNodeProject;
95
+ // Release the standalone projects in the SAME run, at the same version. They
96
+ // are not workspace members, so nothing else would ever bring them along.
97
+ const siblingArgs = this.standaloneReleases
98
+ .map(({ directory, tagPrefix }) => ` --sibling ${directory}:${tagPrefix}`)
99
+ .join("");
100
+ applyTasks(project, {
101
+ bump: {
102
+ exec: taskScript(project, "bump.ts", `--prefix ${this.tagPrefix}${siblingArgs}`),
103
+ receiveArgs: true,
104
+ description: "Bump the release version (default patch), then commit, tag, and push it",
105
+ },
106
+ });
107
+
108
+ // Author the tag-driven publish workflows only when GitHub is enabled - they
109
+ // live in `.github/`, which requires projen's GitHub component.
110
+ if (project.github) {
111
+ this.authorReleaseWorkflow(project);
112
+ for (const standalone of this.standaloneReleases) {
113
+ this.authorStandaloneReleaseWorkflow(project, standalone);
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Emit the `release` GitHub workflow: push `<prefix>1.2.3` and every
120
+ * publishable package is published to npm at 1.2.3. Setting the
121
+ * version on every package first makes the pushed tag the published version
122
+ * (no bump math).
123
+ */
124
+ private authorReleaseWorkflow(project: DBXToolsNodeProject): void {
125
+ const workflow = new GithubWorkflow(project.github!, "release", {
126
+ // Serialize publishes so two tags landing together cannot race to the
127
+ // registry, but never cancel a run already in flight: a half-published
128
+ // release is worse than a queued one.
129
+ limitConcurrency: true,
130
+ concurrencyOptions: { group: "release", cancelInProgress: false },
131
+ });
132
+ // Read-only floor for any job that does not declare its own permissions;
133
+ // the publish job below overrides it with the `id-token` it needs.
134
+ workflow.file?.addOverride("permissions", { contents: "read" });
135
+ workflow.on({ push: { tags: [`${this.tagPrefix}*`] } });
136
+ workflow.addJob("publish", {
137
+ runsOn: ["ubuntu-latest"],
138
+ // `id-token: write` lets npm mint the OIDC token for provenance attestation.
139
+ permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
140
+ timeoutMinutes: 30,
141
+ env: { CI: "true" },
142
+ steps: [
143
+ { name: "Checkout", uses: "actions/checkout@v6", with: { "fetch-depth": 0 } },
144
+ { name: "Setup pnpm", uses: "pnpm/action-setup@v5", with: { version: "10.33.0" } },
145
+ {
146
+ name: "Setup Node.js",
147
+ uses: "actions/setup-node@v6",
148
+ with: { "node-version": "lts/*", "registry-url": "https://registry.npmjs.org" },
149
+ },
150
+ // NOT frozen. The lockfile is gitignored by policy (it can carry a
151
+ // private-registry fingerprint), so CI often has none or a stale one -
152
+ // and `--frozen-lockfile` turns that into a hard release failure rather
153
+ // than resolving. A blocked publish is worse here than a resolved one.
154
+ { name: "Install", run: "pnpm install --no-frozen-lockfile" },
155
+ // The pushed tag is the version: `<prefix>1.2.3` -> `1.2.3`. Set it on
156
+ // every package (manifests are projen-readonly, so unlock them first).
157
+ {
158
+ name: "Set version from tag",
159
+ run: [
160
+ `VERSION="\${GITHUB_REF_NAME#${this.tagPrefix}}"`,
161
+ "chmod -R u+w . || true",
162
+ 'pnpm -r exec npm version "$VERSION" --no-git-tag-version --allow-same-version',
163
+ ].join("\n"),
164
+ },
165
+ {
166
+ name: "Publish to npm",
167
+ // `pnpm -r publish` publishes every non-private package,
168
+ // rewriting `workspace:*` deps to the published version. Provenance is
169
+ // opt-in (omitted from each package's `publishConfig` so local
170
+ // publishes work); CI turns it on here via `npm_config_provenance`.
171
+ run: "pnpm -r publish --no-git-checks --access public",
172
+ env: {
173
+ NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}",
174
+ npm_config_provenance: "true",
175
+ },
176
+ },
177
+ ],
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Emit a {@link StandaloneRelease}'s workflow: push `<prefix>1.2.3` and the
183
+ * single package in `directory` (a non-workspace-member project) is published
184
+ * at 1.2.3 via `npm pack` + `npm publish`. Its `package.json` is
185
+ * projen-generated read-only, so it is unlocked before `npm version` rewrites
186
+ * it. No bump math - the pushed tag IS the published version.
187
+ */
188
+ private authorStandaloneReleaseWorkflow(
189
+ project: DBXToolsNodeProject,
190
+ { name, directory, tagPrefix }: StandaloneRelease,
191
+ ): void {
192
+ const workflow = new GithubWorkflow(project.github!, name, {
193
+ limitConcurrency: true,
194
+ concurrencyOptions: { group: name, cancelInProgress: false },
195
+ });
196
+ workflow.file?.addOverride("permissions", { contents: "read" });
197
+ workflow.on({ push: { tags: [`${tagPrefix}*`] } });
198
+ workflow.addJob("publish", {
199
+ runsOn: ["ubuntu-latest"],
200
+ // `id-token: write` lets npm mint the OIDC token for provenance attestation.
201
+ permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
202
+ timeoutMinutes: 30,
203
+ defaults: { run: { workingDirectory: directory } },
204
+ env: { CI: "true" },
205
+ steps: [
206
+ { name: "Checkout", uses: "actions/checkout@v6", with: { "fetch-depth": 0 } },
207
+ { name: "Setup pnpm", uses: "pnpm/action-setup@v5", with: { version: "10.33.0" } },
208
+ {
209
+ name: "Setup Node.js",
210
+ uses: "actions/setup-node@v6",
211
+ with: { "node-version": "lts/*", "registry-url": "https://registry.npmjs.org" },
212
+ },
213
+ // NOT frozen. The lockfile is gitignored by policy (it can carry a
214
+ // private-registry fingerprint), so CI often has none or a stale one -
215
+ // and `--frozen-lockfile` turns that into a hard release failure rather
216
+ // than resolving. A blocked publish is worse here than a resolved one.
217
+ { name: "Install", run: "pnpm install --no-frozen-lockfile" },
218
+ // The pushed tag is the version: `<prefix>1.2.3` -> `1.2.3`. package.json
219
+ // is projen-generated read-only, so unlock it before `npm version` writes.
220
+ {
221
+ name: "Set version from tag",
222
+ run: [
223
+ "chmod u+w package.json",
224
+ `npm version "\${GITHUB_REF_NAME#${tagPrefix}}" --no-git-tag-version --allow-same-version`,
225
+ ].join("\n"),
226
+ },
227
+ { name: "Pack", run: "pnpm pack --pack-destination dist/js" },
228
+ {
229
+ name: "Publish to npm",
230
+ run: "npm publish dist/js/*.tgz --access public",
231
+ env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" },
232
+ },
233
+ ],
234
+ });
235
+ }
236
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Runs a projen re-synth, for the `sync` task and its watchers.
3
+ */
4
+ import { join } from "node:path";
5
+ import { exec } from "@dbx-tools/core";
6
+ import { repoRoot } from "./packages";
7
+
8
+ /**
9
+ * Re-run projen synth by executing `.projenrc.ts` with `node --import tsx` (no
10
+ * projen network re-exec).
11
+ *
12
+ * `post: true` runs the full flow - projen's post-synth `pnpm install` AND the
13
+ * post-synth barrels component - which is what the one-shot `sync` task
14
+ * wants. The default (`post: false`) sets `PROJEN_DISABLE_POST`, skipping both so
15
+ * the watch loop stays fast; there the caller rebuilds barrels explicitly.
16
+ *
17
+ * Deliberately never forces `CI: "true"` here: besides pnpm's own no-TTY prompt,
18
+ * `CI` also makes pnpm choose a `--frozen-lockfile` install for a MULTI-package
19
+ * workspace's subprojects, which is the wrong tradeoff for routine re-synths (a
20
+ * newly added/edited package's lockfile entry is expected to be behind). A caller
21
+ * that needs the no-TTY prompt answered non-interactively (the `dbx-tools` CLI, when
22
+ * bootstrapping an empty folder) runs with `post: false` and does its own install
23
+ * afterward instead.
24
+ */
25
+ export function runSynth(options: { post?: boolean } = {}): void {
26
+ const env = { ...process.env };
27
+ if (options.post) delete env.PROJEN_DISABLE_POST;
28
+ else env.PROJEN_DISABLE_POST = "true";
29
+ exec.spawnSync(process.execPath, ["--import", "tsx", join(repoRoot, ".projenrc.ts")], {
30
+ cwd: repoRoot,
31
+ env,
32
+ check: true,
33
+ });
34
+ }
package/src/tags.ts ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Tags, expressed as MIXINS (`constructs` `IMixin`).
3
+ *
4
+ * A tag names a target environment (React/Vite, Node, agnostic, ...) - modeled on
5
+ * `databricks apps init` (AppKit): `ui`, `server`, `shared`. Any `src`-bearing folder
6
+ * under a package root is discovered automatically; path-derived tag
7
+ * candidates plus `packageTagPaths` decide which mixins apply. ("Scope" is
8
+ * reserved for the npm `@scope/` in package names.)
9
+ *
10
+ * Mixin factories live in {@link ./mixin}; package predicates live in {@link ./project}
11
+ * ({@link projectPredicate.hasTag}).
12
+ * The per-tag table is {@link PACKAGE_TAG_MIXINS}. Apply with the constructs-native `project.with(...)`
13
+ * across the subtree; the root applies built-in tag mixins during construction and
14
+ * callers add their own afterward.
15
+ */
16
+ import type { IMixin as ConstructsMixin } from "constructs";
17
+ import { DependencyType, javascript } from "projen";
18
+ import { addCliBinLaunchers } from "./cli-bin";
19
+ import { create } from "./mixin";
20
+ import {
21
+ addPackageFiles,
22
+ applyCompilerOptions,
23
+ applyExports,
24
+ applyIncludes,
25
+ applyTasks,
26
+ srcModuleExports,
27
+ } from "./project";
28
+ import * as projectPredicate from "./project-predicate";
29
+ import { ViteConfigFile } from "./vite";
30
+
31
+ /** Node compiler options: ES2022 lib + node types, deliberately no DOM. */
32
+ const NODE_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions = {
33
+ target: "ES2022",
34
+ lib: ["ES2022"],
35
+ types: ["node"],
36
+ };
37
+
38
+ /** The DOM-capable lib list shared by the browser tags (`ui`, `openapi`). */
39
+ const DOM_LIB = ["ES2022", "DOM", "DOM.Iterable"];
40
+
41
+ /**
42
+ * The agnostic floor every package gets at construction: ES2022 stdlib plus the
43
+ * web-platform globals available in every JS runtime (browser, workers, Node 18+)
44
+ * via the `WebWorker` lib - `AbortController`/`AbortSignal`, `URL`, `crypto`, the
45
+ * timer functions, `fetch`, `TextEncoder`, etc. Deliberately NO `DOM` lib (no
46
+ * `document`/`window`) and no node types, so agnostic code stays isomorphic. Also
47
+ * the whole config the `shared` tag applies.
48
+ */
49
+ export const AGNOSTIC_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions = {
50
+ target: "ES2022",
51
+ lib: ["ES2022", "WebWorker"],
52
+ types: [],
53
+ };
54
+
55
+ /**
56
+ * The tag table, as mixins. Each entry configures every package carrying
57
+ * that tag (deps + tsconfig + tasks) when applied via `project.with(...)`. The keys
58
+ * are the known tag names; a package carrying a given tag receives its mixin when
59
+ * that tag appears in `dbxToolsConfig.tags`. Select which apply with the `defaultTagMixins` option (`false` = none,
60
+ * or a subset list; unselected packages fall back to {@link AGNOSTIC_COMPILER_OPTIONS}).
61
+ */
62
+ export const PACKAGE_TAG_MIXINS = {
63
+ // `ui`: a React COMPONENT LIBRARY (source-first, consumed by apps) - modeled
64
+ // on `@databricks/appkit-ui`. React + DOM lib + JSX, and the default `tsc`
65
+ // compile (typecheck). No vite app build / index.html: a full browser app is an
66
+ // `app`-tagged package (see below) that layers vite on top.
67
+ ui: create(projectPredicate.hasTag("ui"), (p) => {
68
+ p.addDeps("react@catalog:", "react-dom@catalog:");
69
+ p.addDevDeps("@types/react@catalog:", "@types/react-dom@catalog:");
70
+ applyCompilerOptions(p, {
71
+ target: "ES2022",
72
+ lib: [...DOM_LIB],
73
+ jsx: javascript.TypeScriptJsxMode.REACT_JSX,
74
+ });
75
+ // A component library's standard subpath surface: `./react` (components),
76
+ // `./styles.css` (Tailwind entry), and `./package.json`. A package that
77
+ // ships more (e.g. ui-appkit's `./vite` preset) overrides this in its own
78
+ // mixin; an `app`-tagged package replaces it with a `.` root (see below).
79
+ applyExports(p, {
80
+ "./react": "./src/react/index.ts",
81
+ "./styles.css": "./src/styles.css",
82
+ "./package.json": "./package.json",
83
+ });
84
+ }),
85
+ // `app`: a full browser app built + served by Vite (needs an `index.html`
86
+ // entry). Self-contained React app: React + DOM lib + JSX + the vite toolchain
87
+ // and app tasks (`dev`/`build`/`preview`). `build` resets the compile task, so
88
+ // `compile` bundles with vite rather than `tsc`.
89
+ app: create(projectPredicate.hasTag("app"), (p) => {
90
+ p.addDeps("react@catalog:", "react-dom@catalog:");
91
+ p.addDevDeps(
92
+ "vite@catalog:",
93
+ "@vitejs/plugin-react@catalog:",
94
+ "@types/react@catalog:",
95
+ "@types/react-dom@catalog:",
96
+ );
97
+ applyCompilerOptions(p, {
98
+ target: "ES2022",
99
+ lib: [...DOM_LIB],
100
+ jsx: javascript.TypeScriptJsxMode.REACT_JSX,
101
+ types: ["vite/client"],
102
+ });
103
+ applyTasks(p, {
104
+ dev: { exec: "vite" },
105
+ build: { exec: "vite build" },
106
+ preview: { exec: "vite preview" },
107
+ });
108
+ new ViteConfigFile(p);
109
+ // An app has a single root entry, not a component library's subpaths - so it
110
+ // replaces the `ui` tag's `./react`/`./styles.css` surface with a `.` root.
111
+ applyExports(p, {
112
+ ".": "./index.ts",
113
+ "./package.json": "./package.json",
114
+ });
115
+ }),
116
+ cli: create(projectPredicate.hasTag("cli"), (p) => {
117
+ // tsx is a RUNTIME dep, not a dev one: a CLI's bin is a `.ts` entry that needs
118
+ // the tsx loader registered before it can run, and an installed consumer
119
+ // (`npm i -g`) has no other source for it. Drop the baseline devDep so it isn't
120
+ // declared in both blocks. The generated `.mjs` launchers are what actually
121
+ // reach it - see {@link addCliBinLaunchers}.
122
+ p.deps.removeDependency("tsx", DependencyType.BUILD);
123
+ p.addDeps("commander@catalog:", "@clack/prompts@catalog:", "tsx@catalog:");
124
+ p.addDevDeps("@types/node@catalog:");
125
+ addCliBinLaunchers(p);
126
+ // A CLI compiles code OUTSIDE `src/` - its root `index.ts` barrel and the
127
+ // `bin/` entries - which the src-only tag default doesn't reach, so the
128
+ // tsconfig is rooted at the package instead.
129
+ applyCompilerOptions(p, { ...NODE_COMPILER_OPTIONS, rootDir: "." });
130
+ applyIncludes(p, "index.ts", "bin/**/*.ts");
131
+ // A CLI's standard surface: the `.` root entry, a `./<module>` subpath per
132
+ // top-level `src` module, and `./package.json`. Derived rather than declared
133
+ // so no CLI has to hand-list its own modules.
134
+ applyExports(p, {
135
+ ".": "./index.ts",
136
+ ...srcModuleExports(p),
137
+ "./package.json": "./package.json",
138
+ });
139
+ // A CLI is the one shape whose entry point lives outside `src`, so its
140
+ // launchers have to be added to the tarball allowlist explicitly.
141
+ addPackageFiles(p, "bin");
142
+ }),
143
+ server: create(projectPredicate.hasTag("server"), (p) => {
144
+ // A Node/Express service. tsoa's decorators (@Route/@Get/...) also drive
145
+ // the `openapi` task (spec + client); experimentalDecorators lets them
146
+ // type-check. `dev`/`start` run the app's `src/server.ts` with tsx.
147
+ p.addDeps("express@catalog:", "tsoa@catalog:");
148
+ p.addDevDeps("@types/node@catalog:", "@types/express@catalog:");
149
+ applyCompilerOptions(p, {
150
+ ...NODE_COMPILER_OPTIONS,
151
+ experimentalDecorators: true,
152
+ });
153
+ applyTasks(p, {
154
+ dev: { exec: "tsx watch src/server.ts" },
155
+ start: { exec: "tsx src/server.ts" },
156
+ });
157
+ }),
158
+ node: create(projectPredicate.hasTag("node"), (p) => {
159
+ p.addDevDeps("@types/node@catalog:");
160
+ applyCompilerOptions(p, NODE_COMPILER_OPTIONS);
161
+ }),
162
+ shared: create(projectPredicate.hasTag("shared"), (p) => {
163
+ applyCompilerOptions(p, AGNOSTIC_COMPILER_OPTIONS);
164
+ }),
165
+ openapi: create(projectPredicate.hasTag("openapi"), (p) => {
166
+ p.addDeps("openapi-fetch@catalog:");
167
+ applyCompilerOptions(p, { target: "ES2022", lib: [...DOM_LIB], types: [] });
168
+ }),
169
+ } satisfies Record<string, ConstructsMixin>;
170
+
171
+ /** A known tag name (a key of {@link PACKAGE_TAG_MIXINS}). */
172
+ export type PackageTag = keyof typeof PACKAGE_TAG_MIXINS;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Root `tsconfig.base.json` and `tsconfig.json` for the projenrc program.
3
+ */
4
+ import { Component, JsonFile, type Project, javascript } from "projen";
5
+
6
+ /**
7
+ * Compiler options for the ROOT program only (`.projenrc.ts` + the engine as
8
+ * seen from the root). Each package now owns its own projen-generated
9
+ * `tsconfig.json` (scope `lib`/`jsx`/`types` overlaid on projen's defaults), so
10
+ * this base no longer feeds packages - it just gives the projenrc/editor program
11
+ * a sane ESM config. `lib` is set narrowly in the root `tsconfig.json`.
12
+ */
13
+ const BASE_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions = {
14
+ target: "ESNext",
15
+ module: "ESNext",
16
+ moduleResolution: javascript.TypeScriptModuleResolution.BUNDLER,
17
+ moduleDetection: javascript.TypeScriptModuleDetection.FORCE,
18
+ allowJs: true,
19
+ esModuleInterop: true,
20
+ resolveJsonModule: true,
21
+ isolatedModules: true,
22
+ allowImportingTsExtensions: true,
23
+ noEmit: true,
24
+ strict: true,
25
+ skipLibCheck: true,
26
+ forceConsistentCasingInFileNames: true,
27
+ noUncheckedIndexedAccess: true,
28
+ noImplicitOverride: true,
29
+ noFallthroughCasesInSwitch: true,
30
+ };
31
+
32
+ /**
33
+ * Emits `tsconfig.base.json` and `tsconfig.json` for the monorepo root program.
34
+ * Packages type-check against their own projen-generated tsconfigs via `compile`.
35
+ */
36
+ export class DBXToolsRootTsconfig extends Component {
37
+ constructor(scope: Project) {
38
+ super(scope);
39
+
40
+ new JsonFile(scope, "tsconfig.base.json", {
41
+ marker: true,
42
+ readonly: true,
43
+ obj: { compilerOptions: BASE_COMPILER_OPTIONS },
44
+ });
45
+
46
+ new JsonFile(scope, "tsconfig.json", {
47
+ marker: true,
48
+ readonly: true,
49
+ obj: {
50
+ extends: "./tsconfig.base.json",
51
+ compilerOptions: {
52
+ lib: ["ESNext"],
53
+ types: ["node"],
54
+ } satisfies javascript.TypeScriptCompilerOptions,
55
+ include: [".projenrc.ts"],
56
+ exclude: ["node_modules", "**/dist", "**/node_modules"],
57
+ },
58
+ });
59
+ }
60
+ }
package/src/vite.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * `vite.config.ts` as a first-class projen file component.
3
+ *
4
+ * {@link ViteConfigFile} extends projen's `TextFile` and emits a generated,
5
+ * read-only Vite config: the React plugin plus a runtime OVERRIDE chain. At Vite
6
+ * startup the generated config looks for each unmanaged override module sitting
7
+ * beside it (default {@link DEFAULT_VITE_OVERRIDES}: `vite.config.override.js`) and, when present, merges that module's default export
8
+ * over the generated config with Vite's `mergeConfig` - in listed order, so later
9
+ * files win and absent ones are skipped. A package thus tweaks Vite WITHOUT editing
10
+ * the projen-owned file.
11
+ *
12
+ * The override modules are `.js` because Vite loads them via a plain dynamic
13
+ * `import()` at config time. Being a package-ROOT file (not under `src/`), the
14
+ * generated `vite.config.ts` is excluded from the package's `tsconfig` `include`, so
15
+ * its `node:*` usage never trips the `ui` package's `compile` under the DOM-only
16
+ * tsconfig; Vite transpiles it with esbuild and runs it in Node at config time.
17
+ */
18
+ import { type Project, TextFile } from "projen";
19
+
20
+ /**
21
+ * Default unmanaged override modules, merged over the generated config in order
22
+ * (later wins, absent files skipped): a package's `vite.config.override.js`.
23
+ */
24
+ const DEFAULT_VITE_OVERRIDES = ["vite.config.override.js"];
25
+
26
+ /** Render the generated `vite.config.ts` source with the override chain inlined. */
27
+ function renderViteConfig(overridePaths: string[]): string {
28
+ const overrides = overridePaths.map((path) => ` ${JSON.stringify(path)},`).join("\n");
29
+ return String.raw`
30
+ import { existsSync } from "node:fs";
31
+ import react from "@vitejs/plugin-react";
32
+ import {
33
+ defineConfig,
34
+ mergeConfig,
35
+ type ConfigEnv,
36
+ type UserConfig,
37
+ type UserConfigExport,
38
+ } from "vite";
39
+
40
+ // Unmanaged override modules (relative to this file), merged over the generated
41
+ // config in order - later wins, absent files are skipped.
42
+ const OVERRIDE_FILES = [
43
+ ${overrides}
44
+ ];
45
+
46
+ async function resolveConfig(
47
+ config: UserConfigExport,
48
+ env: ConfigEnv,
49
+ ): Promise<UserConfig> {
50
+ if (typeof config === "function") {
51
+ return await config(env);
52
+ }
53
+ return await config;
54
+ }
55
+
56
+ export default defineConfig(async (configEnv: ConfigEnv) => {
57
+ let config: UserConfig = {
58
+ plugins: [react()],
59
+ };
60
+
61
+ for (const file of OVERRIDE_FILES) {
62
+ const overrideUrl = new URL(file, import.meta.url);
63
+ if (!existsSync(overrideUrl)) {
64
+ continue;
65
+ }
66
+ const overrideModule = await import(overrideUrl.href);
67
+ const override = await resolveConfig(
68
+ overrideModule.default as UserConfigExport,
69
+ configEnv,
70
+ );
71
+ config = mergeConfig(config, override);
72
+ }
73
+
74
+ return config;
75
+ });
76
+ `.trimStart();
77
+ }
78
+
79
+ /**
80
+ * A projen-owned, read-only `vite.config.ts` (React + the runtime override merge
81
+ * chain described in the module docstring).
82
+ */
83
+ export class ViteConfigFile extends TextFile {
84
+ constructor(project: Project) {
85
+ super(project, "vite.config.ts", {
86
+ marker: true,
87
+ readonly: true,
88
+ lines: renderViteConfig(DEFAULT_VITE_OVERRIDES).split("\n"),
89
+ });
90
+ }
91
+ }
package/src/vscode.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Root `.vscode/*` files: settings, extension recommendations, and tasks.
3
+ *
4
+ * Prettier is projen's built-in component (not emitted here). The auto-run watcher
5
+ * is delivered by `.vscode/tasks.json` (`runOn: folderOpen`) - projen has no native
6
+ * tasks.json component, so a `JsonFile` is the idiomatic emitter.
7
+ */
8
+ import { Component, JsonFile, type Project, vscode } from "projen";
9
+
10
+ /**
11
+ * Configures root `.vscode/settings.json`, `extensions.json`, and `tasks.json`.
12
+ * Only a tree ROOT constructs this component (see {@link DBXToolsNodeProject}).
13
+ */
14
+ export class DBXToolsVsCode extends Component {
15
+ /** projen's built-in VsCode component (settings + extension recommendations). */
16
+ readonly vsCode: vscode.VsCode;
17
+
18
+ constructor(scope: Project) {
19
+ super(scope);
20
+
21
+ this.vsCode = new vscode.VsCode(scope);
22
+ this.vsCode.settings.addSettings({
23
+ "typescript.tsdk": "node_modules/typescript/lib",
24
+ "typescript.preferences.importModuleSpecifier": "non-relative",
25
+ "javascript.preferences.importModuleSpecifier": "non-relative",
26
+ "editor.formatOnSave": true,
27
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
28
+ "files.watcherExclude": {
29
+ "**/node_modules/**": true,
30
+ "**/dist/**": true,
31
+ },
32
+ });
33
+ this.vsCode.extensions.addRecommendations("esbenp.prettier-vscode");
34
+ this.vsCode.settings.file.readonly = true;
35
+ this.vsCode.extensions.file.readonly = true;
36
+
37
+ new JsonFile(scope, ".vscode/tasks.json", {
38
+ marker: false,
39
+ readonly: true,
40
+ obj: {
41
+ version: "2.0.0",
42
+ tasks: [
43
+ {
44
+ label: "sync",
45
+ detail:
46
+ "projen sync --watch - projenrc (.projenrc.ts + syncResynthPaths re-synth) + barrels + openapi watchers",
47
+ type: "shell",
48
+ command: "pnpm exec projen sync --watch",
49
+ isBackground: true,
50
+ problemMatcher: [],
51
+ runOptions: { runOn: "folderOpen" },
52
+ presentation: {
53
+ reveal: "always",
54
+ panel: "dedicated",
55
+ group: "projen",
56
+ },
57
+ },
58
+ {
59
+ label: "synth",
60
+ detail: "projen - synthesize all generated config",
61
+ type: "shell",
62
+ command: "pnpm exec projen",
63
+ problemMatcher: [],
64
+ },
65
+ ],
66
+ },
67
+ });
68
+ }
69
+ }