UNPKG

@lwc/module-resolver

Version:

Resolves paths for LWC components

356 lines (347 loc) 14.1 kB
/** * Copyright (c) 2025 Salesforce, Inc. */ 'use strict'; 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