From c26182ef1b005d36dc7b9e17b3a602adb6787e0c Mon Sep 17 00:00:00 2001
From: Gabriel Cangussu <gabrielcangussu@gmail.com>
Date: Mon, 31 Aug 2020 19:37:12 -0300
Subject: [PATCH 1/3] fix: import with .js and .jsx file extension

---
 package.json                           |  3 +-
 src/index.ts                           | 45 ++++++++++++-
 tests/withJsExtension/.eslintrc.js     | 18 ++++++
 tests/withJsExtension/bar/index.tsx    |  1 +
 tests/withJsExtension/dtsImportee.d.ts |  3 +
 tests/withJsExtension/foo.js/index.ts  |  1 +
 tests/withJsExtension/foo.jsx/index.ts |  1 +
 tests/withJsExtension/foo/index.ts     |  1 +
 tests/withJsExtension/index.ts         | 33 ++++++++++
 tests/withJsExtension/test.js          | 88 ++++++++++++++++++++++++++
 tests/withJsExtension/tsImportee.ts    |  1 +
 tests/withJsExtension/tsconfig.json    | 10 +++
 tests/withJsExtension/tsxImportee.tsx  |  1 +
 13 files changed, 202 insertions(+), 4 deletions(-)
 create mode 100644 tests/withJsExtension/.eslintrc.js
 create mode 100644 tests/withJsExtension/bar/index.tsx
 create mode 100644 tests/withJsExtension/dtsImportee.d.ts
 create mode 100644 tests/withJsExtension/foo.js/index.ts
 create mode 100644 tests/withJsExtension/foo.jsx/index.ts
 create mode 100644 tests/withJsExtension/foo/index.ts
 create mode 100644 tests/withJsExtension/index.ts
 create mode 100644 tests/withJsExtension/test.js
 create mode 100644 tests/withJsExtension/tsImportee.ts
 create mode 100644 tests/withJsExtension/tsconfig.json
 create mode 100644 tests/withJsExtension/tsxImportee.tsx

diff --git a/package.json b/package.json
index 36ab10f..0217eb4 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
     "test": "run-p test:*",
     "test:multipleEslintrcs": "eslint --ext ts,tsx tests/multipleEslintrcs",
     "test:multipleTsconfigs": "eslint --ext ts,tsx tests/multipleTsconfigs",
+    "test:withJsExtension": "node tests/withJsExtension/test.js && eslint --ext ts,tsx tests/withJsExtension",
     "test:withPaths": "eslint --ext ts,tsx tests/withPaths",
     "test:withoutPaths": "eslint --ext ts,tsx tests/withoutPaths",
     "type-coverage": "type-coverage --cache --detail --ignore-catch --strict --update"
@@ -77,6 +78,6 @@
     "prettier": "^2.0.5"
   },
   "typeCoverage": {
-    "atLeast": 98.73
+    "atLeast": 99.27
   }
 }
diff --git a/src/index.ts b/src/index.ts
index 8260b1d..0ea9425 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,7 +8,7 @@ import {
 } from 'tsconfig-paths'
 import { sync as globSync } from 'glob'
 import isGlob from 'is-glob'
-import { isCore, sync } from 'resolve'
+import { isCore, sync, SyncOpts } from 'resolve'
 import debug from 'debug'
 
 const IMPORTER_NAME = 'eslint-import-resolver-typescript'
@@ -70,7 +70,7 @@ export function resolve(
   // note that even if we map the path, we still need to do a final resolve
   let foundNodePath: string | null | undefined
   try {
-    foundNodePath = sync(mappedPath || source, {
+    foundNodePath = tsResolve(mappedPath || source, {
       extensions: options.extensions || defaultExtensions,
       basedir: path.dirname(path.resolve(file)),
       packageFilter: options.packageFilter || packageFilterDefault,
@@ -120,6 +120,27 @@ function packageFilterDefault(pkg: Record<string, string>) {
   return pkg
 }
 
+/**
+ * Like `sync` from `resolve` package, but considers that the module id
+ * could have a .js or .jsx extension.
+ */
+function tsResolve(id: string, opts?: SyncOpts): string {
+  try {
+    return sync(id, opts)
+  } catch (error) {
+    const idWithoutJsExt = removeJsExtension(id)
+    if (idWithoutJsExt !== id) {
+      return sync(idWithoutJsExt, opts)
+    }
+    throw error
+  }
+}
+
+/** Remove .js or .jsx extension from module id. */
+function removeJsExtension(id: string) {
+  return id.replace(/\.jsx?$/, '')
+}
+
 let mappersBuildForOptions: TsResolverOptions
 let mappers:
   | Array<(source: string, file: string) => string | undefined>
@@ -142,6 +163,24 @@ function getMappedPath(source: string, file: string) {
   return paths[0]
 }
 
+/**
+ * Like `createMatchPath` from `tsconfig-paths` package, but considers
+ * that the module id could have a .js or .jsx extension.
+ */
+const createExtendedMatchPath: typeof createMatchPath = (
+  absoluteBaseUrl,
+  paths,
+  ...rest
+) => {
+  const matchPath = createMatchPath(absoluteBaseUrl, paths, ...rest)
+
+  return (id, ...otherArgs) => {
+    const match = matchPath(id, ...otherArgs)
+    if (match != null) return match
+    return matchPath(removeJsExtension(id), ...otherArgs)
+  }
+}
+
 function initMappers(options: TsResolverOptions) {
   if (mappers && mappersBuildForOptions === options) {
     return
@@ -175,7 +214,7 @@ function initMappers(options: TsResolverOptions) {
     // eslint-disable-next-line unicorn/no-fn-reference-in-iterator
     .filter(isConfigLoaderSuccessResult)
     .map(configLoaderResult => {
-      const matchPath = createMatchPath(
+      const matchPath = createExtendedMatchPath(
         configLoaderResult.absoluteBaseUrl,
         configLoaderResult.paths,
       )
diff --git a/tests/withJsExtension/.eslintrc.js b/tests/withJsExtension/.eslintrc.js
new file mode 100644
index 0000000..47a5000
--- /dev/null
+++ b/tests/withJsExtension/.eslintrc.js
@@ -0,0 +1,18 @@
+/* eslint-env node */
+/* eslint-disable @typescript-eslint/no-var-requires */
+const config = require('../baseEslintConfig')(__dirname)
+
+module.exports = {
+  ...config,
+  rules: {
+    ...config.rules,
+    'import/extensions': [
+      2,
+      'ignorePackages',
+      {
+        ts: 'never',
+        tsx: 'never',
+      },
+    ],
+  },
+}
diff --git a/tests/withJsExtension/bar/index.tsx b/tests/withJsExtension/bar/index.tsx
new file mode 100644
index 0000000..4548a26
--- /dev/null
+++ b/tests/withJsExtension/bar/index.tsx
@@ -0,0 +1 @@
+export default 'bar'
diff --git a/tests/withJsExtension/dtsImportee.d.ts b/tests/withJsExtension/dtsImportee.d.ts
new file mode 100644
index 0000000..e645900
--- /dev/null
+++ b/tests/withJsExtension/dtsImportee.d.ts
@@ -0,0 +1,3 @@
+declare const content: 'yes'
+
+export default content
diff --git a/tests/withJsExtension/foo.js/index.ts b/tests/withJsExtension/foo.js/index.ts
new file mode 100644
index 0000000..6833cda
--- /dev/null
+++ b/tests/withJsExtension/foo.js/index.ts
@@ -0,0 +1 @@
+export default 'foo.js'
diff --git a/tests/withJsExtension/foo.jsx/index.ts b/tests/withJsExtension/foo.jsx/index.ts
new file mode 100644
index 0000000..00001a5
--- /dev/null
+++ b/tests/withJsExtension/foo.jsx/index.ts
@@ -0,0 +1 @@
+export default 'foo.jsx'
diff --git a/tests/withJsExtension/foo/index.ts b/tests/withJsExtension/foo/index.ts
new file mode 100644
index 0000000..7e942cf
--- /dev/null
+++ b/tests/withJsExtension/foo/index.ts
@@ -0,0 +1 @@
+export default 'foo'
diff --git a/tests/withJsExtension/index.ts b/tests/withJsExtension/index.ts
new file mode 100644
index 0000000..5406006
--- /dev/null
+++ b/tests/withJsExtension/index.ts
@@ -0,0 +1,33 @@
+// import relative
+import './tsImportee.js'
+import './tsxImportee.jsx'
+import './dtsImportee.js'
+import './dtsImportee.jsx'
+import './foo'
+import './foo.js'
+import './foo.jsx'
+import './bar'
+
+// import using tsconfig.json path mapping
+import '#/tsImportee.js'
+import '#/tsxImportee.jsx'
+import '#/dtsImportee.js'
+import '#/dtsImportee.jsx'
+import '#/foo'
+import '#/foo.js'
+import '#/foo.jsx'
+import '#/bar'
+
+// import using tsconfig.json base url
+import 'tsImportee.js'
+import 'tsxImportee.jsx'
+import 'dtsImportee.js'
+import 'dtsImportee.jsx'
+import 'foo'
+import 'foo.js'
+import 'foo.jsx'
+import 'bar'
+
+// import from node_module
+import 'typescript/lib/typescript.js'
+import 'dummy.js'
diff --git a/tests/withJsExtension/test.js b/tests/withJsExtension/test.js
new file mode 100644
index 0000000..7fb8b42
--- /dev/null
+++ b/tests/withJsExtension/test.js
@@ -0,0 +1,88 @@
+/* eslint-env node */
+/* eslint-disable @typescript-eslint/no-var-requires */
+
+const path = require('path')
+const assert = require('assert')
+
+const { resolve } = require('../../')
+
+const config = {
+  project: path.join(__dirname, 'tsconfig.json'),
+}
+
+const file = path.join(__dirname, 'index.ts')
+
+function assertResolve(id, relativePath) {
+  const filePath = path.join(__dirname, relativePath)
+  assert.deepStrictEqual(resolve(id, file, config), {
+    found: true,
+    path: filePath,
+  })
+  assert.deepStrictEqual(
+    resolve(id, file, { ...config, alwaysTryTypes: true }),
+    { found: true, path: filePath },
+  )
+}
+
+// import relative
+
+assertResolve('./tsImportee.js', 'tsImportee.ts')
+
+assertResolve('./tsxImportee.jsx', 'tsxImportee.tsx')
+
+assertResolve('./dtsImportee.js', 'dtsImportee.d.ts')
+
+assertResolve('./dtsImportee.jsx', 'dtsImportee.d.ts')
+
+assertResolve('./foo', 'foo/index.ts')
+
+assertResolve('./foo.js', 'foo.js/index.ts')
+
+assertResolve('./foo.jsx', 'foo.jsx/index.ts')
+
+assertResolve('./bar', 'bar/index.tsx')
+
+// import using tsconfig.json path mapping
+
+assertResolve('#/tsImportee.js', 'tsImportee.ts')
+
+assertResolve('#/tsxImportee.jsx', 'tsxImportee.tsx')
+
+assertResolve('#/dtsImportee.js', 'dtsImportee.d.ts')
+
+assertResolve('#/dtsImportee.jsx', 'dtsImportee.d.ts')
+
+assertResolve('#/foo', 'foo/index.ts')
+
+assertResolve('#/foo.js', 'foo.js/index.ts')
+
+assertResolve('#/foo.jsx', 'foo.jsx/index.ts')
+
+assertResolve('#/bar', 'bar/index.tsx')
+
+// import using tsconfig.json base url
+
+assertResolve('tsImportee.js', 'tsImportee.ts')
+
+assertResolve('tsxImportee.jsx', 'tsxImportee.tsx')
+
+assertResolve('dtsImportee.js', 'dtsImportee.d.ts')
+
+assertResolve('dtsImportee.jsx', 'dtsImportee.d.ts')
+
+assertResolve('foo', 'foo/index.ts')
+
+assertResolve('foo.js', 'foo.js/index.ts')
+
+assertResolve('foo.jsx', 'foo.jsx/index.ts')
+
+assertResolve('bar', 'bar/index.tsx')
+
+// import from node_module
+
+assertResolve(
+  'typescript/lib/typescript.js',
+  '../../node_modules/typescript/lib/typescript.js',
+)
+
+assertResolve('dummy.js', '../../node_modules/dummy.js/index.js')
diff --git a/tests/withJsExtension/tsImportee.ts b/tests/withJsExtension/tsImportee.ts
new file mode 100644
index 0000000..c824311
--- /dev/null
+++ b/tests/withJsExtension/tsImportee.ts
@@ -0,0 +1 @@
+export default 'tsImportee.ts'
diff --git a/tests/withJsExtension/tsconfig.json b/tests/withJsExtension/tsconfig.json
new file mode 100644
index 0000000..13d6d9f
--- /dev/null
+++ b/tests/withJsExtension/tsconfig.json
@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "jsx": "react",
+    "baseUrl": "./",
+    "paths": {
+      "#/*": ["*"]
+    }
+  },
+  "includes": ["./**/*"]
+}
diff --git a/tests/withJsExtension/tsxImportee.tsx b/tests/withJsExtension/tsxImportee.tsx
new file mode 100644
index 0000000..2a39b98
--- /dev/null
+++ b/tests/withJsExtension/tsxImportee.tsx
@@ -0,0 +1 @@
+export default 'tsxImportee.tsx'

From 54eb91b4faff1bbf310c390fd554e3990844d4a6 Mon Sep 17 00:00:00 2001
From: Gabriel Cangussu <gabrielcangussu@gmail.com>
Date: Tue, 1 Sep 2020 08:19:34 -0300
Subject: [PATCH 2/3] fix: check if id changed before resolving again

---
 src/index.ts | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/index.ts b/src/index.ts
index 0ea9425..a99ae8f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -177,7 +177,11 @@ const createExtendedMatchPath: typeof createMatchPath = (
   return (id, ...otherArgs) => {
     const match = matchPath(id, ...otherArgs)
     if (match != null) return match
-    return matchPath(removeJsExtension(id), ...otherArgs)
+
+    const idWithoutJsExt = removeJsExtension(id)
+    if (idWithoutJsExt !== id) {
+      return matchPath(idWithoutJsExt, ...otherArgs)
+    }
   }
 }
 

From b99425ea2e4032e91da278af5f6b7070895a5986 Mon Sep 17 00:00:00 2001
From: Gabriel Cangussu <gabrielcangussu@gmail.com>
Date: Tue, 1 Sep 2020 08:21:21 -0300
Subject: [PATCH 3/3] fix: treat createMatchPath args as opaque

---
 src/index.ts | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/src/index.ts b/src/index.ts
index a99ae8f..89ee9a4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -167,12 +167,8 @@ function getMappedPath(source: string, file: string) {
  * Like `createMatchPath` from `tsconfig-paths` package, but considers
  * that the module id could have a .js or .jsx extension.
  */
-const createExtendedMatchPath: typeof createMatchPath = (
-  absoluteBaseUrl,
-  paths,
-  ...rest
-) => {
-  const matchPath = createMatchPath(absoluteBaseUrl, paths, ...rest)
+const createExtendedMatchPath: typeof createMatchPath = (...createArgs) => {
+  const matchPath = createMatchPath(...createArgs)
 
   return (id, ...otherArgs) => {
     const match = matchPath(id, ...otherArgs)