@genesislcap/ts-builder 15.19.0 → 15.19.1

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.
@@ -1,1309 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
3
- import { dirname, resolve } from 'node:path';
4
- import consola from 'consola';
5
-
6
- // ── CEM type shapes ──────────────────────────────────────────────────────────
7
-
8
- type CEMType = { text?: string };
9
- type CEMEvent = { name?: string; type?: CEMType; description?: string };
10
- type CEMSuperclass = { name?: string; package?: string };
11
- type CEMMember = {
12
- name?: string;
13
- fieldName?: string;
14
- attribute?: string | null;
15
- kind?: string;
16
- privacy?: string;
17
- type?: CEMType;
18
- };
19
- type CEMDeclaration = {
20
- name?: string;
21
- customElement?: boolean;
22
- tagName?: string;
23
- superclass?: CEMSuperclass;
24
- members?: CEMMember[];
25
- attributes?: CEMMember[];
26
- events?: CEMEvent[];
27
- };
28
- type CEMModule = { path?: string; declarations?: CEMDeclaration[] };
29
- type CEMManifest = { modules?: CEMModule[] };
30
- type CEMElementEntry = { declaration: CEMDeclaration; modulePath: string };
31
-
32
- // ── Type import state ────────────────────────────────────────────────────────
33
-
34
- type TypeImportState = {
35
- importsByIdentifier: Map<string, string>;
36
- ambiguousIdentifiers: Set<string>;
37
- usedImports: Map<string, Set<string>>;
38
- wildcardExportModules: Set<string>;
39
- };
40
-
41
- // ── Prop type overrides ──────────────────────────────────────────────────────
42
- //
43
- // WHY THIS EXISTS. Wrapper props are derived from the element CLASS via
44
- // `Omit<PublicOf<XWC>, …>`, so a prop's type is whatever TypeScript resolves up the inheritance
45
- // chain. That is normally exactly right — but it breaks when a design system RESTYLES a prop whose
46
- // type is a closed union owned by a base package. The reported case: rapid-design-system's
47
- // `button.styles.ts` styles 12 `appearance` values (primary, secondary, danger, …) while the
48
- // inherited FAST type is `ButtonAppearance = 'accent' | 'lightweight' | 'neutral' | 'outline' |
49
- // 'stealth'`. Consumers writing `appearance="danger"` get TS2322 for a value that renders
50
- // correctly, and the three FAST-only values type-check while rendering unstyled.
51
- //
52
- // It CANNOT be fixed on the component. TypeScript requires a derived property to be assignable to
53
- // the base's, so widening an inherited property is rejected — verified for all three candidate
54
- // forms: `declare appearance: RapidButtonAppearance` and `declare appearance: string` both give
55
- // TS2416, and class/interface declaration merging gives TS2415 + TS2430. (`foundation-zero` exports
56
- // a `ZeroButtonAppearance` union but never wires it to the class, which is why that precedent does
57
- // not actually work; `SearchBar` gets away with `appearance: string` only because its own base has
58
- // no `appearance`.) The wrapper boundary is therefore the only place the public prop type can be
59
- // corrected.
60
- //
61
- // Opt-in per package via package.json — same convention as the existing `customElements` field:
62
- //
63
- // "reactWrapperPropTypes": {
64
- // "Button": { "appearance": { "type": "RapidButtonAppearance", "from": "./button/button.styles" } }
65
- // }
66
- //
67
- // A configured prop is removed from the `PublicOf` mapping and re-declared as optional with the
68
- // named type, which is imported into react.d.ts. Packages that configure nothing are byte-for-byte
69
- // unchanged, so this is inert for every wrapper that does not opt in.
70
- type PropTypeOverride = { type: string; from: string };
71
- type PropTypeOverrides = Record<string, Record<string, PropTypeOverride>>;
72
-
73
- /** A bare TS identifier — `type` is emitted into a `.d.ts` verbatim. */
74
- const OVERRIDE_TYPE_RE = /^[A-Za-z_$][\w$]*$/;
75
- /** A safe module specifier — `from` is emitted inside single quotes (`from '<spec>';`), so it must
76
- * not be able to close the quote and inject further declarations. Covers relative paths and
77
- * scoped package names; rejects quotes, semicolons and whitespace. */
78
- const OVERRIDE_FROM_RE = /^[.\w@/-]+$/;
79
- /** A safe prop name. Also emitted into the `.d.ts` — both quoted in the `Omit` key list and as the
80
- * re-declared key — so it carries the same injection risk as `from`. Hyphens are allowed because
81
- * React props legitimately include `aria-*` and `data-*`; quotes, pipes and whitespace are not.
82
- * Without this, a name like `appearance' | 'disabled` widened the `Omit` to two props AND emitted
83
- * a union as a key, which is not valid in a type literal. */
84
- const OVERRIDE_PROP_RE = /^[A-Za-z_$][\w$-]*$/;
85
-
86
- /**
87
- * Read + validate the `reactWrapperPropTypes` map.
88
- *
89
- * Malformed entries are skipped rather than thrown: bad config must never abort a build — the
90
- * wrapper still generates, just without that override. But skipping SILENTLY makes a typo
91
- * undiagnosable, so every skip is reported with the component, prop and reason.
92
- */
93
- function readPropTypeOverrides(packageJson: Record<string, unknown>): PropTypeOverrides {
94
- const raw = packageJson.reactWrapperPropTypes;
95
- if (raw === undefined) return {};
96
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
97
- consola.warn('[ts-builder] Ignoring "reactWrapperPropTypes": expected an object.');
98
- return {};
99
- }
100
- const out: PropTypeOverrides = {};
101
- for (const [component, props] of Object.entries(raw as Record<string, unknown>)) {
102
- if (!props || typeof props !== 'object' || Array.isArray(props)) {
103
- consola.warn(
104
- `[ts-builder] Ignoring reactWrapperPropTypes.${component}: expected an object of prop → { type, from }.`,
105
- );
106
- continue;
107
- }
108
- const byProp: Record<string, PropTypeOverride> = {};
109
- for (const [prop, spec] of Object.entries(props as Record<string, unknown>)) {
110
- const s = spec as Partial<PropTypeOverride> | undefined;
111
- const where = `reactWrapperPropTypes.${component}.${prop}`;
112
- if (!OVERRIDE_PROP_RE.test(prop)) {
113
- consola.warn(
114
- `[ts-builder] Skipping ${where}: prop name must match ${OVERRIDE_PROP_RE} (letters, digits, _, $, -).`,
115
- );
116
- continue;
117
- }
118
- if (!s || typeof s.type !== 'string' || !OVERRIDE_TYPE_RE.test(s.type)) {
119
- consola.warn(
120
- `[ts-builder] Skipping ${where}: "type" must be a bare TypeScript identifier (got ${JSON.stringify(s?.type)}).`,
121
- );
122
- continue;
123
- }
124
- if (typeof s.from !== 'string' || !OVERRIDE_FROM_RE.test(s.from.trim())) {
125
- consola.warn(
126
- `[ts-builder] Skipping ${where}: "from" must be a module specifier matching ${OVERRIDE_FROM_RE} (got ${JSON.stringify(s.from)}).`,
127
- );
128
- continue;
129
- }
130
- byProp[prop] = { type: s.type, from: s.from.trim() };
131
- }
132
- if (Object.keys(byProp).length > 0) out[component] = byProp;
133
- }
134
- return out;
135
- }
136
-
137
- // ── Public API return type ───────────────────────────────────────────────────
138
-
139
- type GenerateResult = { generated: true; path: string } | { generated: false; reason: string };
140
-
141
- // ── Constants ────────────────────────────────────────────────────────────────
142
-
143
- const PRIMITIVE_UNION_REGEX =
144
- /^(?:\s*(?:string|number|boolean|bigint|null|undefined|unknown|any|void|'[^']*'|"[^"]*"|`[^`]*`|(?:\d+(?:\.\d+)?))\s*)(?:\|\s*(?:string|number|boolean|bigint|null|undefined|unknown|any|void|'[^']*'|"[^"]*"|`[^`]*`|(?:\d+(?:\.\d+)?))\s*)*$/;
145
-
146
- const IDENTIFIER_TOKEN_REGEX = /[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*/g;
147
-
148
- const PRIMITIVE_TOKENS = new Set([
149
- 'true',
150
- 'false',
151
- 'null',
152
- 'undefined',
153
- 'string',
154
- 'number',
155
- 'boolean',
156
- 'bigint',
157
- 'symbol',
158
- 'unknown',
159
- 'any',
160
- 'void',
161
- 'never',
162
- ]);
163
-
164
- const KNOWN_TYPE_NAMES = new Set([
165
- 'Array',
166
- 'ReadonlyArray',
167
- 'Promise',
168
- 'Record',
169
- 'Partial',
170
- 'Required',
171
- 'Pick',
172
- 'Omit',
173
- 'Map',
174
- 'Set',
175
- 'WeakMap',
176
- 'WeakSet',
177
- 'Date',
178
- 'RegExp',
179
- 'Error',
180
- 'Node',
181
- 'Element',
182
- 'HTMLElement',
183
- 'SVGElement',
184
- 'Event',
185
- 'CustomEvent',
186
- 'MouseEvent',
187
- 'KeyboardEvent',
188
- 'FocusEvent',
189
- 'InputEvent',
190
- 'PointerEvent',
191
- 'WheelEvent',
192
- 'DragEvent',
193
- 'SubmitEvent',
194
- 'AbortSignal',
195
- 'DOMRect',
196
- 'Document',
197
- 'Window',
198
- 'URL',
199
- 'URLSearchParams',
200
- 'CSSStyleDeclaration',
201
- 'Intl.Locale',
202
- ]);
203
-
204
- /**
205
- * DOM event classes that map directly to `(event: T) => void`.
206
- * Excludes bare `CustomEvent` which needs special handling for its detail type.
207
- */
208
- const DOM_EVENT_CLASS_NAMES = new Set([
209
- 'Event',
210
- 'MouseEvent',
211
- 'KeyboardEvent',
212
- 'FocusEvent',
213
- 'InputEvent',
214
- 'PointerEvent',
215
- 'WheelEvent',
216
- 'DragEvent',
217
- 'SubmitEvent',
218
- ]);
219
-
220
- /**
221
- * React DOM reserves these `on*` prop names for native/synthetic events. When a CEM event
222
- * name maps to the same handler (e.g. `click` → `onClick`), we must not emit a duplicate
223
- * wrapper prop or it clashes with React's built-in typings.
224
- *
225
- * Resolved at startup from the consumer project's `@types/react/index.d.ts` so the set stays
226
- * current without manual maintenance. Falls back to a static snapshot when `@types/react` is
227
- * not installed (e.g. JS-only consumers — in that case the exclusion set is irrelevant anyway).
228
- */
229
- function loadReactEventHandlerNames(): Set<string> {
230
- try {
231
- const typesPath = require.resolve('@types/react/index.d.ts');
232
- const content = readFileSync(typesPath, 'utf-8');
233
- const names = new Set<string>();
234
- for (const m of content.matchAll(/\b(on[A-Z][a-zA-Z]+)\??\s*:/g)) {
235
- names.add(m[1]);
236
- }
237
- // oxlint-disable-next-line no-magic-numbers -- 20 is a sanity threshold for react event names
238
- if (names.size > 20) return names;
239
- } catch {}
240
- // Static snapshot — kept as fallback only.
241
- return new Set([
242
- 'onCopy',
243
- 'onCut',
244
- 'onPaste',
245
- 'onCompositionEnd',
246
- 'onCompositionStart',
247
- 'onCompositionUpdate',
248
- 'onFocus',
249
- 'onBlur',
250
- 'onChange',
251
- 'onBeforeInput',
252
- 'onInput',
253
- 'onReset',
254
- 'onSubmit',
255
- 'onInvalid',
256
- 'onLoad',
257
- 'onError',
258
- 'onKeyDown',
259
- 'onKeyPress',
260
- 'onKeyUp',
261
- 'onAbort',
262
- 'onCanPlay',
263
- 'onCanPlayThrough',
264
- 'onDurationChange',
265
- 'onEmptied',
266
- 'onEncrypted',
267
- 'onEnded',
268
- 'onLoadedData',
269
- 'onLoadedMetadata',
270
- 'onLoadStart',
271
- 'onPause',
272
- 'onPlay',
273
- 'onPlaying',
274
- 'onProgress',
275
- 'onRateChange',
276
- 'onResize',
277
- 'onSeeked',
278
- 'onSeeking',
279
- 'onStalled',
280
- 'onSuspend',
281
- 'onTimeUpdate',
282
- 'onVolumeChange',
283
- 'onWaiting',
284
- 'onAuxClick',
285
- 'onClick',
286
- 'onContextMenu',
287
- 'onDoubleClick',
288
- 'onDrag',
289
- 'onDragEnd',
290
- 'onDragEnter',
291
- 'onDragExit',
292
- 'onDragLeave',
293
- 'onDragOver',
294
- 'onDragStart',
295
- 'onDrop',
296
- 'onMouseDown',
297
- 'onMouseEnter',
298
- 'onMouseLeave',
299
- 'onMouseMove',
300
- 'onMouseOut',
301
- 'onMouseOver',
302
- 'onMouseUp',
303
- 'onSelect',
304
- 'onTouchCancel',
305
- 'onTouchEnd',
306
- 'onTouchMove',
307
- 'onTouchStart',
308
- 'onPointerOver',
309
- 'onPointerEnter',
310
- 'onPointerDown',
311
- 'onPointerMove',
312
- 'onPointerUp',
313
- 'onPointerCancel',
314
- 'onPointerOut',
315
- 'onPointerLeave',
316
- 'onGotPointerCapture',
317
- 'onLostPointerCapture',
318
- 'onScroll',
319
- 'onWheel',
320
- 'onAnimationStart',
321
- 'onAnimationEnd',
322
- 'onAnimationIteration',
323
- 'onTransitionEnd',
324
- 'onToggle',
325
- ]);
326
- }
327
-
328
- const REACT_NATIVE_EVENT_HANDLER_NAMES = loadReactEventHandlerNames();
329
-
330
- /**
331
- * Emitted into every react.d.ts.
332
- * onChange/onInput use method signatures for bivariant parameter checking so both
333
- * native Event and CustomEvent callbacks are accepted without cast.
334
- */
335
- const HELPER_TYPES = `\
336
- /** @internal Maps a web component class to its public props only.
337
- * keyof T skips private/protected members, so this avoids the TS error
338
- * "property may not be private or protected" on exported anonymous types. */
339
- type PublicOf<T> = { [K in keyof T]?: T[K] };
340
-
341
- /** @internal Safe React HTML attributes for web component wrappers.
342
- * onChange/onInput use method signatures for bivariant parameter checking so both
343
- * native Event and CustomEvent callbacks are accepted. */
344
- interface HTMLWCProps extends React.AriaAttributes {
345
- className?: string; style?: React.CSSProperties; id?: string; slot?: string;
346
- tabIndex?: number; dir?: string; lang?: string; title?: string;
347
- onClick?: React.MouseEventHandler<HTMLElement>;
348
- onDoubleClick?: React.MouseEventHandler<HTMLElement>;
349
- onContextMenu?: React.MouseEventHandler<HTMLElement>;
350
- onMouseEnter?: React.MouseEventHandler<HTMLElement>;
351
- onMouseLeave?: React.MouseEventHandler<HTMLElement>;
352
- onMouseDown?: React.MouseEventHandler<HTMLElement>;
353
- onMouseUp?: React.MouseEventHandler<HTMLElement>;
354
- onMouseMove?: React.MouseEventHandler<HTMLElement>;
355
- onKeyDown?: React.KeyboardEventHandler<HTMLElement>;
356
- onKeyUp?: React.KeyboardEventHandler<HTMLElement>;
357
- onFocus?: React.FocusEventHandler<HTMLElement>;
358
- onBlur?: React.FocusEventHandler<HTMLElement>;
359
- onScroll?: React.UIEventHandler<HTMLElement>;
360
- onWheel?: React.WheelEventHandler<HTMLElement>;
361
- onChange?(e: Event): void;
362
- onInput?(e: Event): void;
363
- }
364
- `;
365
-
366
- // ── String utilities ─────────────────────────────────────────────────────────
367
-
368
- function normalizeWhitespace(value: string): string {
369
- return value.replace(/\s+/g, ' ').trim();
370
- }
371
-
372
- function normalizePropertyName(name: string): string | null {
373
- let s = name.trim();
374
- if (!s) return null;
375
-
376
- const bracketQuoted = s.match(/^\[\s*['"](.+?)['"]\s*\]$/);
377
- if (bracketQuoted) {
378
- s = bracketQuoted[1];
379
- } else {
380
- const bracket = s.match(/^\[\s*(.+?)\s*\]$/);
381
- if (bracket) s = bracket[1];
382
- const quoted = s.match(/^['"](.+?)['"]$/);
383
- if (quoted) s = quoted[1];
384
- }
385
-
386
- s = s.trim();
387
- if (!s || s.includes('\n') || s.includes('\r')) return null;
388
- return s;
389
- }
390
-
391
- function toPascalCase(value: string): string {
392
- return value
393
- .split(/[^a-zA-Z0-9]+/)
394
- .filter(Boolean)
395
- .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
396
- .join('');
397
- }
398
-
399
- // ── Type resolution utilities ─────────────────────────────────────────────────
400
-
401
- function isPrimitiveToken(token: string): boolean {
402
- return PRIMITIVE_TOKENS.has(token);
403
- }
404
-
405
- function isKnownTypeIdentifier(identifier: string): boolean {
406
- return KNOWN_TYPE_NAMES.has(identifier) || identifier.startsWith('globalThis.');
407
- }
408
-
409
- function getIdentifierTokens(typeText: string): string[] {
410
- return typeText.match(IDENTIFIER_TOKEN_REGEX) ?? [];
411
- }
412
-
413
- function createTypeImportState(): TypeImportState {
414
- return {
415
- importsByIdentifier: new Map(),
416
- ambiguousIdentifiers: new Set(),
417
- usedImports: new Map(),
418
- wildcardExportModules: new Set(),
419
- };
420
- }
421
-
422
- function registerTypeImport(
423
- state: TypeImportState,
424
- identifier: string,
425
- moduleSpecifier: string,
426
- ): void {
427
- if (state.ambiguousIdentifiers.has(identifier)) return;
428
-
429
- const existing = state.importsByIdentifier.get(identifier);
430
- if (existing && existing !== moduleSpecifier) {
431
- state.importsByIdentifier.delete(identifier);
432
- state.ambiguousIdentifiers.add(identifier);
433
- return;
434
- }
435
-
436
- if (!existing) {
437
- state.importsByIdentifier.set(identifier, moduleSpecifier);
438
- }
439
- }
440
-
441
- function trackImportedIdentifierUsage(
442
- state: TypeImportState,
443
- identifier: string,
444
- moduleSpecifier: string,
445
- ): void {
446
- if (!state.usedImports.has(moduleSpecifier)) {
447
- state.usedImports.set(moduleSpecifier, new Set());
448
- }
449
- state.usedImports.get(moduleSpecifier)!.add(identifier);
450
- }
451
-
452
- function isBareModuleSpecifier(moduleSpecifier: string): boolean {
453
- return !!moduleSpecifier && !moduleSpecifier.startsWith('.') && !moduleSpecifier.startsWith('/');
454
- }
455
-
456
- function canUseComplexType(typeText: string, typeImportState?: TypeImportState): boolean {
457
- if (!typeText) return false;
458
- if (/[{};=]/.test(typeText) || /=>/.test(typeText)) return false;
459
- if (!/^[A-Za-z0-9_$<>[\]()|&,.?'"`\s:-]+$/.test(typeText)) return false;
460
-
461
- for (const token of getIdentifierTokens(typeText)) {
462
- if (isPrimitiveToken(token) || isKnownTypeIdentifier(token)) continue;
463
-
464
- const root = token.split('.')[0];
465
- if (
466
- typeImportState &&
467
- !typeImportState.ambiguousIdentifiers.has(root) &&
468
- typeImportState.importsByIdentifier.has(root)
469
- ) {
470
- trackImportedIdentifierUsage(
471
- typeImportState,
472
- root,
473
- typeImportState.importsByIdentifier.get(root)!,
474
- );
475
- continue;
476
- }
477
-
478
- return false;
479
- }
480
-
481
- return true;
482
- }
483
-
484
- function toSafeType(typeText?: string, typeImportState?: TypeImportState): string {
485
- if (!typeText) return 'unknown';
486
- const normalized = normalizeWhitespace(typeText);
487
- if (!normalized) return 'unknown';
488
-
489
- if (PRIMITIVE_UNION_REGEX.test(normalized)) return normalized;
490
-
491
- // oxlint-disable-next-line no-magic-numbers -- -2 strips trailing '[]'
492
- if (normalized.endsWith('[]') && PRIMITIVE_UNION_REGEX.test(normalized.slice(0, -2).trim())) {
493
- return normalized;
494
- }
495
-
496
- return canUseComplexType(normalized, typeImportState) ? normalized : 'unknown';
497
- }
498
-
499
- function extractDetailTypeFromDescription(description?: string): string | undefined {
500
- return description?.match(/detail:\s*`([^`]+)`/)?.[1]?.trim();
501
- }
502
-
503
- /**
504
- * Maps a CEM event type string to a TypeScript handler signature.
505
- *
506
- * - Known DOM event classes (Event, MouseEvent, FocusEvent, etc.) → `(event: T) => void`
507
- * - `CustomEvent<Detail>` → `(event: CustomEvent<Detail>) => void`
508
- * - Bare `CustomEvent` or unresolvable types → `(event: CustomEvent<unknown>) => void`
509
- */
510
- function toEventHandlerType(
511
- typeText?: string,
512
- typeImportState?: TypeImportState,
513
- description?: string,
514
- ): string {
515
- if (typeText) {
516
- const normalized = typeText.trim();
517
-
518
- // Known DOM event classes map directly — not wrapped as CustomEvent<T> detail.
519
- if (DOM_EVENT_CLASS_NAMES.has(normalized)) {
520
- return `(event: ${normalized}) => void`;
521
- }
522
-
523
- if (normalized.startsWith('CustomEvent<')) {
524
- const match = normalized.match(/^CustomEvent<(.+)>$/);
525
- if (match) {
526
- const detailType = toSafeType(match[1]?.trim(), typeImportState);
527
- return `(event: CustomEvent<${detailType}>) => void`;
528
- }
529
- }
530
-
531
- // For any other resolvable non-DOM type, treat it as the CustomEvent detail payload.
532
- if (normalized !== 'CustomEvent') {
533
- const safeType = toSafeType(normalized, typeImportState);
534
- if (safeType !== 'unknown') {
535
- return `(event: CustomEvent<${safeType}>) => void`;
536
- }
537
- }
538
- }
539
-
540
- const detailFromDesc = extractDetailTypeFromDescription(description);
541
- if (detailFromDesc) {
542
- return `(event: CustomEvent<${toSafeType(detailFromDesc, typeImportState)}>) => void`;
543
- }
544
-
545
- return '(event: CustomEvent<unknown>) => void';
546
- }
547
-
548
- // ── Path helpers ─────────────────────────────────────────────────────────────
549
-
550
- function getCEMManifestPath(cwd: string, packageJson: Record<string, unknown>): string {
551
- if (typeof packageJson.customElements === 'string' && packageJson.customElements.trim()) {
552
- return resolve(cwd, packageJson.customElements);
553
- }
554
- return resolve(cwd, 'dist/custom-elements.json');
555
- }
556
-
557
- // CEM paths are relative to src/ (e.g. "src/entities/entities.ts").
558
- // react.mjs/cjs live in dist/ while compiled JS lives in dist/esm/.
559
- function cemModulePathToJsImport(modulePath: string): string {
560
- // oxlint-disable no-magic-numbers -- numeric offsets for known file extension lengths
561
- let p = modulePath.startsWith('src/') ? modulePath.slice(4) : modulePath;
562
- if (p.endsWith('.tsx')) p = `${p.slice(0, -4)}.js`;
563
- else if (p.endsWith('.ts')) p = `${p.slice(0, -3)}.js`;
564
- // oxlint-enable no-magic-numbers
565
- return `./esm/${p}`;
566
- }
567
-
568
- function cemModulePathToDtsImport(modulePath: string): string {
569
- // oxlint-disable-next-line no-magic-numbers -- 4 = 'src/' prefix length
570
- const p = modulePath.startsWith('src/') ? modulePath.slice(4) : modulePath;
571
- const lastDot = p.lastIndexOf('.');
572
- return `./${lastDot !== -1 ? p.slice(0, lastDot) : p}`;
573
- }
574
-
575
- // ── CEM traversal ─────────────────────────────────────────────────────────────
576
-
577
- function collectCustomElements(manifest: CEMManifest): CEMElementEntry[] {
578
- const elements: CEMElementEntry[] = [];
579
- for (const mod of manifest.modules ?? []) {
580
- const modulePath = mod.path ?? '';
581
- for (const decl of mod.declarations ?? []) {
582
- if (decl.customElement && decl.tagName) {
583
- elements.push({ declaration: decl, modulePath });
584
- }
585
- }
586
- }
587
- return elements;
588
- }
589
-
590
- function mergeUniqueByKey<T>(base: T[], extra: T[], getKey: (item: T) => string | null): T[] {
591
- const merged = [...base];
592
- const knownKeys = new Set(base.map(getKey).filter((k): k is string => !!k));
593
- for (const item of extra) {
594
- const key = getKey(item);
595
- if (!key || !knownKeys.has(key)) {
596
- merged.push(item);
597
- if (key) knownKeys.add(key);
598
- }
599
- }
600
- return merged;
601
- }
602
-
603
- function mergeDeclarationMetadata(base: CEMDeclaration, inherited: CEMDeclaration): CEMDeclaration {
604
- const attrKey = (a: CEMMember) => normalizePropertyName(a.fieldName ?? a.name ?? '');
605
- const nameKey = (m: CEMMember) => normalizePropertyName(m.name ?? '');
606
- const eventKey = (e: CEMEvent) => normalizePropertyName(e.name ?? '');
607
- return {
608
- ...base,
609
- attributes: mergeUniqueByKey(base.attributes ?? [], inherited.attributes ?? [], attrKey),
610
- members: mergeUniqueByKey(base.members ?? [], inherited.members ?? [], nameKey),
611
- events: mergeUniqueByKey(base.events ?? [], inherited.events ?? [], eventKey),
612
- };
613
- }
614
-
615
- function createDeclarationLookup(manifest: CEMManifest): {
616
- byTagAndName: Map<string, CEMDeclaration>;
617
- byTag: Map<string, CEMDeclaration>;
618
- } {
619
- const byTagAndName = new Map<string, CEMDeclaration>();
620
- const byTag = new Map<string, CEMDeclaration>();
621
- for (const mod of manifest.modules ?? []) {
622
- for (const decl of mod.declarations ?? []) {
623
- if (!decl.customElement || !decl.tagName) continue;
624
- if (decl.name) byTagAndName.set(`${decl.tagName}::${decl.name}`, decl);
625
- if (!byTag.has(decl.tagName)) byTag.set(decl.tagName, decl);
626
- }
627
- }
628
- return { byTagAndName, byTag };
629
- }
630
-
631
- async function mergeFastInheritanceFromManifest(
632
- cwd: string,
633
- entries: CEMElementEntry[],
634
- ): Promise<CEMElementEntry[]> {
635
- const fastManifestPath = resolve(cwd, 'ms-fast-components/custom-elements.json');
636
- if (!(await fileExists(fastManifestPath))) return entries;
637
-
638
- const fastManifest = JSON.parse(await readFile(fastManifestPath, 'utf8')) as CEMManifest;
639
- const { byTagAndName, byTag } = createDeclarationLookup(fastManifest);
640
-
641
- return entries.map((entry) => {
642
- const { declaration } = entry;
643
- if (!declaration.tagName || declaration.superclass?.package !== '@microsoft/fast-components') {
644
- return entry;
645
- }
646
-
647
- const inherited =
648
- byTagAndName.get(`${declaration.tagName}::${declaration.name ?? ''}`) ??
649
- byTagAndName.get(`${declaration.tagName}::${declaration.superclass?.name ?? ''}`) ??
650
- byTag.get(declaration.tagName);
651
-
652
- return inherited
653
- ? { ...entry, declaration: mergeDeclarationMetadata(declaration, inherited) }
654
- : entry;
655
- });
656
- }
657
-
658
- function createFoundationDeclarationLookup(manifest: CEMManifest): {
659
- byTag: Map<string, CEMDeclaration>;
660
- byName: Map<string, CEMDeclaration>;
661
- } {
662
- const byTag = new Map<string, CEMDeclaration>();
663
- const byName = new Map<string, CEMDeclaration>();
664
- for (const mod of manifest.modules ?? []) {
665
- for (const decl of mod.declarations ?? []) {
666
- if (decl.name) byName.set(decl.name, decl);
667
- if (decl.customElement && decl.tagName) byTag.set(decl.tagName, decl);
668
- }
669
- }
670
- return { byTag, byName };
671
- }
672
-
673
- async function resolveDependencyManifestPath(
674
- cwd: string,
675
- packageName: string,
676
- ): Promise<string | null> {
677
- const packageRoot = resolve(cwd, 'node_modules', packageName);
678
- const packageJsonPath = resolve(packageRoot, 'package.json');
679
- if (!(await fileExists(packageJsonPath))) return null;
680
-
681
- const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as Record<
682
- string,
683
- unknown
684
- >;
685
- if (typeof packageJson.customElements === 'string' && packageJson.customElements.trim()) {
686
- const manifestPath = resolve(packageRoot, packageJson.customElements);
687
- if (await fileExists(manifestPath)) return manifestPath;
688
- }
689
-
690
- const distManifestPath = resolve(packageRoot, 'dist/custom-elements.json');
691
- if (await fileExists(distManifestPath)) return distManifestPath;
692
-
693
- const rootManifestPath = resolve(packageRoot, 'custom-elements.json');
694
- if (await fileExists(rootManifestPath)) return rootManifestPath;
695
-
696
- return null;
697
- }
698
-
699
- function findFoundationInheritedDeclaration(
700
- declaration: CEMDeclaration,
701
- lookup: ReturnType<typeof createFoundationDeclarationLookup>,
702
- ): CEMDeclaration | undefined {
703
- if (declaration.tagName) {
704
- const byTag = lookup.byTag.get(declaration.tagName);
705
- if (byTag) return byTag;
706
- }
707
-
708
- const superclassName = declaration.superclass?.name;
709
- if (!superclassName) return undefined;
710
-
711
- const byName = lookup.byName.get(superclassName);
712
- if (byName) return byName;
713
-
714
- const normalized = superclassName.replace(/^Foundation(?=[A-Z])/, '').replace(/^foundation/, '');
715
- if (normalized === superclassName) return undefined;
716
-
717
- for (const [name, decl] of lookup.byName) {
718
- if (decl.customElement && name.toLowerCase() === normalized.toLowerCase()) {
719
- return decl;
720
- }
721
- }
722
-
723
- return undefined;
724
- }
725
-
726
- async function mergeFoundationInheritanceFromManifest(
727
- cwd: string,
728
- entries: CEMElementEntry[],
729
- ): Promise<CEMElementEntry[]> {
730
- const foundationManifestPath = await resolveDependencyManifestPath(
731
- cwd,
732
- '@genesislcap/foundation-ui',
733
- );
734
- if (!foundationManifestPath) return entries;
735
-
736
- const foundationManifest = JSON.parse(
737
- await readFile(foundationManifestPath, 'utf8'),
738
- ) as CEMManifest;
739
- const lookup = createFoundationDeclarationLookup(foundationManifest);
740
-
741
- return entries.map((entry) => {
742
- const { declaration } = entry;
743
- if (!declaration.tagName || declaration.superclass?.package !== '@genesislcap/foundation-ui') {
744
- return entry;
745
- }
746
-
747
- const inherited = findFoundationInheritedDeclaration(declaration, lookup);
748
- return inherited
749
- ? { ...entry, declaration: mergeDeclarationMetadata(declaration, inherited) }
750
- : entry;
751
- });
752
- }
753
-
754
- // ── Wrapper event helpers ─────────────────────────────────────────────────────
755
-
756
- function buildWrapperEventEntries(
757
- declaration: CEMDeclaration,
758
- ): Array<{ handlerName: string; eventName: string }> {
759
- const result: Array<{ handlerName: string; eventName: string }> = [];
760
- const seen = new Set<string>();
761
- for (const event of declaration.events ?? []) {
762
- if (!event.name) continue;
763
- const normalized = normalizePropertyName(event.name);
764
- if (!normalized) continue;
765
- const handlerName = `on${toPascalCase(normalized)}`;
766
- if (!handlerName || REACT_NATIVE_EVENT_HANDLER_NAMES.has(handlerName) || seen.has(handlerName))
767
- continue;
768
- seen.add(handlerName);
769
- result.push({ handlerName, eventName: event.name });
770
- }
771
- return result;
772
- }
773
-
774
- function groupEntriesByPath(entries: CEMElementEntry[]): Map<string, CEMElementEntry[]> {
775
- const byPath = new Map<string, CEMElementEntry[]>();
776
- for (const entry of entries) {
777
- if (!byPath.has(entry.modulePath)) byPath.set(entry.modulePath, []);
778
- byPath.get(entry.modulePath)!.push(entry);
779
- }
780
- return byPath;
781
- }
782
-
783
- function cloneTypeImportStateForWrapper(original: TypeImportState): TypeImportState {
784
- return {
785
- importsByIdentifier: new Map(original.importsByIdentifier),
786
- ambiguousIdentifiers: new Set(original.ambiguousIdentifiers),
787
- usedImports: new Map(),
788
- wildcardExportModules: new Set(original.wildcardExportModules),
789
- };
790
- }
791
-
792
- // ── Code generation ───────────────────────────────────────────────────────────
793
-
794
- function renderImportLines(usedImports: Map<string, Set<string>>): string[] {
795
- return [...usedImports.entries()]
796
- .sort(([a], [b]) => a.localeCompare(b))
797
- .map(([spec, ids]) => `import type { ${[...ids].sort().join(', ')} } from '${spec}';`);
798
- }
799
-
800
- function generateReactWrapperJs(entries: CEMElementEntry[], format: 'esm' | 'cjs'): string {
801
- const valid = entries.filter((e) => e.declaration.name && e.declaration.tagName && e.modulePath);
802
- if (!valid.length) return '';
803
-
804
- const esm = format === 'esm';
805
- const lines: string[] = [
806
- '/**',
807
- ' * AUTO-GENERATED FILE - DO NOT EDIT.',
808
- ' * Generated from custom-elements manifest.',
809
- ' */',
810
- '',
811
- ];
812
-
813
- if (!esm) lines.push("'use strict';", '');
814
-
815
- if (esm) {
816
- lines.push("import React from 'react';");
817
- } else {
818
- lines.push("const React = require('react');");
819
- }
820
-
821
- for (const [modulePath, pathEntries] of [...groupEntriesByPath(valid).entries()].sort()) {
822
- const sorted = [...pathEntries].sort((a, b) =>
823
- a.declaration.name!.localeCompare(b.declaration.name!),
824
- );
825
- const jsPath = cemModulePathToJsImport(modulePath);
826
- if (esm) {
827
- lines.push(
828
- `import { ${sorted.map((e) => `${e.declaration.name} as ${e.declaration.name}WC`).join(', ')} } from '${jsPath}';`,
829
- );
830
- } else {
831
- lines.push(
832
- `const { ${sorted.map((e) => `${e.declaration.name}: ${e.declaration.name}WC`).join(', ')} } = require('${jsPath}');`,
833
- );
834
- }
835
- }
836
-
837
- const anyHasEvents = valid.some((e) => buildWrapperEventEntries(e.declaration).length > 0);
838
- if (anyHasEvents) {
839
- lines.push(
840
- '',
841
- 'function _mergeRefs(...refs) {',
842
- ' return (value) => {',
843
- ' for (const ref of refs) {',
844
- " if (typeof ref === 'function') ref(value);",
845
- ' else if (ref != null) ref.current = value;',
846
- ' }',
847
- ' };',
848
- '}',
849
- );
850
- }
851
-
852
- lines.push('');
853
-
854
- for (const { declaration } of valid) {
855
- const name = declaration.name!;
856
- const tagName = declaration.tagName!;
857
- const events = buildWrapperEventEntries(declaration);
858
- const prefix = esm ? 'export const' : 'const';
859
-
860
- lines.push(`${prefix} ${name} = React.forwardRef(function ${name}(props, ref) {`);
861
-
862
- if (events.length) {
863
- lines.push(
864
- ` const { ${events.map((e) => e.handlerName).join(', ')}, children, ...rest } = props;`,
865
- );
866
- lines.push(' const _innerRef = React.useRef(null);');
867
- for (const { handlerName } of events) {
868
- lines.push(` const _${handlerName}Ref = React.useRef(${handlerName});`);
869
- lines.push(` _${handlerName}Ref.current = ${handlerName};`);
870
- }
871
- lines.push(' React.useLayoutEffect(() => {');
872
- lines.push(' const el = _innerRef.current;');
873
- lines.push(' if (!el) return;');
874
- for (const { handlerName, eventName } of events) {
875
- lines.push(` const _${handlerName}Fn = (e) => _${handlerName}Ref.current?.(e);`);
876
- lines.push(` el.addEventListener('${eventName}', _${handlerName}Fn);`);
877
- }
878
- lines.push(' return () => {');
879
- for (const { handlerName, eventName } of events) {
880
- lines.push(` el.removeEventListener('${eventName}', _${handlerName}Fn);`);
881
- }
882
- lines.push(' };');
883
- lines.push(' }, []);');
884
- lines.push(
885
- ` return React.createElement(customElements.getName(${name}WC) ?? '${tagName}', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);`,
886
- );
887
- } else {
888
- lines.push(' const { children, ...rest } = props;');
889
- lines.push(
890
- ` return React.createElement(customElements.getName(${name}WC) ?? '${tagName}', { ...rest, ref }, children);`,
891
- );
892
- }
893
-
894
- lines.push('});');
895
- lines.push('');
896
- }
897
-
898
- if (!esm) {
899
- lines.push('module.exports = {');
900
- for (const { declaration } of valid) lines.push(` ${declaration.name},`);
901
- lines.push('};', '');
902
- }
903
-
904
- return lines.join('\n');
905
- }
906
-
907
- function generateReactWrapperDts(
908
- entries: CEMElementEntry[],
909
- typeImportState: TypeImportState,
910
- propTypeOverrides: PropTypeOverrides = {},
911
- ): string {
912
- const valid = entries.filter((e) => e.declaration.name && e.declaration.tagName && e.modulePath);
913
- if (!valid.length) return '';
914
-
915
- const wrapperTypeState = cloneTypeImportStateForWrapper(typeImportState);
916
-
917
- const classImports: string[] = [];
918
- for (const [modulePath, pathEntries] of [...groupEntriesByPath(valid).entries()].sort()) {
919
- const names = [...pathEntries]
920
- .sort((a, b) => a.declaration.name!.localeCompare(b.declaration.name!))
921
- .map((e) => `${e.declaration.name} as ${e.declaration.name}WC`)
922
- .join(', ');
923
- classImports.push(`import type { ${names} } from '${cemModulePathToDtsImport(modulePath)}';`);
924
- }
925
-
926
- const declarationLines: string[] = [];
927
- for (const { declaration } of valid) {
928
- const name = declaration.name!;
929
-
930
- // Build event lookup scoped to this element to avoid cross-element type pollution.
931
- const eventsByName = new Map(
932
- (declaration.events ?? []).filter((e) => e.name).map((e) => [e.name!, e] as const),
933
- );
934
-
935
- const eventLines = buildWrapperEventEntries(declaration).map(({ handlerName, eventName }) => {
936
- const event = eventsByName.get(eventName);
937
- const handlerType = toEventHandlerType(
938
- event?.type?.text,
939
- wrapperTypeState,
940
- event?.description,
941
- );
942
- return ` ${handlerName}?: ${handlerType};`;
943
- });
944
-
945
- // Prop-type overrides (see PropTypeOverrides): drop the configured props out of the
946
- // `PublicOf` mapping and re-declare them with the named type, so a design system can correct
947
- // a prop whose inherited type it has outgrown. No config ⇒ identical output to before.
948
- // Route each override's type through the SAME import machinery every other type uses
949
- // (registerTypeImport + trackImportedIdentifierUsage) rather than writing `usedImports`
950
- // directly. That is what detects an identifier already imported from a DIFFERENT module: two
951
- // `import type { X }` lines from different specifiers would be a duplicate-identifier error in
952
- // the generated .d.ts. On a collision, drop the override and warn — emitting the widened prop
953
- // without its import would leave the declaration referencing an unresolvable type, which is
954
- // worse than the original narrow type.
955
- const overrides = propTypeOverrides[name] ?? {};
956
- const applied: string[] = [];
957
- for (const prop of Object.keys(overrides).sort()) {
958
- const { type, from } = overrides[prop];
959
- registerTypeImport(wrapperTypeState, type, from);
960
- if (wrapperTypeState.importsByIdentifier.get(type) !== from) {
961
- consola.warn(
962
- `[ts-builder] Skipping the ${name}.${prop} prop-type override: "${type}" is already imported from a different module, so importing it from "${from}" would clash.`,
963
- );
964
- continue;
965
- }
966
- trackImportedIdentifierUsage(wrapperTypeState, type, from);
967
- applied.push(prop);
968
- }
969
- const omitKeys = ["'children'", "'style'", ...applied.map((p) => `'${p}'`)].join(' | ');
970
- const overrideLines = applied.map((prop) => {
971
- // React props are legitimately allowed to be hyphenated (`aria-*`, `data-*`), which is not a
972
- // valid bare key in a type literal — quote those. Identifier-safe names stay unquoted so the
973
- // common output reads naturally. (The Omit key list above is always quoted, so it is safe
974
- // for either form.)
975
- const key = OVERRIDE_TYPE_RE.test(prop) ? prop : `'${prop}'`;
976
- return ` ${key}?: ${overrides[prop].type};`;
977
- });
978
-
979
- declarationLines.push(
980
- `export declare const ${name}: React.ForwardRefExoticComponent<`,
981
- ` React.PropsWithChildren<`,
982
- ` Omit<PublicOf<${name}WC>, ${omitKeys}> &`,
983
- ` HTMLWCProps & {`,
984
- ...overrideLines,
985
- ...eventLines,
986
- ' }',
987
- ` > & React.RefAttributes<${name}WC>`,
988
- '>;',
989
- `export type ${name}Ref = ${name}WC;`,
990
- '',
991
- );
992
- }
993
-
994
- return [
995
- '/**',
996
- ' * AUTO-GENERATED FILE - DO NOT EDIT.',
997
- ' * Generated from custom-elements manifest.',
998
- ' */',
999
- '',
1000
- "import type React from 'react';",
1001
- ...classImports,
1002
- ...renderImportLines(wrapperTypeState.usedImports),
1003
- '',
1004
- HELPER_TYPES,
1005
- ...declarationLines,
1006
- 'export {};',
1007
- '',
1008
- ].join('\n');
1009
- }
1010
-
1011
- // ── File system helpers ───────────────────────────────────────────────────────
1012
-
1013
- async function fileExists(path: string): Promise<boolean> {
1014
- try {
1015
- await stat(path);
1016
- return true;
1017
- } catch {
1018
- return false;
1019
- }
1020
- }
1021
-
1022
- async function collectFilesRecursively(
1023
- rootDirectory: string,
1024
- isTargetFile: (fileName: string) => boolean,
1025
- ): Promise<string[]> {
1026
- const files: string[] = [];
1027
- const stack: string[] = [rootDirectory];
1028
- while (stack.length) {
1029
- const dir = stack.pop()!;
1030
- // oxlint-disable-next-line no-await-in-loop -- iterative directory traversal, sequential by design
1031
- const dirEntries = await readdir(dir, { withFileTypes: true });
1032
- for (const entry of dirEntries) {
1033
- const fullPath = resolve(dir, entry.name);
1034
- if (entry.isDirectory()) stack.push(fullPath);
1035
- else if (entry.isFile() && isTargetFile(entry.name)) files.push(fullPath);
1036
- }
1037
- }
1038
- return files;
1039
- }
1040
-
1041
- async function collectTypeMetadataFiles(
1042
- distDirectory: string,
1043
- ): Promise<{ apiJsonFiles: string[]; dtsFiles: string[] }> {
1044
- const [apiJsonFiles, dtsFiles] = await Promise.all([
1045
- collectFilesRecursively(distDirectory, (n) => n.endsWith('.api.json')),
1046
- collectFilesRecursively(distDirectory, (n) => n.endsWith('.d.ts')),
1047
- ]);
1048
- return { apiJsonFiles, dtsFiles };
1049
- }
1050
-
1051
- // ── Type import state builders ────────────────────────────────────────────────
1052
-
1053
- function addCanonicalReferenceFromValue(value: unknown, state: TypeImportState): void {
1054
- if (!value || typeof value !== 'object') return;
1055
-
1056
- if (Array.isArray(value)) {
1057
- for (const item of value) addCanonicalReferenceFromValue(item, state);
1058
- return;
1059
- }
1060
-
1061
- const record = value as Record<string, unknown>;
1062
- if (typeof record.canonicalReference === 'string') {
1063
- const match = record.canonicalReference.match(/^(@[^!]+)!([^:]+):/);
1064
- if (match) {
1065
- const [, moduleSpecifier, identifier] = match;
1066
- if (identifier && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) {
1067
- registerTypeImport(state, identifier, moduleSpecifier);
1068
- }
1069
- }
1070
- }
1071
-
1072
- for (const nested of Object.values(record)) {
1073
- if (nested && typeof nested === 'object') addCanonicalReferenceFromValue(nested, state);
1074
- }
1075
- }
1076
-
1077
- function parseImportsFromDtsContent(content: string, state: TypeImportState): void {
1078
- const blockRe = /(?:import|export)\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
1079
- for (const match of content.matchAll(blockRe)) {
1080
- const moduleSpecifier = match[2] ?? '';
1081
- if (!isBareModuleSpecifier(moduleSpecifier)) continue;
1082
- for (const raw of (match[1] ?? '').split(',')) {
1083
- const entry = raw.trim();
1084
- if (!entry) continue;
1085
- const alias = entry.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/);
1086
- if (alias) {
1087
- registerTypeImport(state, alias[2], moduleSpecifier);
1088
- continue;
1089
- }
1090
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(entry))
1091
- registerTypeImport(state, entry, moduleSpecifier);
1092
- }
1093
- }
1094
-
1095
- const reExportRe = /export\s+\*\s+from\s+['"]([^'"]+)['"]/g;
1096
- for (const match of content.matchAll(reExportRe)) {
1097
- const moduleSpecifier = match[1] ?? '';
1098
- if (isBareModuleSpecifier(moduleSpecifier)) state.wildcardExportModules.add(moduleSpecifier);
1099
- }
1100
- }
1101
-
1102
- async function parseApiJsonFiles(filePaths: string[], state: TypeImportState): Promise<void> {
1103
- await Promise.all(
1104
- filePaths.map(async (filePath) => {
1105
- const json = JSON.parse(await readFile(filePath, 'utf8')) as unknown;
1106
- addCanonicalReferenceFromValue(json, state);
1107
- }),
1108
- );
1109
- }
1110
-
1111
- async function parseDtsFiles(filePaths: string[], state: TypeImportState): Promise<void> {
1112
- await Promise.all(
1113
- filePaths.map(async (filePath) => {
1114
- parseImportsFromDtsContent(await readFile(filePath, 'utf8'), state);
1115
- }),
1116
- );
1117
- }
1118
-
1119
- function mergeTypeImportStateInto(target: TypeImportState, source: TypeImportState): void {
1120
- for (const id of source.ambiguousIdentifiers) {
1121
- target.importsByIdentifier.delete(id);
1122
- target.ambiguousIdentifiers.add(id);
1123
- }
1124
- for (const [identifier, spec] of source.importsByIdentifier) {
1125
- registerTypeImport(target, identifier, spec);
1126
- }
1127
- for (const mod of source.wildcardExportModules) {
1128
- target.wildcardExportModules.add(mod);
1129
- }
1130
- }
1131
-
1132
- async function findWorkspaceRoot(startDirectory: string): Promise<string> {
1133
- let dir = startDirectory;
1134
- while (true) {
1135
- // oxlint-disable-next-line no-await-in-loop -- upward directory search must be sequential
1136
- const [hasPackages, hasPackageJson] = await Promise.all([
1137
- fileExists(resolve(dir, 'packages')),
1138
- fileExists(resolve(dir, 'package.json')),
1139
- ]);
1140
- if (hasPackages && hasPackageJson) return dir;
1141
- const parent = dirname(dir);
1142
- if (parent === dir) return startDirectory;
1143
- dir = parent;
1144
- }
1145
- }
1146
-
1147
- async function getWorkspacePackageDirectoryByName(
1148
- workspaceRoot: string,
1149
- packageName: string,
1150
- ): Promise<string | undefined> {
1151
- const packagesDir = resolve(workspaceRoot, 'packages');
1152
- if (!(await fileExists(packagesDir))) return undefined;
1153
-
1154
- const packageJsonFiles = await collectFilesRecursively(packagesDir, (n) => n === 'package.json');
1155
- for (const jsonPath of packageJsonFiles) {
1156
- // oxlint-disable-next-line no-await-in-loop -- early-exit search, sequential is correct
1157
- const pkg = JSON.parse(await readFile(jsonPath, 'utf8')) as Record<string, unknown>;
1158
- if (pkg.name === packageName) return dirname(jsonPath);
1159
- }
1160
- return undefined;
1161
- }
1162
-
1163
- async function enrichFromPackageDistMetadata(
1164
- distDir: string,
1165
- state: TypeImportState,
1166
- ): Promise<TypeImportState> {
1167
- const refState = createTypeImportState();
1168
- const { apiJsonFiles, dtsFiles } = await collectTypeMetadataFiles(distDir);
1169
- await parseApiJsonFiles(apiJsonFiles, refState);
1170
- await parseDtsFiles(dtsFiles, refState);
1171
- mergeTypeImportStateInto(state, refState);
1172
- return refState;
1173
- }
1174
-
1175
- async function enrichFromWorkspaceWildcardExports(
1176
- cwd: string,
1177
- state: TypeImportState,
1178
- ): Promise<void> {
1179
- const workspaceRoot = await findWorkspaceRoot(cwd);
1180
- // for...of over a Set processes items added inside the loop (spec-guaranteed behaviour).
1181
- const pending = new Set(
1182
- [...state.wildcardExportModules].filter((m) => m.startsWith('@genesislcap/')),
1183
- );
1184
-
1185
- for (const moduleSpecifier of pending) {
1186
- // oxlint-disable-next-line no-await-in-loop -- sequential per-package processing is intentional
1187
- const pkgDir = await getWorkspacePackageDirectoryByName(workspaceRoot, moduleSpecifier);
1188
- if (!pkgDir) continue;
1189
-
1190
- const distDir = resolve(pkgDir, 'dist');
1191
- // oxlint-disable-next-line no-await-in-loop -- sequential per-package processing is intentional
1192
- if (!(await fileExists(distDir))) continue;
1193
-
1194
- // oxlint-disable-next-line no-await-in-loop -- sequential per-package processing is intentional
1195
- const refState = await enrichFromPackageDistMetadata(distDir, state);
1196
-
1197
- for (const mod of refState.wildcardExportModules) {
1198
- if (mod.startsWith('@genesislcap/')) pending.add(mod);
1199
- }
1200
- }
1201
- }
1202
-
1203
- /**
1204
- * Elements inheriting from another `@genesislcap/*` package merge that package's CEM events
1205
- * (see mergeFoundationInheritanceFromManifest), so their event descriptions may reference
1206
- * detail types exported by the superclass package. Register that package's type metadata so
1207
- * those identifiers resolve instead of silently degrading to `unknown`.
1208
- */
1209
- async function enrichFromInheritedSuperclassPackages(
1210
- cwd: string,
1211
- entries: CEMElementEntry[],
1212
- state: TypeImportState,
1213
- ): Promise<void> {
1214
- const superclassPackages = new Set(
1215
- entries
1216
- .map((e) => e.declaration.superclass?.package)
1217
- .filter((p): p is string => !!p && p.startsWith('@genesislcap/')),
1218
- );
1219
- if (!superclassPackages.size) return;
1220
-
1221
- const workspaceRoot = await findWorkspaceRoot(cwd);
1222
-
1223
- for (const packageName of superclassPackages) {
1224
- // Consumer repos resolve the superclass package from node_modules; inside this
1225
- // monorepo it is a workspace package.
1226
- let distDir = resolve(cwd, 'node_modules', packageName, 'dist');
1227
- // oxlint-disable-next-line no-await-in-loop -- sequential per-package processing is intentional
1228
- if (!(await fileExists(distDir))) {
1229
- // oxlint-disable-next-line no-await-in-loop -- sequential per-package processing is intentional
1230
- const pkgDir = await getWorkspacePackageDirectoryByName(workspaceRoot, packageName);
1231
- if (!pkgDir) continue;
1232
- distDir = resolve(pkgDir, 'dist');
1233
- // oxlint-disable-next-line no-await-in-loop -- sequential per-package processing is intentional
1234
- if (!(await fileExists(distDir))) continue;
1235
- }
1236
-
1237
- // oxlint-disable-next-line no-await-in-loop -- sequential per-package processing is intentional
1238
- await enrichFromPackageDistMetadata(distDir, state);
1239
- }
1240
- }
1241
-
1242
- async function buildTypeImportState(cwd: string): Promise<TypeImportState> {
1243
- const state = createTypeImportState();
1244
- const distDir = resolve(cwd, 'dist');
1245
- if (!(await fileExists(distDir))) return state;
1246
-
1247
- const { apiJsonFiles, dtsFiles } = await collectTypeMetadataFiles(distDir);
1248
- await parseApiJsonFiles(apiJsonFiles, state);
1249
- await parseDtsFiles(dtsFiles, state);
1250
- await enrichFromWorkspaceWildcardExports(cwd, state);
1251
-
1252
- return state;
1253
- }
1254
-
1255
- // ── Entry point ───────────────────────────────────────────────────────────────
1256
-
1257
- export async function generateReactWrappers(cwd: string): Promise<GenerateResult> {
1258
- const packageJsonPath = resolve(cwd, 'package.json');
1259
- if (!(await fileExists(packageJsonPath))) {
1260
- return { generated: false, reason: 'No package.json found.' };
1261
- }
1262
-
1263
- const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as Record<
1264
- string,
1265
- unknown
1266
- >;
1267
- const manifestPath = getCEMManifestPath(cwd, packageJson);
1268
- if (!(await fileExists(manifestPath))) {
1269
- return { generated: false, reason: 'No custom elements manifest found.' };
1270
- }
1271
-
1272
- const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as CEMManifest;
1273
- const rawEntries = collectCustomElements(manifest);
1274
- if (!rawEntries.length) {
1275
- return { generated: false, reason: 'No custom elements discovered in manifest.' };
1276
- }
1277
-
1278
- const afterFast = await mergeFastInheritanceFromManifest(cwd, rawEntries);
1279
- const entries = await mergeFoundationInheritanceFromManifest(cwd, afterFast);
1280
- // A wrapper is worth generating for any element that has a tag name: the dts exposes the
1281
- // element's public members as typed props (via `PublicOf<WC>`) and the runtime wrapper binds
1282
- // object properties that JSX can't set as attributes. Events are layered on top when present,
1283
- // so they are not a precondition — prop-only elements still produce a useful, typed wrapper.
1284
- const hasWrappableElement = entries.some(
1285
- (e) => e.declaration.name && e.declaration.tagName && e.modulePath,
1286
- );
1287
- if (!hasWrappableElement) {
1288
- return { generated: false, reason: 'No wrappable custom elements found in manifest.' };
1289
- }
1290
-
1291
- const dtsRoot = resolve(cwd, 'dist/dts');
1292
- await mkdir(dtsRoot, { recursive: true });
1293
-
1294
- const typeImportState = await buildTypeImportState(cwd);
1295
- await enrichFromInheritedSuperclassPackages(cwd, entries, typeImportState);
1296
- const reactDtsPath = resolve(dtsRoot, 'react.d.ts');
1297
-
1298
- await Promise.all([
1299
- writeFile(resolve(cwd, 'dist/react.mjs'), generateReactWrapperJs(entries, 'esm'), 'utf8'),
1300
- writeFile(resolve(cwd, 'dist/react.cjs'), generateReactWrapperJs(entries, 'cjs'), 'utf8'),
1301
- writeFile(
1302
- reactDtsPath,
1303
- generateReactWrapperDts(entries, typeImportState, readPropTypeOverrides(packageJson)),
1304
- 'utf8',
1305
- ),
1306
- ]);
1307
-
1308
- return { generated: true, path: reactDtsPath };
1309
- }