@openmrs/esm-dynamic-loading 9.0.3-pre.4533 → 9.0.3-pre.4550

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,363 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import createFetchMock from 'vitest-fetch-mock';
3
+
4
+ createFetchMock(vi).enableMocks();
5
+
6
+ describe('import-maps', () => {
7
+ function setDomImportMaps(maps: Array<{ imports: Record<string, string> }>) {
8
+ document.querySelectorAll('script[type="systemjs-importmap"]').forEach((el) => el.remove());
9
+
10
+ for (const map of maps) {
11
+ const script = document.createElement('script');
12
+ script.type = 'systemjs-importmap';
13
+ script.textContent = JSON.stringify(map);
14
+ document.head.appendChild(script);
15
+ }
16
+ }
17
+
18
+ beforeEach(() => {
19
+ localStorage.clear();
20
+ fetchMock.resetMocks();
21
+ document.querySelectorAll('script[type="systemjs-importmap"]').forEach((el) => el.remove());
22
+ });
23
+
24
+ afterEach(() => {
25
+ vi.restoreAllMocks();
26
+ // Reset spaEnv so it can be reassigned in next test
27
+ Object.defineProperty(window, 'spaEnv', { value: undefined, writable: true, configurable: true });
28
+ });
29
+
30
+ describe('production mode', () => {
31
+ beforeEach(async () => {
32
+ (window as any).spaEnv = 'production';
33
+ vi.resetModules();
34
+ });
35
+
36
+ it('getCurrentPageMap returns the base map without overrides', async () => {
37
+ const { setupImportMapOverrides, getCurrentPageMap } = await import('./import-maps');
38
+ setupImportMapOverrides();
39
+
40
+ setDomImportMaps([
41
+ { imports: { '@openmrs/esm-foo': '/foo.js' } },
42
+ { imports: { '@openmrs/esm-bar': '/bar.js' } },
43
+ ]);
44
+
45
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', 'http://evil.com/foo.js');
46
+
47
+ const map = await getCurrentPageMap();
48
+ expect(map.imports['@openmrs/esm-foo']).toBe('/foo.js');
49
+ expect(map.imports['@openmrs/esm-bar']).toBe('/bar.js');
50
+ });
51
+
52
+ it('getImportMapOverrideMap returns empty imports', async () => {
53
+ const { setupImportMapOverrides, getImportMapOverrideMap } = await import('./import-maps');
54
+ setupImportMapOverrides();
55
+
56
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', 'http://evil.com/foo.js');
57
+ const map = getImportMapOverrideMap();
58
+ expect(map.imports).toEqual({});
59
+ });
60
+
61
+ it('addImportMapOverride is a no-op', async () => {
62
+ const { setupImportMapOverrides, addImportMapOverride } = await import('./import-maps');
63
+ setupImportMapOverrides();
64
+
65
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
66
+ addImportMapOverride('@openmrs/esm-foo', 'http://evil.com/foo.js');
67
+ expect(localStorage.getItem('import-map-override:@openmrs/esm-foo')).toBeNull();
68
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('disabled in production'));
69
+ });
70
+
71
+ it('removeImportMapOverride is a no-op', async () => {
72
+ const { setupImportMapOverrides, removeImportMapOverride } = await import('./import-maps');
73
+ setupImportMapOverrides();
74
+
75
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
76
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', '/foo.js');
77
+ removeImportMapOverride('@openmrs/esm-foo');
78
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('disabled in production'));
79
+ });
80
+
81
+ it('resetImportMapOverrides is a no-op', async () => {
82
+ const { setupImportMapOverrides, resetImportMapOverrides } = await import('./import-maps');
83
+ setupImportMapOverrides();
84
+
85
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
86
+ resetImportMapOverrides();
87
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('disabled in production'));
88
+ });
89
+
90
+ it('getImportMapDisabledOverrides returns empty array', async () => {
91
+ const { setupImportMapOverrides, getImportMapDisabledOverrides } = await import('./import-maps');
92
+ setupImportMapOverrides();
93
+
94
+ expect(getImportMapDisabledOverrides()).toEqual([]);
95
+ });
96
+
97
+ it('isImportMapOverrideDisabled returns false', async () => {
98
+ const { setupImportMapOverrides, isImportMapOverrideDisabled } = await import('./import-maps');
99
+ setupImportMapOverrides();
100
+
101
+ expect(isImportMapOverrideDisabled('@openmrs/esm-foo')).toBe(false);
102
+ });
103
+ });
104
+
105
+ describe('development mode', () => {
106
+ beforeEach(async () => {
107
+ (window as any).spaEnv = 'development';
108
+ vi.resetModules();
109
+ });
110
+
111
+ it('getCurrentPageMap merges base map with overrides', async () => {
112
+ setDomImportMaps([{ imports: { '@openmrs/esm-foo': '/foo.js', '@openmrs/esm-bar': '/bar.js' } }]);
113
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', 'http://localhost:8080/foo.js');
114
+
115
+ const { setupImportMapOverrides, getCurrentPageMap } = await import('./import-maps');
116
+ setupImportMapOverrides();
117
+
118
+ const map = await getCurrentPageMap();
119
+ expect(map.imports['@openmrs/esm-foo']).toBe('http://localhost:8080/foo.js');
120
+ expect(map.imports['@openmrs/esm-bar']).toBe('/bar.js');
121
+ });
122
+
123
+ it('getImportMapDefaultMap returns only the base map', async () => {
124
+ setDomImportMaps([{ imports: { '@openmrs/esm-foo': '/foo.js' } }]);
125
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', 'http://localhost:8080/foo.js');
126
+
127
+ const { setupImportMapOverrides, getImportMapDefaultMap } = await import('./import-maps');
128
+ setupImportMapOverrides();
129
+
130
+ const map = await getImportMapDefaultMap();
131
+ expect(map.imports['@openmrs/esm-foo']).toBe('/foo.js');
132
+ });
133
+
134
+ it('addImportMapOverride stores in localStorage', async () => {
135
+ const { setupImportMapOverrides, addImportMapOverride } = await import('./import-maps');
136
+ setupImportMapOverrides();
137
+
138
+ addImportMapOverride('@openmrs/esm-foo', 'http://localhost:8080/foo.js');
139
+ expect(localStorage.getItem('import-map-override:@openmrs/esm-foo')).toBe('http://localhost:8080/foo.js');
140
+ });
141
+
142
+ it('removeImportMapOverride removes from localStorage', async () => {
143
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', 'http://localhost:8080/foo.js');
144
+
145
+ const { setupImportMapOverrides, removeImportMapOverride } = await import('./import-maps');
146
+ setupImportMapOverrides();
147
+
148
+ removeImportMapOverride('@openmrs/esm-foo');
149
+ expect(localStorage.getItem('import-map-override:@openmrs/esm-foo')).toBeNull();
150
+ });
151
+
152
+ it('resetImportMapOverrides clears all override keys', async () => {
153
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', '/foo.js');
154
+ localStorage.setItem('import-map-override:@openmrs/esm-bar', '/bar.js');
155
+ localStorage.setItem('unrelated-key', 'value');
156
+
157
+ const { setupImportMapOverrides, resetImportMapOverrides } = await import('./import-maps');
158
+ setupImportMapOverrides();
159
+
160
+ resetImportMapOverrides();
161
+ expect(localStorage.getItem('import-map-override:@openmrs/esm-foo')).toBeNull();
162
+ expect(localStorage.getItem('import-map-override:@openmrs/esm-bar')).toBeNull();
163
+ expect(localStorage.getItem('unrelated-key')).toBe('value');
164
+ });
165
+
166
+ it('addImportMapOverride fires change event', async () => {
167
+ const { setupImportMapOverrides, addImportMapOverride } = await import('./import-maps');
168
+ setupImportMapOverrides();
169
+
170
+ const handler = vi.fn();
171
+ window.addEventListener('import-map-overrides:change', handler);
172
+
173
+ addImportMapOverride('@openmrs/esm-foo', '/foo.js');
174
+ expect(handler).toHaveBeenCalledTimes(1);
175
+
176
+ window.removeEventListener('import-map-overrides:change', handler);
177
+ });
178
+
179
+ it('getImportMapOverrideMap excludes disabled overrides by default', async () => {
180
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', '/foo.js');
181
+ localStorage.setItem('import-map-override:@openmrs/esm-bar', '/bar.js');
182
+ localStorage.setItem('import-map-overrides-disabled', JSON.stringify(['@openmrs/esm-foo']));
183
+
184
+ const { setupImportMapOverrides, getImportMapOverrideMap } = await import('./import-maps');
185
+ setupImportMapOverrides();
186
+
187
+ const map = getImportMapOverrideMap();
188
+ expect(map.imports['@openmrs/esm-foo']).toBeUndefined();
189
+ expect(map.imports['@openmrs/esm-bar']).toBe('/bar.js');
190
+ });
191
+
192
+ it('getImportMapOverrideMap includes disabled overrides when requested', async () => {
193
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', '/foo.js');
194
+ localStorage.setItem('import-map-overrides-disabled', JSON.stringify(['@openmrs/esm-foo']));
195
+
196
+ const { setupImportMapOverrides, getImportMapOverrideMap } = await import('./import-maps');
197
+ setupImportMapOverrides();
198
+
199
+ const map = getImportMapOverrideMap(true);
200
+ expect(map.imports['@openmrs/esm-foo']).toBe('/foo.js');
201
+ });
202
+
203
+ it('getImportMapNextPageMap merges base map with current overrides', async () => {
204
+ setDomImportMaps([{ imports: { '@openmrs/esm-foo': '/foo.js', '@openmrs/esm-bar': '/bar.js' } }]);
205
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', 'http://localhost:8080/foo.js');
206
+
207
+ const { setupImportMapOverrides, addImportMapOverride, getImportMapNextPageMap } = await import('./import-maps');
208
+ setupImportMapOverrides();
209
+
210
+ // Add a new override after setup — should appear in the next-page map but not the current-page snapshot
211
+ addImportMapOverride('@openmrs/esm-bar', 'http://localhost:8080/bar.js');
212
+
213
+ const map = await getImportMapNextPageMap();
214
+ expect(map.imports['@openmrs/esm-foo']).toBe('http://localhost:8080/foo.js');
215
+ expect(map.imports['@openmrs/esm-bar']).toBe('http://localhost:8080/bar.js');
216
+ });
217
+
218
+ it('isImportMapOverrideDisabled returns true for a disabled override', async () => {
219
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', '/foo.js');
220
+ localStorage.setItem('import-map-overrides-disabled', JSON.stringify(['@openmrs/esm-foo']));
221
+
222
+ const { setupImportMapOverrides, isImportMapOverrideDisabled } = await import('./import-maps');
223
+ setupImportMapOverrides();
224
+
225
+ expect(isImportMapOverrideDisabled('@openmrs/esm-foo')).toBe(true);
226
+ expect(isImportMapOverrideDisabled('@openmrs/esm-bar')).toBe(false);
227
+ });
228
+
229
+ it('enableImportMapOverride re-enables a disabled override', async () => {
230
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', '/foo.js');
231
+ localStorage.setItem('import-map-override:@openmrs/esm-bar', '/bar.js');
232
+ localStorage.setItem('import-map-overrides-disabled', JSON.stringify(['@openmrs/esm-foo', '@openmrs/esm-bar']));
233
+
234
+ const {
235
+ setupImportMapOverrides,
236
+ enableImportMapOverride,
237
+ getImportMapOverrideMap,
238
+ getImportMapDisabledOverrides,
239
+ } = await import('./import-maps');
240
+ setupImportMapOverrides();
241
+
242
+ // Both overrides are disabled — neither appears in the active override map
243
+ expect(getImportMapOverrideMap().imports['@openmrs/esm-foo']).toBeUndefined();
244
+
245
+ enableImportMapOverride('@openmrs/esm-foo');
246
+
247
+ // Now foo is enabled again
248
+ expect(getImportMapOverrideMap().imports['@openmrs/esm-foo']).toBe('/foo.js');
249
+ // bar is still disabled
250
+ expect(getImportMapDisabledOverrides()).toEqual(['@openmrs/esm-bar']);
251
+ });
252
+
253
+ it('enableImportMapOverride removes the disabled key when the last override is re-enabled', async () => {
254
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', '/foo.js');
255
+ localStorage.setItem('import-map-overrides-disabled', JSON.stringify(['@openmrs/esm-foo']));
256
+
257
+ const { setupImportMapOverrides, enableImportMapOverride, getImportMapDisabledOverrides } = await import(
258
+ './import-maps'
259
+ );
260
+ setupImportMapOverrides();
261
+
262
+ enableImportMapOverride('@openmrs/esm-foo');
263
+
264
+ expect(getImportMapDisabledOverrides()).toEqual([]);
265
+ expect(localStorage.getItem('import-map-overrides-disabled')).toBeNull();
266
+ });
267
+
268
+ it('enableImportMapOverride fires a change event', async () => {
269
+ localStorage.setItem('import-map-override:@openmrs/esm-foo', '/foo.js');
270
+ localStorage.setItem('import-map-overrides-disabled', JSON.stringify(['@openmrs/esm-foo']));
271
+
272
+ const { setupImportMapOverrides, enableImportMapOverride } = await import('./import-maps');
273
+ setupImportMapOverrides();
274
+
275
+ const handler = vi.fn();
276
+ window.addEventListener('import-map-overrides:change', handler);
277
+
278
+ enableImportMapOverride('@openmrs/esm-foo');
279
+ expect(handler).toHaveBeenCalledTimes(1);
280
+
281
+ window.removeEventListener('import-map-overrides:change', handler);
282
+ });
283
+
284
+ it('handles remote import maps via fetch', async () => {
285
+ const script = document.createElement('script');
286
+ script.type = 'systemjs-importmap';
287
+ Object.defineProperty(script, 'src', { value: 'http://localhost/importmap.json', writable: false });
288
+ document.head.appendChild(script);
289
+
290
+ fetchMock.mockResponseOnce(JSON.stringify({ imports: { '@openmrs/esm-remote': '/remote.js' } }));
291
+
292
+ const { setupImportMapOverrides, getCurrentPageMap } = await import('./import-maps');
293
+ setupImportMapOverrides();
294
+
295
+ const map = await getCurrentPageMap();
296
+ expect(map.imports['@openmrs/esm-remote']).toBe('/remote.js');
297
+ });
298
+
299
+ it('getImportMapNextPageMap does not include overrides added after setup in getCurrentPageMap', async () => {
300
+ setDomImportMaps([{ imports: { '@openmrs/esm-foo': '/foo.js' } }]);
301
+
302
+ const { setupImportMapOverrides, addImportMapOverride, getCurrentPageMap, getImportMapNextPageMap } =
303
+ await import('./import-maps');
304
+ setupImportMapOverrides();
305
+
306
+ // Add an override after setup
307
+ addImportMapOverride('@openmrs/esm-foo', 'http://localhost:8080/foo.js');
308
+
309
+ // getCurrentPageMap uses the snapshot from setup time — override not reflected
310
+ const currentMap = await getCurrentPageMap();
311
+ expect(currentMap.imports['@openmrs/esm-foo']).toBe('/foo.js');
312
+
313
+ // getImportMapNextPageMap reads the live overrides — reflects the new one
314
+ const nextMap = await getImportMapNextPageMap();
315
+ expect(nextMap.imports['@openmrs/esm-foo']).toBe('http://localhost:8080/foo.js');
316
+ });
317
+ });
318
+
319
+ describe('merging behavior', () => {
320
+ it('later maps override earlier ones', async () => {
321
+ (window as any).spaEnv = 'production';
322
+ vi.resetModules();
323
+
324
+ setDomImportMaps([
325
+ { imports: { '@openmrs/esm-foo': '/foo-v1.js' } },
326
+ { imports: { '@openmrs/esm-foo': '/foo-v2.js' } },
327
+ ]);
328
+
329
+ const { setupImportMapOverrides, getCurrentPageMap } = await import('./import-maps');
330
+ setupImportMapOverrides();
331
+
332
+ const map = await getCurrentPageMap();
333
+ expect(map.imports['@openmrs/esm-foo']).toBe('/foo-v2.js');
334
+ });
335
+ });
336
+
337
+ describe('error handling', () => {
338
+ it('skips malformed inline import map script tags', async () => {
339
+ (window as any).spaEnv = 'production';
340
+ vi.resetModules();
341
+
342
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
343
+
344
+ // One valid map, one with invalid JSON
345
+ const good = document.createElement('script');
346
+ good.type = 'systemjs-importmap';
347
+ good.textContent = JSON.stringify({ imports: { '@openmrs/esm-foo': '/foo.js' } });
348
+ document.head.appendChild(good);
349
+
350
+ const bad = document.createElement('script');
351
+ bad.type = 'systemjs-importmap';
352
+ bad.textContent = '{ not valid json';
353
+ document.head.appendChild(bad);
354
+
355
+ const { setupImportMapOverrides, getCurrentPageMap } = await import('./import-maps');
356
+ setupImportMapOverrides();
357
+
358
+ const map = await getCurrentPageMap();
359
+ expect(map.imports['@openmrs/esm-foo']).toBe('/foo.js');
360
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to parse import map'), expect.anything());
361
+ });
362
+ });
363
+ });
@@ -0,0 +1,274 @@
1
+ /** @module @category Import Map */
2
+ import type { ImportMap } from '@openmrs/esm-globals';
3
+
4
+ const OVERRIDE_PREFIX = 'import-map-override:';
5
+ const DISABLED_KEY = 'import-map-overrides-disabled';
6
+ const CHANGE_EVENT = 'import-map-overrides:change';
7
+
8
+ // Set by setupImportMapOverrides(); controls whether override functionality is active.
9
+ let devMode = false;
10
+
11
+ // Snapshot of overrides at setup time (matches import-map-overrides library behavior:
12
+ // getCurrentPageMap returns the overrides as they were when the page loaded).
13
+ let initialOverrideSnapshot: ImportMap | null = null;
14
+
15
+ /**
16
+ * Reads all `<script type="systemjs-importmap">` tags from the DOM and merges
17
+ * them into a single {@link ImportMap}. Tags with a `src` attribute are fetched;
18
+ * inline tags have their `textContent` parsed as JSON.
19
+ */
20
+ async function readBaseMap(): Promise<ImportMap> {
21
+ const scripts = document.querySelectorAll<HTMLScriptElement>('script[type="systemjs-importmap"]');
22
+ const maps: ImportMap[] = [];
23
+
24
+ for (let i = 0; i < scripts.length; i++) {
25
+ const script = scripts[i];
26
+ try {
27
+ if (script.src) {
28
+ const response = await fetch(script.src);
29
+ maps.push(await response.json());
30
+ } else if (script.textContent) {
31
+ maps.push(JSON.parse(script.textContent));
32
+ }
33
+ } catch (e) {
34
+ console.warn(`[import-maps] Failed to parse import map from script tag at index ${i}`, e);
35
+ }
36
+ }
37
+
38
+ return mergeMaps(maps);
39
+ }
40
+
41
+ function mergeMaps(maps: ImportMap[]): ImportMap {
42
+ const merged: ImportMap = { imports: {} };
43
+ for (const map of maps) {
44
+ if (map?.imports) {
45
+ Object.assign(merged.imports, map.imports);
46
+ }
47
+ }
48
+ return merged;
49
+ }
50
+
51
+ /** Returns all `import-map-override:*` entries from localStorage as an {@link ImportMap}. */
52
+ function readOverrideMap(): ImportMap {
53
+ const imports: Record<string, string> = {};
54
+
55
+ try {
56
+ const disabled = getImportMapDisabledOverrides();
57
+
58
+ for (let i = 0; i < localStorage.length; i++) {
59
+ const key = localStorage.key(i);
60
+ if (key?.startsWith(OVERRIDE_PREFIX)) {
61
+ const moduleName = key.slice(OVERRIDE_PREFIX.length);
62
+ if (!disabled.includes(moduleName)) {
63
+ const url = localStorage.getItem(key);
64
+ if (url) {
65
+ imports[moduleName] = url;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ } catch (e) {
71
+ console.warn('[import-maps] Failed to read import-map overrides from localStorage', e);
72
+ }
73
+
74
+ return { imports };
75
+ }
76
+
77
+ /**
78
+ * Returns the import map for the current page. In dev mode, this merges
79
+ * the base map with the override snapshot captured at page load.
80
+ */
81
+ export async function getCurrentPageMap(): Promise<ImportMap> {
82
+ const base = await readBaseMap();
83
+ if (!devMode) {
84
+ return base;
85
+ }
86
+ const overrides = initialOverrideSnapshot ?? readOverrideMap();
87
+ return mergeMaps([base, overrides]);
88
+ }
89
+
90
+ /**
91
+ * Returns the base import map from the DOM without any overrides applied.
92
+ */
93
+ export async function getImportMapDefaultMap(): Promise<ImportMap> {
94
+ return readBaseMap();
95
+ }
96
+
97
+ /**
98
+ * Returns what the import map will look like on the next page load, including
99
+ * any overrides that have been added/removed since the page loaded.
100
+ * In production, this is the same as the base map.
101
+ */
102
+ export async function getImportMapNextPageMap(): Promise<ImportMap> {
103
+ if (!devMode) {
104
+ return readBaseMap();
105
+ }
106
+ const base = await readBaseMap();
107
+ return mergeMaps([base, readOverrideMap()]);
108
+ }
109
+
110
+ /**
111
+ * Returns the current override map. In production, returns empty imports.
112
+ * @param includeDisabled If true, includes disabled overrides.
113
+ */
114
+ export function getImportMapOverrideMap(includeDisabled?: boolean): ImportMap {
115
+ if (!devMode) {
116
+ return { imports: {} };
117
+ }
118
+
119
+ if (includeDisabled) {
120
+ const imports: Record<string, string> = {};
121
+ for (let i = 0; i < localStorage.length; i++) {
122
+ const key = localStorage.key(i);
123
+ if (key?.startsWith(OVERRIDE_PREFIX)) {
124
+ const url = localStorage.getItem(key);
125
+ if (url) {
126
+ imports[key.slice(OVERRIDE_PREFIX.length)] = url;
127
+ }
128
+ }
129
+ }
130
+ return { imports };
131
+ }
132
+ return readOverrideMap();
133
+ }
134
+
135
+ /**
136
+ * Returns the list of module names whose overrides are disabled.
137
+ * In production, returns an empty array.
138
+ */
139
+ export function getImportMapDisabledOverrides(): string[] {
140
+ if (!devMode) {
141
+ return [];
142
+ }
143
+
144
+ try {
145
+ const raw = localStorage.getItem(DISABLED_KEY);
146
+ if (raw) {
147
+ const parsed = JSON.parse(raw);
148
+ if (Array.isArray(parsed)) {
149
+ return parsed;
150
+ }
151
+ }
152
+ } catch {
153
+ // ignore malformed data
154
+ }
155
+ return [];
156
+ }
157
+
158
+ /**
159
+ * Returns whether the given module's override is disabled.
160
+ * In production, returns false.
161
+ */
162
+ export function isImportMapOverrideDisabled(moduleName: string): boolean {
163
+ if (!devMode) {
164
+ return false;
165
+ }
166
+ return getImportMapDisabledOverrides().includes(moduleName);
167
+ }
168
+
169
+ /**
170
+ * Adds an import map override. In production, this is a no-op.
171
+ */
172
+ export function addImportMapOverride(name: string, url: string): void {
173
+ if (!devMode) {
174
+ console.warn('[Security] Import map overrides are disabled in production mode.');
175
+ return;
176
+ }
177
+ try {
178
+ localStorage.setItem(OVERRIDE_PREFIX + name, url);
179
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT));
180
+ } catch (e) {
181
+ console.warn('[import-maps] Failed to write import-map override to localStorage', e);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Removes an import map override. In production, this is a no-op.
187
+ */
188
+ export function removeImportMapOverride(name: string): void {
189
+ if (!devMode) {
190
+ console.warn('[Security] Import map overrides are disabled in production mode.');
191
+ return;
192
+ }
193
+ try {
194
+ localStorage.removeItem(OVERRIDE_PREFIX + name);
195
+ const disabled = getImportMapDisabledOverrides().filter((n) => n !== name);
196
+ if (disabled.length > 0) {
197
+ localStorage.setItem(DISABLED_KEY, JSON.stringify(disabled));
198
+ } else {
199
+ localStorage.removeItem(DISABLED_KEY);
200
+ }
201
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT));
202
+ } catch (e) {
203
+ console.warn('[import-maps] Failed to update import-map overrides in localStorage', e);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Clears all import map overrides. In production, this is a no-op.
209
+ */
210
+ export function resetImportMapOverrides(): void {
211
+ if (!devMode) {
212
+ console.warn('[Security] Import map overrides are disabled in production mode.');
213
+ return;
214
+ }
215
+ try {
216
+ const keysToRemove: string[] = [];
217
+ for (let i = 0; i < localStorage.length; i++) {
218
+ const key = localStorage.key(i);
219
+ if (key?.startsWith(OVERRIDE_PREFIX)) {
220
+ keysToRemove.push(key);
221
+ }
222
+ }
223
+ keysToRemove.forEach((key) => localStorage.removeItem(key));
224
+ localStorage.removeItem(DISABLED_KEY);
225
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT));
226
+ } catch (e) {
227
+ console.warn('[import-maps] Failed to clear import-map overrides from localStorage', e);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Re-enables a previously disabled import map override. In production, this is a no-op.
233
+ */
234
+ export function enableImportMapOverride(name: string): void {
235
+ if (!devMode) {
236
+ console.warn('[Security] Import map overrides are disabled in production mode.');
237
+ return;
238
+ }
239
+ try {
240
+ const disabled = getImportMapDisabledOverrides().filter((n) => n !== name);
241
+ if (disabled.length > 0) {
242
+ localStorage.setItem(DISABLED_KEY, JSON.stringify(disabled));
243
+ } else {
244
+ localStorage.removeItem(DISABLED_KEY);
245
+ }
246
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT));
247
+ } catch (e) {
248
+ console.warn('[import-maps] Failed to update import-map overrides in localStorage', e);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Initializes the import map override system with mode-aware behavior.
254
+ *
255
+ * In production (`window.spaEnv !== 'development'`), mutation functions are
256
+ * no-ops and localStorage overrides are never consulted. In development, the
257
+ * full override workflow is available.
258
+ *
259
+ * Must be called before any code that depends on the import map functions.
260
+ */
261
+ let initialized = false;
262
+
263
+ export function setupImportMapOverrides() {
264
+ if (initialized) {
265
+ return;
266
+ }
267
+ initialized = true;
268
+
269
+ devMode = window.spaEnv === 'development';
270
+
271
+ if (devMode) {
272
+ initialOverrideSnapshot = readOverrideMap();
273
+ }
274
+ }
package/src/index.ts CHANGED
@@ -1 +1,3 @@
1
1
  export * from './dynamic-loading';
2
+ export * from './import-maps';
3
+ export * from './route-maps';