-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Compare ESM named exports to types #166
Conversation
|
Hey @laverdet, sorry for the extremely late acknowledgment here. Thanks for putting this together! Both work and life have been very busy in the last few months. Thanks for your patience. If you’re interested in continuing on this, I have some feedback. Otherwise, feel free to ignore, and I may pick up where you left off at some future date. Overall, I’d like to present this check to the user as more of an all-or-nothing thing, like “TS thinks there are named exports but Node.js sees none, probably due to cjs-module-lexer being unable to statically analyze them” instead of “These N specific named exports seem to be missing at runtime.” That said, I’m not against storing the diff of named exports in the problem objects (or at least being set up to store them) so we can do more with it later.
I can help with this when the rest appears to be in good shape.
There’s a lot already that’s incomplete because we don’t download dependencies. It’s fine. This also is less relevant if the CLI/UI isn’t trying to report specifically which named exports are missing at runtime.
Agreed 👍
Yeah, it would be nice not to depend on two different parsers, especially when the TS one is already running on everything. |
With the updated code I believe the only known outstanding issues are:
Maybe I'm misunderstanding your statement but it's not always that simple, especially for handwritten CJS.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good example with semver
. The main thing I was trying to avoid was a bunch of errors of the opposite problem: runtime exports that don’t exist in the types. But since we’re only reporting types exports that don’t exist at runtime, I take your point that it can be useful to list them, to a point. I do think the CLI / web UI shouldn’t vomit a list of hundreds of exports (even where console.log(missing)
truncates the lodash list looks excessive), and it is sometimes the case that all named exports are missing, in which case it doesn’t seem useful to list anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, we model Node.js/bundler resolution with ts.resolveModuleName
ignoring declaration files, which is not fully accurate. It’s been on my mental list for a while to replace that with enhanced-resolve, which would fix a few edge cases as well as letting me make the bundler JS resolution be a little more bundler-like, e.g. resolving the top-level module
field in package.json. If you had access to enhanced-resolve
here, would you still need cjsResolve.ts
and esmResolve.ts
for any reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't aware of enhanced-resolve
, that's good to know. It has some inaccuracies (require('./file.js?')
should not resolve since require
accepts filesystem paths, not url fragments). I don't think there's any issues which would be showstoppers though. I'll take a look at integrating that module instead.
My instinct is to avoid "bundler-like" behavior because that's gotten us into this anarchy where we have a dozen different resolution algorithms. I found out a few weeks ago that bun consults paths
from tsconfig.json
during runtime to guide its resolution algorithm and it ruined my day.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's gotten us into this anarchy where we have a dozen different resolution algorithms
100%. But the point of modeling it in this tool would be to point out the cases where that causes concrete problems. TS itself doesn’t have any of those behaviors, and since I’m currently using TS to try to show where type resolution and bundler resolution disagree, there are false negatives. If we were to switch the bundler resolution to be more bundler-y, new problems could be exposed where we’re pretty sure the bundler is going to resolve to one thing and TS is going to resolve to something that doesn’t match. At any rate, I don’t expect that to be part of this PR, just pointing out that the dependency might be able to save on a lot of local code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a look at swapping over to enhanced-resolve
. A big problem is that it doesn't report module format (esm, cjs, wasm, etc) from the resolution, it only reports the resolved path. So we would need to perform an additional step to infer the module format on our own. Webpack obviously wouldn't care about this information because they can operate in a cjs + esm superset mode but this is the exact condition we're trying to report.
Also they require a fully contravariant implementation of basically the entire node:fs
surface area (see: types.d.ts). Obviously the correct way to approach these types would be what you've done in this package and define an expected subset of functionality. So it's any
-town, U.S.A. trying to shim Package
into enhanced-resolve
.
If I hadn't already gone through the legwork of implementing the specified algorithms then I'd probably just go with enhanced-resolve
and hold my nose through the ick. But integrating it now I think makes the PR strictly worse. If you insist I can drop it in though. It would reduce our LOC so that is an advantage.
Correct, a runtime symbol which isn't typed is not a problem. This is fairly common as well, react's
This is probably by far the more common case and worth calling out specifically. Perhaps |
I have some ideas for future UI that would let you drill into individual problem occurrences and see details, generated from problem object fields other than |
I think this is pretty much ready to go, but I screwed up the conflict resolution in the GitHub UI and can’t seem to push a merge commit from the CLI. I’m going to merge this into a staging branch, fix up the lockfile, and figure out the test failures. I’ll open a new PR into main and tag you in it when it’s ready to go. Thanks so much for all your work! ❤️ |
This implements the idea in #35. The patch is incomplete in some ways:
chalk
reportsModifierName
(and others) are missing, but these are type aliases. I can't figure out the correct predicate to only raise runtime types.*
namespaces from other packages, for example@reduxjs/toolkit
(see here). If we want to check these then the module's dependencies will need to be downloaded too. This would be a major change, or we can just bail the check in this case.package.json
in their entries. The TypeScript compiler advertises that the top-level keys of this JSON file are importable when they are not.import { name } from "react/package.json" assert { type: "json" };
will pass compilation but fail to evaluate under nodejs. I'm surprised that even under the strictest modern settings (target: esnext, verbatimModuleSyntax: true, module: nodenext, moduleResolution: nodenext) that TypeScript has this behavior. Filtering JSON entries would probably be fine.acorn
part could be rewritten to use the TypeScript AST instead. This thought eluded me until I had already finished the implementation.The result of
getEsmModuleNamespace
should be the same as the result ofObject.keys(await import(moduleName))
. This information is then compared against the advertised types and an error is raised if the types are incorrect under node16-esm resolution mode (there are many, many problems in the npm ecosystem).The static analysis had to be done outside of TypeScript since its behavior does not match the reality of what nodejs sees. TypeScript would have you believe that the following program is well-formed but it cannot be evaluated under nodejs:
Example including diagnostic output from
request
andlodash
, which do not actually export any of the names that their types would have you believe: