Initial import/export/self-name support#68
Initial import/export/self-name support#68weswigham wants to merge 14 commits intomjscjs-supportfrom
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.
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.
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.
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.
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.
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.
andrewbranch
left a comment
There was a problem hiding this comment.
I won't block on the map thing, it's something that can be optimized later if needed
sandersn
left a comment
There was a problem hiding this comment.
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.
why does node12 delegate to nodeNext...Worker instead of node...Worker or something?
There was a problem hiding this comment.
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.
can you move this closer to its usage, after getLoadModuleFromTargetImportOrExport?
There was a problem hiding this comment.
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.
loadJSOrExactTSFileName looks like it's new in the review. I think I put my comment on a confusing line that indicated a different function.
sandersn
left a comment
There was a problem hiding this comment.
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.
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.
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.
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.
why is 'dummy' added after directory?
There was a problem hiding this comment.
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.
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.
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.
-
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.
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.
you could drop the target !== null above if you move this inside the typeof target === 'object' branch
There was a problem hiding this comment.
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.
where is version declared?
There was a problem hiding this comment.
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.
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).
sandersn
left a comment
There was a problem hiding this comment.
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.
It might be better to uppercase ES in the error. I'm not sure.
There was a problem hiding this comment.
👍 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.
'statement' reads better than 'construct' here -- it's more specific and common.
There was a problem hiding this comment.
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
node12module resolution work - the new resolver itself. It has support for packageexports,importsand package self-name lookup, in addition to implementing the rules for both the cjs and esm resolution modes (eg, noindexor automatic extension resolution inesmflavored 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 )typesVersionsconditions and tests (an unversionedtypescondition is already supported) per the conclusion in Design Meeting Notes, 5/14/2021 microsoft/TypeScript#44097