@astryxdesign/cli 0.7.2-canary.fbc03f7 → 0.7.2-canary.fdd384f

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 (139) hide show
  1. package/README.md +9 -8
  2. package/api/build/help/help.mjs +1 -1
  3. package/api/component/_adapter.d.mts +41 -1
  4. package/api/component/_adapter.mjs +93 -7
  5. package/api/component/component.mjs +22 -5
  6. package/api/component/integration-export-name.test.mjs +644 -0
  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 +67 -4
  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 +51 -97
  33. package/api/integration/pack-check.test.mjs +359 -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.d.mts +9 -0
  40. package/api/search/search.integration-check.test.mjs +24 -0
  41. package/api/search/search.mjs +100 -13
  42. package/api/swizzle/copy/copy.mjs +28 -14
  43. package/api/theme/build/build.family.test.mjs +54 -6
  44. package/api/theme/build/build.mjs +114 -17
  45. package/api/theme/themeBuild.doc.mjs +4 -4
  46. package/assets/docs/README.md +10 -0
  47. package/assets/docs/author-a-theme.doc.mjs +1 -1
  48. package/assets/docs/getting-started.doc.mjs +3 -3
  49. package/assets/docs/theme.doc.dense.mjs +1 -1
  50. package/assets/docs/theme.doc.mjs +1 -1
  51. package/assets/docs/theme.doc.zh.mjs +1 -1
  52. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  53. package/assets/docs/tree/block-template.doc.mjs +2 -2
  54. package/assets/docs/tree/check-the-props.doc.mjs +196 -0
  55. package/assets/docs/tree/check-your-docs.doc.mjs +44 -3
  56. package/assets/docs/tree/checks.doc.mjs +21 -13
  57. package/assets/docs/tree/component-family.doc.mjs +5 -1
  58. package/assets/docs/tree/component-imports.doc.mjs +8 -0
  59. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  60. package/assets/docs/tree/links.doc.mjs +2 -2
  61. package/assets/docs/tree/page-template.doc.mjs +1 -1
  62. package/assets/docs/tree/quick-start.doc.mjs +1 -1
  63. package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
  64. package/assets/docs/tree/single-component.doc.mjs +3 -2
  65. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  66. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  67. package/assets/docs/tree/versioning.doc.mjs +1 -0
  68. package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
  69. package/assets/docs/use-a-theme.doc.mjs +2 -2
  70. package/assets/theme.template.ts +1 -1
  71. package/authoring/doctypes/component/component.doc.mjs +2 -2
  72. package/authoring/doctypes/component/type.ts +6 -3
  73. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  74. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  75. package/authoring/doctypes/template/parse.d.mts +6 -2
  76. package/authoring/doctypes/template/parse.mjs +3 -1
  77. package/authoring/doctypes/template/template.doc.mjs +1 -1
  78. package/authoring/doctypes/template/type.ts +22 -0
  79. package/authoring/index.d.ts +1 -0
  80. package/authoring/integration/integration.doc.mjs +8 -1
  81. package/authoring/integration/parse.test.mjs +10 -0
  82. package/authoring/integration/schema.d.mts +2 -0
  83. package/authoring/integration/schema.mjs +1 -0
  84. package/authoring/integration/type.ts +4 -0
  85. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  86. package/clients/cli/commands/build-theme.mjs +1 -1
  87. package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
  88. package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
  89. package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
  90. package/clients/cli/commands/doctor-integration.test.mjs +837 -0
  91. package/clients/cli/commands/doctor.mjs +190 -0
  92. package/clients/cli/commands/integration-check.doc.mjs +85 -0
  93. package/clients/cli/commands/integration-check.test.mjs +267 -0
  94. package/clients/cli/commands/integration.doc.mjs +6 -2
  95. package/clients/cli/commands/integration.mjs +107 -1
  96. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  97. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  98. package/clients/cli/index.mjs +1 -0
  99. package/foundation/config/project.mjs +82 -0
  100. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  101. package/foundation/discovery/component-discovery.d.mts +178 -10
  102. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  103. package/foundation/discovery/component-discovery.mjs +659 -62
  104. package/foundation/discovery/component-replacement.mjs +18 -0
  105. package/foundation/discovery/component-replacement.test.mjs +40 -0
  106. package/foundation/discovery/doc-types.d.mts +157 -0
  107. package/foundation/discovery/doc-types.mjs +1224 -0
  108. package/foundation/discovery/doc-types.test.mjs +652 -0
  109. package/foundation/discovery/docs-discovery.d.mts +81 -0
  110. package/foundation/discovery/docs-discovery.mjs +57 -0
  111. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  112. package/foundation/discovery/integration-doc-files.mjs +231 -0
  113. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  114. package/foundation/discovery/module-exports.d.mts +27 -0
  115. package/foundation/discovery/module-exports.mjs +356 -31
  116. package/foundation/discovery/module-exports.test.mjs +172 -0
  117. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  118. package/foundation/discovery/package-tsconfig.mjs +228 -0
  119. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  120. package/foundation/discovery/project-typescript.d.mts +23 -0
  121. package/foundation/discovery/project-typescript.mjs +61 -0
  122. package/foundation/discovery/prop-drift.d.mts +278 -0
  123. package/foundation/discovery/prop-drift.mjs +2263 -0
  124. package/foundation/discovery/prop-drift.test.mjs +2219 -0
  125. package/foundation/discovery/type-match.d.mts +53 -0
  126. package/foundation/discovery/type-match.mjs +660 -0
  127. package/foundation/discovery/type-match.test.mjs +169 -0
  128. package/foundation/discovery/type-text.d.mts +68 -0
  129. package/foundation/discovery/type-text.mjs +658 -0
  130. package/foundation/discovery/type-text.test.mjs +196 -0
  131. package/foundation/discovery/unresolved-references.d.mts +62 -0
  132. package/foundation/discovery/unresolved-references.mjs +790 -0
  133. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  134. package/foundation/doc-compiler/doc-loads.test.mjs +6 -0
  135. package/foundation/doc-compiler/links.d.mts +9 -5
  136. package/foundation/doc-compiler/links.mjs +4 -2
  137. package/foundation/integrations/contribution-inventory.mjs +6 -1
  138. package/foundation/response/response-types.doc.mjs +7 -2
  139. package/package.json +9 -9
@@ -0,0 +1,658 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Loose comparison of TypeScript type text, for checking a documented
5
+ * prop type against the type its source declares.
6
+ *
7
+ * {@link normalizeTypeText} parses the text with the project's own TypeScript
8
+ * and prints the type canonically, so grouping always comes from the parse.
9
+ * Two texts are the same type here when they print the same. The printer
10
+ * merges only what TypeScript reads as one type:
11
+ * - comments, whitespace, parentheses that change nothing, and quote style;
12
+ * - the names of function and tuple parameters (a `this` parameter is kept,
13
+ * and a type predicate names its parameter by place);
14
+ * - the order of a union's members and of an object type's properties;
15
+ * - `Array<T>` and `T[]`; a `React.` qualifier;
16
+ * - `true | false` and `boolean`; a string, number or boolean literal beside
17
+ * its primitive; `never` among other union members; and `undefined` on an
18
+ * optional prop.
19
+ * It also sets `readonly`, `Readonly<…>` and `ReadonlyArray<…>` aside: the
20
+ * prop type judgment compares text that writes `readonly` exactly instead.
21
+ * Below the top of a type, a function or conditional type is always printed
22
+ * in parentheses, and a union or intersection wherever its members would
23
+ * otherwise bind differently. Any form the printer does not know is kept as
24
+ * written (comments and whitespace aside), so it never reads as another
25
+ * type. Text that does not parse as one type comes back marked with
26
+ * {@link UNPARSED}, which no printed type starts with, so it never matches.
27
+ * {@link typeTextTooOld} says when the project's TypeScript is too old to
28
+ * read type text with.
29
+ *
30
+ * @input Type text as written in a doc or a source file, or printed by the
31
+ * TypeScript compiler, and the project's own `typescript`.
32
+ * @output The canonical members of the type's top-level union, sorted.
33
+ * @position Used by the prop type judgment (type-match.mjs).
34
+ */
35
+
36
+ /** Marks text that does not parse as one type: no printed type has it. */
37
+ export const UNPARSED = '\u0000';
38
+
39
+ /**
40
+ * Why the project's TypeScript is too old for this module (before 4.8, the
41
+ * oldest every printed form is tested on), or null.
42
+ * @param {{version?: string}} ts
43
+ * @returns {string | null}
44
+ */
45
+ export function typeTextTooOld(ts) {
46
+ const [major, minor] = String(ts.version).split('.').map(Number);
47
+ return major > 4 || (major === 4 && minor >= 8)
48
+ ? null
49
+ : `TypeScript ${ts.version} is older than 4.8, the oldest the type comparison reads`;
50
+ }
51
+
52
+ /** How tightly a printed type binds: a slot needs at least its level. */
53
+ const FN = 0; // a function, constructor or conditional type
54
+ const UNION = 1;
55
+ const INTERSECTION = 2;
56
+ const OPERATOR = 3; // `keyof T`, `unique symbol`, `infer U`
57
+ const POSTFIX = 4; // `T[]`, `T[K]`
58
+ const PRIMARY = 5;
59
+
60
+ /**
61
+ * @typedef {object} Printed
62
+ * @property {string} text
63
+ * @property {number} level
64
+ * @property {'string' | 'number' | 'boolean' | 'other'} [literal] a literal
65
+ * its primitive absorbs in a union
66
+ */
67
+
68
+ /** @type {WeakMap<object, Map<string, readonly string[]>>} */
69
+ const CACHE = new WeakMap();
70
+
71
+ /**
72
+ * Remove comments and whitespace outside string literals, keeping one space
73
+ * between two words. Used for text the printer keeps as written.
74
+ * @param {string} text
75
+ * @returns {string}
76
+ */
77
+ function compact(text) {
78
+ /** @param {number} at */
79
+ const comment = at =>
80
+ text[at] === '/' && (text[at + 1] === '*' || text[at + 1] === '/');
81
+ let out = '';
82
+ for (let i = 0; i < text.length; i++) {
83
+ const c = text[i];
84
+ if (c === '"' || c === "'" || c === '`') {
85
+ let j = i + 1;
86
+ while (j < text.length && text[j] !== c) j += text[j] === '\\' ? 2 : 1;
87
+ out += text.slice(i, j + 1);
88
+ i = j;
89
+ continue;
90
+ }
91
+ // Whitespace and comments: either separates two words.
92
+ if (/\s/.test(c) || comment(i)) {
93
+ let k = i;
94
+ while (k < text.length && (/\s/.test(text[k]) || comment(k))) {
95
+ if (!comment(k)) {
96
+ k++;
97
+ continue;
98
+ }
99
+ const end =
100
+ text[k + 1] === '*'
101
+ ? text.indexOf('*/', k + 2) + 2
102
+ : text.indexOf('\n', k + 2);
103
+ k = end <= 1 ? text.length : end;
104
+ }
105
+ const prev = out[out.length - 1];
106
+ if (prev && /[\w$]/.test(prev) && /[\w$]/.test(text[k] ?? '')) {
107
+ out += ' ';
108
+ }
109
+ i = k - 1;
110
+ continue;
111
+ }
112
+ out += c;
113
+ }
114
+ return out;
115
+ }
116
+
117
+ /** @param {string} value */
118
+ const quote = value =>
119
+ `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n')}'`;
120
+
121
+ /**
122
+ * Print a type node canonically.
123
+ * @param {any} ts
124
+ * @param {any} sf
125
+ * @param {any} node
126
+ * @returns {Printed}
127
+ */
128
+ function print(ts, sf, node) {
129
+ const K = ts.SyntaxKind;
130
+ /** @type {(child: any, level: number) => string} */
131
+ const slot = (child, level) => at(ts, sf, child, level);
132
+ /** @type {(text: string, level?: number) => Printed} */
133
+ const done = (text, level = PRIMARY) => ({text, level});
134
+ const kept = () => done(compact(node.getText(sf)), FN);
135
+
136
+ if (ts.isParenthesizedTypeNode(node)) return print(ts, sf, node.type);
137
+ if (ts.isUnionTypeNode(node)) {
138
+ const parts = unionParts(ts, sf, node, false);
139
+ return parts.length === 1
140
+ ? parts[0]
141
+ : done(parts.map(display).join('|'), UNION);
142
+ }
143
+ if (ts.isIntersectionTypeNode(node)) {
144
+ /** @type {any[]} */
145
+ const parts = [];
146
+ const flatten = (/** @type {any} */ n) => {
147
+ const inner = unwrap(ts, n);
148
+ if (ts.isIntersectionTypeNode(inner)) inner.types.forEach(flatten);
149
+ else parts.push(inner);
150
+ };
151
+ flatten(node);
152
+ return done(parts.map(p => slot(p, OPERATOR)).join('&'), INTERSECTION);
153
+ }
154
+ if (ts.isFunctionTypeNode(node) || ts.isConstructorTypeNode(node)) {
155
+ const prefix = ts.isConstructorTypeNode(node)
156
+ ? `${node.modifiers?.some((/** @type {any} */ m) => m.kind === K.AbstractKeyword) ? 'abstract ' : ''}new`
157
+ : '';
158
+ return done(
159
+ `${prefix}${typeParameters(ts, sf, node)}(${parameters(ts, sf, node.parameters)})=>${returnType(ts, sf, node)}`,
160
+ FN,
161
+ );
162
+ }
163
+ if (ts.isConditionalTypeNode(node)) {
164
+ return done(
165
+ `${slot(node.checkType, UNION)} extends ${slot(node.extendsType, UNION)}?${slot(node.trueType, UNION)}:${slot(node.falseType, UNION)}`,
166
+ FN,
167
+ );
168
+ }
169
+ if (ts.isArrayTypeNode(node)) {
170
+ return done(`${slot(node.elementType, POSTFIX)}[]`, POSTFIX);
171
+ }
172
+ if (ts.isIndexedAccessTypeNode(node)) {
173
+ return done(
174
+ `${slot(node.objectType, POSTFIX)}[${slot(node.indexType, UNION)}]`,
175
+ POSTFIX,
176
+ );
177
+ }
178
+ if (ts.isTypeOperatorNode(node)) {
179
+ // `readonly` is set aside.
180
+ if (node.operator === K.ReadonlyKeyword) return print(ts, sf, node.type);
181
+ const word = node.operator === K.KeyOfKeyword ? 'keyof' : 'unique';
182
+ return done(`${word} ${slot(node.type, OPERATOR)}`, OPERATOR);
183
+ }
184
+ if (ts.isInferTypeNode(node)) {
185
+ const parameter = node.typeParameter;
186
+ return parameter.constraint
187
+ ? done(
188
+ `infer ${parameter.name.text} extends ${slot(parameter.constraint, UNION)}`,
189
+ FN,
190
+ )
191
+ : done(`infer ${parameter.name.text}`, OPERATOR);
192
+ }
193
+ if (ts.isTypeReferenceNode(node)) {
194
+ const name = entityName(ts, node.typeName, true);
195
+ const args = node.typeArguments ?? [];
196
+ if (args.length === 1 && (name === 'Array' || name === 'ReadonlyArray')) {
197
+ return done(`${slot(args[0], POSTFIX)}[]`, POSTFIX);
198
+ }
199
+ // `Readonly<T>` is set aside: it prints as `T`, grouped where `T` is.
200
+ if (args.length === 1 && name === 'Readonly') return print(ts, sf, args[0]);
201
+ return done(name + typeArguments(ts, sf, args));
202
+ }
203
+ if (ts.isTypeQueryNode(node)) {
204
+ return done(
205
+ `typeof ${entityName(ts, node.exprName, false)}${typeArguments(ts, sf, node.typeArguments ?? [])}`,
206
+ );
207
+ }
208
+ if (ts.isImportTypeNode(node)) {
209
+ const literal = node.argument?.literal;
210
+ // Import attributes change which file a module resolves to: kept as
211
+ // written.
212
+ if (
213
+ !literal ||
214
+ !ts.isStringLiteral(literal) ||
215
+ node.attributes ||
216
+ node.assertions
217
+ ) {
218
+ return kept();
219
+ }
220
+ const qualifier = node.qualifier
221
+ ? `.${entityName(ts, node.qualifier, false)}`
222
+ : '';
223
+ return done(
224
+ `${node.isTypeOf ? 'typeof ' : ''}import(${quote(literal.text)})${qualifier}${typeArguments(ts, sf, node.typeArguments ?? [])}`,
225
+ );
226
+ }
227
+ if (ts.isTupleTypeNode(node)) {
228
+ return done(
229
+ `[${node.elements.map((/** @type {any} */ e) => tupleElement(ts, sf, e)).join(',')}]`,
230
+ );
231
+ }
232
+ if (ts.isTypeLiteralNode(node)) return done(typeLiteral(ts, sf, node));
233
+ if (ts.isMappedTypeNode(node)) {
234
+ const parameter = node.typeParameter;
235
+ const as = node.nameType ? ` as ${slot(node.nameType, UNION)}` : '';
236
+ const question = node.questionToken
237
+ ? node.questionToken.kind === K.MinusToken
238
+ ? '-?'
239
+ : '?'
240
+ : '';
241
+ const value = node.type ? `:${slot(node.type, UNION)}` : '';
242
+ return done(
243
+ `{[${parameter.name.text} in ${slot(parameter.constraint, UNION)}${as}]${question}${value}}`,
244
+ );
245
+ }
246
+ if (ts.isLiteralTypeNode(node)) return literal(ts, node.literal) ?? kept();
247
+ if (ts.isTemplateLiteralTypeNode(node)) {
248
+ /** @type {(text: string) => string} */
249
+ const raw = text => text.replace(/\\/g, '\\\\').replace(/`|\$\{/g, '\\$&');
250
+ return done(
251
+ '`' +
252
+ raw(node.head.text) +
253
+ node.templateSpans
254
+ .map(
255
+ (/** @type {any} */ span) =>
256
+ `\${${slot(span.type, UNION)}}${raw(span.literal.text)}`,
257
+ )
258
+ .join('') +
259
+ '`',
260
+ );
261
+ }
262
+ if (node.kind === K.ThisType) return done('this');
263
+ if (
264
+ (node.kind >= K.FirstKeyword && node.kind <= K.LastKeyword) ||
265
+ node.kind === K.NullKeyword
266
+ ) {
267
+ const text = node.getText(sf);
268
+ return /^[a-z]+$/.test(text) ? done(text) : kept();
269
+ }
270
+ return kept();
271
+ }
272
+
273
+ /**
274
+ * A parenthesized type's inner type, at any depth.
275
+ * @param {any} ts
276
+ * @param {any} node
277
+ */
278
+ function unwrap(ts, node) {
279
+ let inner = node;
280
+ while (ts.isParenthesizedTypeNode(inner)) inner = inner.type;
281
+ return inner;
282
+ }
283
+
284
+ /**
285
+ * A union member as it prints among others: a function or conditional type
286
+ * in parentheses, so joining members with `|` cannot read as one function.
287
+ * @param {Printed} part
288
+ */
289
+ function display(part) {
290
+ return part.level < INTERSECTION ? `(${part.text})` : part.text;
291
+ }
292
+
293
+ /**
294
+ * A union's members, canonical, merged and sorted.
295
+ * @param {any} ts
296
+ * @param {any} sf
297
+ * @param {any} node a union, or any type (one member)
298
+ * @param {boolean} optional drop `undefined`
299
+ * @returns {Printed[]}
300
+ */
301
+ function unionParts(ts, sf, node, optional) {
302
+ /** @type {Printed[]} */
303
+ const members = [];
304
+ const visit = (/** @type {any} */ n) => {
305
+ const inner = unwrap(ts, n);
306
+ if (ts.isUnionTypeNode(inner)) {
307
+ inner.types.forEach(visit);
308
+ return;
309
+ }
310
+ // `Readonly<A | B>` and `readonly (A | B)` are `A | B`.
311
+ if (
312
+ ts.isTypeReferenceNode(inner) &&
313
+ inner.typeArguments?.length === 1 &&
314
+ entityName(ts, inner.typeName, true) === 'Readonly'
315
+ ) {
316
+ visit(inner.typeArguments[0]);
317
+ return;
318
+ }
319
+ if (
320
+ ts.isTypeOperatorNode(inner) &&
321
+ inner.operator === ts.SyntaxKind.ReadonlyKeyword
322
+ ) {
323
+ visit(inner.type);
324
+ return;
325
+ }
326
+ members.push(print(ts, sf, inner));
327
+ };
328
+ visit(node);
329
+ const texts = new Set(members.map(display));
330
+ /** @type {Set<string>} */
331
+ const absorbed = new Set(
332
+ ['string', 'number', 'boolean'].filter(t => texts.has(t)),
333
+ );
334
+ if (texts.has('true') && texts.has('false')) absorbed.add('boolean');
335
+ /** @type {Map<string, Printed>} */
336
+ const out = new Map();
337
+ for (const m of members) {
338
+ if (m.literal && m.literal !== 'other' && absorbed.has(m.literal)) continue;
339
+ out.set(display(m), m);
340
+ }
341
+ if (absorbed.has('boolean')) {
342
+ out.set('boolean', {text: 'boolean', level: PRIMARY});
343
+ }
344
+ if (out.size > 1) out.delete('never');
345
+ if (optional) out.delete('undefined');
346
+ return [...out.keys()].sort().map(k => /** @type {Printed} */ (out.get(k)));
347
+ }
348
+
349
+ /**
350
+ * A literal type's text: strings single-quoted, numbers by value.
351
+ * @param {any} ts
352
+ * @param {any} literal
353
+ * @returns {Printed | null}
354
+ */
355
+ function literal(ts, literal) {
356
+ const K = ts.SyntaxKind;
357
+ if (
358
+ ts.isStringLiteral(literal) ||
359
+ literal.kind === K.NoSubstitutionTemplateLiteral
360
+ ) {
361
+ return {text: quote(literal.text), level: PRIMARY, literal: 'string'};
362
+ }
363
+ if (ts.isNumericLiteral(literal)) {
364
+ const value = Number(literal.text);
365
+ return Number.isFinite(value)
366
+ ? {text: String(value), level: PRIMARY, literal: 'number'}
367
+ : null;
368
+ }
369
+ if (
370
+ ts.isPrefixUnaryExpression(literal) &&
371
+ literal.operator === K.MinusToken &&
372
+ ts.isNumericLiteral(literal.operand)
373
+ ) {
374
+ const value = -Number(literal.operand.text);
375
+ return Number.isFinite(value)
376
+ ? {text: String(value), level: PRIMARY, literal: 'number'}
377
+ : null;
378
+ }
379
+ if (literal.kind === K.TrueKeyword || literal.kind === K.FalseKeyword) {
380
+ return {
381
+ text: literal.kind === K.TrueKeyword ? 'true' : 'false',
382
+ level: PRIMARY,
383
+ literal: 'boolean',
384
+ };
385
+ }
386
+ if (literal.kind === K.NullKeyword) return {text: 'null', level: PRIMARY};
387
+ return null;
388
+ }
389
+
390
+ /**
391
+ * An entity name, `React.` dropped where asked.
392
+ * @param {any} ts
393
+ * @param {any} name
394
+ * @param {boolean} dropReact
395
+ * @returns {string}
396
+ */
397
+ function entityName(ts, name, dropReact) {
398
+ if (ts.isIdentifier(name)) return name.text;
399
+ if (ts.isQualifiedName(name)) {
400
+ const left = entityName(ts, name.left, false);
401
+ return dropReact && left === 'React'
402
+ ? name.right.text
403
+ : `${dropReact ? entityName(ts, name.left, true) : left}.${name.right.text}`;
404
+ }
405
+ return compact(name.getText());
406
+ }
407
+
408
+ /**
409
+ * @param {any} ts
410
+ * @param {any} sf
411
+ * @param {readonly any[]} args
412
+ */
413
+ function typeArguments(ts, sf, args) {
414
+ return args.length === 0
415
+ ? ''
416
+ : `<${args.map(a => at(ts, sf, a, UNION)).join(',')}>`;
417
+ }
418
+
419
+ /**
420
+ * Print a child, in parentheses when it binds more loosely than the slot.
421
+ * @param {any} ts
422
+ * @param {any} sf
423
+ * @param {any} node
424
+ * @param {number} level
425
+ */
426
+ function at(ts, sf, node, level) {
427
+ const printed = print(ts, sf, node);
428
+ return printed.level < level ? `(${printed.text})` : printed.text;
429
+ }
430
+
431
+ /**
432
+ * @param {any} ts
433
+ * @param {any} sf
434
+ * @param {any} node a function-like node
435
+ */
436
+ function typeParameters(ts, sf, node) {
437
+ const list = node.typeParameters ?? [];
438
+ if (list.length === 0) return '';
439
+ return `<${list
440
+ .map(
441
+ (/** @type {any} */ p) =>
442
+ p.name.text +
443
+ (p.constraint ? ` extends ${at(ts, sf, p.constraint, UNION)}` : '') +
444
+ (p.default ? `=${at(ts, sf, p.default, UNION)}` : ''),
445
+ )
446
+ .join(',')}>`;
447
+ }
448
+
449
+ /**
450
+ * A parameter list without names: a `this` parameter keeps its name.
451
+ * @param {any} ts
452
+ * @param {any} sf
453
+ * @param {readonly any[]} list
454
+ */
455
+ function parameters(ts, sf, list) {
456
+ return list
457
+ .map(p => {
458
+ const type = p.type ? at(ts, sf, p.type, UNION) : 'any';
459
+ const self =
460
+ ts.isIdentifier(p.name) && p.name.text === 'this' ? 'this:' : '';
461
+ return `${p.dotDotDotToken ? '...' : ''}${self}${type}${p.questionToken ? '?' : ''}`;
462
+ })
463
+ .join(',');
464
+ }
465
+
466
+ /**
467
+ * A function's return type; a type predicate names its parameter by place.
468
+ * @param {any} ts
469
+ * @param {any} sf
470
+ * @param {any} node a function-like node
471
+ */
472
+ function returnType(ts, sf, node) {
473
+ const type = node.type;
474
+ if (!type) return 'any';
475
+ if (!ts.isTypePredicateNode(type)) return print(ts, sf, type).text;
476
+ const name = type.parameterName;
477
+ const place = ts.isIdentifier(name)
478
+ ? node.parameters.findIndex(
479
+ (/** @type {any} */ p) =>
480
+ ts.isIdentifier(p.name) && p.name.text === name.text,
481
+ )
482
+ : -1;
483
+ const subject = ts.isIdentifier(name)
484
+ ? place === -1
485
+ ? name.text
486
+ : `#${place}`
487
+ : 'this';
488
+ const asserts = type.assertsModifier ? 'asserts ' : '';
489
+ return `${asserts}${subject}${type.type ? ` is ${at(ts, sf, type.type, UNION)}` : ''}`;
490
+ }
491
+
492
+ /**
493
+ * A tuple element without its name.
494
+ * @param {any} ts
495
+ * @param {any} sf
496
+ * @param {any} element
497
+ */
498
+ function tupleElement(ts, sf, element) {
499
+ if (ts.isNamedTupleMember(element)) {
500
+ return `${element.dotDotDotToken ? '...' : ''}${at(ts, sf, element.type, element.dotDotDotToken || element.questionToken ? POSTFIX : UNION)}${element.questionToken ? '?' : ''}`;
501
+ }
502
+ if (ts.isOptionalTypeNode(element))
503
+ return `${at(ts, sf, element.type, POSTFIX)}?`;
504
+ if (ts.isRestTypeNode(element))
505
+ return `...${at(ts, sf, element.type, POSTFIX)}`;
506
+ return at(ts, sf, element, UNION);
507
+ }
508
+
509
+ /**
510
+ * An object type: properties sorted, everything else in written order.
511
+ * @param {any} ts
512
+ * @param {any} sf
513
+ * @param {any} node
514
+ */
515
+ function typeLiteral(ts, sf, node) {
516
+ /** @type {string[]} */
517
+ const properties = [];
518
+ /** @type {string[]} */
519
+ const others = [];
520
+ for (const member of node.members) {
521
+ const optional = member.questionToken ? '?' : '';
522
+ if (ts.isPropertySignature(member)) {
523
+ const type = member.type ? at(ts, sf, member.type, UNION) : 'any';
524
+ properties.push(`${propertyName(ts, member.name)}${optional}:${type}`);
525
+ } else if (ts.isMethodSignature(member)) {
526
+ others.push(
527
+ `${propertyName(ts, member.name)}${optional}${typeParameters(ts, sf, member)}(${parameters(ts, sf, member.parameters)}):${returnType(ts, sf, member)}`,
528
+ );
529
+ } else if (ts.isCallSignatureDeclaration(member)) {
530
+ others.push(
531
+ `${typeParameters(ts, sf, member)}(${parameters(ts, sf, member.parameters)}):${returnType(ts, sf, member)}`,
532
+ );
533
+ } else if (ts.isConstructSignatureDeclaration(member)) {
534
+ // `new ` with a space: no method name prints that way, so a method
535
+ // named `'new'` reads apart.
536
+ others.push(
537
+ `new ${typeParameters(ts, sf, member)}(${parameters(ts, sf, member.parameters)}):${returnType(ts, sf, member)}`,
538
+ );
539
+ } else if (ts.isIndexSignatureDeclaration(member)) {
540
+ const key = member.parameters[0]?.type
541
+ ? at(ts, sf, member.parameters[0].type, UNION)
542
+ : 'any';
543
+ // An index signature's key reads apart from a computed property name.
544
+ others.push(
545
+ `[_:${key}]:${member.type ? at(ts, sf, member.type, UNION) : 'any'}`,
546
+ );
547
+ } else {
548
+ // An accessor, as written, without the `;` or `,` that ends it.
549
+ others.push(compact(member.getText(sf)).replace(/[;,]$/, ''));
550
+ }
551
+ }
552
+ return `{${[...properties.sort(), ...others].join(';')}}`;
553
+ }
554
+
555
+ /**
556
+ * A property name: a quoted name that is an identifier reads as one.
557
+ * @param {any} ts
558
+ * @param {any} name
559
+ */
560
+ function propertyName(ts, name) {
561
+ if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) return name.text;
562
+ if (ts.isStringLiteral(name)) {
563
+ return /^[A-Za-z_$][\w$]*$/.test(name.text) ? name.text : quote(name.text);
564
+ }
565
+ if (ts.isNumericLiteral(name)) return String(Number(name.text));
566
+ return compact(name.getText());
567
+ }
568
+
569
+ /**
570
+ * Canonical type text: the members of the type's top-level union, sorted.
571
+ * The result is cached and frozen, so no caller can change it for the next.
572
+ * @param {string} text
573
+ * @param {{ts: any, optional?: boolean}} options `ts` is the project's own
574
+ * `typescript`; with `optional`, `undefined` is dropped, as an optional
575
+ * prop takes it anyway
576
+ * @returns {readonly string[]}
577
+ */
578
+ export function normalizeTypeText(text, options) {
579
+ const {ts, optional = false} = options;
580
+ let cache = CACHE.get(ts);
581
+ if (!cache) {
582
+ cache = new Map();
583
+ CACHE.set(ts, cache);
584
+ }
585
+ const key = `${optional ? 1 : 0}${text}`;
586
+ const hit = cache.get(key);
587
+ if (hit) return hit;
588
+ const source = String(text);
589
+ const sf = ts.createSourceFile(
590
+ 'type-text.ts',
591
+ `type T = ${source}\n;`,
592
+ ts.ScriptTarget.Latest,
593
+ true,
594
+ ts.ScriptKind.TS,
595
+ );
596
+ const [statement] = sf.statements;
597
+ // `Foo<>`, `Foo<A,>`, `typeof Foo<>` and JSDoc's forms (`Array.<T>`,
598
+ // `?string`, `*`) parse, but only the checker rejects them, as it does a
599
+ // mapped type with members, a parameter with a modifier or an initializer,
600
+ // a member with a modifier other than `readonly`, a property with an
601
+ // initializer, an `in` or `out` type parameter, and an index signature
602
+ // without one plain, typed parameter and a type. A comment before the `<`
603
+ // is not a `.`.
604
+ /** @type {(node: any) => boolean} */
605
+ const invalid = node => {
606
+ const args = node.typeArguments;
607
+ const name = node.typeName ?? node.exprName ?? node.qualifier;
608
+ const between =
609
+ args && name
610
+ ? sf.text
611
+ .slice(name.end, args.pos)
612
+ .replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, '')
613
+ : '';
614
+ return (
615
+ (node.kind >= ts.SyntaxKind.FirstJSDocNode &&
616
+ node.kind <= ts.SyntaxKind.LastJSDocNode) ||
617
+ (!!args &&
618
+ (args.length === 0 ||
619
+ args.hasTrailingComma ||
620
+ between.includes('.'))) ||
621
+ (ts.isPropertySignature(node) && !!node.initializer) ||
622
+ (ts.isTypeParameterDeclaration(node) &&
623
+ (node.modifiers ?? []).some(
624
+ (/** @type {any} */ m) =>
625
+ m.kind === ts.SyntaxKind.InKeyword ||
626
+ m.kind === ts.SyntaxKind.OutKeyword,
627
+ )) ||
628
+ (ts.isIndexSignatureDeclaration(node) &&
629
+ (node.parameters.length !== 1 ||
630
+ !node.type ||
631
+ node.parameters.some(
632
+ (/** @type {any} */ p) =>
633
+ p.questionToken || p.dotDotDotToken || !p.type,
634
+ ))) ||
635
+ (ts.isMappedTypeNode(node) && !!node.members?.length) ||
636
+ (ts.isParameter(node) &&
637
+ !!(node.initializer || node.modifiers?.length)) ||
638
+ (ts.isTypeElement(node) &&
639
+ (node.modifiers ?? []).some(
640
+ (/** @type {any} */ m) => m.kind !== ts.SyntaxKind.ReadonlyKeyword,
641
+ )) ||
642
+ !!ts.forEachChild(
643
+ node,
644
+ (/** @type {any} */ child) => invalid(child) || undefined,
645
+ )
646
+ );
647
+ };
648
+ const result = Object.freeze(
649
+ sf.statements.length === 1 &&
650
+ ts.isTypeAliasDeclaration(statement) &&
651
+ (sf.parseDiagnostics ?? []).length === 0 &&
652
+ !invalid(statement.type)
653
+ ? unionParts(ts, sf, statement.type, optional).map(display)
654
+ : [`${UNPARSED}${compact(source)}`],
655
+ );
656
+ cache.set(key, result);
657
+ return result;
658
+ }