UNPKG

@rushstack/heft

Version:

Build all your JavaScript projects the same way: A way that works.

147 lines 8.04 kB
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import { createHash } from 'node:crypto'; import * as path from 'node:path'; import { AlreadyExistsBehavior, FileSystem, Async } from '@rushstack/node-core-library'; import { Constants } from '../utilities/Constants'; import { asAbsoluteFileSelectionSpecifier, getFileSelectionSpecifierPathsAsync } from './FileGlobSpecifier'; import { makePathRelative, tryReadBuildInfoAsync, writeBuildInfoAsync } from '../pluginFramework/IncrementalBuildInfo'; export function asAbsoluteCopyOperation(rootFolderPath, copyOperation) { const absoluteCopyOperation = asAbsoluteFileSelectionSpecifier(rootFolderPath, copyOperation); absoluteCopyOperation.destinationFolders = copyOperation.destinationFolders.map((folder) => path.resolve(rootFolderPath, folder)); return absoluteCopyOperation; } export function asRelativeCopyOperation(rootFolderPath, copyOperation) { return { ...copyOperation, destinationFolders: copyOperation.destinationFolders.map((folder) => makePathRelative(folder, rootFolderPath)), sourcePath: copyOperation.sourcePath && makePathRelative(copyOperation.sourcePath, rootFolderPath) }; } export async function copyFilesAsync(copyOperations, terminal, buildInfoPath, configHash, watchFileSystemAdapter) { const copyDescriptorByDestination = await _getCopyDescriptorsAsync(copyOperations, watchFileSystemAdapter); await _copyFilesInnerAsync(copyDescriptorByDestination, configHash, buildInfoPath, terminal); } async function _getCopyDescriptorsAsync(copyConfigurations, fileSystemAdapter) { // Create a map to deduplicate and prevent double-writes // resolvedDestinationFilePath -> descriptor const copyDescriptorByDestination = new Map(); await Async.forEachAsync(copyConfigurations, async (copyConfiguration) => { // "sourcePath" is required to be a folder. To copy a single file, put the parent folder in "sourcePath" // and the filename in "includeGlobs". const sourceFolder = copyConfiguration.sourcePath; const sourceFiles = await getFileSelectionSpecifierPathsAsync({ fileGlobSpecifier: copyConfiguration, fileSystemAdapter }); // Dedupe and throw if a double-write is detected for (const destinationFolderPath of copyConfiguration.destinationFolders) { // We only need to care about the keys of the map since we know all the keys are paths to files for (const sourceFilePath of sourceFiles.keys()) { // Only include the relative path from the sourceFolder if flatten is false const resolvedDestinationPath = path.resolve(destinationFolderPath, copyConfiguration.flatten ? path.basename(sourceFilePath) : path.relative(sourceFolder, sourceFilePath)); // Throw if a duplicate copy target with a different source or options is specified const existingDestinationCopyDescriptor = copyDescriptorByDestination.get(resolvedDestinationPath); if (existingDestinationCopyDescriptor) { if (existingDestinationCopyDescriptor.sourcePath === sourceFilePath && existingDestinationCopyDescriptor.hardlink === !!copyConfiguration.hardlink) { // Found a duplicate, avoid adding again continue; } throw new Error(`Cannot copy multiple files to the same destination "${resolvedDestinationPath}".`); } // Finally, default hardlink to false, add to the result, and add to the map for deduping const processedCopyDescriptor = { sourcePath: sourceFilePath, destinationPath: resolvedDestinationPath, hardlink: !!copyConfiguration.hardlink }; copyDescriptorByDestination.set(resolvedDestinationPath, processedCopyDescriptor); } } }, { concurrency: Constants.maxParallelism }); return copyDescriptorByDestination; } async function _copyFilesInnerAsync(copyDescriptors, configHash, buildInfoPath, terminal) { if (copyDescriptors.size === 0) { return; } let oldBuildInfo = await tryReadBuildInfoAsync(buildInfoPath); if (oldBuildInfo && oldBuildInfo.configHash !== configHash) { terminal.writeVerboseLine(`File copy configuration changed, discarding incremental state.`); oldBuildInfo = undefined; } // Since in watch mode only changed files will get passed in, need to ensure that all files from // the previous build are still tracked. const inputFileVersions = new Map(oldBuildInfo === null || oldBuildInfo === void 0 ? void 0 : oldBuildInfo.inputFileVersions); const buildInfo = { configHash, inputFileVersions }; const allInputFiles = new Set(); for (const copyDescriptor of copyDescriptors.values()) { allInputFiles.add(copyDescriptor.sourcePath); } await Async.forEachAsync(allInputFiles, async (inputFilePath) => { const fileContent = await FileSystem.readFileToBufferAsync(inputFilePath); const fileHash = createHash('sha256').update(fileContent).digest('base64'); inputFileVersions.set(inputFilePath, fileHash); }, { concurrency: Constants.maxParallelism }); const copyDescriptorsWithWork = []; for (const copyDescriptor of copyDescriptors.values()) { const { sourcePath } = copyDescriptor; const sourceFileHash = inputFileVersions.get(sourcePath); if (!sourceFileHash) { throw new Error(`Missing hash for input file: ${sourcePath}`); } if ((oldBuildInfo === null || oldBuildInfo === void 0 ? void 0 : oldBuildInfo.inputFileVersions.get(sourcePath)) === sourceFileHash) { continue; } copyDescriptorsWithWork.push(copyDescriptor); } if (copyDescriptorsWithWork.length === 0) { terminal.writeLine('All requested file copy operations are up to date. Nothing to do.'); return; } let copiedFileCount = 0; let linkedFileCount = 0; await Async.forEachAsync(copyDescriptorsWithWork, async (copyDescriptor) => { if (copyDescriptor.hardlink) { linkedFileCount++; await FileSystem.createHardLinkAsync({ linkTargetPath: copyDescriptor.sourcePath, newLinkPath: copyDescriptor.destinationPath, alreadyExistsBehavior: AlreadyExistsBehavior.Overwrite }); terminal.writeVerboseLine(`Linked "${copyDescriptor.sourcePath}" to "${copyDescriptor.destinationPath}".`); } else { copiedFileCount++; await FileSystem.copyFilesAsync({ sourcePath: copyDescriptor.sourcePath, destinationPath: copyDescriptor.destinationPath, alreadyExistsBehavior: AlreadyExistsBehavior.Overwrite }); terminal.writeVerboseLine(`Copied "${copyDescriptor.sourcePath}" to "${copyDescriptor.destinationPath}".`); } }, { concurrency: Constants.maxParallelism }); terminal.writeLine(`Copied ${copiedFileCount} file${copiedFileCount === 1 ? '' : 's'} and ` + `linked ${linkedFileCount} file${linkedFileCount === 1 ? '' : 's'}`); await writeBuildInfoAsync(buildInfo, buildInfoPath); } const PLUGIN_NAME = 'copy-files-plugin'; export default class CopyFilesPlugin { apply(taskSession, heftConfiguration, pluginOptions) { taskSession.hooks.registerFileOperations.tap(PLUGIN_NAME, (operations) => { for (const operation of pluginOptions.copyOperations) { operations.copyOperations.add(operation); } return operations; }); } } //# sourceMappingURL=CopyFilesPlugin.js.map