@coat/cli
Version:
TODO: See #3
339 lines (312 loc) • 15.7 kB
JavaScript
"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;
}
}
}
}
}
}