UNPKG

12.7 kBMarkdownView Raw
1# node-module-import-map
2
3Generate 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
24This 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
30import 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
45npm 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
54import {
55 getImportMapFromNodeModules,
56 generateImportMapForProject,
57} from "@jsenv/node-module-import-map"
58
59const projectDirectoryUrl = new URL("./", import.meta.url)
60
61await 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
78const {
79 getImportMapFromNodeModules,
80 generateImportMapForProject,
81} = require("@jsenv/node-module-import-map")
82
83const projectDirectoryUrl = __dirname
84
85await 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
106node 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
132If 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
142If 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
148import { foo } from "./file" // extensionless path specifier
149```
150
151</details>
152
153In this situation, you can:
154
1551. Add extension in the source file
1562. If there is a build step, ensure extension are added during the build
1573. Add remapping in `exports` field of your `package.json`
158
159```json
160{
161 "exports": {
162 "./file": "./file.js"
163 }
164}
165```
166
1674. Maintain an importmap with remappings you need and pass it in [importMapInputs](#importMapInputs)
168
169# Subpath import warning
170
171The 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
177import { foo } from "my-module/feature/index.js"
178import { bar } from "my-module/feature-b"
179```
180
181For 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
193Read 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
197Node.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
207Becomes the following importmap
208
209```json
210{
211 "imports": {
212 "./feature/": "./feature/"
213 }
214}
215```
216
217However 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
228Code below generate an import map from node_modules + a file + an inline importmap.
229
230```js
231import {
232 getImportMapFromNodeModules,
233 getImportMapFromFile,
234 generateImportMapForProject,
235} from "@jsenv/node-module-import-map"
236
237const projectDirectoryUrl = new URL("./", import.meta.url)
238const customImportMapFileUrl = new URL("./import-map-custom.importmap", projectDirectoryUrl)
239const importMapInputs = [
240 getImportMapFromNodeModules({
241 projectDirectoryUrl,
242 projectPackageDevDependenciesIncluded: true,
243 }),
244 getImportMapFromFile(customImportMapFileUrl),
245 {
246 imports: {
247 foo: "./bar.js",
248 },
249 },
250]
251
252await 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
286import { getImportMapFromNodeModules } from "@jsenv/node-module-import-map"
287
288const 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
310It 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
332When 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
337Favoring `"browser"` export:
338
339```js
340import { getImportMapFromNodeModules } from "@jsenv/node-module-import-map"
341
342const importMap = await getImportMapFromNodeModules({
343 projectDirectoryUrl: new URL("./", import.meta.url),
344 packagesExportsPreference: ["browser"],
345})
346```
347
348Favoring `"electron"` and fallback to `"browser"`:
349
350```js
351import { getImportMapFromNodeModules } from "@jsenv/node-module-import-map"
352
353const 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
369import { getImportMapFromFile } from "@jsenv/node-module-import-map"
370
371const importMapFileUrl = new URL("./import-map.importmap", import.meta.url)
372const 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
387It 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
391We do this because import map are used on the web where a file outside project directory cannot be reached.
392
393In 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
397This part explains how to setup a real environment to see `@jsenv/node-module-import-map` in action.
398It 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
406git clone https://github.com/jsenv/jsenv-node-module-import-map.git
407```
408
409```console
410cd ./jsenv-node-module-import-map/docs/basic-project
411```
412
413```console
414npm install
415```
416
417</details>
418
419<details>
420 <summary>Step 2 - Generate project importMap</summary>
421
422Running command below will log importMap generated for that basic project.
423
424```console
425node ./generate-import-map.js
426```
427
428</details>