Repository: okonet/lint-staged
Pull request #1082 (2022-01-06 20:10:22)

fix: Resolve config modules with ESM createRequire()
ES modules do not have require() available, we must construct it first.
This fixes the CLI when passing `--config my-config-package`, broken in v12.0.0.

The tests didn't catch this because Jest still doesn't support mocking ESM, and there's really no way to write a test for this right now. I verified it locally like this:

### Broken
```js
// broken.mjs
console.log(require.resolve('react'))
```
```sh
$ node -v
v14.18.1

$ node ./broken.mjs
file:///path/to/broken.mjs:1
console.log(require.resolve('react'))
            ^

ReferenceError: require is not defined in ES module scope, you can use import instead
    at file:///Users/daniel/code/front-end-shared-config/resolve-config.mjs:1:13
    at ModuleJob.run (internal/modules/esm/module_job.js:183:25)
    at async Loader.import (internal/modules/esm/loader.js:178:24)
    at async Object.loadESM (internal/process/esm_loader.js:68:5)
```

### Fixed
```js
// fixed.mjs
import { createRequire } from 'module'

const require = createRequire(import.meta.url)

console.log(require.resolve('react'))
```
```sh
$ node ./fixed.mjs
/path/to/node_modules/react/index.js
```


iiroj: Thanks for noticing! The tests don't seem to pass, unfortunately.
iiroj: @evocateur can you move `resolveConfig` into its own file, and mock it to use `require.resolve` during tests as a workaround?

I've been meaning to rewrite the tests in ESM as well but it's a huge pain with all the mocking we have.
evocateur: @iiroj Ah, dang, that makes sense. Thought I could get away with not running the tests locally. 😢 I'll fix it momentarily.
evocateur: @iiroj I feel your pain with the Jest ESM mocking story.

I wasn't able to get the automatic `test/__mocks__/resolveConfig.js` pattern to work (probably a race condition between the module parser and the mocker), so I had to copy and paste that wee chunk four places. Then I hit local integration test failures, related to my `git config --global init.defaultBranch` setting of `main`, which I fixed in a separate commit.

Thanks for your responsiveness!
codecov[bot]: # [Codecov](https://codecov.io/gh/okonet/lint-staged/pull/1082?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov) Report
> Merging [#1082](https://codecov.io/gh/okonet/lint-staged/pull/1082?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov) (1321674) into [master](https://codecov.io/gh/okonet/lint-staged/commit/893f3d7825f73115a41ddb3be34af15f4c207315?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov) (893f3d7) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/okonet/lint-staged/pull/1082/graphs/tree.svg?width=650&height=150&src=pr&token=LSoEgsjk4O&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov)](https://codecov.io/gh/okonet/lint-staged/pull/1082?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov)

```diff
@@            Coverage Diff            @@
##            master     #1082   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines          593       589    -4     
  Branches       159       159           
=========================================
- Hits           593       589    -4     
```


| [Impacted Files](https://codecov.io/gh/okonet/lint-staged/pull/1082?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov) | Coverage Δ | |
|---|---|---|
| [lib/loadConfig.js](https://codecov.io/gh/okonet/lint-staged/pull/1082/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov#diff-bGliL2xvYWRDb25maWcuanM=) | `100.00% <ø> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/okonet/lint-staged/pull/1082?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/okonet/lint-staged/pull/1082?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov). Last update [893f3d7...1321674](https://codecov.io/gh/okonet/lint-staged/pull/1082?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Andrey+Okonetchnikov).


Patch:
diff --git a/jest.config.js b/jest.config.js
index b7568c5..878a182 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,6 +1,11 @@
 const config = {
   collectCoverage: true,
-  collectCoverageFrom: ['lib/**/*.js'],
+  collectCoverageFrom: [
+    'lib/**/*.js',
+    // Avoid ESM import.meta parse error.
+    // (Can't measure coverage anyway, it's always mocked)
+    '!lib/resolveConfig.js',
+  ],
   moduleDirectories: ['node_modules'],
   setupFiles: ['./testSetup.js'],
   snapshotSerializers: ['jest-snapshot-serializer-ansi'],
diff --git a/lib/loadConfig.js b/lib/loadConfig.js
index 7649344..8c9b8c5 100644
--- a/lib/loadConfig.js
+++ b/lib/loadConfig.js
@@ -6,6 +6,8 @@ import debug from 'debug'
 import { lilconfig } from 'lilconfig'
 import YAML from 'yaml'
 
+import { resolveConfig } from './resolveConfig.js'
+
 const debugLog = debug('lint-staged:loadConfig')
 
 /**
@@ -49,14 +51,6 @@ const loaders = {
   noExt: yamlParse,
 }
 
-const resolveConfig = (configPath) => {
-  try {
-    return require.resolve(configPath)
-  } catch {
-    return configPath
-  }
-}
-
 /**
  * @param {object} options
  * @param {string} [options.configPath] - Explicit path to a config file
diff --git a/lib/resolveConfig.js b/lib/resolveConfig.js
new file mode 100644
index 0000000..0b9f357
--- /dev/null
+++ b/lib/resolveConfig.js
@@ -0,0 +1,15 @@
+import { createRequire } from 'module'
+
+/**
+ * require() does not exist for ESM, so we must create it to use require.resolve().
+ * @see https://nodejs.org/api/module.html#modulecreaterequirefilename
+ */
+const require = createRequire(import.meta.url)
+
+export function resolveConfig(configPath) {
+  try {
+    return require.resolve(configPath)
+  } catch {
+    return configPath
+  }
+}


Test patch:
diff --git a/test/index.spec.js b/test/index.spec.js
index 004ef74..81bd519 100644
--- a/test/index.spec.js
+++ b/test/index.spec.js
@@ -28,6 +28,16 @@ jest.mock('url', () => ({
 
 jest.mock('../lib/getStagedFiles')
 jest.mock('../lib/gitWorkflow')
+jest.mock('../lib/resolveConfig', () => ({
+  /** Unfortunately necessary due to non-ESM tests. */
+  resolveConfig: (configPath) => {
+    try {
+      return require.resolve(configPath)
+    } catch {
+      return configPath
+    }
+  },
+}))
 jest.mock('../lib/validateOptions', () => ({
   validateOptions: jest.fn().mockImplementation(async () => {}),
 }))
diff --git a/test/index2.spec.js b/test/index2.spec.js
index 8712fef..41de135 100644
--- a/test/index2.spec.js
+++ b/test/index2.spec.js
@@ -4,6 +4,17 @@ import { Listr } from 'listr2'
 import makeConsoleMock from 'consolemock'
 
 jest.mock('listr2')
+jest.mock('../lib/resolveConfig', () => ({
+  /** Unfortunately necessary due to non-ESM tests. */
+  resolveConfig: (configPath) => {
+    try {
+      return require.resolve(configPath)
+    } catch {
+      return configPath
+    }
+  },
+}))
+
 jest.mock('../lib/resolveGitRepo')
 
 import lintStaged from '../lib/index'
diff --git a/test/integration.test.js b/test/integration.test.js
index cc9c2da..d044a3a 100644
--- a/test/integration.test.js
+++ b/test/integration.test.js
@@ -7,6 +7,16 @@ import normalize from 'normalize-path'
 
 jest.unmock('lilconfig')
 jest.unmock('execa')
+jest.mock('../lib/resolveConfig', () => ({
+  /** Unfortunately necessary due to non-ESM tests. */
+  resolveConfig: (configPath) => {
+    try {
+      return require.resolve(configPath)
+    } catch {
+      return configPath
+    }
+  },
+}))
 
 import { execGit as execGitBase } from '../lib/execGit'
 import lintStaged from '../lib/index'
@@ -88,6 +98,9 @@ describe('lint-staged', () => {
 
 const globalConsoleTemp = console
 
+// Tests should be resilient to `git config init.defaultBranch` that is _not_ "master"
+let defaultBranchName = 'UNSET'
+
 describe('lint-staged', () => {
   beforeAll(() => {
     console = makeConsoleMock()
@@ -104,6 +117,10 @@ describe('lint-staged', () => {
     await appendFile('README.md', '# Test\n')
     await execGit(['add', 'README.md'])
     await execGit(['commit', '-m initial commit'])
+
+    if (defaultBranchName === 'UNSET') {
+      defaultBranchName = await execGit(['rev-parse', '--abbrev-ref', 'HEAD'])
+    }
   })
 
   afterEach(async () => {
@@ -451,7 +468,7 @@ describe('lint-staged', () => {
     await gitCommit(fixJsConfig, ['-m commit a'])
     expect(await readFile('test.js')).toEqual(fileInBranchA)
 
-    await execGit(['checkout', 'master'])
+    await execGit(['checkout', defaultBranchName])
 
     // Create another branch
     await execGit(['checkout', '-b', 'branch-b'])
@@ -461,7 +478,7 @@ describe('lint-staged', () => {
     expect(await readFile('test.js')).toEqual(fileInBranchBFixed)
 
     // Merge first branch
-    await execGit(['checkout', 'master'])
+    await execGit(['checkout', defaultBranchName])
     await execGit(['merge', 'branch-a'])
     expect(await readFile('test.js')).toEqual(fileInBranchA)
     expect(await execGit(['log', '-1', '--pretty=%B'])).toMatch('commit a')
@@ -512,7 +529,7 @@ describe('lint-staged', () => {
     await gitCommit(fixJsConfig, ['-m commit a'])
     expect(await readFile('test.js')).toEqual(fileInBranchA)
 
-    await execGit(['checkout', 'master'])
+    await execGit(['checkout', defaultBranchName])
 
     // Create another branch
     await execGit(['checkout', '-b', 'branch-b'])
@@ -522,7 +539,7 @@ describe('lint-staged', () => {
     expect(await readFile('test.js')).toEqual(fileInBranchBFixed)
 
     // Merge first branch
-    await execGit(['checkout', 'master'])
+    await execGit(['checkout', defaultBranchName])
     await execGit(['merge', 'branch-a'])
     expect(await readFile('test.js')).toEqual(fileInBranchA)
     expect(await execGit(['log', '-1', '--pretty=%B'])).toMatch('commit a')
@@ -1073,7 +1090,7 @@ describe('lintStaged', () => {
     await execGit(['add', 'test.js'], { cwd })
 
     await expect(execGit(['log', '-1'], { cwd })).rejects.toThrowErrorMatchingInlineSnapshot(
-      `"fatal: your current branch 'master' does not have any commits yet"`
+      `"fatal: your current branch '${defaultBranchName}' does not have any commits yet"`
     )
 
     await gitCommit({
diff --git a/test/loadConfig.spec.js b/test/loadConfig.spec.js
index 33272e6..2123012 100644
--- a/test/loadConfig.spec.js
+++ b/test/loadConfig.spec.js
@@ -1,3 +1,14 @@
+jest.mock('../lib/resolveConfig', () => ({
+  /** Unfortunately necessary due to non-ESM tests. */
+  resolveConfig: (configPath) => {
+    try {
+      return require.resolve(configPath)
+    } catch {
+      return configPath
+    }
+  },
+}))
+
 import { dynamicImport } from '../lib/loadConfig.js'
 
 describe('dynamicImport', () => {