UNPKG

@teambit/isolator

Version:
1,098 lines (1,080 loc) • 57.7 kB
"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