Closed laverdet closed 1 week ago
Latest commit: 9a62a49718509ea61f78af76f0ebeeb1298e69b9
Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.
Click here to learn what changesets are, and how to add one.
Click here if you're a maintainer who wants to add a changeset to this PR
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.
Some type-only exports are not correctly filtered by my check. For example chalk reports ModifierName (and others) are missing, but these are type aliases. I can't figure out the correct predicate to only raise runtime types.
I can help with this when the rest appears to be in good shape.
Some packages reexport * 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.
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.
Filtering JSON entries would probably be fine.
Agreed π
The acorn part could be rewritten to use the TypeScript AST instead. This thought eluded me until I had already finished the implementation.
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:
cjs-module-lexer
bailouts: "default" function with bag of fake named exports (see: request
), entirely unlexable (see: body-parser
), partial bailout (see: semver
). These all affect handwritten CommonJS modules. This rule would also catch non-bailout conditions where the handwritten TypeScript declarations are simply incorrect.Overall, Iβd like to present this check to the user as more of an all-or-nothing thing
Maybe I'm misunderstanding your statement but it's not always that simple, especially for handwritten CJS. semver
is a very good case study. They have a cjs-module-lexer
bailout in their "index" file. This causes all named exports after that line to be missing. The nuanced diagnostic information here is helpful.
$ git diff
diff --git a/packages/core/src/internal/checks/namedExports.ts b/packages/core/src/internal/checks/namedExports.ts
index 5b0dbc1..35b6cdf 100644
--- a/packages/core/src/internal/checks/namedExports.ts
+++ b/packages/core/src/internal/checks/namedExports.ts
@@ -50,6 +50,7 @@ export default defineCheck({
})();
if (exports) {
const missing = expectedNames.filter((name) => !exports.includes(String(name))).map(String);
+ console.log(missing);
if (missing.length > 0) {
return {
kind: "NamedExports",
$ node ./packages/cli/dist/index.js -p semver
[
'compareIdentifiers',
'rcompareIdentifiers',
'SEMVER_SPEC_VERSION',
'RELEASE_TYPES'
]
semver v7.6.2
@types/semver v7.5.8
π΅οΈ Module advertises named ESM exports which will not exist at runtime. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NamedExports.md
βββββββββββββββββββββ¬βββββββββββββββββββββββ
β β "semver" β
βββββββββββββββββββββΌβββββββββββββββββββββββ€
β node10 β π’ β
βββββββββββββββββββββΌβββββββββββββββββββββββ€
β node16 (from CJS) β π’ (CJS) β
βββββββββββββββββββββΌβββββββββββββββββββββββ€
β node16 (from ESM) β π΅οΈ Named ESM exports β
βββββββββββββββββββββΌβββββββββββββββββββββββ€
β bundler β π’ β
βββββββββββββββββββββ΄βββββββββββββββββββββββ
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.
Correct, a runtime symbol which isn't typed is not a problem. This is fairly common as well, react's __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
comes to mind. Or any project which uses stripInternal: true
. I would not recommend raising an issue for this condition.
it is sometimes the case that all named exports are missing, in which case it doesnβt seem useful to list anything
This is probably by far the more common case and worth calling out specifically. Perhaps problemKindInfo
could be a Record of functions which accept a Problem
parameter and in turn let you enhance the reported diagnostics with information from the problem?
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 kind
. The summary text is intended to be unspecific so that each problem summary appears at most one time. For the sake of ensuring your work gets merged, I think the best strategy is not to try to implement something like that now, but to expose the list of missing named exports in the --json
output (which is also viewable in the web UI) as youβve already done.
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: