@astryxdesign/cli 0.7.2-canary.db3a2e7 → 0.7.2-canary.e26723d
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/README.md +7 -7
- package/api/build/help/help.mjs +1 -1
- package/api/component/_adapter.d.mts +41 -1
- package/api/component/_adapter.mjs +93 -7
- package/api/component/component.mjs +22 -5
- package/api/component/component.test.mjs +420 -167
- package/api/component/integration-export-name.test.mjs +644 -0
- package/api/component/list/list.mjs +42 -4
- package/api/docs/_adapter.mjs +207 -1
- package/api/docs/integrationDocs.test.mjs +124 -0
- package/api/doctor/theme-checks.mjs +1 -1
- package/api/gap-report/gap-report.mjs +67 -4
- package/api/init/run/run.mjs +2 -2
- package/api/integration/_adapter.d.mts +12 -0
- package/api/integration/_adapter.mjs +54 -0
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +7 -3
- package/api/integration/authoring-checks.mjs +11 -2
- package/api/integration/authoring-checks.test.mjs +39 -0
- package/api/integration/authoring-checks.type.d.mts +9 -0
- package/api/integration/authoring-checks.type.mjs +14 -1
- package/api/integration/integrationComponentConflicts.doc.mjs +21 -5
- package/api/integration/pack-check.mjs +61 -107
- package/api/integration/pack-check.test.mjs +359 -0
- package/api/integration/validate-integration.test.mjs +11 -0
- package/api/search/search.d.mts +9 -0
- package/api/search/search.mjs +108 -16
- package/api/search/search.test.mjs +36 -0
- package/api/swizzle/copy/copy.mjs +28 -14
- package/api/template/template.test.mjs +120 -3
- package/api/theme/build/build.family.test.mjs +16 -0
- package/api/theme/build/build.mjs +81 -22
- package/api/theme/themeBuild.doc.mjs +3 -3
- package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
- package/assets/codemods/transforms/next/index.mjs +11 -1
- package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
- package/assets/docs/README.md +10 -0
- package/assets/docs/author-a-theme.doc.mjs +1 -1
- package/assets/docs/getting-started.doc.mjs +3 -3
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/docs/tree/add-a-component.doc.mjs +10 -0
- package/assets/docs/tree/check-the-props.doc.mjs +145 -0
- package/assets/docs/tree/checks.doc.mjs +4 -4
- package/assets/docs/tree/component-family.doc.mjs +5 -1
- package/assets/docs/tree/component-imports.doc.mjs +8 -0
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/links.doc.mjs +2 -2
- package/assets/docs/tree/single-component.doc.mjs +3 -2
- package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
- package/assets/docs/tree/type-scale.doc.mjs +3 -3
- package/assets/docs/tree/versioning.doc.mjs +1 -0
- package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
- package/assets/docs/use-a-theme.doc.mjs +2 -2
- package/assets/docs/working-with-ai.doc.mjs +43 -4
- package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
- package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
- package/assets/templates/pages/detail-page/page.tsx +3 -3
- package/assets/templates/pages/table-inbox/page.tsx +10 -8
- package/assets/theme.template.ts +1 -1
- package/authoring/doctypes/base/type.ts +6 -0
- package/authoring/doctypes/component/component.doc.mjs +4 -4
- package/authoring/doctypes/component/type.ts +6 -3
- package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
- package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
- package/authoring/doctypes/template/parse.d.mts +6 -2
- package/authoring/doctypes/template/parse.mjs +3 -1
- package/authoring/doctypes/template/type.ts +22 -0
- package/authoring/index.d.ts +1 -0
- package/authoring/integration/integration.doc.mjs +8 -1
- package/authoring/integration/parse.test.mjs +10 -0
- package/authoring/integration/schema.d.mts +2 -0
- package/authoring/integration/schema.mjs +1 -0
- package/authoring/integration/type.ts +4 -0
- package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
- package/clients/cli/commands/build-theme.mjs +1 -1
- package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
- package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
- package/clients/cli/commands/doctor-integration-components.doc.mjs +9 -3
- package/clients/cli/commands/doctor-integration.test.mjs +392 -0
- package/clients/cli/commands/doctor.mjs +74 -0
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/lib/component-format.mjs +5 -1
- package/foundation/config/project.mjs +82 -0
- package/foundation/discovery/authoring-self-docs.mjs +1 -0
- package/foundation/discovery/component-discovery.d.mts +190 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +864 -56
- package/foundation/discovery/component-discovery.test.mjs +96 -0
- package/foundation/discovery/component-replacement.mjs +18 -0
- package/foundation/discovery/component-replacement.test.mjs +40 -0
- package/foundation/discovery/doc-types.d.mts +157 -0
- package/foundation/discovery/doc-types.mjs +1224 -0
- package/foundation/discovery/doc-types.test.mjs +652 -0
- package/foundation/discovery/docs-discovery.d.mts +81 -0
- package/foundation/discovery/docs-discovery.mjs +57 -0
- package/foundation/discovery/module-exports.d.mts +27 -0
- package/foundation/discovery/module-exports.mjs +356 -31
- package/foundation/discovery/module-exports.test.mjs +172 -0
- package/foundation/discovery/package-tsconfig.d.mts +14 -0
- package/foundation/discovery/package-tsconfig.mjs +228 -0
- package/foundation/discovery/package-tsconfig.test.mjs +210 -0
- package/foundation/discovery/project-typescript.d.mts +23 -0
- package/foundation/discovery/project-typescript.mjs +61 -0
- package/foundation/discovery/prop-drift.d.mts +220 -0
- package/foundation/discovery/prop-drift.mjs +1765 -0
- package/foundation/discovery/prop-drift.test.mjs +1197 -0
- package/foundation/discovery/template-adapter.mjs +74 -5
- package/foundation/discovery/type-match.d.mts +53 -0
- package/foundation/discovery/type-match.mjs +660 -0
- package/foundation/discovery/type-match.test.mjs +169 -0
- package/foundation/discovery/type-text.d.mts +68 -0
- package/foundation/discovery/type-text.mjs +658 -0
- package/foundation/discovery/type-text.test.mjs +196 -0
- package/foundation/discovery/unresolved-references.d.mts +62 -0
- package/foundation/discovery/unresolved-references.mjs +790 -0
- package/foundation/discovery/unresolved-references.test.mjs +567 -0
- package/foundation/doc-compiler/doc-loads.test.mjs +9 -1
- package/foundation/doc-compiler/inputs.mjs +23 -0
- package/foundation/doc-compiler/inputs.test.mjs +0 -1
- package/foundation/doc-compiler/links.d.mts +9 -5
- package/foundation/doc-compiler/links.mjs +4 -2
- package/foundation/integrations/contribution-inventory.mjs +6 -1
- package/foundation/response/response-types.doc.mjs +1 -1
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +10 -10
|
@@ -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
|
+
}
|