@teambit/isolator
Version:
1,098 lines (1,080 loc) • 57.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.IsolatorMain = exports.CAPSULE_READY_FILE = void 0;
function _rimraf() {
const data = _interopRequireDefault(require("rimraf"));
_rimraf = function () {
return data;
};
return data;
}
function _uuid() {
const data = require("uuid");
_uuid = function () {
return data;
};
return data;
}
function _cli() {
const data = require("@teambit/cli");
_cli = function () {
return data;
};
return data;
}
function _semver() {
const data = _interopRequireDefault(require("semver"));
_semver = function () {
return data;
};
return data;
}
function _chalk() {
const data = _interopRequireDefault(require("chalk"));
_chalk = function () {
return data;
};
return data;
}
function _lodash() {
const data = require("lodash");
_lodash = function () {
return data;
};
return data;
}
function _harmonyModules() {
const data = require("@teambit/harmony.modules.feature-toggle");
_harmonyModules = function () {
return data;
};
return data;
}
function _aspectLoader() {
const data = require("@teambit/aspect-loader");
_aspectLoader = function () {
return data;
};
return data;
}
function _component() {
const data = require("@teambit/component");
_component = function () {
return data;
};
return data;
}
function _componentPackageVersion() {
const data = require("@teambit/component-package-version");
_componentPackageVersion = function () {
return data;
};
return data;
}
function _dependenciesFs() {
const data = require("@teambit/dependencies.fs.linked-dependencies");
_dependenciesFs = function () {
return data;
};
return data;
}
function _graph() {
const data = require("@teambit/graph");
_graph = function () {
return data;
};
return data;
}
function _harmony() {
const data = require("@teambit/harmony");
_harmony = function () {
return data;
};
return data;
}
function _dependencyResolver() {
const data = require("@teambit/dependency-resolver");
_dependencyResolver = function () {
return data;
};
return data;
}
function _logger() {
const data = require("@teambit/logger");
_logger = function () {
return data;
};
return data;
}
function _componentId() {
const data = require("@teambit/component-id");
_componentId = function () {
return data;
};
return data;
}
function _globalConfig() {
const data = require("@teambit/global-config");
_globalConfig = function () {
return data;
};
return data;
}
function _legacy() {
const data = require("@teambit/legacy.constants");
_legacy = function () {
return data;
};
return data;
}
function _component2() {
const data = require("@teambit/component.sources");
_component2 = function () {
return data;
};
return data;
}
function _legacy2() {
const data = require("@teambit/legacy.utils");
_legacy2 = function () {
return data;
};
return data;
}
function _harmonyModules2() {
const data = require("@teambit/harmony.modules.concurrency");
_harmonyModules2 = function () {
return data;
};
return data;
}
function _pkgModules() {
const data = require("@teambit/pkg.modules.component-package-name");
_pkgModules = function () {
return data;
};
return data;
}
function _fsExtra() {
const data = _interopRequireWildcard(require("fs-extra"));
_fsExtra = function () {
return data;
};
return data;
}
function _objectHash() {
const data = _interopRequireDefault(require("object-hash"));
_objectHash = function () {
return data;
};
return data;
}
function _path() {
const data = _interopRequireWildcard(require("path"));
_path = function () {
return data;
};
return data;
}
function _workspaceModules() {
const data = require("@teambit/workspace.modules.node-modules-linker");
_workspaceModules = function () {
return data;
};
return data;
}
function _pMap() {
const data = _interopRequireDefault(require("p-map"));
_pMap = function () {
return data;
};
return data;
}
function _capsule() {
const data = require("./capsule");
_capsule = function () {
return data;
};
return data;
}
function _capsuleList() {
const data = _interopRequireDefault(require("./capsule-list"));
_capsuleList = function () {
return data;
};
return data;
}
function _isolator() {
const data = require("./isolator.aspect");
_isolator = function () {
return data;
};
return data;
}
function _symlinkDependenciesToCapsules() {
const data = require("./symlink-dependencies-to-capsules");
_symlinkDependenciesToCapsules = function () {
return data;
};
return data;
}
function _network() {
const data = require("./network");
_network = function () {
return data;
};
return data;
}
function _configStore() {
const data = require("@teambit/config-store");
_configStore = function () {
return data;
};
return data;
}
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/**
* Context for the isolation process
*/
/**
* it's normally a sha1 of the workspace/scope dir. 40 chars long. however, Windows is not happy with long paths.
* so we use a shorter hash. the number 9 is pretty random, it's what we use for short-hash of snaps.
* we're aware of an extremely low risk of collision. take into account that in most cases you won't have more than 10
* capsules in the machine.
*/
const CAPSULE_DIR_LENGTH = 9;
const DEFAULT_ISOLATE_INSTALL_OPTIONS = {
installPackages: true,
dedupe: true,
installPeersFromEnvs: true,
copyPeerToRuntimeOnComponents: false,
copyPeerToRuntimeOnRoot: true
};
/**
* File name to indicate that the capsule is ready (all packages are installed and links are created)
*/
const CAPSULE_READY_FILE = exports.CAPSULE_READY_FILE = '.bit-capsule-ready';
class IsolatorMain {
// cache moved lock files to avoid show warning about them
static async provider([dependencyResolver, loggerExtension, componentAspect, graphMain, globalConfig, aspectLoader, cli, configStore], _config, [capsuleTransferSlot]) {
const logger = loggerExtension.createLogger(_isolator().IsolatorAspect.id);
const isolator = new IsolatorMain(dependencyResolver, logger, componentAspect, graphMain, cli, globalConfig, aspectLoader, capsuleTransferSlot, configStore);
return isolator;
}
constructor(dependencyResolver, logger, componentAspect, graph, cli, globalConfig, aspectLoader, capsuleTransferSlot, configStore) {
this.dependencyResolver = dependencyResolver;
this.logger = logger;
this.componentAspect = componentAspect;
this.graph = graph;
this.cli = cli;
this.globalConfig = globalConfig;
this.aspectLoader = aspectLoader;
this.capsuleTransferSlot = capsuleTransferSlot;
this.configStore = configStore;
_defineProperty(this, "_componentsPackagesVersionCache", {});
// cache packages versions of components
_defineProperty(this, "_datedHashForName", new Map());
// cache dated hash for a specific name
_defineProperty(this, "_movedLockFiles", new Set());
}
// TODO: the legacy scope used for the component writer, which then decide if it need to write the artifacts and dists
// TODO: we should think of another way to provide it (maybe a new opts) then take the scope internally from the host
async isolateComponents(seeders, opts, legacyScope) {
const host = opts.host || this.componentAspect.getHost();
this.logger.debug(`isolateComponents, ${seeders.join(', ')}. opts: ${JSON.stringify(Object.assign({}, opts, {
host: opts.host?.name
}))}`);
const createGraphOpts = (0, _lodash().pick)(opts, ['includeFromNestedHosts', 'host']);
const componentsToIsolate = opts.seedersOnly ? await host.getMany(seeders) : await this.createGraph(seeders, createGraphOpts);
this.logger.debug(`isolateComponents, total componentsToIsolate: ${componentsToIsolate.length}`);
const seedersWithVersions = seeders.map(seeder => {
if (seeder._legacy.hasVersion()) return seeder;
const comp = componentsToIsolate.find(component => component.id.isEqual(seeder, {
ignoreVersion: true
}));
if (!comp) throw new Error(`unable to find seeder ${seeder.toString()} in componentsToIsolate`);
return comp.id;
});
opts.baseDir = opts.baseDir || host.path;
const shouldUseDatedDirs = this.shouldUseDatedDirs(componentsToIsolate, opts);
const capsuleDir = this.getCapsulesRootDir(_objectSpread(_objectSpread({}, opts), {}, {
useDatedDirs: shouldUseDatedDirs,
baseDir: opts.baseDir || ''
}));
const cacheCapsulesDir = this.getCapsulesRootDir(_objectSpread(_objectSpread({}, opts), {}, {
useDatedDirs: false,
baseDir: opts.baseDir || ''
}));
opts.cacheCapsulesDir = cacheCapsulesDir;
const capsuleList = await this.createCapsules(componentsToIsolate, capsuleDir, opts, legacyScope);
this.logger.debug(`creating network with base dir: ${opts.baseDir}, rootBaseDir: ${opts.rootBaseDir}. final capsule-dir: ${capsuleDir}. capsuleList: ${capsuleList.length}`);
const cacheCapsules = process.env.CACHE_CAPSULES || opts.cacheLockFileOnly;
if (shouldUseDatedDirs && cacheCapsules) {
const targetCapsuleDir = this.getCapsulesRootDir(_objectSpread(_objectSpread({}, opts), {}, {
useDatedDirs: false,
baseDir: opts.baseDir || ''
}));
this.registerMoveCapsuleOnProcessExit(capsuleDir, targetCapsuleDir, opts.cacheLockFileOnly);
// TODO: ideally this should be inside the on process exit hook
// but this is an async op which make it a bit hard
await this.relinkCoreAspectsInCapsuleDir(targetCapsuleDir);
}
return new (_network().Network)(capsuleList, seedersWithVersions, capsuleDir);
}
async createGraph(seeders, opts = {}) {
const host = opts.host || this.componentAspect.getHost();
const getGraphOpts = (0, _lodash().pick)(opts, ['host']);
const graph = await this.graph.getGraphIds(seeders, getGraphOpts);
const successorsSubgraph = graph.successorsSubgraph(seeders.map(id => id.toString()));
const compsAndDepsIds = successorsSubgraph.nodes.map(node => node.attr);
// do not ignore the version here. a component might be in .bitmap with one version and
// installed as a package with another version. we don't want them both.
const existingCompsIds = await Promise.all(compsAndDepsIds.map(async id => {
let existing;
if (opts.includeFromNestedHosts) {
existing = await host.hasIdNested(id, true);
} else {
existing = await host.hasId(id);
}
if (existing) return id;
return undefined;
}));
const componentsToInclude = (0, _lodash().compact)(existingCompsIds);
let filteredComps = await host.getMany(componentsToInclude);
// Optimization: exclude unmodified exported dependencies from capsule creation
if (!(0, _harmonyModules().isFeatureEnabled)(_harmonyModules().DISABLE_CAPSULE_OPTIMIZATION)) {
filteredComps = await this.filterUnmodifiedExportedDependencies(filteredComps, seeders, host);
this.logger.debug(`[OPTIMIZATION] Before filtering: ${componentsToInclude.length}. After filtering: ${filteredComps.length} components remaining`);
}
return filteredComps;
}
registerMoveCapsuleOnProcessExit(datedCapsuleDir, targetCapsuleDir, cacheLockFileOnly = false) {
this.logger.info(`registering process.on(exit) to move capsules from ${datedCapsuleDir} to ${targetCapsuleDir}`);
this.cli.registerOnBeforeExit(async () => {
const allDirs = await this.getAllCapsulesDirsFromRoot(datedCapsuleDir);
if (cacheLockFileOnly) {
await this.moveCapsulesLockFileToTargetDir(allDirs, datedCapsuleDir, targetCapsuleDir);
} else {
await this.moveCapsulesToTargetDir(allDirs, datedCapsuleDir, targetCapsuleDir);
}
});
}
async getAllCapsulesDirsFromRoot(rootDir) {
const allDirs = await _fsExtra().default.readdir(rootDir, {
withFileTypes: true
});
const capsuleDirents = allDirs.filter(dir => dir.isDirectory() && dir.name !== 'node_modules');
return capsuleDirents.map(dir => _path().default.join(rootDir, dir.name));
}
async moveCapsulesLockFileToTargetDir(capsulesDirs, sourceRootDir, targetCapsuleDir) {
this.logger.info(`start moving lock files from ${sourceRootDir} to ${targetCapsuleDir}`);
const promises = capsulesDirs.map(async sourceDir => {
const dirname = _path().default.basename(sourceDir);
const sourceLockFile = _path().default.join(sourceDir, 'pnpm-lock.yaml');
// Lock file is not exist, don't copy it to the cache
if (!_fsExtra().default.pathExistsSync(sourceLockFile)) {
// It was already moved during the process, do not show the log for it
if (!this._movedLockFiles.has(sourceLockFile)) {
this.logger.console(`skipping moving lock file to cache as it is not exist ${sourceDir}`);
}
return;
}
const targetDir = _path().default.join(targetCapsuleDir, dirname);
const targetLockFile = _path().default.join(targetDir, 'pnpm-lock.yaml');
const targetLockFileExists = await _fsExtra().default.pathExists(targetLockFile);
if (targetLockFileExists) {
// Lock file is already in the cache, no need to move it
// this.logger.console(`skipping moving lock file to cache as it is already exist at ${targetDir}`);
// Delete existing lock file so we can update it
await _fsExtra().default.remove(targetLockFile);
return;
}
this.logger.debug(`moving lock file from ${sourceLockFile} to ${targetDir}`);
const mvFunc = this.getCapsuleTransferFn();
try {
await mvFunc(sourceLockFile, _path().default.join(targetDir, 'pnpm-lock.yaml'));
this._movedLockFiles.add(sourceLockFile);
} catch (err) {
this.logger.error(`failed moving lock file from ${sourceLockFile} to ${targetDir}`, err);
}
});
await Promise.all(promises);
}
async moveCapsulesToTargetDir(capsulesDirs, sourceRootDir, targetCapsuleDir) {
this.logger.info(`start moving capsules from ${sourceRootDir} to ${targetCapsuleDir}`);
const promises = capsulesDirs.map(async sourceDir => {
const dirname = _path().default.basename(sourceDir);
const sourceCapsuleReadyFile = this.getCapsuleReadyFilePath(sourceDir);
if (!_fsExtra().default.pathExistsSync(sourceCapsuleReadyFile)) {
// Capsule is not ready, don't copy it to the cache
this.logger.console(`skipping moving capsule to cache as it is not ready ${sourceDir}`);
return;
}
const targetDir = _path().default.join(targetCapsuleDir, dirname);
if (_fsExtra().default.pathExistsSync(_path().default.join(targetCapsuleDir, dirname))) {
const targetCapsuleReadyFile = this.getCapsuleReadyFilePath(targetDir);
if (_fsExtra().default.pathExistsSync(targetCapsuleReadyFile)) {
// Capsule is already in the cache, no need to move it
this.logger.console(`skipping moving capsule to cache as it is already exist at ${targetDir}`);
return;
}
this.logger.console(`cleaning target capsule location as it's not ready at: ${targetDir}`);
_rimraf().default.sync(targetDir);
}
this.logger.console(`moving specific capsule from ${sourceDir} to ${targetDir}`);
// We delete the ready file path first, as the move might take a long time, so we don't want to move
// the ready file indicator before the capsule is ready in the new location
this.removeCapsuleReadyFileSync(sourceDir);
await this.moveWithTempName(sourceDir, targetDir, this.getCapsuleTransferFn());
// Mark the capsule as ready in the new location
this.writeCapsuleReadyFileSync(targetDir);
});
await Promise.all(promises);
}
/**
* The function moves a directory from a source location to a target location using a temporary directory.
* This is using temp dir because sometime the source dir and target dir might be in different FS
* (for example different mounts) which means the move might take a long time
* during the time of moving, another process will see that the capsule is not ready and will try to remove then
* move it again, which lead to the first process throwing an error
* @param sourceDir - The source directory from where the files or directories will be moved.
* @param targetDir - The target directory where the source directory will be moved to.
*/
async moveWithTempName(sourceDir, targetDir, mvFunc = _fsExtra().default.move) {
const tempDir = `${targetDir}-${(0, _uuid().v4)()}`;
this.logger.console(`moving capsule from ${sourceDir} to a temp dir ${tempDir}`);
await mvFunc(sourceDir, tempDir);
const exists = await _fsExtra().default.pathExists(targetDir);
// This might exist if in the time when we move to the temp dir, another process created the target dir already
if (exists) {
this.logger.console(`skip moving capsule from temp dir to real dir as it's already exist: ${targetDir}`);
// Clean leftovers
await (0, _rimraf().default)(tempDir);
return;
}
this.logger.console(`moving capsule from a temp dir ${tempDir} to the target dir ${targetDir}`);
await mvFunc(tempDir, targetDir);
}
/**
* Re-create the core aspects links in the real capsule dir
* This is required mainly for the first time when that folder is empty
*/
async relinkCoreAspectsInCapsuleDir(capsulesDir) {
const linkingOptions = {
linkTeambitBit: true,
linkCoreAspects: true
};
const linker = this.dependencyResolver.getLinker({
rootDir: capsulesDir,
linkingOptions,
linkingContext: {
inCapsule: true
}
});
const {
linkedRootDeps
} = await linker.calculateLinkedDeps(capsulesDir, _component().ComponentMap.create([]), linkingOptions);
// This links are in the global cache which used by many process
// we don't want to delete and re-create the links if they already exist and valid
return (0, _dependenciesFs().createLinks)(capsulesDir, linkedRootDeps, {
skipIfSymlinkValid: true
});
}
shouldUseDatedDirs(componentsToIsolate, opts) {
if (!opts.useDatedDirs) return false;
// No need to use the dated dirs in case we anyway create new capsule for each one
if (opts.alwaysNew) return false;
// if (opts.skipIfExists) return false;
// no point to use dated dir in case of getExistingAsIs as it will be just always empty
if (opts.getExistingAsIs) return false;
// Do not use the dated dirs in case we don't use nesting, as the capsules
// will not work after moving to the real dir
if (!opts.installOptions?.useNesting) return false;
// Getting the real capsule dir to check if all capsules exists
const realCapsulesDir = this.getCapsulesRootDir(_objectSpread(_objectSpread({}, opts), {}, {
useDatedDirs: false,
baseDir: opts.baseDir || ''
}));
// validate all capsules in the real location exists and valid
const allCapsulesExists = componentsToIsolate.every(component => {
const capsuleDir = _path().default.join(realCapsulesDir, _capsule().Capsule.getCapsuleDirName(component));
const readyFilePath = this.getCapsuleReadyFilePath(capsuleDir);
return _fsExtra().default.existsSync(capsuleDir) && _fsExtra().default.existsSync(readyFilePath);
});
if (allCapsulesExists) {
this.logger.debug(`All required capsules already exists and valid in the real (cached) location: ${realCapsulesDir}`);
return false;
}
this.logger.debug(`Missing required capsules in the real (cached) location: ${realCapsulesDir}, using dated (temp) dir`);
return true;
}
/**
*
* @param originalCapsule the capsule that contains the original component
* @param newBaseDir relative path. (it will be saved inside `this.getRootDirOfAllCapsules()`. the final path of the capsule will be getRootDirOfAllCapsules() + newBaseDir + filenameify(component.id))
* @returns a new capsule with the same content of the original capsule but with a new baseDir and all packages
* installed in the newBaseDir.
*/
async cloneCapsule(originalCapsule, newBaseDir) {
const network = await this.isolateComponents([originalCapsule.component.id], {
baseDir: newBaseDir
});
const clonedCapsule = network.seedersCapsules[0];
await _fsExtra().default.copy(originalCapsule.path, clonedCapsule.path);
return clonedCapsule;
}
/**
* Create capsules for the provided components
* do not use this outside directly, use isolate components which build the entire network
* @param components
* @param opts
* @param legacyScope
*/
/* eslint-disable complexity */
async createCapsules(components, capsulesDir, opts, legacyScope) {
this.logger.debug(`createCapsules, ${components.length} components`);
let longProcessLogger;
if (opts.context?.aspects) {
// const wsPath = opts.host?.path || 'unknown';
const wsPath = opts.context.workspaceName || opts.host?.path || opts.name || 'unknown';
longProcessLogger = this.logger.createLongProcessLogger(`ensuring ${_chalk().default.cyan(components.length.toString())} capsule(s) for all envs and aspects for ${_chalk().default.bold(wsPath)} at ${_chalk().default.bold(capsulesDir)}`);
}
const useNesting = this.dependencyResolver.isolatedCapsules() && opts.installOptions?.useNesting;
const installOptions = _objectSpread(_objectSpread(_objectSpread({}, DEFAULT_ISOLATE_INSTALL_OPTIONS), opts.installOptions), {}, {
useNesting
});
if (!opts.emptyRootDir) {
installOptions.dedupe = installOptions.dedupe && this.dependencyResolver.supportsDedupingOnExistingRoot();
}
const config = _objectSpread({
installPackages: true
}, opts);
if (opts.getExistingAsIs && !(await _fsExtra().default.pathExists(capsulesDir))) {
this.logger.console(`💡 Capsules directory not found: ${capsulesDir}. Automatically setting getExistingAsIs to false.`);
opts.getExistingAsIs = false;
}
if (opts.emptyRootDir) {
await _fsExtra().default.emptyDir(capsulesDir);
}
let capsules = await this.createCapsulesFromComponents(components, capsulesDir, config);
this.writeRootPackageJson(capsulesDir, this.getCapsuleDirHash(opts.baseDir || ''));
const allCapsuleList = _capsuleList().default.fromArray(capsules);
let capsuleList = allCapsuleList;
if (opts.getExistingAsIs) {
longProcessLogger?.end();
return capsuleList;
}
if (opts.skipIfExists) {
if (!installOptions.useNesting) {
const existingCapsules = _capsuleList().default.fromArray(capsuleList.filter(capsule => capsule.fs.existsSync('package.json')));
if (existingCapsules.length === capsuleList.length) {
longProcessLogger?.end();
return existingCapsules;
}
} else {
capsules = capsules.filter(capsule => !capsule.fs.existsSync('package.json'));
capsuleList = _capsuleList().default.fromArray(capsules);
}
}
const capsulesWithPackagesData = await this.getCapsulesPreviousPackageJson(capsules);
await this.writeComponentsInCapsules(components, capsuleList, legacyScope, opts);
await this.updateWithCurrentPackageJsonData(capsulesWithPackagesData, capsuleList);
if (installOptions.installPackages) {
const cachePackagesOnCapsulesRoot = opts.cachePackagesOnCapsulesRoot ?? false;
const linkingOptions = opts.linkingOptions ?? {};
let installLongProcessLogger;
// Only show the log message in case we are going to install something
if (capsuleList && capsuleList.length && !opts.context?.aspects) {
installLongProcessLogger = this.logger.createLongProcessLogger(`install packages in ${capsuleList.length} capsules`);
}
const rootLinks = await this.linkInCapsulesRoot(capsulesDir, capsuleList, linkingOptions);
if (installOptions.useNesting) {
await Promise.all(capsuleList.map(async (capsule, index) => {
const newCapsuleList = _capsuleList().default.fromArray([capsule]);
if (opts.cacheCapsulesDir && capsulesDir !== opts.cacheCapsulesDir && opts.cacheLockFileOnly) {
const cacheCapsuleDir = _path().default.join(opts.cacheCapsulesDir, (0, _path().basename)(capsule.path));
const lockFilePath = _path().default.join(cacheCapsuleDir, 'pnpm-lock.yaml');
const lockExists = await _fsExtra().default.pathExists(lockFilePath);
if (lockExists) {
try {
// this.logger.console(`moving lock file from ${lockFilePath} to ${capsule.path}`);
await (0, _fsExtra().copyFile)(lockFilePath, _path().default.join(capsule.path, 'pnpm-lock.yaml'));
} catch (err) {
// We can ignore the error, we don't want to break the flow. the file will be anyway re-generated
// in the target capsule. it will only be a bit slower.
this.logger.error(`failed moving lock file from cache folder path: ${lockFilePath} to local capsule at ${capsule.path} (even though the lock file seems to exist)`, err);
}
}
}
const linkedDependencies = await this.linkInCapsules(newCapsuleList, capsulesWithPackagesData);
if (index === 0) {
linkedDependencies[capsulesDir] = rootLinks;
}
await this.installInCapsules(capsule.path, newCapsuleList, installOptions, {
cachePackagesOnCapsulesRoot,
linkedDependencies,
packageManager: opts.packageManager,
nodeLinker: opts.nodeLinker
});
}));
} else {
const dependenciesGraph = opts.useDependenciesGraph ? await legacyScope?.getDependenciesGraphByComponentIds(capsuleList.getAllComponentIDs()) : undefined;
const linkedDependencies = await this.linkInCapsules(capsuleList, capsulesWithPackagesData);
linkedDependencies[capsulesDir] = rootLinks;
await this.installInCapsules(capsulesDir, capsuleList, installOptions, {
cachePackagesOnCapsulesRoot,
linkedDependencies,
packageManager: opts.packageManager,
dependenciesGraph
});
if (opts.useDependenciesGraph && dependenciesGraph == null) {
// If the graph was not present in the model, we use the just created lockfile inside the capsules
// to populate the graph.
await this.addDependenciesGraphToComponents(capsuleList, components, capsulesDir);
}
}
if (installLongProcessLogger) {
installLongProcessLogger.end('success');
}
}
// rewrite the package-json with the component dependencies in it. the original package.json
// that was written before, didn't have these dependencies in order for the package-manager to
// be able to install them without crashing when the versions don't exist yet.
// skip this rewrite when populateArtifactsFrom is set, because the package.json was already
// written with the correct (merged) dependencies from the last build in writeComponentsInCapsules.
if (!opts.populateArtifactsFrom) {
capsulesWithPackagesData.forEach(capsuleWithPackageData => {
const {
currentPackageJson,
capsule
} = capsuleWithPackageData;
if (!currentPackageJson) throw new Error(`isolator.createCapsules, unable to find currentPackageJson for ${capsule.component.id.toString()}`);
capsuleWithPackageData.capsule.fs.writeFileSync(_legacy().PACKAGE_JSON, JSON.stringify(currentPackageJson, null, 2));
});
}
await this.markCapsulesAsReady(capsuleList);
if (longProcessLogger) {
longProcessLogger.end();
}
// Only show this message if at least one new capsule created
if (longProcessLogger && capsuleList.length) {
// this.logger.consoleSuccess();
const capsuleListOutput = allCapsuleList.map(capsule => capsule.component.id.toString()).join(', ');
this.logger.consoleSuccess(`resolved aspect(s): ${_chalk().default.cyan(capsuleListOutput)}`);
}
return allCapsuleList;
}
/* eslint-enable complexity */
async addDependenciesGraphToComponents(capsuleList, components, capsulesDir) {
const componentIdByPkgName = this.dependencyResolver.createComponentIdByPkgNameMap(components);
const opts = {
componentIdByPkgName,
rootDir: capsulesDir
};
const comps = capsuleList.map(capsule => ({
component: capsule.component,
componentRelativeDir: _path().default.relative(capsulesDir, capsule.path)
}));
await this.dependencyResolver.addDependenciesGraph(comps, opts);
}
async markCapsulesAsReady(capsuleList) {
await Promise.all(capsuleList.map(async capsule => {
return this.markCapsuleAsReady(capsule);
}));
}
async markCapsuleAsReady(capsule) {
const readyFilePath = this.getCapsuleReadyFilePath(capsule.path);
return _fsExtra().default.writeFile(readyFilePath, '');
}
removeCapsuleReadyFileSync(capsulePath) {
const readyFilePath = this.getCapsuleReadyFilePath(capsulePath);
const exist = _fsExtra().default.pathExistsSync(readyFilePath);
if (!exist) return;
_fsExtra().default.removeSync(readyFilePath);
}
writeCapsuleReadyFileSync(capsulePath) {
const readyFilePath = this.getCapsuleReadyFilePath(capsulePath);
const exist = _fsExtra().default.pathExistsSync(readyFilePath);
if (exist) return;
_fsExtra().default.writeFileSync(readyFilePath, '');
}
getCapsuleReadyFilePath(capsulePath) {
return _path().default.join(capsulePath, CAPSULE_READY_FILE);
}
async installInCapsules(capsulesDir, capsuleList, isolateInstallOptions, opts) {
const installer = this.dependencyResolver.getInstaller({
rootDir: capsulesDir,
cacheRootDirectory: opts.cachePackagesOnCapsulesRoot ? capsulesDir : undefined,
installingContext: {
inCapsule: true
},
packageManager: opts.packageManager,
nodeLinker: opts.nodeLinker
});
// When using isolator we don't want to use the policy defined in the workspace directly,
// we only want to instal deps from components and the peer from the workspace
const peerOnlyPolicy = this.getWorkspacePeersOnlyPolicy();
const installOptions = {
installTeambitBit: !!isolateInstallOptions.installTeambitBit,
packageManagerConfigRootDir: isolateInstallOptions.packageManagerConfigRootDir,
resolveVersionsFromDependenciesOnly: true,
linkedDependencies: opts.linkedDependencies,
forcedHarmonyVersion: this.dependencyResolver.harmonyVersionInRootPolicy(),
excludeExtensionsDependencies: true,
dedupeInjectedDeps: true,
dependenciesGraph: opts.dependenciesGraph
};
const packageManagerInstallOptions = {
autoInstallPeers: this.dependencyResolver.config.autoInstallPeers,
dedupe: isolateInstallOptions.dedupe,
copyPeerToRuntimeOnComponents: isolateInstallOptions.copyPeerToRuntimeOnComponents,
copyPeerToRuntimeOnRoot: isolateInstallOptions.copyPeerToRuntimeOnRoot,
installPeersFromEnvs: isolateInstallOptions.installPeersFromEnvs,
nmSelfReferences: this.dependencyResolver.config.capsuleSelfReference,
overrides: this.dependencyResolver.config.capsulesOverrides || this.dependencyResolver.config.overrides,
hoistPatterns: this.dependencyResolver.config.hoistPatterns,
rootComponentsForCapsules: this.dependencyResolver.isolatedCapsules(),
useNesting: isolateInstallOptions.useNesting,
keepExistingModulesDir: this.dependencyResolver.isolatedCapsules(),
hasRootComponents: this.dependencyResolver.isolatedCapsules(),
hoistWorkspacePackages: true
};
await installer.install(capsulesDir, peerOnlyPolicy, this.toComponentMap(capsuleList), installOptions, packageManagerInstallOptions);
}
async linkInCapsules(capsuleList, capsulesWithPackagesData) {
let nestedLinks;
if (!this.dependencyResolver.isolatedCapsules()) {
const capsulesWithModifiedPackageJson = this.getCapsulesWithModifiedPackageJson(capsulesWithPackagesData);
nestedLinks = await (0, _symlinkDependenciesToCapsules().symlinkDependenciesToCapsules)(capsulesWithModifiedPackageJson, capsuleList, this.logger);
}
return nestedLinks ?? {};
}
async linkInCapsulesRoot(capsulesDir, capsuleList, linkingOptions) {
const linker = this.dependencyResolver.getLinker({
rootDir: capsulesDir,
linkingOptions,
linkingContext: {
inCapsule: true
}
});
const {
linkedRootDeps
} = await linker.calculateLinkedDeps(capsulesDir, this.toComponentMap(capsuleList), _objectSpread(_objectSpread({}, linkingOptions), {}, {
linkNestedDepsInNM: !this.dependencyResolver.isolatedCapsules() && linkingOptions.linkNestedDepsInNM
}));
let rootLinks;
if (!this.dependencyResolver.isolatedCapsules()) {
rootLinks = await (0, _symlinkDependenciesToCapsules().symlinkOnCapsuleRoot)(capsuleList, this.logger, capsulesDir);
} else {
const coreAspectIds = this.aspectLoader.getCoreAspectIds();
const coreAspectCapsules = _capsuleList().default.fromArray(capsuleList.filter(capsule => {
const [compIdWithoutVersion] = capsule.component.id.toString().split('@');
return coreAspectIds.includes(compIdWithoutVersion);
}));
rootLinks = await (0, _symlinkDependenciesToCapsules().symlinkOnCapsuleRoot)(coreAspectCapsules, this.logger, capsulesDir);
}
return _objectSpread(_objectSpread({}, linkedRootDeps), this.toLocalLinks(rootLinks));
}
toLocalLinks(rootLinks) {
const localLinks = [];
if (rootLinks) {
rootLinks.forEach(link => {
localLinks.push(this.linkDetailToLocalDepEntry(link));
});
}
return Object.fromEntries(localLinks.map(([key, value]) => [key, `link:${value}`]));
}
linkDetailToLocalDepEntry(linkDetail) {
return [linkDetail.packageName, linkDetail.from];
}
getCapsulesWithModifiedPackageJson(capsulesWithPackagesData) {
const capsulesWithModifiedPackageJson = capsulesWithPackagesData.filter(capsuleWithPackageData => {
const packageJsonHasChanged = this.wereDependenciesInPackageJsonChanged(capsuleWithPackageData);
// @todo: when a component is tagged, it changes all package-json of its dependents, but it
// should not trigger any "npm install" because they dependencies are symlinked by us
return packageJsonHasChanged;
}).map(capsuleWithPackageData => capsuleWithPackageData.capsule);
return capsulesWithModifiedPackageJson;
}
/**
* TODO: The modified/unmodified separation and `importMultipleDistsArtifacts` optimization below
* is likely ineffective and could be removed. See TODO comment on `CapsuleList.capsuleUsePreviouslySavedDists`
* for details. The optimization downloads dist artifacts for unmodified components, but TypeScript
* will recompile them anyway because `tsconfig.tsbuildinfo` is not saved in objects.
*/
async writeComponentsInCapsules(components, capsuleList, legacyScope, opts) {
const modifiedComps = [];
const unmodifiedComps = [];
await Promise.all(components.map(async component => {
if (await _capsuleList().default.capsuleUsePreviouslySavedDists(component)) {
unmodifiedComps.push(component);
} else {
modifiedComps.push(component);
}
}));
const legacyUnmodifiedComps = unmodifiedComps.map(component => component.state._consumer.clone());
const legacyModifiedComps = modifiedComps.map(component => component.state._consumer.clone());
const legacyComponents = [...legacyUnmodifiedComps, ...legacyModifiedComps];
if (legacyScope && unmodifiedComps.length) await (0, _component2().importMultipleDistsArtifacts)(legacyScope, legacyUnmodifiedComps);
const allIds = _componentId().ComponentIdList.fromArray(legacyComponents.map(c => c.id));
const getParentsComp = () => {
const artifactsFrom = opts?.populateArtifactsFrom;
if (!artifactsFrom) return undefined;
if (!legacyScope) throw new Error('populateArtifactsFrom is set but legacyScope is not defined');
return Promise.all(artifactsFrom.map(id => legacyScope.getConsumerComponent(id)));
};
const populateArtifactsFromComps = await getParentsComp();
await Promise.all(components.map(async component => {
const capsule = capsuleList.getCapsule(component.id);
if (!capsule) return;
const scope = (await _capsuleList().default.capsuleUsePreviouslySavedDists(component)) || opts?.populateArtifactsFrom ? legacyScope : undefined;
const dataToPersist = await this.populateComponentsFilesToWriteForCapsule(component, allIds, scope, opts, populateArtifactsFromComps);
await dataToPersist.persistAllToCapsule(capsule, {
keepExistingCapsule: true
});
}));
}
getWorkspacePeersOnlyPolicy() {
const workspacePolicy = this.dependencyResolver.getWorkspacePolicy();
const peerOnlyPolicy = workspacePolicy.byLifecycleType('peer');
return peerOnlyPolicy;
}
toComponentMap(capsuleList) {
const tuples = capsuleList.map(capsule => {
return [capsule.component, capsule.path];
});
return _component().ComponentMap.create(tuples);
}
async list(rootDir) {
try {
const capsules = await _fsExtra().default.readdir(rootDir);
const withoutNodeModules = capsules.filter(c => c !== 'node_modules');
const capsuleFullPaths = withoutNodeModules.map(c => _path().default.join(rootDir, c));
return {
capsules: capsuleFullPaths
};
} catch (e) {
if (e.code === 'ENOENT') {
return {
capsules: []
};
}
throw e;
}
}
registerCapsuleTransferFn(fn) {
this.capsuleTransferSlot.register(fn);
}
getCapsuleTransferFn() {
return this.capsuleTransferSlot.values()[0] || this.getDefaultCapsuleTransferFn();
}
getDefaultCapsuleTransferFn() {
return async (source, target) => {
return _fsExtra().default.move(source, target, {
overwrite: true
});
};
}
getCapsuleDirHash(baseDir) {
return (0, _objectHash().default)(baseDir).substring(0, CAPSULE_DIR_LENGTH);
}
/** @deprecated use the new function signature with an object parameter instead */
getCapsulesRootDir(getCapsuleDirOpts, rootBaseDir, useHash = true, useDatedDirs = false, datedDirId) {
if (typeof getCapsuleDirOpts === 'string') {
getCapsuleDirOpts = {
baseDir: getCapsuleDirOpts,
rootBaseDir,
useHash,
useDatedDirs,
datedDirId
};
}
const getCapsuleDirOptsWithDefaults = _objectSpread({
useHash: true,
useDatedDirs: false,
cacheLockFileOnly: false
}, getCapsuleDirOpts);
const capsulesRootBaseDir = getCapsuleDirOptsWithDefaults.rootBaseDir || this.getRootDirOfAllCapsules();
if (getCapsuleDirOptsWithDefaults.useDatedDirs) {
const date = new Date();
const month = date.getMonth() < 12 ? date.getMonth() + 1 : 1;
const dateDir = `${date.getFullYear()}-${month}-${date.getDate()}`;
const defaultDatedBaseDir = 'dated-capsules';
const datedBaseDir = this.configStore.getConfig(_legacy().CFG_CAPSULES_SCOPES_ASPECTS_DATED_DIR) || defaultDatedBaseDir;
let hashDir;
const finalDatedDirId = getCapsuleDirOpts.datedDirId;
if (finalDatedDirId && this._datedHashForName.has(finalDatedDirId)) {
// Make sure in the same process we always use the same hash for the same datedDirId
hashDir = this._datedHashForName.get(finalDatedDirId);
} else {
hashDir = (0, _uuid().v4)();
if (finalDatedDirId) {
this._datedHashForName.set(finalDatedDirId, hashDir);
}
}
return _path().default.join(capsulesRootBaseDir, datedBaseDir, dateDir, hashDir);
}
const dir = getCapsuleDirOptsWithDefaults.useHash ? this.getCapsuleDirHash(getCapsuleDirOptsWithDefaults.baseDir) : getCapsuleDirOptsWithDefaults.baseDir;
return _path().default.join(capsulesRootBaseDir, dir);
}
async deleteCapsules(rootDir) {
const dirToDelete = rootDir || this.getRootDirOfAllCapsules();
await _fsExtra().default.remove(dirToDelete);
return dirToDelete;
}
writeRootPackageJson(capsulesDir, hashDir) {
const rootPackageJson = _path().default.join(capsulesDir, 'package.json');
if (!_fsExtra().default.existsSync(rootPackageJson)) {
const packageJson = {
name: `capsules-${hashDir}`,
'bit-capsule': true
};
_fsExtra().default.outputJsonSync(rootPackageJson, packageJson);
}
}
async createCapsulesFromComponents(components, baseDir, opts) {
this.logger.debug(`createCapsulesFromComponents: ${components.length} components`);
const capsules = await (0, _pMap().default)(components, component => {
return _capsule().Capsule.createFromComponent(component, baseDir, opts);
}, {
concurrency: (0, _harmonyModules2().concurrentComponentsLimit)()
});
return capsules;
}
getRootDirOfAllCapsules() {
return this.globalConfig.getGlobalCapsulesBaseDir();
}
wereDependenciesInPackageJsonChanged(capsuleWithPackageData) {
const {
previousPackageJson,
currentPackageJson
} = capsuleWithPackageData;
if (!previousPackageJson) return true;
// @ts-ignore at this point, currentPackageJson is set
return _legacy().DEPENDENCIES_FIELDS.some(field => !(0, _lodash().isEqual)(previousPackageJson[field], currentPackageJson[field]));
}
async getCapsulesPreviousPackageJson(capsules) {
return Promise.all(capsules.map(async capsule => {
const packageJsonPath = _path().default.join(capsule.path, 'package.json');
let previousPackageJson = null;
try {
const previousPackageJsonRaw = await capsule.fs.promises.readFile(packageJsonPath, {
encoding: 'utf8'
});
previousPackageJson = JSON.parse(previousPackageJsonRaw);
} catch {
// package-json doesn't exist in the capsule, that's fine, it'll be considered as a cache miss
}
return {
capsule,
previousPackageJson
};
}));
}
async updateWithCurrentPackageJsonData(capsulesWithPackagesData, capsules) {
return Promise.all(capsules.map(async capsule => {
const packageJson = await this.getCurrentPackageJson(capsule, capsules);
const found = capsulesWithPackagesData.filter(c => c.capsule.component.id.isEqual(capsule.component.id));
if (!found.length) throw new Error(`updateWithCurrentPackageJsonData unable to find ${capsule.component.id}`);
if (found.length > 1) throw new Error(`updateWithCurrentPackageJsonData found duplicate capsules: ${capsule.component.id.toString()}""`);
found[0].currentPackageJson = packageJson.packageJsonObject;
}));
}
async getCurrentPackageJson(capsule, capsules) {
const component = capsule.component;
const currentVersion = (0, _componentPackageVersion().getComponentPackageVersion)(component);
const getComponentDepsManifest = async dependencies => {
const manifest = {
dependencies: {},
devDependencies: {},
peerDependencies: {}
};
const compDeps = dependencies.toTypeArray('component');
const promises = compDeps.map(async dep => {
const depCapsule = capsules.getCapsule(dep.componentId);
let version = dep.version;
if (depCapsule) {
version = (0, _componentPackageVersion().getComponentPackageVersion)(depCapsule.component);
} else {
version = (0, _componentPackageVersion().snapToSemver)(version);
}
const keyName = _dependencyResolver().KEY_NAME_BY_LIFECYCLE_TYPE[dep.lifecycle];
const entry = dep.toManifest();
if (entry) {
manifest[keyName][entry.packageName] = dep.versionRange && dep.versionRange !== '+' ? dep.versionRange : version;
}
});
await Promise.all(promises);
return manifest;
};
const deps = this.dependencyResolver.getDependencies(component);
const manifest = await getComponentDepsManifest(deps);
// component.packageJsonFile is not available here. we don't mutate the component object for capsules.
// also, don't use `PackageJsonFile.createFromComponent`, as it looses the intermediate changes
// such as postInstall scripts for custom-module-resolution.
const packageJson = _component2().PackageJsonFile.loadFromCapsuleSync(capsule.path);
const addDependencies = packageJsonFile => {
packageJsonFile.addDependencies(manifest.dependencies);
packageJsonFile.addDevDependencies(manifest.devDependencies);
packageJsonFile.addPeerDependencies(manifest.peerDependencies);
};
addDependencies(packageJson);
packageJson.addOrUpdateProperty('version', currentVersion);
return packageJson;
}
async populateComponentsFilesToWriteForCapsule(component, ids, legacyScope, opts, populateArtifactsFromComps) {
const legacyComp = component.state._consumer;
const dataToPersist = new (_component2().DataToPersist)();
const clonedFiles = legacyComp.files.map(file => file.clone());
const writeToPath = '.';
clonedFiles.forEach(file => file.updatePaths({
newBase: writeToPath
}));
dataToPersist.removePath(new (_component2().RemovePath)(writeToPath));
clonedFiles.map(file => dataToPersist.addFile(file));
const packageJson = this.preparePackageJsonToWrite(component, writeToPath, ids);
if (!legacyComp.id.hasVersion()) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
packageJson.addOrUpdateProperty('version', _semver().default.inc(legacyComp.version, 'prerelease') || '0.0.1-0');
}
await _workspaceModules().PackageJsonTransformer.applyTransformers(component, packageJson);
const valuesToMerge = legacyComp.overrides.componentOverridesPackageJsonData;
packageJson.mergePackageJsonObject(valuesToMerge);
if (populateArtifactsFromComps && !opts?.populateArtifactsIgnorePkgJson) {
const compParent = this.getCompForArtifacts(component, populateArtifactsFromComps);
this.mergePkgJsonFromLastBuild(compParent, packageJson);
}
dataToPersist.addFile(packageJson.toVinylFile());
const artifacts = await this.getArtifacts(component, legacyScope, populateArtifactsFromComps);
dataToPersist.addManyFiles(artifacts);
return dataToPersist;
}
mergePkgJsonFromLastBuild(component, packageJson) {
const suffix = `for ${component.id.toString()}. to workaround this, use --ignore-last-pkg-json flag`;
const aspectsData = component.extensions.findExtension('teambit.pipelines/builder')?.data?.aspectsData;
if (!aspectsData) throw new Error(`getPkgJsonFromLastBuild, unable to find builder aspects data ${suffix}`);
const data = aspectsData?.find(aspectData => aspectData.aspectId === 'teambit.pkg/pkg');
if (!data) throw new Error(`getPkgJsonFromLastBuild, unable to find pkg aspect data ${suffix}`);
const pkgJsonLastBuild = data?.data?.pkgJson;
if (!pkgJsonLastBuild) throw new Error(`getPkgJsonFromLastBuild, unable to find pkgJson of pkg aspect ${suffix}`);
const current = packageJson.packageJsonObject;
pkgJsonLastBuild.componentId = current.componentId;
pkgJsonLastBuild.version = current.version;
const mergeDeps = (currentDeps, depsFromLastBuild) => {
if (!depsFromLastBuild) return;
if (!currentDeps) return depsFromLastBuild;
Object.keys(depsFromLastBuild).forEach(depName => {
if (!currentDeps[depName]) return;
depsFromLastBuild[depName] = currentDeps[depName];
});
return depsFromLastBuild;
};
pkgJsonLastBuild.dependencies = mer