├── .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://data.jsdelivr.com/v1/package/npm/import-map-overrides/badge)](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 | ![import map overrides 3](https://user-images.githubusercontent.com/5524384/77237035-07476600-6b8a-11ea-9041-8b70f633d5d0.gif) 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 | 19 | -------------------------------------------------------------------------------- /test/embedded-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Import Map Overrides test 11 | 12 | 13 | 14 | 15 | 16 | 23 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/ie11.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Import Map Overrides test 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/importmap.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "app1": "https://localhost:9543/app1.js" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Import Map Overrides Tests 7 | 8 | 9 | 29 | 30 | 42 | 43 | 44 |

Current: embedded-map

45 |
46 | Select a test: 47 | 66 |
67 | 68 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /test/no-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Import Map Overrides test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/server-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Import Map Overrides test 7 | 8 | 9 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/url.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Import Map Overrides test 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------