realapi-check 0.1.0 → 0.1.1
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/dist/analyzer.js +44 -3
- package/dist/packages.d.ts +2 -0
- package/dist/packages.js +39 -0
- package/package.json +1 -1
package/dist/analyzer.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
import { collectProjectDeclarations, collectSourceFiles, isDeclarationFile, normalize, } from "./files.js";
|
|
5
|
-
import { declaredDependencies, installedTypesPackages, isBuiltin, isInstalled, installedVersion, missingFromRegistry, packageNameOf, packageOfFile, usesPnp, } from "./packages.js";
|
|
5
|
+
import { declaredDependencies, installedTypesPackages, isBuiltin, isInstalled, installedVersion, missingFromRegistry, packageNameOf, packageOfFile, runtimeSources, usesPnp, } from "./packages.js";
|
|
6
6
|
// TypeScript diagnostics that mean "the code uses an API the types don't have".
|
|
7
7
|
const MISSING_EXPORT_CODES = new Set([2305, 2724, 2614, 2459, 2460]);
|
|
8
8
|
const MISSING_MEMBER_CODES = new Set([2339, 2551]);
|
|
@@ -220,7 +220,26 @@ export async function check(opts = {}) {
|
|
|
220
220
|
return undefined;
|
|
221
221
|
if (kind === "missing-member" && isModuleType(ownerType))
|
|
222
222
|
kind = "missing-export";
|
|
223
|
-
|
|
223
|
+
const name = ts.isIdentifier(node) || ts.isStringLiteralLike(node) ? node.text : undefined;
|
|
224
|
+
// On a union, TypeScript errors unless *every* member has the property.
|
|
225
|
+
// If any member has it, the API is real and the code just needs narrowing.
|
|
226
|
+
if (name && ownerType.isUnion() && ownerType.types.some((t) => checker.getPropertyOfType(t, name))) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
// Long anonymous types read badly; name the expression instead.
|
|
230
|
+
let ownerLabel = checker.typeToString(ownerType);
|
|
231
|
+
const access = ts.isPropertyAccessExpression(parent) ? parent : undefined;
|
|
232
|
+
if (access && ownerLabel.length > 60)
|
|
233
|
+
ownerLabel = access.expression.getText();
|
|
234
|
+
// `doc.internal.getNumberOfPages()` when the real API is `doc.getNumberOfPages()`.
|
|
235
|
+
let suggestion;
|
|
236
|
+
if (name && access && ts.isPropertyAccessExpression(access.expression)) {
|
|
237
|
+
const grandparent = access.expression.expression;
|
|
238
|
+
if (checker.getPropertyOfType(checker.getTypeAtLocation(grandparent), name)) {
|
|
239
|
+
suggestion = `${grandparent.getText()}.${name}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { kind, owner, ownerLabel, suggestion };
|
|
224
243
|
};
|
|
225
244
|
const issues = [];
|
|
226
245
|
const checkedPackages = new Map();
|
|
@@ -308,6 +327,24 @@ export async function check(opts = {}) {
|
|
|
308
327
|
const d = display(p);
|
|
309
328
|
return d.version ? `${d.name}@${d.version}` : d.name;
|
|
310
329
|
};
|
|
330
|
+
/**
|
|
331
|
+
* Plugins such as jspdf-autotable add members to their host's objects at
|
|
332
|
+
* runtime (`doc.lastAutoTable = table`) without typing them. If an
|
|
333
|
+
* imported package that depends on the owner assigns this member in its
|
|
334
|
+
* runtime code, the member is real.
|
|
335
|
+
*/
|
|
336
|
+
const addedByPlugin = (owner, name) => {
|
|
337
|
+
const host = display(owner).name;
|
|
338
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
339
|
+
const assignment = new RegExp(`\\.${escaped}\\s*=(?!=)`);
|
|
340
|
+
for (const pkg of imported.values()) {
|
|
341
|
+
if (pkg.rawName === owner.rawName || !pkg.dependencies.includes(host))
|
|
342
|
+
continue;
|
|
343
|
+
if (runtimeSources(pkg.root).some((code) => assignment.test(code)))
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
};
|
|
311
348
|
const displayFields = (p) => {
|
|
312
349
|
const d = display(p);
|
|
313
350
|
return { package: d.name, version: d.version };
|
|
@@ -343,7 +380,9 @@ export async function check(opts = {}) {
|
|
|
343
380
|
for (const { sf, start, message: tsMessage, located, name } of found) {
|
|
344
381
|
if (located.kind === "missing-member" && assigned.has(memberKey(located, name)))
|
|
345
382
|
continue;
|
|
346
|
-
|
|
383
|
+
if (located.kind === "missing-member" && addedByPlugin(located.owner, name))
|
|
384
|
+
continue;
|
|
385
|
+
const suggestion = suggestionFrom(tsMessage) ?? located.suggestion;
|
|
347
386
|
const where = versioned(located.owner);
|
|
348
387
|
let message;
|
|
349
388
|
if (located.kind === "missing-export") {
|
|
@@ -388,6 +427,8 @@ export async function check(opts = {}) {
|
|
|
388
427
|
.find((t) => t.name === "deprecated")
|
|
389
428
|
?.text?.map((p) => p.text)
|
|
390
429
|
.join("")
|
|
430
|
+
// `{@link register}` / `{@link Foo | text}` → `register` / `text`.
|
|
431
|
+
.replace(/\{@link(?:code|plain)?\s+([^}|]+?)(?:\s*\|\s*([^}]+))?\s*\}/g, (_, target, text) => text ?? target)
|
|
391
432
|
.replace(/\s+/g, " ")
|
|
392
433
|
.trim();
|
|
393
434
|
const name = symbol.getName();
|
package/dist/packages.d.ts
CHANGED
|
@@ -35,3 +35,5 @@ export declare function declaredDependencies(cwd: string): Set<string>;
|
|
|
35
35
|
export declare function installedTypesPackages(cwd: string): string[];
|
|
36
36
|
/** Returns true when the npm registry has no package with this name. */
|
|
37
37
|
export declare function missingFromRegistry(name: string): Promise<boolean | undefined>;
|
|
38
|
+
/** The runtime entry files of a package (main, module, exports), read as text. */
|
|
39
|
+
export declare function runtimeSources(root: string): string[];
|
package/dist/packages.js
CHANGED
|
@@ -173,3 +173,42 @@ export async function missingFromRegistry(name) {
|
|
|
173
173
|
return undefined;
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
+
const sourceCache = new Map();
|
|
177
|
+
/** The runtime entry files of a package (main, module, exports), read as text. */
|
|
178
|
+
export function runtimeSources(root) {
|
|
179
|
+
const cached = sourceCache.get(root);
|
|
180
|
+
if (cached)
|
|
181
|
+
return cached;
|
|
182
|
+
const files = new Set();
|
|
183
|
+
try {
|
|
184
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
185
|
+
const collect = (value) => {
|
|
186
|
+
if (typeof value === "string") {
|
|
187
|
+
if (/\.[cm]?js$/.test(value))
|
|
188
|
+
files.add(value);
|
|
189
|
+
}
|
|
190
|
+
else if (value && typeof value === "object") {
|
|
191
|
+
for (const [key, nested] of Object.entries(value))
|
|
192
|
+
if (key !== "types")
|
|
193
|
+
collect(nested);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
collect(pkg.main ?? "index.js");
|
|
197
|
+
collect(pkg.module);
|
|
198
|
+
collect(pkg.exports);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// No readable package.json.
|
|
202
|
+
}
|
|
203
|
+
const sources = [];
|
|
204
|
+
for (const file of files) {
|
|
205
|
+
try {
|
|
206
|
+
sources.push(fs.readFileSync(path.join(root, file), "utf8"));
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Missing entry file.
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
sourceCache.set(root, sources);
|
|
213
|
+
return sources;
|
|
214
|
+
}
|
package/package.json
CHANGED