@oddkin/analyzer 0.0.0-stage → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -2
  3. package/dist/bin.d.ts +3 -0
  4. package/dist/bin.d.ts.map +1 -0
  5. package/dist/bin.js +65 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/contracts.d.ts +29 -0
  8. package/dist/contracts.d.ts.map +1 -0
  9. package/dist/contracts.js +57 -0
  10. package/dist/contracts.js.map +1 -0
  11. package/dist/docs.d.ts +34 -0
  12. package/dist/docs.d.ts.map +1 -0
  13. package/dist/docs.js +63 -0
  14. package/dist/docs.js.map +1 -0
  15. package/dist/emit.d.ts +38 -0
  16. package/dist/emit.d.ts.map +1 -0
  17. package/dist/emit.js +406 -0
  18. package/dist/emit.js.map +1 -0
  19. package/dist/examples.d.ts +26 -0
  20. package/dist/examples.d.ts.map +1 -0
  21. package/dist/examples.js +185 -0
  22. package/dist/examples.js.map +1 -0
  23. package/dist/extract.d.ts +208 -0
  24. package/dist/extract.d.ts.map +1 -0
  25. package/dist/extract.js +1570 -0
  26. package/dist/extract.js.map +1 -0
  27. package/dist/foundations.d.ts +36 -0
  28. package/dist/foundations.d.ts.map +1 -0
  29. package/dist/foundations.js +108 -0
  30. package/dist/foundations.js.map +1 -0
  31. package/dist/history.d.ts +10 -0
  32. package/dist/history.d.ts.map +1 -0
  33. package/dist/history.js +62 -0
  34. package/dist/history.js.map +1 -0
  35. package/dist/impact-bin.d.ts +3 -0
  36. package/dist/impact-bin.d.ts.map +1 -0
  37. package/dist/impact-bin.js +7 -0
  38. package/dist/impact-bin.js.map +1 -0
  39. package/dist/impact-cli.d.ts +8 -0
  40. package/dist/impact-cli.d.ts.map +1 -0
  41. package/dist/impact-cli.js +121 -0
  42. package/dist/impact-cli.js.map +1 -0
  43. package/dist/impact.d.ts +282 -0
  44. package/dist/impact.d.ts.map +1 -0
  45. package/dist/impact.js +744 -0
  46. package/dist/impact.js.map +1 -0
  47. package/dist/index.d.ts +82 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +227 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/published.d.ts +43 -0
  52. package/dist/published.d.ts.map +1 -0
  53. package/dist/published.js +62 -0
  54. package/dist/published.js.map +1 -0
  55. package/dist/references.d.ts +70 -0
  56. package/dist/references.d.ts.map +1 -0
  57. package/dist/references.js +255 -0
  58. package/dist/references.js.map +1 -0
  59. package/dist/style-mappings.d.ts +46 -0
  60. package/dist/style-mappings.d.ts.map +1 -0
  61. package/dist/style-mappings.js +246 -0
  62. package/dist/style-mappings.js.map +1 -0
  63. package/dist/stylesheets.d.ts +56 -0
  64. package/dist/stylesheets.d.ts.map +1 -0
  65. package/dist/stylesheets.js +83 -0
  66. package/dist/stylesheets.js.map +1 -0
  67. package/dist/verification.d.ts +62 -0
  68. package/dist/verification.d.ts.map +1 -0
  69. package/dist/verification.js +115 -0
  70. package/dist/verification.js.map +1 -0
  71. package/package.json +40 -4
@@ -0,0 +1,1570 @@
1
+ /**
2
+ * Static extraction over a TypeScript program. Never imports or executes analyzed modules.
3
+ * Contract: docs/architecture/metadata-and-analyzer.md §2.
4
+ */
5
+ import { readFileSync } from 'node:fs';
6
+ import { dirname, relative, resolve, sep } from 'node:path';
7
+ import ts from 'typescript';
8
+ import { createPublishedReader } from './published.js';
9
+ import { styleMappingsOf, } from './style-mappings.js';
10
+ const ODDKIN_CORE = /(?:[\\/]|^)(?:@oddkin[\\/]core|packages[\\/]core)[\\/]/;
11
+ const ODDKIN_LAYOUT = /(?:[\\/]|^)(?:@oddkin[\\/]components|packages[\\/]components)[\\/](?:src|dist)[\\/]layout[\\/]/;
12
+ const TAG_NAME = /^[a-z][a-z0-9._-]*-[a-z0-9._-]*$/;
13
+ const DOC_TAGS = [
14
+ 'a11y',
15
+ 'keyboard',
16
+ 'category',
17
+ 'useFor',
18
+ 'notFor',
19
+ 'limitation',
20
+ 'related',
21
+ 'deprecated',
22
+ 'since',
23
+ 'replacement',
24
+ 'migration',
25
+ 'customization',
26
+ 'previewRole',
27
+ 'updated',
28
+ ];
29
+ /** A real calendar date written `YYYY-MM-DD`. */
30
+ const isDate = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) &&
31
+ !Number.isNaN(Date.parse(value)) &&
32
+ new Date(`${value}T00:00:00Z`).toISOString().startsWith(value);
33
+ export function extract(program, root) {
34
+ const checker = program.getTypeChecker();
35
+ const diagnostics = [];
36
+ const components = new Map();
37
+ const registrations = [];
38
+ const recipes = [];
39
+ const models = [];
40
+ const definitions = new Map();
41
+ const exportsBySource = new Map();
42
+ const published = createPublishedReader();
43
+ const publishedComponents = new Map();
44
+ const templateSources = new Map();
45
+ const styleSources = new Map();
46
+ const rel = (file) => relative(root, file).split(sep).join('/');
47
+ const where = (node) => {
48
+ const source = node.getSourceFile();
49
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
50
+ return { file: rel(source.fileName), line: line + 1, column: character + 1 };
51
+ };
52
+ const report = (code, node, message, suggestion, severity = 'warning') => diagnostics.push({
53
+ code,
54
+ severity,
55
+ message,
56
+ source: where(node),
57
+ ...(suggestion ? { suggestion } : {}),
58
+ });
59
+ const resolveSymbol = (node) => {
60
+ const symbol = checker.getSymbolAtLocation(node);
61
+ return symbol && symbol.flags & ts.SymbolFlags.Alias
62
+ ? checker.getAliasedSymbol(symbol)
63
+ : symbol;
64
+ };
65
+ const fromOddkinCore = (node, name) => {
66
+ const symbol = resolveSymbol(node);
67
+ return (!!symbol &&
68
+ symbol.name === name &&
69
+ !!symbol.declarations?.some((d) => ODDKIN_CORE.test(d.getSourceFile().fileName)));
70
+ };
71
+ // `defineLayoutRecipe` from core or from the Oddkin-bound `@oddkin/components/layout`.
72
+ const isRecipeDefinition = (node) => {
73
+ if (fromOddkinCore(node, 'defineLayoutRecipe'))
74
+ return true;
75
+ const symbol = resolveSymbol(node);
76
+ return (symbol?.name === 'defineLayoutRecipe' &&
77
+ !!symbol.declarations?.some((d) => ODDKIN_LAYOUT.test(d.getSourceFile().fileName)));
78
+ };
79
+ // Recipes are defined at module scope; a call inside a function is a wrapper or factory.
80
+ const insideFunction = (node) => {
81
+ for (let parent = node.parent; parent; parent = parent.parent)
82
+ if (ts.isFunctionLike(parent))
83
+ return true;
84
+ return false;
85
+ };
86
+ const isLibrarySource = (file) => !file.isDeclarationFile &&
87
+ !program.isSourceFileFromExternalLibrary(file) &&
88
+ !file.fileName.includes('/node_modules/');
89
+ const literal = (node) => {
90
+ if (!node)
91
+ return { known: false };
92
+ if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node))
93
+ return { known: true, value: node.getText() };
94
+ if (node.kind === ts.SyntaxKind.TrueKeyword ||
95
+ node.kind === ts.SyntaxKind.FalseKeyword ||
96
+ node.kind === ts.SyntaxKind.NullKeyword) {
97
+ return { known: true, value: node.getText() };
98
+ }
99
+ if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand))
100
+ return { known: true, value: node.getText() };
101
+ return { known: false };
102
+ };
103
+ const stringOf = (node) => node && ts.isStringLiteralLike(node) ? node.text : undefined;
104
+ const booleanOf = (node) => node?.kind === ts.SyntaxKind.TrueKeyword
105
+ ? true
106
+ : node?.kind === ts.SyntaxKind.FalseKeyword
107
+ ? false
108
+ : undefined;
109
+ const objectEntries = (node, owner) => {
110
+ const out = new Map();
111
+ for (const property of node.properties) {
112
+ if (ts.isPropertyAssignment(property) &&
113
+ (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
114
+ out.set(property.name.text, property.initializer);
115
+ }
116
+ else if (ts.isShorthandPropertyAssignment(property)) {
117
+ out.set(property.name.text, property.name);
118
+ }
119
+ else {
120
+ report('analyzer.dynamic.property', property, `${owner}: a spread or computed entry cannot be analyzed statically.`, 'Write each entry as a literal key: value.');
121
+ }
122
+ }
123
+ return out;
124
+ };
125
+ const docOf = (node) => {
126
+ const symbol = node.name
127
+ ? checker.getSymbolAtLocation(node.name)
128
+ : undefined;
129
+ const text = symbol
130
+ ? ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim()
131
+ : '';
132
+ return text || undefined;
133
+ };
134
+ /** `@deprecated` and its companion tags in the JSDoc of a member or variable. */
135
+ const deprecationOf = (node) => {
136
+ const tags = node ? ts.getJSDocTags(node) : [];
137
+ const deprecated = tags.find((t) => t.tagName.text === 'deprecated');
138
+ if (!deprecated)
139
+ return undefined;
140
+ const text = (tag) => tag
141
+ ? (typeof tag.comment === 'string'
142
+ ? tag.comment
143
+ : (ts.getTextOfJSDocComment(tag.comment) ?? '')).trim() || undefined
144
+ : undefined;
145
+ const named = (name) => text(tags.find((t) => t.tagName.text === name));
146
+ const reason = text(deprecated);
147
+ const replacement = named('replacement');
148
+ const migration = named('migration');
149
+ return {
150
+ ...(reason ? { reason } : {}),
151
+ ...(replacement ? { replacement } : {}),
152
+ ...(migration ? { migration } : {}),
153
+ location: where(deprecated),
154
+ };
155
+ };
156
+ // ---- prop.* definitions ----
157
+ function properties(definitions, className) {
158
+ const out = [];
159
+ for (const [name, initializer] of objectEntries(definitions, `${className}.with()`)) {
160
+ const call = initializer;
161
+ const callee = ts.isCallExpression(call) && ts.isPropertyAccessExpression(call.expression)
162
+ ? call.expression
163
+ : undefined;
164
+ if (!callee || !ts.isCallExpression(call) || !fromOddkinCore(callee.expression, 'prop')) {
165
+ report('analyzer.dynamic.definition', initializer, `${className}.${name} is not a prop.*() call; its contract cannot be extracted.`, 'Define it inline, e.g. name: prop.string({ default: "…" }).');
166
+ continue;
167
+ }
168
+ const kind = callee.name.text;
169
+ const values = kind === 'enum' && call.arguments[0] && ts.isArrayLiteralExpression(call.arguments[0])
170
+ ? call.arguments[0].elements.map((e) => stringOf(e) ?? e.getText())
171
+ : undefined;
172
+ const optionsNode = kind === 'enum' ? call.arguments[1] : call.arguments[0];
173
+ const options = optionsNode && ts.isObjectLiteralExpression(optionsNode)
174
+ ? objectEntries(optionsNode, `${className}.${name}`)
175
+ : new Map();
176
+ if (optionsNode && !ts.isObjectLiteralExpression(optionsNode))
177
+ report('analyzer.dynamic.options', optionsNode, `${className}.${name}: options must be an object literal.`);
178
+ const attributeNode = options.get('attribute');
179
+ const propertyOnly = kind === 'object' ||
180
+ kind === 'array' ||
181
+ (kind === 'custom' && !options.has('fromAttribute'));
182
+ let attribute = propertyOnly
183
+ ? null
184
+ : name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
185
+ if (attributeNode) {
186
+ if (attributeNode.kind === ts.SyntaxKind.FalseKeyword)
187
+ attribute = null;
188
+ else if (stringOf(attributeNode) !== undefined)
189
+ attribute = stringOf(attributeNode);
190
+ else
191
+ report('analyzer.dynamic.attribute', attributeNode, `${className}.${name}: attribute must be a string literal or false.`);
192
+ }
193
+ const reflects = booleanOf(options.get('reflect')) ?? false;
194
+ const defaultNode = options.get('default');
195
+ const fallback = kind === 'boolean'
196
+ ? { known: true, value: 'false' }
197
+ : kind === 'array' && !defaultNode
198
+ ? { known: true, value: '[]' }
199
+ : defaultNode
200
+ ? literal(defaultNode)
201
+ : { known: true, value: 'undefined' };
202
+ if (defaultNode &&
203
+ !fallback.known &&
204
+ !ts.isArrowFunction(defaultNode) &&
205
+ !ts.isFunctionExpression(defaultNode)) {
206
+ report('analyzer.dynamic.value', defaultNode, `${className}.${name}: the default is computed; it is recorded as unknown.`);
207
+ }
208
+ const description = stringOf(options.get('description'));
209
+ const deprecated = deprecationOf(initializer.parent);
210
+ out.push({
211
+ name,
212
+ kind,
213
+ type: '',
214
+ attribute,
215
+ reflects,
216
+ default: fallback,
217
+ ...(values ? { values } : {}),
218
+ ...(description ? { description } : {}),
219
+ ...(deprecated ? { deprecated } : {}),
220
+ });
221
+ }
222
+ return out;
223
+ }
224
+ // ---- events ----
225
+ function eventFrom(expression, owner) {
226
+ let call = expression;
227
+ let variable;
228
+ if (ts.isIdentifier(call)) {
229
+ // Shorthand entries ({ countChange }) name the variable through the shorthand symbol.
230
+ const shorthand = ts.isShorthandPropertyAssignment(call.parent)
231
+ ? checker.getShorthandAssignmentValueSymbol(call.parent)
232
+ : undefined;
233
+ const symbol = shorthand ?? resolveSymbol(call);
234
+ const declaration = (symbol && symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol)?.valueDeclaration;
235
+ if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) {
236
+ variable = declaration;
237
+ call = declaration.initializer;
238
+ }
239
+ }
240
+ if (!ts.isCallExpression(call) || !fromOddkinCore(call.expression, 'event')) {
241
+ report('analyzer.dynamic.event', expression, `${owner}: events must be defined with event<Detail>('name', { … }).`);
242
+ return undefined;
243
+ }
244
+ const name = stringOf(call.arguments[0]);
245
+ const init = call.arguments[1];
246
+ if (!name || !init || !ts.isObjectLiteralExpression(init)) {
247
+ report('analyzer.dynamic.event', call, `${owner}: the event name and options must be literals.`);
248
+ return undefined;
249
+ }
250
+ const options = objectEntries(init, `${owner} event`);
251
+ const typeArgument = call.typeArguments?.[0];
252
+ const detailType = typeArgument
253
+ ? checker.typeToString(checker.getTypeFromTypeNode(typeArgument))
254
+ : 'void';
255
+ const syncs = stringOf(options.get('syncs'));
256
+ const intentNode = options.get('intent');
257
+ const intentValue = stringOf(intentNode);
258
+ const intent = intentNode && (intentValue === 'user' || intentValue === 'notification')
259
+ ? { value: intentValue, source: where(intentNode) }
260
+ : undefined;
261
+ if (intentNode && !intent)
262
+ report('analyzer.dynamic.value', intentNode, `${owner} event "${name}": intent must be the literal 'user' or 'notification'; it is recorded as unknown.`);
263
+ const description = stringOf(options.get('description')) ?? (variable ? docOf(variable) : undefined);
264
+ const deprecated = deprecationOf(variable);
265
+ return {
266
+ name,
267
+ detailType,
268
+ bubbles: booleanOf(options.get('bubbles')) ?? false,
269
+ composed: booleanOf(options.get('composed')) ?? false,
270
+ cancelable: booleanOf(options.get('cancelable')) ?? false,
271
+ ...(syncs ? { syncs } : {}),
272
+ ...(intent ? { intent } : {}),
273
+ ...(description ? { description } : {}),
274
+ ...(deprecated ? { deprecated } : {}),
275
+ };
276
+ }
277
+ // ---- templates and styles ----
278
+ const templateText = (node) => ts.isNoSubstitutionTemplateLiteral(node)
279
+ ? node.text
280
+ : [node.head.text, ...node.templateSpans.map((s) => s.literal.text)].join('\u0000');
281
+ function scanTemplates(body, tag, visit) {
282
+ const walk = (node) => {
283
+ if (ts.isTaggedTemplateExpression(node) &&
284
+ ts.isIdentifier(node.tag) &&
285
+ (node.tag.text === tag ||
286
+ (tag === 'html' && (node.tag.text === 'svg' || node.tag.text === 'composite')))) {
287
+ visit(templateText(node.template), node);
288
+ }
289
+ ts.forEachChild(node, walk);
290
+ };
291
+ walk(body);
292
+ }
293
+ const attributeValue = (attributes, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'>]+))`).exec(attributes);
294
+ /**
295
+ * CSS custom properties read with `var()` and tokens read with `var(--odd-…)` or `token()`.
296
+ * Private `--_*` properties are the element's own wiring, not a customization path.
297
+ */
298
+ function styleReads(node) {
299
+ const cssProperties = [];
300
+ const tokens = [];
301
+ scanTemplates(node, 'css', (text, template) => {
302
+ for (const match of text.matchAll(/var\(\s*(--[\w-]+)/g)) {
303
+ const variable = match[1];
304
+ if (variable.startsWith('--_'))
305
+ continue;
306
+ (variable.startsWith('--odd-') ? tokens : cssProperties).push(variable);
307
+ }
308
+ const walk = (child) => {
309
+ if (ts.isCallExpression(child) &&
310
+ ts.isIdentifier(child.expression) &&
311
+ child.expression.text === 'token') {
312
+ const name = stringOf(child.arguments[0]);
313
+ if (name)
314
+ tokens.push(name);
315
+ }
316
+ ts.forEachChild(child, walk);
317
+ };
318
+ walk(template);
319
+ });
320
+ return { cssProperties, tokens };
321
+ }
322
+ /** Slots and parts a template renders. */
323
+ function templateAnatomy(node) {
324
+ const found = { slots: new Set(), parts: new Set() };
325
+ scanTemplates(node, 'html', (text) => {
326
+ for (const match of text.matchAll(/<slot\b([^>]*)>/g)) {
327
+ const name = attributeValue(match[1] ?? '', 'name');
328
+ found.slots.add(name ? (name[1] ?? name[2] ?? name[3] ?? '') : '');
329
+ }
330
+ // A composite's role element (`<${roles.x}`) is a tag written as a substitution.
331
+ for (const match of text.matchAll(/<(?:[a-zA-Z][\w-]*\b|\u0000)([^>]*)>/g)) {
332
+ const part = attributeValue(match[1] ?? '', 'part');
333
+ for (const name of (part ? (part[1] ?? part[2] ?? part[3] ?? '') : '').split(/\s+/))
334
+ if (name)
335
+ found.parts.add(name);
336
+ }
337
+ });
338
+ return found;
339
+ }
340
+ /**
341
+ * Which of a component's parts each tag and class selects, read from every template in the
342
+ * template's file (helpers beside it render into the same shadow root).
343
+ */
344
+ function partSelectors(node, componentParts) {
345
+ const tags = new Map();
346
+ const classes = new Map();
347
+ const all = new Set();
348
+ const computedClass = new Set();
349
+ const add = (map, key, names) => map.set(key, [...new Set([...(map.get(key) ?? []), ...names])]);
350
+ const valueOf = (attributes, name) => {
351
+ const value = attributeValue(attributes, name);
352
+ return value ? (value[1] ?? value[2] ?? value[3] ?? '') : '';
353
+ };
354
+ scanTemplates(node.getSourceFile(), 'html', (text) => {
355
+ for (const match of text.matchAll(/<([a-zA-Z][\w-]*)\b([^>]*)>/g)) {
356
+ const attributes = match[2] ?? '';
357
+ const names = valueOf(attributes, 'part')
358
+ .split(/\s+/)
359
+ .filter((name) => componentParts.has(name));
360
+ if (!names.length)
361
+ continue;
362
+ for (const name of names)
363
+ all.add(name);
364
+ add(tags, match[1].toLowerCase(), names);
365
+ const classList = valueOf(attributes, 'class');
366
+ if (classList.includes('\u0000'))
367
+ for (const name of names)
368
+ computedClass.add(name);
369
+ for (const name of classList.split(/\s+/))
370
+ if (name && !name.includes('\u0000'))
371
+ add(classes, name, names);
372
+ }
373
+ });
374
+ return { tags, classes, all: [...all], computedClass: [...computedClass] };
375
+ }
376
+ /**
377
+ * A style source's CSS with each `token('name')` written as `token(name)`, each interpolated
378
+ * style source of this library (`${recipeStyles}`) as its own CSS, and any other substitution
379
+ * as `unknown()`: a css`` template or `unsafeCSS('…')`. `undefined` otherwise.
380
+ */
381
+ function tokenCss(node) {
382
+ if (ts.isCallExpression(node) &&
383
+ ts.isIdentifier(node.expression) &&
384
+ node.expression.text === 'unsafeCSS')
385
+ return stringOf(node.arguments[0]);
386
+ if (!ts.isTaggedTemplateExpression(node) ||
387
+ !ts.isIdentifier(node.tag) ||
388
+ node.tag.text !== 'css')
389
+ return undefined;
390
+ const template = node.template;
391
+ if (ts.isNoSubstitutionTemplateLiteral(template))
392
+ return template.text;
393
+ let text = template.head.text;
394
+ for (const span of template.templateSpans) {
395
+ const expression = span.expression;
396
+ const name = ts.isCallExpression(expression) &&
397
+ ts.isIdentifier(expression.expression) &&
398
+ expression.expression.text === 'token'
399
+ ? stringOf(expression.arguments[0])
400
+ : undefined;
401
+ const found = ts.isIdentifier(expression) ? initializerOf(expression) : undefined;
402
+ const inner = found ? tokenCss(found.node) : undefined;
403
+ text += (name ? `token(${name})` : (inner ?? 'unknown()')) + span.literal.text;
404
+ }
405
+ return text;
406
+ }
407
+ const sourceKey = (ref) => `${ref.file}#${ref.name}`;
408
+ /** Record an exported style source of the analyzed library. */
409
+ function recordStyles(ref, node) {
410
+ if (!styleSources.has(sourceKey(ref))) {
411
+ const css = tokenCss(node);
412
+ styleSources.set(sourceKey(ref), {
413
+ ref,
414
+ ...styleReads(node),
415
+ ...(css === undefined ? {} : { css }),
416
+ });
417
+ }
418
+ }
419
+ /** A section of another package's `oddkin.metadata.json` entry, by module path and name. */
420
+ function publishedSource(pkg, section, module, name) {
421
+ const entries = pkg.metadata?.[section];
422
+ return entries?.[`${module}#${name}`];
423
+ }
424
+ const publishedReads = (pkg, module, name) => {
425
+ const entry = publishedSource(pkg, 'styles', module, name);
426
+ return entry
427
+ ? {
428
+ cssProperties: entry.cssProperties ?? [],
429
+ tokens: entry.tokens ?? [],
430
+ ...(typeof entry.css === 'string' ? { css: entry.css } : {}),
431
+ }
432
+ : undefined;
433
+ };
434
+ // ---- classes ----
435
+ function heritage(node) {
436
+ return node.heritageClauses?.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword)?.types[0]
437
+ ?.expression;
438
+ }
439
+ function classify(node) {
440
+ if (components.has(node))
441
+ return components.get(node);
442
+ components.set(node, null);
443
+ // Diagnostics raised for this class (after its base is resolved) can be covered by explicit metadata.
444
+ let diagnosticsBefore = diagnostics.length;
445
+ const expression = heritage(node);
446
+ if (!expression || !node.name)
447
+ return null;
448
+ const className = node.name.text;
449
+ let base = null;
450
+ let superclass;
451
+ let own = [];
452
+ const baseOf = (target) => {
453
+ if (fromOddkinCore(target, 'OddkinElement')) {
454
+ superclass = { name: 'OddkinElement', package: '@oddkin/core' };
455
+ return true;
456
+ }
457
+ const declaration = resolveSymbol(target)?.declarations?.find(ts.isClassDeclaration);
458
+ if (declaration && isLibrarySource(declaration.getSourceFile())) {
459
+ base = classify(declaration);
460
+ if (base)
461
+ superclass = { name: base.name, file: base.file };
462
+ return !!base;
463
+ }
464
+ // An element class another package publishes: its contract comes from that package's CEM.
465
+ if (declaration?.getSourceFile().isDeclarationFile) {
466
+ base = publishedComponent(declaration, target);
467
+ if (base)
468
+ superclass = { name: base.name, package: base.package };
469
+ return !!base;
470
+ }
471
+ return false;
472
+ };
473
+ if (ts.isCallExpression(expression) &&
474
+ ts.isPropertyAccessExpression(expression.expression) &&
475
+ expression.expression.name.text === 'with') {
476
+ if (!baseOf(expression.expression.expression))
477
+ return null;
478
+ diagnosticsBefore = diagnostics.length;
479
+ const definitions = expression.arguments[0];
480
+ if (definitions && ts.isObjectLiteralExpression(definitions))
481
+ own = properties(definitions, className);
482
+ else
483
+ report('analyzer.dynamic.definitions', expression, `${className}: with() needs an object literal of prop.*() definitions.`);
484
+ }
485
+ else if (!baseOf(expression))
486
+ return null;
487
+ else
488
+ diagnosticsBefore = diagnostics.length;
489
+ const instanceType = checker.getDeclaredTypeOfSymbol(checker.getSymbolAtLocation(node.name));
490
+ const typeOf = (name) => {
491
+ const symbol = instanceType.getProperty(name);
492
+ return symbol
493
+ ? checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol, node))
494
+ : 'unknown';
495
+ };
496
+ own = own.map((p) => ({ ...p, type: typeOf(p.name) }));
497
+ const ref = { name: className, file: rel(node.getSourceFile().fileName) };
498
+ const inherit = (items, from) => (items ?? []).map((item) => item.inheritedFrom
499
+ ? item
500
+ : {
501
+ ...item,
502
+ inheritedFrom: {
503
+ name: from.name,
504
+ file: from.file,
505
+ ...(from.package
506
+ ? { package: from.package }
507
+ : {}),
508
+ },
509
+ });
510
+ const parent = base;
511
+ const events = [];
512
+ const eventKeys = [];
513
+ const members = [];
514
+ const cssStates = new Map();
515
+ const slots = new Map();
516
+ const parts = new Map();
517
+ const cssProperties = new Map();
518
+ const tokenReads = new Map();
519
+ const styleNames = new Set();
520
+ const styleTokens = new Set();
521
+ const renderSlots = new Set();
522
+ const renderParts = new Set();
523
+ for (const member of node.members) {
524
+ const modifiers = ts.canHaveModifiers(member) ? (ts.getModifiers(member) ?? []) : [];
525
+ const isStatic = modifiers.some((m) => m.kind === ts.SyntaxKind.StaticKeyword);
526
+ const hidden = modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword) ||
527
+ (member.name && ts.isPrivateIdentifier(member.name)) ||
528
+ ts.getJSDocTags(member).some((t) => t.tagName.text === 'internal');
529
+ const memberName = member.name && (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name))
530
+ ? member.name.text
531
+ : undefined;
532
+ if (isStatic &&
533
+ memberName === 'events' &&
534
+ ts.isPropertyDeclaration(member) &&
535
+ member.initializer) {
536
+ if (ts.isObjectLiteralExpression(member.initializer)) {
537
+ for (const [key, value] of objectEntries(member.initializer, `${className}.events`)) {
538
+ const info = eventFrom(value, className);
539
+ if (info) {
540
+ events.push(info);
541
+ eventKeys.push({ key, name: info.name, node: value });
542
+ }
543
+ }
544
+ }
545
+ else
546
+ report('analyzer.dynamic.event', member.initializer, `${className}.events must be an object literal.`);
547
+ }
548
+ if (isStatic &&
549
+ memberName === 'styles' &&
550
+ ts.isPropertyDeclaration(member) &&
551
+ member.initializer) {
552
+ const read = styleReads(member.initializer);
553
+ for (const name of read.cssProperties) {
554
+ cssProperties.set(name, { name });
555
+ styleNames.add(name);
556
+ }
557
+ for (const token of read.tokens) {
558
+ tokenReads.set(token, { token, readThrough: 'stylesheet' });
559
+ styleTokens.add(token);
560
+ }
561
+ }
562
+ if (!isStatic && !hidden && memberName && !memberName.startsWith('_')) {
563
+ if ((ts.isGetAccessorDeclaration(member) || ts.isPropertyDeclaration(member)) &&
564
+ memberName !== 'render') {
565
+ const setter = node.members.some((m) => ts.isSetAccessorDeclaration(m) && m.name.getText() === memberName);
566
+ const readonly = ts.isGetAccessorDeclaration(member)
567
+ ? !setter
568
+ : modifiers.some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword);
569
+ const description = docOf(member);
570
+ members.push({
571
+ kind: 'field',
572
+ name: memberName,
573
+ type: typeOf(memberName),
574
+ readonly,
575
+ ...(description ? { description } : {}),
576
+ });
577
+ }
578
+ else if (ts.isMethodDeclaration(member)) {
579
+ const signature = checker.getSignatureFromDeclaration(member);
580
+ const description = docOf(member);
581
+ members.push({
582
+ kind: 'method',
583
+ name: memberName,
584
+ type: signature ? checker.signatureToString(signature) : '',
585
+ readonly: false,
586
+ ...(description ? { description } : {}),
587
+ });
588
+ }
589
+ }
590
+ if (memberName === 'render' && ts.isMethodDeclaration(member) && member.body) {
591
+ scanTemplates(member.body, 'html', (text) => {
592
+ for (const match of text.matchAll(/<slot\b([^>]*)>/g)) {
593
+ const name = attributeValue(match[1] ?? '', 'name');
594
+ const slot = name ? (name[1] ?? name[2] ?? name[3] ?? '') : '';
595
+ slots.set(slot, { name: slot });
596
+ renderSlots.add(slot);
597
+ }
598
+ for (const match of text.matchAll(/<[a-zA-Z][\w-]*\b([^>]*)>/g)) {
599
+ const part = attributeValue(match[1] ?? '', 'part');
600
+ for (const name of (part ? (part[1] ?? part[2] ?? part[3] ?? '') : '')
601
+ .split(/\s+/)
602
+ .filter(Boolean)) {
603
+ parts.set(name, { name });
604
+ renderParts.add(name);
605
+ }
606
+ }
607
+ });
608
+ }
609
+ }
610
+ // Custom states set with this.setState('name', …) and token() references anywhere in the class.
611
+ // Events it emits (`this.emit(def)` or `emit(this, def)`) are compared with the declared ones.
612
+ const emitted = [];
613
+ const walk = (child) => {
614
+ if (ts.isCallExpression(child)) {
615
+ const callee = child.expression;
616
+ if (ts.isPropertyAccessExpression(callee) &&
617
+ callee.expression.kind === ts.SyntaxKind.ThisKeyword &&
618
+ callee.name.text === 'setState') {
619
+ const state = stringOf(child.arguments[0]);
620
+ if (state)
621
+ cssStates.set(state, { name: state });
622
+ else
623
+ report('analyzer.dynamic.state', child, `${className}: setState() with a computed name cannot be documented.`);
624
+ }
625
+ if (ts.isIdentifier(callee) && callee.text === 'token') {
626
+ const name = stringOf(child.arguments[0]);
627
+ if (name)
628
+ tokenReads.set(name, { token: name, readThrough: 'stylesheet' });
629
+ }
630
+ const definition = ts.isPropertyAccessExpression(callee) &&
631
+ callee.expression.kind === ts.SyntaxKind.ThisKeyword &&
632
+ callee.name.text === 'emit'
633
+ ? child.arguments[0]
634
+ : fromOddkinCore(callee, 'emit') &&
635
+ child.arguments[0]?.kind === ts.SyntaxKind.ThisKeyword
636
+ ? child.arguments[1]
637
+ : undefined;
638
+ const variable = definition && ts.isIdentifier(definition)
639
+ ? resolveSymbol(definition)?.valueDeclaration
640
+ : undefined;
641
+ if (variable &&
642
+ ts.isVariableDeclaration(variable) &&
643
+ variable.initializer &&
644
+ ts.isCallExpression(variable.initializer) &&
645
+ fromOddkinCore(variable.initializer.expression, 'event')) {
646
+ const name = stringOf(variable.initializer.arguments[0]);
647
+ if (name)
648
+ emitted.push({ name, node: child });
649
+ }
650
+ }
651
+ ts.forEachChild(child, walk);
652
+ };
653
+ walk(node);
654
+ const declaredEvents = new Set([...(parent?.events ?? []), ...events].map((e) => e.name));
655
+ // `_events` (ElementEvents<typeof Class>) types listeners by key, so keys must be the names.
656
+ if (node.members.some((m) => m.name && ts.isIdentifier(m.name) && m.name.text === '_events'))
657
+ for (const { key, name, node: value } of eventKeys)
658
+ if (key !== name)
659
+ report('analyzer.event.key', value, `${className}.events key "${key}" differs from the event name "${name}", so typed listeners would expect "${key}".`, `Key the entry by the event name: '${name}': …`);
660
+ for (const { name, node: call } of emitted)
661
+ if (!declaredEvents.has(name))
662
+ report('analyzer.event.undeclared', call, `${className} emits "${name}" without declaring it in static events, so the published contract omits it.`, `Add the event definition to ${className}.events.`);
663
+ // JSDoc tags: CEM conventions (@slot, @csspart, @cssprop, @cssstate) and Oddkin tags.
664
+ const tags = {};
665
+ for (const tag of ts.getJSDocTags(node)) {
666
+ const tagName = tag.tagName.text;
667
+ const comment = (typeof tag.comment === 'string'
668
+ ? tag.comment
669
+ : (ts.getTextOfJSDocComment(tag.comment) ?? '')).trim();
670
+ const [first = '', ...restWords] = comment.split(/\s+/);
671
+ const rest = restWords.join(' ').replace(/^-\s*/, '');
672
+ const named = { name: first, ...(rest ? { description: rest } : {}) };
673
+ if (tagName === 'slot') {
674
+ slots.set(first === '-' ? '' : first, { ...named, name: first === '-' ? '' : first });
675
+ renderSlots.delete(first === '-' ? '' : first);
676
+ }
677
+ else if (tagName === 'csspart') {
678
+ parts.set(first, named);
679
+ renderParts.delete(first);
680
+ }
681
+ else if (tagName === 'cssprop' || tagName === 'cssproperty') {
682
+ cssProperties.set(first, named);
683
+ styleNames.delete(first);
684
+ }
685
+ else if (tagName === 'cssstate')
686
+ cssStates.set(first, named);
687
+ else if (DOC_TAGS.includes(tagName)) {
688
+ if (tagName === 'customization' && !/^[1-5]( [1-5])*$/.test(comment.replace(/\s+/g, ' ')))
689
+ report('analyzer.tag.invalid', tag, `${className}: @customization lists supported ladder levels 1 to 5 ("${comment}"); the levels are recorded as unknown.`, 'Write the supported levels as numbers, e.g. @customization 1 2 3.');
690
+ if (tagName === 'updated' && !isDate(comment)) {
691
+ report('analyzer.tag.invalid', tag, `${className}: @updated needs a calendar date written YYYY-MM-DD ("${comment}"); the page date comes from source history or is omitted.`, 'Write the date of the last meaningful change, e.g. @updated 2026-03-14.');
692
+ continue;
693
+ }
694
+ (tags[tagName] ??= []).push({ value: comment, location: where(tag) });
695
+ }
696
+ }
697
+ const description = docOf(node);
698
+ const exported = !!ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
699
+ const explicit = explicitMetadata(node, className, diagnosticsBefore);
700
+ const info = {
701
+ name: className,
702
+ file: ref.file,
703
+ location: where(node),
704
+ ...(description ? { description } : {}),
705
+ superclass: superclass,
706
+ properties: [...inherit(parent?.properties, parent), ...own],
707
+ // A class's own `static events` replaces its parent's at runtime, as any static field does.
708
+ events: events.length ? events : inherit(parent?.events, parent),
709
+ members: [
710
+ ...(parent?.members ?? []).filter((m) => !own.some((p) => p.name === m.name)),
711
+ ...members.filter((m) => !own.some((p) => p.name === m.name)),
712
+ ],
713
+ slots: [...(parent?.slots ?? []), ...slots.values()].filter((s, i, all) => all.findIndex((x) => x.name === s.name) === i),
714
+ parts: [...(parent?.parts ?? []), ...parts.values()].filter((s, i, all) => all.findIndex((x) => x.name === s.name) === i),
715
+ cssProperties: [...(parent?.cssProperties ?? []), ...cssProperties.values()].filter((s, i, all) => all.findIndex((x) => x.name === s.name) === i),
716
+ cssStates: [...(parent?.cssStates ?? []), ...cssStates.values()].filter((s, i, all) => all.findIndex((x) => x.name === s.name) === i),
717
+ // A documented component variable (`@cssprop --odd-button-radius`) is not a token read.
718
+ tokenReads: [...(parent?.tokenReads ?? []), ...tokenReads.values()].filter((s, i, all) => all.findIndex((x) => x.token === s.token) === i &&
719
+ !cssProperties.has(s.token) &&
720
+ !(parent?.cssProperties ?? []).some((c) => c.name === s.token)),
721
+ tags,
722
+ exported,
723
+ derived: {
724
+ slots: [...(parent?.derived?.slots ?? []), ...renderSlots],
725
+ parts: [...(parent?.derived?.parts ?? []), ...renderParts],
726
+ cssProperties: [...(parent?.derived?.cssProperties ?? []), ...styleNames],
727
+ tokens: [...(parent?.derived?.tokens ?? []), ...styleTokens],
728
+ },
729
+ };
730
+ /** True when the class's instance type derives from `HTMLElement`. */
731
+ function isElementClass(declaration) {
732
+ const symbol = declaration.name && checker.getSymbolAtLocation(declaration.name);
733
+ const pending = symbol ? [checker.getDeclaredTypeOfSymbol(symbol)] : [];
734
+ for (let depth = 0; pending.length && depth < 32; depth++) {
735
+ const type = pending.shift();
736
+ if (type.getSymbol()?.name === 'HTMLElement')
737
+ return true;
738
+ if (type.isClassOrInterface())
739
+ pending.push(...(checker.getBaseTypes(type) ?? []));
740
+ else if (type.isIntersection())
741
+ pending.push(...type.types);
742
+ }
743
+ return false;
744
+ }
745
+ /**
746
+ * A component class published by another package, read from its `custom-elements.json`
747
+ * (members, attributes, events, slots, parts, CSS properties and states) and
748
+ * `oddkin.metadata.json` (token reads, sync pairs, and the template and style sources of its
749
+ * definition). The package's modules are never executed.
750
+ */
751
+ function publishedComponent(declaration, at) {
752
+ if (publishedComponents.has(declaration))
753
+ return publishedComponents.get(declaration) ?? null;
754
+ publishedComponents.set(declaration, null);
755
+ const file = declaration.getSourceFile().fileName;
756
+ const pkg = published(file);
757
+ if (!pkg || !declaration.name)
758
+ return null;
759
+ const name = declaration.name.text;
760
+ const module = pkg.moduleOf(file);
761
+ const cem = pkg.declaration(name, module);
762
+ if (!cem?.customElement) {
763
+ if (isElementClass(declaration))
764
+ report('analyzer.published.missing', at, `${pkg.name} publishes no custom-elements.json declaration for ${name}; classes extending it are not documented.`, `Publish ${pkg.name} with its custom-elements.json (package.json "customElements").`);
765
+ return null;
766
+ }
767
+ const metadata = cem.tagName
768
+ ? (pkg.metadata?.['components']?.[`component:${cem.tagName}`] ?? undefined)
769
+ : undefined;
770
+ const from = { name, file: module, package: pkg.name };
771
+ const inherited = (value) => {
772
+ const ref = value;
773
+ return ref?.name
774
+ ? { name: ref.name, file: ref.module ?? module, package: ref.package ?? pkg.name }
775
+ : from;
776
+ };
777
+ const text = (value) => (typeof value === 'string' && value ? value : undefined);
778
+ const described = (item, description) => text(description) ? { ...item, description: text(description) } : item;
779
+ const named = (list) => (list ?? []).map((item) => described({ name: item.name }, item.description));
780
+ const syncPairs = (metadata?.['syncPairs'] ?? []);
781
+ const publishedIntents = (metadata?.['events'] ?? []);
782
+ const tag = cem.tagName;
783
+ const publishedDeprecations = (metadata?.['deprecations'] ?? []);
784
+ /** The publisher's deprecation: reason from CEM, replacement and migration from metadata. */
785
+ const deprecatedOf = (kind, name, value) => {
786
+ if (value === undefined || value === false)
787
+ return {};
788
+ const entry = publishedDeprecations.find((d) => d.id === `${kind}:${tag}#${name}`);
789
+ // A member replacement is renamed with the derived tag, so keep only its name.
790
+ const replacement = entry?.replacement?.startsWith(`${kind}:${tag}#`)
791
+ ? entry.replacement.slice(`${kind}:${tag}#`.length)
792
+ : entry?.replacement;
793
+ return {
794
+ deprecated: {
795
+ ...(typeof value === 'string' && value ? { reason: value } : {}),
796
+ ...(replacement ? { replacement } : {}),
797
+ ...(entry?.migration ? { migration: entry.migration } : {}),
798
+ },
799
+ };
800
+ };
801
+ const typeText = (member) => member['type']?.text;
802
+ const properties = [];
803
+ const members = [];
804
+ for (const member of cem.members ?? []) {
805
+ if (member['static'] ||
806
+ member['privacy'] === 'private' ||
807
+ member['privacy'] === 'protected')
808
+ continue;
809
+ const memberName = member['name'];
810
+ if (member['kind'] === 'method') {
811
+ members.push(described({ kind: 'method', name: memberName, type: '', readonly: false }, member['description']));
812
+ continue;
813
+ }
814
+ if (member['kind'] !== 'field')
815
+ continue;
816
+ // prop.*() fields carry an attribute or a known default; accessors and plain fields do not.
817
+ if (member['attribute'] !== undefined || member['default'] !== undefined) {
818
+ properties.push(described({
819
+ name: memberName,
820
+ kind: 'published',
821
+ type: typeText(member) ?? 'unknown',
822
+ attribute: text(member['attribute']) ?? null,
823
+ reflects: member['reflects'] === true,
824
+ default: typeof member['default'] === 'string'
825
+ ? { known: true, value: member['default'] }
826
+ : { known: false },
827
+ ...deprecatedOf('property', memberName, member['deprecated']),
828
+ inheritedFrom: inherited(member['inheritedFrom']),
829
+ }, member['description']));
830
+ }
831
+ else
832
+ members.push(described({
833
+ kind: 'field',
834
+ name: memberName,
835
+ type: typeText(member) ?? 'unknown',
836
+ readonly: member['readonly'] === true,
837
+ }, member['description']));
838
+ }
839
+ const events = (cem.events ?? []).map((event) => {
840
+ const type = typeText(event) ?? 'CustomEvent';
841
+ const detail = /^CustomEvent<(.*)>$/s.exec(type)?.[1];
842
+ const syncs = syncPairs.find((p) => p.event === event['name'])?.property;
843
+ // The publisher's declared intent; its source location belongs to that package.
844
+ const intent = publishedIntents.find((e) => e.event === `event:${tag}#${event['name']}`)?.intent?.value;
845
+ return described({
846
+ name: event['name'],
847
+ detailType: detail ?? 'void',
848
+ bubbles: false,
849
+ composed: false,
850
+ cancelable: false,
851
+ ...(syncs ? { syncs } : {}),
852
+ ...(intent === 'user' || intent === 'notification'
853
+ ? { intent: { value: intent } }
854
+ : {}),
855
+ ...deprecatedOf('event', event['name'], event['deprecated']),
856
+ inheritedFrom: inherited(event['inheritedFrom']),
857
+ }, event['description']);
858
+ });
859
+ // What its definition's template and styles provide, so a recomposition can replace them.
860
+ const definition = metadata?.['definition'];
861
+ const anatomy = definition?.template
862
+ ? publishedSource(pkg, 'templates', definition.template.module, definition.template.name)
863
+ : undefined;
864
+ const reads = (definition?.styles ?? []).map((s) => publishedReads(pkg, s.module, s.name));
865
+ if ((definition?.template && !anatomy) || reads.some((r) => !r))
866
+ report('analyzer.published.sources', at, `${pkg.name} publishes no template or style facts for the definition of <${cem.tagName}>; facts its template and styles provide are kept when a definition replaces them.`, `Rebuild ${pkg.name} with a current @oddkin/analyzer.`, 'info');
867
+ const superclass = cem.superclass;
868
+ const info = {
869
+ name,
870
+ file: module,
871
+ package: pkg.name,
872
+ location: { file: `${pkg.name}/${module}`, line: 1, column: 1 },
873
+ superclass: {
874
+ name: superclass?.name ?? 'OddkinElement',
875
+ package: superclass?.package ?? pkg.name,
876
+ },
877
+ properties,
878
+ events,
879
+ members,
880
+ slots: named(cem.slots),
881
+ parts: named(cem.cssParts),
882
+ cssProperties: named(cem.cssProperties),
883
+ cssStates: named(cem.cssStates),
884
+ tokenReads: (metadata?.['tokenReads'] ?? []).map((t) => ({
885
+ token: t.token,
886
+ readThrough: 'stylesheet',
887
+ })),
888
+ tags: {},
889
+ exported: true,
890
+ derived: {
891
+ slots: anatomy?.slots ?? [],
892
+ parts: anatomy?.parts ?? [],
893
+ cssProperties: reads.flatMap((r) => r?.cssProperties ?? []),
894
+ tokens: reads.flatMap((r) => r?.tokens ?? []),
895
+ },
896
+ };
897
+ publishedComponents.set(declaration, info);
898
+ return info;
899
+ }
900
+ const merged = explicit ? mergeExplicit(info, explicit) : info;
901
+ components.set(node, merged);
902
+ return merged;
903
+ }
904
+ const LISTS = ['properties', 'events', 'slots', 'parts', 'cssProperties', 'cssStates'];
905
+ function explicitMetadata(node, className, diagnosticsBefore) {
906
+ const tag = ts.getJSDocTags(node).find((t) => t.tagName.text === 'oddkin-metadata');
907
+ if (!tag)
908
+ return undefined;
909
+ const comment = (typeof tag.comment === 'string' ? tag.comment : (ts.getTextOfJSDocComment(tag.comment) ?? '')).trim();
910
+ const absolutePath = resolve(dirname(node.getSourceFile().fileName), comment);
911
+ let data;
912
+ try {
913
+ data = JSON.parse(readFileSync(absolutePath, 'utf8'));
914
+ }
915
+ catch (error) {
916
+ report('analyzer.metadata.missing', tag, `${className}: cannot read explicit metadata "${comment}": ${error instanceof Error ? error.message : String(error)}.`, undefined, 'error');
917
+ return undefined;
918
+ }
919
+ const problems = typeof data !== 'object' || data === null || Array.isArray(data)
920
+ ? ['the file must contain an object']
921
+ : Object.entries(data).flatMap(([key, value]) => key === 'description'
922
+ ? typeof value === 'string'
923
+ ? []
924
+ : ['description must be a string']
925
+ : LISTS.includes(key)
926
+ ? Array.isArray(value) &&
927
+ value.every((v) => typeof v === 'object' &&
928
+ v !== null &&
929
+ typeof v.name === 'string')
930
+ ? []
931
+ : [`${key} must be an array of objects with a name`]
932
+ : [`unknown key "${key}" (allowed: description, ${LISTS.join(', ')})`]);
933
+ if (problems.length) {
934
+ report('analyzer.metadata.invalid', tag, `${className}: explicit metadata ${comment}: ${problems.join('; ')}.`, undefined, 'error');
935
+ return undefined;
936
+ }
937
+ // Explicit metadata answers what static analysis could not: its dynamic warnings become info.
938
+ for (let i = diagnosticsBefore; i < diagnostics.length; i++) {
939
+ const d = diagnostics[i];
940
+ if (d.code.startsWith('analyzer.dynamic.'))
941
+ diagnostics[i] = {
942
+ ...d,
943
+ severity: 'info',
944
+ message: `${d.message} Explicit metadata (${comment}) is merged for this class.`,
945
+ };
946
+ }
947
+ return { file: rel(absolutePath), data: data };
948
+ }
949
+ function mergeExplicit(info, explicit) {
950
+ const ids = [];
951
+ const merge = (list, key, idKind, normalize) => {
952
+ const authored = explicit.data[key] ?? [];
953
+ const out = [...list];
954
+ for (const raw of authored) {
955
+ const entry = normalize(raw);
956
+ const index = out.findIndex((x) => x.name === entry.name);
957
+ if (index >= 0)
958
+ out[index] = entry;
959
+ else
960
+ out.push(entry);
961
+ ids.push(`${idKind}#${entry.name}`);
962
+ }
963
+ return out;
964
+ };
965
+ const text = (raw, field) => typeof raw[field] === 'string' ? raw[field] : undefined;
966
+ const named = (raw) => ({
967
+ name: raw['name'],
968
+ ...(text(raw, 'description') ? { description: text(raw, 'description') } : {}),
969
+ });
970
+ const description = text(explicit.data, 'description');
971
+ return {
972
+ ...info,
973
+ ...(description ? { description } : {}),
974
+ properties: merge(info.properties, 'properties', 'property', (raw) => ({
975
+ name: raw['name'],
976
+ kind: 'authored',
977
+ type: text(raw, 'type') ?? 'unknown',
978
+ attribute: raw['attribute'] === null ? null : (text(raw, 'attribute') ?? null),
979
+ reflects: raw['reflects'] === true,
980
+ default: text(raw, 'default') !== undefined
981
+ ? { known: true, value: text(raw, 'default') }
982
+ : { known: false },
983
+ ...(text(raw, 'description') ? { description: text(raw, 'description') } : {}),
984
+ })),
985
+ events: merge(info.events, 'events', 'event', (raw) => ({
986
+ name: raw['name'],
987
+ detailType: text(raw, 'detailType') ?? 'void',
988
+ bubbles: raw['bubbles'] === true,
989
+ composed: raw['composed'] === true,
990
+ cancelable: raw['cancelable'] === true,
991
+ ...(text(raw, 'syncs') ? { syncs: text(raw, 'syncs') } : {}),
992
+ ...(text(raw, 'description') ? { description: text(raw, 'description') } : {}),
993
+ })),
994
+ slots: merge(info.slots, 'slots', 'slot', named),
995
+ parts: merge(info.parts, 'parts', 'part', named),
996
+ cssProperties: merge(info.cssProperties, 'cssProperties', 'cssprop', named),
997
+ cssStates: merge(info.cssStates, 'cssStates', 'state', named),
998
+ authored: { file: explicit.file, ids },
999
+ };
1000
+ }
1001
+ /** Follow an identifier (through imports) to the expression it is initialized with. */
1002
+ function initializerOf(expression) {
1003
+ if (!ts.isIdentifier(expression))
1004
+ return { node: expression };
1005
+ const declaration = resolveSymbol(expression)?.declarations?.[0];
1006
+ if (!declaration)
1007
+ return undefined;
1008
+ const ref = { name: expression.text, file: rel(declaration.getSourceFile().fileName) };
1009
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer)
1010
+ return {
1011
+ node: declaration.initializer,
1012
+ ref: { ...ref, name: declaration.name.text },
1013
+ };
1014
+ if (ts.isFunctionDeclaration(declaration) && declaration.name)
1015
+ return { node: declaration, ref: { ...ref, name: declaration.name.text } };
1016
+ return undefined;
1017
+ }
1018
+ function stylesFrom(expression, refs, nodes, reads, texts, owner) {
1019
+ const found = initializerOf(expression);
1020
+ if (!found) {
1021
+ const published = publishedStyles(expression);
1022
+ if (!published) {
1023
+ report('analyzer.dynamic.definition', expression, `${owner}: styles cannot be resolved.`);
1024
+ return;
1025
+ }
1026
+ refs.push(published.ref);
1027
+ texts.push(published.reads?.css);
1028
+ if (published.reads)
1029
+ reads.push(published.reads);
1030
+ else
1031
+ report('analyzer.published.sources', expression, `${owner}: ${published.ref.package ?? published.ref.file} publishes no facts for ${published.ref.name}; what it reads is unknown.`, `Rebuild ${published.ref.package ?? published.ref.file} with a current @oddkin/analyzer.`, 'info');
1032
+ return;
1033
+ }
1034
+ const node = found.node;
1035
+ if (ts.isArrayLiteralExpression(node)) {
1036
+ for (const element of node.elements) {
1037
+ if (ts.isSpreadElement(element))
1038
+ report('analyzer.dynamic.definition', element, `${owner}: spread styles are not analyzed.`);
1039
+ else
1040
+ stylesFrom(element, refs, nodes, reads, texts, owner);
1041
+ }
1042
+ return;
1043
+ }
1044
+ if (found.ref) {
1045
+ refs.push(found.ref);
1046
+ recordStyles(found.ref, node);
1047
+ }
1048
+ nodes.push(node);
1049
+ texts.push(tokenCss(node));
1050
+ }
1051
+ /**
1052
+ * Styles imported from another package (`import { hostStyles } from '@oddkin/components/styles'`)
1053
+ * resolve to its declarations, which carry no CSS: they are recorded by specifier and name, and
1054
+ * what they read comes from that package's published `styles` facts.
1055
+ */
1056
+ function publishedStyles(expression) {
1057
+ if (!ts.isIdentifier(expression))
1058
+ return undefined;
1059
+ const declaration = resolveSymbol(expression)?.declarations?.[0];
1060
+ if (!declaration?.getSourceFile().isDeclarationFile || !ts.isVariableDeclaration(declaration))
1061
+ return undefined;
1062
+ const specifier = checker
1063
+ .getSymbolAtLocation(expression)
1064
+ ?.declarations?.find(ts.isImportSpecifier);
1065
+ const from = specifier?.parent.parent.parent.moduleSpecifier;
1066
+ if (!specifier || !from || !ts.isStringLiteral(from) || from.text.startsWith('.'))
1067
+ return undefined;
1068
+ const file = declaration.getSourceFile().fileName;
1069
+ const pkg = published(file);
1070
+ // Recorded as the publisher's module path, the key of its published `styles` facts.
1071
+ if (pkg && ts.isIdentifier(declaration.name)) {
1072
+ const module = pkg.moduleOf(file);
1073
+ const reads = publishedReads(pkg, module, declaration.name.text);
1074
+ return {
1075
+ ref: { name: declaration.name.text, file: module, package: pkg.name },
1076
+ ...(reads ? { reads } : {}),
1077
+ };
1078
+ }
1079
+ return {
1080
+ ref: {
1081
+ name: (specifier.propertyName ?? specifier.name).text,
1082
+ file: from.text,
1083
+ external: true,
1084
+ },
1085
+ };
1086
+ }
1087
+ const definitionRef = (call, ref) => ref ??
1088
+ (ts.isVariableDeclaration(call.parent) && ts.isIdentifier(call.parent.name)
1089
+ ? { name: call.parent.name.text, file: rel(call.getSourceFile().fileName) }
1090
+ : undefined);
1091
+ /**
1092
+ * A definition published by another package (`ElementDefinition<Class>` in its declarations):
1093
+ * its tag is the `HTMLElementTagNameMap` entry for that class in the class's declaration file.
1094
+ * Declarations only; nothing is executed.
1095
+ */
1096
+ function publishedDefinitionTag(expression) {
1097
+ return publishedDefinitionOf(expression)?.tag;
1098
+ }
1099
+ function publishedDefinitionOf(expression) {
1100
+ const declaration = resolveSymbol(expression)?.declarations?.[0];
1101
+ if (!declaration || !declaration.getSourceFile().isDeclarationFile)
1102
+ return undefined;
1103
+ if (!ts.isVariableDeclaration(declaration) || !declaration.type)
1104
+ return undefined;
1105
+ const type = declaration.type;
1106
+ // Emitted declarations write `import("@oddkin/core/element").ElementDefinition<Class>`.
1107
+ const typeName = ts.isTypeReferenceNode(type)
1108
+ ? type.typeName
1109
+ : ts.isImportTypeNode(type)
1110
+ ? type.qualifier
1111
+ : undefined;
1112
+ const typeArguments = ts.isTypeReferenceNode(type) || ts.isImportTypeNode(type) ? type.typeArguments : undefined;
1113
+ if (!typeName || typeArguments?.length !== 1)
1114
+ return undefined;
1115
+ if (!fromOddkinCore(typeName, 'ElementDefinition'))
1116
+ return undefined;
1117
+ const argument = typeArguments[0];
1118
+ if (!argument || !ts.isTypeReferenceNode(argument))
1119
+ return undefined;
1120
+ const classSymbol = resolveSymbol(argument.typeName);
1121
+ const classFile = classSymbol?.declarations?.[0]?.getSourceFile();
1122
+ if (!classSymbol || !classFile)
1123
+ return undefined;
1124
+ let tag;
1125
+ const visit = (node) => {
1126
+ if (tag)
1127
+ return;
1128
+ if (ts.isInterfaceDeclaration(node) && node.name.text === 'HTMLElementTagNameMap') {
1129
+ for (const member of node.members) {
1130
+ if (!ts.isPropertySignature(member) ||
1131
+ !member.type ||
1132
+ !ts.isTypeReferenceNode(member.type))
1133
+ continue;
1134
+ const name = ts.isStringLiteral(member.name) ? member.name.text : undefined;
1135
+ if (name && TAG_NAME.test(name) && resolveSymbol(member.type.typeName) === classSymbol)
1136
+ tag = name;
1137
+ }
1138
+ }
1139
+ ts.forEachChild(node, visit);
1140
+ };
1141
+ visit(classFile);
1142
+ const classNode = classSymbol.declarations?.find(ts.isClassDeclaration);
1143
+ return tag && classNode ? { tag, classNode } : undefined;
1144
+ }
1145
+ /**
1146
+ * Spreading a definition another package publishes: its tag and class come from declarations,
1147
+ * and its template, styles, and dependencies from that package's `oddkin.metadata.json`.
1148
+ */
1149
+ function publishedDefinition(expression) {
1150
+ const found = publishedDefinitionOf(expression);
1151
+ if (!found?.classNode.name)
1152
+ return undefined;
1153
+ const file = found.classNode.getSourceFile().fileName;
1154
+ const pkg = published(file);
1155
+ if (!pkg)
1156
+ return undefined;
1157
+ const entry = pkg.metadata?.['components']?.[`component:${found.tag}`]?.definition;
1158
+ if (!entry)
1159
+ report('analyzer.published.sources', expression, `${pkg.name} publishes no definition facts for <${found.tag}>; its template and styles are unknown.`, `Rebuild ${pkg.name} with a current @oddkin/analyzer.`, 'info');
1160
+ const refOf = (ref) => ({
1161
+ name: ref.name,
1162
+ file: ref.module,
1163
+ package: pkg.name,
1164
+ });
1165
+ const reads = (entry?.styles ?? []).map((s) => publishedReads(pkg, s.module, s.name));
1166
+ return {
1167
+ external: true,
1168
+ classNode: found.classNode,
1169
+ info: {
1170
+ tag: found.tag,
1171
+ className: found.classNode.name.text,
1172
+ classFile: pkg.moduleOf(file),
1173
+ ...(entry?.template ? { template: refOf(entry.template) } : {}),
1174
+ styles: (entry?.styles ?? []).map(refOf),
1175
+ dependencies: Object.fromEntries(Object.entries(entry?.dependencies ?? {}).map(([role, id]) => [
1176
+ role,
1177
+ id.replace(/^component:/, ''),
1178
+ ])),
1179
+ location: where(expression),
1180
+ },
1181
+ ...(entry?.styles
1182
+ ? {
1183
+ styleNodes: [],
1184
+ styleReads: reads.filter((r) => !!r),
1185
+ styleTexts: reads.map((r) => r?.css),
1186
+ }
1187
+ : {}),
1188
+ };
1189
+ }
1190
+ /** Resolve an expression to a `compose()` definition, following imports and spreads. */
1191
+ function definitionFrom(expression) {
1192
+ const found = initializerOf(expression);
1193
+ const call = found?.node;
1194
+ if (!call ||
1195
+ !ts.isCallExpression(call) ||
1196
+ !ts.isIdentifier(call.expression) ||
1197
+ !fromOddkinCore(call.expression, 'compose'))
1198
+ return undefined;
1199
+ if (definitions.has(call))
1200
+ return definitions.get(call) ?? undefined;
1201
+ definitions.set(call, null);
1202
+ const init = call.arguments[0];
1203
+ if (!init || !ts.isObjectLiteralExpression(init)) {
1204
+ report('analyzer.dynamic.definition', call, 'compose() needs an object literal.');
1205
+ return undefined;
1206
+ }
1207
+ let base;
1208
+ let tag;
1209
+ let classNode;
1210
+ let template;
1211
+ let styles;
1212
+ let dependencies;
1213
+ for (const property of init.properties) {
1214
+ if (ts.isSpreadAssignment(property)) {
1215
+ base = definitionFrom(property.expression) ?? publishedDefinition(property.expression);
1216
+ if (!base) {
1217
+ report('analyzer.dynamic.definition', property, 'compose(): only a spread of another compose() definition can be analyzed.');
1218
+ continue;
1219
+ }
1220
+ tag = base.info.tag;
1221
+ classNode = base.classNode;
1222
+ // A published base has no template node: its template's facts arrive with its class.
1223
+ template =
1224
+ base.templateNode || base.info.template
1225
+ ? {
1226
+ ...(base.templateNode ? { node: base.templateNode } : {}),
1227
+ ...(base.info.template ? { ref: base.info.template } : {}),
1228
+ }
1229
+ : undefined;
1230
+ styles = base.styleNodes
1231
+ ? {
1232
+ refs: [...base.info.styles],
1233
+ nodes: [...base.styleNodes],
1234
+ reads: [...(base.styleReads ?? [])],
1235
+ texts: [...(base.styleTexts ?? [])],
1236
+ }
1237
+ : undefined;
1238
+ dependencies = { ...base.info.dependencies };
1239
+ continue;
1240
+ }
1241
+ const key = (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
1242
+ (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
1243
+ ? property.name.text
1244
+ : undefined;
1245
+ const value = ts.isPropertyAssignment(property)
1246
+ ? property.initializer
1247
+ : ts.isShorthandPropertyAssignment(property)
1248
+ ? property.name
1249
+ : undefined;
1250
+ if (!key || !value) {
1251
+ report('analyzer.dynamic.definition', property, 'compose(): computed entries are not analyzed.');
1252
+ continue;
1253
+ }
1254
+ if (key === 'name') {
1255
+ tag = stringOf(value);
1256
+ if (!tag || !TAG_NAME.test(tag))
1257
+ report('analyzer.dynamic.tagName', value, 'compose(): the name is computed or invalid; the definition cannot be documented.', "Use a string literal, e.g. name: 'my-element'.");
1258
+ }
1259
+ else if (key === 'element') {
1260
+ classNode = resolveSymbol(value)?.declarations?.find(ts.isClassDeclaration);
1261
+ if (!classNode)
1262
+ report('analyzer.dynamic.registration', value, 'compose(): the element class cannot be resolved.');
1263
+ }
1264
+ else if (key === 'template') {
1265
+ const resolved = initializerOf(value);
1266
+ if (resolved) {
1267
+ template = { node: resolved.node, ...(resolved.ref ? { ref: resolved.ref } : {}) };
1268
+ if (resolved.ref && !templateSources.has(sourceKey(resolved.ref))) {
1269
+ const anatomy = templateAnatomy(resolved.node);
1270
+ templateSources.set(sourceKey(resolved.ref), {
1271
+ ref: resolved.ref,
1272
+ slots: [...anatomy.slots],
1273
+ parts: [...anatomy.parts],
1274
+ });
1275
+ }
1276
+ }
1277
+ else
1278
+ report('analyzer.dynamic.definition', value, 'compose(): the template cannot be resolved.');
1279
+ }
1280
+ else if (key === 'styles') {
1281
+ styles = { refs: [], nodes: [], reads: [], texts: [] };
1282
+ stylesFrom(value, styles.refs, styles.nodes, styles.reads, styles.texts, `compose(${tag ?? ''})`);
1283
+ }
1284
+ else if (key === 'dependencies') {
1285
+ dependencies = {};
1286
+ if (ts.isObjectLiteralExpression(value)) {
1287
+ for (const entry of value.properties) {
1288
+ const role = ts.isPropertyAssignment(entry) || ts.isShorthandPropertyAssignment(entry)
1289
+ ? entry.name.getText().replace(/^['"]|['"]$/g, '')
1290
+ : undefined;
1291
+ const target = ts.isPropertyAssignment(entry)
1292
+ ? entry.initializer
1293
+ : ts.isShorthandPropertyAssignment(entry)
1294
+ ? entry.name
1295
+ : undefined;
1296
+ const dependency = target ? definitionFrom(target) : undefined;
1297
+ const external = !dependency && target ? publishedDefinitionTag(target) : undefined;
1298
+ if (role && dependency)
1299
+ dependencies[role] = dependency.info.tag;
1300
+ else if (role && external)
1301
+ dependencies[role] = external;
1302
+ else
1303
+ report('analyzer.dynamic.definition', entry, 'compose(): each dependency must be a compose() definition.');
1304
+ }
1305
+ }
1306
+ else
1307
+ report('analyzer.dynamic.definition', value, 'compose(): dependencies must be an object literal.');
1308
+ }
1309
+ }
1310
+ if (!tag || !TAG_NAME.test(tag) || !classNode?.name)
1311
+ return undefined;
1312
+ if (base?.external && classNode === base.classNode) {
1313
+ report('analyzer.dynamic.definition', call, `compose(${tag}): a definition recomposed from a published one needs its own element class.`, 'Pass element: class extends Base {} (one constructor per registered name).');
1314
+ return undefined;
1315
+ }
1316
+ const variable = definitionRef(call, found?.ref);
1317
+ const resolved = {
1318
+ classNode,
1319
+ ...(template?.node ? { templateNode: template.node } : {}),
1320
+ ...(styles
1321
+ ? { styleNodes: styles.nodes, styleReads: styles.reads, styleTexts: styles.texts }
1322
+ : {}),
1323
+ info: {
1324
+ tag,
1325
+ className: classNode.name.text,
1326
+ classFile: rel(classNode.getSourceFile().fileName),
1327
+ ...(variable ? { ref: variable } : {}),
1328
+ ...(template?.ref ? { template: template.ref } : {}),
1329
+ styles: styles?.refs ?? [],
1330
+ dependencies: dependencies ?? {},
1331
+ ...(base ? { extends: base.info.tag } : {}),
1332
+ location: where(call),
1333
+ },
1334
+ };
1335
+ definitions.set(call, resolved);
1336
+ return resolved;
1337
+ }
1338
+ /** Apply a definition's template and styles to its class's facts (they replace render()/static styles). */
1339
+ function overlay(info, resolved) {
1340
+ const derived = info.derived ?? { slots: [], parts: [], cssProperties: [], tokens: [] };
1341
+ let { slots, parts, cssProperties, tokenReads } = info;
1342
+ // Names the new template or styles provide again keep their documentation.
1343
+ const doc = (list, name) => list.find((item) => item.name === name) ?? { name };
1344
+ if (resolved.templateNode) {
1345
+ const found = templateAnatomy(resolved.templateNode);
1346
+ slots = slots.filter((s) => !derived.slots.includes(s.name));
1347
+ parts = parts.filter((s) => !derived.parts.includes(s.name));
1348
+ for (const name of found.slots)
1349
+ if (!slots.some((s) => s.name === name))
1350
+ slots = [...slots, doc(info.slots, name)];
1351
+ for (const name of found.parts)
1352
+ if (!parts.some((s) => s.name === name))
1353
+ parts = [...parts, doc(info.parts, name)];
1354
+ }
1355
+ if (resolved.styleNodes) {
1356
+ cssProperties = cssProperties.filter((c) => !derived.cssProperties.includes(c.name));
1357
+ tokenReads = tokenReads.filter((t) => !derived.tokens.includes(t.token));
1358
+ for (const read of [...resolved.styleNodes.map(styleReads), ...(resolved.styleReads ?? [])]) {
1359
+ for (const name of read.cssProperties)
1360
+ if (!cssProperties.some((c) => c.name === name))
1361
+ cssProperties = [...cssProperties, doc(info.cssProperties, name)];
1362
+ for (const token of read.tokens)
1363
+ if (!tokenReads.some((t) => t.token === token))
1364
+ tokenReads = [...tokenReads, { token, readThrough: 'stylesheet' }];
1365
+ }
1366
+ }
1367
+ tokenReads = tokenReads.filter((t) => !cssProperties.some((c) => c.name === t.token));
1368
+ // Mappings need this library's template and every style source's text, in cascade order.
1369
+ const texts = resolved.styleTexts ?? [];
1370
+ const styleMappings = resolved.templateNode && texts.length && texts.every((t) => t !== undefined)
1371
+ ? styleMappingsOf(texts.join('\n'), partSelectors(resolved.templateNode, new Set(parts.map((p) => p.name))))
1372
+ : [];
1373
+ return {
1374
+ ...info,
1375
+ slots,
1376
+ parts,
1377
+ cssProperties,
1378
+ tokenReads,
1379
+ ...(styleMappings.length ? { styleMappings } : {}),
1380
+ };
1381
+ }
1382
+ // ---- module walk ----
1383
+ for (const file of program.getSourceFiles()) {
1384
+ if (!isLibrarySource(file))
1385
+ continue;
1386
+ const exportedNames = [];
1387
+ const visit = (node) => {
1388
+ if (ts.isClassDeclaration(node))
1389
+ classify(node);
1390
+ // Exported style sources are published with what they read, used by a definition or not.
1391
+ if (ts.isVariableStatement(node) &&
1392
+ ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
1393
+ for (const declaration of node.declarationList.declarations) {
1394
+ const initializer = declaration.initializer;
1395
+ if (ts.isIdentifier(declaration.name) &&
1396
+ initializer &&
1397
+ ts.isTaggedTemplateExpression(initializer) &&
1398
+ fromOddkinCore(initializer.tag, 'css'))
1399
+ recordStyles({ name: declaration.name.text, file: rel(file.fileName) }, initializer);
1400
+ }
1401
+ }
1402
+ if (ts.isCallExpression(node)) {
1403
+ const callee = node.expression;
1404
+ const isDefine = (ts.isIdentifier(callee) && fromOddkinCore(callee, 'define')) ||
1405
+ (ts.isPropertyAccessExpression(callee) &&
1406
+ callee.name.text === 'define' &&
1407
+ callee.expression.getText() === 'customElements');
1408
+ if (ts.isIdentifier(callee) && fromOddkinCore(callee, 'compose'))
1409
+ definitionFrom(node);
1410
+ const definitionArgument = isDefine && node.arguments[0] && !ts.isStringLiteralLike(node.arguments[0])
1411
+ ? node.arguments[0]
1412
+ : undefined;
1413
+ const registered = definitionArgument ? definitionFrom(definitionArgument) : undefined;
1414
+ if (isDefine && definitionArgument && node.arguments.length === 1) {
1415
+ if (!registered)
1416
+ report('analyzer.dynamic.registration', node, 'define(definition) needs a compose() definition the analyzer can resolve.', 'Export the definition as a const initialized with compose({ ... }).');
1417
+ else
1418
+ registrations.push({
1419
+ tag: registered.info.tag,
1420
+ className: registered.info.className,
1421
+ classFile: registered.info.classFile,
1422
+ file: rel(file.fileName),
1423
+ location: where(node),
1424
+ definition: registered.info,
1425
+ });
1426
+ }
1427
+ else if (isDefine) {
1428
+ const tag = stringOf(node.arguments[0]);
1429
+ const target = node.arguments[1];
1430
+ const declaration = target && resolveSymbol(target)?.declarations?.find(ts.isClassDeclaration);
1431
+ if (!tag || !TAG_NAME.test(tag))
1432
+ report('analyzer.dynamic.tagName', node, 'The tag name is computed or invalid; the registration cannot be documented.', "Call define('my-element', MyElement) with a string literal.");
1433
+ else if (!declaration?.name)
1434
+ report('analyzer.dynamic.registration', node, `<${tag}> is registered with a class the analyzer cannot resolve.`);
1435
+ else
1436
+ registrations.push({
1437
+ tag,
1438
+ className: declaration.name.text,
1439
+ classFile: rel(declaration.getSourceFile().fileName),
1440
+ file: rel(file.fileName),
1441
+ location: where(node),
1442
+ });
1443
+ }
1444
+ if (ts.isIdentifier(callee) && isRecipeDefinition(callee) && !insideFunction(node)) {
1445
+ const definition = node.arguments[0];
1446
+ if (definition && ts.isObjectLiteralExpression(definition)) {
1447
+ const entries = objectEntries(definition, 'defineLayoutRecipe');
1448
+ const name = stringOf(entries.get('name'));
1449
+ const regionsNode = entries.get('regions');
1450
+ const regions = {};
1451
+ if (regionsNode && ts.isObjectLiteralExpression(regionsNode)) {
1452
+ for (const [region, helper] of objectEntries(regionsNode, 'regions')) {
1453
+ const call = ts.isArrayLiteralExpression(helper) ? helper.elements[0] : helper;
1454
+ regions[region] =
1455
+ call && ts.isCallExpression(call)
1456
+ ? (call.expression.getText().split('.').pop() ?? 'custom')
1457
+ : 'custom';
1458
+ }
1459
+ }
1460
+ const variantsNode = entries.get('variants');
1461
+ const variants = {};
1462
+ if (variantsNode && ts.isObjectLiteralExpression(variantsNode)) {
1463
+ for (const [attribute, values] of objectEntries(variantsNode, 'variants')) {
1464
+ variants[attribute] = ts.isObjectLiteralExpression(values)
1465
+ ? [...objectEntries(values, attribute).keys()]
1466
+ : [];
1467
+ }
1468
+ }
1469
+ const whenNode = entries.get('when');
1470
+ const conditions = whenNode && ts.isObjectLiteralExpression(whenNode)
1471
+ ? [...objectEntries(whenNode, 'when').keys()]
1472
+ : [];
1473
+ const partsNode = entries.get('parts');
1474
+ const parts = partsNode && ts.isArrayLiteralExpression(partsNode)
1475
+ ? partsNode.elements.map((e) => stringOf(e) ?? '').filter(Boolean)
1476
+ : [];
1477
+ const containerNode = entries.get('container');
1478
+ const container = containerNode && ts.isObjectLiteralExpression(containerNode)
1479
+ ? objectEntries(containerNode, 'container')
1480
+ : undefined;
1481
+ const containerName = stringOf(container?.get('name'));
1482
+ const containerSelector = stringOf(container?.get('selector'));
1483
+ if (!name)
1484
+ report('analyzer.dynamic.recipe', node, 'Recipes need a literal name to be documented.', "Add name: 'my-recipe'.");
1485
+ else {
1486
+ recipes.push({
1487
+ name,
1488
+ regions,
1489
+ variants,
1490
+ conditions,
1491
+ parts,
1492
+ ...(container
1493
+ ? {
1494
+ container: {
1495
+ ...(containerSelector ? { selector: containerSelector } : {}),
1496
+ ...(containerName ? { name: containerName } : {}),
1497
+ },
1498
+ }
1499
+ : {}),
1500
+ location: where(node),
1501
+ });
1502
+ }
1503
+ }
1504
+ else
1505
+ report('analyzer.dynamic.recipe', node, 'defineLayoutRecipe needs an object literal.');
1506
+ }
1507
+ if (ts.isIdentifier(callee) && fromOddkinCore(callee, 'defineModel')) {
1508
+ const name = stringOf(node.arguments[0]);
1509
+ const definition = node.arguments[1];
1510
+ const entries = definition && ts.isObjectLiteralExpression(definition)
1511
+ ? objectEntries(definition, 'defineModel')
1512
+ : undefined;
1513
+ const versionNode = entries?.get('version');
1514
+ const version = versionNode && ts.isNumericLiteral(versionNode) ? Number(versionNode.text) : undefined;
1515
+ const kinds = [];
1516
+ const findUnion = (n) => {
1517
+ if (ts.isCallExpression(n) &&
1518
+ ts.isPropertyAccessExpression(n.expression) &&
1519
+ n.expression.name.text === 'union' &&
1520
+ n.arguments[1] &&
1521
+ ts.isObjectLiteralExpression(n.arguments[1])) {
1522
+ kinds.push(...objectEntries(n.arguments[1], 'union').keys());
1523
+ }
1524
+ ts.forEachChild(n, findUnion);
1525
+ };
1526
+ if (entries?.get('schema'))
1527
+ findUnion(entries.get('schema'));
1528
+ if (!name || version === undefined)
1529
+ report('analyzer.dynamic.model', node, 'defineModel needs a literal name and version to be documented.');
1530
+ else
1531
+ models.push({ name, version, kinds, location: where(node) });
1532
+ }
1533
+ }
1534
+ ts.forEachChild(node, visit);
1535
+ };
1536
+ visit(file);
1537
+ const moduleSymbol = checker.getSymbolAtLocation(file);
1538
+ for (const symbol of moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : [])
1539
+ exportedNames.push(symbol.name);
1540
+ exportsBySource.set(rel(file.fileName), exportedNames.sort());
1541
+ }
1542
+ // Definitions replace the facts their class derives from render() and static styles.
1543
+ for (const registration of registrations) {
1544
+ if (!registration.definition)
1545
+ continue;
1546
+ const resolved = [...definitions.values()].find((d) => d?.info === registration.definition);
1547
+ const info = resolved && classify(resolved.classNode);
1548
+ if (resolved && info)
1549
+ components.set(resolved.classNode, overlay(info, resolved));
1550
+ }
1551
+ return {
1552
+ components: [...components.values()].filter((c) => !!c),
1553
+ registrations,
1554
+ recipes,
1555
+ models,
1556
+ definitions: [...definitions.values()]
1557
+ .filter((d) => !!d)
1558
+ .map((d) => d.info)
1559
+ .sort((a, b) => (a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0)),
1560
+ exports: exportsBySource,
1561
+ sources: {
1562
+ templates: [...templateSources.values()],
1563
+ styles: [...styleSources.values()],
1564
+ },
1565
+ diagnostics,
1566
+ };
1567
+ }
1568
+ /** Resolve a path relative to `root` into an absolute path (helper for callers). */
1569
+ export const absolute = (root, file) => resolve(root, file);
1570
+ //# sourceMappingURL=extract.js.map