npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2021 – Pkg Stats / Ryan Hefner

@jsenv/node-module-import-map

v13.4.1

Published

Generate importmap for node_modules.

Downloads

1,625

Readme

node-module-import-map

Generate importmap for node_modules.

github package npm package github ci codecov coverage

Table of contents

Presentation

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.

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 in 404 File Not Found from server.

Usage

npm install --save-dev @jsenv/node-module-import-map
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",
  },
)
node generate-import-map.js
<!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.

API

writeImportMapFile

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.

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",
})

implementation

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 and writeImportMapFile write an empty importmap file.

importMapFile parameter is a boolean controling if importMap is written to a file. This parameters is optional and enabled by default.

importMapFileRelativeUrl parameter is a string controlling where importMap file is written. This parameter is optional and by default it's "./import-map.importmap".

getImportMapFromProjectFiles

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 in package.json are searched into node_modules, recursively.
  • In every package.json, "main", "exports" and "imports" field.
  • All static and dynamic import found in files, recursively.
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.

implementation

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 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:

  1. devDependencies declared in your package.json are included in the generated importMap.
  2. "development" is favored over "production" in package.json conditions

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.

treeshakeMappings parameter is a boolean controlling if mappings will be treeshaked according to the import found in your files.

When disabled, all mappings needed for Node module resolution will be generated. During development, you can start/stop using a mapping at any time. In that case it's more convenient to keep unused mappings in the generated importmap. Consequently treeshakeMappings parameter is disabled when dev parameter is enabled.

When enabled, only the mappings actually used by your files will be generated. This is the default behaviour as long as dev parameter is disabled. It will drastically decrease the importmap file size.

importMapInput parameter is an importMap object. This parameter is optional and by default it's an empty object.

You can use this parameter to provide mappings that are not already in your package.json.

import { getImportMapFromProjectFiles } from "@jsenv/node-module-import-map"

const importMap = await getImportMapFromProjectFiles({
  projectDirectoryUrl: new URL("./", import.meta.url),
  importMapInput: {
    imports: {
      foo: "./bar.js",
    },
  },
})

console.log(importMap.imports.foo) // "./bar.js"

getImportMapFromFile

getImportMapFromFile is an async function reading importmap from a file.

import { getImportMapFromFile } from "@jsenv/node-module-import-map"

const importMap = await getImportMapFromFile({
  projectDirectoryUrl: new URL("./", import.meta.url),
  importMapRelativeUrl: "./import-map.importmap",
})

implementation

importMapFileRelativeUrl parameter is an url relative to projectDirectoryUrl leading to the importmap file. This parameter is required.

Configure VSCode and ESLint for importmap

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

Animated image showing importmap integration in VSCode and ESLint

Follow steps below to configure VsCode:

  1. Generate importmap file using writeImportMapFile

  2. Use jsConfigFile parameter

    VSCode import resolution can be configured in a file called jsconfig.json. Enabling jsConfigFile converts import mapping into paths and write them into jsconfig.json.

    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

Advanced documentation

Custom node module resolution

@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.

Extensionless import warning

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).

import { foo } from "./file"

In this situation, you can do one of the following:

  1. Add extension in the source file
  2. If there is a build step, ensure extension are added during the build
  3. Add remapping in exports field of your package.json
{
  "exports": {
    "./file": "./file.js"
  }
}
  1. Remap manually each extensionless import and pass that importmap in importMapInputs

Subpath import warning

The generation of importmap takes into account exports field from package.json. These exports field are used to allow subpath imports.

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 https://github.com/WICG/import-maps/issues/232.