@dbx-tools/projen 0.6.39 → 0.6.41

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/tasks/bump.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env -S npx tsx
1
+ #!/usr/bin/env -S bun
2
2
  /**
3
3
  * `projen bump` - synth, compute the next release version, then (by default)
4
4
  * commit, tag, and push it. Pushing the tag is what triggers the release
@@ -11,9 +11,9 @@
11
11
  * - the local `package.json` version,
12
12
  * then incremented by `--level` (patch | minor | major; default patch).
13
13
  *
14
- * `--sibling <dir>:<tagPrefix>` (repeatable) releases a standalone in-repo
15
- * project - one that is NOT a pnpm workspace member, so `pnpm -r` cannot see it -
16
- * at the SAME version as the root, in the same run: its manifest is stamped, its
14
+ * `--sibling <dir>:<tagPrefix>` (repeatable) releases an in-repo project that
15
+ * publishes on its OWN tag namespace (e.g. `projen/`, tagged `projen-v*`) at the
16
+ * SAME version as the root, in the same run: its manifest version is stamped, its
17
17
  * `<tagPrefix><version>` tag is cut and pushed (triggering its own workflow), and
18
18
  * it is included in the local-registry publish. Taking the base version from
19
19
  * every prefix at once is what keeps the two in lockstep: the engine sat at
@@ -32,7 +32,7 @@
32
32
  *
33
33
  * `--local-registry <value>` publishes the just-tagged version to a LOCAL
34
34
  * registry (e.g. a verdaccio) right after the git tag is pushed - so a local
35
- * `pnpm run bump` both fires the GitHub release (public npm) and populates your
35
+ * `bun run bump` both fires the GitHub release (public npm) and populates your
36
36
  * local registry. Values:
37
37
  * - `auto` (default): publish only when `npm config get registry` is a
38
38
  * loopback host (`localhost` / `127.0.0.0/8` / `::1`); otherwise skip.
@@ -44,15 +44,10 @@ import { log, net } from "@dbx-tools/shared-core";
44
44
  import { Command, Option } from "commander";
45
45
  import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
46
46
  import { resolve } from "node:path";
47
+ import { fileURLToPath } from "node:url";
47
48
 
48
49
  const logger = log.logger("projen:bump");
49
50
  const LEVELS = ["patch", "minor", "major"] as const;
50
- const DEPENDENCY_FIELDS = [
51
- "dependencies",
52
- "devDependencies",
53
- "optionalDependencies",
54
- "peerDependencies",
55
- ] as const;
56
51
  type Level = (typeof LEVELS)[number];
57
52
 
58
53
  /** A standalone in-repo project released alongside the root, on its own tag prefix. */
@@ -126,39 +121,30 @@ function readPackageVersion(pkgPath: string): [number, number, number] {
126
121
  }
127
122
 
128
123
  /**
129
- * Write `version` into a manifest projen owns. Those are emitted read-only, so
130
- * the write is bracketed by a chmod; the mode is restored afterwards to leave the
131
- * tree exactly as synth left it.
124
+ * Write ONLY the `version` field into a manifest projen owns (read-only, so
125
+ * bracketed by a chmod that restores the mode). Used for the ROOT and the
126
+ * standalone `projen/` sibling so the committed release marks the version.
127
+ *
128
+ * It deliberately does NOT rewrite `@scope/*` dep ranges. `projen/` is a workspace
129
+ * member with `workspace:*` sibling deps; baking a `^version` for the just-bumped
130
+ * (not-yet-published) version into the COMMITTED manifest breaks the release
131
+ * workflow's initial `bun install`, which checks out that commit before anything
132
+ * is published. Sibling versions are resolved transiently at publish time instead:
133
+ * `projen-release` runs `tasks/publish.ts --stamp-only` (set versions + refresh
134
+ * lockfile), then `bun publish` in `projen/` strips `workspace:*` to those versions.
132
135
  */
133
- function writeManifestVersion(pkgPath: string, version: string, dependencyScope?: string): void {
136
+ function writeManifestVersion(pkgPath: string, version: string): void {
134
137
  const { mode } = statSync(pkgPath);
135
138
  chmodSync(pkgPath, mode | 0o200);
136
139
  try {
137
140
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
138
141
  pkg.version = version;
139
- if (dependencyScope) {
140
- for (const field of DEPENDENCY_FIELDS) {
141
- const dependencies = pkg[field];
142
- if (!dependencies || typeof dependencies !== "object") continue;
143
- for (const name of Object.keys(dependencies)) {
144
- if (name.startsWith(dependencyScope)) {
145
- (dependencies as Record<string, string>)[name] = `^${version}`;
146
- }
147
- }
148
- }
149
- }
150
142
  writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
151
143
  } finally {
152
144
  chmodSync(pkgPath, mode);
153
145
  }
154
146
  }
155
147
 
156
- /** Scoped package prefix (`@scope/`) shared by the root's release packages. */
157
- function releaseDependencyScope(pkgPath: string): string | undefined {
158
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { name?: string };
159
- return /^@[^/]+\//.exec(pkg.name ?? "")?.[0];
160
- }
161
-
162
148
  /**
163
149
  * Resolve the `--local-registry` value to a registry URL to publish to, or
164
150
  * `undefined` to skip. `false` skips; a URL is used as-is; `auto` uses the
@@ -217,7 +203,6 @@ program
217
203
  }) => {
218
204
  const pkgPath = resolve(process.cwd(), "package.json");
219
205
  if (!existsSync(pkgPath)) throw new Error(`no package.json in ${process.cwd()}`);
220
- const dependencyScope = releaseDependencyScope(pkgPath);
221
206
 
222
207
  const siblings = opts.sibling.map((s) => ({ ...s, pkgPath: resolve(s.dir, "package.json") }));
223
208
  for (const s of siblings) {
@@ -228,7 +213,7 @@ program
228
213
  // manifests, workspace file, tasks, ...) rather than a stale one.
229
214
  if (opts.synth) {
230
215
  logger.info("synthesizing (projen)");
231
- exec.spawnSync("pnpm", ["exec", "projen"], {
216
+ exec.spawnSync("bun", [".projenrc.ts"], {
232
217
  cwd: process.cwd(),
233
218
  stdout: "inherit",
234
219
  stderr: "inherit",
@@ -265,7 +250,7 @@ program
265
250
 
266
251
  if (opts.version) {
267
252
  writeManifestVersion(pkgPath, version);
268
- for (const s of siblings) writeManifestVersion(s.pkgPath, version, dependencyScope);
253
+ for (const s of siblings) writeManifestVersion(s.pkgPath, version);
269
254
  const also = siblings.length ? ` (and ${siblings.map((s) => s.dir).join(", ")})` : "";
270
255
  logger.info(`wrote version ${version} to package.json${also}`);
271
256
  }
@@ -304,57 +289,26 @@ program
304
289
  }
305
290
  if (publishToLocalRegistry) {
306
291
  logger.info(`publishing ${version} to local registry ${localRegistry}`);
307
- const runIn = (cwd: string, command: string, args: string[]) =>
308
- exec.spawnSync(command, args, {
309
- cwd,
310
- stdout: "inherit",
311
- stderr: "inherit",
312
- stdin: "ignore",
313
- check: true,
314
- });
315
- const runInRepo = (command: string, args: string[]) => runIn(process.cwd(), command, args);
316
- // Each package keeps `version: 0.0.0` on disk (projen owns the
317
- // manifest); the root bump above only touched the root. Mirror the CI
318
- // `release` workflow: stamp the release version on EVERY package (they're
319
- // projen-readonly, so unlock first) so `pnpm -r publish` publishes them
320
- // as `version` (and rewrites `workspace:*` sibling pins to it) instead of
321
- // `0.0.0`. No restore needed - the next `projen` synth rewrites these
322
- // manifests back to `0.0.0`; the release version lives in the git tag.
323
- runInRepo("chmod", ["-R", "u+w", "."]);
324
- runInRepo("pnpm", [
325
- "-r",
326
- "exec",
327
- "npm",
328
- "version",
329
- version,
330
- "--no-git-tag-version",
331
- "--allow-same-version",
332
- ]);
333
- // Provenance is opt-in (see `.projenrc.ts`): the generated
334
- // `publishConfig` omits it, so local (verdaccio) publishes never try to
335
- // attest. CI turns it on with `npm_config_provenance=true`.
336
- runInRepo("pnpm", [
337
- "-r",
338
- "publish",
339
- "--registry",
340
- localRegistry,
341
- "--no-git-checks",
342
- "--access",
343
- "public",
344
- ]);
345
- // `pnpm -r` cannot see a sibling (not a workspace member), so publish each
346
- // one on its own. Skipping this is what left a local registry serving a
347
- // current CLI against a months-old engine.
348
- for (const s of siblings) {
349
- runIn(s.dir, "pnpm", [
350
- "publish",
351
- "--registry",
352
- localRegistry,
353
- "--no-git-checks",
354
- "--access",
355
- "public",
356
- ]);
357
- }
292
+ // Mirror the CI `release` workflow via the shared publish task: it sets
293
+ // the release version on every workspace member (`bun pm pkg set`; they
294
+ // keep `0.0.0` on disk, projen-owned, so it unlocks each briefly), then
295
+ // `bun publish`es each non-private one - and bun natively strips the
296
+ // `workspace:`/`catalog:` protocols in the packed tarball, resolving each
297
+ // to the version just set. The next `projen` synth restores the manifests
298
+ // to `0.0.0`; the release version lives in the git tag. Provenance is off
299
+ // for a local registry (no OIDC), so `NPM_CONFIG_PROVENANCE` is unset.
300
+ // `publish.ts` is this task's SIBLING in the engine's `tasks/` dir; resolve
301
+ // it off `import.meta.url` (works whether the engine is source-linked in-repo
302
+ // or installed under node_modules) rather than a repo-relative `tasks/...`
303
+ // that only exists inside `projen/`.
304
+ const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
305
+ exec.spawnSync("bun", [publishScript, version, "--registry", localRegistry], {
306
+ cwd: process.cwd(),
307
+ stdout: "inherit",
308
+ stderr: "inherit",
309
+ stdin: "ignore",
310
+ check: true,
311
+ });
358
312
  logger.success(`published ${version} to ${localRegistry}`);
359
313
  }
360
314
 
@@ -364,7 +318,7 @@ program
364
318
  // manifest finish the bump in lockstep.
365
319
  if (opts.version) {
366
320
  writeManifestVersion(pkgPath, version);
367
- for (const s of siblings) writeManifestVersion(s.pkgPath, version, dependencyScope);
321
+ for (const s of siblings) writeManifestVersion(s.pkgPath, version);
368
322
  logger.info(`synchronized release manifests at ${version}`);
369
323
  }
370
324
  },
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env -S bun
2
+ /**
3
+ * `bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run]`
4
+ * - set a release version on every workspace member and publish each non-private
5
+ * one with `bun publish`.
6
+ *
7
+ * Bun has no `pnpm -r publish`, so this loop is the recursive-publish stand-in.
8
+ * It leans on native bun for everything bun already does:
9
+ *
10
+ * - **version stamping** is `bun pm pkg set version=<version>` per member (bun's
11
+ * native package.json editor - idempotent, edits only that dir's manifest, no
12
+ * git side effects; `bun pm version` is for bumping and errors on an unchanged
13
+ * version, so `pkg set` is the right tool for an exact release value);
14
+ * - **`workspace:` / `catalog:` rewriting** is NOT done here - `bun publish`
15
+ * strips both protocols in the PACKED tarball, resolving `workspace:*` to the
16
+ * sibling's version and `catalog:` to the root catalog entry. (Verified: a
17
+ * packed manifest shows `"@scope/x": "<version>"` and the real catalog range,
18
+ * while the on-disk manifest keeps the protocols.) Setting each member's
19
+ * version first is the only prerequisite, so a sibling resolves the release
20
+ * version rather than the disk default of `0.0.0`;
21
+ * - **`publishConfig` substitution** (compiled `lib/` entry points) and the
22
+ * `prepack` (compile) run are `bun publish`'s own pack behavior.
23
+ *
24
+ * `--dry-run` forwards to `bun publish`: it packs + validates (running prepack)
25
+ * but uploads nothing, so the `release` workflow is testable end-to-end via a
26
+ * `workflow_dispatch` run without anything reaching npm. `--registry` targets a
27
+ * non-default registry (a local verdaccio); `--exclude <dir>` (repeatable,
28
+ * repo-relative) skips a member that releases on its OWN tag namespace (e.g.
29
+ * `projen`, published by `projen-release`, not the main `release`).
30
+ *
31
+ * The disk manifests normally carry `version: 0.0.0` (projen owns them, read-only);
32
+ * this unlocks each only long enough to set the version + publish. The next
33
+ * `projen` synth restores them - the release version lives in the git tag.
34
+ */
35
+ import { chmodSync, existsSync, readFileSync, rmSync, statSync } from "node:fs";
36
+ import { dirname, join, resolve } from "node:path";
37
+ import { exec } from "@dbx-tools/core";
38
+ import { log } from "@dbx-tools/shared-core";
39
+ import { parse } from "yaml";
40
+
41
+ const logger = log.logger("dbx-tools:publish");
42
+
43
+ /**
44
+ * Workspace member dirs (absolute), read from the root `pnpm-workspace.yaml` - the
45
+ * file the engine keeps for the Databricks Apps pnpm deploy, which also lists every
46
+ * bun workspace member. (`bun pm ls` reports installed deps, not the member globs,
47
+ * so the manifest list is the source of truth.)
48
+ */
49
+ function workspaceMembers(root: string): string[] {
50
+ const file = join(root, "pnpm-workspace.yaml");
51
+ if (!existsSync(file)) return [];
52
+ const doc = parse(readFileSync(file, "utf8")) as { packages?: string[] } | null;
53
+ return (doc?.packages ?? []).map((m) => resolve(root, m));
54
+ }
55
+
56
+ /** Spawn `command` in `cwd` with `PATH` overridden, failing the task on non-zero. */
57
+ function run(cwd: string, command: string, args: string[], path: string): void {
58
+ exec.spawnSync(command, args, {
59
+ cwd,
60
+ stdout: "inherit",
61
+ stderr: "inherit",
62
+ stdin: "ignore",
63
+ check: true,
64
+ env: { ...process.env, PATH: path },
65
+ });
66
+ }
67
+
68
+ /** Make a projen-readonly manifest writable so `bun pm pkg set` / `bun publish` can edit it. */
69
+ function unlockManifest(pkgPath: string): void {
70
+ chmodSync(pkgPath, statSync(pkgPath).mode | 0o200);
71
+ }
72
+
73
+ /**
74
+ * PATH with the workspace-root `node_modules/.bin` prepended. `bun publish` runs
75
+ * each package's `prepack` (compile) through projen's dax shell, which resolves
76
+ * `tsc` off PATH - but under the hoisted linker `tsc` lives ONLY in the root
77
+ * `.bin`, not a per-package one, so without this the compile fails with
78
+ * `dax: tsc: command not found`. (In CI `bun install` already puts it there; this
79
+ * makes the task self-sufficient when invoked directly too.)
80
+ */
81
+ function enrichedPath(root: string): string {
82
+ const binDir = join(root, "node_modules", ".bin");
83
+ const current = process.env.PATH ?? "";
84
+ return current.split(":").includes(binDir) ? current : `${binDir}:${current}`;
85
+ }
86
+
87
+ const [version, ...rest] = process.argv.slice(2);
88
+ if (!version) {
89
+ logger.error(
90
+ "usage: bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run]",
91
+ );
92
+ process.exit(1);
93
+ }
94
+ const registryIdx = rest.indexOf("--registry");
95
+ const registry = registryIdx >= 0 ? rest[registryIdx + 1] : undefined;
96
+ const dryRun = rest.includes("--dry-run");
97
+ // `--stamp-only`: set versions + refresh the lockfile, then STOP (no publish).
98
+ // The standalone `projen-release` uses this to version-stamp the workspace so its
99
+ // own `bun publish` (run separately, in `projen/`) resolves `workspace:*` siblings
100
+ // to the release version; publishing every member here would double-publish them.
101
+ const stampOnly = rest.includes("--stamp-only");
102
+ const excluded = new Set(
103
+ rest.reduce<string[]>((acc, arg, i) => (arg === "--exclude" ? [...acc, rest[i + 1]] : acc), []),
104
+ );
105
+
106
+ const root = process.cwd();
107
+ const path = enrichedPath(root);
108
+ const members = workspaceMembers(root)
109
+ .filter((dir) => existsSync(join(dir, "package.json")))
110
+ .filter((dir) => !excluded.has(resolve(root, dir).replace(`${resolve(root)}/`, "")));
111
+
112
+ // Set the version on EVERY member first (native `bun pm pkg set`), so a sibling
113
+ // published later has its `workspace:*` dep resolved to the release version - not
114
+ // the disk default of 0.0.0 - by `bun publish`'s own protocol rewriting.
115
+ logger.info(`setting ${version} across ${members.length} members`);
116
+ for (const dir of members) {
117
+ unlockManifest(join(dir, "package.json"));
118
+ run(dir, "bun", ["pm", "pkg", "set", `version=${version}`], path);
119
+ }
120
+
121
+ // Refresh the lockfile so `bun publish` resolves each `workspace:*` to the version
122
+ // just set. `bun publish`/`pm pack` reads the workspace version from the LOCKFILE,
123
+ // not the live manifest, and a plain `bun install` (even `--force`) does NOT
124
+ // re-resolve it after only a version-field change - deleting the lockfile first
125
+ // does. Without this every `workspace:*` dep would publish as the stale `0.0.0`.
126
+ const lockfile = join(root, "bun.lock");
127
+ if (existsSync(lockfile)) rmSync(lockfile);
128
+ logger.info("refreshing lockfile so workspace deps resolve to the release version");
129
+ run(root, "bun", ["install"], path);
130
+
131
+ if (stampOnly) {
132
+ logger.success(`stamped ${members.length} members @ ${version} (no publish)`);
133
+ process.exit(0);
134
+ }
135
+
136
+ const publishArgs = [
137
+ "--access",
138
+ "public",
139
+ ...(registry ? ["--registry", registry] : []),
140
+ ...(dryRun ? ["--dry-run"] : []),
141
+ ];
142
+ let published = 0;
143
+ for (const dir of members) {
144
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
145
+ name?: string;
146
+ private?: boolean;
147
+ };
148
+ if (pkg.private) {
149
+ logger.info(`skip private ${pkg.name ?? dirname(dir)}`);
150
+ continue;
151
+ }
152
+ logger.info(`${dryRun ? "dry-run publishing" : "publishing"} ${pkg.name} @ ${version}`);
153
+ run(dir, "bun", ["publish", ...publishArgs], path);
154
+ published += 1;
155
+ }
156
+ logger.success(`${dryRun ? "dry-run: packed" : "published"} ${published} packages @ ${version}`);
package/src/vite.ts DELETED
@@ -1,99 +0,0 @@
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 (see {@link DEFAULT_VITE_OVERRIDES}) and, when present, merges that
8
- * module's default export over the generated config with Vite's `mergeConfig` - in
9
- * listed order, so later files win and absent ones are skipped. A package thus
10
- * tweaks Vite WITHOUT editing the projen-owned file.
11
- *
12
- * An override may be written in TypeScript or JavaScript. The generated config
13
- * reaches it through a dynamic `import()` of a runtime-computed URL, which Vite's
14
- * config bundling cannot inline and so leaves for Node to execute - and Node
15
- * strips types from a `.ts` file outside `node_modules` on its own. Being a
16
- * package-ROOT file (not under `src/`), neither the generated `vite.config.ts`
17
- * nor the override is in the package's `tsconfig` `include`, so their `node:*`
18
- * usage never trips the `ui` package's `compile` under the DOM-only tsconfig.
19
- */
20
- import { type Project, TextFile } from "projen";
21
-
22
- /**
23
- * Default unmanaged override modules, merged over the generated config in order
24
- * (later wins, absent files skipped). Both extensions are accepted so an existing
25
- * JavaScript override keeps working; the TypeScript one is listed last so it wins
26
- * where a package is mid-migration and still has both.
27
- *
28
- * Exported because ESLint has to ignore them: an override is a package-ROOT file
29
- * outside any `src/**` tsconfig include, so the type-aware parser cannot resolve
30
- * it to a project - the same reason the generated `vite.config.ts` is ignored.
31
- */
32
- export const DEFAULT_VITE_OVERRIDES = ["vite.config.override.js", "vite.config.override.ts"];
33
-
34
- /** Render the generated `vite.config.ts` source with the override chain inlined. */
35
- function renderViteConfig(overridePaths: string[]): string {
36
- const overrides = overridePaths.map((path) => ` ${JSON.stringify(path)},`).join("\n");
37
- return String.raw`
38
- import { existsSync } from "node:fs";
39
- import react from "@vitejs/plugin-react";
40
- import {
41
- defineConfig,
42
- mergeConfig,
43
- type ConfigEnv,
44
- type UserConfig,
45
- type UserConfigExport,
46
- } from "vite";
47
-
48
- // Unmanaged override modules (relative to this file), merged over the generated
49
- // config in order - later wins, absent files are skipped.
50
- const OVERRIDE_FILES = [
51
- ${overrides}
52
- ];
53
-
54
- async function resolveConfig(
55
- config: UserConfigExport,
56
- env: ConfigEnv,
57
- ): Promise<UserConfig> {
58
- if (typeof config === "function") {
59
- return await config(env);
60
- }
61
- return await config;
62
- }
63
-
64
- export default defineConfig(async (configEnv: ConfigEnv) => {
65
- let config: UserConfig = {
66
- plugins: [react()],
67
- };
68
-
69
- for (const file of OVERRIDE_FILES) {
70
- const overrideUrl = new URL(file, import.meta.url);
71
- if (!existsSync(overrideUrl)) {
72
- continue;
73
- }
74
- const overrideModule = await import(overrideUrl.href);
75
- const override = await resolveConfig(
76
- overrideModule.default as UserConfigExport,
77
- configEnv,
78
- );
79
- config = mergeConfig(config, override);
80
- }
81
-
82
- return config;
83
- });
84
- `.trimStart();
85
- }
86
-
87
- /**
88
- * A projen-owned, read-only `vite.config.ts` (React + the runtime override merge
89
- * chain described in the module docstring).
90
- */
91
- export class ViteConfigFile extends TextFile {
92
- constructor(project: Project) {
93
- super(project, "vite.config.ts", {
94
- marker: true,
95
- readonly: true,
96
- lines: renderViteConfig(DEFAULT_VITE_OVERRIDES).split("\n"),
97
- });
98
- }
99
- }