@rushstack/heft
Advanced tools
Comparing version 0.49.0-rc.1 to 0.49.0-rc.2
@@ -90,10 +90,10 @@ import { AsyncParallelHook } from 'tapable'; | ||
/** | ||
* Glob the set of changed files and return a list of absolute paths that match the provided patterns. | ||
* Glob a set of files and return a list of paths that match the provided patterns. | ||
* | ||
* @param patterns - Glob patterns to match against. | ||
* @param options - Options that are used when globbing the set of changed files. | ||
* @param options - Options that are used when globbing the set of files. | ||
* | ||
* @public | ||
*/ | ||
export declare type GlobChangedFilesFn = (patterns: string | string[], options?: IGlobChangedFilesOptions) => string[]; | ||
export declare type GlobFn = (pattern: string | string[], options?: IGlobOptions | undefined) => Promise<string[]>; | ||
@@ -305,7 +305,7 @@ /** | ||
/** | ||
* Options that are used when globbing the set of changed files. | ||
* A supported subset of options used when globbing files. | ||
* | ||
* @public | ||
*/ | ||
export declare interface IGlobChangedFilesOptions { | ||
export declare interface IGlobOptions { | ||
/** | ||
@@ -653,3 +653,3 @@ * Current working directory that the glob pattern will be applied to. | ||
*/ | ||
readonly addCopyOperations: (...copyOperations: ICopyOperation[]) => void; | ||
readonly addCopyOperations: (copyOperations: ICopyOperation[]) => void; | ||
/** | ||
@@ -661,3 +661,3 @@ * Add delete operations to be performed during the `run` hook. These operations will be | ||
*/ | ||
readonly addDeleteOperations: (...deleteOperations: IDeleteOperation[]) => void; | ||
readonly addDeleteOperations: (deleteOperations: IDeleteOperation[]) => void; | ||
} | ||
@@ -672,4 +672,14 @@ | ||
/** | ||
* Add copy operations to be performed during the `runIncremental` hook. These operations will | ||
* be performed after the task `runIncremental` hook has completed. | ||
* | ||
* @public | ||
*/ | ||
readonly addCopyOperations: (copyOperations: IIncrementalCopyOperation[]) => void; | ||
/** | ||
* A map of changed files to the corresponding change state. This can be used to track which | ||
* files have been changed during an incremental build. | ||
* files have been changed during an incremental build. This map is populated with all changed | ||
* files, including files that are not source files. When an incremental build completes | ||
* successfully, the map is cleared and only files changed after the incremental build will be | ||
* included in the map. | ||
*/ | ||
@@ -681,3 +691,3 @@ readonly changedFiles: ReadonlyMap<string, IChangedFileState>; | ||
*/ | ||
readonly globChangedFiles: GlobChangedFilesFn; | ||
readonly globChangedFilesAsync: GlobFn; | ||
/** | ||
@@ -755,4 +765,18 @@ * A cancellation token that is used to signal that the incremental build is cancelled. This | ||
/** | ||
* Used to specify a selection of files to copy from a specific source folder to one | ||
* or more destination folders. | ||
* | ||
* @public | ||
*/ | ||
export declare interface IIncrementalCopyOperation extends ICopyOperation { | ||
/** | ||
* If true, the file will be copied only if the source file is contained in the | ||
* IHeftTaskRunIncrementalHookOptions.changedFiles map. | ||
*/ | ||
onlyIfChanged?: boolean; | ||
} | ||
/** | ||
* @public | ||
*/ | ||
export declare interface IMetricsData { | ||
@@ -759,0 +783,0 @@ /** |
@@ -18,4 +18,4 @@ import type { CommandLineAction } from '@rushstack/ts-command-line'; | ||
readonly watch: boolean; | ||
readonly selectedPhases: Set<HeftPhase>; | ||
readonly selectedPhases: ReadonlySet<HeftPhase>; | ||
} | ||
//# sourceMappingURL=IHeftAction.d.ts.map |
@@ -13,5 +13,5 @@ import { CommandLineAction } from '@rushstack/ts-command-line'; | ||
constructor(options: IPhaseActionOptions); | ||
get selectedPhases(): Set<HeftPhase>; | ||
get selectedPhases(): ReadonlySet<HeftPhase>; | ||
protected onExecute(): Promise<void>; | ||
} | ||
//# sourceMappingURL=PhaseAction.d.ts.map |
@@ -1,4 +0,13 @@ | ||
import { ScopedCommandLineAction, type CommandLineParameterProvider } from '@rushstack/ts-command-line'; | ||
import { ScopedCommandLineAction, type CommandLineParameterProvider, type CommandLineStringListParameter } from '@rushstack/ts-command-line'; | ||
import { type ITerminal } from '@rushstack/node-core-library'; | ||
import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; | ||
import type { IHeftAction, IHeftActionOptions } from './IHeftAction'; | ||
import type { HeftPhase } from '../../pluginFramework/HeftPhase'; | ||
export declare function expandPhases(onlyParameter: CommandLineStringListParameter, toParameter: CommandLineStringListParameter, toExceptParameter: CommandLineStringListParameter, internalHeftSession: InternalHeftSession, terminal: ITerminal): Set<HeftPhase>; | ||
export interface IScopingParameters { | ||
toParameter: CommandLineStringListParameter; | ||
toExceptParameter: CommandLineStringListParameter; | ||
onlyParameter: CommandLineStringListParameter; | ||
} | ||
export declare function definePhaseScopingParameters(action: IHeftAction): IScopingParameters; | ||
export declare class RunAction extends ScopedCommandLineAction implements IHeftAction { | ||
@@ -14,7 +23,6 @@ readonly watch: boolean; | ||
constructor(options: IHeftActionOptions); | ||
get selectedPhases(): Set<HeftPhase>; | ||
get selectedPhases(): ReadonlySet<HeftPhase>; | ||
protected onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; | ||
protected onExecute(): Promise<void>; | ||
private _evaluatePhaseParameter; | ||
} | ||
//# sourceMappingURL=RunAction.d.ts.map |
@@ -5,3 +5,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.RunAction = void 0; | ||
exports.RunAction = exports.definePhaseScopingParameters = exports.expandPhases = void 0; | ||
const ts_command_line_1 = require("@rushstack/ts-command-line"); | ||
@@ -12,49 +12,77 @@ const node_core_library_1 = require("@rushstack/node-core-library"); | ||
const Constants_1 = require("../../utilities/Constants"); | ||
class RunAction extends ts_command_line_1.ScopedCommandLineAction { | ||
constructor(options) { | ||
var _a; | ||
super({ | ||
actionName: `run${options.watch ? '-watch' : ''}`, | ||
documentation: `Run a provided selection of Heft phases${options.watch ? ' in watch mode.' : ''}.`, | ||
summary: `Run a provided selection of Heft phases${options.watch ? ' in watch mode.' : ''}.` | ||
}); | ||
this.watch = (_a = options.watch) !== null && _a !== void 0 ? _a : false; | ||
this._terminal = options.terminal; | ||
this._internalHeftSession = options.internalHeftSession; | ||
this._actionRunner = new HeftActionRunner_1.HeftActionRunner(Object.assign({ action: this }, options)); | ||
this._toParameter = this.defineStringListParameter({ | ||
function expandPhases(onlyParameter, toParameter, toExceptParameter, internalHeftSession, terminal) { | ||
const onlyPhases = evaluatePhaseParameter(onlyParameter, internalHeftSession, terminal); | ||
const toPhases = evaluatePhaseParameter(toParameter, internalHeftSession, terminal); | ||
const toExceptPhases = evaluatePhaseParameter(toExceptParameter, internalHeftSession, terminal); | ||
const expandFn = (phase) => phase.dependencyPhases; | ||
const selectedPhases = Selection_1.Selection.union(Selection_1.Selection.recursiveExpand(toPhases, expandFn), Selection_1.Selection.recursiveExpand(Selection_1.Selection.directDependenciesOf(toExceptPhases, expandFn), expandFn), onlyPhases); | ||
if (selectedPhases.size === 0) { | ||
throw new Error('No phases were selected. Provide at least one phase to the ' + | ||
`${JSON.stringify(Constants_1.Constants.toParameterLongName)}, ` + | ||
`${JSON.stringify(Constants_1.Constants.toExceptParameterLongName)}, or ` + | ||
`${JSON.stringify(Constants_1.Constants.onlyParameterLongName)} parameters.`); | ||
} | ||
return selectedPhases; | ||
} | ||
exports.expandPhases = expandPhases; | ||
function evaluatePhaseParameter(phaseParameter, internalHeftSession, terminal) { | ||
const parameterName = phaseParameter.longName; | ||
const selection = new Set(); | ||
for (const rawSelector of phaseParameter.values) { | ||
const phase = internalHeftSession.phasesByName.get(rawSelector); | ||
if (!phase) { | ||
terminal.writeErrorLine(`The phase name ${JSON.stringify(rawSelector)} passed to ${JSON.stringify(parameterName)} does ` + | ||
'not exist in heft.json.'); | ||
throw new node_core_library_1.AlreadyReportedError(); | ||
} | ||
selection.add(phase); | ||
} | ||
return selection; | ||
} | ||
function definePhaseScopingParameters(action) { | ||
return { | ||
toParameter: action.defineStringListParameter({ | ||
parameterLongName: Constants_1.Constants.toParameterLongName, | ||
parameterShortName: Constants_1.Constants.toParameterShortName, | ||
description: 'The phase to run to, including all transitive dependencies.', | ||
description: `The phase to ${action.actionName} to, including all transitive dependencies.`, | ||
argumentName: 'PHASE', | ||
parameterGroup: ts_command_line_1.ScopedCommandLineAction.ScopingParameterGroup | ||
}); | ||
this._toExceptParameter = this.defineStringListParameter({ | ||
}), | ||
toExceptParameter: action.defineStringListParameter({ | ||
parameterLongName: Constants_1.Constants.toExceptParameterLongName, | ||
parameterShortName: Constants_1.Constants.toExceptParameterShortName, | ||
description: 'The phase to run to (but not include), including all transitive dependencies.', | ||
description: `The phase to ${action.actionName} to (but not include), including all transitive dependencies.`, | ||
argumentName: 'PHASE', | ||
parameterGroup: ts_command_line_1.ScopedCommandLineAction.ScopingParameterGroup | ||
}); | ||
this._onlyParameter = this.defineStringListParameter({ | ||
}), | ||
onlyParameter: action.defineStringListParameter({ | ||
parameterLongName: Constants_1.Constants.onlyParameterLongName, | ||
parameterShortName: Constants_1.Constants.onlyParameterShortName, | ||
description: 'The phase to run.', | ||
description: `The phase to ${action.actionName}.`, | ||
argumentName: 'PHASE', | ||
parameterGroup: ts_command_line_1.ScopedCommandLineAction.ScopingParameterGroup | ||
}) | ||
}; | ||
} | ||
exports.definePhaseScopingParameters = definePhaseScopingParameters; | ||
class RunAction extends ts_command_line_1.ScopedCommandLineAction { | ||
constructor(options) { | ||
var _a; | ||
super({ | ||
actionName: `run${options.watch ? '-watch' : ''}`, | ||
documentation: `Run a provided selection of Heft phases${options.watch ? ' in watch mode.' : ''}.`, | ||
summary: `Run a provided selection of Heft phases${options.watch ? ' in watch mode.' : ''}.` | ||
}); | ||
this.watch = (_a = options.watch) !== null && _a !== void 0 ? _a : false; | ||
this._terminal = options.terminal; | ||
this._internalHeftSession = options.internalHeftSession; | ||
const { toParameter, toExceptParameter, onlyParameter } = definePhaseScopingParameters(this); | ||
this._toParameter = toParameter; | ||
this._toExceptParameter = toExceptParameter; | ||
this._onlyParameter = onlyParameter; | ||
this._actionRunner = new HeftActionRunner_1.HeftActionRunner(Object.assign({ action: this }, options)); | ||
} | ||
get selectedPhases() { | ||
if (!this._selectedPhases) { | ||
const toPhases = this._evaluatePhaseParameter(this._toParameter, this._terminal); | ||
const toExceptPhases = this._evaluatePhaseParameter(this._toExceptParameter, this._terminal); | ||
const onlyPhases = this._evaluatePhaseParameter(this._onlyParameter, this._terminal); | ||
const expandFn = (phase) => phase.dependencyPhases; | ||
this._selectedPhases = Selection_1.Selection.union(Selection_1.Selection.recursiveExpand(toPhases, expandFn), Selection_1.Selection.recursiveExpand(Selection_1.Selection.directDependenciesOf(toExceptPhases, expandFn), expandFn), onlyPhases); | ||
if (this._selectedPhases.size === 0) { | ||
throw new Error('No phases were selected. Provide at least one phase to the ' + | ||
`${JSON.stringify(Constants_1.Constants.toParameterLongName)}, ` + | ||
`${JSON.stringify(Constants_1.Constants.toExceptParameterLongName)}, or ` + | ||
`${JSON.stringify(Constants_1.Constants.onlyParameterLongName)} parameters.`); | ||
} | ||
this._selectedPhases = expandPhases(this._onlyParameter, this._toParameter, this._toExceptParameter, this._internalHeftSession, this._terminal); | ||
} | ||
@@ -69,18 +97,4 @@ return this._selectedPhases; | ||
} | ||
_evaluatePhaseParameter(phaseParameter, terminal) { | ||
const parameterName = phaseParameter.longName; | ||
const selection = new Set(); | ||
for (const rawSelector of phaseParameter.values) { | ||
const phase = this._internalHeftSession.phasesByName.get(rawSelector); | ||
if (!phase) { | ||
terminal.writeErrorLine(`The phase name ${JSON.stringify(rawSelector)} passed to ${JSON.stringify(parameterName)} does ` + | ||
'not exist in heft.json.'); | ||
throw new node_core_library_1.AlreadyReportedError(); | ||
} | ||
selection.add(phase); | ||
} | ||
return selection; | ||
} | ||
} | ||
exports.RunAction = RunAction; | ||
//# sourceMappingURL=RunAction.js.map |
@@ -0,7 +1,14 @@ | ||
import { type ITerminal } from '@rushstack/node-core-library'; | ||
import type { CommandLineParameterProvider } from '@rushstack/ts-command-line'; | ||
import type { HeftConfiguration } from '../configuration/HeftConfiguration'; | ||
import type { LoggingManager } from '../pluginFramework/logging/LoggingManager'; | ||
import type { MetricsCollector } from '../metrics/MetricsCollector'; | ||
import { HeftParameterManager } from '../pluginFramework/HeftParameterManager'; | ||
import type { IHeftAction, IHeftActionOptions } from '../cli/actions/IHeftAction'; | ||
import { CancellationToken } from '../pluginFramework/CancellationToken'; | ||
export interface IHeftActionRunnerOptions extends IHeftActionOptions { | ||
action: IHeftAction; | ||
} | ||
export declare function initializeHeft(heftConfiguration: HeftConfiguration, terminal: ITerminal, isVerbose: boolean): void; | ||
export declare function runWithLoggingAsync(fn: () => Promise<void>, action: IHeftAction, loggingManager: LoggingManager, terminal: ITerminal, metricsCollector: MetricsCollector, cancellationToken: CancellationToken): Promise<void>; | ||
export declare class HeftActionRunner { | ||
@@ -8,0 +15,0 @@ private readonly _action; |
@@ -43,3 +43,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.HeftActionRunner = void 0; | ||
exports.HeftActionRunner = exports.runWithLoggingAsync = exports.initializeHeft = void 0; | ||
const crypto = __importStar(require("crypto")); | ||
@@ -70,3 +70,2 @@ const path = __importStar(require("path")); | ||
const { terminal, watcher, watchOptions, git } = options; | ||
const ignoredSourceFileGlobs = Array.from(watchOptions.ignoredSourceFileGlobs); | ||
const forbiddenSourceFileGlobs = Array.from(watchOptions.forbiddenSourceFileGlobs); | ||
@@ -76,13 +75,22 @@ const changedFileStats = new Map(); | ||
const seenSourceFilePaths = new Set(); | ||
// Create a gitignore filter to test if a file is ignored by git. If it is, it will be counted | ||
// as a non-source file. If it is not, it will be counted as a source file. If git is not present, | ||
// all files will be counted as source files and must manually be ignored by providing a glob to | ||
// the ignoredSourceFileGlobs option. | ||
const isFileUnignored = (yield __await(git.tryCreateGitignoreFilterAsync())) || (() => true); | ||
let resolveFileChange; | ||
let rejectFileChange; | ||
let fileChangePromise; | ||
async function ingestFileChangesAsync(filePaths, ignoreForbidden = false) { | ||
// We can short-circuit the call to git if we already know all files have been seen. | ||
const unseenFilePaths = Selection_1.Selection.difference(filePaths, seenFilePaths); | ||
function ingestFileChanges(filePaths, ignoreForbidden = false) { | ||
const unseenFilePaths = seenFilePaths.size | ||
? Selection_1.Selection.difference(filePaths, seenFilePaths) | ||
: new Set(filePaths); | ||
if (unseenFilePaths.size) { | ||
// Determine which files are ignored or otherwise and stash them away for later. | ||
// We can perform this check in one call to git to save time. | ||
const unseenIgnoredFilePaths = await git.checkIgnore(unseenFilePaths); | ||
const unseenSourceFilePaths = Selection_1.Selection.difference(unseenFilePaths, unseenIgnoredFilePaths); | ||
const unseenIgnoredFilePaths = new Set(); | ||
const unseenSourceFilePaths = new Set(); | ||
for (const filePath of unseenFilePaths) { | ||
const fileIsUnignored = isFileUnignored(filePath); | ||
(fileIsUnignored ? unseenSourceFilePaths : unseenIgnoredFilePaths).add(filePath); | ||
} | ||
if (unseenSourceFilePaths.size) { | ||
@@ -117,13 +125,2 @@ // Use a StaticFileSystemAdapter containing only the unseen source files to determine which files | ||
} | ||
// If the unseen source file is ignored, remove it from the set of unseenSourceFiles. This file | ||
// will then be treated as a non-source file and will not trigger rebuilds. We need to convert | ||
// slashes from the globber if on Windows, since the globber will return the paths with forward | ||
// slashes. | ||
let ignoredFilePaths = fast_glob_1.default.sync(ignoredSourceFileGlobs, unseenSourceFileGlobOptions); | ||
if (IS_WINDOWS) { | ||
ignoredFilePaths = ignoredFilePaths.map(node_core_library_1.Path.convertToBackslashes); | ||
} | ||
for (const ignoredFilePath of ignoredFilePaths) { | ||
unseenSourceFilePaths.delete(ignoredFilePath); | ||
} | ||
} | ||
@@ -196,3 +193,3 @@ // Add the new files to the set of seen files | ||
// since they aren't being "changed", they're just being watched. | ||
yield __await(ingestFileChangesAsync(initialFilePaths, /*ignoreForbidden:*/ true)); | ||
ingestFileChanges(initialFilePaths, /*ignoreForbidden:*/ true); | ||
for (const filePath of initialFilePaths) { | ||
@@ -230,3 +227,3 @@ const state = Object.assign(Object.assign({}, generateChangeState(filePath)), { version: INITIAL_CHANGE_STATE }); | ||
// Process the file changes. In | ||
yield __await(ingestFileChangesAsync(fileChangesToProcess.keys())); | ||
ingestFileChanges(fileChangesToProcess.keys()); | ||
// Update the output map to contain the new file change state | ||
@@ -263,2 +260,66 @@ let containsSourceFiles = false; | ||
} | ||
function initializeHeft(heftConfiguration, terminal, isVerbose) { | ||
// Ensure that verbose is enabled on the terminal if requested. terminalProvider.verboseEnabled | ||
// should already be `true` if the `--debug` flag was provided. This is set in HeftCommandLineParser | ||
if (heftConfiguration.terminalProvider instanceof node_core_library_1.ConsoleTerminalProvider) { | ||
heftConfiguration.terminalProvider.verboseEnabled = | ||
heftConfiguration.terminalProvider.verboseEnabled || isVerbose; | ||
} | ||
// Log some information about the execution | ||
const projectPackageJson = heftConfiguration.projectPackageJson; | ||
terminal.writeVerboseLine(`Project: ${projectPackageJson.name}@${projectPackageJson.version}`); | ||
terminal.writeVerboseLine(`Project build folder: ${heftConfiguration.buildFolderPath}`); | ||
if (heftConfiguration.rigConfig.rigFound) { | ||
terminal.writeVerboseLine(`Rig package: ${heftConfiguration.rigConfig.rigPackageName}`); | ||
terminal.writeVerboseLine(`Rig profile: ${heftConfiguration.rigConfig.rigProfile}`); | ||
} | ||
terminal.writeVerboseLine(`Heft version: ${heftConfiguration.heftPackageJson.version}`); | ||
terminal.writeVerboseLine(`Node version: ${process.version}`); | ||
terminal.writeVerboseLine(''); | ||
} | ||
exports.initializeHeft = initializeHeft; | ||
async function runWithLoggingAsync(fn, action, loggingManager, terminal, metricsCollector, cancellationToken) { | ||
const startTime = perf_hooks_1.performance.now(); | ||
loggingManager.resetScopedLoggerErrorsAndWarnings(); | ||
// Execute the action operations | ||
let encounteredError = false; | ||
try { | ||
await fn(); | ||
} | ||
catch (e) { | ||
encounteredError = true; | ||
throw e; | ||
} | ||
finally { | ||
const warningStrings = loggingManager.getWarningStrings(); | ||
const errorStrings = loggingManager.getErrorStrings(); | ||
const wasCancelled = cancellationToken.isCancelled; | ||
const encounteredWarnings = warningStrings.length > 0 || wasCancelled; | ||
encounteredError = encounteredError || errorStrings.length > 0; | ||
await metricsCollector.recordAsync(action.actionName, { | ||
encounteredError | ||
}, action.getParameterStringMap()); | ||
const finishedLoggingWord = encounteredError ? 'Failed' : wasCancelled ? 'Cancelled' : 'Finished'; | ||
const duration = perf_hooks_1.performance.now() - startTime; | ||
const durationSeconds = Math.round(duration) / 1000; | ||
const finishedLoggingLine = `-------------------- ${finishedLoggingWord} (${durationSeconds}s) --------------------`; | ||
terminal.writeLine(node_core_library_1.Colors.bold((encounteredError ? node_core_library_1.Colors.red : encounteredWarnings ? node_core_library_1.Colors.yellow : node_core_library_1.Colors.green)(finishedLoggingLine))); | ||
if (warningStrings.length > 0) { | ||
terminal.writeWarningLine(`Encountered ${warningStrings.length} warning${warningStrings.length === 1 ? '' : 's'}`); | ||
for (const warningString of warningStrings) { | ||
terminal.writeWarningLine(` ${warningString}`); | ||
} | ||
} | ||
if (errorStrings.length > 0) { | ||
terminal.writeErrorLine(`Encountered ${errorStrings.length} error${errorStrings.length === 1 ? '' : 's'}`); | ||
for (const errorString of errorStrings) { | ||
terminal.writeErrorLine(` ${errorString}`); | ||
} | ||
} | ||
} | ||
if (encounteredError) { | ||
throw new node_core_library_1.AlreadyReportedError(); | ||
} | ||
} | ||
exports.runWithLoggingAsync = runWithLoggingAsync; | ||
class HeftActionRunner { | ||
@@ -345,20 +406,3 @@ constructor(options) { | ||
this._internalHeftSession.parameterManager = this.parameterManager; | ||
// Ensure that verbose is enabled on the terminal if requested. terminalProvider.verboseEnabled | ||
// should already be `true` if the `--debug` flag was provided. This is set in HeftCommandLineParser | ||
if (this._heftConfiguration.terminalProvider instanceof node_core_library_1.ConsoleTerminalProvider) { | ||
this._heftConfiguration.terminalProvider.verboseEnabled = | ||
this._heftConfiguration.terminalProvider.verboseEnabled || | ||
this.parameterManager.defaultParameters.verbose; | ||
} | ||
// Log some information about the execution | ||
const projectPackageJson = this._heftConfiguration.projectPackageJson; | ||
this._terminal.writeVerboseLine(`Project: ${projectPackageJson.name}@${projectPackageJson.version}`); | ||
this._terminal.writeVerboseLine(`Project build folder: ${this._heftConfiguration.buildFolderPath}`); | ||
if (this._heftConfiguration.rigConfig.rigFound) { | ||
this._terminal.writeVerboseLine(`Rig package: ${this._heftConfiguration.rigConfig.rigPackageName}`); | ||
this._terminal.writeVerboseLine(`Rig profile: ${this._heftConfiguration.rigConfig.rigProfile}`); | ||
} | ||
this._terminal.writeVerboseLine(`Heft version: ${this._heftConfiguration.heftPackageJson.version}`); | ||
this._terminal.writeVerboseLine(`Node version: ${process.version}`); | ||
this._terminal.writeVerboseLine(''); | ||
initializeHeft(this._heftConfiguration, this._terminal, this.parameterManager.defaultParameters.verbose); | ||
if (this._action.watch) { | ||
@@ -372,31 +416,41 @@ await this._executeWatchAsync(); | ||
async _executeWatchAsync() { | ||
const chokidarPkg = await this._ensureChokidarLoadedAsync(); | ||
// Create a watcher for the build folder which will return the initial state | ||
const watcherReadyPromise = new Promise((resolve, reject) => { | ||
const watcher = chokidarPkg.watch(this._heftConfiguration.buildFolderPath, { | ||
persistent: true, | ||
// All watcher-returned file paths will be relative to the build folder. Chokidar on Windows | ||
// has some issues with watching when not using a cwd, causing the 'ready' event to never be | ||
// emitted, so we will have to manually resolve the absolute paths in the change handler. | ||
cwd: this._heftConfiguration.buildFolderPath, | ||
// Ignore "node_modules" files and known-unimportant files | ||
ignored: ['node_modules/**'], | ||
// We will use the initial state to build a list of all watched files | ||
ignoreInitial: false, | ||
// Debounce file write events within 100 ms of each other | ||
awaitWriteFinish: { | ||
stabilityThreshold: 100 | ||
} | ||
const terminal = this._terminal; | ||
const watcherCwd = this._heftConfiguration.buildFolderPath; | ||
const watcher = await (0, TaskOperationRunner_1.runAndMeasureAsync)(async () => { | ||
const chokidarPkg = await this._ensureChokidarLoadedAsync(); | ||
const ignoreGlobs = ['node_modules'].concat(this._internalHeftSession.watchOptions.ignoredSourceFileGlobs); | ||
const watcherReadyPromise = new Promise((resolve, reject) => { | ||
const watcher = chokidarPkg.watch(this._heftConfiguration.buildFolderPath, { | ||
persistent: true, | ||
// All watcher-returned file paths will be relative to the build folder. Chokidar on Windows | ||
// has some issues with watching when not using a cwd, causing the 'ready' event to never be | ||
// emitted, so we will have to manually resolve the absolute paths in the change handler. | ||
cwd: watcherCwd, | ||
ignored: ignoreGlobs, | ||
// We use the stats object to generate the change file state, so ensure we have it in all | ||
// cases | ||
alwaysStat: true, | ||
// Prevent add/addDir events from firing during the initial crawl. We will still use the | ||
// initial state, but we will manually crawl watcher.getWatched() to get it. | ||
ignoreInitial: true, | ||
// Debounce file events within 100 ms of each other | ||
awaitWriteFinish: { | ||
stabilityThreshold: 100, | ||
pollInterval: 100 | ||
}, | ||
atomic: 100 | ||
}); | ||
// Remove all listeners once the initial state is returned | ||
watcher.on('ready', () => resolve(watcher)); | ||
watcher.on('error', (error) => reject(error)); | ||
}); | ||
// Remove all listeners once the initial state is returned | ||
watcher.on('ready', () => resolve(watcher.removeAllListeners())); | ||
watcher.on('error', (error) => reject(error)); | ||
}); | ||
const terminal = this._terminal; | ||
const watcher = await watcherReadyPromise; | ||
return await watcherReadyPromise; | ||
}, () => `Starting watcher at path "${watcherCwd}"`, () => 'Finished starting watcher', terminal.writeLine.bind(terminal)); | ||
const git = new GitUtilities_1.GitUtilities(this._heftConfiguration.buildFolderPath); | ||
const changedFiles = new Map(); | ||
const staticFileSystemAdapter = new StaticFileSystemAdapter_1.StaticFileSystemAdapter(); | ||
const globChangedFilesFn = (pattern, options) => { | ||
return fast_glob_1.default.sync(pattern, { | ||
const globChangedFilesAsyncFn = async (pattern, options) => { | ||
// Use the sync method. Since the static file system adapter operations are all done in memory, | ||
// there is no need to use the async method and we can avoid the overhead going async. | ||
return Promise.resolve(fast_glob_1.default.sync(pattern, { | ||
fs: staticFileSystemAdapter, | ||
@@ -407,20 +461,25 @@ cwd: options === null || options === void 0 ? void 0 : options.cwd, | ||
dot: options === null || options === void 0 ? void 0 : options.dot | ||
}); | ||
})); | ||
}; | ||
// Create the async iterator. This will yield void when a changed source file is encountered, giving | ||
// us a chance to kill the current build and start a new one. Await the first iteration, since the | ||
// first iteration should be for the initial state. | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const iterator = _waitForSourceChangesAsync({ | ||
terminal, | ||
watcher, | ||
git, | ||
changedFiles, | ||
staticFileSystemAdapter, | ||
watchOptions: this._internalHeftSession.watchOptions | ||
}); | ||
await iterator.next(); | ||
// us a chance to kill the current build and start a new one. | ||
const iterator = await (0, TaskOperationRunner_1.runAndMeasureAsync)(async () => { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const iterator = _waitForSourceChangesAsync({ | ||
terminal, | ||
watcher, | ||
git, | ||
changedFiles, | ||
staticFileSystemAdapter, | ||
watchOptions: this._internalHeftSession.watchOptions | ||
}); | ||
// Await the first iteration, which is used to ingest the initial state. Once we have the initial | ||
// state, then we can start listening for changes. | ||
await iterator.next(); | ||
return iterator; | ||
}, () => 'Initializing watcher state', () => 'Finished initializing watcher state', terminal.writeVerboseLine.bind(terminal)); | ||
// The file event listener is used to allow task operations to wait for a file change before | ||
// progressing to the next task. | ||
const fileEventListener = new FileEventListener_1.FileEventListener(watcher); | ||
let isFirstRun = true; | ||
// eslint-disable-next-line no-constant-condition | ||
@@ -433,3 +492,3 @@ while (true) { | ||
const sourceChangesPromise = iterator.next().then(() => true); | ||
const executePromise = this._executeOnceAsync(cancellationToken, changedFiles, globChangedFilesFn, fileEventListener).then(() => false); | ||
const executePromise = this._executeOnceAsync(isFirstRun, cancellationToken, changedFiles, globChangedFilesAsyncFn, fileEventListener).then(() => false); | ||
try { | ||
@@ -453,2 +512,5 @@ // Whichever promise settles first will be the result of the race. | ||
staticFileSystemAdapter.removeAllFiles(); | ||
// Mark the first run as completed, to ensure that copy incremental copy operations are now | ||
// enabled. | ||
isFirstRun = false; | ||
this._terminal.writeLine(node_core_library_1.Colors.bold('Waiting for changes. Press CTRL + C to exit...')); | ||
@@ -479,58 +541,16 @@ this._terminal.writeLine(''); | ||
} | ||
async _executeOnceAsync(cancellationToken, changedFiles, globChangedFilesFn, fileEventListener) { | ||
const startTime = perf_hooks_1.performance.now(); | ||
async _executeOnceAsync(isFirstRun = true, cancellationToken, changedFiles, globChangedFilesAsyncFn, fileEventListener) { | ||
cancellationToken = cancellationToken || new CancellationToken_1.CancellationToken(); | ||
this._loggingManager.resetScopedLoggerErrorsAndWarnings(); | ||
const operations = this._generateOperations(isFirstRun, cancellationToken, changedFiles, globChangedFilesAsyncFn, fileEventListener); | ||
const operationExecutionManagerOptions = { | ||
loggingManager: this._loggingManager, | ||
terminal: this._terminal, | ||
// TODO: Allow for running non-parallelized operations. | ||
parallelism: undefined | ||
}; | ||
const executionManager = new OperationExecutionManager_1.OperationExecutionManager(operations, operationExecutionManagerOptions); | ||
// Execute the action operations | ||
let encounteredError = false; | ||
const operations = this._generateOperations(cancellationToken, changedFiles, globChangedFilesFn, fileEventListener); | ||
try { | ||
const operationExecutionManagerOptions = { | ||
loggingManager: this._loggingManager, | ||
terminal: this._terminal, | ||
// TODO: Allow for running non-parallelized operations. | ||
parallelism: undefined | ||
}; | ||
const executionManager = new OperationExecutionManager_1.OperationExecutionManager(operations, operationExecutionManagerOptions); | ||
await executionManager.executeAsync(); | ||
} | ||
catch (e) { | ||
encounteredError = true; | ||
throw e; | ||
} | ||
finally { | ||
const warningStrings = this._loggingManager.getWarningStrings(); | ||
const errorStrings = this._loggingManager.getErrorStrings(); | ||
const wasCancelled = cancellationToken.isCancelled; | ||
const encounteredWarnings = warningStrings.length > 0 || wasCancelled; | ||
encounteredError = encounteredError || errorStrings.length > 0; | ||
await this._metricsCollector.recordAsync(this._action.actionName, { | ||
encounteredError | ||
}, this._action.getParameterStringMap()); | ||
const duration = perf_hooks_1.performance.now() - startTime; | ||
const finishedLoggingWord = encounteredError | ||
? 'Failed' | ||
: wasCancelled | ||
? 'Cancelled' | ||
: 'Finished'; | ||
const finishedLoggingLine = `-------------------- ${finishedLoggingWord} (${Math.round(duration) / 1000}s) --------------------`; | ||
this._terminal.writeLine(node_core_library_1.Colors.bold((encounteredError ? node_core_library_1.Colors.red : encounteredWarnings ? node_core_library_1.Colors.yellow : node_core_library_1.Colors.green)(finishedLoggingLine))); | ||
if (warningStrings.length > 0) { | ||
this._terminal.writeWarningLine(`Encountered ${warningStrings.length} warning${warningStrings.length === 1 ? '' : 's'}`); | ||
for (const warningString of warningStrings) { | ||
this._terminal.writeWarningLine(` ${warningString}`); | ||
} | ||
} | ||
if (errorStrings.length > 0) { | ||
this._terminal.writeErrorLine(`Encountered ${errorStrings.length} error${errorStrings.length === 1 ? '' : 's'}`); | ||
for (const errorString of errorStrings) { | ||
this._terminal.writeErrorLine(` ${errorString}`); | ||
} | ||
} | ||
} | ||
if (encounteredError) { | ||
throw new node_core_library_1.AlreadyReportedError(); | ||
} | ||
await runWithLoggingAsync(executionManager.executeAsync.bind(executionManager), this._action, this._loggingManager, this._terminal, this._metricsCollector, cancellationToken); | ||
} | ||
_generateOperations(cancellationToken, changedFiles, globChangedFilesFn, fileEventListener) { | ||
_generateOperations(isFirstRun, cancellationToken, changedFiles, globChangedFilesAsyncFn, fileEventListener) { | ||
const { selectedPhases } = this._action; | ||
@@ -569,3 +589,3 @@ const { defaultParameters: { clean, cleanCache } } = this.parameterManager; | ||
for (const task of phase.tasks) { | ||
const taskOperation = this._getOrCreateTaskOperation(task, operations, cancellationToken, changedFiles, globChangedFilesFn, fileEventListener); | ||
const taskOperation = this._getOrCreateTaskOperation(task, operations, isFirstRun, cancellationToken, changedFiles, globChangedFilesAsyncFn, fileEventListener); | ||
// Set the phase operation as a dependency of the task operation to ensure the phase operation runs first | ||
@@ -581,3 +601,3 @@ taskOperation.dependencies.add(phaseOperation); | ||
for (const dependencyTask of task.dependencyTasks) { | ||
taskOperation.dependencies.add(this._getOrCreateTaskOperation(dependencyTask, operations, cancellationToken, changedFiles, globChangedFilesFn, fileEventListener)); | ||
taskOperation.dependencies.add(this._getOrCreateTaskOperation(dependencyTask, operations, isFirstRun, cancellationToken, changedFiles, globChangedFilesAsyncFn, fileEventListener)); | ||
} | ||
@@ -622,3 +642,3 @@ // Set all tasks in a in a phase as dependencies of the consuming phase | ||
} | ||
_getOrCreateTaskOperation(task, operations, cancellationToken, changedFiles, globChangedFilesFn, fileEventListener) { | ||
_getOrCreateTaskOperation(task, operations, isFirstRun, cancellationToken, changedFiles, globChangedFilesAsyncFn, fileEventListener) { | ||
const key = `${task.parentPhase.phaseName}.${task.taskName}`; | ||
@@ -632,5 +652,6 @@ let operation = operations.get(key); | ||
task, | ||
isFirstRun, | ||
cancellationToken, | ||
changedFiles, | ||
globChangedFilesFn, | ||
globChangedFilesAsyncFn, | ||
fileEventListener | ||
@@ -637,0 +658,0 @@ }) |
@@ -14,2 +14,3 @@ "use strict"; | ||
const Constants_1 = require("../utilities/Constants"); | ||
const CleanAction_1 = require("./actions/CleanAction"); | ||
const PhaseAction_1 = require("./actions/PhaseAction"); | ||
@@ -78,3 +79,4 @@ const RunAction_1 = require("./actions/RunAction"); | ||
}; | ||
// Add the run action and the individual phase actions | ||
// Add the clean action, the run action, and the individual phase actions | ||
this.addAction(new CleanAction_1.CleanAction(actionOptions)); | ||
this.addAction(new RunAction_1.RunAction(actionOptions)); | ||
@@ -81,0 +83,0 @@ for (const phase of internalHeftSession.phases) { |
@@ -7,7 +7,7 @@ export { HeftConfiguration, IHeftConfigurationInitializationOptions as _IHeftConfigurationInitializationOptions } from './configuration/HeftConfiguration'; | ||
export { IHeftLifecycleSession, IHeftLifecycleHooks, IHeftLifecycleCleanHookOptions, IHeftLifecycleToolStartHookOptions, IHeftLifecycleToolFinishHookOptions } from './pluginFramework/HeftLifecycleSession'; | ||
export { IHeftTaskSession, IHeftTaskHooks, IHeftTaskRunHookOptions, IHeftTaskRunIncrementalHookOptions, IChangedFileState, IGlobChangedFilesOptions, GlobChangedFilesFn } from './pluginFramework/HeftTaskSession'; | ||
export { ICopyOperation } from './plugins/CopyFilesPlugin'; | ||
export { IHeftTaskSession, IHeftTaskHooks, IHeftTaskRunHookOptions, IHeftTaskRunIncrementalHookOptions, IChangedFileState } from './pluginFramework/HeftTaskSession'; | ||
export { ICopyOperation, IIncrementalCopyOperation } from './plugins/CopyFilesPlugin'; | ||
export { IDeleteOperation } from './plugins/DeleteFilesPlugin'; | ||
export { IRunScript, IRunScriptOptions } from './plugins/RunScriptPlugin'; | ||
export { IFileSelectionSpecifier } from './plugins/FileGlobSpecifier'; | ||
export { IFileSelectionSpecifier, IGlobOptions, GlobFn } from './plugins/FileGlobSpecifier'; | ||
export { IHeftRecordMetricsHookOptions, IMetricsData, IPerformanceData as _IPerformanceData, MetricsCollector as _MetricsCollector } from './metrics/MetricsCollector'; | ||
@@ -14,0 +14,0 @@ export { IScopedLogger } from './pluginFramework/logging/ScopedLogger'; |
@@ -67,3 +67,3 @@ "use strict"; | ||
if (deleteOperations.length) { | ||
await (0, DeleteFilesPlugin_1.deleteFilesAsync)(deleteOperations, cleanLogger); | ||
await (0, DeleteFilesPlugin_1.deleteFilesAsync)(deleteOperations, cleanLogger.terminal); | ||
} | ||
@@ -70,0 +70,0 @@ cleanLogger.terminal.writeVerboseLine(`Finished clean (${perf_hooks_1.performance.now() - startTime}ms)`); |
@@ -46,3 +46,3 @@ "use strict"; | ||
if (deleteOperations.length) { | ||
await (0, DeleteFilesPlugin_1.deleteFilesAsync)(deleteOperations, cleanLogger); | ||
await (0, DeleteFilesPlugin_1.deleteFilesAsync)(deleteOperations, cleanLogger.terminal); | ||
} | ||
@@ -49,0 +49,0 @@ cleanLogger.terminal.writeVerboseLine(`Finished clean (${perf_hooks_1.performance.now() - startTime}ms)`); |
@@ -5,13 +5,19 @@ import { OperationStatus } from '../OperationStatus'; | ||
import type { IOperationRunner, IOperationRunnerContext } from '../IOperationRunner'; | ||
import type { GlobChangedFilesFn, IChangedFileState } from '../../pluginFramework/HeftTaskSession'; | ||
import type { IChangedFileState } from '../../pluginFramework/HeftTaskSession'; | ||
import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; | ||
import type { CancellationToken } from '../../pluginFramework/CancellationToken'; | ||
import type { GlobFn } from '../../plugins/FileGlobSpecifier'; | ||
export interface ITaskOperationRunnerOptions { | ||
internalHeftSession: InternalHeftSession; | ||
task: HeftTask; | ||
isFirstRun: boolean; | ||
cancellationToken: CancellationToken; | ||
changedFiles?: Map<string, IChangedFileState>; | ||
globChangedFilesFn?: GlobChangedFilesFn; | ||
globChangedFilesAsyncFn?: GlobFn; | ||
fileEventListener?: FileEventListener; | ||
} | ||
/** | ||
* Log out a start message, run a provided function, and log out an end message | ||
*/ | ||
export declare function runAndMeasureAsync<T = void>(fn: () => Promise<T>, startMessageFn: () => string, endMessageFn: () => string, logFn: (message: string) => void): Promise<T>; | ||
export declare class TaskOperationRunner implements IOperationRunner { | ||
@@ -18,0 +24,0 @@ private readonly _options; |
@@ -28,3 +28,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.TaskOperationRunner = void 0; | ||
exports.TaskOperationRunner = exports.runAndMeasureAsync = void 0; | ||
const path = __importStar(require("path")); | ||
@@ -36,2 +36,38 @@ const perf_hooks_1 = require("perf_hooks"); | ||
const DeleteFilesPlugin_1 = require("../../plugins/DeleteFilesPlugin"); | ||
/** | ||
* Log out a start message, run a provided function, and log out an end message | ||
*/ | ||
async function runAndMeasureAsync(fn, startMessageFn, endMessageFn, logFn) { | ||
logFn(startMessageFn()); | ||
const startTime = perf_hooks_1.performance.now(); | ||
try { | ||
return await fn(); | ||
} | ||
finally { | ||
const endTime = perf_hooks_1.performance.now(); | ||
logFn(`${endMessageFn()} (${endTime - startTime}ms)`); | ||
} | ||
} | ||
exports.runAndMeasureAsync = runAndMeasureAsync; | ||
/** | ||
* Create a lockfile and wait for it to appear in the watcher. This is done to ensure that all watched | ||
* files created prior to the creation of the lockfile are ingested and available before running | ||
* subsequent tasks. | ||
*/ | ||
async function waitForLockFile(lockFileFolder, lockFileName, fileEventListener, terminal) { | ||
// Acquire the lock file and release it once the watcher has ingested it. Acquiring the lock file will | ||
// delete any existing lock file if present and create a new one. The file event listener will listen | ||
// for any event on the lock file and resolve the promise once it is seen, indicating that the watcher | ||
// has caught up to file events prior to the creation/deletion of the lock file. | ||
terminal.writeVerboseLine(`Synchronizing watcher using lock file ${JSON.stringify(lockFileName)}`); | ||
const lockFilePath = node_core_library_1.LockFile.getLockFilePath(lockFileFolder, lockFileName); | ||
const lockfileChangePromise = fileEventListener.waitForEventAsync(lockFilePath); | ||
const taskOperationLockFile = node_core_library_1.LockFile.tryAcquire(lockFileFolder, lockFileName); | ||
if (!taskOperationLockFile) { | ||
throw new node_core_library_1.InternalError(`Failed to acquire lock file ${JSON.stringify(lockFileName)}. Are multiple instances of ` + | ||
'Heft running?'); | ||
} | ||
await lockfileChangePromise; | ||
taskOperationLockFile.release(); | ||
} | ||
class TaskOperationRunner { | ||
@@ -54,3 +90,3 @@ constructor(options) { | ||
async _executeTaskAsync(taskSession) { | ||
const { cancellationToken, changedFiles, fileEventListener } = this._options; | ||
const { cancellationToken, changedFiles, globChangedFilesAsyncFn, fileEventListener, isFirstRun } = this._options; | ||
const { hooks, logger: { terminal } } = taskSession; | ||
@@ -71,68 +107,92 @@ // Exit the task early if cancellation is requested | ||
} | ||
const startTime = perf_hooks_1.performance.now(); | ||
terminal.writeVerboseLine(`Starting ${shouldRunIncremental ? 'incremental ' : ''}task execution`); | ||
// Create the options and provide a utility method to obtain paths to copy | ||
const copyOperations = []; | ||
const deleteOperations = []; | ||
const runHookOptions = { | ||
addCopyOperations: (...copyOperationsToAdd) => copyOperations.push(...copyOperationsToAdd), | ||
addDeleteOperations: (...deleteOperationsToAdd) => deleteOperations.push(...deleteOperationsToAdd) | ||
}; | ||
// Run the plugin run hook | ||
try { | ||
if (shouldRunIncremental) { | ||
const runIncrementalHookOptions = Object.assign(Object.assign({}, runHookOptions), { globChangedFiles: this._options.globChangedFilesFn, changedFiles: changedFiles, cancellationToken: cancellationToken }); | ||
await hooks.runIncremental.promise(runIncrementalHookOptions); | ||
await runAndMeasureAsync(async () => { | ||
// Create the options and provide a utility method to obtain paths to copy | ||
const copyOperations = []; | ||
const incrementalCopyOperations = []; | ||
const deleteOperations = []; | ||
const runHookOptions = { | ||
addCopyOperations: (copyOperationsToAdd) => { | ||
for (const copyOperation of copyOperationsToAdd) { | ||
copyOperations.push(copyOperation); | ||
} | ||
}, | ||
addDeleteOperations: (deleteOperationsToAdd) => { | ||
for (const deleteOperation of deleteOperationsToAdd) { | ||
deleteOperations.push(deleteOperation); | ||
} | ||
} | ||
}; | ||
// Run the plugin run hook | ||
try { | ||
if (shouldRunIncremental) { | ||
const runIncrementalHookOptions = Object.assign(Object.assign({}, runHookOptions), { addCopyOperations: (incrementalCopyOperationsToAdd) => { | ||
for (const incrementalCopyOperation of incrementalCopyOperationsToAdd) { | ||
if (incrementalCopyOperation.onlyIfChanged) { | ||
incrementalCopyOperations.push(incrementalCopyOperation); | ||
} | ||
else { | ||
copyOperations.push(incrementalCopyOperation); | ||
} | ||
} | ||
}, globChangedFilesAsync: globChangedFilesAsyncFn, changedFiles: changedFiles, cancellationToken: cancellationToken }); | ||
await hooks.runIncremental.promise(runIncrementalHookOptions); | ||
} | ||
else { | ||
await hooks.run.promise(runHookOptions); | ||
} | ||
} | ||
else { | ||
await hooks.run.promise(runHookOptions); | ||
catch (e) { | ||
// Log out using the task logger, and return an error status | ||
if (!(e instanceof node_core_library_1.AlreadyReportedError)) { | ||
taskSession.logger.emitError(e); | ||
} | ||
return OperationStatus_1.OperationStatus.Failure; | ||
} | ||
} | ||
catch (e) { | ||
// Log out using the task logger, and return an error status | ||
if (!(e instanceof node_core_library_1.AlreadyReportedError)) { | ||
taskSession.logger.emitError(e); | ||
const fileOperationPromises = []; | ||
const globExistingChangedFilesFn = async (pattern, options) => { | ||
// We expect specific options to be passed. If they aren't the provided options, we may not | ||
// find the changed files in the changedFiles map. | ||
if (!(options === null || options === void 0 ? void 0 : options.absolute)) { | ||
throw new node_core_library_1.InternalError('Options provided to globExistingChangedFilesFn were not expected.'); | ||
} | ||
const globbedChangedFiles = await globChangedFilesAsyncFn(pattern, options); | ||
// Filter out deletes, since we can't copy or delete an already deleted file | ||
return globbedChangedFiles.filter((changedFile) => { | ||
const changedFileState = changedFiles.get(changedFile); | ||
return (changedFileState === null || changedFileState === void 0 ? void 0 : changedFileState.version) !== undefined; | ||
}); | ||
}; | ||
// Copy the files if any were specified. Avoid checking the cancellation token here | ||
// since plugins may be tracking state changes and would have already considered | ||
// added copy operations as "processed" during hook execution. | ||
if (copyOperations.length) { | ||
fileOperationPromises.push((0, CopyFilesPlugin_1.copyFilesAsync)(copyOperations, taskSession.logger)); | ||
} | ||
return OperationStatus_1.OperationStatus.Failure; | ||
} | ||
// Copy the files if any were specified. Avoid checking the cancellation token here | ||
// since plugins may be tracking state changes and would have already considered | ||
// added copy operations as "processed" during hook execution. | ||
if (copyOperations.length) { | ||
await (0, CopyFilesPlugin_1.copyFilesAsync)(copyOperations, taskSession.logger); | ||
} | ||
// Delete the files if any were specified. Avoid checking the cancellation token here | ||
// for the same reasons as above. | ||
if (deleteOperations.length) { | ||
await (0, DeleteFilesPlugin_1.deleteFilesAsync)(deleteOperations, taskSession.logger); | ||
} | ||
if (taskSession.parameters.watch) { | ||
if (!fileEventListener) { | ||
// The file event listener is used to watch for changes to the lockfile. Without it, watch mode could | ||
// go out of sync. | ||
throw new node_core_library_1.InternalError('fileEventListener must be provided when watch is true'); | ||
// Also incrementally copy files if any were specified. We know that globChangedFilesAsyncFn must | ||
// exist because incremental copy operations are only available in incremental mode. | ||
if (incrementalCopyOperations.length) { | ||
fileOperationPromises.push((0, CopyFilesPlugin_1.copyIncrementalFilesAsync)(incrementalCopyOperations, globExistingChangedFilesFn, isFirstRun, taskSession.logger)); | ||
} | ||
// The task temp folder is a unique and relevant name, so re-use it for the lock file name | ||
const lockFileName = path.basename(taskSession.tempFolderPath); | ||
// Create a lockfile and wait for it to appear in the watcher. This is done to ensure that all watched | ||
// files created by the task are ingested and available before running subsequent tasks. This can | ||
// appear as a create or a change, depending on if the lockfile is dirty. | ||
terminal.writeVerboseLine(`Synchronizing watcher using lock file ${JSON.stringify(lockFileName)}`); | ||
const lockFilePath = node_core_library_1.LockFile.getLockFilePath(taskSession.tempFolderPath, lockFileName); | ||
const lockfileChangePromise = Promise.race([ | ||
fileEventListener.waitForChangeAsync(lockFilePath), | ||
fileEventListener.waitForCreateAsync(lockFilePath) | ||
]); | ||
const taskOperationLockFile = node_core_library_1.LockFile.tryAcquire(taskSession.tempFolderPath, lockFileName); | ||
if (!taskOperationLockFile) { | ||
throw new node_core_library_1.InternalError(`Failed to acquire lock file ${JSON.stringify(lockFileName)}. Are multiple instances of ` + | ||
'Heft running?'); | ||
// Delete the files if any were specified. Avoid checking the cancellation token here | ||
// for the same reasons as above. | ||
if (deleteOperations.length) { | ||
fileOperationPromises.push((0, DeleteFilesPlugin_1.deleteFilesAsync)(deleteOperations, taskSession.logger.terminal)); | ||
} | ||
await lockfileChangePromise; | ||
// We can save some time by avoiding deleting the lockfile | ||
taskOperationLockFile.release(/*deleteFile:*/ false); | ||
} | ||
const finishedWord = cancellationToken.isCancelled ? 'Cancelled' : 'Finished'; | ||
terminal.writeVerboseLine(`${finishedWord} ${shouldRunIncremental ? 'incremental ' : ''}task execution ` + | ||
`(${perf_hooks_1.performance.now() - startTime}ms)`); | ||
if (fileOperationPromises.length) { | ||
await Promise.all(fileOperationPromises); | ||
} | ||
if (taskSession.parameters.watch) { | ||
if (!fileEventListener) { | ||
// The file event listener is used to watch for changes to the lockfile. Without it, watch mode could | ||
// go out of sync. | ||
throw new node_core_library_1.InternalError('fileEventListener must be provided when watch is true'); | ||
} | ||
// The task temp folder is a unique and relevant name, so re-use it for the lock file name | ||
const lockFileName = path.basename(taskSession.tempFolderPath); | ||
await waitForLockFile(taskSession.tempFolderPath, lockFileName, fileEventListener, terminal); | ||
} | ||
}, () => `Starting ${shouldRunIncremental ? 'incremental ' : ''}task execution`, () => { | ||
const finishedWord = cancellationToken.isCancelled ? 'Cancelled' : 'Finished'; | ||
return `${finishedWord} ${shouldRunIncremental ? 'incremental ' : ''}task execution`; | ||
}, terminal.writeVerboseLine.bind(terminal)); | ||
// Even if the entire process has completed, we should mark the operation as cancelled if | ||
@@ -139,0 +199,0 @@ // cancellation has been requested. |
import { HeftTaskSession } from './HeftTaskSession'; | ||
import { HeftPluginHost } from './HeftPluginHost'; | ||
import { ScopedLogger } from './logging/ScopedLogger'; | ||
import type { IInternalHeftSessionOptions } from './InternalHeftSession'; | ||
import type { MetricsCollector } from '../metrics/MetricsCollector'; | ||
import type { ScopedLogger } from './logging/ScopedLogger'; | ||
import type { InternalHeftSession } from './InternalHeftSession'; | ||
import type { HeftPhase } from './HeftPhase'; | ||
import type { HeftTask } from './HeftTask'; | ||
import type { LoggingManager } from './logging/LoggingManager'; | ||
import type { HeftParameterManager } from './HeftParameterManager'; | ||
export interface IHeftPhaseSessionOptions extends IInternalHeftSessionOptions { | ||
export interface IHeftPhaseSessionOptions { | ||
internalHeftSession: InternalHeftSession; | ||
phase: HeftPhase; | ||
parameterManager: HeftParameterManager; | ||
} | ||
export declare class HeftPhaseSession extends HeftPluginHost { | ||
readonly loggingManager: LoggingManager; | ||
readonly metricsCollector: MetricsCollector; | ||
readonly phaseLogger: ScopedLogger; | ||
@@ -18,0 +13,0 @@ readonly cleanLogger: ScopedLogger; |
@@ -13,6 +13,5 @@ "use strict"; | ||
this._options = options; | ||
this.metricsCollector = options.metricsCollector; | ||
this.loggingManager = options.loggingManager; | ||
this.phaseLogger = this.loggingManager.requestScopedLogger(options.phase.phaseName); | ||
this.cleanLogger = this.loggingManager.requestScopedLogger(`${options.phase.phaseName}:clean`); | ||
const loggingManager = options.internalHeftSession.loggingManager; | ||
this.phaseLogger = loggingManager.requestScopedLogger(options.phase.phaseName); | ||
this.cleanLogger = loggingManager.requestScopedLogger(`${options.phase.phaseName}:clean`); | ||
} | ||
@@ -25,3 +24,3 @@ /** | ||
if (!taskSession) { | ||
taskSession = new HeftTaskSession_1.HeftTaskSession(Object.assign(Object.assign({}, this._options), { task, taskParameters: this._options.parameterManager.getParametersForPlugin(task.pluginDefinition), pluginHost: this })); | ||
taskSession = new HeftTaskSession_1.HeftTaskSession(Object.assign(Object.assign({}, this._options), { task, pluginHost: this })); | ||
this._taskSessionsByTask.set(task, taskSession); | ||
@@ -35,3 +34,3 @@ } | ||
async applyPluginsInternalAsync() { | ||
const { heftConfiguration, phase: { tasks } } = this._options; | ||
const { internalHeftSession: { heftConfiguration }, phase: { tasks } } = this._options; | ||
// Load up all plugins concurrently | ||
@@ -38,0 +37,0 @@ const loadPluginPromises = []; |
@@ -8,5 +8,6 @@ import { AsyncParallelHook } from 'tapable'; | ||
import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin'; | ||
import type { ICopyOperation } from '../plugins/CopyFilesPlugin'; | ||
import type { ICopyOperation, IIncrementalCopyOperation } from '../plugins/CopyFilesPlugin'; | ||
import type { HeftPluginHost } from './HeftPluginHost'; | ||
import type { CancellationToken } from './CancellationToken'; | ||
import type { GlobFn } from '../plugins/FileGlobSpecifier'; | ||
/** | ||
@@ -105,3 +106,3 @@ * The task session is responsible for providing session-specific information to Heft task plugins. | ||
*/ | ||
readonly addCopyOperations: (...copyOperations: ICopyOperation[]) => void; | ||
readonly addCopyOperations: (copyOperations: ICopyOperation[]) => void; | ||
/** | ||
@@ -113,3 +114,3 @@ * Add delete operations to be performed during the `run` hook. These operations will be | ||
*/ | ||
readonly addDeleteOperations: (...deleteOperations: IDeleteOperation[]) => void; | ||
readonly addDeleteOperations: (deleteOperations: IDeleteOperation[]) => void; | ||
} | ||
@@ -147,46 +148,20 @@ /** | ||
/** | ||
* Options that are used when globbing the set of changed files. | ||
* Options provided to the 'runIncremental' hook. | ||
* | ||
* @public | ||
*/ | ||
export interface IGlobChangedFilesOptions { | ||
export interface IHeftTaskRunIncrementalHookOptions extends IHeftTaskRunHookOptions { | ||
/** | ||
* Current working directory that the glob pattern will be applied to. | ||
*/ | ||
cwd?: string; | ||
/** | ||
* Whether or not the returned file paths should be absolute. | ||
* Add copy operations to be performed during the `runIncremental` hook. These operations will | ||
* be performed after the task `runIncremental` hook has completed. | ||
* | ||
* @defaultValue false | ||
* @public | ||
*/ | ||
absolute?: boolean; | ||
readonly addCopyOperations: (copyOperations: IIncrementalCopyOperation[]) => void; | ||
/** | ||
* Patterns to ignore when globbing. | ||
*/ | ||
ignore?: string[]; | ||
/** | ||
* Whether or not to include dot files when globbing. | ||
* | ||
* @defaultValue false | ||
*/ | ||
dot?: boolean; | ||
} | ||
/** | ||
* Glob the set of changed files and return a list of absolute paths that match the provided patterns. | ||
* | ||
* @param patterns - Glob patterns to match against. | ||
* @param options - Options that are used when globbing the set of changed files. | ||
* | ||
* @public | ||
*/ | ||
export declare type GlobChangedFilesFn = (patterns: string | string[], options?: IGlobChangedFilesOptions) => string[]; | ||
/** | ||
* Options provided to the 'runIncremental' hook. | ||
* | ||
* @public | ||
*/ | ||
export interface IHeftTaskRunIncrementalHookOptions extends IHeftTaskRunHookOptions { | ||
/** | ||
* A map of changed files to the corresponding change state. This can be used to track which | ||
* files have been changed during an incremental build. | ||
* files have been changed during an incremental build. This map is populated with all changed | ||
* files, including files that are not source files. When an incremental build completes | ||
* successfully, the map is cleared and only files changed after the incremental build will be | ||
* included in the map. | ||
*/ | ||
@@ -198,3 +173,3 @@ readonly changedFiles: ReadonlyMap<string, IChangedFileState>; | ||
*/ | ||
readonly globChangedFiles: GlobChangedFilesFn; | ||
readonly globChangedFilesAsync: GlobFn; | ||
/** | ||
@@ -211,13 +186,12 @@ * A cancellation token that is used to signal that the incremental build is cancelled. This | ||
task: HeftTask; | ||
taskParameters: IHeftParameters; | ||
pluginHost: HeftPluginHost; | ||
} | ||
export declare class HeftTaskSession implements IHeftTaskSession { | ||
private _pluginHost; | ||
readonly taskName: string; | ||
readonly hooks: IHeftTaskHooks; | ||
readonly parameters: IHeftParameters; | ||
readonly cacheFolderPath: string; | ||
readonly tempFolderPath: string; | ||
readonly logger: IScopedLogger; | ||
private readonly _options; | ||
private _parameters; | ||
/** | ||
@@ -227,2 +201,3 @@ * @internal | ||
readonly metricsCollector: MetricsCollector; | ||
get parameters(): IHeftParameters; | ||
constructor(options: IHeftTaskSessionOptions); | ||
@@ -229,0 +204,0 @@ requestAccessToPluginByName<T extends object>(pluginToAccessPackage: string, pluginToAccessName: string, pluginApply: (pluginAccessor: T) => void): void; |
@@ -33,3 +33,3 @@ "use strict"; | ||
constructor(options) { | ||
const { heftConfiguration: { cacheFolderPath: cacheFolder, tempFolderPath: tempFolder }, loggingManager, metricsCollector, phase, task, taskParameters, pluginHost } = options; | ||
const { internalHeftSession: { heftConfiguration: { cacheFolderPath: cacheFolder, tempFolderPath: tempFolder }, loggingManager, metricsCollector }, phase, task } = options; | ||
this.logger = loggingManager.requestScopedLogger(`${phase.phaseName}:${task.taskName}`); | ||
@@ -42,3 +42,2 @@ this.metricsCollector = metricsCollector; | ||
}; | ||
this.parameters = taskParameters; | ||
// Guranteed to be unique since phases are uniquely named, tasks are uniquely named within | ||
@@ -54,6 +53,15 @@ // phases, and neither can have '.' in their names. We will also use the phase name and | ||
this.tempFolderPath = path.join(tempFolder, uniqueTaskFolderName); | ||
this._pluginHost = pluginHost; | ||
this._options = options; | ||
} | ||
get parameters() { | ||
// Delay loading the parameters for the task until they're actually needed | ||
if (!this._parameters) { | ||
const parameterManager = this._options.internalHeftSession.parameterManager; | ||
const task = this._options.task; | ||
this._parameters = parameterManager.getParametersForPlugin(task.pluginDefinition); | ||
} | ||
return this._parameters; | ||
} | ||
requestAccessToPluginByName(pluginToAccessPackage, pluginToAccessName, pluginApply) { | ||
this._pluginHost.requestAccessToPluginByName(this.taskName, pluginToAccessPackage, pluginToAccessName, pluginApply); | ||
this._options.pluginHost.requestAccessToPluginByName(this.taskName, pluginToAccessPackage, pluginToAccessName, pluginApply); | ||
} | ||
@@ -60,0 +68,0 @@ } |
@@ -17,3 +17,3 @@ "use strict"; | ||
} | ||
const FORBIDDEN_SOURCE_FILE_GLOBS = ['package.json', 'config/**/*', '.rush/**/*']; | ||
const FORBIDDEN_SOURCE_FILE_GLOBS = ['package.json', '.gitingore', 'config/**/*', '.rush/**/*']; | ||
class InternalHeftSession { | ||
@@ -82,10 +82,3 @@ constructor(heftConfigurationJson, options) { | ||
if (!phaseSession) { | ||
phaseSession = new HeftPhaseSession_1.HeftPhaseSession({ | ||
debug: this.debug, | ||
heftConfiguration: this.heftConfiguration, | ||
loggingManager: this.loggingManager, | ||
metricsCollector: this.metricsCollector, | ||
parameterManager: this.parameterManager, | ||
phase | ||
}); | ||
phaseSession = new HeftPhaseSession_1.HeftPhaseSession({ internalHeftSession: this, phase }); | ||
this._phaseSessionsByPhase.set(phase, phaseSession); | ||
@@ -92,0 +85,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { type IFileSelectionSpecifier } from './FileGlobSpecifier'; | ||
import { type GlobFn, type IFileSelectionSpecifier } from './FileGlobSpecifier'; | ||
import type { HeftConfiguration } from '../configuration/HeftConfiguration'; | ||
@@ -34,2 +34,15 @@ import type { IHeftTaskPlugin } from '../pluginFramework/IHeftPlugin'; | ||
} | ||
/** | ||
* Used to specify a selection of files to copy from a specific source folder to one | ||
* or more destination folders. | ||
* | ||
* @public | ||
*/ | ||
export interface IIncrementalCopyOperation extends ICopyOperation { | ||
/** | ||
* If true, the file will be copied only if the source file is contained in the | ||
* IHeftTaskRunIncrementalHookOptions.changedFiles map. | ||
*/ | ||
onlyIfChanged?: boolean; | ||
} | ||
interface ICopyFilesPluginOptions { | ||
@@ -39,2 +52,3 @@ copyOperations: ICopyOperation[]; | ||
export declare function copyFilesAsync(copyOperations: ICopyOperation[], logger: IScopedLogger): Promise<void>; | ||
export declare function copyIncrementalFilesAsync(copyOperations: ICopyOperation[], globChangedFilesAsyncFn: GlobFn, isFirstRun: boolean, logger: IScopedLogger): Promise<void>; | ||
export default class CopyFilesPlugin implements IHeftTaskPlugin<ICopyFilesPluginOptions> { | ||
@@ -41,0 +55,0 @@ apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration, pluginOptions: ICopyFilesPluginOptions): void; |
@@ -27,5 +27,9 @@ "use strict"; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.copyFilesAsync = void 0; | ||
exports.copyIncrementalFilesAsync = exports.copyFilesAsync = void 0; | ||
const path = __importStar(require("path")); | ||
const fast_glob_1 = __importDefault(require("fast-glob")); | ||
const node_core_library_1 = require("@rushstack/node-core-library"); | ||
@@ -35,7 +39,14 @@ const Constants_1 = require("../utilities/Constants"); | ||
async function copyFilesAsync(copyOperations, logger) { | ||
const copyDescriptors = await _getCopyDescriptorsAsync(copyOperations); | ||
const copyDescriptors = await _getCopyDescriptorsAsync(copyOperations, fast_glob_1.default); | ||
await _copyFilesInnerAsync(copyDescriptors, logger); | ||
} | ||
exports.copyFilesAsync = copyFilesAsync; | ||
async function _getCopyDescriptorsAsync(copyConfigurations) { | ||
async function copyIncrementalFilesAsync(copyOperations, globChangedFilesAsyncFn, isFirstRun, logger) { | ||
const copyDescriptors = await _getCopyDescriptorsAsync(copyOperations, | ||
// Use the normal globber if it is the first run, to ensure that non-watched files are copied | ||
isFirstRun ? fast_glob_1.default : globChangedFilesAsyncFn); | ||
await _copyFilesInnerAsync(copyDescriptors, logger); | ||
} | ||
exports.copyIncrementalFilesAsync = copyIncrementalFilesAsync; | ||
async function _getCopyDescriptorsAsync(copyConfigurations, globFn) { | ||
const processedCopyDescriptors = []; | ||
@@ -57,3 +68,3 @@ // Create a map to deduplicate and prevent double-writes | ||
try { | ||
sourceFilePaths = await (0, FileGlobSpecifier_1.getFilePathsAsync)(Object.assign(Object.assign({}, copyConfiguration), { sourcePath: sourceFolder, includeGlobs: [`${path.basename(copyConfiguration.sourcePath)}/**/*`] })); | ||
sourceFilePaths = await (0, FileGlobSpecifier_1.getFilePathsAsync)(Object.assign(Object.assign({}, copyConfiguration), { sourcePath: sourceFolder, includeGlobs: [`${path.basename(copyConfiguration.sourcePath)}/**/*`] }), globFn); | ||
} | ||
@@ -77,3 +88,3 @@ catch (error) { | ||
sourceFolder = copyConfiguration.sourcePath; | ||
sourceFilePaths = await (0, FileGlobSpecifier_1.getFilePathsAsync)(copyConfiguration); | ||
sourceFilePaths = await (0, FileGlobSpecifier_1.getFilePathsAsync)(copyConfiguration, globFn); | ||
} | ||
@@ -160,27 +171,8 @@ // Dedupe and throw if a double-write is detected | ||
taskSession.hooks.run.tapPromise(PLUGIN_NAME, async (runOptions) => { | ||
await copyFilesAsync(pluginOptions.copyOperations, taskSession.logger); | ||
runOptions.addCopyOperations(pluginOptions.copyOperations); | ||
}); | ||
const impactedFileStates = new Map(); | ||
taskSession.hooks.runIncremental.tapPromise(PLUGIN_NAME, async (runIncrementalOptions) => { | ||
// TODO: Allow the copy descriptors to be resolved from a static list of files so | ||
// that we don't have to query the file system for each copy operation | ||
const copyDescriptors = await _getCopyDescriptorsAsync(pluginOptions.copyOperations); | ||
const incrementalCopyDescriptors = []; | ||
// Cycle through the copy descriptors and check for incremental changes | ||
for (const copyDescriptor of copyDescriptors) { | ||
const changedFileState = runIncrementalOptions.changedFiles.get(copyDescriptor.sourcePath); | ||
// We only care if the file has changed, ignore if not found or deleted | ||
if (changedFileState && changedFileState.version) { | ||
const impactedFileState = impactedFileStates.get(copyDescriptor.sourcePath); | ||
if (!impactedFileState || impactedFileState.version !== changedFileState.version) { | ||
// If we haven't seen this file before or it's version has changed, copy it | ||
incrementalCopyDescriptors.push(copyDescriptor); | ||
} | ||
} | ||
} | ||
await _copyFilesInnerAsync(incrementalCopyDescriptors, taskSession.logger); | ||
// Update the copied file states with the new versions | ||
for (const copyDescriptor of incrementalCopyDescriptors) { | ||
impactedFileStates.set(copyDescriptor.sourcePath, runIncrementalOptions.changedFiles.get(copyDescriptor.sourcePath)); | ||
} | ||
runIncrementalOptions.addCopyOperations(pluginOptions.copyOperations.map((copyOperation) => { | ||
return Object.assign(Object.assign({}, copyOperation), { onlyIfChanged: true }); | ||
})); | ||
}); | ||
@@ -187,0 +179,0 @@ } |
@@ -0,1 +1,2 @@ | ||
import { ITerminal } from '@rushstack/node-core-library'; | ||
import { type IFileSelectionSpecifier } from './FileGlobSpecifier'; | ||
@@ -5,3 +6,2 @@ import type { HeftConfiguration } from '../configuration/HeftConfiguration'; | ||
import type { IHeftTaskSession } from '../pluginFramework/HeftTaskSession'; | ||
import type { IScopedLogger } from '../pluginFramework/logging/ScopedLogger'; | ||
/** | ||
@@ -17,3 +17,3 @@ * Used to specify a selection of source files to delete from the specified source folder. | ||
} | ||
export declare function deleteFilesAsync(deleteOperations: IDeleteOperation[], logger: IScopedLogger): Promise<void>; | ||
export declare function deleteFilesAsync(deleteOperations: IDeleteOperation[], terminal: ITerminal): Promise<void>; | ||
export default class DeleteFilesPlugin implements IHeftTaskPlugin<IDeleteFilesPluginOptions> { | ||
@@ -20,0 +20,0 @@ apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration, pluginOptions: IDeleteFilesPluginOptions): void; |
@@ -27,5 +27,9 @@ "use strict"; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.deleteFilesAsync = void 0; | ||
const path = __importStar(require("path")); | ||
const fast_glob_1 = __importDefault(require("fast-glob")); | ||
const node_core_library_1 = require("@rushstack/node-core-library"); | ||
@@ -46,3 +50,3 @@ const Constants_1 = require("../utilities/Constants"); | ||
// Glob the files under the source path and add them to the set of files to delete | ||
const sourceFilePaths = await (0, FileGlobSpecifier_1.getFilePathsAsync)(deleteOperation); | ||
const sourceFilePaths = await (0, FileGlobSpecifier_1.getFilePathsAsync)(deleteOperation, fast_glob_1.default); | ||
for (const sourceFilePath of sourceFilePaths) { | ||
@@ -55,8 +59,8 @@ pathsToDelete.add(sourceFilePath); | ||
} | ||
async function deleteFilesAsync(deleteOperations, logger) { | ||
async function deleteFilesAsync(deleteOperations, terminal) { | ||
const pathsToDelete = await _getPathsToDeleteAsync(deleteOperations); | ||
await _deleteFilesInnerAsync(pathsToDelete, logger); | ||
await _deleteFilesInnerAsync(pathsToDelete, terminal); | ||
} | ||
exports.deleteFilesAsync = deleteFilesAsync; | ||
async function _deleteFilesInnerAsync(pathsToDelete, logger) { | ||
async function _deleteFilesInnerAsync(pathsToDelete, terminal) { | ||
let deletedFiles = 0; | ||
@@ -67,3 +71,3 @@ let deletedFolders = 0; | ||
await node_core_library_1.FileSystem.deleteFileAsync(pathToDelete, { throwIfNotExists: true }); | ||
logger.terminal.writeVerboseLine(`Deleted "${pathToDelete}".`); | ||
terminal.writeVerboseLine(`Deleted "${pathToDelete}".`); | ||
deletedFiles++; | ||
@@ -79,3 +83,3 @@ } | ||
await node_core_library_1.FileSystem.deleteFolderAsync(pathToDelete); | ||
logger.terminal.writeVerboseLine(`Deleted folder "${pathToDelete}".`); | ||
terminal.writeVerboseLine(`Deleted folder "${pathToDelete}".`); | ||
deletedFolders++; | ||
@@ -90,3 +94,3 @@ } | ||
if (deletedFiles > 0 || deletedFolders > 0) { | ||
logger.terminal.writeLine(`Deleted ${deletedFiles} file${deletedFiles !== 1 ? 's' : ''} ` + | ||
terminal.writeLine(`Deleted ${deletedFiles} file${deletedFiles !== 1 ? 's' : ''} ` + | ||
`and ${deletedFolders} folder${deletedFolders !== 1 ? 's' : ''}`); | ||
@@ -108,3 +112,3 @@ } | ||
taskSession.hooks.run.tapPromise(PLUGIN_NAME, async (runOptions) => { | ||
await deleteFilesAsync(pluginOptions.deleteOperations, taskSession.logger); | ||
runOptions.addDeleteOperations(pluginOptions.deleteOperations); | ||
}); | ||
@@ -111,0 +115,0 @@ } |
@@ -29,3 +29,39 @@ /** | ||
} | ||
export declare function getFilePathsAsync(fileGlobSpecifier: IFileSelectionSpecifier): Promise<Set<string>>; | ||
/** | ||
* A supported subset of options used when globbing files. | ||
* | ||
* @public | ||
*/ | ||
export interface IGlobOptions { | ||
/** | ||
* Current working directory that the glob pattern will be applied to. | ||
*/ | ||
cwd?: string; | ||
/** | ||
* Whether or not the returned file paths should be absolute. | ||
* | ||
* @defaultValue false | ||
*/ | ||
absolute?: boolean; | ||
/** | ||
* Patterns to ignore when globbing. | ||
*/ | ||
ignore?: string[]; | ||
/** | ||
* Whether or not to include dot files when globbing. | ||
* | ||
* @defaultValue false | ||
*/ | ||
dot?: boolean; | ||
} | ||
/** | ||
* Glob a set of files and return a list of paths that match the provided patterns. | ||
* | ||
* @param patterns - Glob patterns to match against. | ||
* @param options - Options that are used when globbing the set of files. | ||
* | ||
* @public | ||
*/ | ||
export declare type GlobFn = (pattern: string | string[], options?: IGlobOptions | undefined) => Promise<string[]>; | ||
export declare function getFilePathsAsync(fileGlobSpecifier: IFileSelectionSpecifier, globFn: GlobFn): Promise<Set<string>>; | ||
//# sourceMappingURL=FileGlobSpecifier.d.ts.map |
@@ -10,8 +10,7 @@ "use strict"; | ||
const fast_glob_1 = __importDefault(require("fast-glob")); | ||
async function getFilePathsAsync(fileGlobSpecifier) { | ||
return new Set(await (0, fast_glob_1.default)(getIncludedGlobPatterns(fileGlobSpecifier), { | ||
async function getFilePathsAsync(fileGlobSpecifier, globFn) { | ||
return new Set(await globFn(getIncludedGlobPatterns(fileGlobSpecifier), { | ||
cwd: fileGlobSpecifier.sourcePath, | ||
ignore: fileGlobSpecifier.excludeGlobs, | ||
dot: true, | ||
onlyFiles: true, | ||
absolute: true | ||
@@ -18,0 +17,0 @@ })); |
@@ -12,16 +12,7 @@ import type * as chokidar from 'chokidar'; | ||
/** | ||
* Wait for the file to be created at the specified path. | ||
* Wait for any file event at the specified path. | ||
*/ | ||
waitForCreateAsync(filePath: string): Promise<void>; | ||
/** | ||
* Wait for the file to change at the specified path. | ||
*/ | ||
waitForChangeAsync(filePath: string): Promise<void>; | ||
/** | ||
* Wait for the file to be deleted at the specified path. | ||
*/ | ||
waitForDeleteAsync(filePath: string): Promise<void>; | ||
private _waitForFileEventAsync; | ||
waitForEventAsync(filePath: string): Promise<void>; | ||
private _handleEvent; | ||
} | ||
//# sourceMappingURL=FileEventListener.d.ts.map |
@@ -37,3 +37,3 @@ "use strict"; | ||
constructor(watcher) { | ||
// File path -> Event name -> Promise resolver pair | ||
// File path -> Promise resolver pair | ||
this._watchedFileEventPromisesByPath = new Map(); | ||
@@ -45,28 +45,13 @@ this._watchPath = watcher.options.cwd; | ||
/** | ||
* Wait for the file to be created at the specified path. | ||
* Wait for any file event at the specified path. | ||
*/ | ||
async waitForCreateAsync(filePath) { | ||
await this._waitForFileEventAsync('add', filePath); | ||
} | ||
/** | ||
* Wait for the file to change at the specified path. | ||
*/ | ||
async waitForChangeAsync(filePath) { | ||
await this._waitForFileEventAsync('change', filePath); | ||
} | ||
/** | ||
* Wait for the file to be deleted at the specified path. | ||
*/ | ||
async waitForDeleteAsync(filePath) { | ||
await this._waitForFileEventAsync('unlink', filePath); | ||
} | ||
async _waitForFileEventAsync(eventName, filePath) { | ||
// Check to see if we're already waiting for this file | ||
let promisesByEventName = this._watchedFileEventPromisesByPath.get(filePath); | ||
if (!promisesByEventName) { | ||
promisesByEventName = new Map(); | ||
this._watchedFileEventPromisesByPath.set(filePath, promisesByEventName); | ||
async waitForEventAsync(filePath) { | ||
if (!path.isAbsolute(filePath)) { | ||
throw new Error(`Provided path must be absolute: "${filePath}"`); | ||
} | ||
if (this._watchPath) { | ||
filePath = path.relative(this._watchPath, filePath); | ||
} | ||
// Check to see if we're already waiting for this event, and create a new promise if not | ||
let { promise } = promisesByEventName.get(eventName) || {}; | ||
let { promise } = this._watchedFileEventPromisesByPath.get(filePath) || {}; | ||
if (!promise) { | ||
@@ -77,21 +62,13 @@ let resolveFn; | ||
}); | ||
promisesByEventName.set(eventName, { promise, resolveFn: resolveFn }); | ||
this._watchedFileEventPromisesByPath.set(filePath, { promise, resolveFn: resolveFn }); | ||
} | ||
return promise; | ||
} | ||
_handleEvent(eventName, relativePath) { | ||
// Path will be relative only when the watch path is specified, otherwise it should be an absolute path | ||
const filePath = this._watchPath ? path.join(this._watchPath, relativePath) : relativePath; | ||
const promisesByEventName = this._watchedFileEventPromisesByPath.get(filePath); | ||
if (!promisesByEventName) { | ||
return; | ||
_handleEvent(eventName, filePath) { | ||
const promiseResolverPair = this._watchedFileEventPromisesByPath.get(filePath); | ||
if (promiseResolverPair) { | ||
// Clean up references to the waiter and resolve the function | ||
this._watchedFileEventPromisesByPath.delete(filePath); | ||
promiseResolverPair.resolveFn(); | ||
} | ||
// Check to see if we were waiting for this event | ||
const { resolveFn } = promisesByEventName.get(eventName) || {}; | ||
if (!resolveFn) { | ||
return; | ||
} | ||
// Clean up references to the waiter and resolve the function | ||
promisesByEventName.delete(eventName); | ||
resolveFn(); | ||
} | ||
@@ -98,0 +75,0 @@ } |
@@ -7,4 +7,6 @@ import { GitRepoInfo as IGitRepoInfo } from 'git-repo-info'; | ||
} | ||
export declare type GitignoreFilterFn = (filePath: string) => boolean; | ||
export declare class GitUtilities { | ||
private readonly _workingDirectory; | ||
private _ignoreMatcherByGitignoreFolder; | ||
private _gitPath; | ||
@@ -38,5 +40,9 @@ private _gitInfo; | ||
/** | ||
* Runs the `git check-ignore` command and returns the result. | ||
* Returns an asynchronous filter function which can be used to filter out files that are ignored by Git. | ||
*/ | ||
checkIgnore(filePaths: Iterable<string>): Promise<Set<string>>; | ||
tryCreateGitignoreFilterAsync(): Promise<GitignoreFilterFn | undefined>; | ||
private _findIgnoreMatcherForFilePath; | ||
private _getIgnoreMatchersAsync; | ||
private _tryReadGitIgnoreFileAsync; | ||
private _findUnignoredFilesAsync; | ||
private _executeGitCommandAndCaptureOutputAsync; | ||
@@ -43,0 +49,0 @@ private _getGitPathOrThrow; |
@@ -35,2 +35,5 @@ "use strict"; | ||
const node_core_library_1 = require("@rushstack/node-core-library"); | ||
const ignore_1 = __importDefault(require("ignore")); | ||
// Matches lines starting with "#" and whitepace lines | ||
const GITIGNORE_IGNORABLE_LINE_REGEX = /^(?:(?:#.*)|(?:\s+))$/; | ||
const UNINITIALIZED = 'UNINITIALIZED'; | ||
@@ -114,73 +117,220 @@ class GitUtilities { | ||
/** | ||
* Runs the `git check-ignore` command and returns the result. | ||
* Returns an asynchronous filter function which can be used to filter out files that are ignored by Git. | ||
*/ | ||
async checkIgnore(filePaths) { | ||
this._ensureGitMinimumVersion({ major: 2, minor: 18, patch: 0 }); | ||
this._ensurePathIsUnderGitWorkingTree(); | ||
const stdinArgs = []; | ||
for (const filePath of filePaths) { | ||
stdinArgs.push(filePath); | ||
async tryCreateGitignoreFilterAsync() { | ||
var _a; | ||
let gitInfo; | ||
if (!this.isGitPresent() || !((_a = (gitInfo = this.getGitInfo())) === null || _a === void 0 ? void 0 : _a.sha)) { | ||
return; | ||
} | ||
const result = await this._executeGitCommandAndCaptureOutputAsync({ | ||
command: 'check-ignore', | ||
args: ['--stdin'], | ||
stdinArgs | ||
}); | ||
// 0 = one or more are ignored, 1 = none are ignored, 128 = fatal error | ||
// Treat all non-0 and non-1 exit codes as fatal errors. | ||
// See: https://git-scm.com/docs/git-check-ignore | ||
if (result.exitCode !== 0 && result.exitCode !== 1) { | ||
throw new Error(`The "git check-ignore" command failed with status ${result.exitCode}: ` + | ||
result.errorLines.join('\n')); | ||
const gitRepoRootPath = gitInfo.root; | ||
const ignoreMatcherMap = await this._getIgnoreMatchersAsync(gitRepoRootPath); | ||
const matcherFiltersByMatcher = new Map(); | ||
return (filePath) => { | ||
const matcher = this._findIgnoreMatcherForFilePath(filePath, ignoreMatcherMap); | ||
let matcherFilter = matcherFiltersByMatcher.get(matcher); | ||
if (!matcherFilter) { | ||
matcherFilter = matcher.createFilter(); | ||
matcherFiltersByMatcher.set(matcher, matcherFilter); | ||
} | ||
// Now that we have the matcher, we can finally check to see if the file is ignored. We need to use | ||
// the path relative to the git repo root, since all produced matchers are relative to the git repo | ||
// root. Additionally, the ignore library expects relative paths to be sourced from the path library, | ||
// so use path.relative() to ensure the path is correctly normalized. | ||
const relativeFilePath = path.relative(gitRepoRootPath, filePath); | ||
return matcherFilter(relativeFilePath); | ||
}; | ||
} | ||
_findIgnoreMatcherForFilePath(filePath, ignoreMatcherMap) { | ||
if (!path.isAbsolute(filePath)) { | ||
throw new Error(`The filePath must be an absolute path: "${filePath}"`); | ||
} | ||
// Backslashes are escaped in the output when surrounded by quotes, so trim the quotes unescape them | ||
const unescapedOutput = new Set(); | ||
for (const outputLine of result.outputLines) { | ||
let unescapedOutputLine = outputLine; | ||
if (outputLine.startsWith('"') && outputLine.endsWith('"')) { | ||
const trimmedQuotesOutputLine = outputLine.substring(1, outputLine.length - 1); | ||
unescapedOutputLine = trimmedQuotesOutputLine.replace(/\\\\/g, '\\'); | ||
const normalizedFilePath = node_core_library_1.Path.convertToSlashes(filePath); | ||
// Find the best matcher for the file path by finding the longest matcher path that is a prefix to | ||
// the file path | ||
// TODO: Use LookupByPath to make this more efficient. Currently not possible because LookupByPath | ||
// does not have support for leaf node traversal. | ||
let longestMatcherPath; | ||
let foundMatcher; | ||
for (const [matcherPath, matcher] of ignoreMatcherMap) { | ||
if (normalizedFilePath.startsWith(matcherPath) && | ||
matcherPath.length > ((longestMatcherPath === null || longestMatcherPath === void 0 ? void 0 : longestMatcherPath.length) || 0)) { | ||
longestMatcherPath = matcherPath; | ||
foundMatcher = matcher; | ||
} | ||
unescapedOutput.add(unescapedOutputLine); | ||
} | ||
return unescapedOutput; | ||
if (!foundMatcher) { | ||
throw new node_core_library_1.InternalError(`Unable to find a gitignore matcher for "${filePath}"`); | ||
} | ||
return foundMatcher; | ||
} | ||
async _getIgnoreMatchersAsync(gitRepoRootPath) { | ||
// Return early if we've already parsed the .gitignore matchers | ||
if (this._ignoreMatcherByGitignoreFolder !== undefined) { | ||
return this._ignoreMatcherByGitignoreFolder; | ||
} | ||
else { | ||
this._ignoreMatcherByGitignoreFolder = new Map(); | ||
} | ||
// Store the raw loaded ignore patterns in a map, keyed by the directory they were loaded from | ||
const rawIgnorePatternsByGitignoreFolder = new Map(); | ||
// Load the .gitignore files for the working directory and all parent directories. We can loop through | ||
// and compare the currentPath length to the gitRepoRootPath length because we know the currentPath | ||
// must be under the gitRepoRootPath | ||
const normalizedWorkingDirectory = node_core_library_1.Path.convertToSlashes(this._workingDirectory); | ||
let currentPath = normalizedWorkingDirectory; | ||
while (currentPath.length >= gitRepoRootPath.length) { | ||
const gitIgnoreFilePath = `${currentPath}/.gitignore`; | ||
const gitIgnorePatterns = await this._tryReadGitIgnoreFileAsync(gitIgnoreFilePath); | ||
if (gitIgnorePatterns) { | ||
rawIgnorePatternsByGitignoreFolder.set(currentPath, gitIgnorePatterns); | ||
} | ||
currentPath = currentPath.slice(0, currentPath.lastIndexOf('/')); | ||
} | ||
// Load the .gitignore files for all subdirectories | ||
const gitignoreRelativeFilePaths = await this._findUnignoredFilesAsync('*.gitignore'); | ||
for (const gitignoreRelativeFilePath of gitignoreRelativeFilePaths) { | ||
const gitignoreFilePath = `${normalizedWorkingDirectory}/${gitignoreRelativeFilePath}`; | ||
const gitIgnorePatterns = await this._tryReadGitIgnoreFileAsync(gitignoreFilePath); | ||
if (gitIgnorePatterns) { | ||
const parentPath = gitignoreFilePath.slice(0, gitignoreFilePath.lastIndexOf('/')); | ||
rawIgnorePatternsByGitignoreFolder.set(parentPath, gitIgnorePatterns); | ||
} | ||
} | ||
// Create the ignore matchers for each found .gitignore file | ||
for (const gitIgnoreParentPath of rawIgnorePatternsByGitignoreFolder.keys()) { | ||
let ignoreMatcherPatterns = []; | ||
currentPath = gitIgnoreParentPath; | ||
// Travel up the directory tree, adding the ignore patterns from each .gitignore file | ||
while (currentPath.length >= gitRepoRootPath.length) { | ||
// Get the root-relative path of the .gitignore file directory. Replace backslashes with forward | ||
// slashes if backslashes are the system default path separator, since gitignore patterns use | ||
// forward slashes. | ||
const rootRelativePath = node_core_library_1.Path.convertToSlashes(path.relative(gitRepoRootPath, currentPath)); | ||
// Parse the .gitignore patterns according to the Git documentation: | ||
// https://git-scm.com/docs/gitignore#_pattern_format | ||
const resolvedGitIgnorePatterns = []; | ||
const gitIgnorePatterns = rawIgnorePatternsByGitignoreFolder.get(currentPath); | ||
for (let gitIgnorePattern of gitIgnorePatterns || []) { | ||
// If the pattern is negated, track this and trim the negation so that we can do path resolution | ||
let isNegated = false; | ||
if (gitIgnorePattern.startsWith('!')) { | ||
isNegated = true; | ||
gitIgnorePattern = gitIgnorePattern.substring(1); | ||
} | ||
// Validate if the path is a relative path. If so, make the path relative to the root directory | ||
// of the Git repo. Slashes at the end of the path indicate that the pattern targets a directory | ||
// and do not indicate the pattern is relative to the gitignore file. Non-relative patterns are | ||
// not processed here since they are valid for all subdirectories at or below the gitignore file | ||
// directory. | ||
const slashIndex = gitIgnorePattern.indexOf('/'); | ||
if (slashIndex >= 0 && slashIndex !== gitIgnorePattern.length - 1) { | ||
// Trim the leading slash (if present) and append to the root relative path | ||
if (slashIndex === 0) { | ||
gitIgnorePattern = gitIgnorePattern.substring(1); | ||
} | ||
gitIgnorePattern = `${rootRelativePath}/${gitIgnorePattern}`; | ||
} | ||
// Add the negation back to the pattern if it was negated | ||
if (isNegated) { | ||
gitIgnorePattern = `!${gitIgnorePattern}`; | ||
} | ||
// Add the pattern to the list of resolved patterns in the order they are read, since the order | ||
// of declaration of patterns in a .gitignore file matters for negations | ||
resolvedGitIgnorePatterns.push(gitIgnorePattern); | ||
} | ||
// Add the patterns to the ignore matcher patterns. Since we are crawling up the directory tree to | ||
// the root of the Git repo we need to prepend the patterns, since the order of declaration of | ||
// patterns in a .gitignore file matters for negations. Do this using Array.concat so that we can | ||
// avoid stack overflows due to the variadic nature of Array.unshift. | ||
ignoreMatcherPatterns = [].concat(resolvedGitIgnorePatterns, ignoreMatcherPatterns); | ||
currentPath = currentPath.slice(0, currentPath.lastIndexOf('/')); | ||
} | ||
this._ignoreMatcherByGitignoreFolder.set(gitIgnoreParentPath, (0, ignore_1.default)().add(ignoreMatcherPatterns)); | ||
} | ||
return this._ignoreMatcherByGitignoreFolder; | ||
} | ||
async _tryReadGitIgnoreFileAsync(filePath) { | ||
let gitIgnoreContent; | ||
try { | ||
gitIgnoreContent = await node_core_library_1.FileSystem.readFileAsync(filePath); | ||
} | ||
catch (error) { | ||
if (!node_core_library_1.FileSystem.isFileDoesNotExistError(error)) { | ||
throw error; | ||
} | ||
} | ||
const foundIgnorePatterns = []; | ||
if (gitIgnoreContent) { | ||
const gitIgnorePatterns = gitIgnoreContent.split(/\r?\n/g); | ||
for (const gitIgnorePattern of gitIgnorePatterns) { | ||
// Ignore whitespace-only lines and comments | ||
if (gitIgnorePattern.length === 0 || GITIGNORE_IGNORABLE_LINE_REGEX.test(gitIgnorePattern)) { | ||
continue; | ||
} | ||
// Push them into the array in the order that they are read, since order matters | ||
foundIgnorePatterns.push(gitIgnorePattern); | ||
} | ||
} | ||
// Only return if we found any valid patterns | ||
return foundIgnorePatterns.length ? foundIgnorePatterns : undefined; | ||
} | ||
async _findUnignoredFilesAsync(searchPattern) { | ||
this._ensureGitMinimumVersion({ major: 2, minor: 22, patch: 0 }); | ||
this._ensurePathIsUnderGitWorkingTree(); | ||
const args = [ | ||
'--cached', | ||
'--modified', | ||
'--others', | ||
'--deduplicate', | ||
'--exclude-standard', | ||
'-z' | ||
]; | ||
if (searchPattern) { | ||
args.push(searchPattern); | ||
} | ||
return await this._executeGitCommandAndCaptureOutputAsync({ | ||
command: 'ls-files', | ||
args, | ||
delimiter: '\0' | ||
}); | ||
} | ||
async _executeGitCommandAndCaptureOutputAsync(options) { | ||
const gitPath = this._getGitPathOrThrow(); | ||
const processArgs = [options.command, ...(options.args || [])]; | ||
const processArgs = [options.command].concat(options.args || []); | ||
const childProcess = node_core_library_1.Executable.spawn(gitPath, processArgs, { | ||
currentWorkingDirectory: this._workingDirectory, | ||
stdio: ['pipe', 'pipe', 'pipe'] | ||
stdio: ['ignore', 'pipe', 'pipe'] | ||
}); | ||
if (!childProcess.stdout || !childProcess.stderr || !childProcess.stdin) { | ||
if (!childProcess.stdout || !childProcess.stderr) { | ||
throw new Error(`Failed to spawn Git process: ${gitPath} ${processArgs.join(' ')}`); | ||
} | ||
childProcess.stdout.setEncoding('utf8'); | ||
childProcess.stderr.setEncoding('utf8'); | ||
return await new Promise((resolve, reject) => { | ||
const outputLines = []; | ||
const errorLines = []; | ||
childProcess.stdout.on('data', (data) => { | ||
for (const line of data.split('\n')) { | ||
if (line) { | ||
outputLines.push(line); | ||
} | ||
const output = []; | ||
const stdoutBuffer = []; | ||
let errorMessage = ''; | ||
childProcess.stdout.on('data', (chunk) => { | ||
stdoutBuffer.push(chunk.toString()); | ||
}); | ||
childProcess.stderr.on('data', (chunk) => { | ||
errorMessage += chunk.toString(); | ||
}); | ||
childProcess.on('close', (exitCode) => { | ||
if (exitCode !== 0) { | ||
reject(new Error(`git exited with error code ${exitCode}${errorMessage ? `: ${errorMessage}` : ''}`)); | ||
} | ||
}); | ||
childProcess.stderr.on('data', (data) => { | ||
for (const line of data.split('\n')) { | ||
if (line) { | ||
errorLines.push(line); | ||
let remainder = ''; | ||
for (let chunk of stdoutBuffer) { | ||
let delimiterIndex; | ||
while ((delimiterIndex = chunk.indexOf(options.delimiter || '\n')) >= 0) { | ||
output.push(`${remainder}${chunk.slice(0, delimiterIndex)}`); | ||
remainder = ''; | ||
chunk = chunk.slice(delimiterIndex + 1); | ||
} | ||
remainder = chunk; | ||
} | ||
resolve(output); | ||
}); | ||
childProcess.on('close', (exitCode) => { | ||
resolve({ outputLines, errorLines, exitCode }); | ||
}); | ||
// If stdin arguments are provided, feed them to stdin and close it. | ||
if (options.stdinArgs) { | ||
for (const arg of options.stdinArgs) { | ||
childProcess.stdin.write(`${arg}\n`); | ||
} | ||
childProcess.stdin.end(); | ||
} | ||
}); | ||
@@ -218,3 +368,4 @@ } | ||
_parseGitVersion(gitVersionOutput) { | ||
// This regexp matches output of "git version" that looks like `git version <number>.<number>.<number>(+whatever)` | ||
// This regexp matches output of "git version" that looks like | ||
// `git version <number>.<number>.<number>(+whatever)` | ||
// Examples: | ||
@@ -221,0 +372,0 @@ // - git version 1.2.3 |
{ | ||
"name": "@rushstack/heft", | ||
"version": "0.49.0-rc.1", | ||
"version": "0.49.0-rc.2", | ||
"description": "Build all your JavaScript projects the same way: A way that works.", | ||
@@ -40,2 +40,3 @@ "keywords": [ | ||
"git-repo-info": "~2.1.0", | ||
"ignore": "~5.1.6", | ||
"tapable": "1.1.3", | ||
@@ -42,0 +43,0 @@ "true-case-path": "~2.2.1" |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1011461
219
10029
12
+ Addedignore@~5.1.6
+ Addedignore@5.1.9(transitive)