@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.
- package/README.md +33 -1
- package/build/assets/prod/lwr-error-shim.js +1 -1
- package/build/assets/prod/lwr-loader-shim-legacy.bundle.js +530 -25
- package/build/assets/prod/lwr-loader-shim-legacy.bundle.min.js +3 -3
- package/build/assets/prod/lwr-loader-shim-legacy.js +317 -14
- package/build/assets/prod/lwr-loader-shim.bundle.js +123 -12
- package/build/assets/prod/lwr-loader-shim.bundle.min.js +3 -3
- package/build/assets/prod/lwr-loader-shim.js +105 -8
- package/build/cjs/modules/lwr/loader/validateLoadSpecifier.cjs +4 -1
- package/build/cjs/modules/lwr/loaderLegacy/importMap/importMap.cjs +1 -1
- package/build/cjs/modules/lwr/loaderLegacy/importMap/importMapResolver.cjs +12 -0
- package/build/cjs/modules/lwr/loaderLegacy/importMap/utils.cjs +13 -1
- package/build/cjs/modules/lwr/loaderLegacy/utils/validation.cjs +93 -0
- package/build/modules/lwr/esmLoader/esmLoader.js +1 -1
- package/build/modules/lwr/loader/loader.js +18 -4
- package/build/modules/lwr/loader/validateLoadSpecifier.d.ts +2 -1
- package/build/modules/lwr/loader/validateLoadSpecifier.js +11 -2
- package/build/modules/lwr/loaderLegacy/importMap/importMap.js +3 -2
- package/build/modules/lwr/loaderLegacy/importMap/importMapResolver.d.ts +1 -0
- package/build/modules/lwr/loaderLegacy/importMap/importMapResolver.js +13 -0
- package/build/modules/lwr/loaderLegacy/importMap/utils.d.ts +12 -1
- package/build/modules/lwr/loaderLegacy/importMap/utils.js +24 -2
- package/build/modules/lwr/loaderLegacy/loaderLegacy.d.ts +11 -1
- package/build/modules/lwr/loaderLegacy/loaderLegacy.js +213 -11
- package/build/modules/lwr/loaderLegacy/utils/validation.d.ts +50 -0
- package/build/modules/lwr/loaderLegacy/utils/validation.js +114 -0
- package/build/shim/defineCacheResolver.d.ts +10 -0
- package/build/shim/defineCacheResolver.js +78 -0
- package/build/shim/loader.d.ts +5 -1
- package/build/shim/loader.js +14 -3
- package/build/shim/shim.js +3 -2
- package/build/shim-legacy/loaderLegacy.d.ts +7 -2
- package/build/shim-legacy/loaderLegacy.js +15 -4
- package/build/shim-legacy/shimLegacy.d.ts +14 -0
- package/build/shim-legacy/shimLegacy.js +53 -4
- package/build/types.d.ts +22 -0
- 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 v0.22.
|
|
7
|
+
/* LWR Legacy Module Loader v0.22.11 */
|
|
8
8
|
const templateRegex = /\{([0-9]+)\}/g;
|
|
9
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
10
|
function templateString(template, args) {
|
|
@@ -143,7 +143,7 @@ const NO_IMPORT_LOADER = Object.freeze({
|
|
|
143
143
|
level: 0,
|
|
144
144
|
message: 'Cannot dynamically import the LWR loader with importer "{0}"',
|
|
145
145
|
});
|
|
146
|
-
Object.freeze({
|
|
146
|
+
const NO_IMPORT_TRANSPORT = Object.freeze({
|
|
147
147
|
code: 3025,
|
|
148
148
|
level: 0,
|
|
149
149
|
message: 'Cannot dynamically import "transport" with importer "{0}"',
|
|
@@ -1399,9 +1399,10 @@ function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else {
|
|
|
1399
1399
|
|
|
1400
1400
|
|
|
1401
1401
|
|
|
1402
|
+
|
|
1402
1403
|
/**
|
|
1403
1404
|
* Validates that the given module id is not one of the forbidden dynamic import specifiers.
|
|
1404
|
-
* Throws LoaderError for: lwc, the LWR loader, and blob URLs.
|
|
1405
|
+
* Throws LoaderError for: lwc, the LWR loader, transport/webruntime/transport, and blob URLs.
|
|
1405
1406
|
*
|
|
1406
1407
|
* @param id - Module identifier or URL
|
|
1407
1408
|
* @param importer - Versioned specifier of the module importer (for error reporting)
|
|
@@ -1414,7 +1415,7 @@ function validateLoadSpecifier(
|
|
|
1414
1415
|
loaderSpecifier,
|
|
1415
1416
|
errors,
|
|
1416
1417
|
) {
|
|
1417
|
-
const { LoaderError, NO_IMPORT_LWC, NO_IMPORT_LOADER, NO_BLOB_IMPORT } = errors;
|
|
1418
|
+
const { LoaderError, NO_IMPORT_LWC, NO_IMPORT_LOADER, NO_IMPORT_TRANSPORT, NO_BLOB_IMPORT } = errors;
|
|
1418
1419
|
|
|
1419
1420
|
// Throw an error if the specifier is "lwc" or a versioned lwc specifier
|
|
1420
1421
|
// Dynamic import of LWC APIs is not allowed
|
|
@@ -1430,6 +1431,18 @@ function validateLoadSpecifier(
|
|
|
1430
1431
|
throw new LoaderError(NO_IMPORT_LOADER, [_nullishCoalesce(importer, () => ( 'unknown'))]);
|
|
1431
1432
|
}
|
|
1432
1433
|
|
|
1434
|
+
// Throw an error if the specifier is "transport" or "webruntime/transport" (or versioned)
|
|
1435
|
+
// Dynamic import of transport exposes unsandboxed fetch, bypassing LWS
|
|
1436
|
+
// Reference: Hackforce report HF-3393
|
|
1437
|
+
if (
|
|
1438
|
+
id === 'transport' ||
|
|
1439
|
+
id.startsWith('transport/v/') ||
|
|
1440
|
+
id === 'webruntime/transport' ||
|
|
1441
|
+
id.startsWith('webruntime/transport/v/')
|
|
1442
|
+
) {
|
|
1443
|
+
throw new LoaderError(NO_IMPORT_TRANSPORT, [_nullishCoalesce(importer, () => ( 'unknown'))]);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1433
1446
|
// Throw an error if the specifier is a blob URL (case-insensitive check)
|
|
1434
1447
|
if (id.toLowerCase().startsWith('blob:')) {
|
|
1435
1448
|
throw new LoaderError(NO_BLOB_IMPORT);
|
|
@@ -1448,7 +1461,8 @@ function getMatch(path, matchObj) {
|
|
|
1448
1461
|
if (segment in matchObj) {
|
|
1449
1462
|
return segment;
|
|
1450
1463
|
}
|
|
1451
|
-
|
|
1464
|
+
sepIndex = path.lastIndexOf('/', sepIndex - 1);
|
|
1465
|
+
} while (sepIndex > 0);
|
|
1452
1466
|
}
|
|
1453
1467
|
function targetWarning(match, target, msg) {
|
|
1454
1468
|
if (hasConsole) {
|
|
@@ -1457,6 +1471,27 @@ function targetWarning(match, target, msg) {
|
|
|
1457
1471
|
}
|
|
1458
1472
|
}
|
|
1459
1473
|
|
|
1474
|
+
/**
|
|
1475
|
+
* Import map entries are write-once: once a specifier is mapped, it cannot be overridden.
|
|
1476
|
+
* This ensures deterministic module resolution and prevents runtime mutation of previously
|
|
1477
|
+
* resolved dependencies. Conflicting mappings are ignored with a warning.
|
|
1478
|
+
*
|
|
1479
|
+
* Merges a single import map entry into a target imports object.
|
|
1480
|
+
* - If the specifier doesn't exist, it is added.
|
|
1481
|
+
* - If it exists with the same value, it is silently skipped.
|
|
1482
|
+
* - If it exists with a different value, a warning is logged and the entry is skipped.
|
|
1483
|
+
*/
|
|
1484
|
+
function mergeImportMapEntry(specifier, uri, target) {
|
|
1485
|
+
const existing = target[specifier];
|
|
1486
|
+
if (existing !== undefined) {
|
|
1487
|
+
if (existing !== uri) {
|
|
1488
|
+
targetWarning(specifier, uri, `already mapped to "${existing}", ignoring conflicting mapping`);
|
|
1489
|
+
}
|
|
1490
|
+
} else {
|
|
1491
|
+
target[specifier] = uri;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1460
1495
|
/**
|
|
1461
1496
|
* Import map support for LWR based on the spec: https://github.com/WICG/import-maps
|
|
1462
1497
|
*
|
|
@@ -1578,7 +1613,8 @@ function resolveAndComposePackages(
|
|
|
1578
1613
|
if (!mapped) {
|
|
1579
1614
|
targetWarning(p, rhs, 'bare specifier did not resolve');
|
|
1580
1615
|
} else {
|
|
1581
|
-
|
|
1616
|
+
// Merge into cache with write-once protection
|
|
1617
|
+
mergeImportMapEntry(resolvedLhs, mapped.uri, outPackages);
|
|
1582
1618
|
}
|
|
1583
1619
|
}
|
|
1584
1620
|
}
|
|
@@ -1629,6 +1665,20 @@ class ImportMapResolver {
|
|
|
1629
1665
|
resolve(resolvedOrPlain, parentUrl) {
|
|
1630
1666
|
return resolveImportMapEntry(this.importMap, resolvedOrPlain, parentUrl);
|
|
1631
1667
|
}
|
|
1668
|
+
|
|
1669
|
+
addImportMapEntries(importMap) {
|
|
1670
|
+
if (importMap.imports) {
|
|
1671
|
+
const current = this.importMap;
|
|
1672
|
+
if (!current.imports) {
|
|
1673
|
+
current.imports = {};
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// Merge into cache with write-once protection
|
|
1677
|
+
for (const specifier in importMap.imports) {
|
|
1678
|
+
mergeImportMapEntry(specifier, importMap.imports[specifier], current.imports);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1632
1682
|
}
|
|
1633
1683
|
|
|
1634
1684
|
/**
|
|
@@ -1700,15 +1750,134 @@ async function evaluateImportMaps(baseUrl) {
|
|
|
1700
1750
|
return importMapPromise;
|
|
1701
1751
|
}
|
|
1702
1752
|
|
|
1703
|
-
|
|
1753
|
+
/**
|
|
1754
|
+
* List of protected module patterns that cannot be remapped.
|
|
1755
|
+
* Uses case-insensitive matching to prevent bypass attempts (e.g., HF-1027).
|
|
1756
|
+
*/
|
|
1757
|
+
const PROTECTED_PATTERNS = [
|
|
1758
|
+
/^lwc$/i, // Core LWC
|
|
1759
|
+
/^lwc\/v\//i, // Versioned LWC variants
|
|
1760
|
+
/^@lwc\//i, // LWC scoped packages
|
|
1761
|
+
/^lightningmobileruntime\//i, // Lightning mobile runtime
|
|
1762
|
+
];
|
|
1763
|
+
|
|
1764
|
+
/**
|
|
1765
|
+
* Checks if a specifier matches any protected module pattern.
|
|
1766
|
+
*
|
|
1767
|
+
* @param specifier - The module specifier to check
|
|
1768
|
+
* @returns true if the specifier is protected, false otherwise
|
|
1769
|
+
*/
|
|
1770
|
+
function isProtectedSpecifier(specifier) {
|
|
1771
|
+
return PROTECTED_PATTERNS.some((pattern) => pattern.test(specifier));
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* Validates that a URI does not use dangerous URL schemes.
|
|
1776
|
+
*
|
|
1777
|
+
* Blocks:
|
|
1778
|
+
* - blob: URLs (HF-1027 bypass prevention) - case-insensitive
|
|
1779
|
+
* - data: URLs (inline code injection) - case-insensitive
|
|
1780
|
+
*
|
|
1781
|
+
* @param uri - The URI to validate
|
|
1782
|
+
* @param specifier - The specifier being mapped (for error messages)
|
|
1783
|
+
* @throws Error if the URI uses a dangerous scheme
|
|
1784
|
+
*/
|
|
1785
|
+
function validateMappingUri(uri, specifier) {
|
|
1786
|
+
const lowerUri = uri.toLowerCase();
|
|
1787
|
+
|
|
1788
|
+
if (lowerUri.startsWith('blob:')) {
|
|
1789
|
+
throw new Error(`Cannot map ${specifier} to blob: URL`);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
if (lowerUri.startsWith('data:')) {
|
|
1793
|
+
throw new Error(`Cannot map ${specifier} to data: URL`);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/**
|
|
1798
|
+
* Validates that a specifier is well-formed and not protected.
|
|
1799
|
+
*
|
|
1800
|
+
* @param specifier - The module specifier to validate
|
|
1801
|
+
* @throws Error if the specifier is invalid or protected
|
|
1802
|
+
*/
|
|
1803
|
+
function validateSpecifier(specifier) {
|
|
1804
|
+
if (typeof specifier !== 'string' || specifier.length === 0) {
|
|
1805
|
+
throw new Error('Specifier must be a non-empty string');
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
if (isProtectedSpecifier(specifier)) {
|
|
1809
|
+
throw new Error(`Cannot remap protected module: ${specifier}`);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Validates and converts an ImportMapUpdate to ImportMap format.
|
|
1815
|
+
*
|
|
1816
|
+
* Performs validation checks and then converts from:
|
|
1817
|
+
* { moduleScriptURL: [moduleName1, moduleName2] }
|
|
1818
|
+
* To:
|
|
1819
|
+
* { imports: { moduleName1: moduleScriptURL, moduleName2: moduleScriptURL } }
|
|
1820
|
+
*
|
|
1821
|
+
* @param update - The ImportMapUpdate object to validate and convert
|
|
1822
|
+
* @returns The converted ImportMap, or null if the update is empty
|
|
1823
|
+
* @throws Error if any validation fails
|
|
1824
|
+
*/
|
|
1825
|
+
function validateAndConvertImportMapUpdate(update) {
|
|
1826
|
+
if (!update || typeof update !== 'object') {
|
|
1827
|
+
throw new Error('LWR.importMap() requires an object argument');
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Check if update is empty
|
|
1831
|
+
const entries = Object.entries(update);
|
|
1832
|
+
if (entries.length === 0) {
|
|
1833
|
+
if (hasConsole) {
|
|
1834
|
+
// eslint-disable-next-line lwr/no-unguarded-apis
|
|
1835
|
+
console.warn('LWR.importMap() called with empty update object');
|
|
1836
|
+
}
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1704
1839
|
|
|
1840
|
+
const convertedImports = {};
|
|
1705
1841
|
|
|
1842
|
+
for (const [moduleScriptURL, moduleNames] of entries) {
|
|
1843
|
+
if (!Array.isArray(moduleNames)) {
|
|
1844
|
+
throw new Error('moduleNames must be an array');
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
if (!moduleScriptURL || typeof moduleScriptURL !== 'string') {
|
|
1848
|
+
throw new Error('moduleScriptURL must be a string');
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
validateMappingUri(moduleScriptURL, moduleScriptURL);
|
|
1852
|
+
|
|
1853
|
+
for (const moduleName of moduleNames) {
|
|
1854
|
+
validateSpecifier(moduleName);
|
|
1855
|
+
if (moduleName in convertedImports) {
|
|
1856
|
+
if (hasConsole) {
|
|
1857
|
+
// eslint-disable-next-line lwr/no-unguarded-apis
|
|
1858
|
+
console.warn(
|
|
1859
|
+
`LWR.importMap(): duplicate module "${moduleName}" — already mapped to "${convertedImports[moduleName]}", ignoring mapping to "${moduleScriptURL}"`,
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
} else {
|
|
1863
|
+
convertedImports[moduleName] = moduleScriptURL;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
return {
|
|
1869
|
+
imports: convertedImports,
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
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; }
|
|
1706
1874
|
/**
|
|
1707
1875
|
* The LWR loader is inspired and borrows from the algorithms and native browser principles of https://github.com/systemjs/systemjs
|
|
1708
1876
|
*/
|
|
1709
1877
|
class Loader {
|
|
1710
1878
|
|
|
1711
1879
|
|
|
1880
|
+
|
|
1712
1881
|
|
|
1713
1882
|
|
|
1714
1883
|
constructor(config) {
|
|
@@ -1794,6 +1963,7 @@ class Loader {
|
|
|
1794
1963
|
LoaderError,
|
|
1795
1964
|
NO_IMPORT_LWC,
|
|
1796
1965
|
NO_IMPORT_LOADER,
|
|
1966
|
+
NO_IMPORT_TRANSPORT,
|
|
1797
1967
|
NO_BLOB_IMPORT,
|
|
1798
1968
|
});
|
|
1799
1969
|
return this.registry.load(id, importer);
|
|
@@ -1836,10 +2006,13 @@ class Loader {
|
|
|
1836
2006
|
// import maps spec if we do this after resolving any imports
|
|
1837
2007
|
importMap = resolveAndComposeImportMap(mappings, this.baseUrl, this.parentImportMap);
|
|
1838
2008
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2009
|
+
if (importMap) {
|
|
2010
|
+
if (this.importMapResolver || this.parentImportMap) {
|
|
2011
|
+
throw new LoaderError(BAD_IMPORT_MAP);
|
|
2012
|
+
}
|
|
2013
|
+
this.importMapResolver = new ImportMapResolver(importMap);
|
|
2014
|
+
this.registry.setImportResolver(this.importMapResolver);
|
|
2015
|
+
this.parentImportMap = this.importMapResolver.importMap;
|
|
1843
2016
|
}
|
|
1844
2017
|
}
|
|
1845
2018
|
|
|
@@ -1852,6 +2025,35 @@ class Loader {
|
|
|
1852
2025
|
this.registry.registerExternalModules(modules);
|
|
1853
2026
|
}
|
|
1854
2027
|
|
|
2028
|
+
/**
|
|
2029
|
+
* Apply import map updates at runtime.
|
|
2030
|
+
* Enables adding new import mappings dynamically.
|
|
2031
|
+
*
|
|
2032
|
+
* @param updates - Import Map Update object containing:
|
|
2033
|
+
- [moduleScriptURL] - Script URL containing LWR define statements
|
|
2034
|
+
- string[] - array of module names which are included in the given script
|
|
2035
|
+
*/
|
|
2036
|
+
importMap(update) {
|
|
2037
|
+
// Validate and convert the import map update to the ImportMap format (moduleName -> moduleScriptURL)
|
|
2038
|
+
const importMap = validateAndConvertImportMapUpdate(update);
|
|
2039
|
+
|
|
2040
|
+
// Early return if update was empty
|
|
2041
|
+
if (!importMap) {
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
if (!this.parentImportMap || !this.importMapResolver) {
|
|
2046
|
+
throw new LoaderError(BAD_IMPORT_MAP);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// Merge the new mappings with the base import map - note this goes against
|
|
2050
|
+
// import maps spec if we do this after resolving any imports
|
|
2051
|
+
const resolvedImportMap = resolveAndComposeImportMap(importMap, this.baseUrl, this.parentImportMap);
|
|
2052
|
+
|
|
2053
|
+
this.importMapResolver.addImportMapEntries(resolvedImportMap);
|
|
2054
|
+
this.parentImportMap = this.importMapResolver.importMap;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
1855
2057
|
getModuleWarnings(isAppMounted = false) {
|
|
1856
2058
|
return this.registry.getModuleWarnings(isAppMounted);
|
|
1857
2059
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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 type { ImportMapUpdate } from '../../../../types.js';
|
|
10
|
+
import type { ImportMap } from '../importMap/importMap.js';
|
|
11
|
+
/**
|
|
12
|
+
* Checks if a specifier matches any protected module pattern.
|
|
13
|
+
*
|
|
14
|
+
* @param specifier - The module specifier to check
|
|
15
|
+
* @returns true if the specifier is protected, false otherwise
|
|
16
|
+
*/
|
|
17
|
+
export declare function isProtectedSpecifier(specifier: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Validates that a URI does not use dangerous URL schemes.
|
|
20
|
+
*
|
|
21
|
+
* Blocks:
|
|
22
|
+
* - blob: URLs (HF-1027 bypass prevention) - case-insensitive
|
|
23
|
+
* - data: URLs (inline code injection) - case-insensitive
|
|
24
|
+
*
|
|
25
|
+
* @param uri - The URI to validate
|
|
26
|
+
* @param specifier - The specifier being mapped (for error messages)
|
|
27
|
+
* @throws Error if the URI uses a dangerous scheme
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateMappingUri(uri: string, specifier: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Validates that a specifier is well-formed and not protected.
|
|
32
|
+
*
|
|
33
|
+
* @param specifier - The module specifier to validate
|
|
34
|
+
* @throws Error if the specifier is invalid or protected
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateSpecifier(specifier: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Validates and converts an ImportMapUpdate to ImportMap format.
|
|
39
|
+
*
|
|
40
|
+
* Performs validation checks and then converts from:
|
|
41
|
+
* { moduleScriptURL: [moduleName1, moduleName2] }
|
|
42
|
+
* To:
|
|
43
|
+
* { imports: { moduleName1: moduleScriptURL, moduleName2: moduleScriptURL } }
|
|
44
|
+
*
|
|
45
|
+
* @param update - The ImportMapUpdate object to validate and convert
|
|
46
|
+
* @returns The converted ImportMap, or null if the update is empty
|
|
47
|
+
* @throws Error if any validation fails
|
|
48
|
+
*/
|
|
49
|
+
export declare function validateAndConvertImportMapUpdate(update: ImportMapUpdate): ImportMap | null;
|
|
50
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -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
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DefineArguments, DefineArgsInput, ParseDefineResult, ResolvedLoader } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse AMD define args. Supports define(id, deps, factory).
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseDefine(def: DefineArgsInput): ParseDefineResult;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the loader's dependencies from defineCache. Throws if definition is invalid or deps cannot be resolved.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveLoaderDepsFromDefineCache(loaderSpecifier: string, definition: DefineArguments, defineCache: Record<string, DefineArguments>): ResolvedLoader;
|
|
10
|
+
//# sourceMappingURL=defineCacheResolver.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse AMD define args. Supports define(id, deps, factory).
|
|
3
|
+
*/
|
|
4
|
+
export function parseDefine(def) {
|
|
5
|
+
const [, depsOrFactory, factory] = def;
|
|
6
|
+
if (Array.isArray(depsOrFactory) && typeof factory === 'function') {
|
|
7
|
+
return { deps: depsOrFactory, factory };
|
|
8
|
+
}
|
|
9
|
+
if (typeof depsOrFactory === 'function') {
|
|
10
|
+
return { deps: [], factory: depsOrFactory };
|
|
11
|
+
}
|
|
12
|
+
throw new Error('Invalid module definition');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a loader dependency: 'exports' returns the bag; otherwise look up in cache and instantiate.
|
|
16
|
+
* Loader deps are assumed to be leaf modules (no deps of their own)—no recursive resolution.
|
|
17
|
+
*/
|
|
18
|
+
function resolveDep(dep, exportsObj, cache) {
|
|
19
|
+
if (dep === 'exports')
|
|
20
|
+
return exportsObj;
|
|
21
|
+
const mod = cache[dep];
|
|
22
|
+
if (!mod) {
|
|
23
|
+
throw new Error(`Dependency "${dep}" not found in defineCache for loader`);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return instantiateLeafModule(mod);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
30
|
+
throw new Error(`Loader dependency "${dep}" has invalid definition: ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Instantiate a leaf module (loader dep). Assumes the module has no dependencies of its own.
|
|
35
|
+
* Supports only deps: [] or ['exports']; any other dep list throws.
|
|
36
|
+
*/
|
|
37
|
+
function instantiateLeafModule(def) {
|
|
38
|
+
const { deps, factory } = parseDefine(def);
|
|
39
|
+
if (deps.length > 1 || (deps.length === 1 && deps[0] !== 'exports')) {
|
|
40
|
+
throw new Error(`Loader dependencies must have no deps or only ['exports']; got [${deps.join(', ')}]`);
|
|
41
|
+
}
|
|
42
|
+
const exports = {};
|
|
43
|
+
const out = factory(exports);
|
|
44
|
+
return out !== undefined ? out : exports;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Normalize loader definition to canonical (exports, ...deps). Injects 'exports' if missing.
|
|
48
|
+
*/
|
|
49
|
+
function normalizeLoaderDefinition(def) {
|
|
50
|
+
const { deps, factory } = parseDefine(def);
|
|
51
|
+
if (deps.includes('exports')) {
|
|
52
|
+
return { deps, factory };
|
|
53
|
+
}
|
|
54
|
+
const isEmptyDeps = deps.length === 0;
|
|
55
|
+
const normalizedFactory = (exports, ...rest) => {
|
|
56
|
+
const result = isEmptyDeps ? factory(exports) : factory(...rest);
|
|
57
|
+
if (result !== undefined && typeof result === 'object') {
|
|
58
|
+
Object.assign(exports, result);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return { deps: ['exports', ...deps], factory: normalizedFactory };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the loader's dependencies from defineCache. Throws if definition is invalid or deps cannot be resolved.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveLoaderDepsFromDefineCache(loaderSpecifier, definition, defineCache) {
|
|
67
|
+
try {
|
|
68
|
+
const { deps, factory } = normalizeLoaderDefinition(definition);
|
|
69
|
+
const exportsObj = {};
|
|
70
|
+
const args = deps.map((dep) => resolveDep(dep, exportsObj, defineCache));
|
|
71
|
+
return { factory: factory, args, exportsObj };
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
75
|
+
throw new Error(`Expected loader with specifier "${loaderSpecifier}" to be a module. ${msg}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=defineCacheResolver.js.map
|
package/build/shim/loader.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { DefineArguments, FingerprintsLoaderAPI as LoaderAPI, FingerprintsLoaderConfig as LoaderConfig } from '../types.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Create a loader from a definition. Original API preserved.
|
|
4
|
+
* Optional defineCache for resolving loader deps (when loader has dependencies).
|
|
5
|
+
*/
|
|
6
|
+
export declare function createLoader(name: string, definition: DefineArguments, config: LoaderConfig, externalModules?: string[], defineCache?: Record<string, DefineArguments>): LoaderAPI;
|
|
3
7
|
//# sourceMappingURL=loader.d.ts.map
|
package/build/shim/loader.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import { resolveLoaderDepsFromDefineCache } from './defineCacheResolver.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a loader from a definition. Original API preserved.
|
|
4
|
+
* Optional defineCache for resolving loader deps (when loader has dependencies).
|
|
5
|
+
*/
|
|
6
|
+
export function createLoader(name, definition, config, externalModules, defineCache) {
|
|
2
7
|
if (!definition || typeof definition[2] !== 'function') {
|
|
3
8
|
throw new Error(`Expected loader with specifier "${name}" to be a module`);
|
|
4
9
|
}
|
|
5
|
-
// Create a Loader instance
|
|
6
10
|
const exports = {};
|
|
7
|
-
|
|
11
|
+
if (defineCache) {
|
|
12
|
+
const { factory, args, exportsObj } = resolveLoaderDepsFromDefineCache(name, definition, defineCache);
|
|
13
|
+
factory(...args);
|
|
14
|
+
Object.assign(exports, exportsObj);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
definition[2].call(null, exports);
|
|
18
|
+
}
|
|
8
19
|
const { Loader } = exports;
|
|
9
20
|
if (!Loader) {
|
|
10
21
|
throw new Error('Expected Loader class to be defined');
|
package/build/shim/shim.js
CHANGED
|
@@ -112,7 +112,7 @@ export default class LoaderShim {
|
|
|
112
112
|
rootComponents: this.config.rootComponents,
|
|
113
113
|
},
|
|
114
114
|
};
|
|
115
|
-
const loader = createLoader(this.loaderSpecifier, this.defineCache[this.loaderSpecifier], loaderConfig, this.config.preloadModules);
|
|
115
|
+
const loader = createLoader(this.loaderSpecifier, this.defineCache[this.loaderSpecifier], loaderConfig, this.config.preloadModules, this.defineCache);
|
|
116
116
|
this.mountApp(loader);
|
|
117
117
|
if (loader &&
|
|
118
118
|
typeof loader.getModuleWarnings === 'function' &&
|
|
@@ -134,6 +134,7 @@ export default class LoaderShim {
|
|
|
134
134
|
resolve();
|
|
135
135
|
}
|
|
136
136
|
else {
|
|
137
|
+
/* istanbul ignore next */
|
|
137
138
|
const observer = new MutationObserver(() => {
|
|
138
139
|
// eslint-disable-next-line lwr/no-unguarded-apis
|
|
139
140
|
if (document.body) {
|
|
@@ -198,7 +199,7 @@ export default class LoaderShim {
|
|
|
198
199
|
])
|
|
199
200
|
.then(() => {
|
|
200
201
|
// eslint-disable-next-line lwr/no-unguarded-apis
|
|
201
|
-
if (typeof window === 'undefined' || typeof document === undefined) {
|
|
202
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
202
203
|
return Promise.resolve();
|
|
203
204
|
}
|
|
204
205
|
if (initDeferDOM) {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
import type { LoaderConfig, DefineArguments
|
|
2
|
-
|
|
1
|
+
import type { LoaderAPI, LoaderConfig, DefineArguments } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a loader from a definition. Original API preserved.
|
|
4
|
+
* Optional defineCache for resolving loader deps.
|
|
5
|
+
* definition[3] (signatures: ownHash, hashes) is passed to loader.define for cache invalidation.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createLoader(name: string, definition: DefineArguments, config: LoaderConfig, externalModules?: string[], defineCache?: Record<string, DefineArguments>): LoaderAPI;
|
|
3
8
|
//# sourceMappingURL=loaderLegacy.d.ts.map
|
|
@@ -1,16 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import { resolveLoaderDepsFromDefineCache } from '../shim/defineCacheResolver.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a loader from a definition. Original API preserved.
|
|
4
|
+
* Optional defineCache for resolving loader deps.
|
|
5
|
+
* definition[3] (signatures: ownHash, hashes) is passed to loader.define for cache invalidation.
|
|
6
|
+
*/
|
|
7
|
+
export function createLoader(name, definition, config, externalModules, defineCache) {
|
|
2
8
|
if (!definition || typeof definition[2] !== 'function') {
|
|
3
9
|
throw new Error(`Expected loader with specifier "${name}" to be a module`);
|
|
4
10
|
}
|
|
5
|
-
// Create a Loader instance
|
|
6
11
|
const exports = {};
|
|
7
|
-
|
|
12
|
+
if (defineCache) {
|
|
13
|
+
const { factory, args, exportsObj } = resolveLoaderDepsFromDefineCache(name, definition, defineCache);
|
|
14
|
+
factory(...args);
|
|
15
|
+
Object.assign(exports, exportsObj);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
definition[2].call(null, exports);
|
|
19
|
+
}
|
|
8
20
|
const { Loader } = exports;
|
|
9
21
|
if (!Loader) {
|
|
10
22
|
throw new Error('Expected Loader class to be defined');
|
|
11
23
|
}
|
|
12
24
|
const loader = new Loader(config);
|
|
13
|
-
// register externally loaded modules
|
|
14
25
|
if (externalModules && externalModules.length) {
|
|
15
26
|
loader.registerExternalModules(externalModules);
|
|
16
27
|
}
|