@dbx-tools/projen 0.6.47 → 0.6.48
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 +8 -3
- package/package.json +4 -4
- package/src/barrels.ts +35 -9
- package/src/generated.ts +22 -2
package/README.md
CHANGED
|
@@ -111,9 +111,14 @@ schema modules. They are written read-only, and the root ESLint task runs with
|
|
|
111
111
|
`--fix` (which fails on a read-only file), so each generated module is added to
|
|
112
112
|
`ignorePatterns` at synth - named individually via `codegen.codegenModulePaths()`,
|
|
113
113
|
never as a blanket `<package>/src/**`. A codegen package may hold hand-written
|
|
114
|
-
modules beside its generated ones, and those must stay linted.
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
modules beside its generated ones, and those must stay linted.
|
|
115
|
+
|
|
116
|
+
`generateBarrels()` writes package-root `index.ts` barrels with module
|
|
117
|
+
namespaces and flat unique type exports, returning the number that actually
|
|
118
|
+
changed. A name two modules both declare is ambiguous and stays namespace-only —
|
|
119
|
+
except when one of them is generated: the hand-written module is the curated view
|
|
120
|
+
of the generated shape (`shared-genie`'s `genie-model.ts` extends its own
|
|
121
|
+
codegen'd `dashboards.ts`), so it owns the name and stays hoisted. A barrel whose export surface is unchanged is left untouched,
|
|
117
122
|
read-only bit included, so concurrent writers never collide over it. Every
|
|
118
123
|
package is attempted even if one fails; the failures are re-thrown together as an
|
|
119
124
|
`AggregateError` naming each package, rather than the first one abandoning the
|
package/package.json
CHANGED
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@clack/prompts": "^1.7.0",
|
|
29
|
-
"@dbx-tools/core": "0.6.
|
|
30
|
-
"@dbx-tools/path": "0.6.
|
|
31
|
-
"@dbx-tools/shared-core": "0.6.
|
|
29
|
+
"@dbx-tools/core": "0.6.48",
|
|
30
|
+
"@dbx-tools/path": "0.6.48",
|
|
31
|
+
"@dbx-tools/shared-core": "0.6.48",
|
|
32
32
|
"commander": "^15.0.0",
|
|
33
33
|
"concurrently": "^10.0.3",
|
|
34
34
|
"constructs": "^10.6.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"main": "index.ts",
|
|
49
49
|
"license": "Apache-2.0",
|
|
50
|
-
"version": "0.6.
|
|
50
|
+
"version": "0.6.48",
|
|
51
51
|
"types": "index.ts",
|
|
52
52
|
"type": "module",
|
|
53
53
|
"exports": {
|
package/src/barrels.ts
CHANGED
|
@@ -33,6 +33,15 @@
|
|
|
33
33
|
* name stays namespace-only. Names that collide with a generated namespace, or
|
|
34
34
|
* that a hand-authored `exports.ts` declares, are never hoisted (that file wins).
|
|
35
35
|
*
|
|
36
|
+
* One collision is NOT ambiguous, though: a HAND-WRITTEN module and a GENERATED
|
|
37
|
+
* one (a codegen `src/` module, recognised by its do-not-edit banner) declaring
|
|
38
|
+
* the same name. The hand-written module is by definition the curated view of the
|
|
39
|
+
* generated shape - `shared-genie`'s `genie-model.ts` extends and re-exports its
|
|
40
|
+
* own generated `dashboards.ts` - so it WINS and its name is still hoisted.
|
|
41
|
+
* Treating that pair as ambiguous is what silently dropped `GenieMessage`,
|
|
42
|
+
* `GenieSpace`, and `MessageStatus` from the barrel the moment the two modules
|
|
43
|
+
* became siblings, breaking every consumer importing them by name.
|
|
44
|
+
*
|
|
36
45
|
* The result gets a do-not-edit header + read-only bit (see `./generated`).
|
|
37
46
|
*/
|
|
38
47
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -40,7 +49,7 @@ import { join, relative } from "node:path";
|
|
|
40
49
|
import { find } from "@dbx-tools/path";
|
|
41
50
|
import { string } from "@dbx-tools/shared-core";
|
|
42
51
|
import isIdentifier from "is-identifier";
|
|
43
|
-
import { header, makeReadonly, makeWritable, type HeaderOpts } from "./generated.ts";
|
|
52
|
+
import { header, isGenerated, makeReadonly, makeWritable, type HeaderOpts } from "./generated.ts";
|
|
44
53
|
import { moduleExports, moduleStatements, type ModuleExport } from "./module-exports.ts";
|
|
45
54
|
import { isModuleFile, toPosix, recordedPackages, repoRoot } from "./packages.ts";
|
|
46
55
|
|
|
@@ -140,8 +149,10 @@ function namespaceLines(content: string): { ns: string; modulePath: string }[] {
|
|
|
140
149
|
* package's modules - `export type { ... }` for types, `export { ... }` for
|
|
141
150
|
* non-function values (classes, consts, enums, …). `export function` names are
|
|
142
151
|
* never hoisted; they stay namespace-only (`posixPath.toPosix`). A name declared
|
|
143
|
-
* by two or more modules is ambiguous and left namespace-only
|
|
144
|
-
*
|
|
152
|
+
* by two or more modules is ambiguous and left namespace-only - UNLESS exactly one
|
|
153
|
+
* of them is hand-written and the rest are generated, in which case the
|
|
154
|
+
* hand-written module owns the name (see the module doc). `suppress` names (a
|
|
155
|
+
* hand-authored `exports.ts` surface) are never hoisted so that file stays
|
|
145
156
|
* authoritative.
|
|
146
157
|
*/
|
|
147
158
|
function hoistUniqueExports(content: string, pkgDir: string, suppress: Set<string>): string {
|
|
@@ -157,17 +168,32 @@ function hoistUniqueExports(content: string, pkgDir: string, suppress: Set<strin
|
|
|
157
168
|
// Uniqueness is tallied over hoistable types AND values together - name ->
|
|
158
169
|
// { count, owning module }. Functions are excluded from hoisting and from
|
|
159
170
|
// this tally so they do not block a same-named type/class in another module.
|
|
160
|
-
|
|
171
|
+
//
|
|
172
|
+
// A generated module never claims a name a hand-written sibling also declares:
|
|
173
|
+
// it is counted only while no hand-written module owns the name, and it yields
|
|
174
|
+
// ownership as soon as one does. So a curated re-export
|
|
175
|
+
// (`genie-model.ts`'s `GenieMessage`, extending generated `dashboards.ts`)
|
|
176
|
+
// stays hoisted, while two HAND-WRITTEN modules claiming one name are still
|
|
177
|
+
// ambiguous and stay namespace-only.
|
|
178
|
+
const seen = new Map<string, { count: number; modulePath: string; generated: boolean }>();
|
|
161
179
|
const perModule = new Map<string, ModuleExport[]>();
|
|
162
180
|
for (const { modulePath } of namespaces) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
);
|
|
181
|
+
const file = join(pkgDir, modulePath.replace(/^\.\//, ""));
|
|
182
|
+
const exports = moduleExports(file).filter((e) => !e.isFunction);
|
|
166
183
|
perModule.set(modulePath, exports);
|
|
184
|
+
const generated = isGenerated(file);
|
|
167
185
|
for (const { name } of exports) {
|
|
168
186
|
const prior = seen.get(name);
|
|
169
|
-
if (prior)
|
|
170
|
-
|
|
187
|
+
if (!prior) {
|
|
188
|
+
seen.set(name, { count: 1, modulePath, generated });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// Hand-written beats generated, either direction, without counting as a clash.
|
|
192
|
+
if (prior.generated !== generated) {
|
|
193
|
+
if (prior.generated) seen.set(name, { count: 1, modulePath, generated });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
prior.count += 1;
|
|
171
197
|
}
|
|
172
198
|
}
|
|
173
199
|
|
package/src/generated.ts
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
|
|
13
|
+
/** First line of every banner {@link header} builds. */
|
|
14
|
+
const MARKER = "// GENERATED by";
|
|
15
|
+
|
|
13
16
|
const READONLY = 0o444;
|
|
14
17
|
const WRITABLE = 0o644;
|
|
15
18
|
|
|
@@ -46,12 +49,29 @@ export interface HeaderOpts {
|
|
|
46
49
|
|
|
47
50
|
/** Build the do-not-edit banner (line comments, for TS/JS). */
|
|
48
51
|
export function header(opts: HeaderOpts): string {
|
|
49
|
-
const lines = [
|
|
52
|
+
const lines = [`${MARKER} ${opts.tool} - DO NOT EDIT.`];
|
|
50
53
|
if (opts.source) lines.push(`// Regenerated from ${opts.source}.`);
|
|
51
54
|
lines.push("// Hand edits are overwritten on the next watch; this file is read-only.");
|
|
52
55
|
return `${lines.join("\n")}\n`;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* True if the file exists and carries the do-not-edit banner {@link header}
|
|
60
|
+
* writes - i.e. a toolchain wrote it and a hand edit would be overwritten.
|
|
61
|
+
*
|
|
62
|
+
* Prefer this over {@link isReadonly} when the answer must survive a fresh
|
|
63
|
+
* clone: git tracks no read-only bit, so a generated file checked out before the
|
|
64
|
+
* first synth looks hand-authored by mode alone, while the banner is part of the
|
|
65
|
+
* committed content.
|
|
66
|
+
*/
|
|
67
|
+
export function isGenerated(file: string): boolean {
|
|
68
|
+
try {
|
|
69
|
+
return readFileSync(file, "utf8").startsWith(MARKER);
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
/**
|
|
56
76
|
* Prepend the do-not-edit header to a file some tool just wrote (e.g. a
|
|
57
77
|
* generated barrel `index.ts`), then set it read-only. Idempotent.
|
|
@@ -59,7 +79,7 @@ export function header(opts: HeaderOpts): string {
|
|
|
59
79
|
export function stampGenerated(file: string, opts: HeaderOpts): void {
|
|
60
80
|
makeWritable(file);
|
|
61
81
|
const body = readFileSync(file, "utf8");
|
|
62
|
-
const next = body.startsWith(
|
|
82
|
+
const next = body.startsWith(MARKER) ? body : `${header(opts)}\n${body}`;
|
|
63
83
|
writeFileSync(file, next);
|
|
64
84
|
makeReadonly(file);
|
|
65
85
|
}
|