@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.
- package/package.json +3 -4
- package/src/index.ts +0 -68
- package/src/react-wrapper-generator.ts +0 -1309
- package/src/resolve-cem-config-path.ts +0 -24
- package/tsconfig.json +0 -11
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -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
|
-
}
|