1 | # node-module-import-map
|
2 |
|
3 | Generate importmap for node_modules.
|
4 |
|
5 | [![github package](https://img.shields.io/github/package-json/v/jsenv/jsenv-node-module-import-map.svg?logo=github&label=package)](https://github.com/jsenv/jsenv-node-module-import-map/packages)
|
6 | [![npm package](https://img.shields.io/npm/v/@jsenv/node-module-import-map.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/node-module-import-map)
|
7 | [![github ci](https://github.com/jsenv/jsenv-node-module-import-map/workflows/ci/badge.svg)](https://github.com/jsenv/jsenv-node-module-import-map/actions?workflow=ci)
|
8 | [![codecov coverage](https://codecov.io/gh/jsenv/jsenv-node-module-import-map/branch/master/graph/badge.svg)](https://codecov.io/gh/jsenv/jsenv-node-module-import-map)
|
9 |
|
10 | # Table of contents
|
11 |
|
12 | - [Presentation](#Presentation)
|
13 | - [Usage](#Usage)
|
14 | - [Extensionless import warning](#Extensionless-import-warning)
|
15 | - [Subpath import warning](#Subpath-import-warning)
|
16 | - [generateImportMapForProject](#generateImportMapForProject)
|
17 | - [getImportMapFromNodeModules](#getImportMapFromNodeModules)
|
18 | - [getImportMapFromFile](#getImportMapFromFile)
|
19 | - [Custom node module resolution](#custom-node-module-resolution)
|
20 | - [Concrete example](#concrete-example)
|
21 |
|
22 | # Presentation
|
23 |
|
24 | This repository generates [import map](https://github.com/WICG/import-maps) for node modules. The generated importmap can be used to make code dependent of node module executable in a browser.
|
25 |
|
26 | <details>
|
27 | <summary>See code relying on node module resolution</summary>
|
28 |
|
29 | ```js
|
30 | import lodash from "lodash"
|
31 | ```
|
32 |
|
33 | > The code above is expecting Node.js to "magically" find file corresponding to `"lodash"`. This magic is the [node module resolution algorith](https://nodejs.org/api/modules.html#modules_all_together).
|
34 |
|
35 | > 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.
|
36 |
|
37 | </details>
|
38 |
|
39 | # Usage
|
40 |
|
41 | <details>
|
42 | <summary>1 - Install <code>@jsenv/node-module-import-map</code></summary>
|
43 |
|
44 | ```console
|
45 | npm install --save-dev @jsenv/node-module-import-map
|
46 | ```
|
47 |
|
48 | </details>
|
49 |
|
50 | <details>
|
51 | <summary>2 - Create <code>generate-import-map.js</code></summary>
|
52 |
|
53 | ```js
|
54 | import {
|
55 | getImportMapFromNodeModules,
|
56 | generateImportMapForProject,
|
57 | } from "@jsenv/node-module-import-map"
|
58 |
|
59 | const projectDirectoryUrl = new URL("./", import.meta.url)
|
60 |
|
61 | await generateImportMapForProject(
|
62 | [
|
63 | getImportMapFromNodeModules({
|
64 | projectDirectoryUrl,
|
65 | }),
|
66 | ],
|
67 | {
|
68 | projectDirectoryUrl,
|
69 | importMapFileRelativeUrl: "./project.importmap",
|
70 | },
|
71 | )
|
72 | ```
|
73 |
|
74 | <details>
|
75 | <summary>See commonjs equivalent of code above</summary>
|
76 |
|
77 | ```js
|
78 | const {
|
79 | getImportMapFromNodeModules,
|
80 | generateImportMapForProject,
|
81 | } = require("@jsenv/node-module-import-map")
|
82 |
|
83 | const projectDirectoryUrl = __dirname
|
84 |
|
85 | await generateImportMapForProject(
|
86 | [
|
87 | getImportMapFromNodeModules({
|
88 | projectDirectoryUrl,
|
89 | }),
|
90 | ],
|
91 | {
|
92 | projectDirectoryUrl,
|
93 | importMapFileRelativeUrl: "./project.importmap",
|
94 | },
|
95 | )
|
96 | ```
|
97 |
|
98 | </details>
|
99 |
|
100 | </details>
|
101 |
|
102 | <details>
|
103 | <summary>3 - Generate <code>project.importmap</code></summary>
|
104 |
|
105 | ```console
|
106 | node generate-import-map.js
|
107 | ```
|
108 |
|
109 | </details>
|
110 |
|
111 | <details>
|
112 | <summary>4 - Add <code>project.importmap</code> to your html</summary>
|
113 |
|
114 | ```html
|
115 | <!DOCTYPE html>
|
116 | <html>
|
117 | <head>
|
118 | <title>Title</title>
|
119 | <meta charset="utf-8" />
|
120 | <link rel="icon" href="data:," />
|
121 | <script type="importmap" src="./project.importmap"></script>
|
122 | </head>
|
123 |
|
124 | <body>
|
125 | <script type="module">
|
126 | import lodash from "lodash"
|
127 | </script>
|
128 | </body>
|
129 | </html>
|
130 | ```
|
131 |
|
132 | If you use a bundler, be sure it's compatible with import maps.
|
133 |
|
134 | > Because import map are standard, you can expect your bundler to be already compatible or to become compatible without plugin in a near future.
|
135 |
|
136 | > [@jsenv/core](https://github.com/jsenv/jsenv-core) seamlessly supports importmap during development, unit testing and when building for production.
|
137 |
|
138 | </details>
|
139 |
|
140 | # Extensionless import warning
|
141 |
|
142 | If the code you wants to run contains one ore more extensionless path specifier, it will result in `404 not found`.
|
143 |
|
144 | <details>
|
145 | <summary>Example of extensionless specifier</summary>
|
146 |
|
147 | ```js
|
148 | import { foo } from "./file" // extensionless path specifier
|
149 | ```
|
150 |
|
151 | </details>
|
152 |
|
153 | In this situation, you can:
|
154 |
|
155 | 1. Add extension in the source file
|
156 | 2. If there is a build step, ensure extension are added during the build
|
157 | 3. Add remapping in `exports` field of your `package.json`
|
158 |
|
159 | ```json
|
160 | {
|
161 | "exports": {
|
162 | "./file": "./file.js"
|
163 | }
|
164 | }
|
165 | ```
|
166 |
|
167 | 4. Maintain an importmap with remappings you need and pass it in [importMapInputs](#importMapInputs)
|
168 |
|
169 | # Subpath import warning
|
170 |
|
171 | The generation of importmap takes into account `exports` field from `package.json`. These `exports` field are used to allow subpath imports.
|
172 |
|
173 | <details>
|
174 | <summary>subpath import example</summary>
|
175 |
|
176 | ```js
|
177 | import { foo } from "my-module/feature/index.js"
|
178 | import { bar } from "my-module/feature-b"
|
179 | ```
|
180 |
|
181 | For the above import to work, `my-module/package.json` must contain the following `exports` field.
|
182 |
|
183 | ```json
|
184 | {
|
185 | "name": "my-module",
|
186 | "exports": {
|
187 | "./*": "./*",
|
188 | "./feature-b": "./feature-b/index.js"
|
189 | }
|
190 | }
|
191 | ```
|
192 |
|
193 | Read more in [Node.js documentation about package entry points](https://nodejs.org/dist/latest-v15.x/docs/api/packages.html#packages_package_entry_points)
|
194 |
|
195 | </details>
|
196 |
|
197 | Node.js allows to put `*` in `exports` field. There is an importmap equivalent when `*` is used for directory/folder remapping.
|
198 |
|
199 | ```json
|
200 | {
|
201 | "exports": {
|
202 | "./feature/*": "./feature/*"
|
203 | }
|
204 | }
|
205 | ```
|
206 |
|
207 | Becomes the following importmap
|
208 |
|
209 | ```json
|
210 | {
|
211 | "imports": {
|
212 | "./feature/": "./feature/"
|
213 | }
|
214 | }
|
215 | ```
|
216 |
|
217 | However using `*` to add file extension (`"./feature/*": "./feature/*.js"`) **is not supported in importmap**. This is tracked in https://github.com/WICG/import-maps/issues/232.
|
218 |
|
219 | # generateImportMapForProject
|
220 |
|
221 | `generateImportMapForProject` 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.
|
222 |
|
223 | > This function is meant to be responsible of generating the final importMap file that a project uses.
|
224 |
|
225 | <details>
|
226 | <summary>generateImportMapForProject code example</summary>
|
227 |
|
228 | Code below generate an import map from node_modules + a file + an inline importmap.
|
229 |
|
230 | ```js
|
231 | import {
|
232 | getImportMapFromNodeModules,
|
233 | getImportMapFromFile,
|
234 | generateImportMapForProject,
|
235 | } from "@jsenv/node-module-import-map"
|
236 |
|
237 | const projectDirectoryUrl = new URL("./", import.meta.url)
|
238 | const customImportMapFileUrl = new URL("./import-map-custom.importmap", projectDirectoryUrl)
|
239 | const importMapInputs = [
|
240 | getImportMapFromNodeModules({
|
241 | projectDirectoryUrl,
|
242 | projectPackageDevDependenciesIncluded: true,
|
243 | }),
|
244 | getImportMapFromFile(customImportMapFileUrl),
|
245 | {
|
246 | imports: {
|
247 | foo: "./bar.js",
|
248 | },
|
249 | },
|
250 | ]
|
251 |
|
252 | await generateImportMapForProject(importMapInputs, {
|
253 | projectDirectoryUrl,
|
254 | importMapFileRelativeUrl: "./import-map.importmap",
|
255 | })
|
256 | ```
|
257 |
|
258 | — source code at [src/generateImportMapForProject.js](./src/generateImportMapForProject.js)
|
259 |
|
260 | </details>
|
261 |
|
262 | ## importMapInputs
|
263 |
|
264 | `importMapInputs` is an array of importmap object or promise resolving to importmap objects. This parameter is optional and is an empty array by default.
|
265 |
|
266 | > When `importMapInputs` is empty a warning is emitted and `generateImportMapForProject` write an empty importmap file.
|
267 |
|
268 | ## importMapFile
|
269 |
|
270 | `importMapFile` parameter is a boolean controling if importMap is written to a file. This parameters is optional and enabled by default.
|
271 |
|
272 | ## importMapFileRelativeUrl
|
273 |
|
274 | `importMapFileRelativeUrl` parameter is a string controlling where importMap file is written. This parameter is optional and by default it's `"./import-map.importmap"`.
|
275 |
|
276 | # getImportMapFromNodeModules
|
277 |
|
278 | `getImportMapFromNodeModules` is an async function returning an importMap object computed from the content of node_modules directory. It reads your project `package.json` and recursively try to find your dependencies.
|
279 |
|
280 | > 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.
|
281 |
|
282 | <details>
|
283 | <summary>getImportMapFromNodeModules code example</summary>
|
284 |
|
285 | ```js
|
286 | import { getImportMapFromNodeModules } from "@jsenv/node-module-import-map"
|
287 |
|
288 | const importMap = await getImportMapFromNodeModules({
|
289 | projectDirectoryUrl: new URL("./", import.meta.url),
|
290 | projectPackageDevDependenciesIncluded: true,
|
291 | })
|
292 | ```
|
293 |
|
294 | — source code at [src/getImportMapFromNodeModules.js](./src/getImportMapFromNodeModules.js)
|
295 |
|
296 | </details>
|
297 |
|
298 | ## projectDirectoryUrl
|
299 |
|
300 | `projectDirectoryUrl` parameter is a string url leading to a folder with a `package.json`. This parameters is **required** and accepted values are documented in https://github.com/jsenv/jsenv-util#assertandnormalizedirectoryurl
|
301 |
|
302 | ## projectPackageDevDependenciesIncluded
|
303 |
|
304 | `projectPackageDevDependenciesIncluded` parameter is a boolean controling if devDependencies from your project `package.json` are included in the generated importMap. This parameter is optional and by default it's disabled when `process.env.NODE_ENV` is `"production"`.
|
305 |
|
306 | ## packagesExportsPreference
|
307 |
|
308 | `packagesExportsPreference` parameter is an array of string representing what conditional export you prefer to pick from package.json. This parameter is optional with a default value of `["import", "browser"]`.
|
309 |
|
310 | It exists to support [conditional exports from Node.js](https://nodejs.org/dist/latest-v13.x/docs/api/esm.html#esm_conditional_exports).
|
311 |
|
312 | <details>
|
313 | <summary>package.json with conditional exports</summary>
|
314 |
|
315 | ```json
|
316 | {
|
317 | "type": "module",
|
318 | "main": "dist/commonjs/main.cjs",
|
319 | "exports": {
|
320 | ".": {
|
321 | "require": "./dist/main.cjs",
|
322 | "browser": "./main.browser.js",
|
323 | "node": "./main.node.js",
|
324 | "import": "./main.node.js"
|
325 | }
|
326 | }
|
327 | }
|
328 | ```
|
329 |
|
330 | </details>
|
331 |
|
332 | When none of `packagesExportsPreference` is found in a `package.json` and if `"default"` is specified in that `package.json`, `"default"` value is read and appears in the importmap.
|
333 |
|
334 | <details>
|
335 | <summary>packagesExportsPreference code example</summary>
|
336 |
|
337 | Favoring `"browser"` export:
|
338 |
|
339 | ```js
|
340 | import { getImportMapFromNodeModules } from "@jsenv/node-module-import-map"
|
341 |
|
342 | const importMap = await getImportMapFromNodeModules({
|
343 | projectDirectoryUrl: new URL("./", import.meta.url),
|
344 | packagesExportsPreference: ["browser"],
|
345 | })
|
346 | ```
|
347 |
|
348 | Favoring `"electron"` and fallback to `"browser"`:
|
349 |
|
350 | ```js
|
351 | import { getImportMapFromNodeModules } from "@jsenv/node-module-import-map"
|
352 |
|
353 | const importMap = await getImportMapFromNodeModules({
|
354 | projectDirectoryUrl: new URL("./", import.meta.url),
|
355 | packagesExportsPreference: ["electron", "browser"],
|
356 | })
|
357 | ```
|
358 |
|
359 | </details>
|
360 |
|
361 | # getImportMapFromFile
|
362 |
|
363 | `getImportMapFromFile` is an async function reading importmap from a file.
|
364 |
|
365 | <details>
|
366 | <summary>getImportMapFromFile code example</summary>
|
367 |
|
368 | ```js
|
369 | import { getImportMapFromFile } from "@jsenv/node-module-import-map"
|
370 |
|
371 | const importMapFileUrl = new URL("./import-map.importmap", import.meta.url)
|
372 | const importMap = await getImportMapFromFile(importMapFileUrl)
|
373 | ```
|
374 |
|
375 | — source code at [src/getImportMapFromFile.js](../src/getImportMapFromFile.js)
|
376 |
|
377 | </details>
|
378 |
|
379 | ## importMapFileUrl
|
380 |
|
381 | `importMapFileUrl` parameter a string or an url leading to the importmap file. This parameter is **required**.
|
382 |
|
383 | # Custom node module resolution
|
384 |
|
385 | `@jsenv/node-module-import-map` uses a custom node module resolution
|
386 |
|
387 | It behaves as Node.js with one big change:
|
388 |
|
389 | **A node module will not be found if it is outside your project directory.**
|
390 |
|
391 | We do this because import map are used on the web where a file outside project directory cannot be reached.
|
392 |
|
393 | 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`.
|
394 |
|
395 | # Concrete example
|
396 |
|
397 | This part explains how to setup a real environment to see `@jsenv/node-module-import-map` in action.
|
398 | It reuses a preconfigured project where you can generate import map file.
|
399 |
|
400 | > You need node 13+ to run this example
|
401 |
|
402 | <details>
|
403 | <summary>Step 1 - Setup basic project</summary>
|
404 |
|
405 | ```console
|
406 | git clone https://github.com/jsenv/jsenv-node-module-import-map.git
|
407 | ```
|
408 |
|
409 | ```console
|
410 | cd ./jsenv-node-module-import-map/docs/basic-project
|
411 | ```
|
412 |
|
413 | ```console
|
414 | npm install
|
415 | ```
|
416 |
|
417 | </details>
|
418 |
|
419 | <details>
|
420 | <summary>Step 2 - Generate project importMap</summary>
|
421 |
|
422 | Running command below will log importMap generated for that basic project.
|
423 |
|
424 | ```console
|
425 | node ./generate-import-map.js
|
426 | ```
|
427 |
|
428 | </details>
|