@jsenv/importmap-eslint-resolver
Advanced tools
Comparing version 2.4.2 to 3.0.0
{ | ||
"name": "@jsenv/importmap-eslint-resolver", | ||
"version": "2.4.2", | ||
"version": "3.0.0", | ||
"description": "importmap resolution for eslint.", | ||
@@ -11,6 +11,7 @@ "license": "MIT", | ||
"engines": { | ||
"node": ">=13.0.0" | ||
"node": ">=14.9.0" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
"access": "public", | ||
"registry": "https://registry.npmjs.org" | ||
}, | ||
@@ -45,3 +46,3 @@ "type": "module", | ||
"dependencies": { | ||
"@jsenv/import-map": "6.13.0", | ||
"@jsenv/import-map": "6.13.2", | ||
"@jsenv/logger": "4.0.0", | ||
@@ -53,11 +54,11 @@ "@jsenv/util": "4.0.6" | ||
"@jsenv/codecov-upload": "3.4.3", | ||
"@jsenv/core": "17.9.0", | ||
"@jsenv/core": "17.10.0", | ||
"@jsenv/eslint-config": "12.9.1", | ||
"@jsenv/github-release-package": "1.2.2", | ||
"@jsenv/node-module-import-map": "13.0.0", | ||
"@jsenv/node-module-import-map": "13.4.1", | ||
"@jsenv/package-publish": "1.5.2", | ||
"@jsenv/prettier-check-project": "5.6.1", | ||
"eslint": "7.22.0", | ||
"eslint": "7.25.0", | ||
"prettier": "2.2.1" | ||
} | ||
} |
223
readme.md
@@ -14,8 +14,5 @@ # importmap-eslint-resolver | ||
- [Installation](#installation) | ||
- [Install eslint-plugin-import](#install-eslint-plugin-import) | ||
- [Install importmap-eslint-resolver](#install-importmap-eslint-resolver) | ||
- [Configure eslint](#configure-eslint) | ||
- [Set importmap file path](#Set-importmap-file-path) | ||
- [Bare specifier](#Bare-specifier) | ||
- [Extensionless import](#extensionless-import) | ||
- [About resolution](#About-resolution) | ||
- [Configuration](#Configuration) | ||
- [Advanced configuration example](#Advanced-configuration-example) | ||
@@ -57,5 +54,6 @@ # Presentation | ||
## Install eslint-plugin-import | ||
<details> | ||
<summary>Install eslint-plugin-import</summary> | ||
If you already use this ESLint plugin you can skip this step. | ||
> If you already use this ESLint plugin you can skip this step. | ||
@@ -66,4 +64,7 @@ ```console | ||
## Install importmap-eslint-resolver | ||
</details> | ||
<details> | ||
<summary>Install importmap-eslint-resolver</summary> | ||
```console | ||
@@ -73,9 +74,15 @@ npm install --save-dev @jsenv/importmap-eslint-resolver | ||
## Configure eslint | ||
</details> | ||
- Your eslint config must enable `eslint-plugin-import` | ||
- Your eslint config must use `@jsenv/importmap-eslint-resolver` resolver | ||
<details> | ||
<summary>Configure ESLint</summary> | ||
It means your minimal `.eslintrc.cjs` file looks like this: | ||
Your ESLint config must: | ||
- enable `eslint-plugin-import` in `plugins` | ||
- configure `eslint-plugin-import` to use `@jsenv/importmap-eslint-resolver` as resolver | ||
- configure `projectDirectoryUrl` and `importMapFileRelativeUrl` | ||
Your minimal `.eslintrc.cjs` file looks like this: | ||
```js | ||
@@ -88,2 +95,3 @@ module.exports = { | ||
projectDirectoryUrl: __dirname, | ||
importMapFileRelativeUrl: "./project.importmap", | ||
}, | ||
@@ -95,36 +103,46 @@ }, | ||
# Set importmap file path | ||
</details> | ||
By default we will search for a file in your project directory named `import-map.importmap`. If the importmap file is located somewhere else you can use `importMapFileRelativeUrl` parameter to tell us where to look at. | ||
# About resolution | ||
**By default** the resolution is: | ||
- case sensitive | ||
- browser like | ||
- consider node core modules (fs, url) as not found | ||
- do not implement node module resolution | ||
- do not understand path without extension (does not try to auto add extension) | ||
This resolution **default** behaviour is documented in this section and **can be configured to your convenience**. | ||
## Case sensitivity | ||
This resolver is case sensitive by default: An import is found only if the import path and actual file on the filesystem have same case. | ||
```js | ||
module.exports = { | ||
plugins: ["import"], | ||
settings: { | ||
"import/resolver": { | ||
[require.resolve("@jsenv/importmap-eslint-resolver")]: { | ||
projectDirectoryUrl: __dirname, | ||
importMapFileRelativeUrl: "./project.importmap", | ||
}, | ||
}, | ||
}, | ||
} | ||
import { getUser } from "./getUser.js" | ||
``` | ||
# Bare specifier | ||
The import above is found only if there is a file `getUser.js`. It won't be found if file is named `getuser.js`, even if the filesystem is case insensitive. | ||
A specifier is what is written after the from keyword in an import statement. | ||
This ensure two things: | ||
- Project is compatible with Windows or other operating system where filesystem is case sensitive. | ||
- import paths are consistent with what is actually on the filesystem | ||
Case sensitivity can be disabled using [caseSensitive parameter](#Configuration) | ||
## Node module resolution | ||
This resolver consider files are written for browsers by default: Node core modules will be considered as not found. | ||
```js | ||
import value from "specifier" | ||
import { readFile } from "fs" | ||
``` | ||
If there is no mapping of `"specifier"` to `"./specifier.js"` the imported file will not be found. | ||
This is because import map consider `"specifier"` as a special kind of specifier called bare specifier. | ||
And every bare specifier must have a mapping or it cannot be resolved. | ||
To fix this either add a mapping or put explicitely `"./specifier.js"`. | ||
The import above would be reported by ESLint as not found. | ||
Please note that `"specifier.js"` is also a bare specifier. You should write `"./specifier.js"` instead. | ||
If the file is written for Node.js, you can consider node core modules as found and enable node module resolution using [node parameter](#Configuration) | ||
# Extensionless import | ||
## Extensionless import | ||
@@ -150,5 +168,27 @@ Extensionless import means an import where the specifier omits the file extension. | ||
But if for some reason this is problematic you can still configure `@jsenv/importmap-eslint-resolver` to understand these extensionless specifiers. | ||
But if for some reason this is problematic you can allow extensionless specifiers using [defaultExtension parameter](#Configuration) | ||
## Bare specifier | ||
A specifier is what is written after the from keyword in an import statement. | ||
```js | ||
import value from "specifier" | ||
``` | ||
If there is no mapping of `"specifier"` to `"./specifier.js"` the imported file will not be found. | ||
This is because import map consider `"specifier"` as a special kind of specifier called bare specifier. | ||
And every bare specifier must have a mapping or it cannot be resolved. | ||
To fix this either add a mapping or put explicitely `"./specifier.js"`. | ||
Please note that `"specifier.js"` is also a bare specifier. You should write `"./specifier.js"` instead. | ||
# Configuration | ||
<details> | ||
<summary>importMapFileRelativeUrl parameter</summary> | ||
`importMapFileRelativeUrl` parameter is a string leading to an importmap file. This parameter is optional and `undefined` by default. | ||
```js | ||
module.exports = { | ||
@@ -160,3 +200,3 @@ plugins: ["import"], | ||
projectDirectoryUrl: __dirname, | ||
defaultExtension: true, | ||
importMapFileRelativeUrl: "./project.importmap", | ||
}, | ||
@@ -168,9 +208,112 @@ }, | ||
By passing `defaultExtension: true` you tell `@jsenv/importmap-eslint-resolver` to automatically add the importer extension when its omitted. | ||
</details> | ||
<details> | ||
<summary>caseSensitive parameter</summary> | ||
`caseSensitive` parameter is a boolean indicating if the file path will be case sensitive. This parameter is optional and enabled by default. See [Case sensitivity](#Case-sensitivity). | ||
```js | ||
module.exports = { | ||
plugins: ["import"], | ||
settings: { | ||
"import/resolver": { | ||
[require.resolve("@jsenv/importmap-eslint-resolver")]: { | ||
projectDirectoryUrl: __dirname, | ||
importMapFileRelativeUrl: "./project.importmap", | ||
caseSensitive: false, | ||
}, | ||
}, | ||
}, | ||
} | ||
``` | ||
</details> | ||
<details> | ||
<summary>defaultExtension parameter</summary> | ||
`defaultExtension` parameter is a boolean indicating if a default extension will be automatically added to import without file extension. This parameter is optional and disabled by default. See [Extensionless import](#Extensionless-import) | ||
When enabled the following import | ||
```js | ||
import { value } from "./file" | ||
``` | ||
If written in `index.js`, searches file at `file.js`.<br /> | ||
If written in `index.ts`, searches file at `file.ts`. | ||
Will search for a file with an extension. The extension is "inherited" from the file where the import is written: | ||
If written in `whatever.js`, searches at `file.js`.<br /> | ||
If written in `whatever.ts`, searches at `file.ts`. | ||
</details> | ||
<details> | ||
<summary>node parameter</summary> | ||
`node` parameter is a boolean indicating if the file are written for Node.js. This parameter is optional and disabled by default. See [Node core modules](#Node-core-modules) | ||
When enabled node core modules (path, fs, url, etc) will be considered as found. | ||
```js | ||
module.exports = { | ||
plugins: ["import"], | ||
settings: { | ||
"import/resolver": { | ||
[require.resolve("@jsenv/importmap-eslint-resolver")]: { | ||
projectDirectoryUrl: __dirname, | ||
importMapFileRelativeUrl: "./project.importmap", | ||
node: true, | ||
}, | ||
}, | ||
}, | ||
} | ||
``` | ||
</details> | ||
# Advanced configuration example | ||
In a project mixing files written for the browser AND for Node.js you should tell ESLint which are which. This is possible thanks to `overrides`. `overrides` is documented on ESLint in [Configuration Based on Glob Patterns](https://eslint.org/docs/user-guide/configuring/configuration-files#configuration-based-on-glob-patterns). | ||
`.eslintrc.cjs` | ||
```js | ||
const importResolverPath = require.resolve("@jsenv/importmap-eslint-resolver") | ||
module.exports = { | ||
plugins: ["import"], | ||
env: { | ||
es6: true, | ||
// ESLint will consider all files as written for a browser by default | ||
browser: true, | ||
node: false, | ||
}, | ||
settings: { | ||
"import/resolver": { | ||
[importResolverPath]: { | ||
projectDirectoryUrl: __dirname, | ||
importMapFileRelativeUrl: "./project.importmap", | ||
}, | ||
}, | ||
}, | ||
overrides: [ | ||
{ | ||
// ESLint will consider all files inside script/ and ending with .cjs as written for Node.js | ||
files: ["script/**/*.js", "**/*.cjs"], | ||
env: { | ||
es6: true, | ||
browser: false, | ||
node: true, | ||
}, | ||
settings: { | ||
"import/resolver": { | ||
[importResolverPath]: { | ||
node: true, | ||
}, | ||
}, | ||
}, | ||
}, | ||
], | ||
} | ||
``` |
@@ -5,9 +5,7 @@ // https://github.com/benmosher/eslint-plugin-import/blob/master/resolvers/node/index.js | ||
import { readFileSync, statSync, realpathSync } from "fs" | ||
import { statSync, realpathSync } from "fs" | ||
import { createLogger } from "@jsenv/logger" | ||
import { normalizeImportMap, resolveImport } from "@jsenv/import-map" | ||
import { isSpecifierForNodeCoreModule } from "@jsenv/import-map/src/isSpecifierForNodeCoreModule.js" | ||
import { | ||
assertAndNormalizeDirectoryUrl, | ||
resolveUrl, | ||
ensureWindowsDriveLetter, | ||
@@ -18,8 +16,5 @@ urlIsInsideOf, | ||
} from "@jsenv/util" | ||
import { applyImportMapResolution } from "./internal/resolution-import-map.js" | ||
import { applyNodeModuleResolution } from "./internal/resolution-node.js" | ||
const applyUrlResolution = (specifier, importer) => { | ||
const url = resolveUrl(specifier, importer) | ||
return ensureWindowsDriveLetter(url, importer) | ||
} | ||
export const interfaceVersion = 2 | ||
@@ -33,3 +28,3 @@ | ||
projectDirectoryUrl, | ||
importMapFileRelativeUrl = "./import-map.importmap", | ||
importMapFileRelativeUrl, | ||
caseSensitive = true, | ||
@@ -39,2 +34,3 @@ ignoreOutside = false, | ||
node = false, | ||
disableNodeModuleResolution = false, | ||
}, | ||
@@ -44,35 +40,2 @@ ) => { | ||
let importMap | ||
if (typeof importMapFileRelativeUrl === "undefined") { | ||
importMap = undefined | ||
} else if (typeof importMapFileRelativeUrl === "string") { | ||
const importMapFileUrl = applyUrlResolution(importMapFileRelativeUrl, projectDirectoryUrl) | ||
if (ignoreOutside && !urlIsInsideOf(importMapFileUrl, projectDirectoryUrl)) { | ||
logger.warn(`import map file is outside project. | ||
--- import map file --- | ||
${urlToFileSystemPath(importMapFileUrl)} | ||
--- project directory --- | ||
${urlToFileSystemPath(projectDirectoryUrl)}`) | ||
} | ||
try { | ||
const importMapFilePath = urlToFileSystemPath(importMapFileUrl) | ||
const importMapFileBuffer = readFileSync(importMapFilePath) | ||
const importMapFileString = String(importMapFileBuffer) | ||
importMap = JSON.parse(importMapFileString) | ||
importMap = normalizeImportMap(importMap, projectDirectoryUrl) | ||
} catch (e) { | ||
if (e && e.code === "ENOENT") { | ||
importMap = {} | ||
} else { | ||
throw e | ||
} | ||
} | ||
} else { | ||
throw new TypeError( | ||
`importMapFileRelativeUrl must be a string, got ${importMapFileRelativeUrl}`, | ||
) | ||
} | ||
const logger = createLogger({ logLevel }) | ||
@@ -101,29 +64,88 @@ | ||
try { | ||
let importUrl | ||
try { | ||
importUrl = resolveImport({ | ||
specifier, | ||
importer, | ||
importMap, | ||
defaultExtension, | ||
}) | ||
} catch (e) { | ||
if (e.message.includes("bare specifier")) { | ||
// this is an expected error and the file cannot be found | ||
logger.debug("unmapped bare specifier") | ||
return { | ||
found: false, | ||
path: null, | ||
} | ||
let importUrl = applyImportResolution(specifier, { | ||
logger, | ||
projectDirectoryUrl, | ||
importMapFileRelativeUrl, | ||
importer, | ||
defaultExtension, | ||
node, | ||
disableNodeModuleResolution, | ||
}) | ||
if (!importUrl) { | ||
return { | ||
found: false, | ||
path: null, | ||
} | ||
// this is an unexpected error | ||
throw e | ||
} | ||
importUrl = ensureWindowsDriveLetter(importUrl, importer) | ||
if (importUrl.startsWith("file://")) { | ||
const importFilePath = urlToFileSystemPath(importUrl) | ||
return handleFileUrl(importUrl, { | ||
logger, | ||
projectDirectoryUrl, | ||
ignoreOutside, | ||
caseSensitive, | ||
}) | ||
} | ||
if (ignoreOutside && !urlIsInsideOf(importUrl, projectDirectoryUrl)) { | ||
logger.warn(`ignoring import outside project | ||
if (importUrl.startsWith("https://") || importUrl.startsWith("http://")) { | ||
logger.debug(`-> consider found because of http(s) scheme ${importUrl}`) | ||
return handleHttpUrl(importUrl) | ||
} | ||
logger.debug(`-> consider not found because of scheme ${importUrl}`) | ||
return handleRemainingUrl(importUrl) | ||
} catch (e) { | ||
logger.error(e.stack) | ||
return { | ||
found: false, | ||
path: null, | ||
} | ||
} | ||
} | ||
const applyImportResolution = ( | ||
specifier, | ||
{ | ||
logger, | ||
projectDirectoryUrl, | ||
importMapFileRelativeUrl, | ||
importer, | ||
defaultExtension, | ||
node, | ||
disableNodeModuleResolution, | ||
}, | ||
) => { | ||
const importResolutionResult = applyImportMapResolution(specifier, { | ||
importer, | ||
logger, | ||
projectDirectoryUrl, | ||
importMapFileRelativeUrl, | ||
defaultExtension, | ||
}) | ||
if (importResolutionResult) { | ||
return importResolutionResult | ||
} | ||
if (node && !disableNodeModuleResolution) { | ||
const nodeModuleResolutionResult = applyNodeModuleResolution(specifier, { | ||
importer, | ||
logger, | ||
}) | ||
return nodeModuleResolutionResult | ||
} | ||
return null | ||
} | ||
const handleFileUrl = ( | ||
importUrl, | ||
{ logger, projectDirectoryUrl, ignoreOutside, caseSensitive }, | ||
) => { | ||
const importFilePath = urlToFileSystemPath(importUrl) | ||
if (ignoreOutside && !urlIsInsideOf(importUrl, projectDirectoryUrl)) { | ||
logger.warn(`ignoring import outside project | ||
--- import file --- | ||
@@ -134,33 +156,25 @@ ${importFilePath} | ||
`) | ||
return { | ||
found: false, | ||
path: importFilePath, | ||
} | ||
} | ||
return { | ||
found: false, | ||
path: importFilePath, | ||
} | ||
} | ||
if (pathLeadsToFile(importFilePath)) { | ||
if (caseSensitive) { | ||
const importFileRealPath = realpathSync.native(importFilePath) | ||
if (importFileRealPath !== importFilePath) { | ||
logger.warn( | ||
`WARNING: file found at ${importFilePath} but would not be found on a case sensitive filesystem. | ||
if (!pathLeadsToFile(importFilePath)) { | ||
logger.debug(`-> file not found at ${importUrl}`) | ||
return { | ||
found: false, | ||
path: importFilePath, | ||
} | ||
} | ||
if (caseSensitive) { | ||
const importFileRealPath = realpathSync.native(importFilePath) | ||
if (importFileRealPath !== importFilePath) { | ||
logger.warn( | ||
`WARNING: file found at ${importFilePath} but would not be found on a case sensitive filesystem. | ||
The real file path is ${importFileRealPath}. | ||
You can choose to disable this warning by disabling case sensitivity. | ||
If you do so keep in mind windows users would not find that file.`, | ||
) | ||
return { | ||
found: false, | ||
path: importFilePath, | ||
} | ||
} | ||
} | ||
logger.debug(`-> found file at ${importUrl}`) | ||
return { | ||
found: true, | ||
path: importFilePath, | ||
} | ||
} | ||
logger.debug(`-> file not found at ${importUrl}`) | ||
) | ||
return { | ||
@@ -171,25 +185,24 @@ found: false, | ||
} | ||
} | ||
if (importUrl.startsWith("https://") || importUrl.startsWith("http://")) { | ||
// this api is synchronous we cannot check | ||
// if a remote http/https file is available | ||
logger.debug(`-> consider found because of http(s) scheme ${importUrl}`) | ||
logger.debug(`-> found file at ${importUrl}`) | ||
return { | ||
found: true, | ||
path: importFilePath, | ||
} | ||
} | ||
return { | ||
found: true, | ||
path: null, | ||
} | ||
} | ||
const handleHttpUrl = () => { | ||
// this api is synchronous we cannot check | ||
// if a remote http/https file is available | ||
return { | ||
found: true, | ||
path: null, | ||
} | ||
} | ||
logger.debug(`-> consider not found because of scheme ${importUrl}`) | ||
return { | ||
found: false, | ||
path: null, | ||
} | ||
} catch (e) { | ||
logger.error(e.stack) | ||
return { | ||
found: false, | ||
path: null, | ||
} | ||
const handleRemainingUrl = () => { | ||
return { | ||
found: false, | ||
path: null, | ||
} | ||
@@ -196,0 +209,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
47096
8
550
311
7
+ Added@jsenv/import-map@6.13.2(transitive)
- Removed@jsenv/import-map@6.13.0(transitive)
Updated@jsenv/import-map@6.13.2