@coat/cli
Version:
TODO: See #3
182 lines (175 loc) • 8.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.UpdatePrompt = exports.FileOperationType = void 0;
exports.getFileOperations = getFileOperations;
var _getFileHash = require("../util/get-file-hash");
var _getNormalizedFilePath = require("../util/get-normalized-file-path");
var _constants = require("../constants");
let UpdatePrompt;
exports.UpdatePrompt = UpdatePrompt;
(function (UpdatePrompt) {
UpdatePrompt[UpdatePrompt["Update"] = 0] = "Update";
UpdatePrompt[UpdatePrompt["FirstUpdate"] = 1] = "FirstUpdate";
})(UpdatePrompt || (exports.UpdatePrompt = UpdatePrompt = {}));
let FileOperationType;
exports.FileOperationType = FileOperationType;
(function (FileOperationType) {
FileOperationType[FileOperationType["Delete"] = 0] = "Delete";
FileOperationType[FileOperationType["DeleteSkipped"] = 1] = "DeleteSkipped";
FileOperationType[FileOperationType["Place"] = 2] = "Place";
FileOperationType[FileOperationType["Update"] = 3] = "Update";
FileOperationType[FileOperationType["UpdateWithPrompt"] = 4] = "UpdateWithPrompt";
})(FileOperationType || (exports.FileOperationType = FileOperationType = {}));
/**
* Writes updates from coat sync to the disk.
*
* The top priority is to prevent accidental loss of data,
* therefore coat sync should prompt the user in certain situations,
* or automatically skip the creation / deletion.
*
* The following table presents the situations where the user shall be prompted:
*
* | Scenario / Disk context | In lockfile (N) On disk (N) | In lockfile (N) Different disk content | In lockfile (N) On disk (Y) | In lockfile (Y) On disk (N) | In lockfile (Y) Different disk content | In lockfile (Y) On disk (Y) |
* |--------------------------|-----------------------------|----------------------------------------|-----------------------------|-----------------------------|----------------------------------------|-----------------------------|
* | Once File - Place | Place | / | / | / | / | / |
* | Continuous File - Place | Place | Prompt | Don't place | Place | Prompt | Don't place |
* | Continuous File - Delete | / | / | / | Don't delete | Don't delete | Delete |
*
* TODO: See #55
* It could be possible to offer automatic customization file placement
* if the detected change in a file can be easily refactored by
* using a customization file.
*
* @param allFilesToPlace All files that should be created or updated
* @param allFilesToRemove All files that should be deleted
* @param currentFiles The current content of the files on disk
* @param context The context of the current coat project
*/
function getFileOperations(allFilesToPlace, allFilesToRemove, currentFiles, context) {
// Determine which files need to be removed
const filesToRemove = allFilesToRemove.map(file => ({
...file,
// Add absolute file path to enable lookups and deletion
absolutePath: (0, _getNormalizedFilePath.getNormalizedFilePath)(file.path, context)
})).filter(file =>
// Filter out files that don't exist anymore
typeof currentFiles[file.absolutePath] !== "undefined" &&
//
// Filter out files that will be placed in the same run.
// This can happen when a file switches from being "local" to "global"
// or vice versa
allFilesToPlace.every(fileToPlace => fileToPlace.file !== file.absolutePath));
// Check which files should not be deleted and rather skipped
// because the file contents have been modified by the user
const filesToDelete = filesToRemove.reduce((accumulator, file) => {
// Type assertion is safe since missing files are filtered out above
const fileContentOnDisk = currentFiles[file.absolutePath];
const fileContentOnDiskHash = (0, _getFileHash.getFileHash)(fileContentOnDisk);
let type;
// If the file content on disk no longer matches the stored hash of the
// lockfile, the deletion should be skipped
if (fileContentOnDiskHash !== file.hash) {
type = FileOperationType.DeleteSkipped;
} else {
type = FileOperationType.Delete;
}
accumulator.push({
type,
relativePath: file.path,
absolutePath: file.absolutePath,
local: file.local
});
return accumulator;
}, []);
// Retrieve the hashes of all files that have already been managed
// and are included in the current lockfiles
const lockfileHashes = Object.fromEntries(
// Use files from both lockfiles
[...context.coatGlobalLockfile.files, ...context.coatLocalLockfile.files]
// Filter out once files, since they don't require updating or deletion
// and therefore don't have hashes
.filter(lockfileEntry => !lockfileEntry.once)
// Return a tuple of the path and hash of the entries to create a map
.map(lockfileEntry => [lockfileEntry.path, lockfileEntry.hash]));
// Split files that can be placed/updated directly and files
// for which the disk contents have changed and that
// require prompting the user before overwriting by
// using different FileOperation types
const filesToPlace = allFilesToPlace
// Filter out files that are already up-to-date on the disk
.filter(file => currentFiles[file.file] !== file.content).reduce((accumulator, file) => {
let promptType;
// Check cases where the user should be prompted
if (
// Special case for package.json
//
// Since package.json is updated in place, any updates
// to it will not prompt the user, since outside modifications
// (e.g. when adding dependencies) are not persisted in coat's lockfile.
file.relativePath !== _constants.PACKAGE_JSON_FILENAME &&
//
// Once files that have already been placed or exist already
// on the disk have already been filtered out before merging,
// and can therefore be placed directly without prompting
!file.once &&
//
// If file would not exist yet on the disk it could be placed without prompting
currentFiles[file.file]) {
if (!lockfileHashes[file.relativePath]) {
// The file does not exist yet in the lockfile but
// the file already exists on the disk.
//
// The user should be prompted to understand that
// this file will now be managed by coat
promptType = UpdatePrompt.FirstUpdate;
} else if (lockfileHashes[file.relativePath] !==
// Type assertion is safe, since a missing file entry has already
// been handled in the if condition above
(0, _getFileHash.getFileHash)(currentFiles[file.file])) {
// The file already exists in the lockfile, but the
// content on the disk is different to the previously generated
// file.
//
// The user should be prompted to understand that they should
// rather customize the file than edit it directly
promptType = UpdatePrompt.Update;
}
}
if (typeof promptType !== "undefined") {
accumulator.push({
absolutePath: file.file,
relativePath: file.relativePath,
content: file.content,
local: file.local,
prompt: promptType,
type: FileOperationType.UpdateWithPrompt
});
} else {
let type;
if (typeof currentFiles[file.file] !== "undefined") {
// If the current file already exists on the disk, it
// is an update rather than a place operation
type = FileOperationType.Update;
} else {
type = FileOperationType.Place;
}
accumulator.push({
absolutePath: file.file,
relativePath: file.relativePath,
content: file.content,
local: file.local,
type
});
}
return accumulator;
}, []);
// Create file operations, to easily log and
// apply the different file update types
const fileOperations = [...filesToDelete, ...filesToPlace];
// Sort file operations by their relative path to
// create a deterministic and readable log message
fileOperations.sort((file1, file2) => file1.relativePath.localeCompare(file2.relativePath));
return fileOperations;
}