@dbx-tools/projen 0.6.14 → 0.6.27
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 +6 -1
- package/package.json +2 -1
- package/src/barrels.ts +73 -22
- package/src/release.ts +57 -48
- package/tasks/barrels.ts +18 -1
- package/tasks/bump.ts +5 -6
package/README.md
CHANGED
|
@@ -95,7 +95,12 @@ barrels.generateBarrels();
|
|
|
95
95
|
|
|
96
96
|
`generateCodegen()` reads `package.json` `codegen.inputs` and writes generated
|
|
97
97
|
schema modules. `generateBarrels()` writes package-root `index.ts` barrels with
|
|
98
|
-
module namespaces and flat unique type exports
|
|
98
|
+
module namespaces and flat unique type exports, returning the number that
|
|
99
|
+
actually changed. A barrel whose export surface is unchanged is left untouched,
|
|
100
|
+
read-only bit included, so concurrent writers never collide over it. Every
|
|
101
|
+
package is attempted even if one fails; the failures are re-thrown together as an
|
|
102
|
+
`AggregateError` naming each package, rather than the first one abandoning the
|
|
103
|
+
rest of the sweep.
|
|
99
104
|
|
|
100
105
|
## Generate OpenAPI Clients
|
|
101
106
|
|
package/package.json
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"main": "index.ts",
|
|
34
34
|
"license": "Apache-2.0",
|
|
35
|
-
"version": "0.6.
|
|
35
|
+
"version": "0.6.27",
|
|
36
36
|
"types": "index.ts",
|
|
37
37
|
"type": "module",
|
|
38
38
|
"exports": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"clobber": "projen clobber",
|
|
52
52
|
"compile": "projen compile",
|
|
53
53
|
"default": "projen default",
|
|
54
|
+
"demo": "projen demo",
|
|
54
55
|
"eject": "projen eject",
|
|
55
56
|
"package": "projen package",
|
|
56
57
|
"post-compile": "projen post-compile",
|
package/src/barrels.ts
CHANGED
|
@@ -36,19 +36,13 @@
|
|
|
36
36
|
* The result gets a do-not-edit header + read-only bit (see `./generated`).
|
|
37
37
|
*/
|
|
38
38
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
39
|
-
import { join } from "node:path";
|
|
39
|
+
import { join, relative } from "node:path";
|
|
40
40
|
import { find } from "@dbx-tools/path";
|
|
41
41
|
import { string } from "@dbx-tools/shared-core";
|
|
42
42
|
import isIdentifier from "is-identifier";
|
|
43
|
-
import {
|
|
44
|
-
header,
|
|
45
|
-
makeReadonly,
|
|
46
|
-
makeWritable,
|
|
47
|
-
stampGenerated,
|
|
48
|
-
type HeaderOpts,
|
|
49
|
-
} from "./generated.ts";
|
|
43
|
+
import { header, makeReadonly, makeWritable, type HeaderOpts } from "./generated.ts";
|
|
50
44
|
import { moduleExports, moduleStatements, type ModuleExport } from "./module-exports.ts";
|
|
51
|
-
import { isModuleFile, toPosix, recordedPackages } from "./packages.ts";
|
|
45
|
+
import { isModuleFile, toPosix, recordedPackages, repoRoot } from "./packages.ts";
|
|
52
46
|
|
|
53
47
|
/**
|
|
54
48
|
* A `src`-relative posix path excluded from the root barrel:
|
|
@@ -292,9 +286,6 @@ function generateForPackage(pkgDir: string): number {
|
|
|
292
286
|
return 0;
|
|
293
287
|
}
|
|
294
288
|
|
|
295
|
-
// Unlock the read-only barrel so the rewrite below can replace it.
|
|
296
|
-
makeWritable(rootBarrel);
|
|
297
|
-
|
|
298
289
|
// `./src/<path>` with the module's REAL extension, namespaced by its path
|
|
299
290
|
// segments (camelCase; invalid identifiers suffixed with `Module`). The
|
|
300
291
|
// extension is written because `tsc` rewrites it on emit
|
|
@@ -315,30 +306,90 @@ function generateForPackage(pkgDir: string): number {
|
|
|
315
306
|
content = mergeCustomExports(content, pkgDir);
|
|
316
307
|
|
|
317
308
|
// The barrel only *changes* when its set of exporting modules does. If the
|
|
318
|
-
// stamped result matches what's already on disk,
|
|
319
|
-
//
|
|
320
|
-
// ordinary in-file edits (which leave the export * as … list
|
|
309
|
+
// stamped result matches what's already on disk, leave the file - and its
|
|
310
|
+
// read-only bit - completely untouched and report no change (0). This keeps the
|
|
311
|
+
// watcher quiet on ordinary in-file edits (which leave the export * as … list
|
|
312
|
+
// identical), and it is also what keeps a no-op cycle off the read-only bit
|
|
313
|
+
// entirely: see {@link writeBarrel} for why unlocking a barrel we are not about
|
|
314
|
+
// to rewrite is what produced spurious EACCES failures.
|
|
321
315
|
content = `${content.replace(/\n+$/, "")}\n`;
|
|
322
316
|
const next = `${header(BARREL_HEADER)}\n${content}`;
|
|
323
|
-
if (before === next)
|
|
324
|
-
makeReadonly(rootBarrel);
|
|
325
|
-
return 0;
|
|
326
|
-
}
|
|
317
|
+
if (before === next) return 0;
|
|
327
318
|
|
|
328
|
-
|
|
329
|
-
|
|
319
|
+
// Written whole (header included) rather than via `stampGenerated`, which would
|
|
320
|
+
// re-read and rewrite the file to prepend the same header - a second write, and
|
|
321
|
+
// therefore a second window in which the read-only bit can come back. `next` is
|
|
322
|
+
// byte-for-byte what the comparison above accepted, so the two cannot drift.
|
|
323
|
+
writeBarrel(rootBarrel, next);
|
|
324
|
+
makeReadonly(rootBarrel);
|
|
330
325
|
return 1;
|
|
331
326
|
}
|
|
332
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Attempts at unlocking and writing a barrel before giving up. A sibling process
|
|
330
|
+
* can restore the read-only bit between the two.
|
|
331
|
+
*/
|
|
332
|
+
const WRITE_ATTEMPTS = 3;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Unlock a read-only barrel and replace it, retrying on `EACCES`.
|
|
336
|
+
*
|
|
337
|
+
* The unlock cannot be hoisted to the top of {@link generateForPackage}: several
|
|
338
|
+
* processes write barrels concurrently under `sync --watch` (the barrels watcher,
|
|
339
|
+
* and the projenrc watcher's post-synth `generateBarrels()` sweep), so any gap
|
|
340
|
+
* between `makeWritable` and the write is a window in which another process's
|
|
341
|
+
* `makeReadonly` lands and this write fails with
|
|
342
|
+
* `EACCES: permission denied, open '<pkg>/index.ts'`. The gap used to span all of
|
|
343
|
+
* the oxc parsing done for export hoisting, which made it wide enough to hit
|
|
344
|
+
* routinely. Keeping the unlock adjacent to the write shrinks it to nothing much,
|
|
345
|
+
* and a retry absorbs what is left.
|
|
346
|
+
*
|
|
347
|
+
* Do NOT "simplify" this back to a single unlock-then-write: the failure is
|
|
348
|
+
* timing-dependent, so it looks fine until a full-repo sweep runs against a
|
|
349
|
+
* concurrent one.
|
|
350
|
+
*/
|
|
351
|
+
function writeBarrel(file: string, content: string): void {
|
|
352
|
+
for (let attempt = 1; ; attempt++) {
|
|
353
|
+
makeWritable(file);
|
|
354
|
+
try {
|
|
355
|
+
writeFileSync(file, content);
|
|
356
|
+
return;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
359
|
+
if (code !== "EACCES" || attempt >= WRITE_ATTEMPTS) throw err;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
333
364
|
/**
|
|
334
365
|
* Rebuild barrels for the given package dirs (default: every package recorded in
|
|
335
366
|
* `pnpm-workspace.yaml` - the source of truth, read via `recordedPackages()`).
|
|
336
367
|
* Returns the number of barrels whose contents actually changed (an unchanged
|
|
337
368
|
* export surface is a no-op), so callers can stay quiet when nothing moved.
|
|
369
|
+
*
|
|
370
|
+
* Every package is attempted even if an earlier one fails, and the failures are
|
|
371
|
+
* re-thrown together as an `AggregateError` naming each package. Letting the first
|
|
372
|
+
* failure propagate instead abandoned every package after it in the iteration
|
|
373
|
+
* order, so one unwritable barrel silently left the rest of the repo stale with
|
|
374
|
+
* nothing in the log to say which packages had been skipped.
|
|
338
375
|
*/
|
|
339
376
|
export function generateBarrels(opts: { dirs?: string[] } = {}): number {
|
|
340
377
|
const dirs = opts.dirs ?? recordedPackages().map((p) => p.dir);
|
|
341
378
|
let total = 0;
|
|
342
|
-
|
|
379
|
+
const failures: { dir: string; err: unknown }[] = [];
|
|
380
|
+
for (const dir of dirs) {
|
|
381
|
+
try {
|
|
382
|
+
total += generateForPackage(dir);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
failures.push({ dir, err });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (failures.length) {
|
|
388
|
+
const names = failures.map((f) => relative(repoRoot, f.dir) || f.dir);
|
|
389
|
+
throw new AggregateError(
|
|
390
|
+
failures.map((f) => f.err),
|
|
391
|
+
`${string.pluralize(failures.length, "barrel")} failed: ${names.join(", ")}`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
343
394
|
return total;
|
|
344
395
|
}
|
package/src/release.ts
CHANGED
|
@@ -6,9 +6,38 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Component } from "projen";
|
|
8
8
|
import { GithubWorkflow } from "projen/lib/github";
|
|
9
|
-
import { JobPermission } from "projen/lib/github/workflows-model";
|
|
9
|
+
import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
|
|
10
10
|
import { applyTasks, taskScript, type DBXToolsNodeProject } from "./project.ts";
|
|
11
11
|
|
|
12
|
+
const NODE_VERSION = "lts/*";
|
|
13
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
14
|
+
const PNPM_VERSION = "10.33.0";
|
|
15
|
+
|
|
16
|
+
interface PublishWorkflow {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly tagPrefix: string;
|
|
19
|
+
readonly steps: readonly JobStep[];
|
|
20
|
+
readonly workingDirectory?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Shared checkout and toolchain setup for every npm publish workflow. */
|
|
24
|
+
function publishSetupSteps(): JobStep[] {
|
|
25
|
+
return [
|
|
26
|
+
{ name: "Checkout", uses: "actions/checkout@v6", with: { "fetch-depth": 0 } },
|
|
27
|
+
{ name: "Setup pnpm", uses: "pnpm/action-setup@v5", with: { version: PNPM_VERSION } },
|
|
28
|
+
{
|
|
29
|
+
name: "Setup Node.js",
|
|
30
|
+
uses: "actions/setup-node@v6",
|
|
31
|
+
// setup-node writes the temporary npmrc that maps NODE_AUTH_TOKEN onto
|
|
32
|
+
// npmjs. Omitting this leaves the secret in the environment but gives npm
|
|
33
|
+
// no registry-scoped auth entry, and every publish fails with ENEEDAUTH.
|
|
34
|
+
with: { "node-version": NODE_VERSION, "registry-url": NPM_REGISTRY_URL },
|
|
35
|
+
},
|
|
36
|
+
// The lockfile is intentionally untracked and may be absent or stale in CI.
|
|
37
|
+
{ name: "Install", run: "pnpm install --no-frozen-lockfile" },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
12
41
|
/**
|
|
13
42
|
* A standalone project that lives in a repo SUBDIRECTORY but is NOT a member of
|
|
14
43
|
* this pnpm workspace (e.g. the `@dbx-tools/projen` engine in `projen/`), yet
|
|
@@ -115,43 +144,44 @@ export class DBXToolsRelease extends Component {
|
|
|
115
144
|
}
|
|
116
145
|
}
|
|
117
146
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
private authorReleaseWorkflow(project: DBXToolsNodeProject): void {
|
|
125
|
-
const workflow = new GithubWorkflow(project.github!, "release", {
|
|
147
|
+
/** Author the common tag trigger, concurrency policy, permissions, and publish job. */
|
|
148
|
+
private authorPublishWorkflow(
|
|
149
|
+
project: DBXToolsNodeProject,
|
|
150
|
+
{ name, tagPrefix, steps, workingDirectory }: PublishWorkflow,
|
|
151
|
+
): void {
|
|
152
|
+
const workflow = new GithubWorkflow(project.github!, name, {
|
|
126
153
|
// Serialize publishes so two tags landing together cannot race to the
|
|
127
154
|
// registry, but never cancel a run already in flight: a half-published
|
|
128
155
|
// release is worse than a queued one.
|
|
129
156
|
limitConcurrency: true,
|
|
130
|
-
concurrencyOptions: { group:
|
|
157
|
+
concurrencyOptions: { group: name, cancelInProgress: false },
|
|
131
158
|
});
|
|
132
159
|
// Read-only floor for any job that does not declare its own permissions;
|
|
133
160
|
// the publish job below overrides it with the `id-token` it needs.
|
|
134
161
|
workflow.file?.addOverride("permissions", { contents: "read" });
|
|
135
|
-
workflow.on({ push: { tags: [`${
|
|
162
|
+
workflow.on({ push: { tags: [`${tagPrefix}*`] } });
|
|
136
163
|
workflow.addJob("publish", {
|
|
137
164
|
runsOn: ["ubuntu-latest"],
|
|
138
165
|
// `id-token: write` lets npm mint the OIDC token for provenance attestation.
|
|
139
166
|
permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
|
|
140
167
|
timeoutMinutes: 30,
|
|
141
168
|
env: { CI: "true" },
|
|
169
|
+
steps: [...publishSetupSteps(), ...steps],
|
|
170
|
+
...(workingDirectory ? { defaults: { run: { workingDirectory } } } : {}),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Emit the `release` GitHub workflow: push `<prefix>1.2.3` and every
|
|
176
|
+
* publishable package is published to npm at 1.2.3. Setting the
|
|
177
|
+
* version on every package first makes the pushed tag the published version
|
|
178
|
+
* (no bump math).
|
|
179
|
+
*/
|
|
180
|
+
private authorReleaseWorkflow(project: DBXToolsNodeProject): void {
|
|
181
|
+
this.authorPublishWorkflow(project, {
|
|
182
|
+
name: "release",
|
|
183
|
+
tagPrefix: this.tagPrefix,
|
|
142
184
|
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
185
|
// The pushed tag is the version: `<prefix>1.2.3` -> `1.2.3`. Set it on
|
|
156
186
|
// every package (manifests are projen-readonly, so unlock them first).
|
|
157
187
|
{
|
|
@@ -189,32 +219,11 @@ export class DBXToolsRelease extends Component {
|
|
|
189
219
|
project: DBXToolsNodeProject,
|
|
190
220
|
{ name, directory, tagPrefix }: StandaloneRelease,
|
|
191
221
|
): void {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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" },
|
|
222
|
+
this.authorPublishWorkflow(project, {
|
|
223
|
+
name,
|
|
224
|
+
tagPrefix,
|
|
225
|
+
workingDirectory: directory,
|
|
205
226
|
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
227
|
// The pushed tag is the version: `<prefix>1.2.3` -> `1.2.3`. package.json
|
|
219
228
|
// is projen-generated read-only, so unlock it before `npm version` writes.
|
|
220
229
|
{
|
package/tasks/barrels.ts
CHANGED
|
@@ -19,11 +19,28 @@ if (process.argv.includes("--watch")) {
|
|
|
19
19
|
watchLoop("barrels", watchRoots(), (changed) => {
|
|
20
20
|
const pkgDirs = recordedPackages().map((p) => p.dir);
|
|
21
21
|
const dirs = new Set<string>();
|
|
22
|
+
const unowned: string[] = [];
|
|
22
23
|
for (const p of changed) {
|
|
23
24
|
const owner = ownerPackageDir(p, pkgDirs);
|
|
24
25
|
if (owner) dirs.add(owner);
|
|
26
|
+
else unowned.push(p);
|
|
25
27
|
}
|
|
26
|
-
|
|
28
|
+
// A change under a package root that no RECORDED package owns is almost always a
|
|
29
|
+
// NEW package folder: it has no `pnpm-workspace.yaml` member yet, so there is no
|
|
30
|
+
// barrel for this watcher to target and only a re-synth can create one. Say so,
|
|
31
|
+
// rather than rebuilding every barrel in the repo - the previous fallback, which
|
|
32
|
+
// did a full-repo sweep on behalf of a file it could not barrel anyway, and in
|
|
33
|
+
// doing so raced the projenrc watcher's own post-synth sweep.
|
|
34
|
+
if (dirs.size === 0) {
|
|
35
|
+
if (unowned.length) {
|
|
36
|
+
logger.warn(
|
|
37
|
+
`no recorded package owns ${string.pluralize(unowned.length, "change")}; ` +
|
|
38
|
+
"run `pnpm exec projen` (or touch .projenrc.ts) to pick up a new package folder",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const n = generateBarrels({ dirs: [...dirs] });
|
|
27
44
|
if (n) logger.success(`rebuilt ${string.pluralize(n, "barrel")}`);
|
|
28
45
|
});
|
|
29
46
|
} else {
|
package/tasks/bump.ts
CHANGED
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
* - `false`: never publish locally.
|
|
40
40
|
* - a URL: always publish to that registry.
|
|
41
41
|
*/
|
|
42
|
-
import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
43
|
-
import { resolve } from "node:path";
|
|
44
|
-
import { Command, Option } from "commander";
|
|
45
42
|
import { exec, project } from "@dbx-tools/core";
|
|
46
43
|
import { log, net } from "@dbx-tools/shared-core";
|
|
44
|
+
import { Command, Option } from "commander";
|
|
45
|
+
import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
46
|
+
import { resolve } from "node:path";
|
|
47
47
|
|
|
48
48
|
const logger = log.logger("projen:bump");
|
|
49
49
|
const LEVELS = ["patch", "minor", "major"] as const;
|
|
@@ -229,7 +229,7 @@ program
|
|
|
229
229
|
const tags = prefixes.map((prefix) => `${prefix}${version}`);
|
|
230
230
|
logger.info(
|
|
231
231
|
`bump ${base.join(".")} -> ${version} (${opts.level}); tags ${tags.join(", ")}` +
|
|
232
|
-
|
|
232
|
+
`${tagged.length ? "" : " [no remote tag]"}`,
|
|
233
233
|
);
|
|
234
234
|
for (const t of tagged) {
|
|
235
235
|
if (compareSemver(t.version, base) < 0) {
|
|
@@ -288,8 +288,7 @@ program
|
|
|
288
288
|
stdin: "ignore",
|
|
289
289
|
check: true,
|
|
290
290
|
});
|
|
291
|
-
const runInRepo = (command: string, args: string[]) =>
|
|
292
|
-
runIn(process.cwd(), command, args);
|
|
291
|
+
const runInRepo = (command: string, args: string[]) => runIn(process.cwd(), command, args);
|
|
293
292
|
// Each package keeps `version: 0.0.0` on disk (projen owns the
|
|
294
293
|
// manifest); the root bump above only touched the root. Mirror the CI
|
|
295
294
|
// `release` workflow: stamp the release version on EVERY package (they're
|