-
Notifications
You must be signed in to change notification settings - Fork 4
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
Initial import/export/self-name support #68
Conversation
cea73a2
to
093002c
Compare
…ptimize import name format detection by relying on parents being set
… import when choosing if they provide extensions
|
||
function getUnderlyingCacheKey(specifier: string, mode: ModuleKind.CommonJS | ModuleKind.ESNext | undefined) { | ||
const result = mode === undefined ? specifier : `${mode}|${specifier}`; | ||
memoizedReverseKeys.set(result, [specifier, mode]); |
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’m still nervous about every cache access now also doing a set in another map. Would it at least be possible to skip this under the old module resolution settings? In those cases, mode
will always be undefined
, so forEach
wouldn’t need to do the lookup and therefore we wouldn’t need to do the set?
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.
No? Then I'd need the compiler options in the cache key/arguments and I'd need to take that in as a parameter and switch on that and... It doesn't really change much. Plus, I really don't think this is going to matter much - this doesn't increase the complexity of the operation at all (there's no extra iteration being done) and only increases the memory usage by the size of a few pointers per element. This is highly unlikely to be any kind of bottleneck, imo.
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.
Then I'd need the compiler options in the cache key
The cache can be used across different programs?
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 should be able to test this with --extendedDiagnostics
... I don’t think our perf suite reports module resolution time (or program construction time) directly, IIRC.
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.
The cache can be used across different programs?
Right, exactly - I'd be trading the one thing I needed a reverse lookup cache for for two (since it's not like I can guarantee the new resolution modes won't ever be used in a context prior to cache construction). It wouldn't really help - the key is already minimal.
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 won't block on the map thing, it's something that can be optimized later if needed
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.
Here's what I have so far. Nothing substantive since I've just started on the bulk of the new stuff in moduleNameResolver.
Also, my two primary work computers are only able to display lines about half as long as the longest length. Please break up the lines more! In the meantime I guess I can open the review on two screens at once or something.
); | ||
} | ||
|
||
function nodeNextModuleNameResolverWorker(features: NodeResolutionFeatures, moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): ResolvedModuleWithFailedLookupLocations { |
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.
why does node12 delegate to nodeNext...Worker instead of node...Worker or something?
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.
nodeNext...Worker
sets the appropriate EsmMode
Feature flag using the resolutionMode
parameter before calling into node...Worker
(which no longer uses resolutionMode
).
} | ||
|
||
return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); | ||
} |
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.
can you move this closer to its usage, after getLoadModuleFromTargetImportOrExport?
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.
Its primary usage is immediately above this in loadFromFile
(which is in turn already used all over) - in getLoadModuleFromTargetImportOrExport
it's a secondary, new usage?
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.
loadJSOrExactTSFileName looks like it's new in the review. I think I put my comment on a confusing line that indicated a different function.
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 haven't looked at the tests yet, but the rest of the code looks like it matches what we've discussed in various meeting (as well as I understand it). So far I just have a couple of questions and readability suggestions, nothing substantive.
} | ||
|
||
return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); | ||
} |
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.
loadJSOrExactTSFileName looks like it's new in the review. I think I put my comment on a confusing line that indicated a different function.
function loadModuleFromImportsOrExports(extensions: Extensions, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined, moduleName: string, lookupTable: object, scope: PackageJsonInfo, isImports: boolean): SearchResult<Resolved> | undefined { | ||
const loadModuleFromTargetImportOrExport = getLoadModuleFromTargetImportOrExport(extensions, state, cache, redirectedReference, moduleName, scope, isImports); | ||
|
||
if (!endsWith(moduleName, directorySeparator) && moduleName.indexOf("*") === -1 && hasProperty(lookupTable, moduleName)) { |
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.
lookupTable is cast to {[x: string]: unknown}
4 out of 5 times. Maybe that should be its type and the callers should cast? Or maybe the cast should happen once at the beginning?
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.
Nah - it's possibly null
or a string
earlier on as well - we just don't have a "dictionary-like object" guard, so there's casts.
return toSearchResult(/*value*/ undefined); | ||
} | ||
const useCaseSensitiveFileNames = typeof state.host.useCaseSensitiveFileNames === "function" ? state.host.useCaseSensitiveFileNames() : state.host.useCaseSensitiveFileNames; | ||
const directoryPath = toPath(combinePaths(directory, "dummy"), state.host.getCurrentDirectory?.(), createGetCanonicalFileName(useCaseSensitiveFileNames === undefined ? true : useCaseSensitiveFileNames)); |
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.
why is 'dummy'
added after directory
?
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.
Because the function expects a file in the directory for the path and not a directory path - the first thing it does is pop off that last segment. We do similar elsewhere in the resolver.
const target = (lookupTable as {[idx: string]: unknown})[moduleName]; | ||
return loadModuleFromTargetImportOrExport(target, /*subpath*/ "", /*pattern*/ false); | ||
} | ||
const expandingKeys = sort(filter(getOwnKeys(lookupTable as MapLike<unknown>), k => k.indexOf("*") !== -1 || endsWith(k, "/")), (a, b) => a.length - b.length); |
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.
can we use String.includes/String.endsWith yet? MDN says ES2015. (and we use startsWith/endsWith in the checker)
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.
Our built-in endsWith
function uses string.prototype.endsWith
if it's available.
} | ||
} | ||
|
||
function matchesPatternWithTrailer(target: string, name: string) { |
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.
-
This would be easier to read if all 3 cases moved their detection AND extraction code into this function, and the containing loop's body was something like
loadModuleFromTargetImportOrExport(...matchesPatternWithTrailer(lookupTable, potentialTarget))
. -
matchesPatternWithTrailer's code could be easier to read if it handled cases in order ['fixed', 'trailing*', 'pattern*trailer'].
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.
Most of the current export/import map lookup structure is structured like it is because it matches how the node spec says it should be structured - departing too much from that is going to make it harder to update or debug future changes, I think.
} | ||
} | ||
} | ||
else if (target === null) { // eslint-disable-line no-null/no-null |
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.
you could drop the target !== null
above if you move this inside the typeof target === 'object'
branch
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.
See prior comment on "it's this order because the node resolver spec days it's in this order".
if (!startsWith(key, "types@")) return false; | ||
const range = VersionRange.tryParse(key.substring("types@".length)); | ||
if (!range) return false; | ||
return range.test(version); |
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.
where is version declared?
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.
In core.ts
. I'd normally write ts.version
to clarify, but our lint settings don't like that, so... 🤷
return isImportCall(walkUpParenthesizedExpressions(usage.parent)) ? ModuleKind.ESNext : ModuleKind.CommonJS; | ||
} | ||
// in esm files, import=require statements are cjs format, otherwise everything is esm | ||
// imports are only parent'd up to their containing declaration/expression, so access farther parents with care |
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.
👍 for this comment
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.
The "imports are parent's up to their containing declaration" is a new change I made, btw, because it was hideously complicated to calculate this without those parent pointers (I did for awhile - it was expensive and bug prone - better to just set the parents when the imports are collected unconditionally).
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.
Just a couple of ideas for better wording. I'm not sure they're improvements however.
// These should _mostly_ work - `import = require` always desugars to require calls, which do have extension and index resolution (but can't load anything that resolves to esm!) | ||
import m24 = require("./"); | ||
~~~~ | ||
!!! error TS1471: Module './' cannot be imported using this construct. The specifier only resolves to an es module, which cannot be imported synchronously. Use dynamic import instead. |
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.
It might be better to uppercase ES in the error. I'm not sure.
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 requested this change on a different error message in one of the other PRs. Let’s use capital ES
throughout.
import * as cjsi from "inner/cjs"; | ||
import * as mjsi from "inner/mjs"; | ||
~~~~~~~~~~~ | ||
!!! error TS1471: Module 'inner/mjs' cannot be imported using this construct. The specifier only resolves to an es module, which cannot be imported synchronously. Use dynamic import instead. |
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.
'statement' reads better than 'construct' here -- it's more specific and common.
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 use construct
because it also applies to cjs require, which is an expression, and having two error messages just to distinguish statement vs expression seems gratuitous.
Merged in microsoft#45884 |
This is the final part of the
node12
module resolution work - the new resolver itself. It has support for packageexports
,imports
and package self-name lookup, in addition to implementing the rules for both the cjs and esm resolution modes (eg, noindex
or automatic extension resolution inesm
flavored imports).Also containing extensive refactoring around our resolution caching infrastructure to handle how the same import (eg,
"./foo"
could be resolved in one of two modes (the cjs or esm resolution mode), and can have separate resolution results for both).TODO:
*
patterns with packageimports
- should work, just no tests to that effect)node12
-only features ( module: deprecate trailing slash pattern mappings nodejs/node#40039 )typesVersions
conditions and tests (an unversionedtypes
condition is already supported) per the conclusion in Design Meeting Notes, 5/14/2021 microsoft/TypeScript#44097