jsenv / importmap-node-module

Generate importmap for node_modules
35 stars 4 forks source link

@jsenv/importmap-node-module npm package

Generate import map with mappings corresponding to node esm resolution algorithm. The importmap can be used to make code dependent on node module resolution executable in a browser.

Example of code relying on node module resolution:

import lodash from "lodash";

How it works

  1. Use package.json and node_modules/**/package.json to generate mappings corresponding to node esm resolution algorithm
  2. (Optional) Test importmap against all import found in js module files. This step allow to remove unused mappings to keep only thoose actually used in the codebase
  3. Write mappings into a file

Usage

CLI

The simplest way to use this project is with npx:

npx @jsenv/importmap-node-module index.html

It will write mappings in index.html, inside a <script type="importmap">.
It is also possible to write mappings into a separate file as follow:

npx @jsenv/importmap-node-module demo.importmap --entrypoint index.html

The CLI supports the following options:

API

The API supports a few more options than the CLI.

1 - Create _generateimportmap.mjs

import { writeImportmaps } from "@jsenv/importmap-node-module";

await writeImportmaps({
  directoryUrl: new URL("./", import.meta.url),
  importmaps: {
    "./index.html": {},
  },
});

2 - Install dependencies

npm install --save-dev @jsenv/importmap-node-module

3 - Generate project.importmap

node ./generate_importmap.mjs
<script type="importmap"> content updated into "/demo/index.html"

API options

writeImportmaps is an async function generating one or many importmap and writing them into files.

import { writeImportmaps } from "@jsenv/importmap-node-module";

await writeImportmaps({
  directoryUrl: new URL("./", import.meta.url),
  importmaps: {
    "./dev.importmap": {
      nodeMappings: {
        devDependencies: true,
        packageUserConditions: ["development"],
      },
      importResolution: {
        entryPoints: ["index.js"],
      },
    },
    "./prod.importmap": {
      nodeMappings: {
        devDependencies: false,
        packageUserConditions: ["production"],
      },
      importResolution: {
        entryPoints: ["index.js"],
      },
    },
  },
});

It supports the following options:

directoryUrl

directoryUrl is a string/url leading to a folder with a package.json.

directoryUrl is required.

importmaps

importmaps is an object where keys are file relative urls and value are objects configuring which mappings will be written in the files.

importmaps is required.

nodeMappings

nodeMappings is an object configuring the mappings generated to implement node module resolution.

nodeMappings is optional.

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.

In case you don't need to mappings corresponding to node resolution, they can be disabled:

import { writeImportmaps } from "@jsenv/importmap-node-module";

await writeImportmaps({
  directoryUrl: new URL("./", import.meta.url),
  importmaps: {
    "./demo.importmap": {
      nodeMappings: false,
    },
  },
});
nodeMappings.devDependencies

nodeMappings.devDependencies is a boolean. When enabled, mappings for "devDependencies" declared in your package.json are generated.

nodeMappings.devDependencies is optional.

nodeMappings.packageUserConditions

nodeMappings.packageUserConditions is an array controlling which conditions are favored in package.json conditions.

nodeMappings.packageUserConditions is optional.

The following conditions will be picked:

  1. conditions passed in nodeMappings.packageUserConditions
  2. "import"
  3. "browser"
  4. "default"

Be sure to use packageUserConditions: ["node"] if the importmap is generated for node and not for the browser.

importResolution

importResolution is an object. When passed the generated mappings will be used to resolve js imports found in entryPoints and their transitive dependencies. When a js import cannot be resolved a warning is logged. It is recommended to use importResolution as it gives confidence in the generated importmap.

importResolution is optional. When the importmap file is written inside a file ending with .html the import resolution starts from the .html file. Otherwise importResolution.entryPoints must be configured.

It is possible to disable importResolution entirely:

import { writeImportmaps } from "@jsenv/importmap-node-module";

await writeImportmaps({
  directoryUrl: new URL("./", import.meta.url),
  importmaps: {
    "./index.html": {
      importResolution: false,
    },
  },
});
importResolution.entryPoints

importResolution.entryPoints is an array composed of string representing file relative urls. Each file is considered as an entry point using the import mappings.

importResolution.entryPoints is optional.

importResolution.magicExtensions

importResolution.magicExtensions is an array of strings. Each string represent an extension that will be tried when an import cannot be resolved to a file.

importResolution.magicExtensions is optional.

import { writeImportmaps } from "@jsenv/importmap-node-module";

await writeImportmaps({
  directoryUrl: new URL("./", import.meta.url),
  importmaps: {
    "./demo.importmap": {
      importResolution: {
        entryPoints: ["./demo.js"],
        magicExtensions: ["inherit", ".js"],
      },
    },
  },
});

"inherit" means the extension tried in taken from the importer.

import "./helper";
importer path path tried
/Users/dmail/file.js /Users/dmail/helper.js
/Users/dmail/file.ts /Users/dmail/helper.ts

All other values in magicExtensions are file extensions that will be tried one after an other.

importResolution.runtime

importResolution.runtime is a string used to know how to resolve js imports.

For example the following import is correct when runtime is "node" but would log a warning when runtime is "browser".

import { writeFile } from "node:fs";

importResolution.runtime is optional and defaults to "browser".

importResolution.keepUnusedMappings

importResolution.keepUnusedMappings is a boolean. When enabled mappings will be kept even if not currently used by import found in js files.

importResolution.keepUnusedMappings is optional.

import { writeImportmaps } from "@jsenv/importmap-node-module";

await writeImportmaps({
  directoryUrl: new URL("./", import.meta.url),
  importmaps: {
    "./demo.html": {
      importResolution: {
        keepUnusedMappings: true,
      },
    },
  },
});

manualImportmap

manualImportmap is an object containing mappings that will be added to the importmap. This can be used to provide additional mappings and/or override node mappings.

manualImportmap is optional.

import { writeImportmaps } from "@jsenv/importmap-node-module";

await writeImportmaps({
  directoryUrl: new URL("./", import.meta.url),
  importmaps: {
    "./demo.importmap": {
      manualImportmap: {
        imports: {
          "#env": "./env.js",
        },
      },
    },
  },
});

packagesManualOverrides

packagesManualOverrides is an object that can be used to override some of your dependencies package.json.

packagesManualOverrides is optional.

packagesManualOverrides exists in case some of your dependencies use non standard fields to configure their entry points in their package.json. Ideally they should use "exports" field documented in https://nodejs.org/dist/latest-v16.x/docs/api/packages.html#packages_package_entry_points. But not every one has updated to this new field yet.

import { writeImportmaps } from "@jsenv/importmap-node-module";

await writeImportmaps({
  directoryUrl: new URL("./", import.meta.url),
  importmaps: {
    "./demo.importmap": {},
  },
  // overrides "react-redux" package because it uses a non-standard "module" field
  // to expose "es/index.js" entry point
  // see https://github.com/reduxjs/react-redux/blob/9021feb9ff573b01b73084f1a7d10b322e6f0201/package.json#L18
  packageManualOverrides: {
    "react-redux": {
      exports: {
        import: "./es/index.js",
      },
    },
  },
});

Using import maps

At the time of writing this documentation external importmap are not supported by web browsers:

External import maps are not yet supported

If you plan to use importmap in a web browser you need to tell @jsenv/importmap-node-module to inline importmap into the HTML file as shown in CLI.

TypeScript

This repository can generate importmap to make code produced by the TypeScript compiler executable in a browser.

You need to have your package.json and _nodemodules into the directory where typescript output js files. You can achieve this with the following "scripts" in your package.json.

{
  "scripts": {
    "prebuild": "rm -rf dist",
    "build": "tsc",
    "postbuild": "cp package.json dist && ln -sf ../node_modules ./dist/node_modules"
  }
}

Then you can use the script below to produce the importmap.

import { writeImportmaps } from "@jsenv/importmap-node-module";

await writeImportmaps({
  directoryUrl: new URL("./dist/", import.meta.url),
  importmaps: {
    "./index.html": {
      importResolution: {
        magicExtensions: ["inherit"],
      },
    },
  },
});

See also