@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.
- package/LICENSE +21 -0
- package/README.md +17 -2
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +65 -0
- package/dist/bin.js.map +1 -0
- package/dist/contracts.d.ts +29 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +57 -0
- package/dist/contracts.js.map +1 -0
- package/dist/docs.d.ts +34 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +63 -0
- package/dist/docs.js.map +1 -0
- package/dist/emit.d.ts +38 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +406 -0
- package/dist/emit.js.map +1 -0
- package/dist/examples.d.ts +26 -0
- package/dist/examples.d.ts.map +1 -0
- package/dist/examples.js +185 -0
- package/dist/examples.js.map +1 -0
- package/dist/extract.d.ts +208 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +1570 -0
- package/dist/extract.js.map +1 -0
- package/dist/foundations.d.ts +36 -0
- package/dist/foundations.d.ts.map +1 -0
- package/dist/foundations.js +108 -0
- package/dist/foundations.js.map +1 -0
- package/dist/history.d.ts +10 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +62 -0
- package/dist/history.js.map +1 -0
- package/dist/impact-bin.d.ts +3 -0
- package/dist/impact-bin.d.ts.map +1 -0
- package/dist/impact-bin.js +7 -0
- package/dist/impact-bin.js.map +1 -0
- package/dist/impact-cli.d.ts +8 -0
- package/dist/impact-cli.d.ts.map +1 -0
- package/dist/impact-cli.js +121 -0
- package/dist/impact-cli.js.map +1 -0
- package/dist/impact.d.ts +282 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +744 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +227 -0
- package/dist/index.js.map +1 -0
- package/dist/published.d.ts +43 -0
- package/dist/published.d.ts.map +1 -0
- package/dist/published.js +62 -0
- package/dist/published.js.map +1 -0
- package/dist/references.d.ts +70 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +255 -0
- package/dist/references.js.map +1 -0
- package/dist/style-mappings.d.ts +46 -0
- package/dist/style-mappings.d.ts.map +1 -0
- package/dist/style-mappings.js +246 -0
- package/dist/style-mappings.js.map +1 -0
- package/dist/stylesheets.d.ts +56 -0
- package/dist/stylesheets.d.ts.map +1 -0
- package/dist/stylesheets.js +83 -0
- package/dist/stylesheets.js.map +1 -0
- package/dist/verification.d.ts +62 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +115 -0
- package/dist/verification.js.map +1 -0
- package/package.json +40 -4
package/dist/extract.js
ADDED
|
@@ -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
|