@lwrjs/loader 0.22.10 → 0.22.12

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.
Files changed (27) hide show
  1. package/README.md +33 -1
  2. package/build/assets/prod/lwr-error-shim.js +1 -1
  3. package/build/assets/prod/lwr-loader-shim-legacy.bundle.js +413 -18
  4. package/build/assets/prod/lwr-loader-shim-legacy.bundle.min.js +3 -3
  5. package/build/assets/prod/lwr-loader-shim-legacy.js +217 -10
  6. package/build/assets/prod/lwr-loader-shim.bundle.js +6 -6
  7. package/build/assets/prod/lwr-loader-shim.bundle.min.js +2 -2
  8. package/build/assets/prod/lwr-loader-shim.js +5 -5
  9. package/build/cjs/modules/lwr/loaderLegacy/importMap/importMap.cjs +1 -1
  10. package/build/cjs/modules/lwr/loaderLegacy/importMap/importMapResolver.cjs +12 -0
  11. package/build/cjs/modules/lwr/loaderLegacy/importMap/utils.cjs +13 -1
  12. package/build/cjs/modules/lwr/loaderLegacy/utils/validation.cjs +93 -0
  13. package/build/modules/lwr/esmLoader/esmLoader.js +1 -1
  14. package/build/modules/lwr/loader/loader.js +1 -1
  15. package/build/modules/lwr/loaderLegacy/importMap/importMap.js +3 -2
  16. package/build/modules/lwr/loaderLegacy/importMap/importMapResolver.d.ts +1 -0
  17. package/build/modules/lwr/loaderLegacy/importMap/importMapResolver.js +13 -0
  18. package/build/modules/lwr/loaderLegacy/importMap/utils.d.ts +12 -1
  19. package/build/modules/lwr/loaderLegacy/importMap/utils.js +24 -2
  20. package/build/modules/lwr/loaderLegacy/loaderLegacy.d.ts +11 -1
  21. package/build/modules/lwr/loaderLegacy/loaderLegacy.js +196 -8
  22. package/build/modules/lwr/loaderLegacy/utils/validation.d.ts +50 -0
  23. package/build/modules/lwr/loaderLegacy/utils/validation.js +114 -0
  24. package/build/shim-legacy/shimLegacy.d.ts +14 -0
  25. package/build/shim-legacy/shimLegacy.js +51 -2
  26. package/build/types.d.ts +4 -0
  27. package/package.json +6 -6
package/README.md CHANGED
@@ -83,6 +83,38 @@ LWR.define('foo/bar', ['exports'], (exports) => {
83
83
  });
84
84
  ```
85
85
 
86
+ ### `loader.importMap()`
87
+
88
+ ```ts
89
+ interface LwrImportMapUpdateMethod {
90
+ (importMapUpdate: ImportMapUpdate): void;
91
+ }
92
+
93
+ type ImportMapUpdate = Record<string, string[]>;
94
+ ```
95
+
96
+ Apply update of import mappings at runtime. This allows dynamically registering new module mappings after the loader has been initialized. This is a synchronous operation.
97
+
98
+ **parameters**
99
+
100
+ - `importMapUpdate` - Import Map Update object containing:
101
+ - [moduleScriptURL] - Script URL containing LWR define statements
102
+ - string[] - array of module names which are included in the given script
103
+
104
+ **Important**: Import map entries are **write-once** — once a specifier is mapped, it cannot be overridden. This ensures deterministic module resolution and prevents runtime mutation of previously resolved dependencies. If a conflicting mapping is provided for an existing specifier, the new mapping is ignored and a warning is logged.
105
+
106
+ **Important**: Protected modules (`lwc`, `lwr/*`) cannot be remapped for security reasons. Attempting to remap these modules will throw an error.
107
+
108
+ **Example**
109
+
110
+ ```js
111
+ // Register a runtime import mapping
112
+ // ONLY AVAILABLE WHEN USING THE GLOBAL API (AMD / Legacy Mode):
113
+ LWR.importMap({
114
+ '/bundles/my-module.js': ['my/module'],
115
+ });
116
+ ```
117
+
86
118
  ### `loader.load()`
87
119
 
88
120
  ```ts
@@ -280,7 +312,7 @@ console.log(result); // 'abc'
280
312
  The Client Runtime Loader Shim bootstraps the [Loader instance](#api) and other required modules for LWR applications in AMD format. The AMD Loader Shim is responsible for:
281
313
 
282
314
  - defining and executing the [`@lwr/loader`](#api)
283
- - exposure of the loader's `define` API at `globalThis.LWR.define`
315
+ - exposure of the loader's `define` and `importMap` APIs at `globalThis.LWR.define` and `globalThis.LWR.importMap`
284
316
  - client-side orchestration for the generated application bootstrap module
285
317
 
286
318
  ### LWR Application Document
@@ -4,5 +4,5 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6
6
  */
7
- /* LWR Error Shim v0.22.10 */
7
+ /* LWR Error Shim v0.22.12 */
8
8
  !function(){"use strict";const o=globalThis;if(!(o.LWR&&o.LWR.define)){const r=new Error("The LWR application failed to bootstrap");if(!o.LWR||!o.LWR.onError)throw r;o.LWR.onError(r)}}();
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6
6
  */
7
- /* LWR Legacy Module Loader Shim v0.22.10 */
7
+ /* LWR Legacy Module Loader Shim v0.22.12 */
8
8
  (function () {
9
9
  'use strict';
10
10
 
@@ -276,9 +276,164 @@
276
276
  }
277
277
  }
278
278
 
279
- /* global document, process, console */
279
+ /* eslint-disable lwr/no-unguarded-apis */
280
+
281
+ const hasConsole$1 = typeof console !== 'undefined';
282
+
283
+ const hasProcess$1 = typeof process !== 'undefined';
284
+
285
+ // eslint-disable-next-line no-undef
286
+ hasProcess$1 && process.env;
287
+
288
+ /**
289
+ * List of protected module patterns that cannot be remapped.
290
+ * Uses case-insensitive matching to prevent bypass attempts (e.g., HF-1027).
291
+ */
292
+ const PROTECTED_PATTERNS = [
293
+ /^lwc$/i, // Core LWC
294
+ /^lwc\/v\//i, // Versioned LWC variants
295
+ /^@lwc\//i, // LWC scoped packages
296
+ /^lightningmobileruntime\//i, // Lightning mobile runtime
297
+ ];
298
+
299
+ /**
300
+ * Checks if a specifier matches any protected module pattern.
301
+ *
302
+ * @param specifier - The module specifier to check
303
+ * @returns true if the specifier is protected, false otherwise
304
+ */
305
+ function isProtectedSpecifier(specifier) {
306
+ return PROTECTED_PATTERNS.some((pattern) => pattern.test(specifier));
307
+ }
308
+
309
+ /**
310
+ * Validates that a URI does not use dangerous URL schemes.
311
+ *
312
+ * Blocks:
313
+ * - blob: URLs (HF-1027 bypass prevention) - case-insensitive
314
+ * - data: URLs (inline code injection) - case-insensitive
315
+ *
316
+ * @param uri - The URI to validate
317
+ * @param specifier - The specifier being mapped (for error messages)
318
+ * @throws Error if the URI uses a dangerous scheme
319
+ */
320
+ function validateMappingUri(uri, specifier) {
321
+ const lowerUri = uri.toLowerCase();
322
+
323
+ if (lowerUri.startsWith('blob:')) {
324
+ throw new Error(`Cannot map ${specifier} to blob: URL`);
325
+ }
326
+
327
+ if (lowerUri.startsWith('data:')) {
328
+ throw new Error(`Cannot map ${specifier} to data: URL`);
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Validates that a specifier is well-formed and not protected.
334
+ *
335
+ * @param specifier - The module specifier to validate
336
+ * @throws Error if the specifier is invalid or protected
337
+ */
338
+ function validateSpecifier(specifier) {
339
+ if (typeof specifier !== 'string' || specifier.length === 0) {
340
+ throw new Error('Specifier must be a non-empty string');
341
+ }
342
+
343
+ if (isProtectedSpecifier(specifier)) {
344
+ throw new Error(`Cannot remap protected module: ${specifier}`);
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Validates and converts an ImportMapUpdate to ImportMap format.
350
+ *
351
+ * Performs validation checks and then converts from:
352
+ * { moduleScriptURL: [moduleName1, moduleName2] }
353
+ * To:
354
+ * { imports: { moduleName1: moduleScriptURL, moduleName2: moduleScriptURL } }
355
+ *
356
+ * @param update - The ImportMapUpdate object to validate and convert
357
+ * @returns The converted ImportMap, or null if the update is empty
358
+ * @throws Error if any validation fails
359
+ */
360
+ function validateAndConvertImportMapUpdate(update) {
361
+ if (!update || typeof update !== 'object') {
362
+ throw new Error('LWR.importMap() requires an object argument');
363
+ }
364
+
365
+ // Check if update is empty
366
+ const entries = Object.entries(update);
367
+ if (entries.length === 0) {
368
+ if (hasConsole$1) {
369
+ // eslint-disable-next-line lwr/no-unguarded-apis
370
+ console.warn('LWR.importMap() called with empty update object');
371
+ }
372
+ return null;
373
+ }
374
+
375
+ const convertedImports = {};
376
+
377
+ for (const [moduleScriptURL, moduleNames] of entries) {
378
+ if (!Array.isArray(moduleNames)) {
379
+ throw new Error('moduleNames must be an array');
380
+ }
381
+
382
+ if (!moduleScriptURL || typeof moduleScriptURL !== 'string') {
383
+ throw new Error('moduleScriptURL must be a string');
384
+ }
385
+
386
+ validateMappingUri(moduleScriptURL, moduleScriptURL);
387
+
388
+ for (const moduleName of moduleNames) {
389
+ validateSpecifier(moduleName);
390
+ if (moduleName in convertedImports) {
391
+ if (hasConsole$1) {
392
+ // eslint-disable-next-line lwr/no-unguarded-apis
393
+ console.warn(
394
+ `LWR.importMap(): duplicate module "${moduleName}" — already mapped to "${convertedImports[moduleName]}", ignoring mapping to "${moduleScriptURL}"`,
395
+ );
396
+ }
397
+ } else {
398
+ convertedImports[moduleName] = moduleScriptURL;
399
+ }
400
+ }
401
+ }
280
402
 
403
+ return {
404
+ imports: convertedImports,
405
+ };
406
+ }
281
407
 
408
+ function targetWarning(match, target, msg) {
409
+ if (hasConsole$1) {
410
+ // eslint-disable-next-line lwr/no-unguarded-apis, no-undef
411
+ console.warn('Package target ' + msg + ", resolving target '" + target + "' for " + match);
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Import map entries are write-once: once a specifier is mapped, it cannot be overridden.
417
+ * This ensures deterministic module resolution and prevents runtime mutation of previously
418
+ * resolved dependencies. Conflicting mappings are ignored with a warning.
419
+ *
420
+ * Merges a single import map entry into a target imports object.
421
+ * - If the specifier doesn't exist, it is added.
422
+ * - If it exists with the same value, it is silently skipped.
423
+ * - If it exists with a different value, a warning is logged and the entry is skipped.
424
+ */
425
+ function mergeImportMapEntry(specifier, uri, target) {
426
+ const existing = target[specifier];
427
+ if (existing !== undefined) {
428
+ if (existing !== uri) {
429
+ targetWarning(specifier, uri, `already mapped to "${existing}", ignoring conflicting mapping`);
430
+ }
431
+ } else {
432
+ target[specifier] = uri;
433
+ }
434
+ }
435
+
436
+ function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }/* global document, process, console */
282
437
 
283
438
  /* eslint-disable lwr/no-unguarded-apis */
284
439
  const hasSetTimeout = typeof setTimeout === 'function';
@@ -293,10 +448,11 @@
293
448
 
294
449
  __init() {this.defineCache = {};}
295
450
  __init2() {this.orderedDefs = [];}
451
+ __init3() {this.importMapUpdatesCache = {};}
296
452
 
297
453
  // eslint-disable-line no-undef, lwr/no-unguarded-apis
298
454
 
299
- constructor(global) {LoaderShim.prototype.__init.call(this);LoaderShim.prototype.__init2.call(this);
455
+ constructor(global) {LoaderShim.prototype.__init.call(this);LoaderShim.prototype.__init2.call(this);LoaderShim.prototype.__init3.call(this);
300
456
  // Start watchdog timer
301
457
  if (hasSetTimeout) {
302
458
  this.watchdogTimerId = this.startWatchdogTimer();
@@ -305,14 +461,16 @@
305
461
  // Parse configuration
306
462
  this.global = global;
307
463
  this.config = global.LWR ;
308
- this.loaderModule = 'lwr/loaderLegacy/v/0_22_10';
464
+ this.loaderModule = 'lwr/loaderLegacy/v/0_22_12';
309
465
 
310
466
  // Set up error handler
311
467
  this.errorHandler = this.config.onError ;
312
468
 
313
- // Set up the temporary LWR.define function and customInit hook
469
+ // Set up the temporary LWR.define and LWR.importMap functions
314
470
  const tempDefine = this.tempDefine.bind(this);
315
471
  global.LWR.define = tempDefine;
472
+ const tempImportMapMethod = this.tempImportMap.bind(this);
473
+ global.LWR.importMap = tempImportMapMethod;
316
474
  this.bootReady = this.config.autoBoot;
317
475
 
318
476
  try {
@@ -368,6 +526,53 @@
368
526
  }
369
527
  }
370
528
 
529
+ /**
530
+ * Create a temporary LWR.importMap() function which captures all
531
+ * import map updates that occur BEFORE the full loader module is available
532
+ *
533
+ * Each import map update is validated, converted to moduleName -> URL mapping,
534
+ * and merged into the importMapUpdatesCache with write-once protection
535
+ */
536
+ tempImportMap(importMapUpdate) {
537
+ try {
538
+ // Validate and convert the import map update to { imports: { moduleName: moduleScriptURL } }
539
+ const importMap = validateAndConvertImportMapUpdate(importMapUpdate);
540
+
541
+ // Early return if update was empty
542
+ if (!importMap) {
543
+ return;
544
+ }
545
+
546
+ // Merge into cache with write-once protection
547
+ for (const [moduleName, moduleScriptURL] of Object.entries(importMap.imports)) {
548
+ mergeImportMapEntry(moduleName, moduleScriptURL, this.importMapUpdatesCache);
549
+ }
550
+ } catch (e) {
551
+ this.enterErrorState(e);
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Apply all cached import map updates and merge with bootstrap import map
557
+ * Returns merged import map
558
+ */
559
+ getImportMappingsWithUpdates() {
560
+ // Start with bootstrap import map
561
+ // Cast importMappings from object to ImportMap to access properties
562
+ const bootstrapMappings = this.config.importMappings ;
563
+
564
+ // Merge with write-once protection: bootstrap mappings take precedence
565
+ const mergedImports = { ...(_optionalChain([bootstrapMappings, 'optionalAccess', _ => _.imports]) || {}) };
566
+ for (const [specifier, uri] of Object.entries(this.importMapUpdatesCache)) {
567
+ mergeImportMapEntry(specifier, uri, mergedImports);
568
+ }
569
+
570
+ return {
571
+ ...(bootstrapMappings || {}),
572
+ imports: mergedImports,
573
+ };
574
+ }
575
+
371
576
  // Called by the customInit hook via lwr.initializeApp()
372
577
  postCustomInit() {
373
578
  this.bootReady = true;
@@ -456,17 +661,19 @@
456
661
  const exporter = (exports) => {
457
662
  Object.assign(exports, { logOperationStart, logOperationEnd });
458
663
  };
459
- define('lwr/profiler/v/0_22_10', ['exports'], exporter, {});
664
+ define('lwr/profiler/v/0_22_12', ['exports'], exporter, {});
460
665
  }
461
666
 
462
667
  // Set up the application globals, import map, root custom element...
463
668
  mountApp(loader) {
464
- const { bootstrapModule, rootComponent, importMappings, rootComponents, serverData, endpoints } =
465
- this.config;
669
+ const { bootstrapModule, rootComponent, rootComponents, serverData, endpoints } = this.config;
670
+
671
+ const importMappings = this.getImportMappingsWithUpdates();
466
672
 
467
673
  // Set global LWR.define to loader.define
468
674
  this.global.LWR = Object.freeze({
469
675
  define: loader.define.bind(loader),
676
+ importMap: loader.importMap.bind(loader),
470
677
  rootComponent,
471
678
  rootComponents,
472
679
  serverData: serverData || {},
@@ -551,14 +758,14 @@
551
758
  // The loader module is ALWAYS required
552
759
  const GLOBAL = globalThis ;
553
760
  GLOBAL.LWR.requiredModules = GLOBAL.LWR.requiredModules || [];
554
- if (GLOBAL.LWR.requiredModules.indexOf('lwr/loaderLegacy/v/0_22_10') < 0) {
555
- GLOBAL.LWR.requiredModules.push('lwr/loaderLegacy/v/0_22_10');
761
+ if (GLOBAL.LWR.requiredModules.indexOf('lwr/loaderLegacy/v/0_22_12') < 0) {
762
+ GLOBAL.LWR.requiredModules.push('lwr/loaderLegacy/v/0_22_12');
556
763
  }
557
764
  new LoaderShim(GLOBAL);
558
765
 
559
766
  })();
560
767
 
561
- LWR.define('lwr/loaderLegacy/v/0_22_10', ['exports'], (function (exports) { 'use strict';
768
+ LWR.define('lwr/loaderLegacy/v/0_22_12', ['exports'], (function (exports) { 'use strict';
562
769
 
563
770
  const templateRegex = /\{([0-9]+)\}/g;
564
771
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -2015,7 +2222,8 @@ LWR.define('lwr/loaderLegacy/v/0_22_10', ['exports'], (function (exports) { 'use
2015
2222
  if (segment in matchObj) {
2016
2223
  return segment;
2017
2224
  }
2018
- } while (path.length > 1 && (sepIndex = path.lastIndexOf('/', sepIndex - 1)) !== -1);
2225
+ sepIndex = path.lastIndexOf('/', sepIndex - 1);
2226
+ } while (sepIndex > 0);
2019
2227
  }
2020
2228
  function targetWarning(match, target, msg) {
2021
2229
  if (hasConsole) {
@@ -2024,6 +2232,27 @@ LWR.define('lwr/loaderLegacy/v/0_22_10', ['exports'], (function (exports) { 'use
2024
2232
  }
2025
2233
  }
2026
2234
 
2235
+ /**
2236
+ * Import map entries are write-once: once a specifier is mapped, it cannot be overridden.
2237
+ * This ensures deterministic module resolution and prevents runtime mutation of previously
2238
+ * resolved dependencies. Conflicting mappings are ignored with a warning.
2239
+ *
2240
+ * Merges a single import map entry into a target imports object.
2241
+ * - If the specifier doesn't exist, it is added.
2242
+ * - If it exists with the same value, it is silently skipped.
2243
+ * - If it exists with a different value, a warning is logged and the entry is skipped.
2244
+ */
2245
+ function mergeImportMapEntry(specifier, uri, target) {
2246
+ const existing = target[specifier];
2247
+ if (existing !== undefined) {
2248
+ if (existing !== uri) {
2249
+ targetWarning(specifier, uri, `already mapped to "${existing}", ignoring conflicting mapping`);
2250
+ }
2251
+ } else {
2252
+ target[specifier] = uri;
2253
+ }
2254
+ }
2255
+
2027
2256
  /**
2028
2257
  * Import map support for LWR based on the spec: https://github.com/WICG/import-maps
2029
2258
  *
@@ -2145,7 +2374,8 @@ LWR.define('lwr/loaderLegacy/v/0_22_10', ['exports'], (function (exports) { 'use
2145
2374
  if (!mapped) {
2146
2375
  targetWarning(p, rhs, 'bare specifier did not resolve');
2147
2376
  } else {
2148
- outPackages[resolvedLhs] = mapped.uri;
2377
+ // Merge into cache with write-once protection
2378
+ mergeImportMapEntry(resolvedLhs, mapped.uri, outPackages);
2149
2379
  }
2150
2380
  }
2151
2381
  }
@@ -2196,6 +2426,20 @@ LWR.define('lwr/loaderLegacy/v/0_22_10', ['exports'], (function (exports) { 'use
2196
2426
  resolve(resolvedOrPlain, parentUrl) {
2197
2427
  return resolveImportMapEntry(this.importMap, resolvedOrPlain, parentUrl);
2198
2428
  }
2429
+
2430
+ addImportMapEntries(importMap) {
2431
+ if (importMap.imports) {
2432
+ const current = this.importMap;
2433
+ if (!current.imports) {
2434
+ current.imports = {};
2435
+ }
2436
+
2437
+ // Merge into cache with write-once protection
2438
+ for (const specifier in importMap.imports) {
2439
+ mergeImportMapEntry(specifier, importMap.imports[specifier], current.imports);
2440
+ }
2441
+ }
2442
+ }
2199
2443
  }
2200
2444
 
2201
2445
  /**
@@ -2267,15 +2511,134 @@ LWR.define('lwr/loaderLegacy/v/0_22_10', ['exports'], (function (exports) { 'use
2267
2511
  return importMapPromise;
2268
2512
  }
2269
2513
 
2270
- function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2514
+ /**
2515
+ * List of protected module patterns that cannot be remapped.
2516
+ * Uses case-insensitive matching to prevent bypass attempts (e.g., HF-1027).
2517
+ */
2518
+ const PROTECTED_PATTERNS = [
2519
+ /^lwc$/i, // Core LWC
2520
+ /^lwc\/v\//i, // Versioned LWC variants
2521
+ /^@lwc\//i, // LWC scoped packages
2522
+ /^lightningmobileruntime\//i, // Lightning mobile runtime
2523
+ ];
2524
+
2525
+ /**
2526
+ * Checks if a specifier matches any protected module pattern.
2527
+ *
2528
+ * @param specifier - The module specifier to check
2529
+ * @returns true if the specifier is protected, false otherwise
2530
+ */
2531
+ function isProtectedSpecifier(specifier) {
2532
+ return PROTECTED_PATTERNS.some((pattern) => pattern.test(specifier));
2533
+ }
2534
+
2535
+ /**
2536
+ * Validates that a URI does not use dangerous URL schemes.
2537
+ *
2538
+ * Blocks:
2539
+ * - blob: URLs (HF-1027 bypass prevention) - case-insensitive
2540
+ * - data: URLs (inline code injection) - case-insensitive
2541
+ *
2542
+ * @param uri - The URI to validate
2543
+ * @param specifier - The specifier being mapped (for error messages)
2544
+ * @throws Error if the URI uses a dangerous scheme
2545
+ */
2546
+ function validateMappingUri(uri, specifier) {
2547
+ const lowerUri = uri.toLowerCase();
2548
+
2549
+ if (lowerUri.startsWith('blob:')) {
2550
+ throw new Error(`Cannot map ${specifier} to blob: URL`);
2551
+ }
2552
+
2553
+ if (lowerUri.startsWith('data:')) {
2554
+ throw new Error(`Cannot map ${specifier} to data: URL`);
2555
+ }
2556
+ }
2557
+
2558
+ /**
2559
+ * Validates that a specifier is well-formed and not protected.
2560
+ *
2561
+ * @param specifier - The module specifier to validate
2562
+ * @throws Error if the specifier is invalid or protected
2563
+ */
2564
+ function validateSpecifier(specifier) {
2565
+ if (typeof specifier !== 'string' || specifier.length === 0) {
2566
+ throw new Error('Specifier must be a non-empty string');
2567
+ }
2568
+
2569
+ if (isProtectedSpecifier(specifier)) {
2570
+ throw new Error(`Cannot remap protected module: ${specifier}`);
2571
+ }
2572
+ }
2573
+
2574
+ /**
2575
+ * Validates and converts an ImportMapUpdate to ImportMap format.
2576
+ *
2577
+ * Performs validation checks and then converts from:
2578
+ * { moduleScriptURL: [moduleName1, moduleName2] }
2579
+ * To:
2580
+ * { imports: { moduleName1: moduleScriptURL, moduleName2: moduleScriptURL } }
2581
+ *
2582
+ * @param update - The ImportMapUpdate object to validate and convert
2583
+ * @returns The converted ImportMap, or null if the update is empty
2584
+ * @throws Error if any validation fails
2585
+ */
2586
+ function validateAndConvertImportMapUpdate(update) {
2587
+ if (!update || typeof update !== 'object') {
2588
+ throw new Error('LWR.importMap() requires an object argument');
2589
+ }
2271
2590
 
2591
+ // Check if update is empty
2592
+ const entries = Object.entries(update);
2593
+ if (entries.length === 0) {
2594
+ if (hasConsole) {
2595
+ // eslint-disable-next-line lwr/no-unguarded-apis
2596
+ console.warn('LWR.importMap() called with empty update object');
2597
+ }
2598
+ return null;
2599
+ }
2600
+
2601
+ const convertedImports = {};
2272
2602
 
2603
+ for (const [moduleScriptURL, moduleNames] of entries) {
2604
+ if (!Array.isArray(moduleNames)) {
2605
+ throw new Error('moduleNames must be an array');
2606
+ }
2607
+
2608
+ if (!moduleScriptURL || typeof moduleScriptURL !== 'string') {
2609
+ throw new Error('moduleScriptURL must be a string');
2610
+ }
2611
+
2612
+ validateMappingUri(moduleScriptURL, moduleScriptURL);
2613
+
2614
+ for (const moduleName of moduleNames) {
2615
+ validateSpecifier(moduleName);
2616
+ if (moduleName in convertedImports) {
2617
+ if (hasConsole) {
2618
+ // eslint-disable-next-line lwr/no-unguarded-apis
2619
+ console.warn(
2620
+ `LWR.importMap(): duplicate module "${moduleName}" — already mapped to "${convertedImports[moduleName]}", ignoring mapping to "${moduleScriptURL}"`,
2621
+ );
2622
+ }
2623
+ } else {
2624
+ convertedImports[moduleName] = moduleScriptURL;
2625
+ }
2626
+ }
2627
+ }
2628
+
2629
+ return {
2630
+ imports: convertedImports,
2631
+ };
2632
+ }
2633
+
2634
+ function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2273
2635
  /**
2274
2636
  * The LWR loader is inspired and borrows from the algorithms and native browser principles of https://github.com/systemjs/systemjs
2275
2637
  */
2276
2638
  class Loader {
2277
2639
 
2278
2640
 
2641
+
2279
2642
 
2280
2643
 
2281
2644
  constructor(config) {
@@ -2404,10 +2767,13 @@ LWR.define('lwr/loaderLegacy/v/0_22_10', ['exports'], (function (exports) { 'use
2404
2767
  // import maps spec if we do this after resolving any imports
2405
2768
  importMap = resolveAndComposeImportMap(mappings, this.baseUrl, this.parentImportMap);
2406
2769
  }
2407
- this.parentImportMap = importMap;
2408
- if (this.parentImportMap) {
2409
- const importMapResolver = new ImportMapResolver(this.parentImportMap);
2410
- this.registry.setImportResolver(importMapResolver);
2770
+ if (importMap) {
2771
+ if (this.importMapResolver || this.parentImportMap) {
2772
+ throw new LoaderError(BAD_IMPORT_MAP);
2773
+ }
2774
+ this.importMapResolver = new ImportMapResolver(importMap);
2775
+ this.registry.setImportResolver(this.importMapResolver);
2776
+ this.parentImportMap = this.importMapResolver.importMap;
2411
2777
  }
2412
2778
  }
2413
2779
 
@@ -2420,6 +2786,35 @@ LWR.define('lwr/loaderLegacy/v/0_22_10', ['exports'], (function (exports) { 'use
2420
2786
  this.registry.registerExternalModules(modules);
2421
2787
  }
2422
2788
 
2789
+ /**
2790
+ * Apply import map updates at runtime.
2791
+ * Enables adding new import mappings dynamically.
2792
+ *
2793
+ * @param updates - Import Map Update object containing:
2794
+ - [moduleScriptURL] - Script URL containing LWR define statements
2795
+ - string[] - array of module names which are included in the given script
2796
+ */
2797
+ importMap(update) {
2798
+ // Validate and convert the import map update to the ImportMap format (moduleName -> moduleScriptURL)
2799
+ const importMap = validateAndConvertImportMapUpdate(update);
2800
+
2801
+ // Early return if update was empty
2802
+ if (!importMap) {
2803
+ return;
2804
+ }
2805
+
2806
+ if (!this.parentImportMap || !this.importMapResolver) {
2807
+ throw new LoaderError(BAD_IMPORT_MAP);
2808
+ }
2809
+
2810
+ // Merge the new mappings with the base import map - note this goes against
2811
+ // import maps spec if we do this after resolving any imports
2812
+ const resolvedImportMap = resolveAndComposeImportMap(importMap, this.baseUrl, this.parentImportMap);
2813
+
2814
+ this.importMapResolver.addImportMapEntries(resolvedImportMap);
2815
+ this.parentImportMap = this.importMapResolver.importMap;
2816
+ }
2817
+
2423
2818
  getModuleWarnings(isAppMounted = false) {
2424
2819
  return this.registry.getModuleWarnings(isAppMounted);
2425
2820
  }