gbs-add-block 2.1.0 → 2.3.0

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.
Files changed (129) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +190 -134
  2. package/.gbs/skills/gbs-components/references/install.md +25 -3
  3. package/.gbs/skills/gbs-components/references/styling.md +246 -207
  4. package/CHANGELOG.md +74 -0
  5. package/README.md +154 -23
  6. package/index.cjs +212 -3
  7. package/package.json +41 -10
  8. package/schema/passport-v1.schema.json +204 -0
  9. package/source/beta-components/accordion/passport.json +259 -0
  10. package/source/beta-components/accordion/styles.css +207 -208
  11. package/source/beta-components/alert/passport.json +250 -0
  12. package/source/beta-components/alert/styles.css +154 -155
  13. package/source/beta-components/avatar/passport.json +294 -0
  14. package/source/beta-components/avatar/styles.css +225 -226
  15. package/source/beta-components/badge/passport.json +332 -0
  16. package/source/beta-components/badge/styles.css +203 -204
  17. package/source/beta-components/breadcrumb/passport.json +243 -0
  18. package/source/beta-components/breadcrumb/styles.css +138 -140
  19. package/source/beta-components/button/passport.json +402 -0
  20. package/source/beta-components/button/passport.manual.json +31 -0
  21. package/source/beta-components/button/styles.css +232 -234
  22. package/source/beta-components/card/passport.json +337 -0
  23. package/source/beta-components/card/styles.css +230 -232
  24. package/source/beta-components/checkbox/passport.json +456 -0
  25. package/source/beta-components/checkbox/styles.css +211 -213
  26. package/source/beta-components/combobox/passport.json +456 -0
  27. package/source/beta-components/combobox/styles.css +419 -417
  28. package/source/beta-components/data-grid/agent/coerce.ts +368 -0
  29. package/source/beta-components/data-grid/agent/contract.ts +410 -0
  30. package/source/beta-components/data-grid/agent/dataset.ts +92 -0
  31. package/source/beta-components/data-grid/agent/engine.ts +470 -0
  32. package/source/beta-components/data-grid/agent/executors.ts +155 -0
  33. package/source/beta-components/data-grid/agent/index.ts +79 -0
  34. package/source/beta-components/data-grid/agent/intent.ts +324 -0
  35. package/source/beta-components/data-grid/agent/operations.ts +335 -0
  36. package/source/beta-components/data-grid/agent/validate.ts +630 -0
  37. package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
  38. package/source/beta-components/data-grid/index.ts +14 -7
  39. package/source/beta-components/data-grid/passport.json +1051 -0
  40. package/source/beta-components/data-grid/passport.manual.json +255 -0
  41. package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
  42. package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
  43. package/source/beta-components/data-grid/styles.css +874 -716
  44. package/source/beta-components/date-picker/passport.json +407 -0
  45. package/source/beta-components/date-picker/styles.css +445 -446
  46. package/source/beta-components/dialog/passport.json +344 -0
  47. package/source/beta-components/dialog/styles.css +280 -279
  48. package/source/beta-components/file-uploader/passport.json +518 -0
  49. package/source/beta-components/file-uploader/styles.css +394 -396
  50. package/source/beta-components/input/passport.json +536 -0
  51. package/source/beta-components/input/styles.css +295 -297
  52. package/source/beta-components/menu/passport.json +322 -0
  53. package/source/beta-components/menu/styles.css +224 -223
  54. package/source/beta-components/modal/passport.json +289 -0
  55. package/source/beta-components/modal/styles.css +241 -240
  56. package/source/beta-components/number-input/passport.json +541 -0
  57. package/source/beta-components/number-input/styles.css +230 -231
  58. package/source/beta-components/popover/passport.json +238 -0
  59. package/source/beta-components/popover/styles.css +148 -147
  60. package/source/beta-components/progress/passport.json +270 -0
  61. package/source/beta-components/progress/styles.css +200 -201
  62. package/source/beta-components/radio-group/passport.json +477 -0
  63. package/source/beta-components/radio-group/styles.css +269 -270
  64. package/source/beta-components/shared/core/agent/adapter.ts +65 -0
  65. package/source/beta-components/shared/core/agent/history.ts +120 -0
  66. package/source/beta-components/shared/core/agent/index.ts +46 -0
  67. package/source/beta-components/shared/core/agent/numbers.ts +217 -0
  68. package/source/beta-components/shared/core/agent/schema.ts +180 -0
  69. package/source/beta-components/shared/core/agent/types.ts +169 -0
  70. package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
  71. package/source/beta-components/shared/index.ts +9 -0
  72. package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
  73. package/source/beta-components/shared/react/useAskAgent.ts +217 -0
  74. package/source/beta-components/shared/styles.css +79 -0
  75. package/source/beta-components/shared/version.json +4 -4
  76. package/source/beta-components/shared/version.ts +6 -6
  77. package/source/beta-components/skeleton/passport.json +251 -0
  78. package/source/beta-components/skeleton/styles.css +185 -187
  79. package/source/beta-components/spinner/passport.json +245 -0
  80. package/source/beta-components/spinner/styles.css +173 -174
  81. package/source/beta-components/switch/passport.json +421 -0
  82. package/source/beta-components/switch/styles.css +227 -229
  83. package/source/beta-components/tabs/passport.json +315 -0
  84. package/source/beta-components/tabs/styles.css +263 -264
  85. package/source/beta-components/textarea/passport.json +382 -0
  86. package/source/beta-components/textarea/styles.css +158 -160
  87. package/source/beta-components/toaster/passport.json +221 -0
  88. package/source/beta-components/toaster/styles.css +282 -282
  89. package/source/beta-components/tooltip/passport.json +170 -0
  90. package/source/beta-components/tooltip/styles.css +71 -73
  91. package/tools/env.cjs +61 -0
  92. package/tools/passport/cli.cjs +79 -0
  93. package/tools/passport/extract.cjs +493 -0
  94. package/tools/passport/index.cjs +185 -0
  95. package/tools/passport/merge.cjs +131 -0
  96. package/tools/passport/policy.cjs +65 -0
  97. package/tools/passport/validate.cjs +277 -0
  98. package/tools/ts-require.cjs +79 -0
  99. package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
  100. package/source/beta-components/alert/__tests__/core.test.ts +0 -17
  101. package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
  102. package/source/beta-components/badge/__tests__/core.test.ts +0 -46
  103. package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
  104. package/source/beta-components/button/__tests__/core.test.ts +0 -31
  105. package/source/beta-components/card/__tests__/core.test.ts +0 -57
  106. package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
  107. package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
  108. package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
  109. package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
  110. package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
  111. package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
  112. package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
  113. package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
  114. package/source/beta-components/input/__tests__/core.test.ts +0 -75
  115. package/source/beta-components/menu/__tests__/core.test.ts +0 -120
  116. package/source/beta-components/modal/__tests__/core.test.ts +0 -55
  117. package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
  118. package/source/beta-components/progress/__tests__/core.test.ts +0 -56
  119. package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
  120. package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
  121. package/source/beta-components/shared/__tests__/core.test.ts +0 -55
  122. package/source/beta-components/shared/__tests__/position.test.ts +0 -143
  123. package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
  124. package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
  125. package/source/beta-components/switch/__tests__/core.test.ts +0 -64
  126. package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
  127. package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
  128. package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
  129. package/source/beta-components/tooltip/__tests__/core.test.ts +0 -42
@@ -0,0 +1,493 @@
1
+ /*
2
+ * Passport extraction: component source -> derived passport fields.
3
+ *
4
+ * Uses the TypeScript TypeChecker, not regex, so `extends` chains resolve. The
5
+ * central rule is ORIGIN FILTERING:
6
+ *
7
+ * own / shared source -> flatten and enumerate (GridOptions, ComboboxSharedProps…)
8
+ * library source -> keep as an `inherits` edge, plus a policy allowlist
9
+ *
10
+ * Measured on this repo: full flattening yields 6,232 props (Tabs alone 853);
11
+ * AST-only yields 633 and misses half the grid's API. Origin filtering gives
12
+ * the real public surface.
13
+ */
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+ const crypto = require("crypto");
18
+ const { allowlistFor } = require("./policy.cjs");
19
+
20
+ const PASSPORT_VERSION = "1.0.0";
21
+
22
+ /**
23
+ * Resolve the TypeScript compiler API.
24
+ *
25
+ * Generation-time only — never needed to USE a component, so this stays out of
26
+ * the runtime dependency story. The caller's project is searched first, so a
27
+ * consumer regenerating a forked component uses their own TypeScript.
28
+ */
29
+ function loadTypeScript(searchFrom = []) {
30
+ const paths = [...searchFrom, process.cwd(), __dirname].filter(Boolean);
31
+ try {
32
+ return require(require.resolve("typescript", { paths }));
33
+ } catch {
34
+ try {
35
+ return require("typescript");
36
+ } catch {
37
+ const error = new Error(
38
+ "Passport generation needs the TypeScript compiler API.\n" +
39
+ " Install it as a dev dependency: npm i -D typescript\n" +
40
+ " It is only needed to generate passports, never to run a component.",
41
+ );
42
+ error.code = "ETYPESCRIPT_MISSING";
43
+ throw error;
44
+ }
45
+ }
46
+ }
47
+
48
+ const posix = (p) => p.replace(/\\/g, "/");
49
+
50
+ /** Every .ts/.tsx under a directory, excluding tests. */
51
+ function sourceFiles(dir) {
52
+ const out = [];
53
+ const walk = (d) => {
54
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
55
+ if (e.name === "__tests__") continue;
56
+ const p = path.join(d, e.name);
57
+ if (e.isDirectory()) walk(p);
58
+ else if (/\.tsx?$/.test(e.name)) out.push(p);
59
+ }
60
+ };
61
+ walk(dir);
62
+ return out.sort();
63
+ }
64
+
65
+ /**
66
+ * Hash of the inputs the passport is derived from. Deterministic: sorted paths,
67
+ * file contents only, no timestamps.
68
+ */
69
+ function hashSources(files, root) {
70
+ const h = crypto.createHash("sha256");
71
+ for (const f of [...files].sort()) {
72
+ h.update(posix(path.relative(root, f)));
73
+ h.update("\0");
74
+ h.update(fs.readFileSync(f));
75
+ h.update("\0");
76
+ }
77
+ return `sha256:${h.digest("hex")}`;
78
+ }
79
+
80
+ /* ------------------------------------------------------------------ program */
81
+
82
+ /**
83
+ * One program across every component, so cross-file project-local types
84
+ * (GridOptions, ComboboxSharedProps) resolve properly.
85
+ */
86
+ function createProgram(ts, libRoot, components, reactTypesDir) {
87
+ const entries = components
88
+ .map((c) => path.join(libRoot, c, "index.ts"))
89
+ .filter((f) => fs.existsSync(f));
90
+
91
+ const options = {
92
+ jsx: ts.JsxEmit.ReactJSX,
93
+ target: ts.ScriptTarget.ES2022,
94
+ module: ts.ModuleKind.ESNext,
95
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
96
+ strict: true,
97
+ skipLibCheck: true,
98
+ noEmit: true,
99
+ };
100
+ if (reactTypesDir) {
101
+ options.baseUrl = path.dirname(path.dirname(reactTypesDir));
102
+ options.paths = {
103
+ react: [reactTypesDir],
104
+ "react/*": [`${reactTypesDir}/*`],
105
+ "react-dom": [reactTypesDir.replace(/react$/, "react-dom")],
106
+ };
107
+ }
108
+ return ts.createProgram(entries, options);
109
+ }
110
+
111
+ /** Where a declaration lives, which decides flatten-vs-edge. */
112
+ function originOfDeclaration(decl, libRoot) {
113
+ if (!decl) return "unknown";
114
+ const file = posix(decl.getSourceFile().fileName);
115
+ if (file.includes("/node_modules/") || /\/lib\.[a-z0-9.]+\.d\.ts$/.test(file)) return "inherited";
116
+ if (file.includes(`${posix(libRoot)}/shared/`)) return "shared";
117
+ if (file.startsWith(posix(libRoot))) return "own";
118
+ return "inherited";
119
+ }
120
+
121
+ /* --------------------------------------------------------------- type shape */
122
+
123
+ /** A compact, stable description of a property's type. */
124
+ function describeType(ts, checker, type, node) {
125
+ const text = checker.typeToString(
126
+ type,
127
+ node,
128
+ ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType,
129
+ );
130
+
131
+ // Optional props are `T | undefined`. Classify on T, or every optional
132
+ // callback reads as an anonymous union rather than a function.
133
+ const NULLISH = ts.TypeFlags.Undefined | ts.TypeFlags.Null;
134
+ if (type.isUnion()) {
135
+ const defined = type.types.filter((t) => !(t.flags & NULLISH));
136
+ if (defined.length === 1) type = defined[0];
137
+ }
138
+
139
+ if (type.isUnion()) {
140
+ const parts = type.types;
141
+ const literals = parts.filter((t) => t.isStringLiteral());
142
+ const nonLiteral = parts.filter(
143
+ (t) => !t.isStringLiteral() && !(t.flags & ts.TypeFlags.Undefined),
144
+ );
145
+ // A pure union of string literals is an enum worth enumerating.
146
+ if (literals.length >= 2 && nonLiteral.length === 0) {
147
+ return { type: "enum", values: literals.map((t) => t.value).sort(), typeText: text };
148
+ }
149
+ const boolish = parts.every(
150
+ (t) => t.flags & (ts.TypeFlags.BooleanLike | ts.TypeFlags.Undefined),
151
+ );
152
+ if (boolish) return { type: "boolean", typeText: text };
153
+ }
154
+
155
+ if (checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0) {
156
+ return { type: "function", signature: text.length > 200 ? `${text.slice(0, 197)}…` : text };
157
+ }
158
+ if (type.flags & ts.TypeFlags.StringLike) return { type: "string", typeText: text };
159
+ if (type.flags & ts.TypeFlags.NumberLike) return { type: "number", typeText: text };
160
+ if (type.flags & ts.TypeFlags.BooleanLike) return { type: "boolean", typeText: text };
161
+
162
+ return { type: "other", typeText: text.length > 200 ? `${text.slice(0, 197)}…` : text };
163
+ }
164
+
165
+ /** First JSDoc sentence attached to a symbol, or undefined. Never invented. */
166
+ function jsDocOf(ts, sym) {
167
+ const parts = sym.getDocumentationComment(undefined);
168
+ if (!parts || parts.length === 0) return undefined;
169
+ const text = ts.displayPartsToString(parts).trim();
170
+ return text || undefined;
171
+ }
172
+
173
+ /* ------------------------------------------------------ defaults from source */
174
+
175
+ /**
176
+ * Defaults are written as destructuring defaults in the component function
177
+ * (`size = "md"`), which is the only place they exist. Best-effort and marked
178
+ * as derived; absent when not found.
179
+ */
180
+ /** `"md"` -> md, `false` -> false, `12` -> 12; anything long or complex is dropped. */
181
+ function normaliseDefault(text) {
182
+ if (text.length > 40) return undefined;
183
+ const quoted = text.match(/^["'`](.*)["'`]$/s);
184
+ if (quoted) return quoted[1];
185
+ if (text === "true") return true;
186
+ if (text === "false") return false;
187
+ if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
188
+ if (text === "null") return null;
189
+ return text;
190
+ }
191
+
192
+ function collectDefaults(ts, sourceFileList, program) {
193
+ const defaults = new Map(); // "Component.prop" -> literal text
194
+ for (const file of sourceFileList) {
195
+ const sf = program.getSourceFile(file);
196
+ if (!sf) continue;
197
+ sf.forEachChild((node) => {
198
+ if (!ts.isFunctionDeclaration(node) || !node.name) return;
199
+ const comp = node.name.text;
200
+ for (const param of node.parameters) {
201
+ if (!ts.isObjectBindingPattern(param.name)) continue;
202
+ for (const el of param.name.elements) {
203
+ if (!el.initializer || !ts.isIdentifier(el.name)) continue;
204
+ const init = normaliseDefault(el.initializer.getText(sf));
205
+ if (init !== undefined) defaults.set(`${comp}.${el.name.text}`, init);
206
+ }
207
+ }
208
+ // `const { a = 1 } = props` inside the body
209
+ if (node.body) {
210
+ for (const stmt of node.body.statements) {
211
+ if (!ts.isVariableStatement(stmt)) continue;
212
+ for (const d of stmt.declarationList.declarations) {
213
+ if (!ts.isObjectBindingPattern(d.name)) continue;
214
+ for (const el of d.name.elements) {
215
+ if (!el.initializer || !ts.isIdentifier(el.name)) continue;
216
+ const init = normaliseDefault(el.initializer.getText(sf));
217
+ if (init !== undefined) defaults.set(`${comp}.${el.name.text}`, init);
218
+ }
219
+ }
220
+ }
221
+ }
222
+ });
223
+ }
224
+ return defaults;
225
+ }
226
+
227
+ /* ------------------------------------------------------------ slots & states */
228
+
229
+ function collectSlots(ts, files, program) {
230
+ const slots = [];
231
+ for (const file of files) {
232
+ const sf = program.getSourceFile(file);
233
+ if (!sf) continue;
234
+ sf.forEachChild((node) => {
235
+ if (!ts.isTypeAliasDeclaration(node)) return;
236
+ if (!/Slot$/.test(node.name.text)) return;
237
+ const text = node.type.getText(sf);
238
+ for (const m of text.matchAll(/"([a-zA-Z][\w-]*)"/g)) {
239
+ if (!slots.includes(m[1])) slots.push(m[1]); // declaration order is meaningful
240
+ }
241
+ });
242
+ }
243
+ return slots;
244
+ }
245
+
246
+ /** `data-*` attributes, from the stylesheet's selectors and the JSX that sets them. */
247
+ function collectStates(dir) {
248
+ const found = new Map(); // attr -> Set(values)
249
+ const add = (attr, value) => {
250
+ if (!found.has(attr)) found.set(attr, new Set());
251
+ if (value) found.get(attr).add(value);
252
+ };
253
+
254
+ const css = path.join(dir, "styles.css");
255
+ if (fs.existsSync(css)) {
256
+ const text = fs.readFileSync(css, "utf8");
257
+ for (const m of text.matchAll(/\[(data-[a-z-]+)(?:=["']([^"']+)["'])?\]/g)) add(m[1], m[2]);
258
+ }
259
+ for (const f of sourceFiles(dir)) {
260
+ const text = fs.readFileSync(f, "utf8");
261
+ for (const m of text.matchAll(/["']?(data-[a-z-]+)["']?\s*[=:]\s*["']?([a-z-]+)?["']?/g))
262
+ add(m[1], /^[a-z-]+$/.test(m[2] ?? "") ? m[2] : undefined);
263
+ }
264
+
265
+ return [...found.entries()]
266
+ .map(([attr, values]) => ({
267
+ attr,
268
+ ...(values.size ? { values: [...values].sort() } : {}),
269
+ }))
270
+ .sort((a, b) => a.attr.localeCompare(b.attr));
271
+ }
272
+
273
+ /* ------------------------------------------------------------------ extract */
274
+
275
+ /**
276
+ * Derive the generated half of one component's passport.
277
+ * @returns {{passport: object, stats: object}}
278
+ */
279
+ /*
280
+ * The component's imperative surface: the members of any exported interface
281
+ * named `*Api` or `*Handle` (GridApi, DatePickerHandle, ComboboxHandle). That
282
+ * is the thing a `ref` hands back, and so the only set an operation's
283
+ * `apiMethod` may name.
284
+ *
285
+ * Collected but not published: v1's schema is out in the world, and an
286
+ * operation naming a method that does not exist is caught either way.
287
+ */
288
+ function collectApi(ts, checker, exported, deAlias) {
289
+ const names = new Set();
290
+ for (const raw of exported) {
291
+ if (!/(?:Api|Handle)$/.test(raw.name)) continue;
292
+ const sym = deAlias(raw);
293
+ const decl = (sym.declarations ?? []).find(ts.isInterfaceDeclaration);
294
+ if (!decl) continue;
295
+ for (const member of checker.getPropertiesOfType(checker.getDeclaredTypeOfSymbol(sym))) {
296
+ names.add(member.name);
297
+ }
298
+ }
299
+ return [...names].sort();
300
+ }
301
+
302
+ function extractComponent(ts, program, checker, { name, dir, libRoot, libraryVersion, repoRoot }) {
303
+ const files = sourceFiles(dir);
304
+ const indexFile = path.join(dir, "index.ts");
305
+ const sf = program.getSourceFile(indexFile);
306
+ if (!sf) throw new Error(`${name}: index.ts did not resolve in the program`);
307
+
308
+ const moduleSymbol = checker.getSymbolAtLocation(sf);
309
+ if (!moduleSymbol) throw new Error(`${name}: no module symbol for index.ts`);
310
+
311
+ const deAlias = (s) => (s.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s);
312
+ const exported = checker.getExportsOfModule(moduleSymbol);
313
+
314
+ const defaults = collectDefaults(ts, files, program);
315
+ const api = collectApi(ts, checker, exported, deAlias);
316
+
317
+ // Pass 1: which exports are actually components?
318
+ const componentExports = [];
319
+ for (const raw of exported) {
320
+ const decls = deAlias(raw).declarations ?? [];
321
+ if (
322
+ /^[A-Z]/.test(raw.name) &&
323
+ decls.some((d) => ts.isFunctionDeclaration(d) || ts.isVariableDeclaration(d))
324
+ ) {
325
+ componentExports.push(raw.name);
326
+ }
327
+ }
328
+
329
+ /*
330
+ * Only `<Component>Props` describes a component's public props. Other
331
+ * `*Props` exports are callback payloads — ButtonRenderProps is the argument
332
+ * to `render`, not something you pass to <Button> — and folding them in
333
+ * corrupts the prop list (it made `className` look required).
334
+ */
335
+ const componentPropsNames = new Set(componentExports.map((c) => `${c}Props`));
336
+
337
+ const props = new Map(); // name -> entry (first declaration wins)
338
+ const externalBases = new Map(); // baseName -> { from, omitted }
339
+ let inheritedConfirmed = 0;
340
+
341
+ for (const raw of exported) {
342
+ const sym = deAlias(raw);
343
+ if (!componentPropsNames.has(raw.name)) continue;
344
+ const decl = (sym.declarations ?? []).find(ts.isInterfaceDeclaration);
345
+ if (!decl) continue;
346
+
347
+ // Record external base types as an inheritance edge rather than flattening.
348
+ for (const clause of decl.heritageClauses ?? []) {
349
+ for (const typeNode of clause.types) {
350
+ const text = typeNode.getText(decl.getSourceFile());
351
+ const omitMatch = text.match(/^Omit<\s*([^,]+),\s*(.+)>$/s);
352
+ const inner = omitMatch ? omitMatch[1].trim() : text;
353
+ const omitted = omitMatch
354
+ ? [...omitMatch[2].matchAll(/"([^"]+)"/g)].map((m) => m[1]).sort()
355
+ : [];
356
+ const baseName = inner.replace(/<.*$/s, "").trim();
357
+
358
+ const baseType = checker.getTypeAtLocation(typeNode.expression ?? typeNode);
359
+ const baseDecl = baseType?.getSymbol?.()?.declarations?.[0];
360
+ const origin = originOfDeclaration(baseDecl, libRoot);
361
+ if (origin === "inherited") {
362
+ externalBases.set(baseName, { from: inner, omitted, passthrough: true });
363
+ }
364
+ }
365
+ }
366
+
367
+ const type = checker.getDeclaredTypeOfSymbol(sym);
368
+ for (const p of checker.getPropertiesOfType(type)) {
369
+ if (props.has(p.name)) continue;
370
+ const pDecl = p.declarations?.[0];
371
+ const origin = originOfDeclaration(pDecl, libRoot);
372
+ if (origin === "inherited") continue; // handled by the allowlist pass below
373
+
374
+ const pType = checker.getTypeOfSymbolAtLocation(p, pDecl ?? decl);
375
+ const shape = describeType(ts, checker, pType, pDecl ?? decl);
376
+ const description = jsDocOf(ts, p);
377
+ const required = !(p.flags & ts.SymbolFlags.Optional);
378
+
379
+ let def;
380
+ for (const comp of componentExports) {
381
+ if (defaults.has(`${comp}.${p.name}`)) { def = defaults.get(`${comp}.${p.name}`); break; }
382
+ }
383
+ if (def === undefined && defaults.has(`${name}.${p.name}`)) def = defaults.get(`${name}.${p.name}`);
384
+
385
+ props.set(p.name, {
386
+ name: p.name,
387
+ ...shape,
388
+ required,
389
+ ...(def !== undefined ? { default: def } : {}),
390
+ origin: origin === "shared" ? "derived" : "derived",
391
+ ...(description ? { description, descriptionOrigin: "jsdoc" } : {}),
392
+ });
393
+ }
394
+ }
395
+
396
+ /* --- inherited allowlist: only what the checker confirms actually exists --- */
397
+ const baseNames = [...externalBases.keys()];
398
+ if (baseNames.length) {
399
+ const wanted = allowlistFor(baseNames);
400
+ for (const raw of exported) {
401
+ if (!componentPropsNames.has(raw.name)) continue;
402
+ const sym = deAlias(raw);
403
+ const decl = (sym.declarations ?? []).find(ts.isInterfaceDeclaration);
404
+ if (!decl) continue;
405
+ const type = checker.getDeclaredTypeOfSymbol(sym);
406
+
407
+ for (const wantedName of wanted) {
408
+ if (props.has(wantedName)) continue;
409
+ const p = checker.getPropertyOfType(type, wantedName);
410
+ if (!p) continue; // not present -> never emitted
411
+ const pDecl = p.declarations?.[0];
412
+ if (originOfDeclaration(pDecl, libRoot) !== "inherited") continue;
413
+
414
+ const pType = checker.getTypeOfSymbolAtLocation(p, pDecl ?? decl);
415
+ const shape = describeType(ts, checker, pType, pDecl ?? decl);
416
+ props.set(wantedName, {
417
+ name: wantedName,
418
+ ...shape,
419
+ required: !(p.flags & ts.SymbolFlags.Optional),
420
+ origin: "inherited",
421
+ });
422
+ inheritedConfirmed++;
423
+ }
424
+ }
425
+ }
426
+
427
+ const propList = [...props.values()].sort((a, b) => a.name.localeCompare(b.name));
428
+ const events = propList
429
+ .filter((p) => /^on[A-Z]/.test(p.name))
430
+ .map((p) => ({ name: p.name, ...(p.signature ? { signature: p.signature } : {}) }));
431
+
432
+ const described = propList.filter((p) => p.description).length;
433
+
434
+ const passport = {
435
+ passportVersion: PASSPORT_VERSION,
436
+ identity: {
437
+ component: componentExports[0] ?? name,
438
+ folder: name,
439
+ exports: componentExports.sort(),
440
+ },
441
+ source: {
442
+ libraryVersion,
443
+ files: files.map((f) => posix(path.relative(dir, f))),
444
+ sourceHash: hashSources([...files, ...(fs.existsSync(path.join(dir, "styles.css")) ? [path.join(dir, "styles.css")] : [])], repoRoot),
445
+ },
446
+ props: propList,
447
+ ...(externalBases.size
448
+ ? { inherits: [...externalBases.values()].sort((a, b) => a.from.localeCompare(b.from)) }
449
+ : {}),
450
+ events,
451
+ slots: collectSlots(ts, files, program),
452
+ states: collectStates(dir),
453
+ accessibility: { supplied: [], required: [] },
454
+ composition: {},
455
+ operations: {},
456
+ intentDomains: [],
457
+ safeMutations: {},
458
+ examples: [],
459
+ coverage: {
460
+ propsTotal: propList.length,
461
+ propsDescribed: described,
462
+ describedPct: propList.length ? Math.round((described / propList.length) * 100) : 0,
463
+ authoredSections: [],
464
+ },
465
+ };
466
+
467
+ return {
468
+ passport,
469
+ api,
470
+ stats: {
471
+ component: name,
472
+ props: propList.length,
473
+ own: propList.filter((p) => p.origin === "derived").length,
474
+ inherited: inheritedConfirmed,
475
+ inheritsEdges: externalBases.size,
476
+ events: events.length,
477
+ slots: passport.slots.length,
478
+ states: passport.states.length,
479
+ api: api.length,
480
+ described,
481
+ },
482
+ };
483
+ }
484
+
485
+ module.exports = {
486
+ PASSPORT_VERSION,
487
+ loadTypeScript,
488
+ createProgram,
489
+ extractComponent,
490
+ sourceFiles,
491
+ hashSources,
492
+ posix,
493
+ };
@@ -0,0 +1,185 @@
1
+ /*
2
+ * Passport orchestration: generate, check, regen.
3
+ *
4
+ * `generate` and `regen` write passport.json. `check` writes nothing and exits
5
+ * non-zero on drift — that is the CI gate.
6
+ */
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+
11
+ const {
12
+ PASSPORT_VERSION, loadTypeScript, createProgram, extractComponent, posix,
13
+ } = require("./extract.cjs");
14
+ const { mergePassport } = require("./merge.cjs");
15
+ const {
16
+ validateStructure, validateReferences, coverageIssues, textIssues,
17
+ } = require("./validate.cjs");
18
+
19
+ const MANUAL_FILE = "passport.manual.json";
20
+ const LOCAL_FILE = "passport.local.json";
21
+ const PASSPORT_FILE = "passport.json";
22
+
23
+ const readJson = (file) => {
24
+ if (!fs.existsSync(file)) return {};
25
+ try {
26
+ return JSON.parse(fs.readFileSync(file, "utf8"));
27
+ } catch (error) {
28
+ const e = new Error(`${file} is not valid JSON: ${error.message}`);
29
+ e.code = "EBADJSON";
30
+ throw e;
31
+ }
32
+ };
33
+
34
+ /** Stable key order + trailing newline, so writes are byte-identical run to run. */
35
+ function serialise(passport) {
36
+ const ORDER = [
37
+ "passportVersion", "identity", "source", "purpose", "props", "inherits", "events",
38
+ "slots", "states", "accessibility", "composition", "operations", "intentDomains",
39
+ "safeMutations", "examples", "coverage",
40
+ ];
41
+ const ordered = {};
42
+ for (const key of ORDER) if (passport[key] !== undefined) ordered[key] = passport[key];
43
+ for (const key of Object.keys(passport)) if (!(key in ordered)) ordered[key] = passport[key];
44
+ return `${JSON.stringify(ordered, null, 2)}\n`;
45
+ }
46
+
47
+ /** Components in a beta-components root, excluding `shared`. */
48
+ function listComponents(libRoot) {
49
+ return fs
50
+ .readdirSync(libRoot, { withFileTypes: true })
51
+ .filter((e) => e.isDirectory() && e.name !== "shared")
52
+ .map((e) => e.name)
53
+ .sort();
54
+ }
55
+
56
+ /** Best-effort locate React's types so the checker can resolve DOM attributes. */
57
+ function findReactTypes(startDir) {
58
+ let dir = startDir;
59
+ for (let i = 0; i < 6; i++) {
60
+ const candidate = path.join(dir, "node_modules", "@types", "react");
61
+ if (fs.existsSync(candidate)) return posix(candidate);
62
+ const nested = path.join(dir, "demo-showroom", "node_modules", "@types", "react");
63
+ if (fs.existsSync(nested)) return posix(nested);
64
+ const parent = path.dirname(dir);
65
+ if (parent === dir) break;
66
+ dir = parent;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ /**
72
+ * Build the effective passport for every component.
73
+ *
74
+ * @param {object} opts
75
+ * @param {string} opts.libRoot directory holding the component folders
76
+ * @param {string} opts.repoRoot for stable relative paths in the hash
77
+ * @param {string} opts.libraryVersion
78
+ * @param {boolean} [opts.includeLocal] read passport.local.json (consumer side)
79
+ * @param {number} [opts.coverageThreshold]
80
+ */
81
+ function buildAll(opts) {
82
+ const { libRoot, repoRoot, libraryVersion, includeLocal = false, coverageThreshold } = opts;
83
+ const ts = loadTypeScript([repoRoot, path.join(repoRoot, "demo-showroom")]);
84
+ const components = listComponents(libRoot);
85
+ const reactTypes = findReactTypes(repoRoot) || findReactTypes(__dirname);
86
+ const program = createProgram(ts, libRoot, components, reactTypes);
87
+ const checker = program.getTypeChecker();
88
+
89
+ const results = [];
90
+ for (const name of components) {
91
+ const dir = path.join(libRoot, name);
92
+ const { passport: derived, api, stats } = extractComponent(ts, program, checker, {
93
+ name, dir, libRoot, libraryVersion, repoRoot,
94
+ });
95
+
96
+ const manual = readJson(path.join(dir, MANUAL_FILE));
97
+ const local = includeLocal ? readJson(path.join(dir, LOCAL_FILE)) : {};
98
+ const effective = mergePassport(derived, manual, local);
99
+
100
+ /*
101
+ * Without React's type definitions the checker cannot resolve
102
+ * ButtonHTMLAttributes and friends, so every inherited prop silently
103
+ * disappears — and the reference check then blames the manual file for
104
+ * naming props that "no longer exist". Say what actually happened.
105
+ */
106
+ const unresolvedInherits =
107
+ !reactTypes && Array.isArray(effective.inherits) && effective.inherits.length > 0;
108
+
109
+ const issues = [
110
+ ...validateStructure(effective, name),
111
+ ...(unresolvedInherits
112
+ ? [{
113
+ level: "warn",
114
+ code: "react-types-unresolved",
115
+ component: name,
116
+ message:
117
+ "React type definitions were not found, so inherited props could not be " +
118
+ "resolved. Install @types/react to get the full prop surface.",
119
+ }]
120
+ : validateReferences(effective, { manual, local, component: name, api })),
121
+ ...textIssues(effective, name),
122
+ ...coverageIssues(effective, name, coverageThreshold),
123
+ ];
124
+
125
+ results.push({ name, dir, derived, manual, local, effective, issues, stats, api });
126
+ }
127
+ return { results, reactTypes, components };
128
+ }
129
+
130
+ /** Write passport.json for each component. Returns the paths written. */
131
+ function generate(opts) {
132
+ const { results } = buildAll(opts);
133
+ const written = [];
134
+ for (const r of results) {
135
+ const file = path.join(r.dir, PASSPORT_FILE);
136
+ const next = serialise(r.effective);
137
+ const prev = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
138
+ if (prev !== next) {
139
+ fs.writeFileSync(file, next);
140
+ written.push(posix(path.relative(opts.repoRoot, file)));
141
+ }
142
+ }
143
+ return { results, written };
144
+ }
145
+
146
+ /**
147
+ * CI gate. Regenerates in memory and compares with what is committed.
148
+ * Never writes.
149
+ */
150
+ function check(opts) {
151
+ const { results } = buildAll(opts);
152
+ const report = [];
153
+ let errors = 0;
154
+ let warnings = 0;
155
+
156
+ for (const r of results) {
157
+ const file = path.join(r.dir, PASSPORT_FILE);
158
+ const expected = serialise(r.effective);
159
+ const actual = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
160
+
161
+ const problems = [...r.issues];
162
+ if (actual === null) {
163
+ problems.unshift({ level: "error", code: "missing-passport", component: r.name,
164
+ message: "no passport.json committed; run `npm run passport`" });
165
+ } else if (actual !== expected) {
166
+ const reason =
167
+ JSON.parse(actual).source?.sourceHash !== r.effective.source.sourceHash
168
+ ? "source changed since the passport was generated"
169
+ : "committed passport differs from generated output";
170
+ problems.unshift({ level: "error", code: "stale-passport", component: r.name,
171
+ message: `passport stale — ${reason}; run \`npm run passport\`` });
172
+ }
173
+
174
+ errors += problems.filter((p) => p.level === "error").length;
175
+ warnings += problems.filter((p) => p.level === "warn").length;
176
+ report.push({ component: r.name, ok: problems.every((p) => p.level !== "error"), problems, stats: r.stats });
177
+ }
178
+
179
+ return { report, errors, warnings, results };
180
+ }
181
+
182
+ module.exports = {
183
+ PASSPORT_VERSION, MANUAL_FILE, LOCAL_FILE, PASSPORT_FILE,
184
+ buildAll, generate, check, serialise, listComponents, readJson, findReactTypes,
185
+ };