Skip to content

Commit 9f6ec63

Browse files
committed
Cleaner path splitting, refine file extension and case sensitivity handling
1 parent 98b14e3 commit 9f6ec63

2 files changed

Lines changed: 98 additions & 19 deletions

File tree

src/services/codefixes/importFixes.ts

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -504,55 +504,54 @@ namespace ts.codefix {
504504
return undefined;
505505
}
506506

507-
const indexOfTopNodeModules = moduleFileName.indexOf("node_modules");
508-
if (indexOfTopNodeModules < 0) {
507+
const parts = getNodeModulePathParts(moduleFileName);
508+
509+
if (!parts) {
509510
return undefined;
510511
}
511512

512513
// Simplify the full file path to something that can be resolved by Node.
513-
// First remove the extension
514-
let moduleSpecifier = removeFileExtension(moduleFileName);
514+
515515
// If the module could be imported by a directory name, use that directory's name
516-
moduleSpecifier = getDirectoryOrFileName(moduleSpecifier);
516+
let moduleSpecifier = getDirectoryOrExtensionlessFileName(moduleFileName);
517517
// Get a path that's relative to node_modules or the importing file's path
518518
moduleSpecifier = getNodeResolvablePath(moduleSpecifier);
519-
// If the module was found in @types, get the actual node package name
519+
// If the module was found in @types, get the actual Node package name
520520
return getPackageNameFromAtTypesDirectory(moduleSpecifier);
521521

522-
function getDirectoryOrFileName(fullModulePathWithoutExtension: string): string {
522+
function getDirectoryOrExtensionlessFileName(path: string): string {
523523
// If the file is the main module, it can be imported by the package name
524-
const indexOfLastNodeModules = moduleFileName.lastIndexOf("node_modules");
525-
const indexOfSlashAtPackageRoot = moduleFileName.indexOf("/", indexOfLastNodeModules + 13 /* "node_modules\".length */);
526-
const packageRootPath = moduleFileName.substring(0, indexOfSlashAtPackageRoot);
524+
const packageRootPath = path.substring(0, parts.packageRootIndex);
527525
const packageJsonPath = combinePaths(packageRootPath, "package.json");
528526
if (context.host.fileExists(packageJsonPath)) {
529527
const packageJsonContent = JSON.parse(context.host.readFile(packageJsonPath));
530528
if (packageJsonContent) {
531529
const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main;
532530
if (mainFileRelative) {
533531
const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName);
534-
if (removeFileExtension(mainExportFile) === removeFileExtension(moduleFileName)) {
532+
if (mainExportFile === getCanonicalFileName(path)) {
535533
return packageRootPath;
536534
}
537535
}
538536
}
539537
}
540538

541-
// If the file is index.js, it can be imported by its directory name
542-
if (endsWith(fullModulePathWithoutExtension, "/index")) {
543-
return getDirectoryPath(fullModulePathWithoutExtension);
539+
// We still have a file name - remove the extension
540+
const fullModulePathWithoutExtension = removeFileExtension(path);
541+
542+
// If the file is /index, it can be imported by its directory name
543+
if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index") {
544+
return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex);
544545
}
545546

546547
return fullModulePathWithoutExtension;
547548
}
548549

549550
function getNodeResolvablePath(path: string): string {
550-
const fullPathUptoNodeModules = moduleFileName.substring(0, indexOfTopNodeModules - 1);
551-
if (sourceDirectory.indexOf(fullPathUptoNodeModules) === 0) {
552-
const indexOfTopPackageName = indexOfTopNodeModules + 13 /* "node_modules\".length */;
551+
const basePath = path.substring(0, parts.topLevelNodeModulesIndex);
552+
if (sourceDirectory.indexOf(basePath) === 0) {
553553
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
554-
const relativeToTopNodeModules = path.substring(indexOfTopPackageName);
555-
return relativeToTopNodeModules;
554+
return path.substring(parts.topLevelPackageNameIndex + 1);
556555
}
557556
else {
558557
return getRelativePath(path, sourceDirectory);
@@ -561,6 +560,57 @@ namespace ts.codefix {
561560
}
562561
}
563562

563+
function getNodeModulePathParts(fullPath: string) {
564+
// If fullPath can't be valid module file within node_modules, returns undefined.
565+
// Example of expected pattern: /base/path/node_modules/[otherpackage/node_modules/]package/[subdirectory/]file.js
566+
// Returns indices: ^ ^ ^ ^
567+
568+
let topLevelNodeModulesIndex = 0;
569+
let topLevelPackageNameIndex = 0;
570+
let packageRootIndex = 0;
571+
let fileNameIndex = 0;
572+
573+
const enum States {
574+
BeforeNodeModules,
575+
NodeModules,
576+
PackageContent
577+
}
578+
579+
let partStart = 0;
580+
let partEnd = 0;
581+
let state = States.BeforeNodeModules;
582+
583+
while (partEnd >= 0) {
584+
partStart = partEnd;
585+
partEnd = fullPath.indexOf("/", partStart + 1);
586+
switch (state) {
587+
case States.BeforeNodeModules:
588+
if (fullPath.indexOf("/node_modules/", partStart) === partStart) {
589+
topLevelNodeModulesIndex = partStart;
590+
topLevelPackageNameIndex = partEnd;
591+
state = States.NodeModules;
592+
}
593+
break;
594+
case States.NodeModules:
595+
packageRootIndex = partEnd;
596+
state = States.PackageContent;
597+
break;
598+
case States.PackageContent:
599+
if (fullPath.indexOf("/node_modules/", partStart) === partStart) {
600+
state = States.NodeModules;
601+
}
602+
else {
603+
state = States.PackageContent;
604+
}
605+
break;
606+
}
607+
}
608+
609+
fileNameIndex = partStart;
610+
611+
return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined;
612+
}
613+
564614
function getPathRelativeToRootDirs(path: string, rootDirs: string[]) {
565615
for (const rootDir of rootDirs) {
566616
const relativeName = getRelativePathIfInDirectory(path, rootDir);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// [|f1/*0*/('');|]
4+
5+
// @Filename: package.json
6+
//// { "dependencies": { "package-name": "0.0.1" } }
7+
8+
// @Filename: node_modules/package-name/bin/lib/libfile.d.ts
9+
//// export declare function f1(text: string): string;
10+
11+
// @Filename: node_modules/package-name/bin/lib/libfile.js
12+
//// function f1(text) {}
13+
//// exports.f1 = f1;
14+
15+
// @Filename: node_modules/package-name/package.json
16+
//// { "main": "bin/lib/libfile.js" }
17+
18+
19+
// In this case, importing the module by its package name:
20+
// import { f1 } from 'package-name'
21+
// could in theory work, however the resulting code compiles with a module resolution error
22+
// since bin/lib/libfile.d.ts isn't declared under "typings" in package.json
23+
// Therefore just import the module by its qualified path
24+
25+
verify.importFixAtPosition([
26+
`import { f1 } from "package-name/bin/lib/libfile";
27+
28+
f1('');`
29+
]);

0 commit comments

Comments
 (0)