@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.
- package/README.md +9 -8
- package/api/build/help/help.mjs +1 -1
- package/api/component/_adapter.d.mts +14 -0
- package/api/component/_adapter.mjs +48 -0
- package/api/component/component.mjs +10 -2
- package/api/component/integration-export-name.test.mjs +233 -8
- package/api/docs/_adapter.d.mts +11 -0
- package/api/docs/_adapter.mjs +244 -1
- package/api/docs/integrationDocs.test.mjs +124 -0
- package/api/doctor/theme-checks.mjs +1 -1
- package/api/gap-report/gap-report.mjs +2 -1
- package/api/index.d.mts +2 -0
- package/api/index.mjs +2 -0
- 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 +8 -4
- package/api/integration/add-contribution.test.mjs +4 -2
- 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/check/check.d.mts +96 -0
- package/api/integration/check/check.mjs +357 -0
- package/api/integration/check/check.test.mjs +489 -0
- package/api/integration/integration-check.type.d.mts +118 -0
- package/api/integration/integration-check.type.mjs +84 -0
- package/api/integration/integrationCheck.doc.d.mts +10 -0
- package/api/integration/integrationCheck.doc.mjs +73 -0
- package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
- package/api/integration/pack-check.mjs +48 -3
- package/api/integration/pack-check.test.mjs +247 -0
- package/api/integration/validate-integration.mjs +8 -0
- package/api/integration/validate-integration.test.mjs +11 -0
- package/api/integration/validate-integration.type.d.mts +8 -0
- package/api/integration/validate-integration.type.mjs +12 -1
- package/api/integration/validateIntegration.doc.mjs +17 -3
- package/api/search/search.integration-check.test.mjs +24 -0
- package/api/search/search.mjs +20 -8
- package/api/theme/build/build.family.test.mjs +54 -6
- package/api/theme/build/build.mjs +114 -17
- package/api/theme/themeBuild.doc.mjs +4 -4
- 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/block-template.doc.mjs +2 -2
- package/assets/docs/tree/check-the-props.doc.mjs +196 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +44 -3
- package/assets/docs/tree/checks.doc.mjs +21 -13
- package/assets/docs/tree/component-family.doc.mjs +5 -1
- package/assets/docs/tree/component-imports.doc.mjs +1 -1
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/links.doc.mjs +2 -2
- package/assets/docs/tree/page-template.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
- 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/theme.template.ts +1 -1
- package/authoring/doctypes/component/component.doc.mjs +1 -1
- 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/template.doc.mjs +1 -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 +155 -21
- package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
- package/clients/cli/commands/doctor-integration.test.mjs +837 -0
- package/clients/cli/commands/doctor.mjs +190 -0
- package/clients/cli/commands/integration-check.doc.mjs +85 -0
- package/clients/cli/commands/integration-check.test.mjs +267 -0
- package/clients/cli/commands/integration.doc.mjs +6 -2
- package/clients/cli/commands/integration.mjs +107 -1
- package/clients/cli/commands/text-json-parity.test.mjs +5 -0
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/index.mjs +1 -0
- package/foundation/config/project.mjs +82 -0
- package/foundation/discovery/authoring-self-docs.mjs +1 -0
- package/foundation/discovery/component-discovery.d.mts +108 -17
- package/foundation/discovery/component-discovery.export-names.test.mjs +469 -2
- package/foundation/discovery/component-discovery.mjs +380 -47
- package/foundation/discovery/component-replacement.test.mjs +20 -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/integration-doc-files.d.mts +22 -0
- package/foundation/discovery/integration-doc-files.mjs +231 -0
- package/foundation/discovery/integration-doc-files.test.mjs +246 -0
- package/foundation/discovery/module-exports.d.mts +6 -0
- package/foundation/discovery/module-exports.mjs +15 -2
- 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 +278 -0
- package/foundation/discovery/prop-drift.mjs +2263 -0
- package/foundation/discovery/prop-drift.test.mjs +2219 -0
- 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 +6 -0
- 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 +7 -2
- package/package.json +9 -9
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Find the references in a package's TypeScript project that the
|
|
5
|
+
* compiler cannot resolve, so a check that reads the package's types knows
|
|
6
|
+
* when part of them is a guess: a dependency that is not installed can make
|
|
7
|
+
* any type built from it the compiler's error type, whatever the code looks
|
|
8
|
+
* like.
|
|
9
|
+
*
|
|
10
|
+
* - From given files: their imports, re-exports, `/// <reference>`
|
|
11
|
+
* directives and side-effect imports, and the files those reach in turn;
|
|
12
|
+
* and in the package's own files, each type the compiler can only give its
|
|
13
|
+
* error type.
|
|
14
|
+
* - From the program's global roots: the package's own declaration files
|
|
15
|
+
* that act globally and the type packages the program loads by name
|
|
16
|
+
* (`compilerOptions.types`).
|
|
17
|
+
*
|
|
18
|
+
* @input The project's own `typescript`, a program and its checker.
|
|
19
|
+
* @output The first reference that does not resolve, or null.
|
|
20
|
+
* @position Foundation discovery, used by the documented-props comparison.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What the walk learns about one program for one `mine`: each source file's
|
|
27
|
+
* references (the first that doesn't resolve, the first type it can't give,
|
|
28
|
+
* the files they reach), what each declaration's type is inferred from, and
|
|
29
|
+
* the declarations whose whole reach reads clean. Each is a fact about the
|
|
30
|
+
* program, so an answer never depends on what an earlier call read; the
|
|
31
|
+
* record goes when the program does.
|
|
32
|
+
* @type {WeakMap<any, WeakMap<Function, {files: Map<any, any>, local: Map<any, any>, clean: Set<any>, untypedNames: Map<any, Set<string>>, errors: Map<any, any[]>}>>}
|
|
33
|
+
*/
|
|
34
|
+
const PROGRAMS = new WeakMap();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} UnresolvedReference
|
|
38
|
+
* @property {string} name the reference as written, quoted
|
|
39
|
+
* @property {string} file the file that writes it
|
|
40
|
+
* @property {'imported' | 'used' | 'named'} how a file imports it, a file
|
|
41
|
+
* uses it as a type name, or the config names it
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The JSDoc tags whose types the compiler reads in a JavaScript file, in the
|
|
46
|
+
* TypeScript versions that have them. It never checks the others (`@throws`,
|
|
47
|
+
* `@exception`, `@see`, `@example` …), so a name declared nowhere in one
|
|
48
|
+
* decides no type.
|
|
49
|
+
*/
|
|
50
|
+
const TYPED_JSDOC_TAGS = [
|
|
51
|
+
'JSDocTypeTag',
|
|
52
|
+
'JSDocParameterTag',
|
|
53
|
+
'JSDocReturnTag',
|
|
54
|
+
'JSDocTypedefTag',
|
|
55
|
+
'JSDocCallbackTag',
|
|
56
|
+
'JSDocTemplateTag',
|
|
57
|
+
'JSDocPropertyTag',
|
|
58
|
+
'JSDocThisTag',
|
|
59
|
+
'JSDocEnumTag',
|
|
60
|
+
'JSDocAugmentsTag',
|
|
61
|
+
'JSDocImplementsTag',
|
|
62
|
+
'JSDocSatisfiesTag',
|
|
63
|
+
'JSDocOverloadTag',
|
|
64
|
+
'JSDocImportTag',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The first reference that does not resolve from the given files, the files
|
|
69
|
+
* they import, and so on, as far as `follow` allows. A reference fails when
|
|
70
|
+
* the program finds no module, no type package or no file for it, when a
|
|
71
|
+
* module does not export a name imported from it, or, in the files `mine`
|
|
72
|
+
* accepts, when a type name resolves to no declaration (TS2304). A module
|
|
73
|
+
* found without types (a JavaScript file) resolves: its names are `any`. A
|
|
74
|
+
* side-effect import counts where the compiler checks one
|
|
75
|
+
* (`noUncheckedSideEffectImports`, on by default from TypeScript 6).
|
|
76
|
+
* @param {any} ts the project's own `typescript`
|
|
77
|
+
* @param {any} program
|
|
78
|
+
* @param {any} checker
|
|
79
|
+
* @param {any[]} roots source files
|
|
80
|
+
* @param {(file: string) => boolean} follow which files it reads past them
|
|
81
|
+
* @param {(file: string) => boolean} mine whose type names it checks
|
|
82
|
+
* @returns {UnresolvedReference | null}
|
|
83
|
+
*/
|
|
84
|
+
export function unresolvedReference(ts, program, checker, roots, follow, mine) {
|
|
85
|
+
const K = ts.SyntaxKind;
|
|
86
|
+
const typedTags = new Set(
|
|
87
|
+
TYPED_JSDOC_TAGS.map(name => K[name]).filter(kind => kind !== undefined),
|
|
88
|
+
);
|
|
89
|
+
const STARTS = new Set([
|
|
90
|
+
K.TypeReference,
|
|
91
|
+
K.TypeQuery,
|
|
92
|
+
K.IndexedAccessType,
|
|
93
|
+
K.ImportType,
|
|
94
|
+
K.ExpressionWithTypeArguments,
|
|
95
|
+
]);
|
|
96
|
+
const options = program.getCompilerOptions();
|
|
97
|
+
const sideEffects =
|
|
98
|
+
options.noUncheckedSideEffectImports ??
|
|
99
|
+
Number.parseInt(ts.versionMajorMinor, 10) >= 6;
|
|
100
|
+
let byMine = PROGRAMS.get(program);
|
|
101
|
+
if (!byMine) {
|
|
102
|
+
byMine = new WeakMap();
|
|
103
|
+
PROGRAMS.set(program, byMine);
|
|
104
|
+
}
|
|
105
|
+
let state = byMine.get(mine);
|
|
106
|
+
if (!state) {
|
|
107
|
+
state = {
|
|
108
|
+
files: new Map(),
|
|
109
|
+
local: new Map(),
|
|
110
|
+
clean: new Set(),
|
|
111
|
+
untypedNames: new Map(),
|
|
112
|
+
errors: new Map(),
|
|
113
|
+
};
|
|
114
|
+
byMine.set(mine, state);
|
|
115
|
+
}
|
|
116
|
+
const record = state;
|
|
117
|
+
/**
|
|
118
|
+
* The names a file imports from modules found without types.
|
|
119
|
+
* @type {(file: any) => Set<string>}
|
|
120
|
+
*/
|
|
121
|
+
const untypedNamesOf = file => {
|
|
122
|
+
let names = record.untypedNames.get(file);
|
|
123
|
+
if (names) return names;
|
|
124
|
+
names = new Set();
|
|
125
|
+
for (const literal of file.imports ?? []) {
|
|
126
|
+
if (literal.pos < 0 || checker.getSymbolAtLocation(literal)) continue;
|
|
127
|
+
if (
|
|
128
|
+
ts.resolveModuleName(literal.text, file.fileName, options, ts.sys)
|
|
129
|
+
.resolvedModule
|
|
130
|
+
) {
|
|
131
|
+
for (const name of boundNames(ts, literal.parent)) names.add(name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
record.untypedNames.set(file, names);
|
|
135
|
+
return names;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Whether `tsc` reports an error inside a node. It leaves a line
|
|
139
|
+
* `@ts-expect-error` covers alone, and a `@ts-nocheck` file, and without
|
|
140
|
+
* `noImplicitAny` it gives some reads the error type with no diagnostic.
|
|
141
|
+
* @type {(file: any, node: any) => boolean}
|
|
142
|
+
*/
|
|
143
|
+
const errorReported = (file, node) => {
|
|
144
|
+
const errors =
|
|
145
|
+
record.errors.get(file) ??
|
|
146
|
+
program
|
|
147
|
+
.getSemanticDiagnostics(file)
|
|
148
|
+
.filter(
|
|
149
|
+
(/** @type {any} */ d) => d.category === ts.DiagnosticCategory.Error,
|
|
150
|
+
);
|
|
151
|
+
record.errors.set(file, errors);
|
|
152
|
+
const start = node.getStart(file);
|
|
153
|
+
return errors.some(
|
|
154
|
+
(/** @type {any} */ d) =>
|
|
155
|
+
d.start !== undefined && d.start >= start && d.start < node.end,
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Whether a function's type comes from overload signatures.
|
|
160
|
+
* @type {(fn: any) => boolean}
|
|
161
|
+
*/
|
|
162
|
+
const overloaded = fn => {
|
|
163
|
+
const symbol = fn.name && checker.getSymbolAtLocation(fn.name);
|
|
164
|
+
return (symbol?.declarations ?? []).some(
|
|
165
|
+
(/** @type {any} */ d) => d !== fn && ts.isFunctionLike(d) && !d.body,
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* The expressions a declaration's type is inferred from: an initializer
|
|
170
|
+
* with no type written, what a function returns when its return type
|
|
171
|
+
* isn't written, and a parameter's default. A class's names come from
|
|
172
|
+
* its written members and its base; a member's type is read where a type
|
|
173
|
+
* indexes it. A type written down, or overloads, decide it alone.
|
|
174
|
+
* @type {(declaration: any) => any[]}
|
|
175
|
+
*/
|
|
176
|
+
const typeSources = declaration => {
|
|
177
|
+
/** @type {any[]} */
|
|
178
|
+
const sources = [];
|
|
179
|
+
const fromFunction = (/** @type {any} */ fn) => {
|
|
180
|
+
if (overloaded(fn)) return;
|
|
181
|
+
for (const parameter of fn.parameters ?? []) {
|
|
182
|
+
if (!parameter.type && parameter.initializer) {
|
|
183
|
+
sources.push(parameter.initializer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (fn.type || !fn.body) return;
|
|
187
|
+
if (!ts.isBlock(fn.body)) {
|
|
188
|
+
sources.push(fn.body);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// A function or class nested in the body returns for itself.
|
|
192
|
+
const visit = (/** @type {any} */ n) => {
|
|
193
|
+
if (ts.isFunctionLike(n) || ts.isClassLike(n)) return;
|
|
194
|
+
if (ts.isReturnStatement(n) && n.expression) sources.push(n.expression);
|
|
195
|
+
ts.forEachChild(n, visit);
|
|
196
|
+
};
|
|
197
|
+
ts.forEachChild(fn.body, visit);
|
|
198
|
+
};
|
|
199
|
+
if (ts.isFunctionLike(declaration)) {
|
|
200
|
+
fromFunction(declaration);
|
|
201
|
+
} else if (ts.isClassLike(declaration)) {
|
|
202
|
+
for (const clause of declaration.heritageClauses ?? []) {
|
|
203
|
+
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
204
|
+
for (const base of clause.types) sources.push(base.expression);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else if (ts.isBindingElement(declaration)) {
|
|
208
|
+
if (declaration.initializer) sources.push(declaration.initializer);
|
|
209
|
+
let root = declaration.parent;
|
|
210
|
+
while (root && !ts.isVariableDeclaration(root) && !ts.isParameter(root)) {
|
|
211
|
+
root = root.parent;
|
|
212
|
+
}
|
|
213
|
+
if (root && !root.type && root.initializer) {
|
|
214
|
+
sources.push(root.initializer);
|
|
215
|
+
}
|
|
216
|
+
} else if (!declaration.type && declaration.initializer) {
|
|
217
|
+
sources.push(declaration.initializer);
|
|
218
|
+
}
|
|
219
|
+
return sources;
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* What one declaration's type is inferred from, read on its own: the
|
|
223
|
+
* first value read there that the compiler can only give its error type
|
|
224
|
+
* and `tsc` reports, and the declarations it names, whose types count
|
|
225
|
+
* too.
|
|
226
|
+
* @type {(declaration: any) => {found: {node: any, file: any} | null, names: any[]}}
|
|
227
|
+
*/
|
|
228
|
+
const localScan = declaration => {
|
|
229
|
+
const known = record.local.get(declaration);
|
|
230
|
+
if (known) return known;
|
|
231
|
+
const file = declaration.getSourceFile();
|
|
232
|
+
const untypedHere = untypedNamesOf(file);
|
|
233
|
+
/** @type {any[]} */
|
|
234
|
+
const names = [];
|
|
235
|
+
/** @type {{node: any, file: any} | null} */
|
|
236
|
+
let found = null;
|
|
237
|
+
/** @type {(node: any) => void} */
|
|
238
|
+
const scan = node => {
|
|
239
|
+
if (found) return;
|
|
240
|
+
// A JSX element is a `JSX.Element`, whatever it holds.
|
|
241
|
+
if (
|
|
242
|
+
ts.isJsxElement(node) ||
|
|
243
|
+
ts.isJsxSelfClosingElement(node) ||
|
|
244
|
+
ts.isJsxFragment(node)
|
|
245
|
+
) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// A cast that writes the type decides it, whatever the value.
|
|
249
|
+
if (
|
|
250
|
+
(ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) &&
|
|
251
|
+
!ts.isConstTypeReference(node.type)
|
|
252
|
+
) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (ts.isFunctionLike(node) || ts.isClassLike(node)) {
|
|
256
|
+
for (const source of typeSources(node)) scan(source);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
ts.forEachChild(node, scan);
|
|
260
|
+
if (found) return;
|
|
261
|
+
if (
|
|
262
|
+
readsValue(ts, node) &&
|
|
263
|
+
!untypedHere.has(rootName(ts, node)) &&
|
|
264
|
+
isErrorValue(checker, node) &&
|
|
265
|
+
errorReported(file, node)
|
|
266
|
+
) {
|
|
267
|
+
found = {node, file};
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// A name for another declaration: its type comes from there.
|
|
271
|
+
if (!ts.isIdentifier(node)) return;
|
|
272
|
+
let symbol =
|
|
273
|
+
ts.isShorthandPropertyAssignment(node.parent) &&
|
|
274
|
+
node.parent.name === node
|
|
275
|
+
? checker.getShorthandAssignmentValueSymbol(node.parent)
|
|
276
|
+
: checker.getSymbolAtLocation(node);
|
|
277
|
+
if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
|
|
278
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
279
|
+
}
|
|
280
|
+
for (const named of symbol?.declarations ?? []) {
|
|
281
|
+
if (!names.includes(named)) names.push(named);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
for (const source of typeSources(declaration)) {
|
|
285
|
+
scan(source);
|
|
286
|
+
if (found) break;
|
|
287
|
+
}
|
|
288
|
+
const result = {found, names};
|
|
289
|
+
record.local.set(declaration, result);
|
|
290
|
+
return result;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* The first such read in what the declarations' types are inferred from,
|
|
294
|
+
* in the package's own TypeScript files, following the declarations each
|
|
295
|
+
* names, depth first. A declaration whose whole reach read clean before
|
|
296
|
+
* is skipped, which changes no answer.
|
|
297
|
+
* @type {(declarations: readonly any[]) => {node: any, file: any} | null}
|
|
298
|
+
*/
|
|
299
|
+
const scanDeclarations = declarations => {
|
|
300
|
+
const visited = new Set();
|
|
301
|
+
/** @type {(list: readonly any[]) => {node: any, file: any} | null} */
|
|
302
|
+
const walk = list => {
|
|
303
|
+
for (const declaration of list) {
|
|
304
|
+
const file = declaration.getSourceFile();
|
|
305
|
+
if (
|
|
306
|
+
visited.has(declaration) ||
|
|
307
|
+
record.clean.has(declaration) ||
|
|
308
|
+
file.isDeclarationFile ||
|
|
309
|
+
!/\.[cm]?tsx?$/.test(file.fileName) ||
|
|
310
|
+
!mine(file.fileName)
|
|
311
|
+
) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
visited.add(declaration);
|
|
315
|
+
const {found, names} = localScan(declaration);
|
|
316
|
+
const reached = found ?? walk(names);
|
|
317
|
+
if (reached) return reached;
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
};
|
|
321
|
+
const found = walk(declarations);
|
|
322
|
+
if (!found)
|
|
323
|
+
for (const declaration of visited) record.clean.add(declaration);
|
|
324
|
+
return found;
|
|
325
|
+
};
|
|
326
|
+
/** @type {(sf: any) => {missing: UnresolvedReference | null, untyped: UnresolvedReference | null, next: any[]}} */
|
|
327
|
+
const read = sf => {
|
|
328
|
+
const cache = record.files;
|
|
329
|
+
if (cache.has(sf)) return cache.get(sf);
|
|
330
|
+
/** @type {UnresolvedReference | null} */
|
|
331
|
+
let missing = null;
|
|
332
|
+
/** @type {any[]} */
|
|
333
|
+
const next = [];
|
|
334
|
+
/** @type {(name: string, how?: UnresolvedReference['how']) => void} */
|
|
335
|
+
const fail = (name, how = 'imported') => {
|
|
336
|
+
missing ??= {name, file: sf.fileName, how};
|
|
337
|
+
};
|
|
338
|
+
// A module found without types gives its names no types: they are
|
|
339
|
+
// `any`, not missing.
|
|
340
|
+
/** @type {Set<string>} */
|
|
341
|
+
const untypedNames = new Set();
|
|
342
|
+
for (const literal of sf.imports ?? []) {
|
|
343
|
+
const parent = literal.parent;
|
|
344
|
+
// The JSX runtime import the compiler adds itself shapes no types.
|
|
345
|
+
if (!parent || missing || literal.pos < 0) continue;
|
|
346
|
+
if (
|
|
347
|
+
ts.isImportDeclaration(parent) &&
|
|
348
|
+
!parent.importClause &&
|
|
349
|
+
!sideEffects
|
|
350
|
+
) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const module = checker.getSymbolAtLocation(literal);
|
|
354
|
+
if (!module) {
|
|
355
|
+
const found = ts.resolveModuleName(
|
|
356
|
+
literal.text,
|
|
357
|
+
sf.fileName,
|
|
358
|
+
options,
|
|
359
|
+
ts.sys,
|
|
360
|
+
).resolvedModule;
|
|
361
|
+
if (!found) fail(`\`${literal.text}\``);
|
|
362
|
+
else for (const name of boundNames(ts, parent)) untypedNames.add(name);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
for (const d of module.declarations ?? []) next.push(d.getSourceFile());
|
|
366
|
+
// Each name an import or a re-export takes from the module. One it
|
|
367
|
+
// does not export resolves to the compiler's `unknown` symbol.
|
|
368
|
+
const clause = ts.isImportDeclaration(parent)
|
|
369
|
+
? parent.importClause
|
|
370
|
+
: null;
|
|
371
|
+
const bound = clause?.namedBindings;
|
|
372
|
+
const exported = ts.isExportDeclaration(parent)
|
|
373
|
+
? parent.exportClause
|
|
374
|
+
: null;
|
|
375
|
+
const names = [
|
|
376
|
+
...(clause?.name ? [clause.name] : []),
|
|
377
|
+
...(bound && ts.isNamedImports(bound) ? bound.elements : []),
|
|
378
|
+
...(exported && ts.isNamedExports(exported) ? exported.elements : []),
|
|
379
|
+
].map(/** @param {any} n */ n => n.name ?? n);
|
|
380
|
+
for (const name of names) {
|
|
381
|
+
const alias = checker.getSymbolAtLocation(name);
|
|
382
|
+
const resolved = alias && checker.getAliasedSymbol(alias);
|
|
383
|
+
if (
|
|
384
|
+
!resolved ||
|
|
385
|
+
(resolved.escapedName === 'unknown' && !resolved.declarations?.length)
|
|
386
|
+
) {
|
|
387
|
+
fail(`\`${name.text}\` from \`${literal.text}\``);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const ref of sf.typeReferenceDirectives ?? []) {
|
|
392
|
+
const found = ts.resolveTypeReferenceDirective(
|
|
393
|
+
ref.fileName,
|
|
394
|
+
sf.fileName,
|
|
395
|
+
options,
|
|
396
|
+
ts.sys,
|
|
397
|
+
).resolvedTypeReferenceDirective?.resolvedFileName;
|
|
398
|
+
if (!found) fail(`the types \`${ref.fileName}\``);
|
|
399
|
+
else if (program.getSourceFile(found)) {
|
|
400
|
+
next.push(program.getSourceFile(found));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const ref of sf.referencedFiles ?? []) {
|
|
404
|
+
const file = path.resolve(path.dirname(sf.fileName), ref.fileName);
|
|
405
|
+
const found = ['', '.ts', '.tsx', '.d.ts']
|
|
406
|
+
.map(ext => program.getSourceFile(file + ext))
|
|
407
|
+
.find(Boolean);
|
|
408
|
+
if (found) next.push(found);
|
|
409
|
+
else fail(`\`${ref.fileName}\``);
|
|
410
|
+
}
|
|
411
|
+
// A type the compiler can only give its error type: a name or value
|
|
412
|
+
// declared nowhere, a property that doesn't exist. `keyof` or a template
|
|
413
|
+
// literal key over one hides the error, so it is found where it starts.
|
|
414
|
+
// `as const` is a type reference named `const`. A JavaScript file's
|
|
415
|
+
// JSDoc types count too, in the tags the compiler reads types from;
|
|
416
|
+
// TypeScript reads no types from a `.ts` file's.
|
|
417
|
+
/** @type {UnresolvedReference | null} */
|
|
418
|
+
let untyped = null;
|
|
419
|
+
const js = /\.[cm]?jsx?$/.test(sf.fileName);
|
|
420
|
+
// A name, or an `import()` type, from a module found without types.
|
|
421
|
+
/** @type {(node: any) => boolean} */
|
|
422
|
+
const fromUntyped = node => {
|
|
423
|
+
if (ts.isImportTypeNode(node)) {
|
|
424
|
+
const literal = node.argument?.literal;
|
|
425
|
+
return (
|
|
426
|
+
!!literal &&
|
|
427
|
+
ts.isStringLiteral(literal) &&
|
|
428
|
+
!checker.getSymbolAtLocation(literal) &&
|
|
429
|
+
!!ts.resolveModuleName(literal.text, sf.fileName, options, ts.sys)
|
|
430
|
+
.resolvedModule
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
let name = ts.isTypeQueryNode(node)
|
|
434
|
+
? node.exprName
|
|
435
|
+
: ts.isTypeReferenceNode(node)
|
|
436
|
+
? node.typeName
|
|
437
|
+
: null;
|
|
438
|
+
while (name && ts.isQualifiedName(name)) name = name.left;
|
|
439
|
+
return !!name && ts.isIdentifier(name) && untypedNames.has(name.text);
|
|
440
|
+
};
|
|
441
|
+
// A value `typeof` reads, or a class a type names, in a TypeScript file
|
|
442
|
+
// of the package's own, whose type is inferred from a value the compiler
|
|
443
|
+
// can't type: a property that doesn't exist, spread into an object,
|
|
444
|
+
// turns it `any`, so `keyof typeof` it takes any name. Only an error
|
|
445
|
+
// `tsc` reports counts.
|
|
446
|
+
/** @type {(name: any, classes?: boolean) => boolean} */
|
|
447
|
+
const readsErrorValue = (name, classes = false) => {
|
|
448
|
+
let target = checker.getSymbolAtLocation(name);
|
|
449
|
+
if (target && target.flags & ts.SymbolFlags.Alias) {
|
|
450
|
+
target = checker.getAliasedSymbol(target);
|
|
451
|
+
}
|
|
452
|
+
const declarations = (target?.declarations ?? []).filter(
|
|
453
|
+
(/** @type {any} */ d) => !classes || ts.isClassLike(d),
|
|
454
|
+
);
|
|
455
|
+
const found = scanDeclarations(declarations);
|
|
456
|
+
if (found) {
|
|
457
|
+
untyped = {
|
|
458
|
+
name: `\`${oneLine(found.node.getText(found.file))}\``,
|
|
459
|
+
file: found.file.fileName,
|
|
460
|
+
how: 'used',
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
};
|
|
465
|
+
// The members a type indexes, such as `Store['slots']`: a member's
|
|
466
|
+
// type is inferred from its own initializer or body.
|
|
467
|
+
/** @type {(node: any) => boolean} */
|
|
468
|
+
const readsIndexedMembers = node => {
|
|
469
|
+
const object = checker.getTypeFromTypeNode(node.objectType);
|
|
470
|
+
const index = checker.getTypeFromTypeNode(node.indexType);
|
|
471
|
+
const keys = (index.isUnion() ? index.types : [index]).filter(
|
|
472
|
+
(/** @type {any} */ key) =>
|
|
473
|
+
key.isStringLiteral() || key.isNumberLiteral(),
|
|
474
|
+
);
|
|
475
|
+
const members = keys.map((/** @type {any} */ key) =>
|
|
476
|
+
checker.getPropertyOfType(object, String(key.value)),
|
|
477
|
+
);
|
|
478
|
+
const found = scanDeclarations(
|
|
479
|
+
members.flatMap(
|
|
480
|
+
(/** @type {any} */ member) => member?.declarations ?? [],
|
|
481
|
+
),
|
|
482
|
+
);
|
|
483
|
+
if (found) {
|
|
484
|
+
untyped = {
|
|
485
|
+
name: `\`${oneLine(found.node.getText(found.file))}\``,
|
|
486
|
+
file: found.file.fileName,
|
|
487
|
+
how: 'used',
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return false;
|
|
491
|
+
};
|
|
492
|
+
// A class's base, in its `extends` clause or a JavaScript file's
|
|
493
|
+
// `@augments` tag, that names nothing: the compiler can give the class no
|
|
494
|
+
// type, so it has no props to read. A base that resolves is read as a
|
|
495
|
+
// value below.
|
|
496
|
+
/** @type {(node: any) => boolean} */
|
|
497
|
+
const baseless = node =>
|
|
498
|
+
isValueWithTypeArguments(ts, node) &&
|
|
499
|
+
(ts.isHeritageClause(node.parent) ||
|
|
500
|
+
ts.isJSDocAugmentsTag(node.parent)) &&
|
|
501
|
+
ts.isEntityNameExpression(node.expression) &&
|
|
502
|
+
!checker.getSymbolAtLocation(node.expression);
|
|
503
|
+
// The innermost such type names it: its parts are read first.
|
|
504
|
+
/** @type {(node: any) => void} */
|
|
505
|
+
const visit = node => {
|
|
506
|
+
if (untyped) return;
|
|
507
|
+
for (const doc of js ? (node.jsDoc ?? []) : []) {
|
|
508
|
+
for (const tag of doc.tags ?? []) {
|
|
509
|
+
if (typedTags.has(tag.kind)) visit(tag);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
ts.forEachChild(node, visit);
|
|
513
|
+
if (!untyped && baseless(node)) {
|
|
514
|
+
untyped = {
|
|
515
|
+
name: `\`${node.expression.getText(sf)}\``,
|
|
516
|
+
file: sf.fileName,
|
|
517
|
+
how: 'used',
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (
|
|
521
|
+
!untyped &&
|
|
522
|
+
((STARTS.has(node.kind) &&
|
|
523
|
+
!(ts.isTypeReferenceNode(node) && ts.isConstTypeReference(node)) &&
|
|
524
|
+
!isValueWithTypeArguments(ts, node) &&
|
|
525
|
+
!fromUntyped(node) &&
|
|
526
|
+
isErrorType(checker, node)) ||
|
|
527
|
+
(ts.isTypeQueryNode(node) && readsErrorValue(node.exprName)) ||
|
|
528
|
+
(ts.isIndexedAccessTypeNode(node) && readsIndexedMembers(node)) ||
|
|
529
|
+
// A class a type names: its instance type is inferred too.
|
|
530
|
+
(ts.isTypeReferenceNode(node) &&
|
|
531
|
+
readsErrorValue(node.typeName, true)) ||
|
|
532
|
+
(ts.isExpressionWithTypeArguments(node) &&
|
|
533
|
+
readsErrorValue(node.expression, true)))
|
|
534
|
+
) {
|
|
535
|
+
untyped = {
|
|
536
|
+
name: `\`${node.getText(sf)}\``,
|
|
537
|
+
file: sf.fileName,
|
|
538
|
+
how: 'used',
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
if (!missing && mine(sf.fileName)) visit(sf);
|
|
543
|
+
const entry = {missing, untyped, next};
|
|
544
|
+
cache.set(sf, entry);
|
|
545
|
+
return entry;
|
|
546
|
+
};
|
|
547
|
+
// A reference that doesn't resolve, anywhere the walk goes, names what to
|
|
548
|
+
// install; a type the compiler can't give is often only where it shows.
|
|
549
|
+
/** @type {UnresolvedReference | null} */
|
|
550
|
+
let firstUntyped = null;
|
|
551
|
+
const seen = new Set();
|
|
552
|
+
const queue = roots.filter(Boolean);
|
|
553
|
+
while (queue.length > 0) {
|
|
554
|
+
const sf = queue.shift();
|
|
555
|
+
if (seen.has(sf)) continue;
|
|
556
|
+
seen.add(sf);
|
|
557
|
+
const {missing, untyped, next} = read(sf);
|
|
558
|
+
if (missing) return missing;
|
|
559
|
+
firstUntyped ??= untyped;
|
|
560
|
+
queue.push(...next.filter(file => follow(file.fileName)));
|
|
561
|
+
}
|
|
562
|
+
return firstUntyped;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* The local names an import binds: its default, its namespace, the names it
|
|
567
|
+
* lists, or an `import x = require()`.
|
|
568
|
+
* @param {any} ts
|
|
569
|
+
* @param {any} parent the import's declaration
|
|
570
|
+
* @returns {string[]}
|
|
571
|
+
*/
|
|
572
|
+
function boundNames(ts, parent) {
|
|
573
|
+
if (ts.isExternalModuleReference(parent)) return [parent.parent.name.text];
|
|
574
|
+
const clause = ts.isImportDeclaration(parent) ? parent.importClause : null;
|
|
575
|
+
const bound = clause?.namedBindings;
|
|
576
|
+
return [
|
|
577
|
+
...(clause?.name ? [clause.name.text] : []),
|
|
578
|
+
...(bound && ts.isNamespaceImport(bound) ? [bound.name.text] : []),
|
|
579
|
+
...(bound && ts.isNamedImports(bound)
|
|
580
|
+
? bound.elements.map(/** @param {any} e */ e => e.name.text)
|
|
581
|
+
: []),
|
|
582
|
+
];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Whether an import takes any names; one that takes none loads its module
|
|
587
|
+
* for what that does globally, as a side-effect import does.
|
|
588
|
+
* @param {any} ts
|
|
589
|
+
* @param {any} statement
|
|
590
|
+
*/
|
|
591
|
+
function importsNames(ts, statement) {
|
|
592
|
+
const clause = statement.importClause;
|
|
593
|
+
const bound = clause?.namedBindings;
|
|
594
|
+
return (
|
|
595
|
+
!!clause &&
|
|
596
|
+
(!!clause.name ||
|
|
597
|
+
(!!bound && (!ts.isNamedImports(bound) || bound.elements.length > 0)))
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Whether a node gives type arguments to a value, not a type name: the base
|
|
603
|
+
* a class extends, or an instantiation expression (`select<string>`).
|
|
604
|
+
* @param {any} ts
|
|
605
|
+
* @param {any} node
|
|
606
|
+
*/
|
|
607
|
+
function isValueWithTypeArguments(ts, node) {
|
|
608
|
+
if (!ts.isExpressionWithTypeArguments(node)) return false;
|
|
609
|
+
const clause = node.parent;
|
|
610
|
+
if (!ts.isHeritageClause(clause)) return true;
|
|
611
|
+
return (
|
|
612
|
+
clause.token === ts.SyntaxKind.ExtendsKeyword &&
|
|
613
|
+
(ts.isClassDeclaration(clause.parent) ||
|
|
614
|
+
ts.isClassExpression(clause.parent))
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Source text on one line, comments dropped, for a reason.
|
|
620
|
+
* @param {string} text
|
|
621
|
+
*/
|
|
622
|
+
function oneLine(text) {
|
|
623
|
+
return text
|
|
624
|
+
.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, '')
|
|
625
|
+
.replace(/\s+/g, ' ')
|
|
626
|
+
.trim();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Whether a node reads a value: a property or element access, a call, or a
|
|
631
|
+
* name spread, assigned, passed or returned. A name that only declares or
|
|
632
|
+
* imports something is not read here, and may stand for a type.
|
|
633
|
+
* @param {any} ts
|
|
634
|
+
* @param {any} node
|
|
635
|
+
*/
|
|
636
|
+
function readsValue(ts, node) {
|
|
637
|
+
if (
|
|
638
|
+
ts.isPropertyAccessExpression(node) ||
|
|
639
|
+
ts.isElementAccessExpression(node) ||
|
|
640
|
+
ts.isCallExpression(node)
|
|
641
|
+
) {
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
if (!ts.isIdentifier(node)) return false;
|
|
645
|
+
const parent = node.parent;
|
|
646
|
+
return (
|
|
647
|
+
ts.isSpreadAssignment(parent) ||
|
|
648
|
+
ts.isSpreadElement(parent) ||
|
|
649
|
+
ts.isJsxSpreadAttribute(parent) ||
|
|
650
|
+
ts.isShorthandPropertyAssignment(parent) ||
|
|
651
|
+
((ts.isVariableDeclaration(parent) || ts.isPropertyAssignment(parent)) &&
|
|
652
|
+
parent.initializer === node) ||
|
|
653
|
+
(ts.isCallExpression(parent) && parent.arguments.includes(node)) ||
|
|
654
|
+
ts.isReturnStatement(parent) ||
|
|
655
|
+
ts.isArrayLiteralExpression(parent) ||
|
|
656
|
+
ts.isParenthesizedExpression(parent) ||
|
|
657
|
+
ts.isAsExpression(parent) ||
|
|
658
|
+
ts.isConditionalExpression(parent) ||
|
|
659
|
+
ts.isJsxExpression(parent) ||
|
|
660
|
+
ts.isExportAssignment(parent)
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* The name an access or call starts from: `fmt` in `fmt.parse(text)`.
|
|
666
|
+
* @param {any} ts
|
|
667
|
+
* @param {any} node
|
|
668
|
+
* @returns {string}
|
|
669
|
+
*/
|
|
670
|
+
function rootName(ts, node) {
|
|
671
|
+
let root = node;
|
|
672
|
+
while (
|
|
673
|
+
ts.isPropertyAccessExpression(root) ||
|
|
674
|
+
ts.isElementAccessExpression(root) ||
|
|
675
|
+
ts.isCallExpression(root)
|
|
676
|
+
) {
|
|
677
|
+
root = root.expression;
|
|
678
|
+
}
|
|
679
|
+
return ts.isIdentifier(root) ? root.text : '';
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Whether the compiler can only give a value its error type.
|
|
684
|
+
* @param {any} checker
|
|
685
|
+
* @param {any} node
|
|
686
|
+
*/
|
|
687
|
+
function isErrorValue(checker, node) {
|
|
688
|
+
try {
|
|
689
|
+
return checker.getTypeAtLocation(node).intrinsicName === 'error';
|
|
690
|
+
} catch {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Whether the compiler can only give a type node its error type.
|
|
697
|
+
* @param {any} checker
|
|
698
|
+
* @param {any} node
|
|
699
|
+
*/
|
|
700
|
+
function isErrorType(checker, node) {
|
|
701
|
+
try {
|
|
702
|
+
return checker.getTypeFromTypeNode(node).intrinsicName === 'error';
|
|
703
|
+
} catch {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* The first reference that does not resolve from the program's global roots,
|
|
710
|
+
* whose types every file sees: a type package the program loads by name
|
|
711
|
+
* (`compilerOptions.types`, `*` expanded as TypeScript does, or every
|
|
712
|
+
* package in `typeRoots` where TypeScript loads them all) that cannot be
|
|
713
|
+
* found, or a reference in a declaration file the package's own config
|
|
714
|
+
* includes, wherever it sits, or in the package's own type packages, or in a
|
|
715
|
+
* file they import. Of those, only a file that acts globally starts the walk:
|
|
716
|
+
* a script, `declare global`, `declare module`, `export as namespace`, an
|
|
717
|
+
* import that takes no names (a side-effect import, `import {} from`), a
|
|
718
|
+
* bare `export *`, or a `/// <reference>`. A config from above the package,
|
|
719
|
+
* such as a monorepo's root config, may include the whole repository, so
|
|
720
|
+
* only the package's own declaration files start the walk then. A
|
|
721
|
+
* dependency's declarations (below `node_modules`) are not followed: `tsc`
|
|
722
|
+
* skips their errors (`skipLibCheck`), and they reach a component only
|
|
723
|
+
* through the globals they declare.
|
|
724
|
+
* @param {any} ts the project's own `typescript`
|
|
725
|
+
* @param {any} program
|
|
726
|
+
* @param {any} checker
|
|
727
|
+
* @param {string[]} declarations the declaration files the config includes
|
|
728
|
+
* @param {any} options the program's compiler options
|
|
729
|
+
* @param {(file: string) => boolean} isOwnFile whether a file is the
|
|
730
|
+
* package's own
|
|
731
|
+
* @returns {UnresolvedReference | null}
|
|
732
|
+
*/
|
|
733
|
+
export function unresolvedGlobals(
|
|
734
|
+
ts,
|
|
735
|
+
program,
|
|
736
|
+
checker,
|
|
737
|
+
declarations,
|
|
738
|
+
options,
|
|
739
|
+
isOwnFile,
|
|
740
|
+
) {
|
|
741
|
+
const config =
|
|
742
|
+
options.configFilePath ??
|
|
743
|
+
path.join(ts.sys.getCurrentDirectory(), 'tsconfig.json');
|
|
744
|
+
/** @type {(file: string) => boolean} */
|
|
745
|
+
const follow = file =>
|
|
746
|
+
isOwnFile(file) || !/[\\/]node_modules[\\/]/.test(file);
|
|
747
|
+
const own = isOwnFile(config);
|
|
748
|
+
// A declaration file reaches a component through what it does globally: a
|
|
749
|
+
// script, `declare global`, `declare module`, `export as namespace`, or an
|
|
750
|
+
// import that takes no names, a bare `export *` or a `/// <reference>`, made
|
|
751
|
+
// for what those load. A module that does none of these counts only where a
|
|
752
|
+
// component imports it.
|
|
753
|
+
/** @type {(sf: any) => boolean} */
|
|
754
|
+
const global = sf =>
|
|
755
|
+
sf &&
|
|
756
|
+
(!ts.isExternalModule(sf) ||
|
|
757
|
+
sf.referencedFiles.length > 0 ||
|
|
758
|
+
sf.typeReferenceDirectives.length > 0 ||
|
|
759
|
+
sf.statements.some(
|
|
760
|
+
(/** @type {any} */ statement) =>
|
|
761
|
+
(ts.isModuleDeclaration(statement) &&
|
|
762
|
+
(ts.isGlobalScopeAugmentation(statement) ||
|
|
763
|
+
ts.isStringLiteral(statement.name))) ||
|
|
764
|
+
ts.isNamespaceExportDeclaration(statement) ||
|
|
765
|
+
(ts.isImportDeclaration(statement) && !importsNames(ts, statement)) ||
|
|
766
|
+
(ts.isExportDeclaration(statement) &&
|
|
767
|
+
!!statement.moduleSpecifier &&
|
|
768
|
+
!statement.exportClause),
|
|
769
|
+
));
|
|
770
|
+
const roots = declarations
|
|
771
|
+
.filter(file => own || isOwnFile(file))
|
|
772
|
+
.map(file => program.getSourceFile(file))
|
|
773
|
+
.filter(global);
|
|
774
|
+
// With `types` unset TypeScript 5 loads every package in `typeRoots`, and
|
|
775
|
+
// TypeScript 6 loads none; this asks the project's own which it loads.
|
|
776
|
+
for (const name of ts.getAutomaticTypeDirectiveNames(options, ts.sys)) {
|
|
777
|
+
const found = ts.resolveTypeReferenceDirective(
|
|
778
|
+
name,
|
|
779
|
+
path.join(path.dirname(config), '__inferred type names__.ts'),
|
|
780
|
+
options,
|
|
781
|
+
ts.sys,
|
|
782
|
+
).resolvedTypeReferenceDirective?.resolvedFileName;
|
|
783
|
+
if (!found) {
|
|
784
|
+
return {name: `the types \`${name}\``, file: config, how: 'named'};
|
|
785
|
+
}
|
|
786
|
+
const sf = follow(found) && program.getSourceFile(found);
|
|
787
|
+
if (global(sf)) roots.push(sf);
|
|
788
|
+
}
|
|
789
|
+
return unresolvedReference(ts, program, checker, roots, follow, follow);
|
|
790
|
+
}
|