@lwrjs/loader 0.22.10 → 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 (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
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Validation logic for LWR.importMap() API
3
+ *
4
+ * This module provides validation functions to ensure that import map updates:
5
+ * 1. Do not remap protected system modules (lwc, lwr/*, etc.)
6
+ * 2. Do not use dangerous URL schemes (blob:, data:)
7
+ * 3. Have well-formed specifiers and URIs
8
+ */
9
+ import { hasConsole } from '../utils/dom.js';
10
+ /**
11
+ * List of protected module patterns that cannot be remapped.
12
+ * Uses case-insensitive matching to prevent bypass attempts (e.g., HF-1027).
13
+ */
14
+ const PROTECTED_PATTERNS = [
15
+ /^lwc$/i,
16
+ /^lwc\/v\//i,
17
+ /^@lwc\//i,
18
+ /^lightningmobileruntime\//i, // Lightning mobile runtime
19
+ ];
20
+ /**
21
+ * Checks if a specifier matches any protected module pattern.
22
+ *
23
+ * @param specifier - The module specifier to check
24
+ * @returns true if the specifier is protected, false otherwise
25
+ */
26
+ export function isProtectedSpecifier(specifier) {
27
+ return PROTECTED_PATTERNS.some((pattern) => pattern.test(specifier));
28
+ }
29
+ /**
30
+ * Validates that a URI does not use dangerous URL schemes.
31
+ *
32
+ * Blocks:
33
+ * - blob: URLs (HF-1027 bypass prevention) - case-insensitive
34
+ * - data: URLs (inline code injection) - case-insensitive
35
+ *
36
+ * @param uri - The URI to validate
37
+ * @param specifier - The specifier being mapped (for error messages)
38
+ * @throws Error if the URI uses a dangerous scheme
39
+ */
40
+ export function validateMappingUri(uri, specifier) {
41
+ const lowerUri = uri.toLowerCase();
42
+ if (lowerUri.startsWith('blob:')) {
43
+ throw new Error(`Cannot map ${specifier} to blob: URL`);
44
+ }
45
+ if (lowerUri.startsWith('data:')) {
46
+ throw new Error(`Cannot map ${specifier} to data: URL`);
47
+ }
48
+ }
49
+ /**
50
+ * Validates that a specifier is well-formed and not protected.
51
+ *
52
+ * @param specifier - The module specifier to validate
53
+ * @throws Error if the specifier is invalid or protected
54
+ */
55
+ export function validateSpecifier(specifier) {
56
+ if (typeof specifier !== 'string' || specifier.length === 0) {
57
+ throw new Error('Specifier must be a non-empty string');
58
+ }
59
+ if (isProtectedSpecifier(specifier)) {
60
+ throw new Error(`Cannot remap protected module: ${specifier}`);
61
+ }
62
+ }
63
+ /**
64
+ * Validates and converts an ImportMapUpdate to ImportMap format.
65
+ *
66
+ * Performs validation checks and then converts from:
67
+ * { moduleScriptURL: [moduleName1, moduleName2] }
68
+ * To:
69
+ * { imports: { moduleName1: moduleScriptURL, moduleName2: moduleScriptURL } }
70
+ *
71
+ * @param update - The ImportMapUpdate object to validate and convert
72
+ * @returns The converted ImportMap, or null if the update is empty
73
+ * @throws Error if any validation fails
74
+ */
75
+ export function validateAndConvertImportMapUpdate(update) {
76
+ if (!update || typeof update !== 'object') {
77
+ throw new Error('LWR.importMap() requires an object argument');
78
+ }
79
+ // Check if update is empty
80
+ const entries = Object.entries(update);
81
+ if (entries.length === 0) {
82
+ if (hasConsole) {
83
+ // eslint-disable-next-line lwr/no-unguarded-apis
84
+ console.warn('LWR.importMap() called with empty update object');
85
+ }
86
+ return null;
87
+ }
88
+ const convertedImports = {};
89
+ for (const [moduleScriptURL, moduleNames] of entries) {
90
+ if (!Array.isArray(moduleNames)) {
91
+ throw new Error('moduleNames must be an array');
92
+ }
93
+ if (!moduleScriptURL || typeof moduleScriptURL !== 'string') {
94
+ throw new Error('moduleScriptURL must be a string');
95
+ }
96
+ validateMappingUri(moduleScriptURL, moduleScriptURL);
97
+ for (const moduleName of moduleNames) {
98
+ validateSpecifier(moduleName);
99
+ if (moduleName in convertedImports) {
100
+ if (hasConsole) {
101
+ // eslint-disable-next-line lwr/no-unguarded-apis
102
+ console.warn(`LWR.importMap(): duplicate module "${moduleName}" — already mapped to "${convertedImports[moduleName]}", ignoring mapping to "${moduleScriptURL}"`);
103
+ }
104
+ }
105
+ else {
106
+ convertedImports[moduleName] = moduleScriptURL;
107
+ }
108
+ }
109
+ }
110
+ return {
111
+ imports: convertedImports,
112
+ };
113
+ }
114
+ //# sourceMappingURL=validation.js.map
@@ -6,6 +6,7 @@ export default class LoaderShim {
6
6
  private loaderModule;
7
7
  private defineCache;
8
8
  private orderedDefs;
9
+ private importMapUpdatesCache;
9
10
  private errorHandler?;
10
11
  private watchdogTimerId?;
11
12
  constructor(global: GlobalThis);
@@ -20,6 +21,19 @@ export default class LoaderShim {
20
21
  * the order in which the modules were defined
21
22
  */
22
23
  private tempDefine;
24
+ /**
25
+ * Create a temporary LWR.importMap() function which captures all
26
+ * import map updates that occur BEFORE the full loader module is available
27
+ *
28
+ * Each import map update is validated, converted to moduleName -> URL mapping,
29
+ * and merged into the importMapUpdatesCache with write-once protection
30
+ */
31
+ private tempImportMap;
32
+ /**
33
+ * Apply all cached import map updates and merge with bootstrap import map
34
+ * Returns merged import map
35
+ */
36
+ private getImportMappingsWithUpdates;
23
37
  private postCustomInit;
24
38
  private initApp;
25
39
  private waitForBody;
@@ -4,6 +4,8 @@ import { logOperationStart, logOperationEnd } from 'lwr/profiler';
4
4
  import { createLoader } from './loaderLegacy.js';
5
5
  import { REQUIRED_MODULES_TIMEOUT } from '../shim/constants.js';
6
6
  import { customInit } from '../shim/customInit.js';
7
+ import { validateAndConvertImportMapUpdate } from '../modules/lwr/loaderLegacy/utils/validation.js';
8
+ import { mergeImportMapEntry } from '../modules/lwr/loaderLegacy/importMap/utils.js';
7
9
  /* eslint-disable lwr/no-unguarded-apis */
8
10
  const hasSetTimeout = typeof setTimeout === 'function';
9
11
  const hasConsole = typeof console !== 'undefined';
@@ -13,6 +15,7 @@ export default class LoaderShim {
13
15
  constructor(global) {
14
16
  this.defineCache = {};
15
17
  this.orderedDefs = [];
18
+ this.importMapUpdatesCache = {};
16
19
  // Start watchdog timer
17
20
  if (hasSetTimeout) {
18
21
  this.watchdogTimerId = this.startWatchdogTimer();
@@ -23,9 +26,11 @@ export default class LoaderShim {
23
26
  this.loaderModule = 'lwr/loaderLegacy/v/__VERSION__';
24
27
  // Set up error handler
25
28
  this.errorHandler = this.config.onError;
26
- // Set up the temporary LWR.define function and customInit hook
29
+ // Set up the temporary LWR.define and LWR.importMap functions
27
30
  const tempDefine = this.tempDefine.bind(this);
28
31
  global.LWR.define = tempDefine;
32
+ const tempImportMapMethod = this.tempImportMap.bind(this);
33
+ global.LWR.importMap = tempImportMapMethod;
29
34
  this.bootReady = this.config.autoBoot;
30
35
  try {
31
36
  this.createProfilerModule(global.LWR.define);
@@ -73,6 +78,48 @@ export default class LoaderShim {
73
78
  this.initApp();
74
79
  }
75
80
  }
81
+ /**
82
+ * Create a temporary LWR.importMap() function which captures all
83
+ * import map updates that occur BEFORE the full loader module is available
84
+ *
85
+ * Each import map update is validated, converted to moduleName -> URL mapping,
86
+ * and merged into the importMapUpdatesCache with write-once protection
87
+ */
88
+ tempImportMap(importMapUpdate) {
89
+ try {
90
+ // Validate and convert the import map update to { imports: { moduleName: moduleScriptURL } }
91
+ const importMap = validateAndConvertImportMapUpdate(importMapUpdate);
92
+ // Early return if update was empty
93
+ if (!importMap) {
94
+ return;
95
+ }
96
+ // Merge into cache with write-once protection
97
+ for (const [moduleName, moduleScriptURL] of Object.entries(importMap.imports)) {
98
+ mergeImportMapEntry(moduleName, moduleScriptURL, this.importMapUpdatesCache);
99
+ }
100
+ }
101
+ catch (e) {
102
+ this.enterErrorState(e);
103
+ }
104
+ }
105
+ /**
106
+ * Apply all cached import map updates and merge with bootstrap import map
107
+ * Returns merged import map
108
+ */
109
+ getImportMappingsWithUpdates() {
110
+ // Start with bootstrap import map
111
+ // Cast importMappings from object to ImportMap to access properties
112
+ const bootstrapMappings = this.config.importMappings;
113
+ // Merge with write-once protection: bootstrap mappings take precedence
114
+ const mergedImports = { ...(bootstrapMappings?.imports || {}) };
115
+ for (const [specifier, uri] of Object.entries(this.importMapUpdatesCache)) {
116
+ mergeImportMapEntry(specifier, uri, mergedImports);
117
+ }
118
+ return {
119
+ ...(bootstrapMappings || {}),
120
+ imports: mergedImports,
121
+ };
122
+ }
76
123
  // Called by the customInit hook via lwr.initializeApp()
77
124
  postCustomInit() {
78
125
  this.bootReady = true;
@@ -155,10 +202,12 @@ export default class LoaderShim {
155
202
  }
156
203
  // Set up the application globals, import map, root custom element...
157
204
  mountApp(loader) {
158
- const { bootstrapModule, rootComponent, importMappings, rootComponents, serverData, endpoints } = this.config;
205
+ const { bootstrapModule, rootComponent, rootComponents, serverData, endpoints } = this.config;
206
+ const importMappings = this.getImportMappingsWithUpdates();
159
207
  // Set global LWR.define to loader.define
160
208
  this.global.LWR = Object.freeze({
161
209
  define: loader.define.bind(loader),
210
+ importMap: loader.importMap.bind(loader),
162
211
  rootComponent,
163
212
  rootComponents,
164
213
  serverData: serverData || {},
package/build/types.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { ProfilerAPI, LogDispatcher } from 'lwr/profiler';
4
4
  export type GlobalThis = {
5
5
  LWR: Partial<ClientBootstrapConfig> & {
6
6
  define: LoaderDefine;
7
+ importMap?: LwrImportMapUpdateMethod;
7
8
  };
8
9
  [key: string]: unknown;
9
10
  };
@@ -30,6 +31,8 @@ export interface ResolvedLoader {
30
31
  export type LoaderClass = {
31
32
  new (config?: LoaderConfig): LoaderAPI;
32
33
  };
34
+ export type ImportMapUpdate = Record<string, string[]>;
35
+ export type LwrImportMapUpdateMethod = (update: ImportMapUpdate) => void;
33
36
  export interface BaseLoaderAPI {
34
37
  define: LoaderDefine;
35
38
  load(id: string): Promise<unknown>;
@@ -40,6 +43,7 @@ export interface BaseLoaderAPI {
40
43
  }
41
44
  export interface LoaderAPI extends BaseLoaderAPI {
42
45
  registerImportMappings(mappings?: ImportMap): Promise<void>;
46
+ importMap: LwrImportMapUpdateMethod;
43
47
  }
44
48
  export type LoaderConfig = {
45
49
  baseUrl?: string;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
- "version": "0.22.10",
8
+ "version": "0.22.11",
9
9
  "homepage": "https://developer.salesforce.com/docs/platform/lwr/overview",
10
10
  "repository": {
11
11
  "type": "git",
@@ -61,16 +61,16 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@locker/trusted-types": "0.26.4",
64
- "@lwrjs/diagnostics": "0.22.10",
65
- "@lwrjs/types": "0.22.10",
64
+ "@lwrjs/diagnostics": "0.22.11",
65
+ "@lwrjs/types": "0.22.11",
66
66
  "@rollup/plugin-node-resolve": "^15.2.3",
67
67
  "@rollup/plugin-sucrase": "^5.0.2",
68
68
  "@rollup/plugin-terser": "^0.4.4",
69
69
  "rollup": "^2.80.0"
70
70
  },
71
71
  "dependencies": {
72
- "@lwrjs/client-modules": "0.22.10",
73
- "@lwrjs/shared-utils": "0.22.10"
72
+ "@lwrjs/client-modules": "0.22.11",
73
+ "@lwrjs/shared-utils": "0.22.11"
74
74
  },
75
75
  "lwc": {
76
76
  "modules": [
@@ -90,5 +90,5 @@
90
90
  "volta": {
91
91
  "extends": "../../../package.json"
92
92
  },
93
- "gitHead": "52b77087a10567ad62b48bde04607c627580105d"
93
+ "gitHead": "85710371aa359747e08a064d4b4a2b3dfed1d30a"
94
94
  }