vite-import-maps 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # vite-plugin-native-import-maps
2
+
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 2490e79: Improve importMapHtmlTransformer signature
8
+
9
+ ## 0.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 9bd2ce2: Fix missing types in published package
14
+
15
+ ## 0.1.0
16
+
17
+ ### Minor Changes
18
+
19
+ - d8d4a14: Add support for vite7. SSR improvements
20
+ - 9f40567: Add support for import maps `integrity` field
21
+
22
+ ## 0.0.4
23
+
24
+ ### Patch Changes
25
+
26
+ - 4016f08: add support to output import maps as a file
27
+ - 91389f1: Add `virtual-modules` strategy in build mode. Add support to local entries as import maps
28
+
29
+ ## 0.0.3
30
+
31
+ ### Patch Changes
32
+
33
+ - add repository information to npm
34
+
35
+ ## 0.0.2
36
+
37
+ ### Patch Changes
38
+
39
+ - publish dist folder
40
+
41
+ ## 0.0.1
42
+
43
+ ### Patch Changes
44
+
45
+ - ac41be4: First release
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Riccardo Perra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,343 @@
1
+ <h1 align="center">vite-import-maps</h1>
2
+ <br/>
3
+ <p align="center">
4
+ <a href="https://npmjs.com/package/vite-plugin-native-import-maps"><img src="https://img.shields.io/npm/v/vite-plugin-native-import-maps.svg" alt="npm package"></a>
5
+ <a href="https://github.com/riccardoperra/vite-plugin-import-maps/actions/workflows/ci.yml"><img src="https://github.com/riccardoperra/vite-plugin-import-maps/actions/workflows/release.yml/badge.svg?branch=main" alt="build status"></a>
6
+ </p>
7
+
8
+ A Vite plugin that generates and keeps **browser import maps** in sync with your Vite dev server and production build.
9
+
10
+ It's aimed at **micro-frontends**, **plugin systems**, and any setup where you load ESM modules at runtime and want to:
11
+
12
+ - Share dependencies (React, Solid, etc.) **without relying on CDNs**
13
+ - Avoid bundling multiple copies of the same library
14
+ - Expose npm packages or **your own local entry modules** through an import map
15
+ - Keep **remote modules truly "native"**: remotes can be plain ESM files **without requiring you** to setup build step or use other plugins.
16
+
17
+ ---
18
+
19
+ ## Table of Contents
20
+
21
+ - [Install](#install)
22
+ - [Setup](#setup)
23
+ - [Configuration](#configuration)
24
+ - [Do You Need This Plugin?](#do-you-need-this-plugin)
25
+ - [Recipes](#recipes)
26
+ - [Troubleshooting](#troubleshooting)
27
+ - [How It Works](#how-it-works)
28
+ - [Examples](#examples)
29
+ - [License](#license)
30
+
31
+ ---
32
+
33
+ ## Install
34
+
35
+ ```shell
36
+ # pnpm
37
+ pnpm i -D vite-import-maps
38
+
39
+ # npm
40
+ npm i -D vite-import-maps
41
+
42
+ # yarn
43
+ yarn add -D vite-import-maps
44
+ ```
45
+
46
+ ## Setup
47
+
48
+ ```ts
49
+ import { defineConfig } from "vite";
50
+ import { vitePluginNativeImportMaps } from "vite-plugin-native-import-maps";
51
+
52
+ // Host app configuration
53
+ export default defineConfig({
54
+ plugins: [
55
+ vitePluginNativeImportMaps({
56
+ // Add SRI hashes to verify module integrity in build
57
+ integrity: 'sha-384',
58
+ log: true,
59
+ imports: [
60
+ // Wanna expose react with import maps?
61
+ "react",
62
+ "react-dom",
63
+ // Expose a custom/local entry under a public specifier
64
+ { name: "react/jsx-runtime", entry: "./src/custom-jsx-runtime.ts" },
65
+ { name: "my-app-shared-lib", entry: "./src/my-app-shared-oib.ts" },
66
+ ],
67
+ }),
68
+ ],
69
+ });
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Configuration
75
+
76
+ ### Options
77
+
78
+ - `imports` — List of modules to expose via the import map. Each entry can be a string (the specifier to expose, e.g.
79
+ `"react"`) or an object with `name` (the specifier), `entry` (the local path or package to resolve), and optionally
80
+ `integrity` (enable SRI hash).
81
+
82
+ - `modulesOutDir` — Directory prefix for emitted shared chunks in production. Defaults to `""` (root of output
83
+ directory).
84
+
85
+ - `integrity` —
86
+ Enable [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap#integrity_metadata_map)
87
+ for all shared dependencies. Set to `true`, `"sha256"`, `"sha384"`, or `"sha512"`. This adds an `integrity` map to the
88
+ import map so browsers can verify module contents. Can also be configured per-dependency via the object form in
89
+ `imports`.
90
+
91
+ - `log` — Enable debug logging. Defaults to `false`.
92
+
93
+ - `injectImportMapsToHtml` — Automatically inject a `<script type="importmap">` into the HTML `<head>`. Defaults to
94
+ `true`. Set to `false` for SSR apps and use the `virtual:importmap` module instead.
95
+
96
+ - `importMapHtmlTransformer` — A function to transform the resolved `imports` object before injecting into HTML. Useful
97
+ for adding a base path prefix, rewriting URLs to a CDN, or filtering entries.
98
+
99
+ - `outputAsFile` — Emit the import map as a standalone JSON file. Set to `true` for `/import-map.json`, or provide a
100
+ custom name (e.g. `"my-map"` → `/my-map.json`). The file is served by Vite in dev and emitted as an asset in build.
101
+
102
+ ---
103
+
104
+ ## Do You Need This Plugin?
105
+
106
+ If you're considering [import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap),
107
+ you're likely building one of the following:
108
+
109
+ - **Micro-frontend architecture** — A host app loads remote modules at runtime, and all parts need to share the same dependency instances (React, Solid, etc.)
110
+ - **Plugin system** — Your app dynamically loads user-provided or third-party modules that rely on shared libraries
111
+ - **Self-hosted dependency sharing** — You want to share dependencies across apps without relying on external CDN services like esm.sh or jspm.io
112
+
113
+ ### Why Not Third-Party Services?
114
+
115
+ Services like [esm.sh](https://esm.sh) or [jspm.io](https://jspm.io) are convenient, but they come with trade-offs:
116
+
117
+ - **External dependency** — Your app relies on a third-party service you don't control. If it goes down or changes, your app breaks.
118
+ - **Network restrictions** — Many corporate environments, VPNs, and air-gapped networks block connections to public services. Your app simply won't work.
119
+ - **Version alignment** — Ensuring host and remotes use the exact same dependency version from an external source can be error-prone.
120
+ - **Limited flexibility** — You can't easily expose modified builds, subsets of exports, or local wrapper modules.
121
+
122
+ With this plugin, **your host app becomes the source of truth**. Shared dependencies are built and served from your own infrastructure.
123
+
124
+ ### Why This Plugin?
125
+
126
+ **Works in both development and production**
127
+
128
+ Most import map solutions only work at build time. This plugin keeps the import map in sync with Vite's dev server _and_ production builds. During development, it resolves to Vite's optimized deps; in production, it points to the correct hashed chunk filenames. No manual updates, no mismatches.
129
+
130
+ **No build step required for remotes**
131
+
132
+ Remote modules can be plain ESM files—no bundler, no plugins, no special conventions. They just `import "your-lib"` and the browser resolves it via the import map provided by the host.
133
+
134
+ **Single dependency instance**
135
+
136
+ Host and all remotes share the exact same module instances.
137
+
138
+ **Full control over what you share**
139
+
140
+ Expose npm packages as-is, or provide custom wrapper modules, modified builds, or local files. You decide exactly what each specifier resolves to.
141
+
142
+ > **Note:** If a remote _does_ use a bundler, shared dependencies must be marked as **external**.
143
+ > Otherwise the remote bundles its own copy and you lose the single-instance benefit.
144
+
145
+ Import maps are simple in concept, but keeping them in sync with your build is tedious:
146
+
147
+ - In dev, Vite serves optimized deps from `node_modules/.vite/deps` with cache-busting hashes
148
+ - In production, chunks have content hashes in their filenames
149
+ - Manually updating the import map every time something changes is error-prone
150
+
151
+ This plugin handles all of that. You declare what to share, and it generates the correct import map for both dev and build—automatically.
152
+
153
+ **Example output:**
154
+
155
+ ```html
156
+ <script type="importmap">
157
+ {
158
+ "imports": {
159
+ "react": "/shared/react-DyndEn3u.js",
160
+ "react/jsx-runtime": "/shared/react_jsx-runtime-CAvv468t.js"
161
+ }
162
+ }
163
+ </script>
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Recipes
169
+
170
+ ### Expose Local Entry Points (Custom ESM Wrappers)
171
+
172
+ Expose a local file that re-exports a dependency, giving you full control over what gets shared:
173
+
174
+ ```ts
175
+ vitePluginNativeImportMaps({
176
+ imports: [
177
+ { name: "react", entry: "./src/react-esm.ts" },
178
+ { name: "react/jsx-runtime", entry: "./src/react-jsx-runtime.ts" },
179
+ "react-dom",
180
+ ],
181
+ modulesOutDir: "shared",
182
+ });
183
+ ```
184
+
185
+ ---
186
+
187
+ ### Enable Integrity Checks
188
+
189
+ Add SRI hashes to verify module integrity:
190
+
191
+ ```ts
192
+ vitePluginNativeImportMaps({
193
+ imports: ["react", "react-dom"],
194
+ integrity: "sha384", // applies to all
195
+ });
196
+
197
+ // Or per-dependency:
198
+ vitePluginNativeImportMaps({
199
+ imports: [
200
+ { name: "react", entry: "react", integrity: "sha384" },
201
+ { name: "react-dom", entry: "react-dom", integrity: false },
202
+ ],
203
+ });
204
+ ```
205
+
206
+ ---
207
+
208
+ ### Mark Shared Deps as `external` in Remote Builds
209
+
210
+ If a remote module uses a bundler, configure shared dependencies as `external` to prevent bundling them:
211
+
212
+ **tsdown example:**
213
+
214
+ ```ts
215
+ import { defineConfig } from "tsdown";
216
+
217
+ export default defineConfig({
218
+ external: ["react", "react-dom", "react/jsx-runtime"],
219
+ });
220
+ ```
221
+
222
+ **Vite (library mode) example:**
223
+
224
+ ```ts
225
+ import { defineConfig } from "vite";
226
+
227
+ export default defineConfig({
228
+ build: {
229
+ lib: {
230
+ entry: "./src/index.ts",
231
+ formats: ["es"],
232
+ },
233
+ rollupOptions: {
234
+ external: ["react", "react-dom", "react/jsx-runtime"],
235
+ },
236
+ },
237
+ });
238
+ ```
239
+
240
+ ---
241
+
242
+ ### Serve Import Map as JSON File
243
+
244
+ ```ts
245
+ vitePluginNativeImportMaps({
246
+ imports: ["react"],
247
+ outputAsFile: true, // /import-map.json
248
+ });
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Troubleshooting
254
+
255
+ ### SSR App Doesn't Show the Import Map
256
+
257
+ Set `injectImportMapsToHtml: false` and inject the import map yourself using `virtual:importmap`:
258
+
259
+ ```ts
260
+ import importMap from "virtual:importmap";
261
+ // Inject into your SSR HTML template
262
+ ```
263
+
264
+ ---
265
+
266
+ ### Specifier Resolves to the Wrong Module
267
+
268
+ Ensure the specifier matches exactly what your code imports:
269
+
270
+ - `react/jsx-runtime` ≠ `react`
271
+ - `solid-js/web` ≠ `solid-js`
272
+
273
+ ---
274
+
275
+ ### Import Maps Not Supported in Target Browser
276
+
277
+ Import maps require modern browsers. For broader support, use a polyfill
278
+ like [es-module-shims](https://github.com/guybedford/es-module-shims).
279
+
280
+ See the example: [`./examples/react-host-es-module-shims`](./examples/react-host-es-module-shims)
281
+
282
+ ### Integrate with es-module-shims (dynamic import maps)
283
+
284
+ You can integrate this plugin with **es-module-shims** in two common ways depending on how you want import maps applied at runtime:
285
+
286
+ - **Apply import maps dynamically at runtime** — If you prefer the plugin to emit a JSON file (use `outputAsFile: true`) or to use the `virtual:importmap` module, you can fetch or import the map and pass it to the es-module-shims runtime via the global `importShim` API (it exposes helpers like `addImportMap` and `import`).
287
+
288
+ ```ts
289
+ import "es-module-shims";
290
+
291
+ // When using outputAsFile: true (e.g. /import-map.json)
292
+ fetch("/import-map.json")
293
+ .then((r) => r.json())
294
+ .then((map) => importShim.addImportMap(map))
295
+ .then(() => importShim.import("/your/entry.js"));
296
+ ```
297
+
298
+ Example (virtual module):
299
+
300
+ ```js
301
+ import "es-module-shims";
302
+ import importMap from "virtual:importmap";
303
+
304
+ importShim.addImportMap(importMap).then(() => {
305
+ // now safe to dynamically import shimmed modules
306
+ });
307
+ ```
308
+
309
+ ---
310
+
311
+ ## How It Works
312
+
313
+ 1. **Collects** the `shared` entries from your config
314
+ 2. **In dev:** Resolves corresponding Vite dev-server URLs
315
+ 3. **In build:** Adds extra Rollup inputs so shared deps get dedicated output chunks, then records the final chunk URLs
316
+ 4. **Exposes** the mapping via:
317
+ - HTML injection (optional)
318
+ - `virtual:importmap` module (always)
319
+ - JSON file (optional)
320
+
321
+ **Build snapshot:**
322
+
323
+ - [`./test/fixture/basic`](./test/fixture/basic)
324
+ - [`./test/__snapshot__/build-project-with-right-import-maps`](./test/__snapshot__/build-project-with-right-import-maps)
325
+
326
+ ---
327
+
328
+ ## Examples
329
+
330
+ | Example | Description |
331
+ | --------------------------------------------------------------------- | ---------------------------------------- |
332
+ | [`solidjs-host`](./examples/solidjs-host) | Solid.js host app |
333
+ | [`solidjs-remote-counter`](./examples/solidjs-remote-counter) | Solid.js remote module |
334
+ | [`react-host-custom`](./examples/react-host-custom) | React host with custom ESM wrappers |
335
+ | [`react-host-es-module-shims`](./examples/react-host-es-module-shims) | React host with es-module-shims polyfill |
336
+ | [`react-remote-counter`](./examples/react-remote-counter) | React remote module |
337
+ | [`react-tanstack-start-ssr`](./examples/react-tanstack-start-ssr) | SSR example with TanStack Start |
338
+
339
+ ---
340
+
341
+ ## License
342
+
343
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,125 @@
1
+ import { Plugin } from "vite";
2
+
3
+ //#region src/store.d.ts
4
+ interface RegisteredDependency {
5
+ packageName: string;
6
+ url: string;
7
+ integrity?: string;
8
+ }
9
+ interface NormalizedDependencyInput {
10
+ name: string;
11
+ entry: string;
12
+ localFile: boolean;
13
+ integrity: DependencyIntegrityCheck | boolean;
14
+ }
15
+ declare class VitePluginImportMapsStore {
16
+ readonly defaultIntegrity: boolean | DependencyIntegrityCheck;
17
+ readonly sharedDependencies: ReadonlyArray<NormalizedDependencyInput>;
18
+ readonly modulesOutDir: string;
19
+ readonly log: boolean;
20
+ readonly importMapHtmlTransformer: ImportMapTransformerFn;
21
+ readonly importMapDependencies: Map<string, RegisteredDependency>;
22
+ readonly inputs: Array<ImportMapBuildChunkEntrypoint>;
23
+ constructor(options: VitePluginImportMapsConfig);
24
+ private normalizeDependencyInput;
25
+ clearDependencies(): void;
26
+ addDependency(dependency: RegisteredDependency): void;
27
+ getNormalizedDependencyName(dependency: string): string;
28
+ getEntrypointPath(entrypoint: string): string;
29
+ addInput(input: NormalizedDependencyInput): ImportMapBuildChunkEntrypoint;
30
+ getImportMapAsJson(): Record<string, any>;
31
+ }
32
+ interface ImportMapBuildChunkEntrypoint {
33
+ originalDependencyName: string;
34
+ normalizedDependencyName: string;
35
+ entrypoint: string;
36
+ idToResolve: string;
37
+ localFile: boolean;
38
+ integrity: DependencyIntegrityCheck | boolean;
39
+ }
40
+ //#endregion
41
+ //#region src/config.d.ts
42
+ interface ImportMapSignature {
43
+ /**
44
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap#imports
45
+ */
46
+ imports?: Record<string, any>;
47
+ /**
48
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap#integrity
49
+ */
50
+ integrity?: Record<string, string>;
51
+ /**
52
+ * @see @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap#scopes
53
+ */
54
+ scopes?: Record<string, any>;
55
+ }
56
+ type ImportMapTransformerFn = (importMap: ImportMapSignature, meta: {
57
+ entries: Map<string, RegisteredDependency>;
58
+ store: VitePluginImportMapsStore;
59
+ }) => ImportMapSignature;
60
+ type DependencyIntegrityCheck = "sha256" | "sha384" | "sha512";
61
+ interface SharedDependencyObjectConfig {
62
+ /**
63
+ * The name of the dependency that will be resolved
64
+ */
65
+ name: string;
66
+ /**
67
+ * Local path to the entry file, or the dependency name (e.g., react)
68
+ */
69
+ entry: string;
70
+ /**
71
+ * Enable integrity check for the dependency (only in build)
72
+ *
73
+ * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap#integrity_metadata_map
74
+ */
75
+ integrity?: boolean | DependencyIntegrityCheck;
76
+ }
77
+ type SharedDependencyConfig = Array<string | SharedDependencyObjectConfig>;
78
+ interface VitePluginImportMapsConfig {
79
+ /**
80
+ * Dependencies shared by modules
81
+ */
82
+ imports: SharedDependencyConfig;
83
+ /**
84
+ * Directory where the shared chunks are stored
85
+ *
86
+ * @default ""
87
+ */
88
+ modulesOutDir?: string;
89
+ /**
90
+ * Default `integrity` value for entries.
91
+ * Can be customized per dependency through {@link SharedDependencyObjectConfig#integrity}
92
+ *
93
+ * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap#integrity_metadata_map
94
+ */
95
+ integrity?: boolean | DependencyIntegrityCheck;
96
+ /**
97
+ * Enable logging
98
+ */
99
+ log?: boolean;
100
+ /**
101
+ * Whether to inject the import map in to the main HTML file. Defaults to true.
102
+ *
103
+ * NOTE: You probably have to set `false` in apps with SSR enabled,
104
+ * and use the `virtual:importmap` dynamic import instead.
105
+ */
106
+ injectImportMapsToHtml?: boolean;
107
+ /**
108
+ * Transform the resolved import map `imports` before writing it to the HTML file
109
+ */
110
+ importMapHtmlTransformer?: ImportMapTransformerFn;
111
+ /**
112
+ * Whether to generate an import file.
113
+ *
114
+ * If a string is provided, it will be used as the output file name. Default as 'import-map.json'.
115
+ *
116
+ * Output file will be generated in the root directory of your generated bundle and will be
117
+ * available also in development mode via the Vite Dev Server.
118
+ */
119
+ outputAsFile?: boolean | string;
120
+ }
121
+ //#endregion
122
+ //#region src/index.d.ts
123
+ declare function vitePluginNativeImportMaps(options: VitePluginImportMapsConfig): Array<Plugin>;
124
+ //#endregion
125
+ export { vitePluginNativeImportMaps };
package/dist/index.mjs ADDED
@@ -0,0 +1,364 @@
1
+ import path from "node:path";
2
+ import { normalizePath } from "vite";
3
+ import { createHash } from "node:crypto";
4
+
5
+ //#region src/utils.ts
6
+ /**
7
+ * Normalize a dependency name to be used as an entrypoint input
8
+ *
9
+ * @example
10
+ * ```
11
+ * @scope/package-name -> @scope_package-name
12
+ * package-name/sub-entrypoint -> package-name_sub-entrypoint
13
+ * ```
14
+ */
15
+ function normalizeDependencyName(dep) {
16
+ return dep.replace(/\//g, "_");
17
+ }
18
+ /**
19
+ * Prefix for resolved fs paths, since windows paths may not be valid as URLs.
20
+ *
21
+ * @see https://github.com/vitejs/vite/blob/fd38d076fe2455aac1e00a7b15cd51159bf12bb5/packages/vite/src/node/constants.ts#L108
22
+ */
23
+ const FS_PREFIX = `/@fs/`;
24
+ function fileToUrl(file, root) {
25
+ const url = path.relative(root, file);
26
+ if (url[0] === ".") return path.posix.join(FS_PREFIX, normalizePath(file));
27
+ return "/" + normalizePath(url);
28
+ }
29
+
30
+ //#endregion
31
+ //#region src/store.ts
32
+ var VitePluginImportMapsStore = class {
33
+ defaultIntegrity;
34
+ sharedDependencies = [];
35
+ modulesOutDir = "";
36
+ log;
37
+ importMapHtmlTransformer = (importMap) => importMap;
38
+ importMapDependencies = /* @__PURE__ */ new Map();
39
+ inputs = [];
40
+ constructor(options) {
41
+ this.defaultIntegrity = options.integrity || false;
42
+ this.sharedDependencies = [...options.imports.map(this.normalizeDependencyInput)];
43
+ this.log = options.log || false;
44
+ if (options.modulesOutDir) this.modulesOutDir = options.modulesOutDir;
45
+ if (options.importMapHtmlTransformer) this.importMapHtmlTransformer = options.importMapHtmlTransformer;
46
+ }
47
+ normalizeDependencyInput = (entry) => {
48
+ if (typeof entry === "string") return {
49
+ name: entry,
50
+ entry,
51
+ localFile: false,
52
+ integrity: this.defaultIntegrity
53
+ };
54
+ return {
55
+ name: entry.name,
56
+ entry: entry.entry,
57
+ localFile: entry.entry.startsWith("./") || entry.entry.startsWith("../"),
58
+ integrity: entry.integrity ?? this.defaultIntegrity
59
+ };
60
+ };
61
+ clearDependencies() {
62
+ this.importMapDependencies.clear();
63
+ }
64
+ addDependency(dependency) {
65
+ this.importMapDependencies.set(dependency.packageName, dependency);
66
+ }
67
+ getNormalizedDependencyName(dependency) {
68
+ return normalizeDependencyName(dependency);
69
+ }
70
+ getEntrypointPath(entrypoint) {
71
+ return path.join(this.modulesOutDir, entrypoint);
72
+ }
73
+ addInput(input) {
74
+ const dependency = input.name;
75
+ const normalizedDepName = this.getNormalizedDependencyName(dependency);
76
+ const meta = {
77
+ originalDependencyName: dependency,
78
+ entrypoint: this.getEntrypointPath(normalizedDepName),
79
+ normalizedDependencyName: normalizedDepName,
80
+ idToResolve: input.entry,
81
+ localFile: input.localFile,
82
+ integrity: input.integrity
83
+ };
84
+ this.inputs.push(meta);
85
+ return meta;
86
+ }
87
+ getImportMapAsJson() {
88
+ const imports = {};
89
+ const integrity = {};
90
+ this.importMapDependencies.forEach((dep) => {
91
+ imports[dep.packageName] = dep.url;
92
+ if (dep.integrity) integrity[dep.url] = dep.integrity;
93
+ });
94
+ const importMap = { imports };
95
+ if (Object.keys(integrity).length > 0) importMap.integrity = integrity;
96
+ return this.importMapHtmlTransformer(importMap, {
97
+ store: this,
98
+ entries: this.importMapDependencies
99
+ });
100
+ }
101
+ };
102
+
103
+ //#endregion
104
+ //#region src/config.ts
105
+ const PLUGIN_NAME = "vite-plugin-import-maps";
106
+ function pluginName(name) {
107
+ return `${PLUGIN_NAME}:${name}`;
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/build-only/virtual-chunk-resolver.ts
112
+ const VIRTUAL_ID_PREFIX = `\0virtual:import-map-chunk`;
113
+ function getVirtualFileName(name) {
114
+ return `${VIRTUAL_ID_PREFIX}/${name}`;
115
+ }
116
+ function virtualChunksResolverPlugin(store) {
117
+ return {
118
+ name: pluginName("build:virtual-chunks-loader"),
119
+ apply: "build",
120
+ resolveId(id) {
121
+ if (this.environment.name === "ssr") return;
122
+ if (id.startsWith(VIRTUAL_ID_PREFIX)) {
123
+ const normalizedId = id.slice(VIRTUAL_ID_PREFIX.length + 1);
124
+ return {
125
+ id,
126
+ meta: { info: store.inputs.find((input) => input.normalizedDependencyName === normalizedId) }
127
+ };
128
+ }
129
+ },
130
+ async load(id) {
131
+ if (this.environment.name === "ssr") return;
132
+ if (!id.startsWith(VIRTUAL_ID_PREFIX)) return;
133
+ const virtualModuleInfo = this.getModuleInfo(id);
134
+ if (!virtualModuleInfo) return;
135
+ const chunk = virtualModuleInfo.meta["info"];
136
+ const resolvedId = await this.resolve(chunk.idToResolve);
137
+ if (!resolvedId) return;
138
+ let hasDefaultExport = false;
139
+ const [fileName] = resolvedId.id.split("?");
140
+ const moduleInfo = this.getModuleInfo(fileName);
141
+ if (moduleInfo) {
142
+ hasDefaultExport = moduleInfo.hasDefaultExport ?? false;
143
+ if (!hasDefaultExport) {
144
+ if ("commonjs" in moduleInfo.meta && moduleInfo.meta.commonjs.isCommonJS) {
145
+ const requires = moduleInfo.meta.commonjs.requires;
146
+ if (Array.isArray(requires)) {
147
+ for (const require of requires) if (require.resolved) {
148
+ const innerResolvedId = this.getModuleInfo(require.resolved.id);
149
+ if (!innerResolvedId) break;
150
+ hasDefaultExport = innerResolvedId.hasDefaultExport || false;
151
+ if (hasDefaultExport) break;
152
+ if (innerResolvedId.exports?.includes("__require")) {
153
+ hasDefaultExport = true;
154
+ break;
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ let code = `export * from "${chunk.originalDependencyName}"`;
162
+ if (hasDefaultExport) code += `\nexport { default } from '${chunk.originalDependencyName}'`;
163
+ return {
164
+ moduleSideEffects: "no-treeshake",
165
+ code
166
+ };
167
+ }
168
+ };
169
+ }
170
+
171
+ //#endregion
172
+ //#region src/build-only/virtual-chunk-generator.ts
173
+ function virtualChunksGeneratorPlugin(store) {
174
+ const name = pluginName("build:virtual");
175
+ const virtualModules = /* @__PURE__ */ new Map();
176
+ const localModules = /* @__PURE__ */ new Map();
177
+ let config;
178
+ return {
179
+ name,
180
+ apply: "build",
181
+ configResolved(resolvedConfig) {
182
+ config = resolvedConfig;
183
+ },
184
+ buildStart() {
185
+ for (const input of store.inputs) if (input.localFile) {
186
+ const id = path.resolve(input.idToResolve);
187
+ if (!localModules.has(id)) this.emitFile({
188
+ type: "chunk",
189
+ name: input.entrypoint,
190
+ id,
191
+ preserveSignature: "strict"
192
+ });
193
+ localModules.set(id, input);
194
+ } else {
195
+ const id = getVirtualFileName(input.normalizedDependencyName);
196
+ if (!virtualModules.has(id)) this.emitFile({
197
+ type: "chunk",
198
+ name: input.entrypoint,
199
+ id,
200
+ preserveSignature: "strict"
201
+ });
202
+ virtualModules.set(id, input);
203
+ }
204
+ },
205
+ generateBundle(_, bundle) {
206
+ store.clearDependencies();
207
+ const keys = Object.keys(bundle);
208
+ for (const key of keys) {
209
+ const entry = bundle[key];
210
+ if (entry.type !== "chunk") continue;
211
+ const handledModules = new Map([...virtualModules.entries(), ...localModules.entries()]);
212
+ if (entry.facadeModuleId && (entry.facadeModuleId.startsWith(VIRTUAL_ID_PREFIX) || path.isAbsolute(entry.facadeModuleId))) {
213
+ const entryImportMap = handledModules.get(entry.facadeModuleId);
214
+ if (!entryImportMap) continue;
215
+ entry.isEntry = false;
216
+ let integrity;
217
+ if (entryImportMap.integrity !== false) {
218
+ const algorithm = typeof entryImportMap.integrity === "string" ? entryImportMap.integrity : "sha384";
219
+ integrity = `${algorithm}-${createHash(algorithm).update(entry.code).digest("base64")}`;
220
+ }
221
+ const url = `./${entry.fileName}`, packageName = entryImportMap.originalDependencyName;
222
+ store.addDependency({
223
+ url,
224
+ packageName,
225
+ integrity
226
+ });
227
+ store.log && config.logger.info(`[${name}] Added ${packageName}: ${url}`, { timestamp: true });
228
+ }
229
+ }
230
+ }
231
+ };
232
+ }
233
+
234
+ //#endregion
235
+ //#region src/build-only/build-plugin.ts
236
+ function pluginImportMapsBuildEnv(store) {
237
+ const plugins = [];
238
+ for (const dep of store.sharedDependencies) store.addInput(dep);
239
+ plugins.push(virtualChunksGeneratorPlugin(store));
240
+ plugins.push(virtualChunksResolverPlugin(store));
241
+ return plugins;
242
+ }
243
+
244
+ //#endregion
245
+ //#region src/import-map-html.ts
246
+ function pluginImportMapsInject(store) {
247
+ return {
248
+ name: pluginName("inject-html-import-map"),
249
+ transformIndexHtml(source) {
250
+ const importMap = store.getImportMapAsJson();
251
+ return {
252
+ html: source,
253
+ tags: [{
254
+ tag: "script",
255
+ attrs: { type: "importmap" },
256
+ children: JSON.stringify(importMap),
257
+ injectTo: "head-prepend"
258
+ }]
259
+ };
260
+ }
261
+ };
262
+ }
263
+
264
+ //#endregion
265
+ //#region src/dev/dev-plugin.ts
266
+ function pluginImportMapsDevelopmentEnv(store) {
267
+ const name = pluginName("development");
268
+ let latestBrowserHash = void 0;
269
+ let cachedResolvedModules = [];
270
+ return {
271
+ name,
272
+ apply: "serve",
273
+ async transformIndexHtml(_, { server }) {
274
+ if (!server) return;
275
+ const { pluginContainer, config } = server, devOptimizer = server.environments["client"].depsOptimizer;
276
+ let resolvedModules;
277
+ if (devOptimizer.metadata.browserHash === latestBrowserHash) resolvedModules = cachedResolvedModules;
278
+ else resolvedModules = (await Promise.all(store.sharedDependencies.map(async (dependency) => {
279
+ const resolvedId = await pluginContainer.resolveId(dependency.entry);
280
+ if (!resolvedId) return null;
281
+ const path = fileToUrl(resolvedId.id, config.root);
282
+ store.log && server.config.logger.info(`[${name}] Added ${dependency.entry}: ${path}`, { timestamp: true });
283
+ return {
284
+ name: dependency.name,
285
+ path
286
+ };
287
+ }))).filter((value) => !!value);
288
+ cachedResolvedModules = resolvedModules;
289
+ latestBrowserHash = devOptimizer.metadata.browserHash;
290
+ for (const { path: url, name: packageName } of resolvedModules) store.addDependency({
291
+ packageName,
292
+ url
293
+ });
294
+ }
295
+ };
296
+ }
297
+
298
+ //#endregion
299
+ //#region src/import-map-file.ts
300
+ function pluginImportMapsAsFile(store, options) {
301
+ const { name = "import-map" } = options;
302
+ return {
303
+ name: pluginName("import-maps-as-file"),
304
+ configureServer(server) {
305
+ server.middlewares.use((req, res, next) => {
306
+ if (req.url === `/${name}.json`) {
307
+ res.setHeader("Content-Type", "application/json");
308
+ res.end(JSON.stringify(store.getImportMapAsJson()));
309
+ } else next();
310
+ });
311
+ },
312
+ generateBundle() {
313
+ const json = store.getImportMapAsJson();
314
+ this.emitFile({
315
+ type: "asset",
316
+ fileName: `/${name}.json`,
317
+ source: JSON.stringify(json, null, 2)
318
+ });
319
+ }
320
+ };
321
+ }
322
+
323
+ //#endregion
324
+ //#region src/import-map-module.ts
325
+ const virtualImportMapId = "virtual:importmap";
326
+ const resolvedVirtualImportMapId = "\0" + virtualImportMapId;
327
+ function pluginImportMapsAsModule(store) {
328
+ return {
329
+ name: pluginName("virtual-module-import-map"),
330
+ resolveId(id) {
331
+ if (id === virtualImportMapId) return resolvedVirtualImportMapId;
332
+ },
333
+ load(id) {
334
+ if (id === resolvedVirtualImportMapId) {
335
+ const content = JSON.stringify(store.getImportMapAsJson());
336
+ return `
337
+ export const importMapRaw = '${content}';
338
+ export const importMap = ${content};
339
+ export default importMap;
340
+ `;
341
+ }
342
+ }
343
+ };
344
+ }
345
+
346
+ //#endregion
347
+ //#region src/index.ts
348
+ function vitePluginNativeImportMaps(options) {
349
+ const { injectImportMapsToHtml = true, outputAsFile } = options;
350
+ const plugins = [];
351
+ const store = new VitePluginImportMapsStore(options);
352
+ plugins.push(...pluginImportMapsBuildEnv(store));
353
+ plugins.push(pluginImportMapsDevelopmentEnv(store));
354
+ if (injectImportMapsToHtml) plugins.push(pluginImportMapsInject(store));
355
+ plugins.push(pluginImportMapsAsModule(store));
356
+ if (outputAsFile) {
357
+ const name = typeof outputAsFile === "string" ? outputAsFile : void 0;
358
+ plugins.push(pluginImportMapsAsFile(store, { name }));
359
+ }
360
+ return plugins;
361
+ }
362
+
363
+ //#endregion
364
+ export { vitePluginNativeImportMaps };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "vite-import-maps",
3
+ "version": "0.1.2",
4
+ "description": "A Vite plugin that manages import maps for shared dependencies in your Vite applications.",
5
+ "main": "./dist/index.mjs",
6
+ "module ": "./dist/index.mjs",
7
+ "types": "./dist/index.d.mts",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.mts",
12
+ "default": "./dist/index.mjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "CHANGELOG.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "vite",
22
+ "vite-plugin",
23
+ "import-maps",
24
+ "mfe",
25
+ "import maps",
26
+ "microfrontend",
27
+ "plugin"
28
+ ],
29
+ "repository": "https://github.com/riccardoperra/vite-plugin-import-maps",
30
+ "homepage": "https://github.com/riccardoperra/vite-plugin-import-maps",
31
+ "author": {
32
+ "name": "Riccardo Perra",
33
+ "email": "riccardo.perra@icloud.com",
34
+ "url": "https://github.com/riccardoperra"
35
+ },
36
+ "license": "MIT",
37
+ "dependencies": {
38
+ "vite": "^7.3.1"
39
+ },
40
+ "devDependencies": {
41
+ "@changesets/cli": "^2.29.8",
42
+ "@tanstack/eslint-config": "^0.3.4",
43
+ "@types/node": "^25.2.0",
44
+ "eslint": "^9.39.2",
45
+ "prettier": "^3.8.1",
46
+ "rollup": "^4.57.1",
47
+ "tsdown": "^0.20.1",
48
+ "typescript": "^5.9.3",
49
+ "vitest": "^4.0.18"
50
+ },
51
+ "peerDependencies": {
52
+ "vite": ">=7.0.0"
53
+ },
54
+ "scripts": {
55
+ "test": "vitest --config ./vitest.config.ts",
56
+ "build": "tsdown",
57
+ "format": "prettier --write .",
58
+ "ci:publish": "changeset publish",
59
+ "ci:version": "changeset version"
60
+ }
61
+ }