UNPKG

@coat/cli

Version:

TODO: See #3

339 lines (312 loc) 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.sync = sync; var _execa = _interopRequireDefault(require("execa")); var _ora = _interopRequireDefault(require("ora")); var _mergeFiles = require("./merge-files"); var _mergeScripts = require("./merge-scripts"); var _mergeDependencies = require("./merge-dependencies"); var _coatManifestFile = require("../types/coat-manifest-file"); var _constants = require("../constants"); var _polishFiles = require("./polish-files"); var _isEqual = _interopRequireDefault(require("lodash/isEqual")); var _generateLockfileFiles = require("../lockfiles/generate-lockfile-files"); var _getUnmanagedFiles = require("./get-unmanaged-files"); var _getAllTemplates = require("../util/get-all-templates"); var _updateLockfile = require("../lockfiles/update-lockfile"); var _setup = require("../setup"); var _immer = _interopRequireDefault(require("immer")); var _groupFiles = require("./group-files"); var _getDefaultFiles = require("./get-default-files"); var _getCurrentFiles = require("./get-current-files"); var _getFileOperations = require("./get-file-operations"); var _removeUnmanagedDependencies = require("./remove-unmanaged-dependencies"); var _getNormalizedFilePath = require("../util/get-normalized-file-path"); var _writeLockfiles = require("../lockfiles/write-lockfiles"); var _promptForFileOperations = require("./prompt-for-file-operations"); var _performFileOperations = require("./perform-file-operations"); var _chalk = _interopRequireDefault(require("chalk")); var _createFileOperationLogMessage = require("./create-file-operation-log-message"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * Generates all files from the current coat project. * * The sync function gathers all extended templates of the current project * and merges all files that should be written to the project directory. * * Setup tasks that have not been run before will be run at the beginning * of the sync process, to ensure that the templates have access to the * latest configuration and task results that are required to generate * the project files. * * @param options.cwd The working directory of the current coat project * @param options.check Whether a dry-run should be performed that checks * @param options.skipInstall Whether dependency installation should be skipped * and exits if the coat project is out of sync and has pending file updates */ async function sync({ cwd, check, skipInstall }) { var _context$packageJson, _context$packageJson2, _context$packageJson3, _context$packageJson4, _context$packageJson5; const checkFlag = !!check; // Run setup tasks that have not been run before // // TODO: See #36 // Let user skip setup tasks if they should be run later let context = await (0, _setup.setup)({ cwd, check: checkFlag, force: false }); // Gather all extended templates const allTemplates = (0, _getAllTemplates.getAllTemplates)(context); // Merge scripts const { scripts: mergedScripts, parallelScriptPrefixes } = (0, _mergeScripts.mergeScripts)(allTemplates.map(template => template.scripts)); // Get all current scripts from the project's package.json file // const previouslyManagedScripts = new Set(context.coatGlobalLockfile.scripts); const currentScripts = Object.fromEntries(Object.entries(((_context$packageJson = context.packageJson) === null || _context$packageJson === void 0 ? void 0 : _context$packageJson.scripts) || {}).filter(([scriptName]) => // Filter out scripts that have been added / managed by coat. // They will be re-added from mergedScripts or will be removed // in case the coat manifest or its templates no longer supply // a particular script !previouslyManagedScripts.has(scriptName) && // Also filter out scripts that start with a script that is managed by // coat and will be run in parallel. This is done in order to ensure // that the scripts from coat are running as expected parallelScriptPrefixes.every(prefix => !scriptName.startsWith(prefix)))); const scripts = { ...currentScripts, ...mergedScripts }; // Merge dependencies // // Add current dependencies from package.json, to satisfy the // correct minimum required versions for potentially existing dependencies const currentDependencies = { dependencies: ((_context$packageJson2 = context.packageJson) === null || _context$packageJson2 === void 0 ? void 0 : _context$packageJson2.dependencies) ?? {}, devDependencies: ((_context$packageJson3 = context.packageJson) === null || _context$packageJson3 === void 0 ? void 0 : _context$packageJson3.devDependencies) ?? {}, optionalDependencies: ((_context$packageJson4 = context.packageJson) === null || _context$packageJson4 === void 0 ? void 0 : _context$packageJson4.optionalDependencies) ?? {}, peerDependencies: ((_context$packageJson5 = context.packageJson) === null || _context$packageJson5 === void 0 ? void 0 : _context$packageJson5.peerDependencies) ?? {} }; const templateDependencies = (0, _mergeDependencies.mergeDependencies)(allTemplates.map(template => template.dependencies)); // Remove dependencies that have been previously added // by coat, but are no longer a part of any template const strippedCurrentDependencies = (0, _removeUnmanagedDependencies.removeUnmanagedDependencies)(currentDependencies, templateDependencies, context); const mergedDependencies = (0, _mergeDependencies.mergeDependencies)([strippedCurrentDependencies, templateDependencies]); const allFiles = []; // Add package.json file entry that can be merged and customized const packageJsonFileContent = { ...context.packageJson, ...mergedDependencies, scripts }; // Remove empty dependency and scripts properties const keysToRemove = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "scripts"]; keysToRemove.forEach(key => { if (!Object.keys(packageJsonFileContent[key]).length) { delete packageJsonFileContent[key]; } }); // Only add package.json to the allFiles array if it currently exists // or if the contents of the file have been updated by coat if (context.packageJson || !(0, _isEqual.default)(packageJsonFileContent, {})) { allFiles.push({ type: _coatManifestFile.CoatManifestFileType.Json, file: _constants.PACKAGE_JSON_FILENAME, content: packageJsonFileContent }); // Update package.json in context // to let files access the newest version context = (0, _immer.default)(context, draft => { draft.packageJson = packageJsonFileContent; }); } // Add default files that are generated by coat sync const defaultFiles = (0, _getDefaultFiles.getDefaultFiles)(); allFiles.push(...defaultFiles); // Add files from all templates allFiles.push(...allTemplates.flatMap(template => template.files)); // Group files by file path const groupedFiles = (0, _groupFiles.groupFiles)(allFiles, context); // Gather previously placed files to exclude one-time files // that have already been generated const previouslyPlacedFiles = [...context.coatGlobalLockfile.files, ...context.coatLocalLockfile.files].map(file => file.path); // Gather the files of which the current disk content // should be retrieved to determine they need to be updated const filesToRetrieve = [...Object.keys(groupedFiles), ...previouslyPlacedFiles.map(relativePath => (0, _getNormalizedFilePath.getNormalizedFilePath)(relativePath, context))]; // Filter out duplicate file paths, since filesToRetrieve // contains both the current lockfile entries and new groupedFiles // keys that overlap for consecutive sync runs const filesToRetrieveUnique = [...new Set(filesToRetrieve)]; // Retrieve the contents of the files const currentFiles = await (0, _getCurrentFiles.getCurrentFiles)(filesToRetrieveUnique); // Create a Set to easily access the generated file paths // when grouping files const previouslyPlacedFileSet = new Set(previouslyPlacedFiles); // Group by files that should only be placed once // and have already been placed in a previous run of coat sync const { onceAlreadyPlaced, filesToMerge } = Object.values(groupedFiles).reduce((accumulator, file) => { let targetProperty; if (file.once && ( // Check if the once file has already been placed via coat // and is tracked in a lockfile previouslyPlacedFileSet.has(file.relativePath) || // Even if the file has not yet been tracked in a lockfile // it should also not be placed if it already exists on the disk typeof currentFiles[file.file] !== "undefined")) { targetProperty = accumulator.onceAlreadyPlaced; } else { targetProperty = accumulator.filesToMerge; } targetProperty[file.file] = file; return accumulator; }, { onceAlreadyPlaced: {}, filesToMerge: {} }); // Merge files const mergedFiles = await (0, _mergeFiles.mergeFiles)(filesToMerge, context); // Polish files and convert their content into strings to // be able to place them on the disk const polishedFiles = (0, _polishFiles.polishFiles)(mergedFiles, context); // Generate new coat lockfile files property from merged files const lockFileCanditates = [...Object.values(onceAlreadyPlaced), ...polishedFiles]; // Split lockfile candidates by local and global file entries const { localLockFileEntries, globalLockFileEntries } = lockFileCanditates.reduce((accumulator, file) => { if (file.local) { accumulator.localLockFileEntries.push(file); } else { accumulator.globalLockFileEntries.push(file); } return accumulator; }, { localLockFileEntries: [], globalLockFileEntries: [] }); const newLocalLockFiles = (0, _generateLockfileFiles.generateLockfileFiles)(localLockFileEntries); const newGlobalLockFiles = (0, _generateLockfileFiles.generateLockfileFiles)(globalLockFileEntries); const filesToRemove = [...(0, _getUnmanagedFiles.getUnmanagedFiles)(newLocalLockFiles, context.coatLocalLockfile).map(unmanagedFile => ({ ...unmanagedFile, local: true })), ...(0, _getUnmanagedFiles.getUnmanagedFiles)(newGlobalLockFiles, context.coatGlobalLockfile).map(unmanagedFile => ({ ...unmanagedFile, local: false }))]; // Determine necessary file operations: // Place or update new polished files and remove unmanaged files const fileOperations = (0, _getFileOperations.getFileOperations)(polishedFiles, filesToRemove, currentFiles, context); // --check flag if (checkFlag) { // If there are any pending global file operations that // are not skipped, the coat project is out of sync const pendingGlobalFileOperations = fileOperations.filter(fileOperation => // Only check global file operations, pending local file operations // does not indicate that a coat project is out of sync !fileOperation.local && // // Ignore delete operations that are skipped and purely logged // for information. This will however likely lead to the --check // still failing, since the lockfile will require an update. fileOperation.type !== _getFileOperations.FileOperationType.DeleteSkipped); if (pendingGlobalFileOperations.length) { const pendingFileOperationMessages = pendingGlobalFileOperations.map(fileOperation => (0, _createFileOperationLogMessage.createFileOperationLogMessage)(fileOperation, _createFileOperationLogMessage.Tense.Future)); const messages = ["", (0, _chalk.default)`The {cyan coat} project is not in sync.`, "There are pending file updates:", "", ...pendingFileOperationMessages, "", (0, _chalk.default)`Run {cyan coat sync} to bring the project back in sync.`]; console.error(messages.join("\n")); process.exit(1); } } // Prompt the user if there are dangerous file operations that might have // unintended consequences. See getFileOperations for operations that // lead to prompts const shouldPerformFileOperations = await (0, _promptForFileOperations.promptForFileOperations)(fileOperations); if (!shouldPerformFileOperations) { // If the prompt is declined, sync should be aborted and // coat should exit immediately. console.error("Aborting coat sync due to user request."); process.exit(1); } const newLockfileDependencies = Object.fromEntries(Object.entries(templateDependencies).map(([dependencyKey, dependencyEntries]) => [dependencyKey, Object.keys(dependencyEntries).sort()])); const newLockfileScripts = Object.keys(mergedScripts).sort(); // Update the lockfiles with the new file entries // // global lockfile const newGlobalLockfile = (0, _updateLockfile.updateGlobalLockfile)(context.coatGlobalLockfile, { files: newGlobalLockFiles, dependencies: newLockfileDependencies, scripts: newLockfileScripts }); if (!(0, _isEqual.default)(context.coatGlobalLockfile, newGlobalLockfile)) { // // --check flag if (checkFlag) { // If the global lockfile needs to be updated, // the coat project is out of sync const messages = ["", (0, _chalk.default)`The {cyan coat} project is not in sync.`, (0, _chalk.default)`The global lockfile ({green coat.lock}) needs to be updated.`, "", (0, _chalk.default)`Run {cyan coat sync} to bring the project back in sync.`]; console.error(messages.join("\n")); process.exit(1); } context = (0, _immer.default)(context, draft => { draft.coatGlobalLockfile = newGlobalLockfile; }); await (0, _writeLockfiles.writeGlobalLockfile)(newGlobalLockfile, context); } if (checkFlag) { // sync --check can end here, the coat project is in sync console.log(`\n${_constants.EVERYTHING_UP_TO_DATE_MESSAGE}\n`); } else { // local lockfile const newLocalLockfile = (0, _updateLockfile.updateLocalLockfile)(context.coatLocalLockfile, { files: newLocalLockFiles }); if (!(0, _isEqual.default)(context.coatLocalLockfile, newLocalLockfile)) { context = (0, _immer.default)(context, draft => { draft.coatLocalLockfile = newLocalLockfile; }); await (0, _writeLockfiles.writeLocalLockfile)(newLocalLockfile, context); } // Update files on disk await (0, _performFileOperations.performFileOperations)(fileOperations); if (!skipInstall) { var _mergedFiles$find; // Retrieve dependencies after merging to run npm install if they have changed // // If the package.json file still exists, it has to be a JsonObject, // since an altered file type would throw an error during merging const mergedPackageJson = (_mergedFiles$find = mergedFiles.find(file => file.relativePath === _constants.PACKAGE_JSON_FILENAME)) === null || _mergedFiles$find === void 0 ? void 0 : _mergedFiles$find.content; if (mergedPackageJson) { const finalDependencies = { dependencies: mergedPackageJson.dependencies ?? {}, devDependencies: mergedPackageJson.devDependencies ?? {}, optionalDependencies: mergedPackageJson.optionalDependencies ?? {}, peerDependencies: mergedPackageJson.peerDependencies ?? {} }; if (!(0, _isEqual.default)(finalDependencies, currentDependencies)) { const installSpinner = (0, _ora.default)("Installing dependencies\n").start(); try { await (0, _execa.default)("npm", ["install"], { cwd: context.cwd }); installSpinner.succeed(); } catch (error) { installSpinner.fail(); throw error; } } } } } }