├── .babelrc
├── .changeset
├── README.md
└── config.json
├── .github
├── FUNDING.yml
└── workflows
│ └── build-test-release.yml
├── .gitignore
├── .husky
└── pre-commit
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs
├── api.md
├── configuration.md
├── installation.md
├── security.md
└── ui.md
├── eslint.config.mjs
├── jest.config.cjs
├── jest.setup.js
├── package.json
├── pnpm-lock.yaml
├── rollup.config.js
├── serve.json
├── src
├── api
│ ├── js-api.domainsElement.test.js
│ ├── js-api.external.test.js
│ ├── js-api.imo.test.js
│ ├── js-api.js
│ ├── js-api.test.js
│ └── local-storage-mock.js
├── import-map-overrides-api.js
├── import-map-overrides-server.js
├── import-map-overrides.js
├── server
│ ├── server-api.js
│ └── server-api.test.js
├── ui
│ ├── custom-elements.js
│ ├── dev-lib-overrides.component.js
│ ├── full-ui.component.js
│ ├── import-map-overrides.css
│ ├── list
│ │ ├── external-importmap-dialog.component.js
│ │ ├── list.component.js
│ │ └── module-dialog.component.js
│ └── popup.component.js
└── util
│ ├── includes.js
│ ├── includes.test.js
│ ├── string-regex.js
│ ├── string-regex.test.js
│ ├── url-parameter.js
│ └── url-parameter.test.js
└── test
├── deny-list.html
├── embedded-map.html
├── ie11.html
├── importmap.json
├── index.html
├── no-map.html
├── server-map.html
└── url.html
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | ],
5 | "plugins": [
6 | ["@babel/plugin-transform-react-jsx", { "pragma": "h" }],
7 | "@babel/plugin-proposal-class-properties",
8 | ],
9 | }
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | { "repo": "single-spa/import-map-overrides" }
6 | ],
7 | "commit": false,
8 | "fixed": [],
9 | "linked": [],
10 | "access": "public",
11 | "baseBranch": "origin/main",
12 | "updateInternalDependencies": "patch",
13 | "ignore": []
14 | }
15 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [jolyndenning]
4 | patreon: singlespa
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/workflows/build-test-release.yml:
--------------------------------------------------------------------------------
1 | name: Build, Test, Release
2 |
3 | on:
4 | push:
5 | branches: main
6 | pull_request:
7 | branches: "*"
8 |
9 | jobs:
10 | build_lint_test:
11 | name: Build, Lint, and Test
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout Repo
16 | uses: actions/checkout@v4
17 | with:
18 | # check out full history
19 | fetch-depth: 0
20 |
21 | - name: Install Pnpm
22 | uses: pnpm/action-setup@v4
23 |
24 | - name: Setup Node.js
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: 22
28 | cache: pnpm
29 |
30 | - name: Install dependencies
31 | run: pnpm install --frozen-lockfile
32 |
33 | - name: Validate
34 | run: |
35 | pnpm run lint
36 | pnpm run check-format
37 | pnpm exec changeset status --since=origin/main
38 |
39 | - name: Test
40 | run: pnpm run test
41 |
42 | - name: Build
43 | run: pnpm run build
44 |
45 | release:
46 | name: Release
47 | needs: build_lint_test
48 | permissions:
49 | contents: write # To push release tags
50 | pull-requests: write # To create release pull requests
51 | runs-on: ubuntu-latest
52 | if: ${{ github.ref == 'refs/heads/main' }}
53 | steps:
54 | - name: Checkout Repo
55 | uses: actions/checkout@v4
56 |
57 | - name: Install Pnpm
58 | uses: pnpm/action-setup@v4
59 |
60 | - name: Setup Node.js
61 | uses: actions/setup-node@v3
62 | with:
63 | node-version: 22
64 | cache: pnpm
65 |
66 | - name: Install dependencies
67 | run: pnpm install --frozen-lockfile
68 |
69 | - name: Build
70 | run: pnpm run build
71 |
72 | - name: Changesets
73 | run: pnpm exec changeset status
74 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 | dist
63 |
64 | # jetbrain IDE files
65 | /.idea
66 |
67 | # VSCode IDE files
68 | .vscode
69 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm exec concurrently pnpm:lint "pretty-quick --staged"
2 | pnpm run test --coverage --watchAll=false
3 |
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | yarn.lock
2 | LICENSE
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # import-map-overrides
2 |
3 | ## 6.0.1
4 |
5 | ### Patch Changes
6 |
7 | - [#137](https://github.com/single-spa/import-map-overrides/pull/137) [`c7dff64`](https://github.com/single-spa/import-map-overrides/commit/c7dff648e1e3158e73d1a511660ce8eeb3c12205) Thanks [@PopCristianGabriel](https://github.com/PopCristianGabriel)! - The url of the documentation reference in the popup pointed to an outdated hijacked repository"
8 |
9 | ## 6.0.0
10 |
11 | ### Major Changes
12 |
13 | - [#135](https://github.com/single-spa/import-map-overrides/pull/135) [`f503162`](https://github.com/single-spa/import-map-overrides/commit/f503162337a7225c2a5657eff46f9c7cded254ba) Thanks [@MehmetYararVX](https://github.com/MehmetYararVX)! - Bump cookie to 1.0.2 and add server-api tests
14 |
15 | ## 5.1.1
16 |
17 | ### Patch Changes
18 |
19 | - [#133](https://github.com/single-spa/import-map-overrides/pull/133) [`519da38`](https://github.com/single-spa/import-map-overrides/commit/519da380ae4ff481b11afd9fca802b906ac396e6) Thanks [@joeldenning](https://github.com/joeldenning)! - Fix popup not opening bug
20 |
21 | ## 5.1.0
22 |
23 | ### Minor Changes
24 |
25 | - [#131](https://github.com/single-spa/import-map-overrides/pull/131) [`1e066a2`](https://github.com/single-spa/import-map-overrides/commit/1e066a20185ba60c6cb64a15bd883bb1a19f7bb5) Thanks [@joeldenning](https://github.com/joeldenning)! - Support for style-nonce attribute on import-map-overrides-popup and import-map-overrides-list custom elements
26 |
27 | ### Patch Changes
28 |
29 | - [#131](https://github.com/single-spa/import-map-overrides/pull/131) [`1e066a2`](https://github.com/single-spa/import-map-overrides/commit/1e066a20185ba60c6cb64a15bd883bb1a19f7bb5) Thanks [@joeldenning](https://github.com/joeldenning)! - Fix CSS issue with dialogs/popups
30 |
31 | ## 5.0.0
32 |
33 | ### Major Changes
34 |
35 | - [#126](https://github.com/single-spa/import-map-overrides/pull/126) [`2241fed`](https://github.com/single-spa/import-map-overrides/commit/2241feddf19cac3c387b364ebec9ffc21fe10b6f) Thanks [@agevry](https://github.com/agevry)! - The css for import-map-overrides UI is no longer injected into the main page, but only within the shadow dom for the UI
36 |
37 | ### Minor Changes
38 |
39 | - [#126](https://github.com/single-spa/import-map-overrides/pull/126) [`2241fed`](https://github.com/single-spa/import-map-overrides/commit/2241feddf19cac3c387b364ebec9ffc21fe10b6f) Thanks [@agevry](https://github.com/agevry)! - Add style-nonce attribute to import-map-overrides-full element to support use under a Content Security Policy(CSP)
40 |
41 | ## 4.2.0
42 |
43 | ### Minor Changes
44 |
45 | - [#121](https://github.com/single-spa/import-map-overrides/pull/121) [`1381dc0`](https://github.com/single-spa/import-map-overrides/commit/1381dc01baba839c1366ec64afb5f8b70850fcc2) Thanks [@joeldenning](https://github.com/joeldenning)! - Add new getOverrideScopes API for inheriting scoped dependencies for overridden modules
46 |
47 | ## 4.1.0
48 |
49 | ### Minor Changes
50 |
51 | - [#117](https://github.com/single-spa/import-map-overrides/pull/117) [`a7c7970`](https://github.com/single-spa/import-map-overrides/commit/a7c79702f9a6bc17fdf47fe6f2d4806330bbcf6c) Thanks [@joeldenning](https://github.com/joeldenning)! - New `use-injector` attribute on `` element
52 |
53 | New `resetDefaultMap` js api
54 |
55 | ## 4.0.1
56 |
57 | ### Patch Changes
58 |
59 | - [#114](https://github.com/single-spa/import-map-overrides/pull/114) [`4b180f6`](https://github.com/single-spa/import-map-overrides/commit/4b180f6f34d9a7b6153838819e3b68861158bf39) Thanks [@joeldenning](https://github.com/joeldenning)! - Update inline override icon to avoid CSP issues
60 |
61 | ## 4.0.0
62 |
63 | ### Major Changes
64 |
65 | - [#104](https://github.com/single-spa/import-map-overrides/pull/104) [`fa6a22c`](https://github.com/single-spa/import-map-overrides/commit/fa6a22c27e786c88c314efe532871ff15d5089e0) Thanks [@artygus](https://github.com/artygus)! - Disable query parameter overrides, by default. Add support for `allow-query-param-override` attribute to `` element, to opt-in to query parameter overrides.
66 |
67 | - [#112](https://github.com/single-spa/import-map-overrides/pull/112) [`0b60e71`](https://github.com/single-spa/import-map-overrides/commit/0b60e71da26b762d023fd304d430ae39126f8643) Thanks [@joeldenning](https://github.com/joeldenning)! - Upgrade rollup build from v2 to v4. Upgrade cookie dependency from 0.4 to 0.6. Upgrade all devDependencies
68 |
69 | ## 3.1.1
70 |
71 | ### Patch Changes
72 |
73 | - [#96](https://github.com/single-spa/import-map-overrides/pull/96) [`cd137ce`](https://github.com/single-spa/import-map-overrides/commit/cd137ce9edcbf7d3c5571e1c630c21bdee81979e) Thanks [@robmosca](https://github.com/robmosca)! - Add tests on API and automate release flow
74 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Joel Denning
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # import-map-overrides
2 |
3 | [](https://www.jsdelivr.com/package/npm/import-map-overrides)
4 |
5 | A browser and nodejs javascript library for being able to override [import maps](https://github.com/WICG/import-maps). This works
6 | with native browser import maps, [SystemJS](https://github.com/systemjs/systemjs) import maps, [es-module-shims](https://github.com/guybedford/es-module-shims) import maps, and more.
7 |
8 | ## Motivation
9 |
10 | 
11 |
12 | Import maps are a way of controlling which url to download javascript modules from. The import-map-overrides library allows you
13 | to dynamically change the url for javascript modules by storing overrides in local storage. This allows developers to **override individual modules to point to their localhost during development of a module, without having to boot up a local environment with all the other modules and a backend server.**
14 |
15 | You should not use import-map-overrides as the **only** import map on your page, since you cannot count on everyone's local storage having
16 | valid values for all of your modules. Instead, import-map-overrides should be viewed as a developer experience enhancement and dev tool --
17 | developers can develop and debug on deployed environments instead of having to boot up a local environment.
18 |
19 | Here are some tutorial videos that explain this in more depth:
20 |
21 | - [In-browser vs build-time modules](https://www.youtube.com/watch?v=Jxqiu6pdMSU&list=PLLUD8RtHvsAOhtHnyGx57EYXoaNsxGrTU&index=2)
22 | - [Import Maps](https://www.youtube.com/watch?v=Lfm2Ge_RUxs&list=PLLUD8RtHvsAOhtHnyGx57EYXoaNsxGrTU&index=3)
23 | - [Local development with import map overrides](https://www.youtube.com/watch?v=vjjcuIxqIzY&list=PLLUD8RtHvsAOhtHnyGx57EYXoaNsxGrTU&index=4)
24 |
25 | ## Documentation
26 |
27 | The UI for import-map-overrides works in evergreen browsers (web components support required). The javascript API works in IE11+.
28 |
29 | - [Security](/docs/security.md)
30 |
31 | ### Browser
32 |
33 | - [Installation](/docs/installation.md#browser)
34 | - [Configuration](/docs/configuration.md)
35 | - [User Interface](/docs/ui.md)
36 | - [Javascript API](/docs/api.md#browser)
37 |
38 | ### NodeJS
39 |
40 | - [Installation](/docs/installation.md#node)
41 | - [API](/docs/api.md#node)
42 |
43 | ## Contributing
44 |
45 | Make sure you commit a changeset with `pnpm changeset` before you open a PR.
46 | This will allow to automatically bump the version and maintain the CHANGELOG
47 | once released.
48 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # Javascript API
2 |
3 | ## Browser
4 |
5 | import-map-overrides provides the following functions. Note that these functions are always put onto window.importMapOverrides, even
6 | if you installed it as an npm package.
7 |
8 | ### getOverrideMap
9 |
10 | Returns the override import map as an object. The returned object represents the overrides
11 | **that will take effect the next time you reload the page**, including any additions or removals you've recently made after
12 | the current page's [acquiringImportMaps boolean](https://github.com/WICG/import-maps/blob/master/spec.md#acquiring-import-maps) was set to false.
13 |
14 | `includeDisabled` is an optional boolean you can pass in to include any "disabled overrides." See `disableOverride()` for more information.
15 |
16 | ```js
17 | const overrideMap = window.importMapOverrides.getOverrideMap();
18 | /*
19 | {
20 | "imports": {
21 | "module1": "https://mycdn.com/module1.js",
22 | "lodash": "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.core.js"
23 | }
24 | }
25 | */
26 |
27 | const overrideMapWithDisabledOverrides =
28 | window.importMapOverrides.getOverrideMap(true);
29 | /*
30 | {
31 | "imports": {
32 | "app1": "/app1.js",
33 | "module1": "https://mycdn.com/module1.js",
34 | "lodash": "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.core.js"
35 | }
36 | }
37 | */
38 | ```
39 |
40 | ### getOverrideScopes
41 |
42 | Returns a Promise that resolves with an import map containing scopes for overridden modules. The scopes are inherited from the default map.
43 |
44 | ```html
45 |
49 |
62 |
63 |
78 | ```
79 |
80 | ### addOverride
81 |
82 | A function that accepts a string `moduleName` and a string `url` as arguments. This will set up an override **which takes effect
83 | the next time you reload the page**. Returns the new override import map.
84 |
85 | **Note that if you provide a port instead of a full url, that import-map-overrides will provide a default url to your localhost**.
86 |
87 | ```js
88 | window.importMapOverrides.addOverride("react", "https://unpkg.com/react");
89 |
90 | // Alternatively, provide a port number. Default url will be //localhost:8085/module1.js
91 | window.importMapOverrides.addOverride("module1", "8085");
92 | ```
93 |
94 | ### removeOverride
95 |
96 | A function that accepts a string `moduleName` as an argument. This will remove an override **which takes effect the next time you
97 | reload the page**. Returns a boolean that indicates whether the override existed.
98 |
99 | ```js
100 | const wasRemoved = window.importMapOverrides.removeOverride("vue");
101 | console.log(wasRemoved); // Either true or false
102 | ```
103 |
104 | ### resetOverrides
105 |
106 | A function that removes all overrides from local storage, so that the next time the page is reloaded an override import map won't be created. Accepts
107 | no arguments and returns the reset override import map.
108 |
109 | ```js
110 | window.importMapOverrides.resetOverrides();
111 | ```
112 |
113 | ### getUrlFromPort
114 |
115 | A function used internally by import-map-overrides to create a full url when calling `addOverride()` with just a
116 | port number:
117 |
118 | ```js
119 | const defaultOverrideUrl = window.importMapOverrides.getUrlFromPort(
120 | "module1",
121 | "8085",
122 | );
123 | console.log(defaultOverrideUrl); // "//localhost:8085/module1.js"
124 | ```
125 |
126 | The `getUrlFromPort` function is exposed as an API to allow you to customize the logic yourself:
127 |
128 | ```js
129 | window.importMapOverrides.getUrlFromPort = (moduleName, port) =>
130 | `http://127.0.0.1:${port}/${moduleName}.js`;
131 |
132 | // Now whenever you call `addOverride()` with a port number, your custom logic will be called
133 | window.importMapOverrides.addOverride("module1", "8085");
134 | console.log(window.importMapOverrides.getOverrideMap().imports.module1); // "http://127.0.0.1:8085/module1.js"
135 | ```
136 |
137 | ### enableUI
138 |
139 | This will force the full import map overrides UI to be displayed (as long as the code for it is loaded on the page).
140 |
141 | It will set local storage to match the `show-when-local-storage` key and/or it will append a `` element to the DOM.
142 |
143 | ### mergeImportMap
144 |
145 | This function accepts two arguments, `firstMap` and `secondMap`, and creates a new import map that is the first map merged with the second map. Items in the second map take priority.
146 |
147 | ```js
148 | const firstMap = { imports: { foo: "./foo1.js" } };
149 | const secondMap = { imports: { foo: "./foo2.js" } };
150 |
151 | // {imports: {foo: './foo2.js'}}
152 | window.importMapOverrides.mergeImportMap(firstMap, secondMap);
153 | ```
154 |
155 | ### getDefaultMap
156 |
157 | This function returns a promise that resolves the import map(s) on the page, without the presence of any import map overrides.
158 |
159 | ```js
160 | window.importMapOverrides.getDefaultMap().then((importMap) => {
161 | // The default map is the import map that exists on the page before any overrides are applied.
162 | // {imports: {}}
163 | console.log(importMap);
164 | });
165 | ```
166 |
167 | ### resetDefaultMap
168 |
169 | The [`getDefaultMap`](#getDefaultMap) function only derives the default map once upfront and then caches it. Calling the `resetDefaultMap()` function clears that cache, so that a subsequent call to `getDefaultMap()` derives the default map again.
170 |
171 | ### getCurrentPageMap
172 |
173 | This function returns a promise that resolves the final import map (including overrides) that was applied to the current page. Any overrides set after the page load will not be included.
174 |
175 | ```js
176 | window.importMapOverrides.getCurrentPageMap().then((importMap) => {
177 | // The current page map is a merge of the default map and the overrides **at the time the page was loaded**.
178 | // Any overrides after the page was loaded will not show here.
179 | // {imports: {}}
180 | console.log(importMap);
181 | });
182 | ```
183 |
184 | ### getNextPageMap
185 |
186 | This function returns a promise that resolves with the final import map (including overrides) that will be applied the next time the page is reloaded.
187 |
188 | ```js
189 | window.importMapOverrides.getNextPageMap().then((importMap) => {
190 | // The next page map is a merge of the default map and all overrides, including those that were applied **after the page was loaded**.
191 | // {imports: {}}
192 | console.log(importMap);
193 | });
194 | ```
195 |
196 | ### disableOverride
197 |
198 | This function accepts one argument, `moduleName`, and will temporarily disable an import map override. This is similar to `removeOverride()` except that it will preserve what the override URL was so that you can toggle the override on and off.
199 |
200 | Returns true if the module was already disabled, and false otherwise.
201 |
202 | ```js
203 | // Once disabled, some-module will be loaded from the default URL
204 | window.importMapOverrides.disableOverride("some-module");
205 | ```
206 |
207 | ### enableOverride
208 |
209 | This function accepts one argument, `moduleName`, and will will re-renable an import map override that was previously disabled via `disableOverride()`.
210 |
211 | Returns true if the module was already disabled, and false otherwise.
212 |
213 | ```js
214 | // Once enabled, some-module will be loaded from the override URL
215 | window.importMapOverrides.enableOverride("some-module");
216 | ```
217 |
218 | ### getDisabledOverrides
219 |
220 | A function that returns an array of strings, where each string is the name of a module that is currently disabled.
221 |
222 | ```js
223 | // ['module-1', 'module-1']
224 | window.importMapOverrides.getDisabledOverrides();
225 | ```
226 |
227 | ### isDisabled
228 |
229 | A function that accepts one argument, `moduleName`, and returns a boolean indicated whether the string module name is a currently disabled or not.
230 |
231 | ```js
232 | // true means it is disabled. false means enabled.
233 | window.importMapOverrides.isDisabled("module-1");
234 | ```
235 |
236 | ### addExternalOverride
237 |
238 | A function that accepts one argument, `urlToImportMap`, that sets up an override to an external import map that is hosted at a different URL. The external import map has lower precendence than inline overrides created via `addOverride()` when using multiple import maps, and higher precedence when using a single import map.
239 |
240 | ```js
241 | window.importMapOverrides.addExternalOverride(
242 | "https://localhost:8080/my-override-import-map.json",
243 | );
244 | ```
245 |
246 | ### removeExternalOverride
247 |
248 | A function that accepts one argument, `urlToImportMap`, that removes an external import map override. Returns a boolean that indicates whether the override existed in the first place.
249 |
250 | ```js
251 | // A return value of true means the override existed in the first place
252 | window.importMapOverrides.removeExternalOverride(
253 | "https://localhost:8080/my-override-import-map.json",
254 | );
255 | ```
256 |
257 | ### getExternalOverrides
258 |
259 | A function that returns an array of string URLs, where each string is the URL to an external override import map.
260 |
261 | ```js
262 | // ['https://localhost:8080/my-override-import-map.json', 'https://some-cdn.com/importmap.json']
263 | window.importMapOverrides.getExternalOverrides();
264 | ```
265 |
266 | ### getCurrentPageExternalOverrides
267 |
268 | Similar to `getExternalOverrides()`, except it ignores any changes to the external overrides since the page was loaded.
269 |
270 | ```js
271 | // ['https://localhost:8080/my-override-import-map.json']
272 | window.importMapOverrides.getExternalOverrides();
273 | ```
274 |
275 | ### getExternalOverrideMap
276 |
277 | A function that returns a promise that resolves with the merged import map of all external override import maps. You can provide an array of strings `urlsToImportMap` to control which external import maps to fetch and merge.
278 |
279 | ```js
280 | window.importMapOverrides.getExternalOverrideMap().then((importMap) => {
281 | // {imports: {foo: './foo.js'}}
282 | console.log(importMap);
283 | });
284 |
285 | window.importMapOverrides
286 | .getExternalOverrideMap(["https://some-url.com/importmap.json"])
287 | .then((importMap) => {
288 | // {imports: {foo: './bar.js'}}
289 | console.log(importMap);
290 | });
291 | ```
292 |
293 | ### isExternalMapValid
294 |
295 | Takes one argument, `urlToImport`, and returns a promise that resolves with a boolean. When `true`, the url provided is one that hosts a valid import map. When `false`, the url provided doesn't host a valid import map.
296 |
297 | ```js
298 | // true | false. True means the external map was successfully downloaded and parsed as json
299 | window.importMapOverrides.isExternalMapValid(
300 | "https://localhost:8080/my-custom-override-import-mapm.json",
301 | );
302 | ```
303 |
304 | ### Events
305 |
306 | #### Init
307 |
308 | The import-map-overrides library fires an event called `import-map-overrides:init` on the window when the library has successfully initialized. Note that this event will not fire if import-map-overrides is disabled.
309 |
310 | ```js
311 | window.addEventListener("import-map-overrides:init", () => {
312 | console.log("init");
313 | });
314 | ```
315 |
316 | #### Change
317 |
318 | The import-map-overrides library fires an event called `import-map-overrides:change` on the window whenever the
319 | override import map changes. The event is a [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent)
320 | that has no [detail property](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail).
321 |
322 | Example usage:
323 |
324 | ```js
325 | window.addEventListener("import-map-overrides:change", logImportMap);
326 |
327 | // Later on you can remove the event listener
328 | window.removeEventListener("import-map-overrides:change", logImportMap);
329 |
330 | function logImportMap(evt) {
331 | console.log(window.importMapOverrides.getOverrideMap());
332 | }
333 | ```
334 |
335 | ## Node
336 |
337 | import-map-overrides exposes functions for applying overrides to import maps in NodeJS. This is commonly paired with [`@node-loader/import-maps`](https://github.com/node-loader/node-loader-import-maps), but can be used with any javascript object that is an import map.
338 |
339 | ### applyOverrides
340 |
341 | A function that merges overrides into an import map.
342 |
343 | **Arguments:**
344 |
345 | - `importMap`: An object that is an import map (has an `imports` object property)
346 | - `overrides`: An object where the keys are import specifiers and the values are their URLs in the import map.
347 |
348 | **Return value:**
349 |
350 | A **new** import map with the overrides applied. The original map remains unmodified.
351 |
352 | ```js
353 | import { applyOverrides } from "import-map-overrides";
354 |
355 | const importMap = {
356 | imports: {
357 | foo: "./foo.js",
358 | bar: "./bar.js",
359 | },
360 | };
361 |
362 | const overrides = {
363 | bar: "./overridden-bar.js",
364 | };
365 |
366 | const overriddenMap = applyOverrides(importMap, overrides);
367 | /*
368 | {
369 | imports: {
370 | foo: './foo.js',
371 | bar: './overridden-bar.js'
372 | }
373 | }
374 | */
375 | ```
376 |
377 | ### getOverridesFromCookies
378 |
379 | A function that accepts an [HTTP Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage) (commonly referred to as `req`) and returns an object of import map overrides. The cookies are generally set by the import-map-overrides browser library, and are of the format `import-map-override:module-name=https://localhost:8080/module-name.js`.
380 |
381 | **Arguments:**
382 |
383 | - `req` (required): An [HTTP Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage). The `req` objects from Express / Hapi servers are supported.
384 | - `getUrlFromPort` (optional): A function that converts a port number to a full URL. Defaults to generating a localhost URL.
385 |
386 | ```js
387 | import { getOverridesFromCookies, applyOverrides } from "import-map-overrides";
388 |
389 | const overrides = getOverridesFromCookies(req);
390 |
391 | const mapWithOverrides = applyOverrides(originalMap, overrides);
392 |
393 | // Optionally convert port numbers to URLs
394 | const overrides = getOverridesFromCookies(req, (port, moduleName, req) => {
395 | return `https://localhost:${port}/${moduleName}.js`;
396 | });
397 | ```
398 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | import-map-overrides has two primary configuration options:
4 |
5 | 1. [Import Map Type](#import-map-type)
6 | 2. [Override Mode](#override-mode)
7 | 3. [Domain List](#domain-list)
8 |
9 | ## Import Map Type
10 |
11 | By default, import map overrides will assume you are working with native import maps. However, other import map polyfills (such as SystemJS) can also be used with import map overrides.
12 |
13 | If using an import map polyfill, you must indicate what kind of import map you are setting overrides for. You do this by inserting a ``
14 | element to your html file **before the import-map-overrides library is loaded**.
15 |
16 | ```html
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | ```
26 |
27 | | Import Map type | `importmap-type` |
28 | | ---------------------------------------------------------------- | --------------------- |
29 | | Native 1 | `importmap` (default) |
30 | | [SystemJS](https://github.com/systemjs/systemjs) | `systemjs-importmap` |
31 | | [es-module-shims](https://github.com/guybedford/es-module-shims) | `importmap-shim` |
32 |
33 | ### use-injector
34 |
35 | The `use-injector` attribute instructs import-map-overrides to skip inserting an import-map into the DOM, instead expecting the [import-map-injector](https://github.com/single-spa/import-map-injector) project to do so. This is necessary since browsers do not support multiple import maps on the same page.
36 |
37 | For more details, see [injector override mode](#client-side-injector) [import-map-injector docs](https://github.com/single-spa/import-map-injector#compatibility)
38 |
39 | ## Override Mode
40 |
41 | To support a variety of use cases, import map overrides has four "override modes" that control how and whether import-map-overrides inserts import maps into the DOM:
42 |
43 | 1. [Client-side multiple maps](#client-side-multiple-maps) (default)
44 | 1. [Client-side single map](#client-side-single-map)
45 | 1. [Client-side import-map-injector](#client-side-injector)
46 | 1. [Server-side multiple maps](#server-side-multiple-maps)
47 | 1. [Server-side single map](#server-side-single-map)
48 |
49 | If you're just getting started, first try client-side multiple import maps.
50 |
51 | Client-side versus server-side refers to whether the browser or server is responsible for ensuring that the override is applied. Multiple maps refers to multiple `
82 |
83 |
90 |
91 |
92 |
93 | ```
94 |
95 | Client-side multiple maps mode is the default mode and will be used unless you enable one of the other modes.
96 |
97 | ### Client-side single map
98 |
99 | The import maps specification [only allows for a single import map per web page](https://github.com/WICG/import-maps/#multiple-import-map-support). Many import map polyfills, including systemjs and es-module-shims, allow for multiple import maps that are merged together.
100 |
101 | To use single import map mode, change the `type` attribute of your import map to be `overridable-importmap`:
102 |
103 | ```html
104 |
111 |
112 | ```
113 |
114 | The `overridable-importmap` will be ignored by the browser, but import-map-overrides will insert an import map with the correct script `type` attribute and overrides applied, which will be used by the browser.
115 |
116 | Note that `overridable-importmap` scripts must be inline import maps, not external maps (those with `src=""`). This is because import-map-overrides executes synchronously to inject the single map, but downloading an external map is inherently asynchronous.
117 |
118 | ### Client-side injector
119 |
120 | If using [import-map-injector](https://github.com/single-spa/import-map-injector), import-map-overrides is not responsible for inserting the importmap script element into the DOM. To use the two projects together, do the following:
121 |
122 | 1. Load import-map-overrides.js **before** import-map-injector.js
123 |
124 | ```html
125 |
126 |
127 |
128 | ```
129 |
130 | 2. Add the `use-injector` attribute to the `` element that configures import-map-overrides. See [import-map-overrides docs](https://github.com/single-spa/import-map-overrides/blob/main/docs/configuration.md#import-map-type) for more details
131 |
132 | ```html
133 |
134 | ```
135 |
136 | ### Server-side multiple maps
137 |
138 | If your server needs to be aware of import map overrides, you may use server-side multiple maps mode. To enable this mode, add a `server` attribute to your `` element:
139 |
140 | ```html
141 |
142 | ```
143 |
144 | Once enabled, a cookie is sent to the server for each override. The format of the cookies is `import-map-overrides:module-name=http://localhost:8080/module-name.js`.
145 |
146 | In addition to the cookie, the import-map-overrides library will automatically inject an overrides import map into the DOM.
147 |
148 | ### Server-side single map
149 |
150 | To enable server-side single map mode, add `server-cookie` and `server-only` attributes to your `` element:
151 |
152 | ```html
153 |
154 |
155 |
161 | ```
162 |
163 | Once enabled, a cookie is sent to the server for each override. The format of the cookies is `import-map-overrides:module-name=http://localhost:8080/module-name.js`.
164 |
165 | In this mode, import-map-overrides library will no longer dynamically inject any override maps into the DOM. Instead, your web server is expected to read the `import-map-overrides:` cookies and update the URLs in its inlined import map accordingly.
166 |
167 | ## Domain List
168 |
169 | If you wish to reuse the same HTML file on multiple domains (usually for dev/test/stage/prod environments), you can configure which domains import-map-overrides is enabled for. This feature was built so that it is easy to turn off import-map-overrides in production environments. Turning off import-map-overrides in production does not make your web application more secure ([explanation](./security.md)), but may be desireable for other reasons such as preventing users from finding a dev-only tool by [setting local storage](./ui.md).
170 |
171 | An alternative way of accomplishing this is to serve a different HTML file for your production environment than other environments. That implementation is more performant since you avoid downloading the import-map-overrides library entirely when it will not be used. If that option is possible and practical for you, it is probably better than using `import-map-overrides-domains`.
172 |
173 | To configure domains, add a `` element to your HTML page. You can specify either an an allow-list or a deny-list in the `content` attribute. Allow lists never result in unintended domains being able to use import-map-overrides, but require you to update the allow list every time you create a non-prod environment, which can be a bit of a nuisance. If you have a single production environment with many non-prod environments, a deny-list might be easier.
174 |
175 | ```html
176 |
177 |
181 |
182 |
183 |
184 |
185 |
186 |
190 | ```
191 |
192 | ## Query Parameter Overrides
193 |
194 | import-map-overrides has an opt-in feature that allows users to set overrides via the `imo` query parameter on the current page. When enabled, the `imo` query parameter value should be a URL-encoded import map. For example, an override map of `{"imports": {"module1": "/module1.js"}}` would be encoded via https://example.com?imo=%7B%22imports%22%3A%7B%22module1%22%3A%22%2Fmodule1.js%22%7D%7D
195 |
196 | To enable query parameter overrides, add the `allow-query-param-override` attribute to the `` element:
197 |
198 | ```html
199 |
204 | ```
205 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | ## Browser
4 |
5 | The import-map-overrides library is used via a global variable `window.importMapOverrides`. The global variable exists because import-map-overrides needs
6 | to be usable regardless of build config and without dependence on ESM modules, since
7 | [once you use ESM modules you can no longer modify the import map](https://github.com/WICG/import-maps/blob/master/spec.md#acquiring-import-maps).
8 |
9 | It is preferred to install import-map-overrides with a `
20 |
21 |
24 | ```
25 |
26 | Alternatively, you can use it as an npm package:
27 |
28 | ```sh
29 | npm install --save import-map-overrides
30 | # Or
31 | yarn add import-map-overrides
32 | ```
33 |
34 | ```js
35 | /*
36 | Make sure this js file gets executed BEFORE any
76 | ```
77 |
78 | ## Not using the UI
79 |
80 | The UI is completely optional. If you don't want to use it, simply don't include the ``
81 | custom element in your page. Additionally, you can use the
82 | [`/dist/import-map-overrides-api.js` file](https://unpkg.com/browse/import-map-overrides/dist/import-map-overrides-api.js)
83 | instead of [`/dist/import-map-overrides.js`](https://unpkg.com/browse/import-map-overrides/dist/import-map-overrides.js),
84 | which avoids downloading the code for the UI and reduces the library size.
85 |
86 | ## Inline versus external overrides
87 |
88 | In the UI, you can add inline overrides and external overrides.
89 |
90 | An _inline override_ is an override for a single module. Each inline override is stored in local storage. You may add inline overrides by clicking on a module or on "Add Module".
91 |
92 | An _external override_ is a partial import map hosted on a different server. Any imports in that external import map will be applied as overrides to the current page's imports. You can add external overrides by clicking on the "Add import map" button. Note that external overrides are only supported in [multiple import map modes](/docs/configuration.md#override-modes).
93 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import es5 from "eslint-plugin-es5";
2 | import babelParser from "@babel/eslint-parser";
3 | import path from "node:path";
4 | import { fileURLToPath } from "node:url";
5 | import js from "@eslint/js";
6 | import { FlatCompat } from "@eslint/eslintrc";
7 |
8 | const __filename = fileURLToPath(import.meta.url);
9 | const __dirname = path.dirname(__filename);
10 | const compat = new FlatCompat({
11 | baseDirectory: __dirname,
12 | recommendedConfig: js.configs.recommended,
13 | allConfig: js.configs.all,
14 | });
15 |
16 | export default [
17 | ...compat.extends("important-stuff"),
18 | {
19 | plugins: {
20 | es5,
21 | },
22 |
23 | languageOptions: {
24 | parser: babelParser,
25 | },
26 |
27 | rules: {
28 | "es5/no-es6-static-methods": "error",
29 | "es5/no-binary-and-octal-literals": "error",
30 | "es5/no-classes": "off",
31 | "es5/no-for-of": "off",
32 | "es5/no-generators": "error",
33 | "es5/no-object-super": "error",
34 | "es5/no-typeof-symbol": "error",
35 | "es5/no-unicode-code-point-escape": "error",
36 | "es5/no-unicode-regex": "error",
37 | "es5/no-computed-properties": "off",
38 | "es5/no-destructuring": "off",
39 | "es5/no-default-parameters": "off",
40 | "es5/no-spread": "off",
41 | "es5/no-modules": "off",
42 | "es5/no-exponentiation-operator": "off",
43 | "es5/no-block-scoping": "off",
44 | "es5/no-arrow-functions": "off",
45 | "es5/no-shorthand-properties": "off",
46 | "es5/no-rest-parameters": "off",
47 | "es5/no-template-literals": "off",
48 | },
49 | },
50 | {
51 | files: ["src/server/**/*"],
52 |
53 | rules: {
54 | "es5/no-es6-methods": "off",
55 | "es5/no-es6-static-methods": "off",
56 | "es5/no-binary-and-octal-literals": "off",
57 | "es5/no-classes": "off",
58 | "es5/no-for-of": "off",
59 | "es5/no-generators": "off",
60 | "es5/no-object-super": "off",
61 | "es5/no-typeof-symbol": "off",
62 | "es5/no-unicode-code-point-escape": "off",
63 | "es5/no-unicode-regex": "off",
64 | },
65 | },
66 | ];
67 |
--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('jest').Config} */
2 | module.exports = {
3 | collectCoverageFrom: ["src/**/*.js", "!src/api/local-storage-mock.js"],
4 | resetMocks: true,
5 | restoreMocks: true,
6 | setupFiles: ["./jest.setup.js"],
7 | testEnvironment: "jsdom",
8 | };
9 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | import jestFetchMock from "jest-fetch-mock";
2 |
3 | jestFetchMock.enableMocks();
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "import-map-overrides",
3 | "version": "6.0.1",
4 | "main": "dist/import-map-overrides-server.js",
5 | "type": "module",
6 | "repository": "https://github.com/single-spa/import-map-overrides.git",
7 | "author": "Jolyn Denning ",
8 | "license": "MIT",
9 | "scripts": {
10 | "build": "pnpm run clean && cross-env NODE_ENV=production rollup -c",
11 | "build:dev": "pnpm run clean && cross-env NODE_ENV=development rollup -c",
12 | "check-format": "prettier --check src",
13 | "clean": "rimraf dist",
14 | "watch": "cross-env NODE_ENV=development rollup -c --watch",
15 | "copy-test-files": "copyfiles test/**/* dist -f",
16 | "serve": "serve . -l 5050",
17 | "embed-serve": "serve . -l 3333",
18 | "test": "jest",
19 | "lint": "eslint src",
20 | "prepublishOnly": "pnpm run build",
21 | "prepare": "husky",
22 | "changeset": "changeset",
23 | "release": "changeset publish"
24 | },
25 | "exports": {
26 | "node": "./dist/import-map-overrides-server.js",
27 | "default": "./dist/import-map-overrides.js"
28 | },
29 | "browserslist": {
30 | "production": [
31 | "IE >= 11"
32 | ],
33 | "server": [
34 | "Node 10"
35 | ]
36 | },
37 | "files": [
38 | "dist"
39 | ],
40 | "devDependencies": {
41 | "@babel/core": "^7.25.2",
42 | "@babel/eslint-parser": "^7.25.1",
43 | "@babel/plugin-proposal-class-properties": "^7.18.6",
44 | "@babel/plugin-transform-react-jsx": "^7.25.2",
45 | "@babel/preset-env": "^7.25.4",
46 | "@changesets/changelog-github": "^0.5.0",
47 | "@changesets/cli": "^2.27.8",
48 | "@eslint/eslintrc": "^3.1.0",
49 | "@eslint/js": "^9.11.1",
50 | "@rollup/plugin-babel": "^6.0.4",
51 | "@rollup/plugin-node-resolve": "^15.3.0",
52 | "@testing-library/jest-dom": "^6.5.0",
53 | "babel-jest": "^29.7.0",
54 | "concurrently": "^9.0.1",
55 | "copyfiles": "^2.4.1",
56 | "cross-env": "^7.0.3",
57 | "eslint": "^9.11.0",
58 | "eslint-config-important-stuff": "^1.1.0",
59 | "eslint-plugin-es5": "^1.5.0",
60 | "husky": "^9.1.6",
61 | "jest": "^29.7.0",
62 | "jest-cli": "^29.7.0",
63 | "jest-environment-jsdom": "^29.7.0",
64 | "jest-fetch-mock": "^3.0.3",
65 | "postcss": "^8.4.47",
66 | "preact": "^10.24.0",
67 | "prettier": "^3.3.3",
68 | "pretty-quick": "^4.0.0",
69 | "regenerator-runtime": "^0.14.1",
70 | "rimraf": "^6.0.1",
71 | "rollup": "^4.22.4",
72 | "rollup-plugin-postcss": "^4.0.2",
73 | "rollup-plugin-terser": "^7.0.2",
74 | "serve": "^14.2.3"
75 | },
76 | "dependencies": {
77 | "cookie": "^1.0.2"
78 | },
79 | "packageManager": "pnpm@9.12.1"
80 | }
81 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { babel } from "@rollup/plugin-babel";
2 | import { terser } from "rollup-plugin-terser";
3 | import { nodeResolve } from "@rollup/plugin-node-resolve";
4 | import postcss from "rollup-plugin-postcss";
5 | import packageJson from "./package.json" with { type: "json" };
6 |
7 | const isProduction = process.env.NODE_ENV === "production";
8 |
9 | export default [
10 | // Full library with UI
11 | {
12 | input: "src/import-map-overrides.js",
13 | output: {
14 | banner: `/* import-map-overrides@${packageJson.version} */`,
15 | file: "dist/import-map-overrides.js",
16 | format: "iife",
17 | sourcemap: true,
18 | },
19 | plugins: [
20 | babel({
21 | exclude: "node_modules/**",
22 | babelHelpers: "bundled",
23 | }),
24 | nodeResolve(),
25 | postcss({
26 | inject: false,
27 | }),
28 | isProduction &&
29 | terser({
30 | compress: {
31 | passes: 2,
32 | },
33 | output: {
34 | comments: terserComments,
35 | },
36 | }),
37 | ],
38 | },
39 | // Only the global variable API. No UI
40 | {
41 | input: "src/import-map-overrides-api.js",
42 | output: {
43 | banner: `/* import-map-overrides@${packageJson.version} */`,
44 | file: "dist/import-map-overrides-api.js",
45 | format: "iife",
46 | sourcemap: true,
47 | },
48 | plugins: [
49 | babel({
50 | exclude: "node_modules/**",
51 | babelHelpers: "bundled",
52 | }),
53 | isProduction &&
54 | terser({
55 | compress: {
56 | passes: 2,
57 | },
58 | output: {
59 | comments: terserComments,
60 | },
61 | }),
62 | ],
63 | },
64 | // Server ESM
65 | {
66 | input: "src/import-map-overrides-server.js",
67 | output: {
68 | banner: `/* import-map-overrides@${packageJson} (server) */`,
69 | file: "dist/import-map-overrides-server.js",
70 | format: "es",
71 | sourcemap: true,
72 | },
73 | plugins: [
74 | babel({
75 | exclude: "node_modules/**",
76 | presets: [
77 | [
78 | "@babel/preset-env",
79 | {
80 | browserslistEnv: "server",
81 | },
82 | ],
83 | ],
84 | }),
85 | isProduction &&
86 | terser({
87 | compress: {
88 | passes: 2,
89 | },
90 | output: {
91 | comments: terserComments,
92 | },
93 | }),
94 | ],
95 | },
96 | ];
97 |
98 | function terserComments(node, comment) {
99 | const text = comment.value;
100 | return text.trim().startsWith(`import-map-overrides@`);
101 | }
102 |
--------------------------------------------------------------------------------
/serve.json:
--------------------------------------------------------------------------------
1 | {
2 | "public": "dist"
3 | }
4 |
--------------------------------------------------------------------------------
/src/api/js-api.domainsElement.test.js:
--------------------------------------------------------------------------------
1 | describe("domainsElement", () => {
2 | const getDocument = (domainsElementContent) => `
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | `;
11 | beforeEach(() => {
12 | window.importMapOverrides = undefined;
13 | jest.resetModules();
14 | });
15 |
16 | it("should not initialize if host is in denyList", () => {
17 | document.body.innerHTML = getDocument("denylist:localhost");
18 | import("./js-api").then(() => {
19 | expect(window.importMapOverrides).toBeUndefined();
20 | });
21 | });
22 |
23 | it("should not initialize if host is not in allowList", () => {
24 | document.body.innerHTML = getDocument("allowlist:randomhost");
25 | import("./js-api").then(() => {
26 | expect(window.importMapOverrides).toBeUndefined();
27 | });
28 | });
29 |
30 | it("should initialize if host is not in denyList", () => {
31 | document.body.innerHTML = getDocument("denylist:randomhost");
32 | import("./js-api").then(() => {
33 | expect(window.importMapOverrides).toBeDefined();
34 | });
35 | });
36 |
37 | it("should initialize if host is in allowList", () => {
38 | document.body.innerHTML = getDocument("allowlist:localhost");
39 | import("./js-api").then(() => {
40 | expect(window.importMapOverrides).toBeDefined();
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/api/js-api.external.test.js:
--------------------------------------------------------------------------------
1 | import { localStorageMock } from "./local-storage-mock";
2 |
3 | describe("window.importMapOverrides", () => {
4 | const changeEventListener = jest.fn();
5 | let localStorageBackup;
6 |
7 | const defaultMap = {
8 | imports: {
9 | package1: "https://cdn.skypack.dev/package1",
10 | package2: "https://cdn.skypack.dev/package2",
11 | package3: "https://cdn.skypack.dev/package3",
12 | },
13 | };
14 | const defaultMapScript = ``;
17 |
18 | beforeEach(() => {
19 | jest.resetModules();
20 | fetch.resetMocks();
21 | localStorageBackup = window.localStorage;
22 | Object.defineProperty(window, "localStorage", { value: localStorageMock });
23 | window.localStorage.clear();
24 | window.addEventListener("import-map-overrides:change", changeEventListener);
25 | jest.spyOn(console, "warn").mockImplementation(() => {});
26 | });
27 |
28 | afterEach(() => {
29 | Object.defineProperty(window, "localStorage", {
30 | value: localStorageBackup,
31 | });
32 | window.removeEventListener(
33 | "import-map-overrides:change",
34 | changeEventListener,
35 | );
36 | });
37 |
38 | const setDocumentAndLoadScript = (maps = [defaultMapScript]) => {
39 | document.head.innerHTML = `${maps.map((map) => map || "").join("\n")}`;
40 | return import("./js-api");
41 | };
42 |
43 | async function assertChangeEventListenerIsCalled() {
44 | // Verify that the event listener is called
45 | await new Promise((resolve) =>
46 | changeEventListener.mockImplementation(() => {
47 | resolve();
48 | }),
49 | );
50 | }
51 |
52 | describe("getExternalOverrides", () => {
53 | it("should return an empty object if no overrides are set", async () => {
54 | await setDocumentAndLoadScript();
55 | expect(window.importMapOverrides.getExternalOverrides()).toEqual([]);
56 | });
57 |
58 | it("should return an object with the overrides", async () => {
59 | const overrides = [
60 | "https://cdn.skypack.dev/importmap1.json",
61 | "https://cdn.skypack.dev/importmap2.json",
62 | ];
63 | localStorageMock.setItem(
64 | "import-map-overrides-external-maps",
65 | JSON.stringify(overrides),
66 | );
67 | await setDocumentAndLoadScript();
68 |
69 | expect(window.importMapOverrides.getExternalOverrides()).toEqual(
70 | overrides,
71 | );
72 | });
73 | });
74 |
75 | describe("addExternalOverride", () => {
76 | it("should add an external override if not there already", async () => {
77 | const overrides = [
78 | "https://cdn.skypack.dev/importmap1.json",
79 | "https://cdn.skypack.dev/importmap2.json",
80 | ];
81 | localStorageMock.setItem(
82 | "import-map-overrides-external-maps",
83 | JSON.stringify(overrides),
84 | );
85 | await setDocumentAndLoadScript();
86 |
87 | expect(
88 | window.importMapOverrides.addExternalOverride(
89 | "https://cdn.skypack.dev/importmap3.json",
90 | ),
91 | ).toEqual(true);
92 | expect(window.importMapOverrides.getExternalOverrides()).toEqual(
93 | overrides.concat("https://cdn.skypack.dev/importmap3.json"),
94 | );
95 | await assertChangeEventListenerIsCalled();
96 | });
97 |
98 | it("should not add an external override if already there", async () => {
99 | const overrides = [
100 | "https://cdn.skypack.dev/importmap1.json",
101 | "https://cdn.skypack.dev/importmap2.json",
102 | ];
103 | localStorageMock.setItem(
104 | "import-map-overrides-external-maps",
105 | JSON.stringify(overrides),
106 | );
107 | await setDocumentAndLoadScript();
108 |
109 | expect(
110 | window.importMapOverrides.addExternalOverride(
111 | "https://cdn.skypack.dev/importmap2.json",
112 | ),
113 | ).toEqual(false);
114 | expect(window.importMapOverrides.getExternalOverrides()).toEqual(
115 | overrides,
116 | );
117 | });
118 | });
119 |
120 | describe("removeExternalOverride", () => {
121 | it("should remove an external override if there", async () => {
122 | const overrides = [
123 | "https://cdn.skypack.dev/importmap1.json",
124 | "https://cdn.skypack.dev/importmap2.json",
125 | ];
126 | localStorageMock.setItem(
127 | "import-map-overrides-external-maps",
128 | JSON.stringify(overrides),
129 | );
130 | await setDocumentAndLoadScript();
131 |
132 | expect(
133 | window.importMapOverrides.removeExternalOverride(
134 | "https://cdn.skypack.dev/importmap2.json",
135 | ),
136 | ).toEqual(true);
137 | expect(window.importMapOverrides.getExternalOverrides()).toEqual([
138 | "https://cdn.skypack.dev/importmap1.json",
139 | ]);
140 | await assertChangeEventListenerIsCalled();
141 | });
142 |
143 | it("should not remove an external override if not there", async () => {
144 | const overrides = [
145 | "https://cdn.skypack.dev/importmap1.json",
146 | "https://cdn.skypack.dev/importmap2.json",
147 | ];
148 | localStorageMock.setItem(
149 | "import-map-overrides-external-maps",
150 | JSON.stringify(overrides),
151 | );
152 | await setDocumentAndLoadScript();
153 |
154 | expect(
155 | window.importMapOverrides.removeExternalOverride(
156 | "https://cdn.skypack.dev/importmap3.json",
157 | ),
158 | ).toEqual(false);
159 | expect(window.importMapOverrides.getExternalOverrides()).toEqual(
160 | overrides,
161 | );
162 | });
163 | });
164 |
165 | describe("getExternalOverrideMap", () => {
166 | it("should return an empty override map if no external overrides are set", async () => {
167 | await setDocumentAndLoadScript();
168 | expect(
169 | window.importMapOverrides.getExternalOverrideMap(),
170 | ).resolves.toEqual({ imports: {}, scopes: {} });
171 | });
172 |
173 | it("should return an override map with the external overrides", async () => {
174 | await setDocumentAndLoadScript();
175 | window.importMapOverrides.getExternalOverrides = jest
176 | .fn()
177 | .mockReturnValue([
178 | "https://cdn.skypack.dev/importmap1.json",
179 | "https://cdn.skypack.dev/importmap2.json",
180 | ]);
181 | fetch.mockImplementation((url) => {
182 | if (url === "https://cdn.skypack.dev/importmap1.json") {
183 | return Promise.resolve({
184 | ok: true,
185 | json: () =>
186 | Promise.resolve({
187 | imports: {
188 | package1: "https://cdn.skypack.dev/package1",
189 | package2: "https://cdn.skypack.dev/package2",
190 | },
191 | }),
192 | });
193 | } else if (url === "https://cdn.skypack.dev/importmap2.json") {
194 | return Promise.resolve({
195 | ok: true,
196 | json: () =>
197 | Promise.resolve({
198 | imports: {
199 | package2: "https://cdn.skypack.dev/package22",
200 | package3: "https://cdn.skypack.dev/package3",
201 | },
202 | }),
203 | });
204 | }
205 | });
206 |
207 | const result = await window.importMapOverrides.getExternalOverrideMap();
208 |
209 | expect(result).toEqual({
210 | imports: {
211 | package1: "https://cdn.skypack.dev/package1",
212 | package2: "https://cdn.skypack.dev/package22",
213 | package3: "https://cdn.skypack.dev/package3",
214 | },
215 | scopes: {},
216 | });
217 | });
218 | });
219 |
220 | describe("isExternalMapValid", () => {
221 | it("should return true if the external map is valid", async () => {
222 | await setDocumentAndLoadScript();
223 | window.importMapOverrides.getExternalOverrides = jest
224 | .fn()
225 | .mockReturnValue(["https://cdn.skypack.dev/importmap1.json"]);
226 |
227 | fetch.mockImplementation((url) => {
228 | if (url === "https://cdn.skypack.dev/importmap1.json") {
229 | return Promise.resolve({
230 | ok: true,
231 | json: () =>
232 | Promise.resolve({
233 | imports: {
234 | package1: "https://cdn.skypack.dev/package1",
235 | package2: "https://cdn.skypack.dev/package2",
236 | },
237 | }),
238 | });
239 | }
240 | });
241 |
242 | const result = await window.importMapOverrides.isExternalMapValid(
243 | "https://cdn.skypack.dev/importmap1.json",
244 | );
245 |
246 | expect(result).toEqual(true);
247 | });
248 |
249 | it("should return false if the external map contains non-valid JSON", async () => {
250 | await setDocumentAndLoadScript();
251 | window.importMapOverrides.getExternalOverrides = jest
252 | .fn()
253 | .mockReturnValue(["https://cdn.skypack.dev/importmap1.json"]);
254 |
255 | fetch.mockImplementation((url) => {
256 | if (url === "https://cdn.skypack.dev/importmap1.json") {
257 | return Promise.resolve({
258 | ok: true,
259 | json: () => Promise.reject(new Error("Invalid JSON")),
260 | url: "https://cdn.skypack.dev/importmap1.json",
261 | });
262 | }
263 | });
264 |
265 | const result = await window.importMapOverrides.isExternalMapValid(
266 | "https://cdn.skypack.dev/importmap1.json",
267 | );
268 |
269 | expect(result).toEqual(false);
270 | });
271 |
272 | it("should return false if fetching the external map returns a not-OK response", async () => {
273 | await setDocumentAndLoadScript();
274 | window.importMapOverrides.getExternalOverrides = jest
275 | .fn()
276 | .mockReturnValue(["https://cdn.skypack.dev/importmap1.json"]);
277 |
278 | fetch.mockImplementation((url) => {
279 | if (url === "https://cdn.skypack.dev/importmap1.json") {
280 | return Promise.resolve({
281 | ok: false,
282 | url: "https://cdn.skypack.dev/importmap1.json",
283 | });
284 | }
285 | });
286 |
287 | const result = await window.importMapOverrides.isExternalMapValid(
288 | "https://cdn.skypack.dev/importmap1.json",
289 | );
290 |
291 | expect(result).toEqual(false);
292 | });
293 |
294 | it("should return false if fetching the external map fails", async () => {
295 | await setDocumentAndLoadScript();
296 | window.importMapOverrides.getExternalOverrides = jest
297 | .fn()
298 | .mockReturnValue(["https://cdn.skypack.dev/importmap1.json"]);
299 |
300 | fetch.mockRejectedValue(new Error("Failed to fetch"));
301 |
302 | const result = await window.importMapOverrides.isExternalMapValid(
303 | "https://cdn.skypack.dev/importmap1.json",
304 | );
305 |
306 | expect(result).toEqual(false);
307 | });
308 | });
309 |
310 | describe("getCurrentPageExternalOverrides", () => {
311 | it("should return an empty array if no external overrides are set", async () => {
312 | await setDocumentAndLoadScript();
313 |
314 | const result =
315 | await window.importMapOverrides.getCurrentPageExternalOverrides();
316 |
317 | expect(result).toEqual([]);
318 | });
319 |
320 | it("should return an array of external overrides if they are set", async () => {
321 | await setDocumentAndLoadScript([
322 | '',
323 | '',
324 | ]);
325 |
326 | const result =
327 | await window.importMapOverrides.getCurrentPageExternalOverrides();
328 |
329 | expect(result).toEqual([
330 | "https://cdn.skypack.dev/importmap1.json",
331 | "https://cdn.skypack.dev/importmap2.json",
332 | ]);
333 | });
334 | });
335 |
336 | describe("getCurrentPageMap", () => {
337 | it("should return the default map if no overrides are set", async () => {
338 | await setDocumentAndLoadScript();
339 |
340 | const result = await window.importMapOverrides.getCurrentPageMap();
341 |
342 | expect(result).toEqual({ ...defaultMap, scopes: {} });
343 | });
344 |
345 | it("should return a merge of the default map and the overrides", async () => {
346 | await setDocumentAndLoadScript([
347 | defaultMapScript,
348 | '',
349 | ]);
350 |
351 | fetch.mockImplementation((url) => {
352 | if (url === "https://cdn.skypack.dev/importmap1.json") {
353 | return Promise.resolve({
354 | ok: true,
355 | json: () =>
356 | Promise.resolve({
357 | imports: {
358 | package3: "https://cdn.skypack.dev/package33",
359 | package4: "https://cdn.skypack.dev/package4",
360 | },
361 | scopes: {
362 | "/scope1": {
363 | package1: "https://cdn.skypack.dev/scope1/package1",
364 | package2: "https://cdn.skypack.dev/scope1/package2",
365 | },
366 | },
367 | }),
368 | });
369 | }
370 | });
371 |
372 | const result = await window.importMapOverrides.getCurrentPageMap();
373 |
374 | expect(result).toEqual({
375 | imports: {
376 | package1: "https://cdn.skypack.dev/package1",
377 | package2: "https://cdn.skypack.dev/package2",
378 | package3: "https://cdn.skypack.dev/package33",
379 | package4: "https://cdn.skypack.dev/package4",
380 | },
381 | scopes: {
382 | "/scope1": {
383 | package1: "https://cdn.skypack.dev/scope1/package1",
384 | package2: "https://cdn.skypack.dev/scope1/package2",
385 | },
386 | },
387 | });
388 | });
389 | });
390 |
391 | describe("getNextPageMap", () => {
392 | it("should return the default map if no overrides are set", async () => {
393 | await setDocumentAndLoadScript();
394 |
395 | const result = await window.importMapOverrides.getNextPageMap();
396 |
397 | expect(result).toEqual({ ...defaultMap, scopes: {} });
398 | });
399 |
400 | it("should return a merge of the default map and the overrides", async () => {
401 | await setDocumentAndLoadScript();
402 | window.importMapOverrides.getExternalOverrides = jest
403 | .fn()
404 | .mockReturnValue([
405 | "https://cdn.skypack.dev/importmap1.json",
406 | "https://cdn.skypack.dev/importmap2.json",
407 | ]);
408 |
409 | fetch.mockImplementation((url) => {
410 | if (url === "https://cdn.skypack.dev/importmap1.json") {
411 | return Promise.resolve({
412 | ok: true,
413 | json: () =>
414 | Promise.resolve({
415 | imports: {
416 | package2: "https://cdn.skypack.dev/package22",
417 | package4: "https://cdn.skypack.dev/package4",
418 | },
419 | }),
420 | });
421 | } else if (url === "https://cdn.skypack.dev/importmap2.json") {
422 | return Promise.resolve({
423 | ok: true,
424 | json: () =>
425 | Promise.resolve({
426 | imports: {
427 | package3: "https://cdn.skypack.dev/package33",
428 | package5: "https://cdn.skypack.dev/package5",
429 | },
430 | scopes: {
431 | "/scope1": {
432 | package1: "https://cdn.skypack.dev/scope1/package1",
433 | package2: "https://cdn.skypack.dev/scope1/package2",
434 | },
435 | },
436 | }),
437 | });
438 | }
439 | });
440 |
441 | const result = await window.importMapOverrides.getNextPageMap();
442 |
443 | expect(result).toEqual({
444 | imports: {
445 | package1: "https://cdn.skypack.dev/package1",
446 | package2: "https://cdn.skypack.dev/package22",
447 | package3: "https://cdn.skypack.dev/package33",
448 | package4: "https://cdn.skypack.dev/package4",
449 | package5: "https://cdn.skypack.dev/package5",
450 | },
451 | scopes: {
452 | "/scope1": {
453 | package1: "https://cdn.skypack.dev/scope1/package1",
454 | package2: "https://cdn.skypack.dev/scope1/package2",
455 | },
456 | },
457 | });
458 | });
459 | });
460 | });
461 |
--------------------------------------------------------------------------------
/src/api/js-api.imo.test.js:
--------------------------------------------------------------------------------
1 | import { localStorageMock } from "./local-storage-mock";
2 |
3 | describe("window.importMapOverrides", () => {
4 | let localStorageBackup;
5 |
6 | const defaultMap = {
7 | imports: {
8 | package1: "https://cdn.skypack.dev/package1",
9 | package2: "https://cdn.skypack.dev/package2",
10 | package3: "https://cdn.skypack.dev/package3",
11 | },
12 | };
13 | const defaultMapScript = ``;
16 |
17 | beforeEach(() => {
18 | jest.resetModules();
19 | fetch.resetMocks();
20 | localStorageBackup = window.localStorage;
21 | Object.defineProperty(window, "localStorage", { value: localStorageMock });
22 | window.localStorage.clear();
23 | });
24 |
25 | afterEach(() => {
26 | Object.defineProperty(window, "localStorage", {
27 | value: localStorageBackup,
28 | });
29 | });
30 |
31 | const setDocumentAndLoadScript = (maps = [defaultMapScript]) => {
32 | document.head.innerHTML = `${maps.map((map) => map || "").join("\n")}`;
33 | return import("./js-api");
34 | };
35 |
36 | describe("getDefaultMap", () => {
37 | // Test getDefaultMap
38 | it("should return the default inline map", async () => {
39 | await setDocumentAndLoadScript();
40 | const map = await window.importMapOverrides.getDefaultMap();
41 |
42 | expect(map).toEqual({ ...defaultMap, scopes: {} });
43 | });
44 |
45 | // Test getDefaultMap
46 | // Test the case where there is no default map
47 | it("should return an empty map when there is no default map", async () => {
48 | await setDocumentAndLoadScript([""]);
49 | const map = await window.importMapOverrides.getDefaultMap();
50 |
51 | expect(map).toEqual({
52 | imports: {},
53 | scopes: {},
54 | });
55 | });
56 |
57 | // Test the case where the map is empty
58 | it("should throw an error when the default map is empty", async () => {
59 | await setDocumentAndLoadScript([""]);
60 |
61 | expect.assertions(1);
62 | try {
63 | await window.importMapOverrides.getDefaultMap();
64 | } catch (e) {
65 | expect(e.message).toEqual("Unexpected end of JSON input");
66 | }
67 | });
68 |
69 | // Test the case where the map is malformed
70 | it("should throw an error when the default map is malformed", async () => {
71 | await setDocumentAndLoadScript([
72 | "",
73 | ]);
74 | expect.assertions(1);
75 | try {
76 | const map = await window.importMapOverrides.getDefaultMap();
77 | } catch (e) {
78 | expect(e.message).toEqual(
79 | "Unexpected token 'M', \"Malformed\" is not valid JSON",
80 | );
81 | }
82 | });
83 |
84 | // Test the case where there are multiple inline maps
85 | it("should return the union of all maps when there are multiple inline maps", async () => {
86 | await setDocumentAndLoadScript([
87 | defaultMapScript,
88 | ``,
97 | ]);
98 | const map = await window.importMapOverrides.getDefaultMap();
99 |
100 | expect(map).toEqual({
101 | imports: {
102 | package1: "https://cdn.skypack.dev/package1",
103 | package2: "https://cdn.skypack.dev/package2",
104 | package3: "https://cdn.skypack.dev/package3",
105 | package4: "https://cdn.skypack.dev/package4",
106 | package5: "https://cdn.skypack.dev/package5",
107 | package6: "https://cdn.skypack.dev/package6",
108 | },
109 | scopes: {},
110 | });
111 | });
112 |
113 | // Test the case where there are multiple maps and one has the attribute
114 | // data-is-importmap-override
115 | it("should return the union of all maps except the ones having the attribute data-is-importmap-override", async () => {
116 | await setDocumentAndLoadScript([
117 | defaultMapScript,
118 | ``,
126 | ``,
134 | ]);
135 | const map = await window.importMapOverrides.getDefaultMap();
136 |
137 | expect(map).toEqual({
138 | imports: {
139 | package1: "https://cdn.skypack.dev/package1",
140 | package2: "https://cdn.skypack.dev/package2",
141 | package3: "https://cdn.skypack.dev/package3",
142 | package6: "https://cdn.skypack.dev/package6",
143 | package7: "https://cdn.skypack.dev/package7",
144 | },
145 | scopes: {},
146 | });
147 | });
148 |
149 | // Test the case where there are multiple maps and one is external
150 | it("should return the union of all maps including the external ones", async () => {
151 | await setDocumentAndLoadScript([
152 | defaultMapScript,
153 | ``,
154 | ]);
155 |
156 | fetch.mockResponseOnce(
157 | JSON.stringify({
158 | imports: {
159 | package4: "https://cdn.skypack.dev/package4",
160 | package5: "https://cdn.skypack.dev/package5",
161 | },
162 | }),
163 | );
164 |
165 | const map = await window.importMapOverrides.getDefaultMap();
166 |
167 | expect(map).toEqual({
168 | imports: {
169 | package1: "https://cdn.skypack.dev/package1",
170 | package2: "https://cdn.skypack.dev/package2",
171 | package3: "https://cdn.skypack.dev/package3",
172 | package4: "https://cdn.skypack.dev/package4",
173 | package5: "https://cdn.skypack.dev/package5",
174 | },
175 | scopes: {},
176 | });
177 | });
178 | });
179 |
180 | describe("getOverrideMap", () => {
181 | it("should return an empty map when there is no override map", async () => {
182 | await setDocumentAndLoadScript();
183 | const map = await window.importMapOverrides.getOverrideMap();
184 |
185 | expect(map).toEqual({
186 | imports: {},
187 | scopes: {},
188 | });
189 | });
190 |
191 | it("should return return an override map when overrides are stored in the local storage", async () => {
192 | await setDocumentAndLoadScript();
193 | window.localStorage.setItem(
194 | "import-map-override:package3",
195 | "https://cdn.skypack.dev/package33",
196 | );
197 | window.localStorage.setItem(
198 | "import-map-override:package4",
199 | "https://cdn.skypack.dev/package4",
200 | );
201 | const map = await window.importMapOverrides.getOverrideMap();
202 |
203 | expect(map).toEqual({
204 | imports: {
205 | package3: "https://cdn.skypack.dev/package33",
206 | package4: "https://cdn.skypack.dev/package4",
207 | },
208 | scopes: {},
209 | });
210 | });
211 |
212 | describe("with overrides via query string", () => {
213 | const metaElement = document.createElement("meta");
214 | const url = new URL(window.location.href);
215 |
216 | beforeEach(() => {
217 | url.searchParams.set(
218 | "imo",
219 | JSON.stringify({
220 | imports: { package3: "https://cdn.skypack.dev/package33" },
221 | }),
222 | );
223 | window.history.replaceState({}, "", url);
224 | document.body.appendChild(metaElement);
225 | });
226 |
227 | afterEach(() => {
228 | url.searchParams.delete("imo");
229 | window.history.replaceState({}, "", url);
230 | document.body.removeChild(metaElement);
231 | });
232 |
233 | it("should return an override map", async () => {
234 | metaElement.setAttribute("name", "importmap-type");
235 | metaElement.setAttribute("content", "importmap");
236 | metaElement.setAttribute("allow-query-param-override", "");
237 |
238 | await setDocumentAndLoadScript();
239 |
240 | const map = await window.importMapOverrides.getOverrideMap();
241 |
242 | expect(map).toEqual({
243 | imports: { package3: "https://cdn.skypack.dev/package33" },
244 | scopes: {},
245 | });
246 | });
247 |
248 | it("disabled by default", async () => {
249 | metaElement.removeAttribute("allow-query-param-override");
250 |
251 | await setDocumentAndLoadScript();
252 |
253 | const map = await window.importMapOverrides.getOverrideMap();
254 |
255 | expect(map).toEqual({
256 | imports: {},
257 | scopes: {},
258 | });
259 | });
260 | });
261 | });
262 |
263 | describe("Add/Remove/Enable/Disable overrides", () => {
264 | const changeEventListener = jest.fn();
265 |
266 | beforeEach(() => {
267 | window.addEventListener(
268 | "import-map-overrides:change",
269 | changeEventListener,
270 | );
271 | });
272 |
273 | afterEach(() => {
274 | window.removeEventListener(
275 | "import-map-overrides:change",
276 | changeEventListener,
277 | );
278 | });
279 |
280 | async function assertChangeEventListenerIsCalled() {
281 | // Verify that the event listener is called
282 | await new Promise((resolve) =>
283 | changeEventListener.mockImplementation(() => {
284 | resolve();
285 | }),
286 | );
287 | }
288 |
289 | describe("getDisabledOverrides", () => {
290 | it("should return an empty array when there is no override", async () => {
291 | await setDocumentAndLoadScript();
292 |
293 | expect(window.importMapOverrides.getDisabledOverrides()).toEqual([]);
294 | });
295 |
296 | it("should return an array of disabled overrides", async () => {
297 | await setDocumentAndLoadScript();
298 | window.localStorage.setItem(
299 | "import-map-overrides-disabled",
300 | JSON.stringify(["package3", "package4"]),
301 | );
302 |
303 | expect(window.importMapOverrides.getDisabledOverrides()).toEqual([
304 | "package3",
305 | "package4",
306 | ]);
307 | });
308 | });
309 |
310 | describe("isDisabled", () => {
311 | it("should return true if the override is disabled", async () => {
312 | await setDocumentAndLoadScript();
313 | window.localStorage.setItem(
314 | "import-map-overrides-disabled",
315 | JSON.stringify(["package3"]),
316 | );
317 |
318 | expect(window.importMapOverrides.isDisabled("package3")).toEqual(true);
319 | });
320 |
321 | it("should return false if the override is not disabled", async () => {
322 | await setDocumentAndLoadScript();
323 | window.localStorage.setItem(
324 | "import-map-overrides-disabled",
325 | JSON.stringify(["package3"]),
326 | );
327 |
328 | expect(window.importMapOverrides.isDisabled("package4")).toEqual(false);
329 | });
330 | });
331 |
332 | describe("disableOverride", () => {
333 | it("should disable an override and return true if it wasn't disabled before", async () => {
334 | await setDocumentAndLoadScript();
335 | const result =
336 | await window.importMapOverrides.disableOverride("package3");
337 |
338 | expect(window.importMapOverrides.getDisabledOverrides()).toEqual([
339 | "package3",
340 | ]);
341 | expect(result).toEqual(true);
342 | await assertChangeEventListenerIsCalled();
343 | });
344 |
345 | it("should maintain override disabled and return false if it was already disabled before", async () => {
346 | await setDocumentAndLoadScript();
347 | window.localStorage.setItem(
348 | "import-map-overrides-disabled",
349 | JSON.stringify(["package3"]),
350 | );
351 | const result =
352 | await window.importMapOverrides.disableOverride("package3");
353 |
354 | expect(window.importMapOverrides.getDisabledOverrides()).toEqual([
355 | "package3",
356 | ]);
357 | expect(result).toEqual(false);
358 | });
359 | });
360 |
361 | describe("enableOverride", () => {
362 | it("should re-enable a disabled override and return true", async () => {
363 | await setDocumentAndLoadScript();
364 | window.localStorage.setItem(
365 | "import-map-overrides-disabled",
366 | JSON.stringify(["package3"]),
367 | );
368 | const result =
369 | await window.importMapOverrides.enableOverride("package3");
370 |
371 | expect(window.importMapOverrides.getDisabledOverrides()).toEqual([]);
372 | expect(result).toEqual(true);
373 | await assertChangeEventListenerIsCalled();
374 | });
375 |
376 | it("should return false if override was not disabled before", async () => {
377 | await setDocumentAndLoadScript();
378 | const result =
379 | await window.importMapOverrides.enableOverride("package3");
380 |
381 | expect(window.importMapOverrides.getDisabledOverrides()).toEqual([]);
382 | expect(result).toEqual(false);
383 | });
384 | });
385 |
386 | describe("addOverride", () => {
387 | it("should add an override", async () => {
388 | await setDocumentAndLoadScript();
389 | const map = await window.importMapOverrides.addOverride(
390 | "package3",
391 | "https://cdn.skypack.dev/package33.js",
392 | );
393 |
394 | expect(localStorage.getItem("import-map-override:package3")).toEqual(
395 | "https://cdn.skypack.dev/package33.js",
396 | );
397 | expect(map).toEqual({
398 | imports: {
399 | package3: "https://cdn.skypack.dev/package33.js",
400 | },
401 | scopes: {},
402 | });
403 | await assertChangeEventListenerIsCalled();
404 | });
405 |
406 | it("should add an override by specifying only the port number", async () => {
407 | await setDocumentAndLoadScript();
408 | const map = await window.importMapOverrides.addOverride(
409 | "@demo/package33",
410 | "8080",
411 | );
412 |
413 | expect(
414 | localStorage.getItem("import-map-override:@demo/package33"),
415 | ).toEqual("//localhost:8080/demo-package33.js");
416 | expect(map).toEqual({
417 | imports: {
418 | "@demo/package33": "//localhost:8080/demo-package33.js",
419 | },
420 | scopes: {},
421 | });
422 | await assertChangeEventListenerIsCalled();
423 | });
424 | });
425 |
426 | describe("removeOverride", () => {
427 | it("should remove an existing override", async () => {
428 | await setDocumentAndLoadScript();
429 | window.localStorage.setItem(
430 | "import-map-override:package3",
431 | "https://cdn.skypack.dev/package33",
432 | );
433 | const result =
434 | await window.importMapOverrides.removeOverride("package3");
435 |
436 | expect(localStorage.getItem("import-map-override:package3")).toEqual(
437 | null,
438 | );
439 | expect(result).toBe(true);
440 | await assertChangeEventListenerIsCalled();
441 | });
442 | });
443 |
444 | describe("resetOverrides", () => {
445 | it("should remove all overrides", async () => {
446 | await setDocumentAndLoadScript();
447 | window.localStorage.setItem(
448 | "import-map-override:package3",
449 | "https://cdn.skypack.dev/package33",
450 | );
451 | window.localStorage.setItem(
452 | "import-map-override:package4",
453 | "https://cdn.skypack.dev/package4",
454 | );
455 | const map = await window.importMapOverrides.resetOverrides();
456 |
457 | expect(localStorage.getItem("import-map-override:package3")).toEqual(
458 | null,
459 | );
460 | expect(localStorage.getItem("import-map-override:package4")).toEqual(
461 | null,
462 | );
463 | expect(map).toEqual({
464 | imports: {},
465 | scopes: {},
466 | });
467 | await assertChangeEventListenerIsCalled();
468 | });
469 | });
470 |
471 | describe("hasOverrides", () => {
472 | it("should return true if there are overrides", async () => {
473 | await setDocumentAndLoadScript();
474 | window.localStorage.setItem(
475 | "import-map-override:package3",
476 | "https://cdn.skypack.dev/package33",
477 | );
478 |
479 | expect(window.importMapOverrides.hasOverrides()).toEqual(true);
480 | });
481 |
482 | it("should return false if there are no overrides", async () => {
483 | await setDocumentAndLoadScript();
484 |
485 | expect(window.importMapOverrides.hasOverrides()).toEqual(false);
486 | });
487 | });
488 | });
489 |
490 | describe("enableUI", () => {
491 | it("should enable the UI", async () => {
492 | await setDocumentAndLoadScript();
493 | const customElement = document.createElement("import-map-overrides-full");
494 | customElement.setAttribute("show-when-local-storage", "true");
495 | customElement.renderWithPreact = jest.fn();
496 | customElement.setAttribute("show-when-local-storage", "swlsKey");
497 | document.body.appendChild(customElement);
498 |
499 | window.importMapOverrides.enableUI();
500 |
501 | expect(window.localStorage.getItem("swlsKey")).toBe("true");
502 | expect(customElement.renderWithPreact).toHaveBeenCalledTimes(1);
503 | });
504 | });
505 |
506 | describe("mergeImportMap", () => {
507 | it("should merge an import map", async () => {
508 | await setDocumentAndLoadScript();
509 | const map = await window.importMapOverrides.mergeImportMap(
510 | {
511 | imports: {
512 | package1: "https://cdn.skypack.dev/package10.js",
513 | package3: "https://cdn.skypack.dev/package33.js",
514 | },
515 | scopes: {
516 | scope1: {
517 | package5: "https://cdn.skypack.dev/package55.js",
518 | package6: "https://cdn.skypack.dev/package66.js",
519 | },
520 | scope3: {
521 | package8: "https://cdn.skypack.dev/package89.js",
522 | package3: "https://cdn.skypack.dev/package31.js",
523 | },
524 | },
525 | },
526 | {
527 | imports: {
528 | package1: "https://cdn.skypack.dev/package11.js",
529 | package2: "https://cdn.skypack.dev/package20.js",
530 | },
531 | scopes: {
532 | scope2: {
533 | package7: "https://cdn.skypack.dev/package77.js",
534 | },
535 | scope3: {
536 | package3: "https://cdn.skypack.dev/package32.js",
537 | package9: "https://cdn.skypack.dev/package99.js",
538 | },
539 | },
540 | },
541 | );
542 |
543 | expect(map).toEqual({
544 | imports: {
545 | package1: "https://cdn.skypack.dev/package11.js",
546 | package2: "https://cdn.skypack.dev/package20.js",
547 | package3: "https://cdn.skypack.dev/package33.js",
548 | },
549 | scopes: {
550 | scope1: {
551 | package5: "https://cdn.skypack.dev/package55.js",
552 | package6: "https://cdn.skypack.dev/package66.js",
553 | },
554 | scope2: {
555 | package7: "https://cdn.skypack.dev/package77.js",
556 | },
557 | scope3: {
558 | package3: "https://cdn.skypack.dev/package32.js",
559 | package9: "https://cdn.skypack.dev/package99.js",
560 | },
561 | },
562 | });
563 | });
564 | });
565 | });
566 |
--------------------------------------------------------------------------------
/src/api/js-api.js:
--------------------------------------------------------------------------------
1 | import { escapeStringRegexp } from "../util/string-regex";
2 | import { includes } from "../util/includes.js";
3 | import { getParameterByName } from "../util/url-parameter";
4 |
5 | const localStoragePrefix = "import-map-override:";
6 | const disabledOverridesLocalStorageKey = "import-map-overrides-disabled";
7 | const externalOverridesLocalStorageKey = "import-map-overrides-external-maps";
8 | const overrideAttribute = "data-is-importmap-override";
9 | const domainsMeta = "import-map-overrides-domains";
10 | const allowListPrefix = "allowlist:";
11 | const denyListPrefix = "denylist:";
12 | export const queryParamOverridesName = "imo";
13 |
14 | const importMapMetaElement = document.querySelector(
15 | 'meta[name="importmap-type"]',
16 | );
17 |
18 | const domainsElement = document.querySelector(`meta[name="${domainsMeta}"]`);
19 |
20 | const externalOverrideMapPromises = {};
21 |
22 | export const importMapType =
23 | importMapMetaElement?.getAttribute("content") ?? "importmap";
24 |
25 | export let isDisabled;
26 |
27 | if (domainsElement) {
28 | const content = domainsElement.getAttribute("content");
29 | if (!content) {
30 | console.warn(`Invalid ${domainsMeta} meta element - content required.`);
31 | }
32 |
33 | const matchHostname = (domain) =>
34 | new RegExp(escapeStringRegexp(domain).replace("\\*", ".+")).test(
35 | window.location.hostname,
36 | );
37 |
38 | if (content.indexOf(allowListPrefix) === 0) {
39 | const allowedDomains = content.slice(allowListPrefix.length).split(",");
40 | isDisabled = !allowedDomains.some(matchHostname);
41 | } else if (content.indexOf(denyListPrefix) === 0) {
42 | const deniedDomains = content.slice(denyListPrefix.length).split(",");
43 | isDisabled = deniedDomains.some(matchHostname);
44 | } else {
45 | // eslint-disable-next-line no-console
46 | console.log(
47 | `Invalid ${domainsMeta} meta content attribute - must start with ${allowListPrefix} or ${denyListPrefix}`,
48 | );
49 | }
50 | } else {
51 | isDisabled = false;
52 | }
53 |
54 | if (!canAccessLocalStorage()) {
55 | console.warn(
56 | "Disabling import-map-overrides, since local storage is not readable",
57 | );
58 | isDisabled = true;
59 | }
60 |
61 | if (!isDisabled) {
62 | init();
63 | }
64 |
65 | function init() {
66 | const serverOverrides = importMapMetaElement
67 | ? importMapMetaElement.hasAttribute("server-cookie")
68 | : false;
69 | const serverOnly = importMapMetaElement
70 | ? importMapMetaElement.hasAttribute("server-only")
71 | : false;
72 | const allowQueryParamOverride = importMapMetaElement
73 | ? importMapMetaElement.hasAttribute("allow-query-param-override")
74 | : false;
75 |
76 | let defaultMapPromise;
77 |
78 | window.importMapOverrides = {
79 | addOverride(moduleName, url) {
80 | const portRegex = /^\d+$/g;
81 | if (portRegex.test(url)) {
82 | url = imo.getUrlFromPort(moduleName, url);
83 | }
84 | const key = localStoragePrefix + moduleName;
85 | localStorage.setItem(key, url);
86 | if (serverOverrides) {
87 | document.cookie = `${key}=${url}`;
88 | }
89 | fireChangedEvent();
90 | return imo.getOverrideMap();
91 | },
92 | getOverrideMap(includeDisabled = false) {
93 | const overrides = createEmptyImportMap();
94 | const disabledOverrides = imo.getDisabledOverrides();
95 |
96 | const setOverride = (moduleName, path) => {
97 | if (includeDisabled || !(disabledOverrides.indexOf(moduleName) >= 0)) {
98 | overrides.imports[moduleName] = path;
99 | }
100 | };
101 |
102 | // get from localstorage
103 | for (let i = 0; i < localStorage.length; i++) {
104 | const key = localStorage.key(i);
105 | if (key.indexOf(localStoragePrefix) === 0) {
106 | setOverride(
107 | key.slice(localStoragePrefix.length),
108 | localStorage.getItem(key),
109 | );
110 | }
111 | }
112 |
113 | // get from url if query param exist
114 | if (allowQueryParamOverride) {
115 | const queryParam = getParameterByName(
116 | queryParamOverridesName,
117 | window.location != window.parent.location
118 | ? document.referrer
119 | : window.location.href,
120 | );
121 |
122 | if (queryParam) {
123 | let queryParamImportMap;
124 | try {
125 | queryParamImportMap = JSON.parse(queryParam);
126 | } catch (e) {
127 | throw Error(
128 | `Invalid importMap query param - text content must be json`,
129 | );
130 | }
131 | Object.keys(queryParamImportMap.imports).forEach((moduleName) => {
132 | setOverride(moduleName, queryParamImportMap.imports[moduleName]);
133 | });
134 | }
135 | }
136 |
137 | return overrides;
138 | },
139 | removeOverride(moduleName) {
140 | const key = localStoragePrefix + moduleName;
141 | const hasItem = localStorage.getItem(key) !== null;
142 | localStorage.removeItem(key);
143 | if (serverOverrides) {
144 | document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
145 | }
146 | imo.enableOverride(moduleName);
147 | fireChangedEvent();
148 | return hasItem;
149 | },
150 | async getOverrideScopes() {
151 | const scopes = {};
152 | const defaultMap = await imo.getDefaultMap();
153 | const overrideMap = imo.getOverrideMap();
154 | for (let moduleName in overrideMap.imports) {
155 | const defaultUrl = defaultMap.imports[moduleName];
156 | if (defaultUrl) {
157 | const defaultResolution = new URL(defaultUrl, window.location.href)
158 | .href;
159 | const overrideUrl = new URL(
160 | overrideMap.imports[moduleName],
161 | window.location.href,
162 | ).href;
163 | const overrideBase =
164 | overrideUrl.slice(0, overrideUrl.lastIndexOf("/")) + "/";
165 | for (let scope in defaultMap.scopes || {}) {
166 | if (defaultResolution.startsWith(scope)) {
167 | scopes[overrideBase] = {
168 | ...(scopes[overrideBase] || {}),
169 | ...defaultMap.scopes[scope],
170 | };
171 | }
172 | }
173 | }
174 | }
175 |
176 | return { scopes };
177 | },
178 | resetOverrides() {
179 | Object.keys(imo.getOverrideMap(true).imports).forEach((moduleName) => {
180 | imo.removeOverride(moduleName);
181 | });
182 | localStorage.removeItem(disabledOverridesLocalStorageKey);
183 | localStorage.removeItem(externalOverridesLocalStorageKey);
184 | fireChangedEvent();
185 | return imo.getOverrideMap();
186 | },
187 | hasOverrides() {
188 | return Object.keys(imo.getOverrideMap().imports).length > 0;
189 | },
190 | getUrlFromPort(moduleName, port) {
191 | const fileName = moduleName.replace(/@/g, "").replace(/\//g, "-");
192 | return `//localhost:${port}/${fileName}.js`;
193 | },
194 | enableUI() {
195 | const customElementName = "import-map-overrides-full";
196 | const showWhenLocalStorage = "show-when-local-storage";
197 | let customElement = document.querySelector(customElementName);
198 |
199 | if (!customElement) {
200 | customElement = document.createElement(customElementName);
201 | customElement.setAttribute(showWhenLocalStorage, "true");
202 | document.body.appendChild(customElement);
203 | }
204 |
205 | const localStorageKey = customElement.getAttribute(showWhenLocalStorage);
206 | if (localStorageKey) {
207 | localStorage.setItem(localStorageKey, true);
208 | customElement.renderWithPreact();
209 | }
210 | },
211 | mergeImportMap(originalMap, newMap) {
212 | const outMap = createEmptyImportMap();
213 | for (let i in originalMap.imports) {
214 | outMap.imports[i] = originalMap.imports[i];
215 | }
216 | for (let i in newMap.imports) {
217 | outMap.imports[i] = newMap.imports[i];
218 | }
219 | for (let i in originalMap.scopes) {
220 | outMap.scopes[i] = originalMap.scopes[i];
221 | }
222 | for (let i in newMap.scopes) {
223 | outMap.scopes[i] = newMap.scopes[i];
224 | }
225 | return outMap;
226 | },
227 | resetDefaultMap() {
228 | defaultMapPromise = null;
229 | },
230 | getDefaultMap() {
231 | return (
232 | defaultMapPromise ||
233 | (defaultMapPromise = Array.prototype.reduce.call(
234 | document.querySelectorAll(
235 | `script[type="${importMapType}"], script[type="overridable-importmap"]`,
236 | ),
237 | (promise, scriptEl) => {
238 | if (scriptEl.hasAttribute(overrideAttribute)) {
239 | return promise;
240 | } else {
241 | let nextPromise;
242 | if (scriptEl.src) {
243 | nextPromise = fetchExternalMap(scriptEl.src);
244 | } else {
245 | nextPromise = Promise.resolve(JSON.parse(scriptEl.textContent));
246 | }
247 |
248 | return Promise.all([promise, nextPromise]).then(
249 | ([originalMap, newMap]) =>
250 | imo.mergeImportMap(originalMap, newMap),
251 | );
252 | }
253 | },
254 | Promise.resolve(createEmptyImportMap()),
255 | ))
256 | );
257 | },
258 | getCurrentPageMap() {
259 | return Promise.all([
260 | imo.getDefaultMap(),
261 | imo.getExternalOverrideMap(imo.getCurrentPageExternalOverrides()),
262 | ]).then(([defaultMap, externalOverridesMap]) => {
263 | return imo.mergeImportMap(
264 | imo.mergeImportMap(defaultMap, externalOverridesMap),
265 | initialOverrideMap,
266 | );
267 | });
268 | },
269 | getCurrentPageExternalOverrides() {
270 | const currentPageExternalOverrides = [];
271 | document
272 | .querySelectorAll(
273 | `[${overrideAttribute}]:not([id="import-map-overrides"])`,
274 | )
275 | .forEach((externalOverrideEl) => {
276 | currentPageExternalOverrides.push(externalOverrideEl.src);
277 | });
278 | return currentPageExternalOverrides;
279 | },
280 | getNextPageMap() {
281 | return Promise.all([
282 | imo.getDefaultMap(),
283 | imo.getExternalOverrideMap(),
284 | ]).then(([defaultMap, externalOverridesMap]) => {
285 | return imo.mergeImportMap(
286 | imo.mergeImportMap(defaultMap, externalOverridesMap),
287 | imo.getOverrideMap(),
288 | );
289 | });
290 | },
291 | disableOverride(moduleName) {
292 | const disabledOverrides = imo.getDisabledOverrides();
293 | if (!includes(disabledOverrides, moduleName)) {
294 | localStorage.setItem(
295 | disabledOverridesLocalStorageKey,
296 | JSON.stringify(disabledOverrides.concat(moduleName)),
297 | );
298 | fireChangedEvent();
299 | return true;
300 | } else {
301 | return false;
302 | }
303 | },
304 | enableOverride(moduleName) {
305 | const disabledOverrides = imo.getDisabledOverrides();
306 | const index = disabledOverrides.indexOf(moduleName);
307 | if (index >= 0) {
308 | disabledOverrides.splice(index, 1);
309 | localStorage.setItem(
310 | disabledOverridesLocalStorageKey,
311 | JSON.stringify(disabledOverrides),
312 | );
313 | fireChangedEvent();
314 | return true;
315 | } else {
316 | return false;
317 | }
318 | },
319 | getDisabledOverrides() {
320 | const disabledOverrides = localStorage.getItem(
321 | disabledOverridesLocalStorageKey,
322 | );
323 | return disabledOverrides ? JSON.parse(disabledOverrides) : [];
324 | },
325 | isDisabled(moduleName) {
326 | return includes(imo.getDisabledOverrides(), moduleName);
327 | },
328 | getExternalOverrides() {
329 | let localStorageValue = localStorage.getItem(
330 | externalOverridesLocalStorageKey,
331 | );
332 | return localStorageValue ? JSON.parse(localStorageValue).sort() : [];
333 | },
334 | addExternalOverride(url) {
335 | url = new URL(url, document.baseURI).href;
336 | const overrides = imo.getExternalOverrides();
337 | if (includes(overrides, url)) {
338 | return false;
339 | } else {
340 | localStorage.setItem(
341 | externalOverridesLocalStorageKey,
342 | JSON.stringify(overrides.concat(url)),
343 | );
344 | fireChangedEvent();
345 | return true;
346 | }
347 | },
348 | removeExternalOverride(url) {
349 | const overrides = imo.getExternalOverrides();
350 | if (includes(overrides, url)) {
351 | localStorage.setItem(
352 | externalOverridesLocalStorageKey,
353 | JSON.stringify(overrides.filter((override) => override !== url)),
354 | );
355 | fireChangedEvent();
356 | return true;
357 | } else {
358 | return false;
359 | }
360 | },
361 | getExternalOverrideMap(externalOverrides = imo.getExternalOverrides()) {
362 | return externalOverrides.reduce((result, externalOverride) => {
363 | const fetchPromise =
364 | externalOverrideMapPromises[externalOverride] ||
365 | (externalOverrideMapPromises[externalOverride] =
366 | fetchExternalMap(externalOverride));
367 | return Promise.all([result, fetchPromise]).then(
368 | ([firstMap, secondMap]) => {
369 | return imo.mergeImportMap(firstMap, secondMap);
370 | },
371 | );
372 | }, Promise.resolve(createEmptyImportMap()));
373 | },
374 | isExternalMapValid(importMapUrl) {
375 | const promise =
376 | externalOverrideMapPromises[importMapUrl] ||
377 | (externalOverrideMapPromises[importMapUrl] =
378 | fetchExternalMap(importMapUrl));
379 | return promise.then(
380 | () => !includes(imo.invalidExternalMaps, importMapUrl),
381 | );
382 | },
383 | invalidExternalMaps: [],
384 | };
385 |
386 | const imo = window.importMapOverrides;
387 |
388 | let canFireCustomEvents = true;
389 | try {
390 | if (CustomEvent) {
391 | new CustomEvent("a");
392 | } else {
393 | canFireCustomEvents = false;
394 | }
395 | } catch (err) {
396 | canFireCustomEvents = false;
397 | }
398 |
399 | function fireChangedEvent() {
400 | fireEvent("change");
401 | }
402 |
403 | function fireEvent(type) {
404 | // Set timeout so that event fires after the change has totally finished
405 | setTimeout(() => {
406 | const eventType = `import-map-overrides:${type}`;
407 | const event = canFireCustomEvents
408 | ? new CustomEvent(eventType)
409 | : document.createEvent("CustomEvent");
410 | if (!canFireCustomEvents) {
411 | event.initCustomEvent(eventType, true, true, null);
412 | }
413 | window.dispatchEvent(event);
414 | });
415 | }
416 |
417 | const initialOverrideMap = imo.getOverrideMap();
418 | const initialExternalOverrideMaps = imo.getExternalOverrides();
419 |
420 | let referenceNode;
421 |
422 | if (!serverOnly) {
423 | const overridableImportMap = document.querySelector(
424 | 'script[type="overridable-importmap"]',
425 | );
426 |
427 | referenceNode = overridableImportMap;
428 |
429 | if (!referenceNode) {
430 | const importMaps = document.querySelectorAll(
431 | `script[type="${importMapType}"]`,
432 | );
433 | referenceNode = importMaps ? importMaps[importMaps.length - 1] : null;
434 | }
435 |
436 | if (overridableImportMap) {
437 | if (overridableImportMap.src) {
438 | throw Error(
439 | `import-map-overrides: external import maps with type="overridable-importmap" are not supported`,
440 | );
441 | }
442 | let originalMap;
443 | try {
444 | originalMap = JSON.parse(overridableImportMap.textContent);
445 | } catch (e) {
446 | throw Error(
447 | `Invalid
13 |
14 |
15 |
16 |
17 |
18 |