Repository navigation
feat(cli): read a package's TypeScript project the way tsc reads it - #7681
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewNo new or modified components detected. Bundle Size SummaryNo component packages changed. Accessibility AuditStatus: No accessibility violations detected. Visual Regression40 of 482 shot(s) changed. A change here is a question, not a failure: check whether the after is the
and 20 more. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
There was a problem hiding this comment.
Approval is on hold for a maintainer decision: the only authority this groundwork cites, AST-039 FR12 (and FR13 for the shared TypeScript resolver), exists only in the draft spec PR #7658, so a maintainer needs to approve that requirement, or identify the current spec, covering how the CLI picks a project's TypeScript and reads its tsconfig.json. Independently of that decision, package-tsconfig.mjs needs attention:
readPackageTsconfigthrowsENOENTinstead of reporting a config error when afilesentry names a missing.tsfile, because the ambient-file filter reads every included file outside thetry(tscreports this case as TS6053).- Two smaller gaps contradict the file header: references are followed only four levels deep although it says "at any depth", and a global script file (one with no import or export) is not picked up as an ambient file.
[Automated review]
| declarations: (parsed?.fileNames ?? []).filter( | ||
| /** @param {string} file */ file => | ||
| /\.d\.[cm]?ts$/.test(file) || | ||
| /\bdeclare\s+(?:global|module)\b/.test(fs.readFileSync(file, 'utf8')), | ||
| ), |
There was a problem hiding this comment.
This reads every non-declaration file in fileNames, and it runs outside the try above. TypeScript's config parse does not check that a files entry exists, so "files": ["src/Button.tsx", "src/missing.ts"] parses with no error and this line then throws ENOENT. tsc reports the same config as TS6053. The header promises that "A config with errors is reported". Treating a file that cannot be read as non-ambient, or returning it as error, would fix this; please add a test for that case.
There was a problem hiding this comment.
Fixed in 577fdd0. A files entry that doesn't exist is now a config error, tsconfig.json: File 'src/missing.ts' not found., matching tsc's TS6053. The ambient filter also treats a file it can't read as not ambient, so the reading never throws. The test 'reports a config with errors instead of reading defaults' adds the case "files": ["src/Button.tsx", "src/missing.ts"].
| const including = (config, depth) => | ||
| includes(config) | ||
| ? config | ||
| : depth < 4 |
There was a problem hiding this comment.
The header and the test title say references are followed "at any depth", but this stops after four levels. A five-level chain returns the solution root's empty options with no error, so the package is read with TypeScript's built-in defaults. Tracking the config paths already visited, instead of capping depth, would match the docs and still stop cycles. Otherwise, please document the cap and report an error when it is reached.
There was a problem hiding this comment.
Fixed in 577fdd0. The walk now follows references at any depth, tracking the config paths it has visited instead of capping the depth, so a cycle ends. The test now follows a six-level chain whose every level also refers back to the root, and reads the deepest config's options.
There was a problem hiding this comment.
Approval is on hold for a maintainer decision on spec authority: the only requirement this groundwork cites, AST-039 FR12 (and FR13 for the shared TypeScript resolver), is still in the draft spec PR #7658, so a maintainer needs to approve it, or identify a current spec, covering how the CLI picks a project's TypeScript (including reporting a 7.x install by its version) and which tsconfig.json governs a package. The three issues raised on the previous revision (the ENOENT throw on a missing files entry, the four-level reference cap, and missed global scripts) are fixed, each with a test that fails without the fix. No further change is needed from the author while that decision is pending.
[Automated review]
There was a problem hiding this comment.
Approval is on hold for a maintainer decision on spec authority: the only requirement this groundwork cites, AST-039 FR12 (and FR13 for the shared TypeScript resolver), is still in the draft spec PR #7658, so a maintainer needs to approve it, or identify a current spec, covering how the CLI picks a project's TypeScript and which tsconfig.json governs a package. Independently of that decision, the new files check in package-tsconfig.mjs needs a fix: it resolves the raw files strings itself, so a config that tsc -p compiles cleanly is reported as a config error when an entry uses ${configDir} (which the previous revision read correctly) or leaves off its extension. Resolving each entry the way TypeScript does, with a test for each case, would fix it; the fix for include matches that vanish mid-read works as described.
[Automated review]
| .filter(/** @param {unknown} file */ file => typeof file === 'string') | ||
| .map( | ||
| /** @param {string} file */ file => | ||
| path.resolve(path.dirname(configPath), file), |
There was a problem hiding this comment.
This resolves the raw files string itself, so it misses TypeScript's own resolution: "files": ["${configDir}/src/Button.ts"] (also when inherited from a shared base) and "files": ["src/Button"] with src/Button.ts present both compile with tsc -p (exit 0), but this returns File '…' not found. and the whole config reads as an error. The previous check, over the file names TypeScript had already substituted, read the ${configDir} case correctly.
Substituting a leading ${configDir} with the config's directory, accepting a .ts, .tsx or .d.ts file for an entry without an extension, and checking with ts.sys.fileExists (false for a directory, which tsc rejects as TS6231) would match tsc; please add those cases to the test.
There was a problem hiding this comment.
Fixed in c950711. TypeScript now reads each files entry itself, so the check can't drift from tsc. The entries are TypeScript's own parse of the config with nothing matched by include. Whether each is found is a program over just those entries (no library, no types, no imports): TS6053 for a missing file, TS6231 for a folder. That substitutes ${configDir} on a version that has it, resolves an entry against the config that writes it, and tries the TypeScript extensions for one without its own. The files it finds are the ones read. Test: 'reads each files entry as tsc finds it' covers ${configDir} in the package's config and in a shared base it extends, entries without their extension (an ambient script among them is loaded), a missing file and a folder. On TypeScript 5.0 and 4.8, which have no ${configDir}, tsc -p reports such an entry as not found, and so does the check.
d82125a to
c950711
Compare
There was a problem hiding this comment.
Approval is on hold for a maintainer decision on spec authority: the only requirement this groundwork cites, AST-039 FR12 (and FR13 for the shared TypeScript resolver), is still in the draft spec PR #7658, so a maintainer needs to approve it, or identify a current spec, covering how the CLI picks a project's TypeScript and which tsconfig.json governs a package. The files fix works as described, but two issues in package-tsconfig.mjs need fixing regardless of that decision:
- A solution-style
tsconfig.jsonwhose referenced config is missing or does not parse is read with the root's options and no error, whiletscreports it (TS6053, TS5083 or TS1136); returning that reference's error when no config includes the sources, with a test, would fix it. - A global script that declares a namespace, such as
declare namespace Acme { export interface Props {} }, is not loaded, because the module check also matches the indentedexport; TypeScript's own module detection, or counting only an unindentedimportorexport, would fix it.
[Automated review]
| const next = path.resolve(ts.resolveProjectReferencePath(ref)); | ||
| if (visited.has(next)) continue; | ||
| visited.add(next); | ||
| const hit = including(parse(next)); |
There was a problem hiding this comment.
When a referenced config is missing or does not parse, parse(next) sets its error, but it has no file names, so including passes over it and line 196 falls back to the solution root's own options with no error. For the same project tsc -p reports TS6053 and tsc -b reports TS5083 or TS1136, and the header says a config with errors is reported, never read as defaults. Keeping the first referenced config that carries an error and returning it when no config includes the sources would fix this; please add a test for a missing and an unparsable reference.
There was a problem hiding this comment.
Fixed in 665d0d3. The reference walk keeps the first referenced config that is missing or doesn't parse. When no config includes the sources, that config's error is the project's error, as tsc reports it, and the comparison lists every table with it. The references test adds a reference to a missing config and one to a config that doesn't parse; both failed before.
| } | ||
| return ( | ||
| /\bdeclare\s+(?:global|module)\b/.test(text) || | ||
| (/\.tsx?$/.test(file) && !/^\s*(?:import|export)\b/m.test(text)) |
There was a problem hiding this comment.
^\s* also matches an indented export inside a namespace, so a global script such as declare namespace Acme { export interface Props {} }, which TypeScript treats as a script rather than a module, is not loaded. Deciding with TypeScript's own parse (ts.isExternalModule on the parsed file), or counting only an unindented import or export, would load it; please add the case to the test.
There was a problem hiding this comment.
Fixed in 665d0d3. TypeScript's own module detection decides now: a .ts file that writes import or export anywhere is parsed, and it counts as a script when isExternalModule is false. So declare namespace Acme { export interface Props {} } is loaded. The defaults test adds that file. A file that writes neither word skips the parse; on this repository's Core, the parse adds about a second over 1,292 files.
There was a problem hiding this comment.
Approval is on hold until a maintainer approves the draft AST-039 FR12 (and FR13 for the shared TypeScript resolver) in #7658, or identifies a current spec, covering how the CLI picks a project's TypeScript, which tsconfig.json governs a package, and which unresolved references make a package's types a guess. Both issues from the previous revision are fixed, each with a test. Independently of that decision, unresolvedGlobals in the new unresolved-references.mjs starts from every declaration file the config includes, so a module .d.ts that no component imports, but that imports a package that isn't installed, marks the whole package unresolved even though tsc compiles it with skipLibCheck. Starting the walk only from declaration files that declare globals or augment a module, with a test, would fix it.
[Automated review]
| const roots = declarations | ||
| .filter(file => own || isOwnFile(file)) | ||
| .map(file => program.getSourceFile(file)); |
There was a problem hiding this comment.
declarations holds every .d.ts the config includes, because isAmbient returns true for any .d.ts. So a module declaration file becomes a global root here. A module .d.ts declares nothing global, yet a missing import in one that no component imports is reported as the package's unresolved reference. For example, take src/legacy/old-api.d.ts containing import type {Old} from '@acme/removed-package'. This returns @acme/removed-package, while tsc -p exits 0 under skipLibCheck: true. Keeping only the declaration files TypeScript reads as no module, or that contain declare global or declare module, would match "whose types every file sees". Please add the case to the unresolvedGlobals test.
There was a problem hiding this comment.
Fixed in b986443. Only a declaration file that acts globally starts the walk now: one TypeScript reads as no module, or with declare global or declare module, or with an import that takes no names (a side-effect import, import {} from, import type {} from), an export *, or a /// <reference>. Those last forms load a package for its augmentations, which is why they stay; an own type package whose only content is import '@missing/css-prop' would otherwise lose its css prop silently. Type packages named in types take the same test. A module like src/legacy/old-api.d.ts counts only where a component imports it. The unresolvedGlobals test adds that case (compared now, and it failed before) and keeps the augmenting and loading forms as roots.
665d0d3 to
b986443
Compare
There was a problem hiding this comment.
Approval is on hold until a maintainer approves AST-039 FR12 (and FR13 for the shared TypeScript resolver), which reached main through #7658 but is still a draft record, or identifies another current spec covering how the CLI picks a project's TypeScript, which tsconfig.json governs a package, and which unresolved references make a package's types a guess. The previous revision's issue is fixed, with a test. Independently of that decision, unresolved-references.mjs needs two fixes:
- An instantiation expression such as
export const StringSelect = Select<string>;is reported as an unresolved type, thoughtsc -paccepts it, becauseExpressionWithTypeArgumentsis checked outside heritage clauses; checking it only in anextendsorimplementsclause, with a test, would fix it. export * as x from 'pkg'in a module.d.tsthat no component imports starts the global walk, unlikeimport * as x from 'pkg', so a missing package there marks every component unresolved; counting only a bareexport *would fix it.
[Automated review]
| K.TypeQuery, | ||
| K.IndexedAccessType, | ||
| K.ImportType, | ||
| K.ExpressionWithTypeArguments, |
There was a problem hiding this comment.
An instantiation expression is also an ExpressionWithTypeArguments, so export const StringSelect = Select<string>; reaches isErrorType: getTypeFromTypeNode resolves Select as a type name, finds only a value, and returns the error type. The walk then reports `Select<string>` as unresolved, while tsc -p exits 0 on the same package; the same happens through a helper a component imports (makeBox<string>). isClassBase only exempts a class's extends. Checking an ExpressionWithTypeArguments only when node.parent is a HeritageClause (an interface's extends, a class's implements) would fix it; please add an instantiation expression to the cases that resolve.
There was a problem hiding this comment.
Fixed in 6a3895e. An ExpressionWithTypeArguments is checked only where it names a type: in an interface's extends or a class's implements. An instantiation expression (select<string>), like a class's base, gives type arguments to a value, so it's left alone. The reference test adds export const selectString = select<string>; with a typeof of it to the cases that resolve; it failed before.
| (ts.isExportDeclaration(statement) && | ||
| !!statement.moduleSpecifier && | ||
| (!statement.exportClause || | ||
| ts.isNamespaceExport(statement.exportClause))), |
There was a problem hiding this comment.
export * as legacy from 'pkg' binds every name under one, like import * as legacy from 'pkg', which importsNames counts as taking names, but this arm makes it a global root. An unimported src/legacy/index.d.ts with export * as legacy from '@acme/removed-package' makes unresolvedGlobals return `@acme/removed-package` for the whole package, while tsc -p exits 0 under skipLibCheck and the import * as form returns null. Dropping the isNamespaceExport arm, so only a bare export * counts, would fix it; please add the case to the unresolvedGlobals test.
There was a problem hiding this comment.
Fixed in 6a3895e. Only a bare export * counts as acting globally now; export * as legacy from 'pkg' binds names as import * as does. The unresolvedGlobals test adds an unimported module .d.ts with each of import * as legacy and export * as legacy from a missing package, and both return null; the export * as one failed before.
b986443 to
6a3895e
Compare
There was a problem hiding this comment.
Changes requested: one fix in unresolved-references.mjs is needed before approval; the governing claims are now current in the CLI surface record (INV33/INV34), and both issues from the previous revision are fixed with tests. unresolvedReference caches each file's result across calls in the module-level IMPORTS map, but that result depends on the per-call checkedDeclarations set, so a file an earlier call on the same program already walked through is cached as clean: CardHeader.ts, whose keyof typeof defaults comes from {...theme.sizes} (a TS2339 error), returns theme.sizes on a fresh program but null once a call on Card.ts, which imports it, has run. Caching only missing and next across calls (or memoizing each declaration's scan result instead of skipping it), with a test that calls twice on one program in both orders, would fix it.
[Automated review]
| }; | ||
| /** @type {(sf: any) => {missing: UnresolvedReference | null, untyped: UnresolvedReference | null, next: any[]}} */ | ||
| const read = sf => { | ||
| const cache = IMPORTS[mine(sf.fileName) ? 1 : 0]; |
There was a problem hiding this comment.
This cache outlives the call, but the untyped it stores depends on checkedDeclarations, which is per call: a declaration already scanned in this call is skipped (lines 186–194), so a file reached after it is cached with untyped: null even when its type comes from the same erroneous value. On one program, CardHeader.ts (with keyof typeof defaults, where defaults = {...theme.sizes} is a TS2339 error) returns `theme.sizes` when checked first, but null once a call on Card.ts, which imports it, has run. Caching only missing and next across calls, or memoizing each declaration's scan result instead of skipping it, would fix it; please add a test that calls twice on one program in both orders.
There was a problem hiding this comment.
Fixed in 56eb82a. Nothing the walk learns outlives the program now, and no answer depends on what an earlier call read. The record is kept per program and per mine test, in a WeakMap keyed on the program. Each declaration's own scan is memoized by declaration: the first error read in what its type is inferred from, and the declarations it names. Each start then walks those depth first with a fresh visited set. The only thing shared across calls is the set of declarations whose whole reach read clean, which can't change an answer. The new test calls unresolvedReference on Card.ts and CardHeader.ts in both orders on one program and gets theme.sizes for both each time; it failed before. Core and Lab are unchanged. The props check passes one mine per run, so each file is still read once.
6a3895e to
56eb82a
Compare
There was a problem hiding this comment.
Changes requested: one small fix in unresolved-references.mjs is needed before approval; the order-dependent cache from the previous revision is fixed and tested, and the governing claims (CLI surface INV33/INV34) are current. unresolvedGlobals walks type packages only when compilerOptions.types is set, but TypeScript 5.x loads every typeRoots package automatically when types is unset, so a package's own type package there is never walked: on 5.9.3, a types/css-prop/index.d.ts that references an uninstalled package returns null, while naming it in types reports the missing package. Iterating ts.getAutomaticTypeDirectiveNames(options, ts.sys) unconditionally (it already returns [] where TypeScript 6 loads nothing), with a test, would fix it.
[Automated review]
| for (const name of options.types | ||
| ? ts.getAutomaticTypeDirectiveNames(options, ts.sys) | ||
| : []) { |
There was a problem hiding this comment.
Before TypeScript 6, an unset types makes the program load every package in typeRoots, so a package's own type package there is loaded but never walked. On 5.9.3, with typeRoots: ['./types', './node_modules/@types'] and types/css-prop/index.d.ts containing /// <reference types="@acme/not-installed-css" />, this returns null, while types: ['css-prop'] returns the types `@acme/not-installed-css` . TypeScript 6 loads nothing automatically, which is why the suite on 6.0.3 can't see it. Iterating ts.getAutomaticTypeDirectiveNames(options, ts.sys) unconditionally would fix it: it returns [] where TypeScript 6 loads nothing, follow still keeps a dependency's type package out, and the existing suite still passes with that change. Please add a test.
There was a problem hiding this comment.
Fixed in 4178089. unresolvedGlobals now asks the project's TypeScript which type packages it loads, ts.getAutomaticTypeDirectiveNames(options, ts.sys), whatever types says: every typeRoots package on TypeScript 5 with types unset, none on TypeScript 6. So a package's own types/css-prop is walked on 5.x and its missing @acme/not-installed-css is named. The test models TypeScript 5's default with TypeScript 6's types: ['*'], since the repository installs 6, and failed before.
56eb82a to
4178089
Compare
There was a problem hiding this comment.
Recommending approval: the fix requested on the previous revision is in, since unresolvedGlobals now walks every type package the project's TypeScript loads, including those TypeScript 5 loads automatically when types is unset, and a check against the compiler on TypeScript 5.9.3 and 6.0.3 confirms it. Every change is internal groundwork for the current CLI-surface INV33/INV34, nothing reaches it until the stacked pull requests wire it in, and the changed tests, lint, the changeset check and CI pass. No further changes are needed; this is ready for a maintainer's approval.
[Automated review]
Groundwork for the documented-props comparison: `resolveProjectTypeScript` finds the project's own `typescript` (and names an installed version that has no JavaScript compiler API, such as TypeScript 7), and `readPackageTsconfig` reads the package's TypeScript project as `tsc` reads it: the nearest `tsconfig.json`, which is the package's own or a monorepo root's for a local package but never an app's above an installed package; a solution-style config's references, at any depth, to the config that includes the sources; and the declaration files the config includes. A config with errors is reported, never read as defaults. Nothing reaches it yet, so the CLI's behavior does not change.
…h, and read global scripts A `files` entry that does not exist is a config error, as `tsc` reports it (TS6053), instead of a crash. References are followed at any depth, each config once, so a cycle ends. An included script (a `.ts` file with no import or export) declares globals and joins the files to read, like a declaration file.
…nnot find it
A file the config's `include` matched that is gone by the time it is read,
such as one another process writes and removes, made the package's config
read as having an error, and every prop table was listed as not compared.
Each `files` entry is now read by TypeScript itself, which substitutes
`${configDir}`, resolves the entry against the config that writes it, and
tries the TypeScript extensions for one written without its own. Only an
entry it cannot find is an error, with the message `tsc` gives (TS6053, or
TS6231 for a folder), and the files it finds are the ones read.
…compiler cannot resolve A foundation module, `unresolved-references.mjs`, finds the first reference the compiler cannot resolve, so a check that reads a package's types knows when part of them is a guess. `unresolvedReference` walks given files, the files they import, and so on, as far as the caller allows: a module the program cannot find, a name its module does not export, a missing `/// <reference>` type package or file, a side-effect import where the compiler checks one fail, and so does, in the package's own files, a type the compiler can only give its error type (a name or value declared nowhere, a property that doesn't exist, a JavaScript file's JSDoc type), or a value read that way, with an error `tsc` reports, in what the type of a value a `typeof` reads, a member a type indexes, or a class's base is inferred from; a module found without types resolves. A missing reference anywhere the walk goes is named before such a type. What the walk learns is kept per program, so an answer never depends on what an earlier call read. `unresolvedGlobals` walks the program's global roots: the declaration files the package's own config includes, wherever they sit, its own type packages, and each type package the program loads (the ones the config names, or with `types` unset, every one in `typeRoots` where the project's TypeScript loads them all), which fails when it cannot be found. A config from above the package starts only from the package's own declaration files, and a dependency's declarations are not followed, as `tsc` skips their errors. The documented-props comparison, stacked on this, uses it to list a component as not compared when a dependency it reads is not installed.
…bal script that declares a namespace A solution-style config whose referenced config is missing or does not parse was read with the root's own options and no error, while `tsc` reports it. When no config includes the sources, the first broken reference is now the project's error. A global script that declares a namespace with an `export` inside it was taken for a module, so its types were not loaded; TypeScript's own module detection decides now.
4178089 to
9a2f678
Compare
There was a problem hiding this comment.
Changes requested: one small fix in unresolved-references.mjs is needed before approval; every change here serves the current CLI-surface INV33/INV34, and the rebase left the delta unchanged. unresolvedReference checks every JSDoc type in a JavaScript file, including @throws and @exception, which the compiler never checks, so a component whose file documents @throws {HttpError} with HttpError declared nowhere is reported as having an unresolved reference even though its props are fully typed and tsc -p exits 0, with or without checkJs. Skipping that tag when visiting a JavaScript file's JSDoc (for example, returning early from visit for ts.SyntaxKind.JSDocThrowsTag), with a @throws case among the references that resolve in unresolved-references.test.mjs, would fix it.
[Automated review]
| /** @type {(node: any) => void} */ | ||
| const visit = node => { | ||
| if (untyped) return; | ||
| if (js) for (const doc of node.jsDoc ?? []) visit(doc); |
There was a problem hiding this comment.
This visits every JSDoc tag of a JavaScript file, including @throws and @exception, whose type the compiler never checks. isErrorType then gives an undeclared name there the error type, so a file documenting @throws {HttpError} with HttpError declared nowhere returns `HttpError` as unresolved, and a component with fully typed props would be listed as not compared, while tsc -p exits 0 with checkJs off and on. Skipping that tag, for example with an early if (node.kind === ts.SyntaxKind.JSDocThrowsTag) return; in visit, and adding a @throws case to the references that resolve in the test, would fix it.
There was a problem hiding this comment.
Fixed in 07caa63 with an allow-list rather than one more skip. In a JavaScript file the walk now reads only the JSDoc tags the compiler reads types from: @type, @param, @returns, @typedef, @callback, @template, @property, @this, @enum, @augments/@extends, @implements, @satisfies, @overload and @import, where the project's TypeScript has them. It passes over every other tag (@throws, @exception, @see, @example, unknown tags). A file with @throws {HttpError}, @exception, and @see in two forms is now among the references that resolve in unresolved-references.test.mjs; it failed before. Every allowed tag still reports a name declared nowhere, on TypeScript 4.8, 5.0 and 6.0. An independent audit of this change also found that a class whose base names nothing (class AcmeX extends NowhereBase<P>, or @augments {NowhereBase} in JavaScript) was compared, with its documented props reported as missing, though <AcmeX /> doesn't type-check. The walk read a class's base only as a value; a base whose name resolves to nothing now counts as a reference that does not resolve, with tests that failed before.
…d a class base that names nothing
In a JavaScript file, the walk for references that do not resolve read the
type in every JSDoc tag, `@throws` and `@exception` included, which the
compiler never checks. So a component whose file documents
`@throws {HttpError}`, with `HttpError` declared nowhere, was listed as not
compared, though its props are fully typed and `tsc -p` exits 0. It now
reads only the tags the compiler reads types from (`@type`, `@param`,
`@returns`, `@typedef`, `@callback`, `@template`, `@property`, `@this`,
`@enum`, `@augments`, `@implements`, `@satisfies`, `@overload` and
`@import`, where the project's TypeScript has them) and passes over the
rest (`@throws`, `@see`, `@example` …).
A class whose base names nothing (`class AcmeX extends NowhereBase<P>`, in
TypeScript or JavaScript, or `@augments {NowhereBase}`) has no type the
compiler can give, so `<AcmeX />` does not type-check, yet it was compared
and its documented props reported as missing. The walk read a class's base
only as a value, for the class it names; a base that resolves to nothing
now counts as a reference that does not resolve, so the component is
listed as not compared.
There was a problem hiding this comment.
Recommending approval: both changes in this revision work as described (a JavaScript file's JSDoc is now read only in the tags the compiler takes types from, and a class whose base names nothing is reported), and each agrees with tsc -p on TypeScript 5.9.3 and 6.0.3. Everything here remains internal groundwork for the CLI-surface INV33/INV34 checks, nothing reaches it until the stacked pull requests wire it in, and the changed suites, lint and the changeset check pass. One optional follow-up: a class whose base is read off an any value (declare const lib: any; class X extends lib.Component<P> {}) is reported as a base that does not resolve although tsc accepts it, so also requiring the compiler's error type in baseless would keep that reason accurate.
[Automated review]
| (ts.isHeritageClause(node.parent) || | ||
| ts.isJSDocAugmentsTag(node.parent)) && | ||
| ts.isEntityNameExpression(node.expression) && | ||
| !checker.getSymbolAtLocation(node.expression); |
There was a problem hiding this comment.
Non-blocking: getSymbolAtLocation also returns no symbol for a property read off an any value, so declare const lib: any; export class X extends lib.Component<{label?: string}> {} returns `lib.Component` here as a base that names nothing, while tsc -p exits 0 on TypeScript 5.9.3 and 6.0.3. The class's props are any, so listing it as not compared is still right, but the reason would send the author after a name that resolves. Also requiring isErrorValue(checker, node.expression) here leaves that case unreported and still reports NowhereBase, in TypeScript and in @augments.
User need
The documented-props comparison (#7663) reads a package's props with the package's own TypeScript, so that it agrees with
tsc. That holds only if it reads the package's TypeScript project the waytscdoes. Read the wrong config, or none, and it compares the doc with the wrong types: a path alias reads as a missing package, a monorepo's settings are dropped, or an app's config above an installed package remaps the package's own imports to the app's files.Problem and solution fit
This PR adds that reading as foundation groundwork, and #7663, stacked on it, uses it. They were one PR until #7663 passed 2,000 lines, and it split along this seam.
resolveProjectTypeScriptfinds the project's owntypescript. When the installed TypeScript has no JavaScript compiler API to read types with (TypeScript 7), it names that version.readPackageTsconfigreads the package's TypeScript project astscreads it:tsconfig.json: the package's own, or a monorepo root's for a local package. An app's config above an installed package (one belownode_modules) is not the package's, so it isn't read."files": []withreferences) is followed through its references, at any depth and each config once, to the config that includes the sources. When none does, a reference that's missing or doesn't parse is the project's error, astscreports it..tsfile TypeScript's own module detection reads as no module) join the files to read, so ambient types resolve astscresolves them.filesentrytsccan't find (TS6053, or TS6231 for a folder). feat(cli): compare documented component props with the TypeScript source #7663 then lists every table with the error.unresolvedReferencefinds the first reference the compiler can't resolve, from given files through the files they import, as far as the caller says. A reference fails when the program finds no module, no type package or no file for it, when a module doesn't export a name imported from it, or, in the package's own files, when the compiler can only give a type its error type: a name or value declared nowhere, a property that doesn't exist, or such a type in a JavaScript file's JSDoc, whichkeyof, a key or an alias would otherwise hide. A reference that doesn't resolve, anywhere the walk goes, is named before such a type, since it names what to install. A side-effect import counts where the compiler checks one (the default from TypeScript 6), and a module found without types resolves.unresolvedGlobalswalks the program's global roots: the declaration files the package's own config includes, wherever they sit, its own type packages, and each type package the config names, which fails when it can't be found. For a config from above the package (a monorepo's root), only the package's own declaration files start the walk. A dependency's declarations aren't followed:tscskips their errors (skipLibCheck), and they reach a component only through the globals they declare.Current specification
Supports
architecture:cli-surfaceINV33, "Documented component props agree with the source" (AST-039 FR12, which now points to it; DEC-5), with #7663, #7676, #7684 and #7667. Its evidence row names the prop-drift, type-text, doctor-integration and core-prop-docs tests.project-typescript.mjsalso serves INV34's doc type check (#7664).Public delta and caller burden
[chore]entry that says so.Design and alternatives
findConfigFile,parseJsonConfigFileContent,resolveProjectReferencePath), so the reading followstscas it changes, with no second parser.tsconfig.json. Reading the app's config above it would apply the app'spathsto the package's own imports, such as remapping@/baseto the app's file.Evidence
foundation/discovery/unresolved-references.test.mjs(3 tests):/// <reference types>or/// <reference path>, a side-effect import under TypeScript 6 but not withnoUncheckedSideEffectImports: false, and a type the compiler can only give its error type, reached throughkeyof, a template literal key ortypeof(a name declared nowhere, a value declared nowhere behind a binding, a property that doesn't exist, a name in a JavaScript file's JSDoc, named by its innermost part), only in the files the caller checks, and never from a.tsfile's JSDoc; and a value the compiler can't type that atypeofreads, spread into an object, in the same file or another of the package's, or returned by an own function it calls, passes on or constructs (a class field), or held by a class member a type indexes (a field, a getter, a parameter property, or a member of a base built by a call, also through an interface that extends the class), or a class's base built by a call; and what resolves: a module found without types, the JSX runtime import the compiler adds,as const,globalThis,typeof undefined,typeof arguments, a class's value base, an instantiation expression,typeofof a name from a module found without types, a value read with no errortscreports (under@ts-expect-error, also on a later line of a read, in a@ts-nocheckfile, or an index that misses withoutnoImplicitAny), an error in a function whose return type is written, that has overloads, or that is called only for its effect, a value cast to a written type, a broken member of a class that no type indexes (a component'sstate, or a field of the package's own base component), and names that resolve; a dependency's own missing import found only where the walk follows it;types, a type name declared nowhere in the package's own declaration file, and a type package the config names that can't be found; a module declaration file no component imports, not a root, nor one that re-exports a module under one name (export * as), while one that augments a module, makes a side-effect import, imports no names, re-exports everything or declares a UMD global is; and not followed: a dependency's type package, named, under"*", or referenced from the package's own declaration file;foundation/discovery/package-tsconfig.test.mjs(6 tests):exportinside included), and not its modules;filesentry that doesn't exist, anextendsthat isn't installed, a config that doesn't parse, and a solution's reference that is missing or doesn't parse; while a file theincludematched that is gone by the time it is read is no error;filesentry read astscfinds it:${configDir}, in the package's config and in a shared base it extends, and an entry without its extension are read (an ambient script listed that way is loaded), while a missing file (TS6053) and a folder (TS6231) are errors.resolveProjectTypeScript.Since the first automated review
Each fix has a test that failed first:
filesentry naming a file that doesn't exist crashed the reading withENOENT, since the config parses without checkingfiles. It is reported as a config error now, astscreports it (TS6053), and an included file that can't be read is never taken as ambient..tsfile with no import or export), whose declarations are global, wasn't loaded. It is loaded now, like a declaration file. On Core and Lab the files loaded are unchanged.project-typescript.mjsis unchanged, and still byte-identical to feat(cli): check each authored doc against its published authoring type #7664's.After the next review
includematched that was gone by the time it was read, such as one another process wrote and removed, made the config read as having an error. Every prop table was then listed as not compared. A test run showed it: a package created inside a repository read the repository's config while a parallel test removed a file that config'sincludematched. Only afilesentry that doesn't exist is an error now, astscreports it (TS6053). A test reproduces the race and failed before the fix.After the review that came next
filescheck resolved each entry's text itself, so a configtsc -pcompiles cleanly was reported as an error. That happened when an entry used${configDir}, or left off its extension. TypeScript now reads each entry itself. It substitutes${configDir}on a version that has it, resolves the entry against the config that writes it, and tries the TypeScript extensions for an entry without one. Only an entry TypeScript can't find is an error, with the messagetscgives: TS6053 for a missing file, TS6231 for a folder. The files it finds are the ones read, so an ambient script listed without its extension is loaded.${configDir}in the package's config and in a shared base it extends, entries without their extension, a missing file and a folder; it failed before the fix. On TypeScript 6, 5.0 and 4.8 each case reads as that version'stscreads it: 5.0 and 4.8 have no${configDir}, and theirtscreports such an entry as not found, as the check does. Core and Lab are unchanged.After the most recent review
typeRootsfolder and named intypes, wasn't walked, since found type packages were skipped as dependencies. A type package that isn't a dependency's (one belownode_modules) is walked now.keyof, aRecord's keys, an alias or a template literal key, gave the compiler's error type no part to show it:keyofof it isstring | number | symbol. The walk now treats such a name in the package's own files as a reference that doesn't resolve, the counterpart of a missing import.as const, which TypeScript writes as a type reference namedconst, isn't one, and neither areglobalThis,undefinedandarguments, which the compiler makes itself without a declaration; only the compiler's own stand-in for a missing name counts.exportinside it was taken for a module, so its types weren't loaded. TypeScript's own module detection decides now. It parses each included.tsfile that writesimportorexport. On Core's 1,292 files that costs about 1 s on a shared machine, against a comparison that takes about 5 s.project-typescript.mjsis unchanged, and still byte-identical to feat(cli): check each authored doc against its published authoring type #7664's.After the review that followed
A type the compiler can only give its error type, but whose root isn't a name, got past the walk: a value declared nowhere behind a binding (
keyof typeof dafterconst d = missing), a property that doesn't exist (keyof Theme['sizes']), and a name declared nowhere in a JavaScript component's JSDoc, whichtscdoesn't report there. The walk now checks each type reference, type query, indexed access, import type and heritage type in the package's own files for the compiler's error type, and reads a JavaScript file's JSDoc types; the innermost such part is named. A reference that doesn't resolve, anywhere the walk goes, is still named first, since it names the package to install. The reference test adds all three and a.tsfile whose JSDoc names something declared nowhere, which TypeScript ignores; each failed before.An independent audit of that rule found two kinds of correct code it would have listed, and two augmentation loaders it missed, each fixed with a test that failed first:
class X extends Base<P>afterconst Base = React.Component, or a mixin'sclass extends B), which has no type of its own name. Theextendsof a class is skipped.any, not missing, sotypeof fmtortypeof import('./format.js').fmtno longer counts, as the plain import already didn't.import {} from,import type {} from) or throughexport *acts globally too, so it starts the walk like a side-effect import.The global walk started from every declaration file the config includes, so a module
.d.tsthat no component imports, but that imports a package that isn't installed, marked the whole package unresolved, thoughtsccompiles it underskipLibCheck. Only a declaration file that acts globally starts the walk now: a script,declare global,declare module, or a side-effect import or/// <reference>, which load augmentations for every file. Type packages named intypestake the same test. A module that does none of these counts only where a component imports it. The global-roots test adds the module, a module that augments one, and a module with a side-effect import; the first failed before.After the round-14 audit and review
export as namespace) declares a global namespace for its module, yet it didn't start the walk, so a component reached through it was listed with a vaguer reason that didn't name the missing package. It counts as acting globally now. The global-roots test adds it, and it failed before.export const StringSelect = Select<string>;) is anExpressionWithTypeArgumentstoo, so it was reported as a type that doesn't resolve, thoughtsc -paccepts it. Type arguments count now only in an interface'sextendsor a class'simplements; a class's base was already left alone.export * as legacy from 'pkg'binds the module's names under one name, asimport * asdoes, yet it made an unimported module.d.tsa global root. Only a bareexport *acts globally now.const defaults = {...theme.sizes}wherethemehas nosizes, or{...undeclaredTones}), turns the object intoany. Sokeyof typeof defaultstakes any name, and no type node carries the error. Where atypeofin the package's own files reads a value, the walk now also checks what that value's type is inferred from, in any file of the package's own, for a value read the compiler can only give its error type. That means an initializer with no type written, and what a function returns when its return type isn't written and it has no overloads, a parameter's default, and a class's base and members. It follows the package's own declarations those name, soconst slots = makeSlots(),lazy(makeSlots)and a class field are read through to the spread. A class member that a type indexes (Store['slots'], also through a subclass or an interface that extends the class) is read the same way, from its initializer or getter, and so is a class's base where a call builds it. A broken member that no type indexes decides no prop name, so it is left alone. A written type, a cast that writes one (notas const), a call made only for its effect, and JSX decide nothing about the type, so they are left alone. A read counts only wheretscreports an error inside it. So a line@ts-expect-errorcovers, a@ts-nocheckfile and an index that misses withoutnoImplicitAnyare left alone, astscleaves them, and so are names from a module found without types.@ts-expect-error. It also tripled the time on Core. So the check follows what the types read.infer(type Get<K> = Store[K],T extends {slots: infer S} ? S : never) isn't read, so a broken member there leaves the component compared. That only happens wheretsc --noEmitalready fails, and the recommended typecheck script runstscfirst. Reading every member of a class passed as a type argument would catch it, but it would also uncheck a component over an unrelated broken member of a class it only refers to (aRefObject<Widget>prop).After the review of the class and spread fixes
CardHeader.ts, whosekeyof typeof defaultscomes from{...theme.sizes}, was found on a fresh program but missed once a call onCard.ts, which imports it, had run. Now nothing the walk learns outlives the program, and no answer depends on what an earlier call read. The record is kept per program, each declaration's own scan is memoized, and each start walks with its own visited set. Only declarations whose whole reach read clean are shared, which changes no answer.Card.tsandCardHeader.tsin both orders on one program; it failed before. Core and Lab are unchanged.After the review of the type packages
typesunset, TypeScript 5 loads every package intypeRoots, but the walk read type packages only whentypeswas set, so the package's own type package there was never walked. It now asks the project's TypeScript which packages it loads (getAutomaticTypeDirectiveNames), whatevertypessays; TypeScript 6 loads none whentypesis unset. The test models TypeScript 5's default with TypeScript 6'stypes: ['*'], and failed before.Rebased onto current main
After the review of JSDoc tags
@throwsand@exceptionincluded, which the compiler never checks. So a component whose file documents@throws {HttpError}, withHttpErrordeclared nowhere, was listed as not compared, though its props are fully typed andtsc -pexits 0 with or withoutcheckJs.@type,@param,@returns,@typedef,@callback,@template,@property,@this,@enum,@augments/@extends,@implements,@satisfies,@overloadand@import, where the project's TypeScript has them. Everything else (@throws,@exception,@see,@example, unknown tags) is passed over, so a new tag can't bring the problem back.@throws,@exceptionand two@seeforms is now among the references that resolve; it failed before. Every allowed tag still reports a name declared nowhere, on TypeScript 4.8, 5.0 and 6.0.class AcmeX extends NowhereBase<P>in TypeScript or JavaScript, or@augments {NowhereBase}) has no type the compiler can give, so<AcmeX />doesn't type-check, yet it was compared and its documented props were reported as missing. The walk read a class's base only as a value, for the class it names. A base whose name resolves to nothing now counts as a reference that does not resolve, so the component is listed as not compared. Bases that resolve, call bases such asmixin(), and instantiation expressions read as before. Three cases join the test (two that name nothing, one that names a class); they failed before.Merge order
The order is required:
doctor integration components, stacked on feat(cli): compare documented component props with the TypeScript source #7663), then feat(cli): read two written TypeScript types as one when they differ only in how they are written #7684 (reading two written types as one, stacked on feat(cli): doctor integration components compares documented props with the code #7676), then feat(cli): doctor integration components judges documented prop types against the code #7667 (prop types, stacked on feat(cli): read two written TypeScript types as one when they differ only in how they are written #7684).validate, stacked on feat(cli): check each authored doc against its published authoring type #7664).integration check) last, after a rebase that adds these PRs' fields and codes to its failures list. Merged earlier, it breaks main.This PR must ship in the same release as
integration check, with #7663, #7676, #7684 and #7667.Overlap with open PRs
foundation/discovery/project-typescript.mjs, and the entry this PR adds todoc-loads.test.mjs, are byte-identical in feat(cli): check each authored doc against its published authoring type #7664, so the two merge cleanly in either order.astryx componentprints #7623, fix(cli): init writes agent docs every agent reads, at the project root, never in the home folder #7497, spec(AST-039, cli-surface): integration docs are found by their documented names and checked in one command #7658, feat(cli): a component doc can name a differently named export of the file beside it #7659, fix(cli): integration verify reads exports with the reader discovery uses #7660, feat(docsite): put the integration guide's pages under CLI Integrations in the sidebar #7661, feat(cli): integration family members are components under their own names #7674, or the other PRs in this set.Size
1,867 changed lines.
Scope
Testing
On main 9b8b723 with this branch: copyright, sync-exports, the published
./apitype check,readme:check, the four CLI typechecks (strict, json-api, authoring-contract, template-docs),check:changesets, the lint job's changeset coverage check (run on the merge commit GitHub builds),check:cli-structure,check:repo, eslint on the changed files, prettier on the new files, and the fullpnpm vitest run --project nodesuite: 10,291 tests pass. The 22 failures are in the npm pack, packed real-world package, theme resolution and CSS graph tests, which fail the same way on current main on the same machine (they need a package registry it doesn't reach), plus a build test and a search test that timed out on the busy machine and pass on their own. With all nine PRs merged into main, the fourdoctor integrationsubcommands were run in text and JSON on 64 package shapes, against main: templates output is byte-identical everywhere;validateandcomponentsonly add their new section and JSON field;docsdiffers only where the doc-links PR fixes a link warning.integration checkexits 1 exactly when one of the four would, each of its sections is byte-identical to that subcommand's output, and onlyintegration check's three list tests fail until its planned rebase.