@lwrjs/loader 0.22.9 → 0.22.11

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 (37) 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 +530 -25
  4. package/build/assets/prod/lwr-loader-shim-legacy.bundle.min.js +3 -3
  5. package/build/assets/prod/lwr-loader-shim-legacy.js +317 -14
  6. package/build/assets/prod/lwr-loader-shim.bundle.js +123 -12
  7. package/build/assets/prod/lwr-loader-shim.bundle.min.js +3 -3
  8. package/build/assets/prod/lwr-loader-shim.js +105 -8
  9. package/build/cjs/modules/lwr/loader/validateLoadSpecifier.cjs +4 -1
  10. package/build/cjs/modules/lwr/loaderLegacy/importMap/importMap.cjs +1 -1
  11. package/build/cjs/modules/lwr/loaderLegacy/importMap/importMapResolver.cjs +12 -0
  12. package/build/cjs/modules/lwr/loaderLegacy/importMap/utils.cjs +13 -1
  13. package/build/cjs/modules/lwr/loaderLegacy/utils/validation.cjs +93 -0
  14. package/build/modules/lwr/esmLoader/esmLoader.js +1 -1
  15. package/build/modules/lwr/loader/loader.js +18 -4
  16. package/build/modules/lwr/loader/validateLoadSpecifier.d.ts +2 -1
  17. package/build/modules/lwr/loader/validateLoadSpecifier.js +11 -2
  18. package/build/modules/lwr/loaderLegacy/importMap/importMap.js +3 -2
  19. package/build/modules/lwr/loaderLegacy/importMap/importMapResolver.d.ts +1 -0
  20. package/build/modules/lwr/loaderLegacy/importMap/importMapResolver.js +13 -0
  21. package/build/modules/lwr/loaderLegacy/importMap/utils.d.ts +12 -1
  22. package/build/modules/lwr/loaderLegacy/importMap/utils.js +24 -2
  23. package/build/modules/lwr/loaderLegacy/loaderLegacy.d.ts +11 -1
  24. package/build/modules/lwr/loaderLegacy/loaderLegacy.js +213 -11
  25. package/build/modules/lwr/loaderLegacy/utils/validation.d.ts +50 -0
  26. package/build/modules/lwr/loaderLegacy/utils/validation.js +114 -0
  27. package/build/shim/defineCacheResolver.d.ts +10 -0
  28. package/build/shim/defineCacheResolver.js +78 -0
  29. package/build/shim/loader.d.ts +5 -1
  30. package/build/shim/loader.js +14 -3
  31. package/build/shim/shim.js +3 -2
  32. package/build/shim-legacy/loaderLegacy.d.ts +7 -2
  33. package/build/shim-legacy/loaderLegacy.js +15 -4
  34. package/build/shim-legacy/shimLegacy.d.ts +14 -0
  35. package/build/shim-legacy/shimLegacy.js +53 -4
  36. package/build/types.d.ts +22 -0
  37. package/package.json +6 -6
@@ -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.9 */
7
+ /* LWR Legacy Module Loader Shim v0.22.11 */
8
8
  (function () {
9
9
  'use strict';
10
10
 
@@ -105,26 +105,121 @@
105
105
  }
106
106
  }
107
107
 
108
+ /**
109
+ * Parse AMD define args. Supports define(id, deps, factory).
110
+ */
111
+ function parseDefine(def) {
112
+ const [, depsOrFactory, factory] = def;
113
+ if (Array.isArray(depsOrFactory) && typeof factory === 'function') {
114
+ return { deps: depsOrFactory, factory };
115
+ }
116
+ if (typeof depsOrFactory === 'function') {
117
+ return { deps: [], factory: depsOrFactory };
118
+ }
119
+ throw new Error('Invalid module definition');
120
+ }
121
+
122
+ /**
123
+ * Resolve a loader dependency: 'exports' returns the bag; otherwise look up in cache and instantiate.
124
+ * Loader deps are assumed to be leaf modules (no deps of their own)—no recursive resolution.
125
+ */
126
+ function resolveDep(dep, exportsObj, cache) {
127
+ if (dep === 'exports') return exportsObj;
128
+ const mod = cache[dep];
129
+ if (!mod) {
130
+ throw new Error(`Dependency "${dep}" not found in defineCache for loader`);
131
+ }
132
+ try {
133
+ return instantiateLeafModule(mod);
134
+ } catch (e) {
135
+ const msg = e instanceof Error ? e.message : String(e);
136
+ throw new Error(`Loader dependency "${dep}" has invalid definition: ${msg}`);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Instantiate a leaf module (loader dep). Assumes the module has no dependencies of its own.
142
+ * Supports only deps: [] or ['exports']; any other dep list throws.
143
+ */
144
+ function instantiateLeafModule(def) {
145
+ const { deps, factory } = parseDefine(def);
146
+ if (deps.length > 1 || (deps.length === 1 && deps[0] !== 'exports')) {
147
+ throw new Error(
148
+ `Loader dependencies must have no deps or only ['exports']; got [${deps.join(', ')}]`,
149
+ );
150
+ }
151
+ const exports = {};
152
+ const out = factory(exports);
153
+ return out !== undefined ? out : exports;
154
+ }
155
+
156
+ /**
157
+ * Normalize loader definition to canonical (exports, ...deps). Injects 'exports' if missing.
158
+ */
159
+ function normalizeLoaderDefinition(def) {
160
+ const { deps, factory } = parseDefine(def);
161
+ if (deps.includes('exports')) {
162
+ return { deps, factory };
163
+ }
164
+ const isEmptyDeps = deps.length === 0;
165
+ const normalizedFactory = (exports, ...rest) => {
166
+ const result = isEmptyDeps ? factory(exports) : factory(...rest);
167
+ if (result !== undefined && typeof result === 'object') {
168
+ Object.assign(exports, result);
169
+ }
170
+ };
171
+ return { deps: ['exports', ...deps], factory: normalizedFactory };
172
+ }
173
+
174
+ /**
175
+ * Resolve the loader's dependencies from defineCache. Throws if definition is invalid or deps cannot be resolved.
176
+ */
177
+ function resolveLoaderDepsFromDefineCache(
178
+ loaderSpecifier,
179
+ definition,
180
+ defineCache,
181
+ ) {
182
+ try {
183
+ const { deps, factory } = normalizeLoaderDefinition(definition);
184
+ const exportsObj = {};
185
+ const args = deps.map((dep) => resolveDep(dep, exportsObj, defineCache)) ;
186
+ return { factory: factory , args, exportsObj };
187
+ } catch (e) {
188
+ const msg = e instanceof Error ? e.message : String(e);
189
+ throw new Error(`Expected loader with specifier "${loaderSpecifier}" to be a module. ${msg}`);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Create a loader from a definition. Original API preserved.
195
+ * Optional defineCache for resolving loader deps.
196
+ * definition[3] (signatures: ownHash, hashes) is passed to loader.define for cache invalidation.
197
+ */
108
198
  function createLoader(
109
199
  name,
110
200
  definition,
111
201
  config,
112
202
  externalModules,
203
+ defineCache,
113
204
  ) {
114
205
  if (!definition || typeof definition[2] !== 'function') {
115
206
  throw new Error(`Expected loader with specifier "${name}" to be a module`);
116
207
  }
117
208
 
118
- // Create a Loader instance
119
209
  const exports = {};
120
- definition[2].call(null, exports);
210
+ if (defineCache) {
211
+ const { factory, args, exportsObj } = resolveLoaderDepsFromDefineCache(name, definition, defineCache);
212
+ factory(...args);
213
+ Object.assign(exports, exportsObj);
214
+ } else {
215
+ definition[2].call(null, exports);
216
+ }
121
217
  const { Loader } = exports;
122
218
  if (!Loader) {
123
219
  throw new Error('Expected Loader class to be defined');
124
220
  }
125
221
  const loader = new Loader(config);
126
222
 
127
- // register externally loaded modules
128
223
  if (externalModules && externalModules.length) {
129
224
  loader.registerExternalModules(externalModules);
130
225
  }
@@ -181,9 +276,164 @@
181
276
  }
182
277
  }
183
278
 
184
- /* 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
+ }
185
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
+ }
186
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
+ }
402
+
403
+ return {
404
+ imports: convertedImports,
405
+ };
406
+ }
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 */
187
437
 
188
438
  /* eslint-disable lwr/no-unguarded-apis */
189
439
  const hasSetTimeout = typeof setTimeout === 'function';
@@ -198,10 +448,11 @@
198
448
 
199
449
  __init() {this.defineCache = {};}
200
450
  __init2() {this.orderedDefs = [];}
451
+ __init3() {this.importMapUpdatesCache = {};}
201
452
 
202
453
  // eslint-disable-line no-undef, lwr/no-unguarded-apis
203
454
 
204
- 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);
205
456
  // Start watchdog timer
206
457
  if (hasSetTimeout) {
207
458
  this.watchdogTimerId = this.startWatchdogTimer();
@@ -210,14 +461,16 @@
210
461
  // Parse configuration
211
462
  this.global = global;
212
463
  this.config = global.LWR ;
213
- this.loaderModule = 'lwr/loaderLegacy/v/0_22_9';
464
+ this.loaderModule = 'lwr/loaderLegacy/v/0_22_11';
214
465
 
215
466
  // Set up error handler
216
467
  this.errorHandler = this.config.onError ;
217
468
 
218
- // Set up the temporary LWR.define function and customInit hook
469
+ // Set up the temporary LWR.define and LWR.importMap functions
219
470
  const tempDefine = this.tempDefine.bind(this);
220
471
  global.LWR.define = tempDefine;
472
+ const tempImportMapMethod = this.tempImportMap.bind(this);
473
+ global.LWR.importMap = tempImportMapMethod;
221
474
  this.bootReady = this.config.autoBoot;
222
475
 
223
476
  try {
@@ -273,6 +526,53 @@
273
526
  }
274
527
  }
275
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
+
276
576
  // Called by the customInit hook via lwr.initializeApp()
277
577
  postCustomInit() {
278
578
  this.bootReady = true;
@@ -305,6 +605,7 @@
305
605
  this.defineCache[this.loaderModule],
306
606
  loaderConfig,
307
607
  this.config.preloadModules,
608
+ this.defineCache,
308
609
  );
309
610
  this.mountApp(loader);
310
611
  if (
@@ -360,17 +661,19 @@
360
661
  const exporter = (exports) => {
361
662
  Object.assign(exports, { logOperationStart, logOperationEnd });
362
663
  };
363
- define('lwr/profiler/v/0_22_9', ['exports'], exporter, {});
664
+ define('lwr/profiler/v/0_22_11', ['exports'], exporter, {});
364
665
  }
365
666
 
366
667
  // Set up the application globals, import map, root custom element...
367
668
  mountApp(loader) {
368
- const { bootstrapModule, rootComponent, importMappings, rootComponents, serverData, endpoints } =
369
- this.config;
669
+ const { bootstrapModule, rootComponent, rootComponents, serverData, endpoints } = this.config;
670
+
671
+ const importMappings = this.getImportMappingsWithUpdates();
370
672
 
371
673
  // Set global LWR.define to loader.define
372
674
  this.global.LWR = Object.freeze({
373
675
  define: loader.define.bind(loader),
676
+ importMap: loader.importMap.bind(loader),
374
677
  rootComponent,
375
678
  rootComponents,
376
679
  serverData: serverData || {},
@@ -395,7 +698,7 @@
395
698
  .registerImportMappings(importMappings)
396
699
  .then(() => {
397
700
  // eslint-disable-next-line lwr/no-unguarded-apis
398
- if (typeof window === 'undefined' || typeof document === undefined) {
701
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
399
702
  return Promise.resolve();
400
703
  }
401
704
  if (initDeferDOM) {
@@ -455,14 +758,14 @@
455
758
  // The loader module is ALWAYS required
456
759
  const GLOBAL = globalThis ;
457
760
  GLOBAL.LWR.requiredModules = GLOBAL.LWR.requiredModules || [];
458
- if (GLOBAL.LWR.requiredModules.indexOf('lwr/loaderLegacy/v/0_22_9') < 0) {
459
- GLOBAL.LWR.requiredModules.push('lwr/loaderLegacy/v/0_22_9');
761
+ if (GLOBAL.LWR.requiredModules.indexOf('lwr/loaderLegacy/v/0_22_11') < 0) {
762
+ GLOBAL.LWR.requiredModules.push('lwr/loaderLegacy/v/0_22_11');
460
763
  }
461
764
  new LoaderShim(GLOBAL);
462
765
 
463
766
  })();
464
767
 
465
- LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use strict';
768
+ LWR.define('lwr/loaderLegacy/v/0_22_11', ['exports'], (function (exports) { 'use strict';
466
769
 
467
770
  const templateRegex = /\{([0-9]+)\}/g;
468
771
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -602,7 +905,7 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
602
905
  level: 0,
603
906
  message: 'Cannot dynamically import the LWR loader with importer "{0}"',
604
907
  });
605
- Object.freeze({
908
+ const NO_IMPORT_TRANSPORT = Object.freeze({
606
909
  code: 3025,
607
910
  level: 0,
608
911
  message: 'Cannot dynamically import "transport" with importer "{0}"',
@@ -1857,9 +2160,10 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
1857
2160
 
1858
2161
 
1859
2162
 
2163
+
1860
2164
  /**
1861
2165
  * Validates that the given module id is not one of the forbidden dynamic import specifiers.
1862
- * Throws LoaderError for: lwc, the LWR loader, and blob URLs.
2166
+ * Throws LoaderError for: lwc, the LWR loader, transport/webruntime/transport, and blob URLs.
1863
2167
  *
1864
2168
  * @param id - Module identifier or URL
1865
2169
  * @param importer - Versioned specifier of the module importer (for error reporting)
@@ -1872,7 +2176,7 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
1872
2176
  loaderSpecifier,
1873
2177
  errors,
1874
2178
  ) {
1875
- const { LoaderError, NO_IMPORT_LWC, NO_IMPORT_LOADER, NO_BLOB_IMPORT } = errors;
2179
+ const { LoaderError, NO_IMPORT_LWC, NO_IMPORT_LOADER, NO_IMPORT_TRANSPORT, NO_BLOB_IMPORT } = errors;
1876
2180
 
1877
2181
  // Throw an error if the specifier is "lwc" or a versioned lwc specifier
1878
2182
  // Dynamic import of LWC APIs is not allowed
@@ -1888,6 +2192,18 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
1888
2192
  throw new LoaderError(NO_IMPORT_LOADER, [_nullishCoalesce(importer, () => ( 'unknown'))]);
1889
2193
  }
1890
2194
 
2195
+ // Throw an error if the specifier is "transport" or "webruntime/transport" (or versioned)
2196
+ // Dynamic import of transport exposes unsandboxed fetch, bypassing LWS
2197
+ // Reference: Hackforce report HF-3393
2198
+ if (
2199
+ id === 'transport' ||
2200
+ id.startsWith('transport/v/') ||
2201
+ id === 'webruntime/transport' ||
2202
+ id.startsWith('webruntime/transport/v/')
2203
+ ) {
2204
+ throw new LoaderError(NO_IMPORT_TRANSPORT, [_nullishCoalesce(importer, () => ( 'unknown'))]);
2205
+ }
2206
+
1891
2207
  // Throw an error if the specifier is a blob URL (case-insensitive check)
1892
2208
  if (id.toLowerCase().startsWith('blob:')) {
1893
2209
  throw new LoaderError(NO_BLOB_IMPORT);
@@ -1906,7 +2222,8 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
1906
2222
  if (segment in matchObj) {
1907
2223
  return segment;
1908
2224
  }
1909
- } while (path.length > 1 && (sepIndex = path.lastIndexOf('/', sepIndex - 1)) !== -1);
2225
+ sepIndex = path.lastIndexOf('/', sepIndex - 1);
2226
+ } while (sepIndex > 0);
1910
2227
  }
1911
2228
  function targetWarning(match, target, msg) {
1912
2229
  if (hasConsole) {
@@ -1915,6 +2232,27 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
1915
2232
  }
1916
2233
  }
1917
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
+
1918
2256
  /**
1919
2257
  * Import map support for LWR based on the spec: https://github.com/WICG/import-maps
1920
2258
  *
@@ -2036,7 +2374,8 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
2036
2374
  if (!mapped) {
2037
2375
  targetWarning(p, rhs, 'bare specifier did not resolve');
2038
2376
  } else {
2039
- outPackages[resolvedLhs] = mapped.uri;
2377
+ // Merge into cache with write-once protection
2378
+ mergeImportMapEntry(resolvedLhs, mapped.uri, outPackages);
2040
2379
  }
2041
2380
  }
2042
2381
  }
@@ -2087,6 +2426,20 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
2087
2426
  resolve(resolvedOrPlain, parentUrl) {
2088
2427
  return resolveImportMapEntry(this.importMap, resolvedOrPlain, parentUrl);
2089
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
+ }
2090
2443
  }
2091
2444
 
2092
2445
  /**
@@ -2158,15 +2511,134 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
2158
2511
  return importMapPromise;
2159
2512
  }
2160
2513
 
2161
- 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
+ }
2162
2552
 
2553
+ if (lowerUri.startsWith('data:')) {
2554
+ throw new Error(`Cannot map ${specifier} to data: URL`);
2555
+ }
2556
+ }
2163
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
+ }
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 = {};
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; }
2164
2635
  /**
2165
2636
  * The LWR loader is inspired and borrows from the algorithms and native browser principles of https://github.com/systemjs/systemjs
2166
2637
  */
2167
2638
  class Loader {
2168
2639
 
2169
2640
 
2641
+
2170
2642
 
2171
2643
 
2172
2644
  constructor(config) {
@@ -2252,6 +2724,7 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
2252
2724
  LoaderError,
2253
2725
  NO_IMPORT_LWC,
2254
2726
  NO_IMPORT_LOADER,
2727
+ NO_IMPORT_TRANSPORT,
2255
2728
  NO_BLOB_IMPORT,
2256
2729
  });
2257
2730
  return this.registry.load(id, importer);
@@ -2294,10 +2767,13 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
2294
2767
  // import maps spec if we do this after resolving any imports
2295
2768
  importMap = resolveAndComposeImportMap(mappings, this.baseUrl, this.parentImportMap);
2296
2769
  }
2297
- this.parentImportMap = importMap;
2298
- if (this.parentImportMap) {
2299
- const importMapResolver = new ImportMapResolver(this.parentImportMap);
2300
- 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;
2301
2777
  }
2302
2778
  }
2303
2779
 
@@ -2310,6 +2786,35 @@ LWR.define('lwr/loaderLegacy/v/0_22_9', ['exports'], (function (exports) { 'use
2310
2786
  this.registry.registerExternalModules(modules);
2311
2787
  }
2312
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
+
2313
2818
  getModuleWarnings(isAppMounted = false) {
2314
2819
  return this.registry.getModuleWarnings(isAppMounted);
2315
2820
  }