@astryxdesign/cli 0.7.2-canary.ebcc404 → 0.7.2-canary.f8f7cf1

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 (134) hide show
  1. package/README.md +9 -8
  2. package/api/build/help/help.mjs +1 -1
  3. package/api/component/_adapter.d.mts +14 -0
  4. package/api/component/_adapter.mjs +48 -0
  5. package/api/component/component.mjs +10 -2
  6. package/api/component/integration-export-name.test.mjs +233 -8
  7. package/api/docs/_adapter.d.mts +11 -0
  8. package/api/docs/_adapter.mjs +244 -1
  9. package/api/docs/integrationDocs.test.mjs +124 -0
  10. package/api/doctor/theme-checks.mjs +1 -1
  11. package/api/gap-report/gap-report.mjs +2 -1
  12. package/api/index.d.mts +2 -0
  13. package/api/index.mjs +2 -0
  14. package/api/init/run/run.mjs +2 -2
  15. package/api/integration/_adapter.d.mts +12 -0
  16. package/api/integration/_adapter.mjs +54 -0
  17. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  18. package/api/integration/add-contribution.mjs +8 -4
  19. package/api/integration/add-contribution.test.mjs +4 -2
  20. package/api/integration/authoring-checks.mjs +11 -2
  21. package/api/integration/authoring-checks.test.mjs +39 -0
  22. package/api/integration/authoring-checks.type.d.mts +9 -0
  23. package/api/integration/authoring-checks.type.mjs +14 -1
  24. package/api/integration/check/check.d.mts +96 -0
  25. package/api/integration/check/check.mjs +357 -0
  26. package/api/integration/check/check.test.mjs +489 -0
  27. package/api/integration/integration-check.type.d.mts +118 -0
  28. package/api/integration/integration-check.type.mjs +84 -0
  29. package/api/integration/integrationCheck.doc.d.mts +10 -0
  30. package/api/integration/integrationCheck.doc.mjs +73 -0
  31. package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
  32. package/api/integration/pack-check.mjs +48 -3
  33. package/api/integration/pack-check.test.mjs +247 -0
  34. package/api/integration/validate-integration.mjs +8 -0
  35. package/api/integration/validate-integration.test.mjs +11 -0
  36. package/api/integration/validate-integration.type.d.mts +8 -0
  37. package/api/integration/validate-integration.type.mjs +12 -1
  38. package/api/integration/validateIntegration.doc.mjs +17 -3
  39. package/api/search/search.integration-check.test.mjs +24 -0
  40. package/api/search/search.mjs +20 -8
  41. package/api/theme/build/build.family.test.mjs +54 -6
  42. package/api/theme/build/build.mjs +114 -17
  43. package/api/theme/themeBuild.doc.mjs +4 -4
  44. package/assets/docs/README.md +10 -0
  45. package/assets/docs/author-a-theme.doc.mjs +1 -1
  46. package/assets/docs/getting-started.doc.mjs +3 -3
  47. package/assets/docs/theme.doc.dense.mjs +1 -1
  48. package/assets/docs/theme.doc.mjs +1 -1
  49. package/assets/docs/theme.doc.zh.mjs +1 -1
  50. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  51. package/assets/docs/tree/block-template.doc.mjs +2 -2
  52. package/assets/docs/tree/check-the-props.doc.mjs +196 -0
  53. package/assets/docs/tree/check-your-docs.doc.mjs +44 -3
  54. package/assets/docs/tree/checks.doc.mjs +21 -13
  55. package/assets/docs/tree/component-family.doc.mjs +5 -1
  56. package/assets/docs/tree/component-imports.doc.mjs +1 -1
  57. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  58. package/assets/docs/tree/links.doc.mjs +2 -2
  59. package/assets/docs/tree/page-template.doc.mjs +1 -1
  60. package/assets/docs/tree/quick-start.doc.mjs +1 -1
  61. package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
  62. package/assets/docs/tree/single-component.doc.mjs +3 -2
  63. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  64. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  65. package/assets/docs/tree/versioning.doc.mjs +1 -0
  66. package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
  67. package/assets/docs/use-a-theme.doc.mjs +2 -2
  68. package/assets/theme.template.ts +1 -1
  69. package/authoring/doctypes/component/component.doc.mjs +1 -1
  70. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  71. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  72. package/authoring/doctypes/template/parse.d.mts +6 -2
  73. package/authoring/doctypes/template/parse.mjs +3 -1
  74. package/authoring/doctypes/template/template.doc.mjs +1 -1
  75. package/authoring/doctypes/template/type.ts +22 -0
  76. package/authoring/index.d.ts +1 -0
  77. package/authoring/integration/integration.doc.mjs +8 -1
  78. package/authoring/integration/parse.test.mjs +10 -0
  79. package/authoring/integration/schema.d.mts +2 -0
  80. package/authoring/integration/schema.mjs +1 -0
  81. package/authoring/integration/type.ts +4 -0
  82. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  83. package/clients/cli/commands/build-theme.mjs +1 -1
  84. package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
  85. package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
  86. package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
  87. package/clients/cli/commands/doctor-integration.test.mjs +837 -0
  88. package/clients/cli/commands/doctor.mjs +190 -0
  89. package/clients/cli/commands/integration-check.doc.mjs +85 -0
  90. package/clients/cli/commands/integration-check.test.mjs +267 -0
  91. package/clients/cli/commands/integration.doc.mjs +6 -2
  92. package/clients/cli/commands/integration.mjs +107 -1
  93. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  94. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  95. package/clients/cli/index.mjs +1 -0
  96. package/foundation/config/project.mjs +82 -0
  97. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  98. package/foundation/discovery/component-discovery.d.mts +108 -17
  99. package/foundation/discovery/component-discovery.export-names.test.mjs +469 -2
  100. package/foundation/discovery/component-discovery.mjs +380 -47
  101. package/foundation/discovery/component-replacement.test.mjs +20 -0
  102. package/foundation/discovery/doc-types.d.mts +157 -0
  103. package/foundation/discovery/doc-types.mjs +1224 -0
  104. package/foundation/discovery/doc-types.test.mjs +652 -0
  105. package/foundation/discovery/docs-discovery.d.mts +81 -0
  106. package/foundation/discovery/docs-discovery.mjs +57 -0
  107. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  108. package/foundation/discovery/integration-doc-files.mjs +231 -0
  109. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  110. package/foundation/discovery/module-exports.d.mts +6 -0
  111. package/foundation/discovery/module-exports.mjs +15 -2
  112. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  113. package/foundation/discovery/package-tsconfig.mjs +228 -0
  114. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  115. package/foundation/discovery/project-typescript.d.mts +23 -0
  116. package/foundation/discovery/project-typescript.mjs +61 -0
  117. package/foundation/discovery/prop-drift.d.mts +278 -0
  118. package/foundation/discovery/prop-drift.mjs +2263 -0
  119. package/foundation/discovery/prop-drift.test.mjs +2219 -0
  120. package/foundation/discovery/type-match.d.mts +53 -0
  121. package/foundation/discovery/type-match.mjs +660 -0
  122. package/foundation/discovery/type-match.test.mjs +169 -0
  123. package/foundation/discovery/type-text.d.mts +68 -0
  124. package/foundation/discovery/type-text.mjs +658 -0
  125. package/foundation/discovery/type-text.test.mjs +196 -0
  126. package/foundation/discovery/unresolved-references.d.mts +62 -0
  127. package/foundation/discovery/unresolved-references.mjs +790 -0
  128. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  129. package/foundation/doc-compiler/doc-loads.test.mjs +6 -0
  130. package/foundation/doc-compiler/links.d.mts +9 -5
  131. package/foundation/doc-compiler/links.mjs +4 -2
  132. package/foundation/integrations/contribution-inventory.mjs +6 -1
  133. package/foundation/response/response-types.doc.mjs +7 -2
  134. package/package.json +9 -9
@@ -0,0 +1,660 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Judge a documented prop type against the type the code declares,
5
+ * only where both sides can be judged exactly.
6
+ *
7
+ * A documented type matches when its text equals the type as the source
8
+ * writes it (or as the compiler prints it) once type-text.mjs normalizes
9
+ * both, unless a `React.X` on either side names another type than a bare `X`
10
+ * does where the prop is declared (`React.KeyboardEvent` beside the DOM's
11
+ * `KeyboardEvent`, or a file's own `Key`); text that writes `readonly` matches only exactly as written,
12
+ * whitespace aside. Otherwise both sides are judged only when each is built from exact
13
+ * parts: the primitives `string`, `number`, `boolean`, `bigint`, `symbol`,
14
+ * `null`, `undefined`, `void` and `object`; string, number and boolean
15
+ * literals; string enum members (each of its own enum: another enum's member
16
+ * of the same value is another type, and a member beside the literal of its
17
+ * own value adds nothing, as TypeScript passes a member where that literal is
18
+ * taken); arrays of these (`T[]`, `Array<T>`,
19
+ * `ReadonlyArray<T>`); unions of these; and a name that resolves to one of
20
+ * these where the prop is declared (not one only TypeScript's own libraries
21
+ * declare, which the code may not mean; `React.X` resolves through `React`
22
+ * there, as the code would read it). The two sides must then hold the same
23
+ * parts, in any order, as TypeScript reduces a union at any depth (a literal
24
+ * beside its primitive is absorbed by it), with `undefined` set aside on an
25
+ * optional prop, and `null` and `undefined` dropped at any depth when the
26
+ * project has no strict null checks.
27
+ *
28
+ * Every other form is not compared, and never reported as a mismatch: a
29
+ * callback, an object, a tuple, a generic with arguments, `keyof`, an
30
+ * intersection, a mapped or conditional type, `any` or `unknown`, a numeric
31
+ * enum, two enums of one name (TypeScript relates them by their members, by
32
+ * rules its versions differ on), and a type that does not resolve.
33
+ *
34
+ * @input A documented type string, the prop's types, and the compiler.
35
+ * @output 'match', 'mismatch' or 'not_compared'.
36
+ * @position Used by the prop-drift comparison (prop-drift.mjs).
37
+ */
38
+
39
+ import {normalizeTypeText, UNPARSED} from './type-text.mjs';
40
+
41
+ /** @typedef {'match' | 'mismatch' | 'not_compared'} TypeVerdict */
42
+
43
+ /**
44
+ * @typedef {object} JudgeContext
45
+ * @property {any} ts
46
+ * @property {any} checker
47
+ * @property {any} location where names in the documented type resolve (the
48
+ * prop's declaration, else the component's source file)
49
+ * @property {boolean} optional the prop is optional, so `undefined` is set
50
+ * aside on both sides
51
+ * @property {(file: any) => boolean} [isDefaultLibrary] whether a source
52
+ * file is one of TypeScript's own libraries
53
+ * @property {boolean} [nullable] the project checks without strict null
54
+ * checks, so the compiler drops `null` and `undefined` from every type and
55
+ * both are set aside on both sides
56
+ * @property {boolean} [exactOptional] the project sets
57
+ * `exactOptionalPropertyTypes`: an optional prop takes `undefined` only when
58
+ * its type says so, and the compiler's mark for an absent prop is not a
59
+ * type the prop takes
60
+ * @property {string[][]} written the code's type as the source writes it and
61
+ * as the compiler prints it, each a list of union members
62
+ */
63
+
64
+ const MAX_DEPTH = 6;
65
+
66
+ /**
67
+ * A string literal as one part: quoted, with quotes and backslashes escaped,
68
+ * so no text inside it reads as other parts (`"a'|'b"` is one literal).
69
+ * @param {string} value
70
+ */
71
+ const literalPart = value =>
72
+ `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
73
+
74
+ /**
75
+ * Text as part of an enum part: each character but a word character as its
76
+ * code point (`%41;`), so no text reads as a separator and no string, an
77
+ * unpaired surrogate included, fails to encode.
78
+ * @param {string} text
79
+ */
80
+ const encodePart = text =>
81
+ [...text]
82
+ .map(c =>
83
+ /[\w-]/.test(c) ? c : `%${(c.codePointAt(0) ?? 0).toString(16)};`,
84
+ )
85
+ .join('');
86
+
87
+ /** @param {string} text */
88
+ const decodePart = text =>
89
+ text.replace(/%([0-9a-f]+);/g, (_, hex) =>
90
+ String.fromCodePoint(parseInt(hex, 16)),
91
+ );
92
+
93
+ /** Type keywords, which never name an alias. */
94
+ const KEYWORD_TYPES = new Set([
95
+ 'string',
96
+ 'number',
97
+ 'boolean',
98
+ 'bigint',
99
+ 'symbol',
100
+ 'object',
101
+ 'any',
102
+ 'unknown',
103
+ 'never',
104
+ 'void',
105
+ 'undefined',
106
+ 'null',
107
+ 'true',
108
+ 'false',
109
+ ]);
110
+
111
+ /**
112
+ * Judge a documented type. A doc may name an alias and spell out what it
113
+ * stands for, as `Name = expansion` or `Name (expansion)` (text that is not a
114
+ * type by itself): both parts are judged, and a mismatch in either is one;
115
+ * with one value after it (`Size = 'md'`, `Size = Size.Md`), or after a
116
+ * keyword, that part may be a default, and only the name is judged.
117
+ * @param {string} docType
118
+ * @param {any[]} codeTypes the prop's type in each member of the props type
119
+ * @param {JudgeContext} ctx
120
+ * @returns {TypeVerdict}
121
+ */
122
+ export function judgeType(docType, codeTypes, ctx) {
123
+ const notation = parseType(ctx.ts, docType)
124
+ ? null
125
+ : (/^\s*([A-Za-z_$][\w$.]*)\s*=(?!>)\s*([\s\S]+)$/.exec(docType) ??
126
+ /^\s*([A-Za-z_$][\w$.]*)\s*\(([\s\S]+)\)\s*$/.exec(docType));
127
+ /** @type {string[]} */
128
+ let texts = [docType];
129
+ if (notation) {
130
+ const [, name, rest] = notation;
131
+ const restNode = parseType(ctx.ts, rest);
132
+ if (!restNode) return 'not_compared'; // prose, not a type or a default
133
+ // After a type keyword, or as one value (`'md'`, `Size.Md`), the second
134
+ // part may be the prop's default, not an alias's expansion: only the
135
+ // type is judged.
136
+ const isDefault =
137
+ KEYWORD_TYPES.has(name) ||
138
+ (restNode != null && isValue(ctx.ts, restNode));
139
+ texts = isDefault ? [name] : [name, rest];
140
+ }
141
+ const verdicts = texts.map(text => judgeOne(text, codeTypes, ctx));
142
+ if (verdicts.includes('mismatch')) return 'mismatch';
143
+ return verdicts.every(verdict => verdict === 'match')
144
+ ? 'match'
145
+ : 'not_compared';
146
+ }
147
+
148
+ /** A string literal, kept as written, or a run of whitespace. */
149
+ const QUOTED_OR_SPACE =
150
+ /'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`|\s+/g;
151
+
152
+ /**
153
+ * A type's text without whitespace outside string literals, but for one space
154
+ * between two words.
155
+ * @param {string} text
156
+ */
157
+ function asWritten(text) {
158
+ return text
159
+ .trim()
160
+ .replace(QUOTED_OR_SPACE, (match, offset, whole) =>
161
+ !/^\s/.test(match)
162
+ ? match
163
+ : /[\w$]/.test(whole[offset - 1] ?? '') &&
164
+ /[\w$]/.test(whole[offset + match.length] ?? '')
165
+ ? ' '
166
+ : '',
167
+ );
168
+ }
169
+
170
+ /**
171
+ * @param {string} docType
172
+ * @param {any[]} codeTypes
173
+ * @param {JudgeContext} ctx
174
+ * @returns {TypeVerdict}
175
+ */
176
+ function judgeOne(docType, codeTypes, ctx) {
177
+ const {optional} = ctx;
178
+ const doc = normalizeTypeText(docType, {optional, ts: ctx.ts}).join('|');
179
+ // Text that does not parse is never a match.
180
+ if (doc.startsWith(UNPARSED)) return 'not_compared';
181
+ // The text match sets `readonly` aside, so text that writes `readonly`
182
+ // matches only as written: the same text, whitespace aside, with
183
+ // `readonly` kept on both sides. Any other goes to the parts below.
184
+ // Outside string literals: `'readonly'` is a word, not the modifier.
185
+ const readonly = (/** @type {string} */ text) =>
186
+ /\breadonly\b(?!\s*[:?])|\bReadonly(?:Array)?\s*</.test(
187
+ text.replace(QUOTED_OR_SPACE, part => (/^\s/.test(part) ? part : "''")),
188
+ );
189
+ const docReadonly = readonly(docType);
190
+ const sameText = ctx.written.some(
191
+ texts =>
192
+ (docReadonly || texts.some(readonly)
193
+ ? texts.length === 1 && asWritten(texts[0]) === asWritten(docType)
194
+ : texts.length > 0 &&
195
+ [
196
+ ...new Set(
197
+ texts.flatMap(text =>
198
+ normalizeTypeText(text, {optional, ts: ctx.ts}),
199
+ ),
200
+ ),
201
+ ]
202
+ .sort()
203
+ .join('|') === doc) && !reactNameDiffers(ctx, docType, texts),
204
+ );
205
+ if (sameText) return 'match';
206
+ const node = parseType(ctx.ts, docType);
207
+ const docParts = node ? docAtoms(ctx, node, 0) : null;
208
+ const codeParts = codeAtoms(ctx, codeTypes, 0);
209
+ if (!docParts || !codeParts) return 'not_compared';
210
+ // `readonly` set aside, outside string literals.
211
+ const mutable = (/** @type {string[]} */ atoms) =>
212
+ reduced(
213
+ ctx,
214
+ atoms.map(atom =>
215
+ atom.replace(/'(?:\\.|[^'\\])*'|readonly\[/g, part =>
216
+ part === 'readonly[' ? '[' : part,
217
+ ),
218
+ ),
219
+ true,
220
+ );
221
+ const doc_ = reduced(ctx, docParts, true);
222
+ const code = reduced(ctx, codeParts, true);
223
+ // A doc that names a mutable array where the code takes a readonly one
224
+ // matches, as TypeScript passes it.
225
+ if (doc_ === code || doc_ === mutable(codeParts)) return 'match';
226
+ // Two enums of one name are not judged (see the file's comment).
227
+ const owners = new Set(
228
+ [...docParts, ...codeParts].join('|').match(/enum:[^#|\]]*#[^#|\]]*/g),
229
+ );
230
+ const names = [...owners].map(owner => owner.split('#')[0]);
231
+ if (new Set(names).size < names.length) return 'not_compared';
232
+ if (mutable(docParts) !== mutable(codeParts)) return 'mismatch';
233
+ // Only `readonly` differs: a readonly doc for mutable code is wrong; a
234
+ // nested mix is not placed.
235
+ return code === mutable(codeParts) ? 'mismatch' : 'not_compared';
236
+ }
237
+
238
+ /**
239
+ * A union's exact parts, as TypeScript reduces them, at any depth: a literal
240
+ * beside its primitive is absorbed by it (`IconName | string` is `string`),
241
+ * and `null` and `undefined` are dropped without strict null checks, as the
242
+ * compiler drops them. On an optional prop `undefined` is set aside too, as
243
+ * the prop takes it anyway.
244
+ * @param {JudgeContext} ctx
245
+ * @param {string[]} atoms
246
+ * @param {boolean} top the prop's own type, not an array element
247
+ * @returns {string}
248
+ */
249
+ function reduced(ctx, atoms, top) {
250
+ const kept = new Set(
251
+ atoms.filter(
252
+ atom =>
253
+ !(atom === 'undefined' && ((top && ctx.optional) || ctx.nullable)) &&
254
+ !(atom === 'null' && ctx.nullable),
255
+ ),
256
+ );
257
+ if (kept.has('string')) {
258
+ for (const atom of [...kept]) {
259
+ if (atom.startsWith("'") || atom.startsWith('enum:')) kept.delete(atom);
260
+ }
261
+ }
262
+ for (const atom of [...kept]) {
263
+ const value = atom.startsWith('enum:')
264
+ ? decodePart(atom.slice(atom.lastIndexOf('#') + 1))
265
+ : null;
266
+ if (value != null && kept.has(literalPart(value))) kept.delete(atom);
267
+ }
268
+ if (kept.has('number')) {
269
+ for (const atom of [...kept]) {
270
+ if (/^-?\d/.test(atom) || /^(?:NaN|-?Infinity)$/.test(atom)) {
271
+ kept.delete(atom);
272
+ }
273
+ }
274
+ }
275
+ // A member another covers adds nothing, as TypeScript passes it there:
276
+ // `undefined` beside `void`, an array beside `object`, and an array whose
277
+ // element types another array (readonly, or as mutable) also takes.
278
+ if (kept.has('void')) kept.delete('undefined');
279
+ for (const atom of [...kept]) {
280
+ if (
281
+ isArrayPart(atom) &&
282
+ (kept.has('object') ||
283
+ [...kept].some(other => other !== atom && arrayCovers(other, atom)))
284
+ ) {
285
+ kept.delete(atom);
286
+ }
287
+ }
288
+ return [...kept].sort().join('|');
289
+ }
290
+
291
+ /** @param {string} atom */
292
+ const isArrayPart = atom => /^(?:readonly)?\[/.test(atom);
293
+
294
+ /**
295
+ * The parts of a reduced union's text, split at the top level, outside
296
+ * string literals and arrays.
297
+ * @param {string} text
298
+ * @returns {string[]}
299
+ */
300
+ function unionParts(text) {
301
+ /** @type {string[]} */
302
+ const parts = [];
303
+ let depth = 0;
304
+ let quoted = false;
305
+ let start = 0;
306
+ for (let i = 0; i < text.length; i++) {
307
+ const c = text[i];
308
+ if (quoted) {
309
+ if (c === '\\') i++;
310
+ else if (c === "'") quoted = false;
311
+ } else if (c === "'") quoted = true;
312
+ else if (c === '[') depth++;
313
+ else if (c === ']') depth--;
314
+ else if (c === '|' && depth === 0) {
315
+ parts.push(text.slice(start, i));
316
+ start = i + 1;
317
+ }
318
+ }
319
+ parts.push(text.slice(start));
320
+ return parts.filter(Boolean);
321
+ }
322
+
323
+ /**
324
+ * Does array part `wide` take every value of array part `narrow`: readonly,
325
+ * or both mutable, with each of `narrow`'s element types one of `wide`'s or
326
+ * covered by one?
327
+ * @param {string} wide
328
+ * @param {string} narrow
329
+ * @returns {boolean}
330
+ */
331
+ function arrayCovers(wide, narrow) {
332
+ if (!isArrayPart(wide) || !isArrayPart(narrow)) return false;
333
+ const readonly = (/** @type {string} */ part) => part.startsWith('readonly');
334
+ if (readonly(narrow) && !readonly(wide)) return false;
335
+ const inner = (/** @type {string} */ part) =>
336
+ unionParts(part.slice(part.indexOf('[') + 1, -1));
337
+ const taken = inner(wide);
338
+ return inner(narrow).every(
339
+ part =>
340
+ taken.includes(part) ||
341
+ (taken.includes('string') &&
342
+ (part.startsWith("'") || part.startsWith('enum:'))) ||
343
+ (taken.includes('number') && /^(?:-?\d|NaN$|-?Infinity$)/.test(part)) ||
344
+ (part === 'undefined' && taken.includes('void')) ||
345
+ (isArrayPart(part) &&
346
+ (taken.includes('object') ||
347
+ taken.some(other => arrayCovers(other, part)))),
348
+ );
349
+ }
350
+
351
+ /**
352
+ * Is a type node one value a default could be: a literal (`'md'`, `0`,
353
+ * `false`, `null`), or a name with no type arguments (`Size.Md`)?
354
+ * @param {any} ts
355
+ * @param {any} node
356
+ * @returns {boolean}
357
+ */
358
+ function isValue(ts, node) {
359
+ if (ts.isParenthesizedTypeNode(node)) return isValue(ts, node.type);
360
+ return (
361
+ ts.isLiteralTypeNode(node) ||
362
+ node.kind === ts.SyntaxKind.UndefinedKeyword ||
363
+ node.kind === ts.SyntaxKind.NullKeyword ||
364
+ (ts.isTypeReferenceNode(node) && !node.typeArguments?.length)
365
+ );
366
+ }
367
+
368
+ /**
369
+ * Parse a type expression with the project's TypeScript.
370
+ * @param {any} ts
371
+ * @param {string} text
372
+ * @returns {any | null} the type node, or null when the text is not a type
373
+ */
374
+ function parseType(ts, text) {
375
+ const source = ts.createSourceFile(
376
+ '__documented_type__.ts',
377
+ `type __Documented = ${text};`,
378
+ ts.ScriptTarget.Latest,
379
+ true,
380
+ ts.ScriptKind.TS,
381
+ );
382
+ const [statement, ...rest] = source.statements;
383
+ if (
384
+ (source.parseDiagnostics?.length ?? 0) > 0 ||
385
+ rest.length > 0 ||
386
+ !statement ||
387
+ !ts.isTypeAliasDeclaration(statement) ||
388
+ statement.type.end !== source.text.length - 1
389
+ ) {
390
+ return null;
391
+ }
392
+ return statement.type;
393
+ }
394
+
395
+ /**
396
+ * The exact parts of a documented type, or null when it has a part that is
397
+ * not exact.
398
+ * @param {JudgeContext} ctx
399
+ * @param {any} node
400
+ * @param {number} depth
401
+ * @returns {string[] | null}
402
+ */
403
+ function docAtoms(ctx, node, depth) {
404
+ const {ts} = ctx;
405
+ const SyntaxKind = ts.SyntaxKind;
406
+ if (depth > MAX_DEPTH) return null;
407
+ if (ts.isParenthesizedTypeNode(node)) return docAtoms(ctx, node.type, depth);
408
+ if (ts.isUnionTypeNode(node)) {
409
+ const parts = node.types.map(
410
+ /** @param {any} t */ t => docAtoms(ctx, t, depth + 1),
411
+ );
412
+ return parts.every(Boolean) ? parts.flat() : null;
413
+ }
414
+ /** @type {Record<number, string[]>} */
415
+ const keywords = {
416
+ [SyntaxKind.StringKeyword]: ['string'],
417
+ [SyntaxKind.NumberKeyword]: ['number'],
418
+ [SyntaxKind.BooleanKeyword]: ['false', 'true'],
419
+ [SyntaxKind.BigIntKeyword]: ['bigint'],
420
+ [SyntaxKind.SymbolKeyword]: ['symbol'],
421
+ [SyntaxKind.UndefinedKeyword]: ['undefined'],
422
+ [SyntaxKind.VoidKeyword]: ['void'],
423
+ [SyntaxKind.ObjectKeyword]: ['object'],
424
+ [SyntaxKind.NullKeyword]: ['null'],
425
+ [SyntaxKind.NeverKeyword]: [],
426
+ };
427
+ if (keywords[node.kind]) return keywords[node.kind];
428
+ if (ts.isLiteralTypeNode(node)) {
429
+ const literal = node.literal;
430
+ if (
431
+ ts.isStringLiteral(literal) ||
432
+ ts.isNoSubstitutionTemplateLiteral(literal)
433
+ )
434
+ return [literalPart(literal.text)];
435
+ if (ts.isNumericLiteral(literal)) return [String(Number(literal.text))];
436
+ if (
437
+ ts.isPrefixUnaryExpression(literal) &&
438
+ literal.operator === SyntaxKind.MinusToken &&
439
+ ts.isNumericLiteral(literal.operand)
440
+ ) {
441
+ return [String(-Number(literal.operand.text))];
442
+ }
443
+ if (literal.kind === SyntaxKind.TrueKeyword) return ['true'];
444
+ if (literal.kind === SyntaxKind.FalseKeyword) return ['false'];
445
+ if (literal.kind === SyntaxKind.NullKeyword) return ['null'];
446
+ return null;
447
+ }
448
+ // A readonly array is its own type: TypeScript does not pass one where a
449
+ // mutable array is taken.
450
+ const array = (/** @type {any} */ element, readonly = false) => {
451
+ const parts = docAtoms(ctx, element, depth + 1);
452
+ return parts
453
+ ? [`${readonly ? 'readonly' : ''}[${reduced(ctx, parts, false)}]`]
454
+ : null;
455
+ };
456
+ if (ts.isArrayTypeNode(node)) return array(node.elementType);
457
+ if (
458
+ ts.isTypeOperatorNode(node) &&
459
+ node.operator === SyntaxKind.ReadonlyKeyword &&
460
+ ts.isArrayTypeNode(node.type)
461
+ ) {
462
+ return array(node.type.elementType, true);
463
+ }
464
+ if (ts.isTypeReferenceNode(node)) {
465
+ const name = referenceName(ts, node.typeName);
466
+ const args = node.typeArguments ?? [];
467
+ if ((name === 'Array' || name === 'ReadonlyArray') && args.length === 1)
468
+ return array(args[0], name === 'ReadonlyArray');
469
+ if (args.length > 0) return null;
470
+ const named = resolveName(ctx, name);
471
+ return named ? codeAtoms(ctx, [named], depth + 1) : null;
472
+ }
473
+ return null;
474
+ }
475
+
476
+ /**
477
+ * The exact parts of the code's type, or null when it has a part that is not
478
+ * exact.
479
+ * @param {JudgeContext} ctx
480
+ * @param {any[]} types
481
+ * @param {number} depth
482
+ * @returns {string[] | null}
483
+ */
484
+ function codeAtoms(ctx, types, depth) {
485
+ const {ts, checker} = ctx;
486
+ const F = ts.TypeFlags;
487
+ if (depth > MAX_DEPTH) return null;
488
+ /** @type {string[]} */
489
+ const out = [];
490
+ for (const type of types) {
491
+ for (const t of type.isUnion() ? type.types : [type]) {
492
+ if (t.flags & F.EnumLiteral) {
493
+ // A string enum member is exact, and only its own enum's: TypeScript
494
+ // takes no other enum's member of the same value. A numeric enum also
495
+ // takes numbers.
496
+ const decl = t.symbol?.valueDeclaration;
497
+ const owner = decl && checker.getSymbolAtLocation(decl.parent.name);
498
+ if (!(t.flags & F.StringLiteral) || !owner) return null;
499
+ out.push(
500
+ `enum:${owner.name}#${encodePart(checker.getFullyQualifiedName(owner))}#${encodePart(t.value)}`,
501
+ );
502
+ } else if (t.flags & F.StringLiteral) out.push(literalPart(t.value));
503
+ else if (t.flags & F.NumberLiteral) out.push(String(t.value));
504
+ else if (t.flags & F.BooleanLiteral) out.push(t.intrinsicName);
505
+ else if (t.flags & F.String) out.push('string');
506
+ else if (t.flags & F.Number) out.push('number');
507
+ else if (t.flags & F.BigInt) out.push('bigint');
508
+ else if (t.flags & F.ESSymbol) out.push('symbol');
509
+ else if (t.flags & F.Null) out.push('null');
510
+ else if (t.flags & F.Undefined) {
511
+ // `exactOptionalPropertyTypes` marks an optional prop with its own
512
+ // `undefined`, which `prop={undefined}` does not satisfy.
513
+ if (!ctx.exactOptional || t === ctx.checker.getUndefinedType?.()) {
514
+ out.push('undefined');
515
+ }
516
+ } else if (t.flags & F.Void) out.push('void');
517
+ else if (t.flags & F.NonPrimitive) out.push('object');
518
+ else if (t.flags & F.Never) continue;
519
+ else {
520
+ const name = t.symbol?.name;
521
+ const element =
522
+ (name === 'Array' || name === 'ReadonlyArray') &&
523
+ t.objectFlags & ts.ObjectFlags.Reference
524
+ ? checker.getTypeArguments(t)[0]
525
+ : undefined;
526
+ const parts = element ? codeAtoms(ctx, [element], depth + 1) : null;
527
+ if (!parts) return null;
528
+ out.push(
529
+ `${name === 'ReadonlyArray' ? 'readonly' : ''}[${reduced(ctx, parts, false)}]`,
530
+ );
531
+ }
532
+ }
533
+ }
534
+ return out;
535
+ }
536
+
537
+ /**
538
+ * The text of a type reference's name, `React.` kept: it resolves through
539
+ * `React` where the prop is declared, which may not be what a bare name
540
+ * means there.
541
+ * @param {any} ts
542
+ * @param {any} typeName
543
+ * @returns {string}
544
+ */
545
+ function referenceName(ts, typeName) {
546
+ if (ts.isIdentifier(typeName)) return typeName.text;
547
+ return `${referenceName(ts, typeName.left)}.${typeName.right.text}`;
548
+ }
549
+
550
+ /**
551
+ * Does one side write `React.X` where the other writes a bare `X` that names
552
+ * another type where the prop is declared (the DOM's `KeyboardEvent`, a
553
+ * file's own `Key`)? The text reading sets the qualifier aside, so then the
554
+ * two must not be read as one. A bare name that resolves to nothing there
555
+ * leaves `React.X` as what it means.
556
+ * @param {JudgeContext} ctx
557
+ * @param {string} docType
558
+ * @param {string[]} texts the code's type, one way it is written
559
+ * @returns {boolean}
560
+ */
561
+ function reactNameDiffers(ctx, docType, texts) {
562
+ const names = (/** @type {string[]} */ list) => {
563
+ const masked = list
564
+ .map(text =>
565
+ text.replace(QUOTED_OR_SPACE, part => (/^\s/.test(part) ? ' ' : "''")),
566
+ )
567
+ .join(' ');
568
+ const qualified = new Set(
569
+ [...masked.matchAll(/(?<![\w$.])React\s*\.\s*([A-Za-z_$][\w$]*)/g)].map(
570
+ match => match[1],
571
+ ),
572
+ );
573
+ const bare = new Set(
574
+ [...masked.matchAll(/(?<![\w$.]|\.\s)([A-Za-z_$][\w$]*)/g)].map(
575
+ match => match[1],
576
+ ),
577
+ );
578
+ return {qualified, bare};
579
+ };
580
+ const doc = names([docType]);
581
+ const code = names(texts);
582
+ const crossed = [
583
+ ...[...doc.qualified].filter(name => code.bare.has(name)),
584
+ ...[...code.qualified].filter(name => doc.bare.has(name)),
585
+ ];
586
+ return crossed.some(name => {
587
+ const bare = resolveSymbol(ctx, name);
588
+ return !!bare && bare !== resolveSymbol(ctx, `React.${name}`);
589
+ });
590
+ }
591
+
592
+ /**
593
+ * The type a name the documented type uses resolves to, where the prop is
594
+ * declared, or null.
595
+ * @param {JudgeContext} ctx
596
+ * @param {string} name
597
+ * @returns {any | null}
598
+ */
599
+ function resolveName(ctx, name) {
600
+ const {ts, checker} = ctx;
601
+ try {
602
+ const resolved = resolveSymbol(ctx, name);
603
+ // A name only TypeScript's own libraries declare (the DOM's
604
+ // `ScrollBehavior`) is one the code may not mean: not resolved.
605
+ const declarations = resolved?.declarations ?? [];
606
+ if (
607
+ ctx.isDefaultLibrary &&
608
+ declarations.length > 0 &&
609
+ declarations.every(
610
+ /** @param {any} d */ d => ctx.isDefaultLibrary?.(d.getSourceFile()),
611
+ )
612
+ ) {
613
+ return null;
614
+ }
615
+ return resolved && resolved.flags & ts.SymbolFlags.Type
616
+ ? checker.getDeclaredTypeOfSymbol(resolved)
617
+ : null;
618
+ } catch {
619
+ return null;
620
+ }
621
+ }
622
+
623
+ /**
624
+ * The symbol a name resolves to where the prop is declared, through aliases
625
+ * and namespaces (`React.Key` through `React`, imported or global), or
626
+ * undefined.
627
+ * @param {JudgeContext} ctx
628
+ * @param {string} name
629
+ * @returns {any}
630
+ */
631
+ function resolveSymbol(ctx, name) {
632
+ const {ts, checker} = ctx;
633
+ const meaning =
634
+ ts.SymbolFlags.Type | ts.SymbolFlags.Alias | ts.SymbolFlags.Namespace;
635
+ const target = (/** @type {any} */ symbol) =>
636
+ symbol && symbol.flags & ts.SymbolFlags.Alias
637
+ ? checker.getAliasedSymbol(symbol)
638
+ : symbol;
639
+ try {
640
+ const [head, ...tail] = name.split('.');
641
+ // A module's own declarations can come back as local symbols without
642
+ // flags: take the export symbol, and the first one that names a type or
643
+ // namespace.
644
+ let symbol = checker
645
+ .getSymbolsInScope(ctx.location, meaning)
646
+ .filter(/** @param {any} s */ s => s.name === head)
647
+ .map(/** @param {any} s */ s => checker.getExportSymbolOfSymbol(s))
648
+ .find(/** @param {any} s */ s => (s.flags & meaning) !== 0);
649
+ for (const part of tail) {
650
+ symbol = symbol
651
+ ? checker
652
+ .getExportsOfModule(target(symbol))
653
+ .find(/** @param {any} s */ s => s.name === part)
654
+ : undefined;
655
+ }
656
+ return target(symbol);
657
+ } catch {
658
+ return undefined;
659
+ }
660
+ }