@sap-ux/project-access
Version:
Library to access SAP Fiori tools projects
102 lines • 4.16 kB
JavaScript
import { existsSync } from 'node:fs';
import { mkdir, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { createRequire } from 'node:module';
import { getNodeModulesPath } from './dependencies.js';
import { FileName, moduleCacheRoot } from '../constants.js';
import { execNpmCommand } from '../command/index.js';
import { pathToFileURL } from 'node:url';
const require = createRequire(import.meta.url);
/**
* Get the module path from project or app. Throws error if module is not installed.
*
* @param projectRoot - root path of the project/app.
* @param moduleName - name of the node module.
* @returns - path to module.
*/
export async function getModulePath(projectRoot, moduleName) {
if (!getNodeModulesPath(projectRoot, moduleName)) {
throw Error('Path to module not found.');
}
return require.resolve(moduleName, { paths: [projectRoot] });
}
/**
* Load module from project or app. Throws error if module is not installed.
*
* Note: Node's require.resolve() caches file access results in internal statCache, see:
* (https://github.com/nodejs/node/blob/d150316a8ecad1a9c20615ae62fcaf4f8d060dcc/lib/internal/modules/cjs/loader.js#L155)
* This means, if a module is not installed and require.resolve() is executed, it will never resolve, even after the
* module is installed later on. To prevent filling cjs loader's statCache with entries for non existing files,
* we check if the module exists using getNodeModulesPath() before require.resolve().
*
* @param projectRoot - root path of the project/app.
* @param moduleName - name of the node module.
* @returns - loaded module.
*/
export async function loadModuleFromProject(projectRoot, moduleName) {
let module;
try {
const modulePath = await getModulePath(projectRoot, moduleName);
module = (await import(pathToFileURL(modulePath).href));
}
catch (error) {
throw Error(`Module '${moduleName}' not installed in project '${projectRoot}'.\n${error.toString()}`);
}
return module;
}
/**
* Get a module, if it is not cached it will be installed and returned.
*
* @param module - name of the module
* @param version - version of the module
* @param options - optional options
* @param options.logger - optional logger instance
* @returns - module
*/
export async function getModule(module, version, options) {
const logger = options?.logger;
const moduleDirectory = join(moduleCacheRoot, module, version);
const modulePackagePath = join(moduleDirectory, FileName.Package);
const installCommand = ['install', '--prefix', moduleDirectory, `${module}@${version}`];
if (!existsSync(modulePackagePath)) {
if (existsSync(moduleDirectory)) {
await rm(moduleDirectory, { recursive: true });
}
await mkdir(moduleDirectory, { recursive: true });
await execNpmCommand(installCommand, {
cwd: moduleDirectory,
logger
});
}
let resolvedModule;
try {
resolvedModule = await loadModuleFromProject(moduleDirectory, module);
}
catch (e) {
logger?.error(`Failed to load module: ${module}. Attempting to fix installation.`);
const modulePackageLockPath = join(moduleDirectory, FileName.PackageLock);
// If 'package-lock.json' file exists then use 'npm ci', otherwise try reinstall
const command = existsSync(modulePackageLockPath) ? ['ci'] : installCommand;
// Run reinstall only if the first attempt fails
await execNpmCommand(command, {
cwd: moduleDirectory,
logger
});
// Retry loading the module
resolvedModule = await loadModuleFromProject(moduleDirectory, module);
}
return resolvedModule;
}
/**
* Delete a module from cache.
*
* @param module - name of the module
* @param version - version of the module
*/
export async function deleteModule(module, version) {
const moduleDirectory = join(moduleCacheRoot, module, version);
if (existsSync(moduleDirectory)) {
await rm(moduleDirectory, { recursive: true });
}
}
//# sourceMappingURL=module-loader.js.map