Generate importmap for node_modules.
This repository generates import map from package.json
files in your node_modules
directory. The generated importmap can be used to make code dependent of node module executable in a browser.
See code relying on node module resolution
import lodash from "lodash"
The code above is expecting Node.js to "magically" find file corresponding to
"lodash"
. This magic is the node module resolution algorith.
Other runtimes than Node.js, a browser like Chrome for instance, don't have this algorithm. Executing that code in a browser fetches
http://example.com/lodash
and likely results in404 File Not Found
from server.
1 - Install @jsenv/node-module-import-map
npm install --save-dev @jsenv/node-module-import-map
2 - Create generate-import-map.js
import { getImportMapFromProjectFiles, writeImportMapFile } from "@jsenv/node-module-import-map"
const projectDirectoryUrl = new URL("./", import.meta.url)
await writeImportMapFile(
[
getImportMapFromProjectFiles({
projectDirectoryUrl,
}),
],
{
projectDirectoryUrl,
importMapFileRelativeUrl: "./project.importmap",
},
)
Or use the commonjs equivalent if you need (or want):
const {
getImportMapFromProjectFiles,
writeImportMapFile,
} = require("@jsenv/node-module-import-map")
const projectDirectoryUrl = __dirname
await writeImportMapFile(
[
getImportMapFromProjectFiles({
projectDirectoryUrl,
}),
],
{
projectDirectoryUrl,
importMapFileRelativeUrl: "./project.importmap",
},
)
3 - Generate project.importmap
node generate-import-map.js
4 - Add project.importmap
to your html
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
<script type="importmap" src="./project.importmap"></script>
</head>
<body>
<script type="module">
import lodash from "lodash"
</script>
</body>
</html>
If you use a bundler or an other tool, be sure it's compatible with import maps.
Because import map are standard, you can expect your bundler/tools to be already compatible or to become compatible without plugin in a near future.
@jsenv/core seamlessly supports importmap during development, unit testing and when building for production.
writeImportMapFile
is an async function receiving an array of promise resolving to importmaps. It awaits for every importmap, compose them into one and write it into a file.
writeImportMapFile code example
Code below generate an import map from node_modules + an inline importmap.
import { getImportMapFromProjectFiles, writeImportMapFile } from "@jsenv/node-module-import-map"
const projectDirectoryUrl = new URL("./", import.meta.url)
const importMapInputs = [
getImportMapFromProjectFiles({
projectDirectoryUrl,
dev: true,
}),
{
imports: {
foo: "./bar.js",
},
},
]
await writeImportMapFile(importMapInputs, {
projectDirectoryUrl,
importMapFileRelativeUrl: "./import-map.importmap",
})
importMapInputs parameter
importMapInputs
is an array of importmap object or promise resolving to importmap objects. This parameter is optional and is an empty array by default.
When
importMapInputs
is empty a warning is emitted andwriteImportMapFile
write an empty importmap file.
importMapFile parameter
importMapFile
parameter is a boolean controling if importMap is written to a file. This parameters is optional and enabled by default.
importMapFileRelativeUrl parameter
importMapFileRelativeUrl
parameter is a string controlling where importMap file is written. This parameter is optional and by default it's "./import-map.importmap"
.
getImportMapFromProjectFiles
is an async function returning an importMap object computed from infos found in package.json
files and source files.
The following source of information are used to create complete and coherent mappings in the importmap.
- Your
package.json
- All
dependencies
declared inpackage.json
are searched intonode_modules
, recursively. - In every
package.json
, "main", "exports" and "imports" field. - All static and dynamic import found in files, recursively.
getImportMapFromProjectFiles code example
import { getImportMapFromProjectFiles } from "@jsenv/node-module-import-map"
const importMap = await getImportMapFromProjectFiles({
projectDirectoryUrl: new URL("./", import.meta.url),
dev: false,
runtime: "browser",
})
Be sure node modules are on your filesystem because we'll use the filesystem structure to generate the importmap. For that reason, you must use it after
npm install
or anything that is responsible to generate the node_modules folder and its content on your filesystem.
projectDirectoryUrl parameter
projectDirectoryUrl
parameter is a string url leading to a folder with a package.json
. This parameters is required and accepted values are documented in @jsenv/util#assertAndNormalizeDirectoryUrl
dev parameter
dev
parameter is a boolean indicating if the importmap will be used for development or production. This parameter is optional and by default it's disabled.
When enabled the following happens:
devDependencies
declared in yourpackage.json
are included in the generated importMap."development"
is favored over"production"
in package.json conditions
runtime parameter
runtime
parameter is a string indicating where the importmap will be used. This parameter is optional with a default of "browser"
.
When runtime
is "browser"
, "browser"
is favored over "node"
in package.json conditions.
When it is "node"
, "node"
is favored.
getImportMapFromFile
is an async function reading importmap from a file.
getImportMapFromFile code example
import { getImportMapFromFile } from "@jsenv/node-module-import-map"
const importMap = await getImportMapFromFile({
projectDirectoryUrl: new URL("./", import.meta.url),
importMapRelativeUrl: "./import-map.importmap",
})
importMapFileRelativeUrl
importMapFileRelativeUrl
parameter is an url relative to projectDirectoryUrl
leading to the importmap file. This parameter is required.
VSCode and ESLint can be configured to understand importmap. This will make ESLint and VSCode capable to resolve your imports. Amongst other things it will give you the following:
- ESLint tells your when import cannot be resolved (help to fix typo)
- ESLint tells your when a named import does not exists (help to fix typo too)
- VSCode "go to definition" opens the imported file (cmd + click too)
- VSCode autocompletion is improved because it can read imported files
The animated image below shows how configuring ESLint and VsCode helps to fix an import with a typo and navigate to an imported file. This example uses "demo/log.js"
import that is remapped to "src/log.js"
by docs/vscode-importmap-demo/custom.importmap
Follow steps below to configure VsCode:
-
Generate importmap file using writeImportMapFile
-
Use
jsConfigFile
parameterVSCode import resolution can be configured in a file called jsconfig.json. Enabling
jsConfigFile
converts import mapping intopaths
and write them intojsconfig.json
.Code example using jsConfigFile
import { writeImportMapFile } from "@jsenv/node-module-import-map" const projectDirectoryUrl = new URL("./", import.meta.url) await writeImportMapFile( [ { imports: { "src/": "./src/", }, }, ], { projectDirectoryUrl, jsConfigFile: true, }, )
Code above would result into the following
jsconfig.json
file{ "compilerOptions": { "baseUrl": ".", "paths": { "src/*": ["./src/*"] } } }
At this stage, VsCode is configured to understand import mappings. It means "Go to definition" is working and allow you to navigate in your codebase using cmd+click
keyboard shortcut.
If you also want to configure ESLint to be alerted when an import cannot be found, follow steps described in @jsenv/importmap-eslint-resolver
@jsenv/node-module-import-map
uses a custom node module resolution
It behaves as Node.js with one big change:
A node module will not be found if it is outside your project directory.
We do this because import map are used on the web where a file outside project directory cannot be reached.
In practice, it has no impact because node modules are inside your project directory. If they are not, ensure all your dependencies are in your package.json
and re-run npm install
.
If the code you wants to run contains one ore more extensionless path specifier, it will not be found by a browser (not even by Node.js).
extensionless import example
import { foo } from "./file"
In this situation, you can do one of the following:
- Add extension in the source file
- If there is a build step, ensure extension are added during the build
- Add remapping in
exports
field of yourpackage.json
{
"exports": {
"./file": "./file.js"
}
}
- Remap manually each extensionless import and pass that importmap in importMapInputs
The generation of importmap takes into account exports
field from package.json
. These exports
field are used to allow subpath imports.
subpath import example
import { foo } from "my-module/feature/index.js"
import { bar } from "my-module/feature-b"
For the above import to work, my-module/package.json
must contain the following exports
field.
{
"name": "my-module",
"exports": {
"./*": "./*",
"./feature-b": "./feature-b/index.js"
}
}
Read more in Node.js documentation about package entry points
Node.js allows to put *
in exports
field. There is an importmap equivalent when *
is used for directory/folder remapping.
{
"exports": {
"./feature/*": "./feature/*"
}
}
Becomes the following importmap
{
"imports": {
"./feature/": "./feature/"
}
}
However using *
to add file extension as in
{
"exports": {
"./feature/*": "./feature/*.js"
}
}
is not supported in importmap. Nothing suggests it will be supported for now, read more in WICG/import-maps#232.