@lwc/module-resolver
Version:
Resolves paths for LWC components
356 lines (347 loc) • 14.1 kB
JavaScript
/**
* Copyright (c) 2025 Salesforce, Inc.
*/
;
Object.defineProperty(exports, '__esModule', { value: true });
var fs = require('fs');
var path = require('path');
var resolve = require('resolve');
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
class NoLwcModuleFound extends Error {
constructor(importee, importer) {
super(`Unable to resolve "${importee}" from "${importer}"`);
this.code = 'NO_LWC_MODULE_FOUND';
}
}
class LwcConfigError extends Error {
constructor(message, { scope }) {
super(`Invalid LWC configuration in "${scope}". ${message}`);
this.code = 'LWC_CONFIG_ERROR';
this.scope = scope;
}
}
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
/**
* Determines whether the given value is an object or null.
* @param obj The value to check
* @returns true if the value is an object or null
* @example isObject(null) // true
*/
function isObject(obj) {
return typeof obj === 'object';
}
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
const PACKAGE_JSON = 'package.json';
const LWC_CONFIG_FILE = 'lwc.config.json';
function isNpmModuleRecord(moduleRecord) {
return 'npm' in moduleRecord;
}
function isDirModuleRecord(moduleRecord) {
return 'dir' in moduleRecord;
}
function isAliasModuleRecord(moduleRecord) {
return 'name' in moduleRecord && 'path' in moduleRecord;
}
function getEntry(moduleDir, moduleName, ext) {
return path.join(moduleDir, `${moduleName}.${ext}`);
}
function getModuleEntry(moduleDir, moduleName, opts) {
const entryJS = getEntry(moduleDir, moduleName, 'js');
const entryTS = getEntry(moduleDir, moduleName, 'ts');
const entryHTML = getEntry(moduleDir, moduleName, 'html');
const entryCSS = getEntry(moduleDir, moduleName, 'css');
// Order is important
if (fs.existsSync(entryJS)) {
return entryJS;
}
else if (fs.existsSync(entryTS)) {
return entryTS;
}
else if (fs.existsSync(entryHTML)) {
return entryHTML;
}
else if (fs.existsSync(entryCSS)) {
return entryCSS;
}
throw new LwcConfigError(`Unable to find a valid entry point for "${moduleDir}/${moduleName}"`, { scope: opts.rootDir });
}
function normalizeConfig(config, scope) {
const rootDir = config.rootDir ? path.resolve(config.rootDir) : process.cwd();
const modules = config.modules || [];
const normalizedModules = modules.map((m) => {
if (!isObject(m)) {
throw new LwcConfigError(`Invalid module record. Module record must be an object, instead got ${JSON.stringify(m)}.`, { scope });
}
return isDirModuleRecord(m) ? { ...m, dir: path.resolve(rootDir, m.dir) } : m;
});
return {
modules: normalizedModules,
rootDir,
};
}
function normalizeDirName(dirName) {
return dirName.endsWith('/') ? dirName : `${dirName}/`;
}
// User defined modules will have precedence over the ones defined elsewhere (ex. npm)
function mergeModules(userModules, configModules = []) {
const visitedAlias = new Set();
const visitedDirs = new Set();
const visitedNpm = new Set();
const modules = userModules.slice();
// Visit the user modules to created an index with the name as keys
userModules.forEach((m) => {
if (isAliasModuleRecord(m)) {
visitedAlias.add(m.name);
}
else if (isDirModuleRecord(m)) {
visitedDirs.add(normalizeDirName(m.dir));
}
else if (isNpmModuleRecord(m)) {
visitedNpm.add(m.npm);
}
});
configModules.forEach((m) => {
if ((isAliasModuleRecord(m) && !visitedAlias.has(m.name)) ||
(isDirModuleRecord(m) && !visitedDirs.has(normalizeDirName(m.dir))) ||
(isNpmModuleRecord(m) && !visitedNpm.has(m.npm))) {
modules.push(m);
}
});
return modules;
}
function findFirstUpwardConfigPath(dirname) {
const parts = dirname.split(path.sep);
while (parts.length > 1) {
const upwardsPath = parts.join(path.sep);
const pkgJsonPath = path.join(upwardsPath, PACKAGE_JSON);
const configJsonPath = path.join(upwardsPath, LWC_CONFIG_FILE);
const dirHasPkgJson = fs.existsSync(pkgJsonPath);
const dirHasLwcConfig = fs.existsSync(configJsonPath);
if (dirHasLwcConfig && !dirHasPkgJson) {
throw new LwcConfigError(`"lwc.config.json" must be at the package root level along with the "package.json"`, { scope: upwardsPath });
}
if (dirHasPkgJson) {
return path.dirname(pkgJsonPath);
}
parts.pop();
}
throw new LwcConfigError(`Unable to find any LWC configuration file`, { scope: dirname });
}
function validateNpmConfig(config, opts) {
if (!config.modules) {
throw new LwcConfigError('Missing "modules" property for a npm config', {
scope: opts.rootDir,
});
}
if (!config.expose) {
throw new LwcConfigError('Missing "expose" attribute: An imported npm package must explicitly define all the modules that it contains', { scope: opts.rootDir });
}
}
function validateNpmAlias(exposed, map, opts) {
Object.keys(map).forEach((specifier) => {
if (!exposed.includes(specifier)) {
throw new LwcConfigError(`Unable to apply mapping: The specifier "${specifier}" is not exposed by the npm module`, { scope: opts.rootDir });
}
});
}
function getLwcConfig(dirname) {
const packageJsonPath = path.resolve(dirname, PACKAGE_JSON);
const lwcConfigPath = path.resolve(dirname, LWC_CONFIG_FILE);
if (fs.existsSync(lwcConfigPath)) {
// Using require() to read JSON, rather than load a module
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require(lwcConfigPath);
}
else {
// Using require() to read JSON, rather than load a module
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require(packageJsonPath).lwc ?? {};
}
}
function createRegistryEntry(entry, specifier, type, opts) {
return {
entry,
specifier,
type,
scope: opts.rootDir,
};
}
function remapList(exposed, map) {
return exposed.reduce((renamed, item) => {
renamed.push(map[item] || item);
return renamed;
}, []);
}
function transposeObject(map) {
return Object.entries(map).reduce((r, [key, value]) => ((r[value] = key), r), {});
}
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
exports.RegistryType = void 0;
(function (RegistryType) {
RegistryType["alias"] = "alias";
RegistryType["dir"] = "dir";
})(exports.RegistryType || (exports.RegistryType = {}));
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
function resolveModuleFromAlias(specifier, moduleRecord, opts) {
const { name, path: modulePath } = moduleRecord;
if (specifier !== name) {
return;
}
const entry = path.resolve(opts.rootDir, modulePath);
if (!fs.existsSync(entry)) {
throw new LwcConfigError(`Invalid alias module record "${JSON.stringify(moduleRecord)}", file "${entry}" does not exist`, { scope: opts.rootDir });
}
return createRegistryEntry(entry, specifier, exports.RegistryType.alias, opts);
}
function resolveModuleFromDir(specifier, moduleRecord, opts) {
const { dir } = moduleRecord;
const absModuleDir = path.isAbsolute(dir) ? dir : path.join(opts.rootDir, dir);
if (!fs.existsSync(absModuleDir)) {
throw new LwcConfigError(`Invalid dir module record "${JSON.stringify(moduleRecord)}", directory ${absModuleDir} doesn't exists`, { scope: opts.rootDir });
}
// A module dir record can only resolve module specifier with the following form "[ns]/[name]".
// We can early exit if the required specifier doesn't match.
const parts = specifier.split('/');
if (parts.length !== 2) {
return;
}
const [ns, name] = parts;
const moduleDir = path.join(absModuleDir, ns, name);
// Exit if the expected module directory doesn't exists.
if (!fs.existsSync(moduleDir)) {
return;
}
const entry = getModuleEntry(moduleDir, name, opts);
return createRegistryEntry(entry, specifier, exports.RegistryType.dir, opts);
}
function resolveModuleFromNpm(specifier, npmModuleRecord, opts) {
const { npm, map: aliasMapping } = npmModuleRecord;
let pkgJsonPath;
try {
pkgJsonPath = resolve.sync(`${npm}/package.json`, {
basedir: opts.rootDir,
preserveSymlinks: true,
});
}
catch (error) {
// If the module "package.json" can't be found, throw an an invalid config error. Otherwise
// rethrow the original error.
if (error.code === 'MODULE_NOT_FOUND') {
throw new LwcConfigError(`Invalid npm module record "${JSON.stringify(npmModuleRecord)}", "${npm}" npm module can't be resolved`, { scope: opts.rootDir });
}
throw error;
}
const packageDir = path.dirname(pkgJsonPath);
const lwcConfig = getLwcConfig(packageDir);
validateNpmConfig(lwcConfig, { rootDir: packageDir });
let exposedModules = lwcConfig.expose;
let reverseMapping;
if (aliasMapping) {
validateNpmAlias(lwcConfig.expose, aliasMapping, { rootDir: packageDir });
exposedModules = remapList(lwcConfig.expose, aliasMapping);
reverseMapping = transposeObject(aliasMapping);
}
if (exposedModules.includes(specifier)) {
for (const moduleRecord of lwcConfig.modules) {
const aliasedSpecifier = reverseMapping && reverseMapping[specifier];
const registryEntry = resolveModuleRecordType(aliasedSpecifier || specifier, moduleRecord, {
rootDir: packageDir,
});
if (registryEntry) {
if (aliasedSpecifier) {
registryEntry.specifier = specifier;
registryEntry.type = exports.RegistryType.alias;
}
return registryEntry;
}
}
throw new LwcConfigError(`Unable to find "${specifier}" under npm package "${npmModuleRecord.npm}"`, { scope: packageDir });
}
}
function resolveModuleRecordType(specifier, moduleRecord, opts) {
const { rootDir } = opts;
if (isAliasModuleRecord(moduleRecord)) {
return resolveModuleFromAlias(specifier, moduleRecord, { rootDir });
}
else if (isDirModuleRecord(moduleRecord)) {
return resolveModuleFromDir(specifier, moduleRecord, { rootDir });
}
else if (isNpmModuleRecord(moduleRecord)) {
return resolveModuleFromNpm(specifier, moduleRecord, opts);
}
throw new LwcConfigError(`Unknown module record "${JSON.stringify(moduleRecord)}"`, {
scope: rootDir,
});
}
/**
* Resolves LWC modules using a custom resolution algorithm, using the configuration from your
* project's `lwc.config.json` or the `"lwc"` key in the `package.json`. The resolver iterates
* through the modules provided in the config and returns the first module that matches the
* requested module specifier. There are three types of module record:
* - Alias module record: A file path where an LWC module can be resolved.
* - Directory module record: A folder path where LWC modules can be resolved.
* - NPM package module record: An NPM package that exposes one or more LWC modules.
* @param importee The module specifier to resolve
* @param dirname The directory to resolve relative to
* @param config Root directory and additional modules to
* @param config.rootDir Root dir use for module resolution, defaults to `process.cwd()`
* @param config.modules Array of additional modules to check, takes precedence over the project config
* @returns A registry entry for the resolved module
* @throws If the resolver processes an invalid configuration, it throws an error with the
* LWC_CONFIG_ERROR error code. If the resolver can't locate the module, it throws an error with the
* NO_LWC_MODULE_FOUND error code.
* @example resolveModule('x/foo', './index.js')
*/
function resolveModule(importee, dirname, config) {
if (typeof importee !== 'string') {
throw new TypeError(`The importee argument must be a string. Received type ${typeof importee}`);
}
if (typeof dirname !== 'string') {
throw new TypeError(`The dirname argument must be a string. Received type ${typeof dirname}`);
}
if (importee.startsWith('.') || importee.startsWith('/')) {
throw new TypeError(`The importee argument must be a valid LWC module name. Received "${importee}"`);
}
const rootDir = findFirstUpwardConfigPath(path.resolve(dirname));
const lwcConfig = getLwcConfig(rootDir);
let modules = lwcConfig.modules || [];
if (config) {
const userConfig = normalizeConfig(config, rootDir);
modules = mergeModules(userConfig.modules, modules);
}
for (const moduleRecord of modules) {
const registryEntry = resolveModuleRecordType(importee, moduleRecord, { rootDir });
if (registryEntry) {
return registryEntry;
}
}
throw new NoLwcModuleFound(importee, dirname);
}
exports.resolveModule = resolveModule;
/** version: 8.22.4 */
//# sourceMappingURL=index.cjs.js.map