@genesislcap/ts-builder 14.418.2 → 14.419.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.
@@ -0,0 +1,823 @@
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
+
5
+ // ── CEM type shapes ──────────────────────────────────────────────────────────
6
+
7
+ type CEMType = { text?: string };
8
+ type CEMEvent = { name?: string; type?: CEMType; description?: string };
9
+ type CEMSuperclass = { name?: string; package?: string };
10
+ type CEMMember = {
11
+ name?: string;
12
+ fieldName?: string;
13
+ attribute?: string | null;
14
+ kind?: string;
15
+ privacy?: string;
16
+ type?: CEMType;
17
+ };
18
+ type CEMDeclaration = {
19
+ name?: string;
20
+ customElement?: boolean;
21
+ tagName?: string;
22
+ superclass?: CEMSuperclass;
23
+ members?: CEMMember[];
24
+ attributes?: CEMMember[];
25
+ events?: CEMEvent[];
26
+ };
27
+ type CEMModule = { path?: string; declarations?: CEMDeclaration[] };
28
+ type CEMManifest = { modules?: CEMModule[] };
29
+ type CEMElementEntry = { declaration: CEMDeclaration; modulePath: string };
30
+
31
+ // ── Type import state ────────────────────────────────────────────────────────
32
+
33
+ type TypeImportState = {
34
+ importsByIdentifier: Map<string, string>;
35
+ ambiguousIdentifiers: Set<string>;
36
+ usedImports: Map<string, Set<string>>;
37
+ wildcardExportModules: Set<string>;
38
+ };
39
+
40
+ // ── Public API return type ───────────────────────────────────────────────────
41
+
42
+ type GenerateResult =
43
+ | { generated: true; path: string }
44
+ | { generated: false; reason: string };
45
+
46
+ // ── Constants ────────────────────────────────────────────────────────────────
47
+
48
+ const PRIMITIVE_UNION_REGEX =
49
+ /^(?:\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*)*$/;
50
+
51
+ const IDENTIFIER_TOKEN_REGEX = /[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*/g;
52
+
53
+ const PRIMITIVE_TOKENS = new Set([
54
+ 'true', 'false', 'null', 'undefined', 'string', 'number', 'boolean',
55
+ 'bigint', 'symbol', 'unknown', 'any', 'void', 'never',
56
+ ]);
57
+
58
+ const KNOWN_TYPE_NAMES = new Set([
59
+ 'Array', 'ReadonlyArray', 'Promise', 'Record', 'Partial', 'Required', 'Pick', 'Omit',
60
+ 'Map', 'Set', 'WeakMap', 'WeakSet', 'Date', 'RegExp', 'Error', 'Node', 'Element',
61
+ 'HTMLElement', 'SVGElement', 'Event', 'CustomEvent', 'MouseEvent', 'KeyboardEvent',
62
+ 'FocusEvent', 'InputEvent', 'PointerEvent', 'WheelEvent', 'DragEvent', 'SubmitEvent',
63
+ 'AbortSignal', 'DOMRect', 'Document', 'Window', 'URL', 'URLSearchParams',
64
+ 'CSSStyleDeclaration', 'Intl.Locale',
65
+ ]);
66
+
67
+ /**
68
+ * DOM event classes that map directly to `(event: T) => void`.
69
+ * Excludes bare `CustomEvent` which needs special handling for its detail type.
70
+ */
71
+ const DOM_EVENT_CLASS_NAMES = new Set([
72
+ 'Event', 'MouseEvent', 'KeyboardEvent', 'FocusEvent', 'InputEvent',
73
+ 'PointerEvent', 'WheelEvent', 'DragEvent', 'SubmitEvent',
74
+ ]);
75
+
76
+ /**
77
+ * React DOM reserves these `on*` prop names for native/synthetic events. When a CEM event
78
+ * name maps to the same handler (e.g. `click` → `onClick`), we must not emit a duplicate
79
+ * wrapper prop or it clashes with React's built-in typings.
80
+ *
81
+ * Resolved at startup from the consumer project's `@types/react/index.d.ts` so the set stays
82
+ * current without manual maintenance. Falls back to a static snapshot when `@types/react` is
83
+ * not installed (e.g. JS-only consumers — in that case the exclusion set is irrelevant anyway).
84
+ */
85
+ function loadReactEventHandlerNames(): Set<string> {
86
+ try {
87
+ const typesPath = require.resolve('@types/react/index.d.ts');
88
+ const content = readFileSync(typesPath, 'utf-8');
89
+ const names = new Set<string>();
90
+ for (const m of content.matchAll(/\b(on[A-Z][a-zA-Z]+)\??\s*:/g)) {
91
+ names.add(m[1]);
92
+ }
93
+ if (names.size > 20) return names;
94
+ } catch {}
95
+ // Static snapshot — kept as fallback only.
96
+ return new Set([
97
+ 'onCopy', 'onCut', 'onPaste', 'onCompositionEnd', 'onCompositionStart', 'onCompositionUpdate',
98
+ 'onFocus', 'onBlur', 'onChange', 'onBeforeInput', 'onInput', 'onReset', 'onSubmit',
99
+ 'onInvalid', 'onLoad', 'onError', 'onKeyDown', 'onKeyPress', 'onKeyUp', 'onAbort',
100
+ 'onCanPlay', 'onCanPlayThrough', 'onDurationChange', 'onEmptied', 'onEncrypted', 'onEnded',
101
+ 'onLoadedData', 'onLoadedMetadata', 'onLoadStart', 'onPause', 'onPlay', 'onPlaying',
102
+ 'onProgress', 'onRateChange', 'onResize', 'onSeeked', 'onSeeking', 'onStalled', 'onSuspend',
103
+ 'onTimeUpdate', 'onVolumeChange', 'onWaiting', 'onAuxClick', 'onClick', 'onContextMenu',
104
+ 'onDoubleClick', 'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave',
105
+ 'onDragOver', 'onDragStart', 'onDrop', 'onMouseDown', 'onMouseEnter', 'onMouseLeave',
106
+ 'onMouseMove', 'onMouseOut', 'onMouseOver', 'onMouseUp', 'onSelect', 'onTouchCancel',
107
+ 'onTouchEnd', 'onTouchMove', 'onTouchStart', 'onPointerOver', 'onPointerEnter',
108
+ 'onPointerDown', 'onPointerMove', 'onPointerUp', 'onPointerCancel', 'onPointerOut',
109
+ 'onPointerLeave', 'onGotPointerCapture', 'onLostPointerCapture', 'onScroll', 'onWheel',
110
+ 'onAnimationStart', 'onAnimationEnd', 'onAnimationIteration', 'onTransitionEnd', 'onToggle',
111
+ ]);
112
+ }
113
+
114
+ const REACT_NATIVE_EVENT_HANDLER_NAMES = loadReactEventHandlerNames();
115
+
116
+ /**
117
+ * Emitted into every react.d.ts.
118
+ * onChange/onInput use method signatures for bivariant parameter checking so both
119
+ * native Event and CustomEvent callbacks are accepted without cast.
120
+ */
121
+ const HELPER_TYPES = `\
122
+ /** @internal Maps a web component class to its public props only.
123
+ * keyof T skips private/protected members, so this avoids the TS error
124
+ * "property may not be private or protected" on exported anonymous types. */
125
+ type PublicOf<T> = { [K in keyof T]?: T[K] };
126
+
127
+ /** @internal Safe React HTML attributes for web component wrappers.
128
+ * onChange/onInput use method signatures for bivariant parameter checking so both
129
+ * native Event and CustomEvent callbacks are accepted. */
130
+ interface HTMLWCProps extends React.AriaAttributes {
131
+ className?: string; style?: React.CSSProperties; id?: string; slot?: string;
132
+ tabIndex?: number; dir?: string; lang?: string; title?: string;
133
+ onClick?: React.MouseEventHandler<HTMLElement>;
134
+ onDoubleClick?: React.MouseEventHandler<HTMLElement>;
135
+ onContextMenu?: React.MouseEventHandler<HTMLElement>;
136
+ onMouseEnter?: React.MouseEventHandler<HTMLElement>;
137
+ onMouseLeave?: React.MouseEventHandler<HTMLElement>;
138
+ onMouseDown?: React.MouseEventHandler<HTMLElement>;
139
+ onMouseUp?: React.MouseEventHandler<HTMLElement>;
140
+ onMouseMove?: React.MouseEventHandler<HTMLElement>;
141
+ onKeyDown?: React.KeyboardEventHandler<HTMLElement>;
142
+ onKeyUp?: React.KeyboardEventHandler<HTMLElement>;
143
+ onFocus?: React.FocusEventHandler<HTMLElement>;
144
+ onBlur?: React.FocusEventHandler<HTMLElement>;
145
+ onScroll?: React.UIEventHandler<HTMLElement>;
146
+ onWheel?: React.WheelEventHandler<HTMLElement>;
147
+ onChange?(e: Event): void;
148
+ onInput?(e: Event): void;
149
+ }
150
+ `;
151
+
152
+ // ── String utilities ─────────────────────────────────────────────────────────
153
+
154
+ function normalizeWhitespace(value: string): string {
155
+ return value.replace(/\s+/g, ' ').trim();
156
+ }
157
+
158
+ function normalizePropertyName(name: string): string | null {
159
+ let s = name.trim();
160
+ if (!s) return null;
161
+
162
+ const bracketQuoted = s.match(/^\[\s*['"](.+?)['"]\s*\]$/);
163
+ if (bracketQuoted) {
164
+ s = bracketQuoted[1];
165
+ } else {
166
+ const bracket = s.match(/^\[\s*(.+?)\s*\]$/);
167
+ if (bracket) s = bracket[1];
168
+ const quoted = s.match(/^['"](.+?)['"]$/);
169
+ if (quoted) s = quoted[1];
170
+ }
171
+
172
+ s = s.trim();
173
+ if (!s || s.includes('\n') || s.includes('\r')) return null;
174
+ return s;
175
+ }
176
+
177
+ function toPascalCase(value: string): string {
178
+ return value
179
+ .split(/[^a-zA-Z0-9]+/)
180
+ .filter(Boolean)
181
+ .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
182
+ .join('');
183
+ }
184
+
185
+ // ── Type resolution utilities ─────────────────────────────────────────────────
186
+
187
+ function isPrimitiveToken(token: string): boolean {
188
+ return PRIMITIVE_TOKENS.has(token);
189
+ }
190
+
191
+ function isKnownTypeIdentifier(identifier: string): boolean {
192
+ return KNOWN_TYPE_NAMES.has(identifier) || identifier.startsWith('globalThis.');
193
+ }
194
+
195
+ function getIdentifierTokens(typeText: string): string[] {
196
+ return typeText.match(IDENTIFIER_TOKEN_REGEX) ?? [];
197
+ }
198
+
199
+ function createTypeImportState(): TypeImportState {
200
+ return {
201
+ importsByIdentifier: new Map(),
202
+ ambiguousIdentifiers: new Set(),
203
+ usedImports: new Map(),
204
+ wildcardExportModules: new Set(),
205
+ };
206
+ }
207
+
208
+ function registerTypeImport(state: TypeImportState, identifier: string, moduleSpecifier: string): void {
209
+ if (state.ambiguousIdentifiers.has(identifier)) return;
210
+
211
+ const existing = state.importsByIdentifier.get(identifier);
212
+ if (existing && existing !== moduleSpecifier) {
213
+ state.importsByIdentifier.delete(identifier);
214
+ state.ambiguousIdentifiers.add(identifier);
215
+ return;
216
+ }
217
+
218
+ if (!existing) {
219
+ state.importsByIdentifier.set(identifier, moduleSpecifier);
220
+ }
221
+ }
222
+
223
+ function trackImportedIdentifierUsage(state: TypeImportState, identifier: string, moduleSpecifier: string): void {
224
+ if (!state.usedImports.has(moduleSpecifier)) {
225
+ state.usedImports.set(moduleSpecifier, new Set());
226
+ }
227
+ state.usedImports.get(moduleSpecifier)!.add(identifier);
228
+ }
229
+
230
+ function isBareModuleSpecifier(moduleSpecifier: string): boolean {
231
+ return !!moduleSpecifier && !moduleSpecifier.startsWith('.') && !moduleSpecifier.startsWith('/');
232
+ }
233
+
234
+ function canUseComplexType(typeText: string, typeImportState?: TypeImportState): boolean {
235
+ if (!typeText) return false;
236
+ if (/[{};=]/.test(typeText) || /=>/.test(typeText)) return false;
237
+ if (!/^[A-Za-z0-9_$<>\[\]()|&,.?'"`\s:-]+$/.test(typeText)) return false;
238
+
239
+ for (const token of getIdentifierTokens(typeText)) {
240
+ if (isPrimitiveToken(token) || isKnownTypeIdentifier(token)) continue;
241
+
242
+ const root = token.split('.')[0];
243
+ if (
244
+ typeImportState &&
245
+ !typeImportState.ambiguousIdentifiers.has(root) &&
246
+ typeImportState.importsByIdentifier.has(root)
247
+ ) {
248
+ trackImportedIdentifierUsage(typeImportState, root, typeImportState.importsByIdentifier.get(root)!);
249
+ continue;
250
+ }
251
+
252
+ return false;
253
+ }
254
+
255
+ return true;
256
+ }
257
+
258
+ function toSafeType(typeText?: string, typeImportState?: TypeImportState): string {
259
+ if (!typeText) return 'unknown';
260
+ const normalized = normalizeWhitespace(typeText);
261
+ if (!normalized) return 'unknown';
262
+
263
+ if (PRIMITIVE_UNION_REGEX.test(normalized)) return normalized;
264
+
265
+ if (normalized.endsWith('[]') && PRIMITIVE_UNION_REGEX.test(normalized.slice(0, -2).trim())) {
266
+ return normalized;
267
+ }
268
+
269
+ return canUseComplexType(normalized, typeImportState) ? normalized : 'unknown';
270
+ }
271
+
272
+ function extractDetailTypeFromDescription(description?: string): string | undefined {
273
+ return description?.match(/detail:\s*`([^`]+)`/)?.[1]?.trim();
274
+ }
275
+
276
+ /**
277
+ * Maps a CEM event type string to a TypeScript handler signature.
278
+ *
279
+ * - Known DOM event classes (Event, MouseEvent, FocusEvent, etc.) → `(event: T) => void`
280
+ * - `CustomEvent<Detail>` → `(event: CustomEvent<Detail>) => void`
281
+ * - Bare `CustomEvent` or unresolvable types → `(event: CustomEvent<unknown>) => void`
282
+ */
283
+ function toEventHandlerType(
284
+ typeText?: string,
285
+ typeImportState?: TypeImportState,
286
+ description?: string,
287
+ ): string {
288
+ if (typeText) {
289
+ const normalized = typeText.trim();
290
+
291
+ // Known DOM event classes map directly — not wrapped as CustomEvent<T> detail.
292
+ if (DOM_EVENT_CLASS_NAMES.has(normalized)) {
293
+ return `(event: ${normalized}) => void`;
294
+ }
295
+
296
+ if (normalized.startsWith('CustomEvent<')) {
297
+ const match = normalized.match(/^CustomEvent<(.+)>$/);
298
+ if (match) {
299
+ const detailType = toSafeType(match[1]?.trim(), typeImportState);
300
+ return `(event: CustomEvent<${detailType}>) => void`;
301
+ }
302
+ }
303
+
304
+ // For any other resolvable non-DOM type, treat it as the CustomEvent detail payload.
305
+ if (normalized !== 'CustomEvent') {
306
+ const safeType = toSafeType(normalized, typeImportState);
307
+ if (safeType !== 'unknown') {
308
+ return `(event: CustomEvent<${safeType}>) => void`;
309
+ }
310
+ }
311
+ }
312
+
313
+ const detailFromDesc = extractDetailTypeFromDescription(description);
314
+ if (detailFromDesc) {
315
+ return `(event: CustomEvent<${toSafeType(detailFromDesc, typeImportState)}>) => void`;
316
+ }
317
+
318
+ return '(event: CustomEvent<unknown>) => void';
319
+ }
320
+
321
+ // ── Path helpers ─────────────────────────────────────────────────────────────
322
+
323
+ function getCEMManifestPath(cwd: string, packageJson: Record<string, unknown>): string {
324
+ if (typeof packageJson.customElements === 'string' && packageJson.customElements.trim()) {
325
+ return resolve(cwd, packageJson.customElements);
326
+ }
327
+ return resolve(cwd, 'dist/custom-elements.json');
328
+ }
329
+
330
+ // CEM paths are relative to src/ (e.g. "src/entities/entities.ts").
331
+ // react.mjs/cjs live in dist/ while compiled JS lives in dist/esm/.
332
+ function cemModulePathToJsImport(modulePath: string): string {
333
+ let p = modulePath.startsWith('src/') ? modulePath.slice(4) : modulePath;
334
+ if (p.endsWith('.tsx')) p = `${p.slice(0, -4)}.js`;
335
+ else if (p.endsWith('.ts')) p = `${p.slice(0, -3)}.js`;
336
+ return `./esm/${p}`;
337
+ }
338
+
339
+ function cemModulePathToDtsImport(modulePath: string): string {
340
+ let p = modulePath.startsWith('src/') ? modulePath.slice(4) : modulePath;
341
+ const lastDot = p.lastIndexOf('.');
342
+ return `./${lastDot !== -1 ? p.slice(0, lastDot) : p}`;
343
+ }
344
+
345
+ // ── CEM traversal ─────────────────────────────────────────────────────────────
346
+
347
+ function collectCustomElements(manifest: CEMManifest): CEMElementEntry[] {
348
+ const elements: CEMElementEntry[] = [];
349
+ for (const mod of manifest.modules ?? []) {
350
+ const modulePath = mod.path ?? '';
351
+ for (const decl of mod.declarations ?? []) {
352
+ if (decl.customElement && decl.tagName) {
353
+ elements.push({ declaration: decl, modulePath });
354
+ }
355
+ }
356
+ }
357
+ return elements;
358
+ }
359
+
360
+ function mergeUniqueByKey<T>(base: T[], extra: T[], getKey: (item: T) => string | null): T[] {
361
+ const merged = [...base];
362
+ const knownKeys = new Set(base.map(getKey).filter((k): k is string => !!k));
363
+ for (const item of extra) {
364
+ const key = getKey(item);
365
+ if (!key || !knownKeys.has(key)) {
366
+ merged.push(item);
367
+ if (key) knownKeys.add(key);
368
+ }
369
+ }
370
+ return merged;
371
+ }
372
+
373
+ function mergeDeclarationMetadata(base: CEMDeclaration, inherited: CEMDeclaration): CEMDeclaration {
374
+ const attrKey = (a: CEMMember) => normalizePropertyName(a.fieldName ?? a.name ?? '');
375
+ const nameKey = (m: CEMMember) => normalizePropertyName(m.name ?? '');
376
+ const eventKey = (e: CEMEvent) => normalizePropertyName(e.name ?? '');
377
+ return {
378
+ ...base,
379
+ attributes: mergeUniqueByKey(base.attributes ?? [], inherited.attributes ?? [], attrKey),
380
+ members: mergeUniqueByKey(base.members ?? [], inherited.members ?? [], nameKey),
381
+ events: mergeUniqueByKey(base.events ?? [], inherited.events ?? [], eventKey),
382
+ };
383
+ }
384
+
385
+ function createDeclarationLookup(manifest: CEMManifest): {
386
+ byTagAndName: Map<string, CEMDeclaration>;
387
+ byTag: Map<string, CEMDeclaration>;
388
+ } {
389
+ const byTagAndName = new Map<string, CEMDeclaration>();
390
+ const byTag = new Map<string, CEMDeclaration>();
391
+ for (const mod of manifest.modules ?? []) {
392
+ for (const decl of mod.declarations ?? []) {
393
+ if (!decl.customElement || !decl.tagName) continue;
394
+ if (decl.name) byTagAndName.set(`${decl.tagName}::${decl.name}`, decl);
395
+ if (!byTag.has(decl.tagName)) byTag.set(decl.tagName, decl);
396
+ }
397
+ }
398
+ return { byTagAndName, byTag };
399
+ }
400
+
401
+ async function mergeFastInheritanceFromManifest(
402
+ cwd: string,
403
+ entries: CEMElementEntry[],
404
+ ): Promise<CEMElementEntry[]> {
405
+ const fastManifestPath = resolve(cwd, 'ms-fast-components/custom-elements.json');
406
+ if (!(await fileExists(fastManifestPath))) return entries;
407
+
408
+ const fastManifest = JSON.parse(await readFile(fastManifestPath, 'utf8')) as CEMManifest;
409
+ const { byTagAndName, byTag } = createDeclarationLookup(fastManifest);
410
+
411
+ return entries.map((entry) => {
412
+ const { declaration } = entry;
413
+ if (!declaration.tagName || declaration.superclass?.package !== '@microsoft/fast-components') {
414
+ return entry;
415
+ }
416
+
417
+ const inherited =
418
+ byTagAndName.get(`${declaration.tagName}::${declaration.name ?? ''}`) ??
419
+ byTagAndName.get(`${declaration.tagName}::${declaration.superclass?.name ?? ''}`) ??
420
+ byTag.get(declaration.tagName);
421
+
422
+ return inherited ? { ...entry, declaration: mergeDeclarationMetadata(declaration, inherited) } : entry;
423
+ });
424
+ }
425
+
426
+ // ── Wrapper event helpers ─────────────────────────────────────────────────────
427
+
428
+ function buildWrapperEventEntries(
429
+ declaration: CEMDeclaration,
430
+ ): Array<{ handlerName: string; eventName: string }> {
431
+ const result: Array<{ handlerName: string; eventName: string }> = [];
432
+ const seen = new Set<string>();
433
+ for (const event of declaration.events ?? []) {
434
+ if (!event.name) continue;
435
+ const normalized = normalizePropertyName(event.name);
436
+ if (!normalized) continue;
437
+ const handlerName = `on${toPascalCase(normalized)}`;
438
+ if (!handlerName || REACT_NATIVE_EVENT_HANDLER_NAMES.has(handlerName) || seen.has(handlerName)) continue;
439
+ seen.add(handlerName);
440
+ result.push({ handlerName, eventName: event.name });
441
+ }
442
+ return result;
443
+ }
444
+
445
+ function groupEntriesByPath(entries: CEMElementEntry[]): Map<string, CEMElementEntry[]> {
446
+ const byPath = new Map<string, CEMElementEntry[]>();
447
+ for (const entry of entries) {
448
+ if (!byPath.has(entry.modulePath)) byPath.set(entry.modulePath, []);
449
+ byPath.get(entry.modulePath)!.push(entry);
450
+ }
451
+ return byPath;
452
+ }
453
+
454
+ function cloneTypeImportStateForWrapper(original: TypeImportState): TypeImportState {
455
+ return {
456
+ importsByIdentifier: new Map(original.importsByIdentifier),
457
+ ambiguousIdentifiers: new Set(original.ambiguousIdentifiers),
458
+ usedImports: new Map(),
459
+ wildcardExportModules: new Set(original.wildcardExportModules),
460
+ };
461
+ }
462
+
463
+ // ── Code generation ───────────────────────────────────────────────────────────
464
+
465
+ function renderImportLines(usedImports: Map<string, Set<string>>): string[] {
466
+ return [...usedImports.entries()]
467
+ .sort(([a], [b]) => a.localeCompare(b))
468
+ .map(([spec, ids]) => `import type { ${[...ids].sort().join(', ')} } from '${spec}';`);
469
+ }
470
+
471
+ function generateReactWrapperJs(entries: CEMElementEntry[], format: 'esm' | 'cjs'): string {
472
+ const valid = entries.filter((e) => e.declaration.name && e.modulePath);
473
+ if (!valid.length) return '';
474
+
475
+ const esm = format === 'esm';
476
+ const lines: string[] = [
477
+ '/**',
478
+ ' * AUTO-GENERATED FILE - DO NOT EDIT.',
479
+ ' * Generated from custom-elements manifest.',
480
+ ' */',
481
+ '',
482
+ ];
483
+
484
+ if (!esm) lines.push("'use strict';", '');
485
+
486
+ if (esm) {
487
+ lines.push(
488
+ "import { provideReactWrapper } from '@microsoft/fast-react-wrapper';",
489
+ "import React from 'react';",
490
+ );
491
+ } else {
492
+ lines.push(
493
+ "const { provideReactWrapper } = require('@microsoft/fast-react-wrapper');",
494
+ "const React = require('react');",
495
+ );
496
+ }
497
+
498
+ for (const [modulePath, pathEntries] of [...groupEntriesByPath(valid).entries()].sort()) {
499
+ const sorted = [...pathEntries].sort((a, b) => a.declaration.name!.localeCompare(b.declaration.name!));
500
+ const jsPath = cemModulePathToJsImport(modulePath);
501
+ if (esm) {
502
+ lines.push(`import { ${sorted.map((e) => `${e.declaration.name} as ${e.declaration.name}WC`).join(', ')} } from '${jsPath}';`);
503
+ } else {
504
+ lines.push(`const { ${sorted.map((e) => `${e.declaration.name}: ${e.declaration.name}WC`).join(', ')} } = require('${jsPath}');`);
505
+ }
506
+ }
507
+
508
+ lines.push('', 'const { wrap } = provideReactWrapper(React);', '');
509
+
510
+ for (const { declaration } of valid) {
511
+ const name = declaration.name!;
512
+ const events = buildWrapperEventEntries(declaration);
513
+ const prefix = esm ? 'export const' : 'const';
514
+ if (!events.length) {
515
+ lines.push(`${prefix} ${name} = wrap(${name}WC);`);
516
+ } else {
517
+ lines.push(`${prefix} ${name} = wrap(${name}WC, {`);
518
+ lines.push(' events: {');
519
+ for (const { handlerName, eventName } of events) lines.push(` ${handlerName}: '${eventName}',`);
520
+ lines.push(' },');
521
+ lines.push('});');
522
+ }
523
+ lines.push('');
524
+ }
525
+
526
+ if (!esm) {
527
+ lines.push('module.exports = {');
528
+ for (const { declaration } of valid) lines.push(` ${declaration.name},`);
529
+ lines.push('};', '');
530
+ }
531
+
532
+ return lines.join('\n');
533
+ }
534
+
535
+ function generateReactWrapperDts(entries: CEMElementEntry[], typeImportState: TypeImportState): string {
536
+ const valid = entries.filter((e) => e.declaration.name && e.modulePath);
537
+ if (!valid.length) return '';
538
+
539
+ const wrapperTypeState = cloneTypeImportStateForWrapper(typeImportState);
540
+
541
+ const classImports: string[] = [];
542
+ for (const [modulePath, pathEntries] of [...groupEntriesByPath(valid).entries()].sort()) {
543
+ const names = [...pathEntries]
544
+ .sort((a, b) => a.declaration.name!.localeCompare(b.declaration.name!))
545
+ .map((e) => `${e.declaration.name} as ${e.declaration.name}WC`)
546
+ .join(', ');
547
+ classImports.push(`import type { ${names} } from '${cemModulePathToDtsImport(modulePath)}';`);
548
+ }
549
+
550
+ const declarationLines: string[] = [];
551
+ for (const { declaration } of valid) {
552
+ const name = declaration.name!;
553
+
554
+ // Build event lookup scoped to this element to avoid cross-element type pollution.
555
+ const eventsByName = new Map(
556
+ (declaration.events ?? [])
557
+ .filter((e) => e.name)
558
+ .map((e) => [e.name!, e] as const),
559
+ );
560
+
561
+ const eventLines = buildWrapperEventEntries(declaration).map(({ handlerName, eventName }) => {
562
+ const event = eventsByName.get(eventName);
563
+ const handlerType = toEventHandlerType(event?.type?.text, wrapperTypeState, event?.description);
564
+ return ` ${handlerName}?: ${handlerType};`;
565
+ });
566
+
567
+ declarationLines.push(
568
+ `export declare const ${name}: React.ForwardRefExoticComponent<`,
569
+ ` React.PropsWithChildren<`,
570
+ ` Omit<PublicOf<${name}WC>, 'children' | 'style'> &`,
571
+ ` HTMLWCProps & {`,
572
+ ...eventLines,
573
+ ' }',
574
+ ` > & React.RefAttributes<${name}WC>`,
575
+ '>;',
576
+ '',
577
+ );
578
+ }
579
+
580
+ return [
581
+ '/**',
582
+ ' * AUTO-GENERATED FILE - DO NOT EDIT.',
583
+ ' * Generated from custom-elements manifest.',
584
+ ' */',
585
+ '',
586
+ "import type React from 'react';",
587
+ ...classImports,
588
+ ...renderImportLines(wrapperTypeState.usedImports),
589
+ '',
590
+ HELPER_TYPES,
591
+ ...declarationLines,
592
+ 'export {};',
593
+ '',
594
+ ].join('\n');
595
+ }
596
+
597
+ // ── File system helpers ───────────────────────────────────────────────────────
598
+
599
+ async function fileExists(path: string): Promise<boolean> {
600
+ try {
601
+ await stat(path);
602
+ return true;
603
+ } catch {
604
+ return false;
605
+ }
606
+ }
607
+
608
+ async function collectFilesRecursively(
609
+ rootDirectory: string,
610
+ isTargetFile: (fileName: string) => boolean,
611
+ ): Promise<string[]> {
612
+ const files: string[] = [];
613
+ const stack: string[] = [rootDirectory];
614
+ while (stack.length) {
615
+ const dir = stack.pop()!;
616
+ const dirEntries = await readdir(dir, { withFileTypes: true });
617
+ for (const entry of dirEntries) {
618
+ const fullPath = resolve(dir, entry.name);
619
+ if (entry.isDirectory()) stack.push(fullPath);
620
+ else if (entry.isFile() && isTargetFile(entry.name)) files.push(fullPath);
621
+ }
622
+ }
623
+ return files;
624
+ }
625
+
626
+ async function collectTypeMetadataFiles(
627
+ distDirectory: string,
628
+ ): Promise<{ apiJsonFiles: string[]; dtsFiles: string[] }> {
629
+ const [apiJsonFiles, dtsFiles] = await Promise.all([
630
+ collectFilesRecursively(distDirectory, (n) => n.endsWith('.api.json')),
631
+ collectFilesRecursively(distDirectory, (n) => n.endsWith('.d.ts')),
632
+ ]);
633
+ return { apiJsonFiles, dtsFiles };
634
+ }
635
+
636
+ // ── Type import state builders ────────────────────────────────────────────────
637
+
638
+ function addCanonicalReferenceFromValue(value: unknown, state: TypeImportState): void {
639
+ if (!value || typeof value !== 'object') return;
640
+
641
+ if (Array.isArray(value)) {
642
+ for (const item of value) addCanonicalReferenceFromValue(item, state);
643
+ return;
644
+ }
645
+
646
+ const record = value as Record<string, unknown>;
647
+ if (typeof record.canonicalReference === 'string') {
648
+ const match = record.canonicalReference.match(/^(@[^!]+)!([^:]+):/);
649
+ if (match) {
650
+ const [, moduleSpecifier, identifier] = match;
651
+ if (identifier && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) {
652
+ registerTypeImport(state, identifier, moduleSpecifier);
653
+ }
654
+ }
655
+ }
656
+
657
+ for (const nested of Object.values(record)) {
658
+ if (nested && typeof nested === 'object') addCanonicalReferenceFromValue(nested, state);
659
+ }
660
+ }
661
+
662
+ function parseImportsFromDtsContent(content: string, state: TypeImportState): void {
663
+ const blockRe = /(?:import|export)\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
664
+ for (const match of content.matchAll(blockRe)) {
665
+ const moduleSpecifier = match[2] ?? '';
666
+ if (!isBareModuleSpecifier(moduleSpecifier)) continue;
667
+ for (const raw of (match[1] ?? '').split(',')) {
668
+ const entry = raw.trim();
669
+ if (!entry) continue;
670
+ const alias = entry.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/);
671
+ if (alias) {
672
+ registerTypeImport(state, alias[2], moduleSpecifier);
673
+ continue;
674
+ }
675
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(entry)) registerTypeImport(state, entry, moduleSpecifier);
676
+ }
677
+ }
678
+
679
+ const reExportRe = /export\s+\*\s+from\s+['"]([^'"]+)['"]/g;
680
+ for (const match of content.matchAll(reExportRe)) {
681
+ const moduleSpecifier = match[1] ?? '';
682
+ if (isBareModuleSpecifier(moduleSpecifier)) state.wildcardExportModules.add(moduleSpecifier);
683
+ }
684
+ }
685
+
686
+ async function parseApiJsonFiles(filePaths: string[], state: TypeImportState): Promise<void> {
687
+ await Promise.all(
688
+ filePaths.map(async (filePath) => {
689
+ const json = JSON.parse(await readFile(filePath, 'utf8')) as unknown;
690
+ addCanonicalReferenceFromValue(json, state);
691
+ }),
692
+ );
693
+ }
694
+
695
+ async function parseDtsFiles(filePaths: string[], state: TypeImportState): Promise<void> {
696
+ await Promise.all(
697
+ filePaths.map(async (filePath) => {
698
+ parseImportsFromDtsContent(await readFile(filePath, 'utf8'), state);
699
+ }),
700
+ );
701
+ }
702
+
703
+ function mergeTypeImportStateInto(target: TypeImportState, source: TypeImportState): void {
704
+ for (const id of source.ambiguousIdentifiers) {
705
+ target.importsByIdentifier.delete(id);
706
+ target.ambiguousIdentifiers.add(id);
707
+ }
708
+ for (const [identifier, spec] of source.importsByIdentifier) {
709
+ registerTypeImport(target, identifier, spec);
710
+ }
711
+ for (const mod of source.wildcardExportModules) {
712
+ target.wildcardExportModules.add(mod);
713
+ }
714
+ }
715
+
716
+ async function findWorkspaceRoot(startDirectory: string): Promise<string> {
717
+ let dir = startDirectory;
718
+ while (true) {
719
+ const [hasPackages, hasPackageJson] = await Promise.all([
720
+ fileExists(resolve(dir, 'packages')),
721
+ fileExists(resolve(dir, 'package.json')),
722
+ ]);
723
+ if (hasPackages && hasPackageJson) return dir;
724
+ const parent = dirname(dir);
725
+ if (parent === dir) return startDirectory;
726
+ dir = parent;
727
+ }
728
+ }
729
+
730
+ async function getWorkspacePackageDirectoryByName(
731
+ workspaceRoot: string,
732
+ packageName: string,
733
+ ): Promise<string | undefined> {
734
+ const packagesDir = resolve(workspaceRoot, 'packages');
735
+ if (!(await fileExists(packagesDir))) return undefined;
736
+
737
+ const packageJsonFiles = await collectFilesRecursively(packagesDir, (n) => n === 'package.json');
738
+ for (const jsonPath of packageJsonFiles) {
739
+ const pkg = JSON.parse(await readFile(jsonPath, 'utf8')) as Record<string, unknown>;
740
+ if (pkg.name === packageName) return dirname(jsonPath);
741
+ }
742
+ return undefined;
743
+ }
744
+
745
+ async function enrichFromWorkspaceWildcardExports(cwd: string, state: TypeImportState): Promise<void> {
746
+ const workspaceRoot = await findWorkspaceRoot(cwd);
747
+ // for...of over a Set processes items added inside the loop (spec-guaranteed behaviour).
748
+ const pending = new Set(
749
+ [...state.wildcardExportModules].filter((m) => m.startsWith('@genesislcap/')),
750
+ );
751
+
752
+ for (const moduleSpecifier of pending) {
753
+ const pkgDir = await getWorkspacePackageDirectoryByName(workspaceRoot, moduleSpecifier);
754
+ if (!pkgDir) continue;
755
+
756
+ const distDir = resolve(pkgDir, 'dist');
757
+ if (!(await fileExists(distDir))) continue;
758
+
759
+ const refState = createTypeImportState();
760
+ const { apiJsonFiles, dtsFiles } = await collectTypeMetadataFiles(distDir);
761
+ await parseApiJsonFiles(apiJsonFiles, refState);
762
+ await parseDtsFiles(dtsFiles, refState);
763
+ mergeTypeImportStateInto(state, refState);
764
+
765
+ for (const mod of refState.wildcardExportModules) {
766
+ if (mod.startsWith('@genesislcap/')) pending.add(mod);
767
+ }
768
+ }
769
+ }
770
+
771
+ async function buildTypeImportState(cwd: string): Promise<TypeImportState> {
772
+ const state = createTypeImportState();
773
+ const distDir = resolve(cwd, 'dist');
774
+ if (!(await fileExists(distDir))) return state;
775
+
776
+ const { apiJsonFiles, dtsFiles } = await collectTypeMetadataFiles(distDir);
777
+ await parseApiJsonFiles(apiJsonFiles, state);
778
+ await parseDtsFiles(dtsFiles, state);
779
+ await enrichFromWorkspaceWildcardExports(cwd, state);
780
+
781
+ return state;
782
+ }
783
+
784
+ // ── Entry point ───────────────────────────────────────────────────────────────
785
+
786
+ export async function generateReactWrappers(cwd: string): Promise<GenerateResult> {
787
+ const packageJsonPath = resolve(cwd, 'package.json');
788
+ if (!(await fileExists(packageJsonPath))) {
789
+ return { generated: false, reason: 'No package.json found.' };
790
+ }
791
+
792
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as Record<string, unknown>;
793
+ const manifestPath = getCEMManifestPath(cwd, packageJson);
794
+ if (!(await fileExists(manifestPath))) {
795
+ return { generated: false, reason: 'No custom elements manifest found.' };
796
+ }
797
+
798
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as CEMManifest;
799
+ const rawEntries = collectCustomElements(manifest);
800
+ if (!rawEntries.length) {
801
+ return { generated: false, reason: 'No custom elements discovered in manifest.' };
802
+ }
803
+
804
+ const entries = await mergeFastInheritanceFromManifest(cwd, rawEntries);
805
+ const hasEvents = entries.some((e) => (e.declaration.events ?? []).length > 0);
806
+ if (!hasEvents) {
807
+ return { generated: false, reason: 'No custom events found in any element.' };
808
+ }
809
+
810
+ const dtsRoot = resolve(cwd, 'dist/dts');
811
+ await mkdir(dtsRoot, { recursive: true });
812
+
813
+ const typeImportState = await buildTypeImportState(cwd);
814
+ const reactDtsPath = resolve(dtsRoot, 'react.d.ts');
815
+
816
+ await Promise.all([
817
+ writeFile(resolve(cwd, 'dist/react.mjs'), generateReactWrapperJs(entries, 'esm'), 'utf8'),
818
+ writeFile(resolve(cwd, 'dist/react.cjs'), generateReactWrapperJs(entries, 'cjs'), 'utf8'),
819
+ writeFile(reactDtsPath, generateReactWrapperDts(entries, typeImportState), 'utf8'),
820
+ ]);
821
+
822
+ return { generated: true, path: reactDtsPath };
823
+ }