UNPKG

@sprucelabs/spruce-cli

Version:

Command line interface for building Spruce skills.

315 lines • 13.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FeatureInstallerImpl = void 0; const schema_1 = require("@sprucelabs/schema"); const spruce_skill_utils_1 = require("@sprucelabs/spruce-skill-utils"); const lodash_1 = require("lodash"); const merge_1 = __importDefault(require("lodash/merge")); const SpruceError_1 = __importDefault(require("../errors/SpruceError")); class FeatureInstallerImpl { cwd; featureMap = {}; serviceFactory; featuresMarkedAsSkippedThisRun = []; static startInFlightIntertainmentHandler; static stopInFlightIntertainmentHandler; packagesToInstall = []; devPackagesToInstall = []; featuresToMarkAsInstalled = []; afterPackageInstalls = []; pendingFeatureInstalls = {}; constructor(cwd, serviceFactory) { this.cwd = cwd; this.serviceFactory = serviceFactory; } isInSpruceModule() { return spruce_skill_utils_1.diskUtil.doesDirExist(spruce_skill_utils_1.diskUtil.resolvePath(this.cwd, spruce_skill_utils_1.HASH_SPRUCE_DIR)); } async isInstalled(code) { const feature = this.getFeature(code); if (feature.isInstalled) { return feature.isInstalled(); } return this.Service('settings').isMarkedAsInstalled(code); } markAsSkippedThisRun(code) { if (!this.isMarkedAsSkipped(code)) { this.featuresMarkedAsSkippedThisRun.push(code); } } markAsPermanentlySkipped(code) { if (!this.isMarkedAsSkipped(code)) { this.Service('settings').markAsPermanentlySkipped(code); } } isMarkedAsSkipped(code) { return (this.featuresMarkedAsSkippedThisRun.indexOf(code) > -1 || this.Service('settings').isMarkedAsPermanentlySkipped(code)); } mapFeature(code, feature) { this.featureMap[code] = feature; } getFeature(code) { const feature = this.featureMap[code]; if (!feature) { throw new SpruceError_1.default({ code: 'INVALID_FEATURE_CODE', featureCode: code, }); } return feature; } async areInstalled(codes) { const results = await Promise.all(codes.map((f) => { return this.isInstalled(f); })); for (const result of results) { if (!result) { return false; } } return true; } getFeatureDependencies(featureCode // trackedFeatures: FeatureDependency[] = [] ) { let deps = this.getFeatureDependenciesIncludingSelf({ code: featureCode, isRequired: true }, [] // trackedFeatures ).filter((f) => f.code !== featureCode); deps = this.sortFeatures(deps); return deps; } getFeatureDependenciesIncludingSelf(featureDependency, trackedFeatures = []) { const features = []; if (!this.isDependencyInTracker(trackedFeatures, featureDependency)) { features.push(featureDependency); trackedFeatures.push(featureDependency); } const feature = this.getFeature(featureDependency.code); const dependencies = feature.dependencies; for (const dependency of dependencies) { if (!this.isDependencyInTracker(trackedFeatures, dependency)) { features.push(dependency); trackedFeatures.push(dependency); } } for (const dependency of dependencies) { let dependencyDependencies = this.getFeatureDependenciesIncludingSelf(dependency, trackedFeatures); if (!dependency.isRequired) { dependencyDependencies = dependencyDependencies.map((f) => ({ ...f, isRequired: false, })); } features.push(...dependencyDependencies); } return features; } isDependencyInTracker(trackedFeatures, dependency) { return !!trackedFeatures.find((f) => f.code === dependency.code); } async install(options) { let { features, installFeatureDependencies = true, didUpdateHandler, } = options; const shouldAllowEntertainment = !!features.find((f) => f.code === 'skill'); if (FeatureInstallerImpl.startInFlightIntertainmentHandler && shouldAllowEntertainment) { FeatureInstallerImpl.startInFlightIntertainmentHandler((handler) => { didUpdateHandler = handler; }); } this.pendingFeatureInstalls = {}; let results = {}; let dependenciesToInstall = []; for (const f of features) { const code = f.code; didUpdateHandler?.(`Checking if ${code} is installed...`); const isInstalled = await this.isInstalled(code); if (!isInstalled && installFeatureDependencies) { didUpdateHandler?.(`It is not, checking dependencies...`); dependenciesToInstall = dependenciesToInstall.concat(this.getFeatureDependenciesIncludingSelf({ code, isRequired: true, })); } else if (!isInstalled) { dependenciesToInstall.push({ code, isRequired: true }); } } dependenciesToInstall = (0, lodash_1.uniq)(dependenciesToInstall); dependenciesToInstall = this.sortFeatures(dependenciesToInstall); for (const { code, isRequired } of dependenciesToInstall) { const isInstalled = await this.isInstalledOrPendingInstall(code); if (!isInstalled && isRequired) { const installOptions = //@ts-ignore options.features.find((f) => f.code === code)?.options; const installFeature = { code, options: installOptions, }; didUpdateHandler?.(`Installing the ${installFeature.code} feature....`); const installResults = await this.installFeature(installFeature, didUpdateHandler); results = (0, merge_1.default)(results, installResults); } } const pendingResults = await this.installAllPending(didUpdateHandler); results = (0, merge_1.default)(results, pendingResults); if (FeatureInstallerImpl.stopInFlightIntertainmentHandler && shouldAllowEntertainment) { FeatureInstallerImpl.stopInFlightIntertainmentHandler(); } return results; } isInstalledOrPendingInstall(code) { return (this.pendingFeatureInstalls[code] || this.isInstalled(code)); } async installFeature(installFeature, didUpdateHandler) { this.pendingFeatureInstalls[installFeature.code] = true; const feature = this.getFeature(installFeature.code); if (feature.optionsSchema) { (0, schema_1.validateSchemaValues)(feature.optionsSchema, //@ts-ignore installFeature.options ?? {}); } didUpdateHandler?.(`Running before package install hook...`); const beforeInstallResults = await feature.beforePackageInstall( //@ts-ignore installFeature.options); if (beforeInstallResults.cwd) { this.cwd = beforeInstallResults.cwd; } didUpdateHandler?.(`Installing package dependencies...`); const packagesInstalled = await this.queueInstallPackageDependenciesWithoutEntertainment(feature, didUpdateHandler); didUpdateHandler?.(`Running after package install hook...`); this.afterPackageInstalls.push(installFeature); if (!feature.isInstalled) { this.featuresToMarkAsInstalled.push(feature.code); } const files = [...(beforeInstallResults.files ?? [])]; return { files: files ?? undefined, packagesInstalled, }; } async installPackageDependencies(feature, didUpdateHandler) { return this.installPackageDependenciesForFeatures([feature], didUpdateHandler); } async installPackageDependenciesForFeatures(features, didUpdateHandler) { if (FeatureInstallerImpl.startInFlightIntertainmentHandler) { FeatureInstallerImpl.startInFlightIntertainmentHandler((handler) => { didUpdateHandler = handler; }); } for (const feature of features) { await this.queueInstallPackageDependenciesWithoutEntertainment(feature, didUpdateHandler); } await this.installAllPending(didUpdateHandler); if (FeatureInstallerImpl.stopInFlightIntertainmentHandler) { FeatureInstallerImpl.stopInFlightIntertainmentHandler(); } } async installAllPending(didUpdateHandler) { const pkgService = this.Service('pkg'); if (this.packagesToInstall.length > 0) { didUpdateHandler?.(`Installing ${this.packagesToInstall.length} node module${this.packagesToInstall.length === 1 ? '' : 's'}. Please be patient.`); await pkgService.install(this.packagesToInstall, {}); } if (this.devPackagesToInstall.length > 0) { didUpdateHandler?.(`Now installing ${this.devPackagesToInstall.length} DEV node module${this.devPackagesToInstall.length === 1 ? '' : 's'}. Please be patient.`); await pkgService.install(this.devPackagesToInstall, { isDev: true, }); } this.packagesToInstall = []; this.devPackagesToInstall = []; const settings = this.Service('settings'); for (const code of this.featuresToMarkAsInstalled) { settings.markAsInstalled(code); } this.featuresToMarkAsInstalled = []; let results = {}; for (const installFeature of this.afterPackageInstalls) { const feature = this.getFeature(installFeature.code); const afterInstallResults = await feature.afterPackageInstall( //@ts-ignore installFeature.options); results = (0, merge_1.default)(results, afterInstallResults); } return results; } async queueInstallPackageDependenciesWithoutEntertainment(feature, didUpdateHandler) { const packagesInstalled = []; const pkgService = this.Service('pkg'); feature.packageDependencies?.forEach((pkg) => { const packageName = pkgService.buildPackageName(pkg); packagesInstalled.push(pkg); didUpdateHandler?.(`Checking node dependency: ${pkg.name}`); if (pkg.isDev && this.devPackagesToInstall.indexOf(packageName) === -1) { this.devPackagesToInstall.push(packageName); } else if (!pkg.isDev && this.packagesToInstall.indexOf(packageName) === -1) { this.packagesToInstall.push(packageName); } }); if (this.packagesToInstall.length > 0) { didUpdateHandler?.(`Queueing install of ${this.packagesToInstall.length} node dependenc${this.packagesToInstall.length === 1 ? 'y.' : 'ies for ' + this.getFeatureNameAndDesc(feature) + '...'}.`); } if (this.devPackagesToInstall.length > 0) { didUpdateHandler?.(`Queueing install of ${this.devPackagesToInstall.length} DEV node dependenc${this.devPackagesToInstall.length === 1 ? 'y.' : 'ies for ' + this.getFeatureNameAndDesc(feature) + '. 🤘'}.`); } return packagesInstalled; } getFeatureNameAndDesc(feature) { return `${feature.nameReadable}${feature.description ? ' (' + feature.description + ')' : ''}`; } sortFeatures(featureDependencies) { return [...featureDependencies].sort((a, b) => { const aFeature = this.getFeature(a.code); const bFeature = this.getFeature(b.code); const aDependsOnB = aFeature.dependencies.find((d) => d.code === b.code); const bDependsOnA = bFeature.dependencies.find((d) => d.code === a.code); if (aDependsOnB || aFeature.installOrderWeight < bFeature.installOrderWeight) { return 1; } else if (bDependsOnA || aFeature.installOrderWeight > bFeature.installOrderWeight) { return -1; } return 0; }); } Service(type, cwd) { return this.serviceFactory.Service(cwd ?? this.cwd, type); } getOptionsForFeature(code) { return this.getFeature(code).optionsSchema; } getAllCodes() { const codes = Object.keys(this.featureMap); return this.sortFeatures(codes.map((code) => ({ code, isRequired: true }))).map((dep) => dep.code); } async getInstalledFeatures() { const installed = await Promise.all(this.getAllCodes().map(async (code) => { const isInstalled = await this.isInstalled(code); if (isInstalled) { return this.getFeature(code); } return false; })); return installed.filter((f) => !!f); } } exports.FeatureInstallerImpl = FeatureInstallerImpl; //# sourceMappingURL=FeatureInstaller.js.map