зеркало из https://github.com/microsoft/lage.git
Adding a new concept of reporters (#54)
* adding basics e2e * Change files * mock it * executable * a;ekfaj;elkfja;ldjfk * fixing up install of lage * fixing the symlink of lage * WIP: major refactor to allow for reporters * getting json reporter working! * clean up index * adding big app tests * get rid of change file * fix CliOptions * Change files * adding docs about the CliOptions changes * get rid unused types
This commit is contained in:
Родитель
e590f2de84
Коммит
2599523e22
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "minor",
|
||||
"comment": "Changing the logger to allow json mode and adding real e2e tests for orders",
|
||||
"packageName": "lage",
|
||||
"email": "kchau@microsoft.com",
|
||||
"dependentChangeType": "patch",
|
||||
"date": "2020-08-05T23:54:28.996Z"
|
||||
}
|
|
@ -73,6 +73,14 @@ This has the semantic of running tasks up to what is specified in the command li
|
|||
such as with `--scope` or `--since`
|
||||
|
||||
|
||||
#### grouped
|
||||
_type: boolean_
|
||||
|
||||
Specify whether to make the console logger to group the logs per package task
|
||||
|
||||
Example: `lage --grouped`
|
||||
|
||||
|
||||
#### ignore
|
||||
_type: string[]_
|
||||
|
||||
|
@ -115,6 +123,14 @@ Creates a flamegraph-profile JSON for Chromium-based devtool
|
|||
Pay attention to the output summary to find the location of the JSON file.
|
||||
|
||||
|
||||
#### reporter
|
||||
_type: string_
|
||||
|
||||
Specify whether to use the JSON Reporter to create a parsable log output
|
||||
|
||||
Example: `lage --reporter json`
|
||||
|
||||
|
||||
#### resetCache
|
||||
_type: boolean_
|
||||
|
||||
|
|
|
@ -3,13 +3,14 @@ import { logger } from "../logger";
|
|||
import { Config } from "../types/Config";
|
||||
import { generateTopologicGraph } from "../workspace/generateTopologicalGraph";
|
||||
import { signal } from "../task/abortSignal";
|
||||
import { killAllActiveProcesses } from "../task/npmTask";
|
||||
import { displayReportAndExit } from "../displayReportAndExit";
|
||||
import { createContext } from "../context";
|
||||
import { runTasks } from "../task/taskRunner";
|
||||
import { NpmScriptTask } from "../task/NpmScriptTask";
|
||||
import { Reporter } from "../logger/reporters/Reporter";
|
||||
|
||||
// Create context
|
||||
export async function run(cwd: string, config: Config) {
|
||||
export async function run(cwd: string, config: Config, reporters: Reporter[]) {
|
||||
const context = createContext(config);
|
||||
const workspace = getWorkspace(cwd, config);
|
||||
|
||||
|
@ -21,20 +22,20 @@ export async function run(cwd: string, config: Config) {
|
|||
|
||||
// die faster if an abort signal is seen
|
||||
signal.addEventListener("abort", () => {
|
||||
killAllActiveProcesses();
|
||||
displayReportAndExit(context);
|
||||
NpmScriptTask.killAllActiveProcesses();
|
||||
displayReportAndExit(reporters, context);
|
||||
});
|
||||
|
||||
try {
|
||||
await runTasks({ graph, workspace, context, config });
|
||||
} catch (e) {
|
||||
logger.error("runTasks", e);
|
||||
logger.error("runTasks: " + e);
|
||||
}
|
||||
|
||||
if (config.profile) {
|
||||
const profileFile = profiler.output();
|
||||
logger.info("runTasks", `Profile saved to ${profileFile}`);
|
||||
logger.info(`runTasks: Profile saved to ${profileFile}`);
|
||||
}
|
||||
|
||||
displayReportAndExit(context);
|
||||
displayReportAndExit(reporters, context);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ export function getConfig(cwd: string): Config {
|
|||
: true;
|
||||
|
||||
return {
|
||||
reporter: parsedArgs.reporter || "npmLog",
|
||||
grouped: parsedArgs.grouped || false,
|
||||
args: getPassThroughArgs(parsedArgs),
|
||||
cache: parsedArgs.cache === false ? false : true,
|
||||
resetCache: parsedArgs.resetCache || false,
|
||||
|
|
|
@ -15,13 +15,13 @@ export function createContext(config: Pick<Config, "concurrency">): RunContext {
|
|||
measures: {
|
||||
start: [0, 0],
|
||||
duration: [0, 0],
|
||||
taskStats: [],
|
||||
failedTask: undefined,
|
||||
},
|
||||
tasks: new Map(),
|
||||
profiler: new Profiler({
|
||||
concurrency,
|
||||
prefix: "lage",
|
||||
outDir: profilerOutputDir,
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import { RunContext } from "./types/RunContext";
|
||||
import { reportSummary } from "./logger/reportSummary";
|
||||
import { Reporter } from "./logger/reporters/Reporter";
|
||||
|
||||
export function displayReportAndExit(context: RunContext) {
|
||||
export function displayReportAndExit(
|
||||
reporters: Reporter[],
|
||||
context: RunContext
|
||||
) {
|
||||
context.measures.duration = process.hrtime(context.measures.start);
|
||||
reportSummary(context);
|
||||
|
||||
for (const reporter of reporters) {
|
||||
reporter.summarize(context);
|
||||
}
|
||||
|
||||
if (context.measures.failedTask) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
28
src/index.ts
28
src/index.ts
|
@ -1,11 +1,12 @@
|
|||
import { getConfig } from "./config/getConfig";
|
||||
import { logLevel } from "./logger";
|
||||
import { init } from "./command/init";
|
||||
import { run } from "./command/run";
|
||||
import { showHelp } from "./showHelp";
|
||||
|
||||
console.log(`🔨 Lage task runner - let's make it`);
|
||||
console.log(``);
|
||||
import { logger } from "./logger";
|
||||
import { Logger } from "./logger/Logger";
|
||||
import { NpmLogReporter } from "./logger/reporters/NpmLogReporter";
|
||||
import { LogLevel } from "./logger/LogLevel";
|
||||
import { JsonReporter } from "./logger/reporters/JsonReporter";
|
||||
|
||||
// Parse CLI args
|
||||
const cwd = process.cwd();
|
||||
|
@ -13,14 +14,25 @@ try {
|
|||
const config = getConfig(cwd);
|
||||
|
||||
// Initialize logger
|
||||
if (config.verbose) {
|
||||
logLevel("verbose");
|
||||
}
|
||||
const logLevel = config.verbose ? LogLevel.verbose : LogLevel.info;
|
||||
|
||||
const reporters = [
|
||||
config.reporter === "json"
|
||||
? new JsonReporter({ logLevel })
|
||||
: new NpmLogReporter({
|
||||
logLevel,
|
||||
grouped: config.grouped,
|
||||
}),
|
||||
];
|
||||
|
||||
Logger.reporters = reporters;
|
||||
|
||||
logger.info(`Lage task runner - let's make it`);
|
||||
|
||||
if (config.command[0] === "init") {
|
||||
init(cwd);
|
||||
} else {
|
||||
run(cwd, config);
|
||||
run(cwd, config, reporters);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { LogLevel } from "./LogLevel";
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number;
|
||||
level: LogLevel;
|
||||
msg: string;
|
||||
data?: TaskData;
|
||||
}
|
||||
|
||||
export interface TaskData {
|
||||
status?: "pending" | "started" | "completed" | "failed" | "skipped";
|
||||
package?: string;
|
||||
task?: string;
|
||||
duration?: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export enum LogLevel {
|
||||
error = 10,
|
||||
warn = 20,
|
||||
info = 30,
|
||||
verbose = 40,
|
||||
silly = 50,
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { LogEntry, TaskData } from "./LogEntry";
|
||||
import { LogLevel } from "./LogLevel";
|
||||
import { Reporter } from "./reporters/Reporter";
|
||||
import { NpmLogReporter } from "./reporters/NpmLogReporter";
|
||||
|
||||
export class Logger {
|
||||
static reporters: Reporter[] = [
|
||||
new NpmLogReporter({ logLevel: LogLevel.info }),
|
||||
];
|
||||
|
||||
logs: LogEntry[] = [];
|
||||
|
||||
log(level: LogLevel, msg: string, data?: TaskData) {
|
||||
const entry = {
|
||||
timestamp: Date.now(),
|
||||
level,
|
||||
msg,
|
||||
data,
|
||||
};
|
||||
|
||||
this.logs.push(entry);
|
||||
|
||||
for (const reporter of Logger.reporters) {
|
||||
reporter.log(entry);
|
||||
}
|
||||
}
|
||||
|
||||
info(msg: string, data?: TaskData) {
|
||||
this.log(LogLevel.info, msg, data);
|
||||
}
|
||||
|
||||
warn(msg: string, data?: TaskData) {
|
||||
this.log(LogLevel.warn, msg, data);
|
||||
}
|
||||
|
||||
error(msg: string, data?: TaskData) {
|
||||
this.log(LogLevel.error, msg, data);
|
||||
}
|
||||
|
||||
verbose(msg: string, data?: TaskData) {
|
||||
this.log(LogLevel.verbose, msg, data);
|
||||
}
|
||||
|
||||
silly(msg: string, data?: TaskData) {
|
||||
this.log(LogLevel.silly, msg, data);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { Writable } from "stream";
|
||||
import { TaskLogger } from "./TaskLogger";
|
||||
|
||||
export class TaskLogWritable extends Writable {
|
||||
private buffer: string = "";
|
||||
|
||||
constructor(private taskLogger: TaskLogger) {
|
||||
super();
|
||||
}
|
||||
|
||||
_write(
|
||||
chunk: Buffer,
|
||||
_encoding: string,
|
||||
callback: (error?: Error | null) => void
|
||||
) {
|
||||
let prev = 0;
|
||||
let curr = 0;
|
||||
while (curr < chunk.byteLength) {
|
||||
if (chunk[curr] === 13 || (chunk[curr] === 10 && curr - prev > 1)) {
|
||||
this.buffer =
|
||||
this.buffer +
|
||||
chunk
|
||||
.slice(prev, curr)
|
||||
.toString()
|
||||
.replace(/^(\r\n|\n|\r)|(\r\n|\n|\r)$/g, "")
|
||||
.trimRight();
|
||||
this.taskLogger.verbose(this.buffer);
|
||||
this.buffer = "";
|
||||
prev = curr;
|
||||
}
|
||||
curr++;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { Logger } from "./Logger";
|
||||
import { TaskData } from "./LogEntry";
|
||||
|
||||
export class TaskLogger {
|
||||
logger: Logger;
|
||||
|
||||
constructor(private pkg: string, private task: string) {
|
||||
this.logger = new Logger();
|
||||
}
|
||||
|
||||
info(msg: string, data?: TaskData) {
|
||||
this.logger.info(msg, { package: this.pkg, task: this.task, ...data });
|
||||
}
|
||||
|
||||
warn(msg: string, data?: TaskData) {
|
||||
this.logger.warn(msg, { package: this.pkg, task: this.task, ...data });
|
||||
}
|
||||
|
||||
error(msg: string, data?: TaskData) {
|
||||
this.logger.error(msg, { package: this.pkg, task: this.task, ...data });
|
||||
}
|
||||
|
||||
verbose(msg: string, data?: TaskData) {
|
||||
this.logger.verbose(msg, { package: this.pkg, task: this.task, ...data });
|
||||
}
|
||||
|
||||
silly(msg: string, data?: TaskData) {
|
||||
this.logger.silly(msg, { package: this.pkg, task: this.task, ...data });
|
||||
}
|
||||
|
||||
getLogs() {
|
||||
return this.logger.logs;
|
||||
}
|
||||
}
|
|
@ -1,152 +1,3 @@
|
|||
import log from "npmlog";
|
||||
import { getTaskId } from "../task/taskId";
|
||||
import { Writable } from "stream";
|
||||
import chalk from "chalk";
|
||||
import { TaskLogs, TaskLogger } from "../types/Task";
|
||||
import { Logger } from "./Logger";
|
||||
|
||||
const taskLogs: TaskLogs = new Map();
|
||||
|
||||
const maxLengths = {
|
||||
pkg: 0,
|
||||
task: 0,
|
||||
};
|
||||
const colors = {
|
||||
info: chalk.white,
|
||||
verbose: chalk.gray,
|
||||
warn: chalk.white,
|
||||
error: chalk.white,
|
||||
task: chalk.cyan,
|
||||
pkg: chalk.magenta,
|
||||
};
|
||||
|
||||
export function getTaskLogs() {
|
||||
return taskLogs;
|
||||
}
|
||||
|
||||
export function setTaskLogMaxLengths(
|
||||
maxPkgLength: number,
|
||||
maxTaskLength: number
|
||||
) {
|
||||
maxLengths.pkg = maxPkgLength;
|
||||
maxLengths.task = maxTaskLength;
|
||||
}
|
||||
|
||||
export function getTaskLogPrefix(pkg: string, task: string) {
|
||||
return `${colors.pkg(pkg.padStart(maxLengths.pkg))} ${colors.task(
|
||||
task.padStart(maxLengths.task)
|
||||
)}`;
|
||||
}
|
||||
|
||||
function addToTaskLog(pkg: string, task: string, message: string) {
|
||||
const taskId = getTaskId(pkg, task);
|
||||
|
||||
if (!taskLogs.has(taskId)) {
|
||||
taskLogs.set(taskId, []);
|
||||
}
|
||||
|
||||
taskLogs.get(taskId)!.push(message);
|
||||
}
|
||||
|
||||
function normalize(prefixOrMessage: string, message?: string) {
|
||||
if (typeof message === "string") {
|
||||
const prefix = prefixOrMessage;
|
||||
return { prefix, message };
|
||||
} else {
|
||||
const prefix = "";
|
||||
const message = prefixOrMessage;
|
||||
return { prefix, message };
|
||||
}
|
||||
}
|
||||
|
||||
function info(prefixOrMessage: string, message?: string) {
|
||||
const normalizedArgs = normalize(prefixOrMessage, message);
|
||||
return log.info(normalizedArgs.prefix, colors.info(normalizedArgs.message));
|
||||
}
|
||||
|
||||
function warn(prefixOrMessage: string, message?: string) {
|
||||
const normalizedArgs = normalize(prefixOrMessage, message);
|
||||
return log.warn(normalizedArgs.prefix, colors.warn(normalizedArgs.message));
|
||||
}
|
||||
|
||||
function error(prefixOrMessage: string, message?: string) {
|
||||
const normalizedArgs = normalize(prefixOrMessage, message);
|
||||
return log.error(normalizedArgs.prefix, colors.error(normalizedArgs.message));
|
||||
}
|
||||
|
||||
function verbose(prefixOrMessage: string, message?: string, ...args: any) {
|
||||
const normalizedArgs = normalize(prefixOrMessage, message);
|
||||
return log.verbose(
|
||||
normalizedArgs.prefix,
|
||||
colors.verbose(normalizedArgs.message)
|
||||
);
|
||||
}
|
||||
|
||||
export class NpmLogWritable extends Writable {
|
||||
private buffer: string = "";
|
||||
private taskLogger: TaskLogger;
|
||||
|
||||
constructor(pkg: string, task: string) {
|
||||
super();
|
||||
this.taskLogger = taskLogger(pkg, task);
|
||||
}
|
||||
|
||||
_write(
|
||||
chunk: Buffer,
|
||||
_encoding: string,
|
||||
callback: (error?: Error | null) => void
|
||||
) {
|
||||
let prev = 0;
|
||||
let curr = 0;
|
||||
while (curr < chunk.byteLength) {
|
||||
if (chunk[curr] === 13 || (chunk[curr] === 10 && curr - prev > 1)) {
|
||||
this.buffer =
|
||||
this.buffer +
|
||||
chunk
|
||||
.slice(prev, curr)
|
||||
.toString()
|
||||
.replace(/^(\r\n|\n|\r)|(\r\n|\n|\r)$/g, "")
|
||||
.trimRight();
|
||||
this.taskLogger.verbose(this.buffer);
|
||||
this.buffer = "";
|
||||
prev = curr;
|
||||
}
|
||||
curr++;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
export function logLevel(level: "error" | "warn" | "info" | "verbose") {
|
||||
log.level = level;
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
verbose,
|
||||
};
|
||||
|
||||
export function taskLogger(pkg, task) {
|
||||
return {
|
||||
info: (message: string) => {
|
||||
addToTaskLog(pkg, task, message);
|
||||
return log.info(getTaskLogPrefix(pkg, task), colors.info(message));
|
||||
},
|
||||
|
||||
warn: (message: string) => {
|
||||
addToTaskLog(pkg, task, message);
|
||||
return log.warns(getTaskLogPrefix(pkg, task), colors.warn(message));
|
||||
},
|
||||
|
||||
error: (message: string) => {
|
||||
addToTaskLog(pkg, task, message);
|
||||
return log.error(getTaskLogPrefix(pkg, task), colors.error(message));
|
||||
},
|
||||
|
||||
verbose: (message: string) => {
|
||||
addToTaskLog(pkg, task, message);
|
||||
return log.verbose(getTaskLogPrefix(pkg, task), colors.verbose(message));
|
||||
},
|
||||
};
|
||||
}
|
||||
export const logger = new Logger();
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
import { formatDuration } from "./formatDuration";
|
||||
import { getTaskId } from "../task/taskId";
|
||||
import { getTaskLogs, getTaskLogPrefix } from "./index";
|
||||
import { logger } from ".";
|
||||
import { RunContext } from "../types/RunContext";
|
||||
import chalk from "chalk";
|
||||
|
||||
function hr() {
|
||||
logger.info("----------------------------------------------");
|
||||
}
|
||||
|
||||
export function reportSummary(context: RunContext) {
|
||||
const { measures } = context;
|
||||
|
||||
const taskLogs = getTaskLogs();
|
||||
|
||||
const statusColorFn = {
|
||||
success: chalk.greenBright,
|
||||
failed: chalk.redBright,
|
||||
skipped: chalk.gray,
|
||||
};
|
||||
|
||||
hr();
|
||||
|
||||
logger.info(chalk.cyanBright(`🏗 Summary\n`));
|
||||
|
||||
if (measures.failedTask) {
|
||||
const { pkg, task } = measures.failedTask;
|
||||
const taskId = getTaskId(pkg, task);
|
||||
|
||||
logger.error(`ERROR DETECTED IN ${pkg} ${task}`);
|
||||
logger.error(taskLogs.get(taskId)!.join("\n"));
|
||||
|
||||
hr();
|
||||
}
|
||||
|
||||
if (measures.taskStats.length > 0) {
|
||||
for (const stats of measures.taskStats) {
|
||||
const colorFn = statusColorFn[stats.status];
|
||||
logger.info(
|
||||
getTaskLogPrefix(stats.pkg, stats.task),
|
||||
colorFn(`${stats.status}, took ${formatDuration(stats.duration)}`)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.info("Nothing has been run.");
|
||||
}
|
||||
|
||||
hr();
|
||||
|
||||
logger.info(
|
||||
`Took a total of ${formatDuration(measures.duration)} to complete`
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { Reporter } from "./Reporter";
|
||||
import { LogEntry } from "../LogEntry";
|
||||
import { LogLevel } from "../LogLevel";
|
||||
import { RunContext } from "../../types/RunContext";
|
||||
import { hrToSeconds } from "./formatDuration";
|
||||
|
||||
export class JsonReporter implements Reporter {
|
||||
constructor(private options: { logLevel: LogLevel }) {}
|
||||
|
||||
log(entry: LogEntry) {
|
||||
if (this.options.logLevel >= entry.level) {
|
||||
console.log(JSON.stringify(entry));
|
||||
}
|
||||
}
|
||||
|
||||
summarize(context: RunContext) {
|
||||
const { measures, tasks } = context;
|
||||
const summary: any = {};
|
||||
const taskStats: any[] = [];
|
||||
|
||||
for (const task of tasks.values()) {
|
||||
taskStats.push({
|
||||
package: task.info.name,
|
||||
task: task.task,
|
||||
duration: hrToSeconds(task.duration),
|
||||
status: task.status,
|
||||
npmArgs: task.npmArgs,
|
||||
});
|
||||
}
|
||||
|
||||
if (measures.failedTask) {
|
||||
summary.failedTask = measures.failedTask;
|
||||
}
|
||||
|
||||
summary.duration = hrToSeconds(measures.duration);
|
||||
summary.taskStats = taskStats;
|
||||
|
||||
console.log(JSON.stringify({ summary }));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
import log from "npmlog";
|
||||
import chalk from "chalk";
|
||||
import { Reporter } from "./Reporter";
|
||||
import { LogLevel } from "../LogLevel";
|
||||
import { LogEntry } from "../LogEntry";
|
||||
import { formatDuration, hrToSeconds } from "./formatDuration";
|
||||
import { getTaskId } from "../../task/taskId";
|
||||
import { RunContext } from "../../types/RunContext";
|
||||
|
||||
const maxLengths = {
|
||||
pkg: 0,
|
||||
task: 0,
|
||||
};
|
||||
const colors = {
|
||||
info: chalk.white,
|
||||
verbose: chalk.gray,
|
||||
warn: chalk.white,
|
||||
error: chalk.white,
|
||||
task: chalk.cyan,
|
||||
pkg: chalk.magenta,
|
||||
};
|
||||
|
||||
function getTaskLogPrefix(pkg: string, task: string) {
|
||||
return `${colors.pkg(pkg.padStart(maxLengths.pkg))} ${colors.task(
|
||||
task.padStart(maxLengths.task)
|
||||
)}`;
|
||||
}
|
||||
|
||||
function normalize(prefixOrMessage: string, message?: string) {
|
||||
if (typeof message === "string") {
|
||||
const prefix = prefixOrMessage;
|
||||
return { prefix, message };
|
||||
} else {
|
||||
const prefix = "";
|
||||
const message = prefixOrMessage;
|
||||
return { prefix, message };
|
||||
}
|
||||
}
|
||||
|
||||
export class NpmLogReporter implements Reporter {
|
||||
readonly groupedEntries = new Map<string, LogEntry[]>();
|
||||
|
||||
constructor(private options: { logLevel?: LogLevel; grouped?: boolean }) {
|
||||
options.logLevel = options.logLevel || LogLevel.info;
|
||||
log.level = LogLevel[options.logLevel];
|
||||
}
|
||||
|
||||
log(entry: LogEntry) {
|
||||
if (this.options.logLevel! >= entry.level) {
|
||||
const isTaskLogEntry =
|
||||
entry.data && entry.data.package && entry.data.task;
|
||||
|
||||
if (isTaskLogEntry && !this.options.grouped) {
|
||||
return this.logTaskEntry(
|
||||
entry.data!.package!,
|
||||
entry.data!.task!,
|
||||
entry
|
||||
);
|
||||
} else if (isTaskLogEntry && this.options.grouped) {
|
||||
return this.logTaskEntryInGroup(
|
||||
entry.data!.package!,
|
||||
entry.data!.task!,
|
||||
entry
|
||||
);
|
||||
} else {
|
||||
return this.logGenericEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private logGenericEntry(entry: LogEntry) {
|
||||
const normalizedArgs = normalize(entry.msg);
|
||||
|
||||
const logFn = log[LogLevel[entry.level]];
|
||||
const colorFn = colors[LogLevel[entry.level]];
|
||||
|
||||
return logFn(normalizedArgs.prefix, colorFn(normalizedArgs.message));
|
||||
}
|
||||
|
||||
private logTaskEntry(pkg: string, task: string, entry: LogEntry) {
|
||||
const normalizedArgs = this.options.grouped
|
||||
? normalize(entry.msg)
|
||||
: normalize(getTaskLogPrefix(pkg, task), entry.msg);
|
||||
const logFn = log[LogLevel[entry.level]];
|
||||
const colorFn = colors[LogLevel[entry.level]];
|
||||
|
||||
if (entry.data && entry.data.status) {
|
||||
const pkgTask = this.options.grouped
|
||||
? `${chalk.magenta(pkg)} ${chalk.cyan(task)}`
|
||||
: "";
|
||||
|
||||
switch (entry.data.status) {
|
||||
case "started":
|
||||
return logFn(normalizedArgs.prefix, colorFn(`▶️ start ${pkgTask}`));
|
||||
|
||||
case "completed":
|
||||
return logFn(
|
||||
normalizedArgs.prefix,
|
||||
colorFn(
|
||||
`✔️ done ${pkgTask} - ${formatDuration(entry.data.duration!)}`
|
||||
)
|
||||
);
|
||||
|
||||
case "failed":
|
||||
return logFn(normalizedArgs.prefix, colorFn(`❌ fail ${pkgTask}`));
|
||||
|
||||
case "skipped":
|
||||
return logFn(normalizedArgs.prefix, colorFn(`⏭️ skip ${pkgTask}`));
|
||||
}
|
||||
} else {
|
||||
return logFn(
|
||||
normalizedArgs.prefix,
|
||||
colorFn("| " + normalizedArgs.message)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private logTaskEntryInGroup(pkg: string, task: string, logEntry: LogEntry) {
|
||||
const taskId = getTaskId(pkg, task);
|
||||
|
||||
this.groupedEntries.set(taskId, this.groupedEntries.get(taskId) || []);
|
||||
this.groupedEntries.get(taskId)?.push(logEntry);
|
||||
|
||||
if (
|
||||
logEntry.data &&
|
||||
(logEntry.data.status === "completed" ||
|
||||
logEntry.data.status === "failed" ||
|
||||
logEntry.data.status === "skipped")
|
||||
) {
|
||||
const entries = this.groupedEntries.get(taskId)!;
|
||||
for (const entry of entries) {
|
||||
this.logTaskEntry(entry.data?.package!, entry.data?.task!, entry);
|
||||
}
|
||||
|
||||
this.hr();
|
||||
}
|
||||
}
|
||||
|
||||
hr() {
|
||||
log.info("", "----------------------------------------------");
|
||||
}
|
||||
|
||||
summarize(context: RunContext) {
|
||||
const { measures, tasks } = context;
|
||||
const { hr } = this;
|
||||
|
||||
const statusColorFn = {
|
||||
completed: chalk.greenBright,
|
||||
failed: chalk.redBright,
|
||||
skipped: chalk.gray,
|
||||
};
|
||||
|
||||
log.info("", chalk.cyanBright(`🏗 Summary\n`));
|
||||
|
||||
if (measures.failedTask) {
|
||||
const { pkg, task } = measures.failedTask;
|
||||
const taskId = getTaskId(pkg, task);
|
||||
const taskLogs = tasks.get(taskId)?.logger.getLogs();
|
||||
|
||||
log.error("", `ERROR DETECTED IN ${pkg} ${task}`);
|
||||
|
||||
if (taskLogs) {
|
||||
log.error("", taskLogs?.map((entry) => entry.msg).join("\n"));
|
||||
}
|
||||
|
||||
hr();
|
||||
}
|
||||
|
||||
if (tasks.size > 0) {
|
||||
for (const npmScriptTask of tasks.values()) {
|
||||
const colorFn = statusColorFn[npmScriptTask.status];
|
||||
|
||||
log.info(
|
||||
"",
|
||||
getTaskLogPrefix(npmScriptTask.info.name, npmScriptTask.task),
|
||||
colorFn(
|
||||
`${npmScriptTask.status}, took ${formatDuration(
|
||||
hrToSeconds(npmScriptTask.duration)
|
||||
)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log.info("", "Nothing has been run.");
|
||||
}
|
||||
|
||||
hr();
|
||||
|
||||
log.info(
|
||||
"",
|
||||
`Took a total of ${formatDuration(
|
||||
hrToSeconds(measures.duration)
|
||||
)} to complete`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { LogEntry } from "../LogEntry";
|
||||
import { RunContext } from "../../types/RunContext";
|
||||
|
||||
export interface Reporter {
|
||||
log(entry: LogEntry): void;
|
||||
summarize(context: RunContext): void;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
export function formatDuration(hrtime: [number, number]) {
|
||||
let raw = hrtime[0] + hrtime[1] / 1e9;
|
||||
export function formatDuration(seconds: string) {
|
||||
let raw = parseFloat(seconds);
|
||||
if (raw > 60) {
|
||||
const minutes = Math.floor(raw / 60);
|
||||
const seconds = (raw - minutes * 60).toFixed(2);
|
||||
|
@ -9,3 +9,8 @@ export function formatDuration(hrtime: [number, number]) {
|
|||
return `${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
export function hrToSeconds(hrtime: [number, number]) {
|
||||
let raw = hrtime[0] + hrtime[1] / 1e9;
|
||||
return raw.toFixed(2);
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
import { TaskLogger } from "../logger/TaskLogger";
|
||||
import { ChildProcess } from "child_process";
|
||||
import { PackageInfo } from "workspace-tools";
|
||||
import { Config } from "../types/Config";
|
||||
import { findNpmClient } from "../workspace/findNpmClient";
|
||||
import { spawn } from "child_process";
|
||||
import { controller } from "./abortSignal";
|
||||
import path from "path";
|
||||
import { TaskLogWritable } from "../logger/TaskLogWritable";
|
||||
import { cacheHash, cacheFetch, cachePut } from "../cache/backfill";
|
||||
import { RunContext } from "../types/RunContext";
|
||||
import { hrToSeconds } from "../logger/reporters/formatDuration";
|
||||
|
||||
export class NpmScriptTask {
|
||||
static npmCmd: string = "";
|
||||
static bail = false;
|
||||
static activeProcesses = new Set<ChildProcess>();
|
||||
|
||||
npmArgs: string[] = [];
|
||||
startTime: [number, number] = [0, 0];
|
||||
duration: [number, number] = [0, 0];
|
||||
status: "completed" | "failed" | "pending" | "started" | "skipped";
|
||||
logger: TaskLogger;
|
||||
|
||||
static killAllActiveProcesses() {
|
||||
for (const cp of NpmScriptTask.activeProcesses) {
|
||||
cp.kill("SIGKILL");
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
public task: string,
|
||||
private root: string,
|
||||
public info: PackageInfo,
|
||||
private config: Config,
|
||||
private context: RunContext
|
||||
) {
|
||||
NpmScriptTask.npmCmd =
|
||||
NpmScriptTask.npmCmd || findNpmClient(config.npmClient);
|
||||
this.status = "pending";
|
||||
this.logger = new TaskLogger(info.name, task);
|
||||
|
||||
const { node, args } = config;
|
||||
this.npmArgs = [...node, "run", task, "--", ...args];
|
||||
}
|
||||
|
||||
onStart() {
|
||||
this.status = "started";
|
||||
this.startTime = process.hrtime();
|
||||
this.logger.info("started", { status: "started" });
|
||||
}
|
||||
|
||||
onComplete() {
|
||||
this.status = "completed";
|
||||
this.duration = process.hrtime(this.startTime);
|
||||
this.logger.info("completed", {
|
||||
status: "completed",
|
||||
duration: hrToSeconds(this.duration),
|
||||
});
|
||||
}
|
||||
|
||||
onFail() {
|
||||
this.status = "failed";
|
||||
this.duration = process.hrtime(this.startTime);
|
||||
this.logger.info("failed", {
|
||||
status: "failed",
|
||||
duration: hrToSeconds(this.duration),
|
||||
});
|
||||
}
|
||||
|
||||
onSkipped() {
|
||||
this.status = "skipped";
|
||||
this.duration = process.hrtime(this.startTime);
|
||||
this.logger.info("skipped", {
|
||||
status: "skipped",
|
||||
duration: hrToSeconds(this.duration),
|
||||
});
|
||||
}
|
||||
|
||||
async getCache() {
|
||||
let hash: string | null = null;
|
||||
let cacheHit = false;
|
||||
|
||||
const { task, info, root, config, logger } = this;
|
||||
|
||||
if (config.cache) {
|
||||
hash = await cacheHash(task, info, root, config);
|
||||
|
||||
if (hash && !config.resetCache) {
|
||||
cacheHit = await cacheFetch(hash, info, config);
|
||||
}
|
||||
|
||||
logger.verbose(`hash: ${hash}, cache hit? ${cacheHit}`);
|
||||
}
|
||||
|
||||
return { hash, cacheHit };
|
||||
}
|
||||
|
||||
async saveCache(hash: string) {
|
||||
const { logger, info, config } = this;
|
||||
logger.verbose(`hash put ${hash}`);
|
||||
await cachePut(hash, info, config);
|
||||
}
|
||||
|
||||
runScript() {
|
||||
const { info, logger, npmArgs } = this;
|
||||
const { npmCmd } = NpmScriptTask;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.verbose(`Running ${[npmCmd, ...npmArgs].join(" ")}`);
|
||||
|
||||
const cp = spawn(npmCmd, npmArgs, {
|
||||
cwd: path.dirname(info.packageJsonPath),
|
||||
stdio: "pipe",
|
||||
env: {
|
||||
...process.env,
|
||||
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
|
||||
LAGE_PACKAGE_NAME: info.name,
|
||||
},
|
||||
});
|
||||
|
||||
NpmScriptTask.activeProcesses.add(cp);
|
||||
|
||||
const stdoutLogger = new TaskLogWritable(this.logger);
|
||||
cp.stdout.pipe(stdoutLogger);
|
||||
|
||||
const stderrLogger = new TaskLogWritable(this.logger);
|
||||
cp.stderr.pipe(stderrLogger);
|
||||
|
||||
cp.on("exit", handleChildProcessExit);
|
||||
|
||||
function handleChildProcessExit(code: number) {
|
||||
if (code === 0) {
|
||||
NpmScriptTask.activeProcesses.delete(cp);
|
||||
return resolve();
|
||||
}
|
||||
|
||||
NpmScriptTask.bail = true;
|
||||
cp.stdout.destroy();
|
||||
cp.stdin.destroy();
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const { info, task, context, config } = this;
|
||||
|
||||
this.onStart();
|
||||
|
||||
try {
|
||||
const { hash, cacheHit } = await this.getCache();
|
||||
|
||||
// skip if cache hit!
|
||||
if (cacheHit) {
|
||||
this.onSkipped();
|
||||
return true;
|
||||
}
|
||||
|
||||
await context.profiler.run(
|
||||
() => this.runScript(),
|
||||
`${info.name}.${task}`
|
||||
);
|
||||
|
||||
if (config.cache && hash) {
|
||||
await this.saveCache(hash);
|
||||
}
|
||||
|
||||
this.onComplete();
|
||||
} catch (e) {
|
||||
context.measures.failedTask = { pkg: info.name, task };
|
||||
controller.abort();
|
||||
this.onFail();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -18,12 +18,12 @@ export function filterPackages(options: {
|
|||
// If scoped is defined, get scoped packages
|
||||
if (typeof scopedPackages !== "undefined") {
|
||||
filtered = filtered.filter((pkg) => scopedPackages.includes(pkg));
|
||||
logger.verbose("filterPackages", `scope: ${scopedPackages.join(",")}`);
|
||||
logger.verbose(`filterPackages scope: ${scopedPackages.join(",")}`);
|
||||
}
|
||||
|
||||
if (typeof changedPackages !== "undefined") {
|
||||
filtered = filtered.filter((pkg) => changedPackages.includes(pkg));
|
||||
logger.verbose("filterPackages", `changed: ${changedPackages.join(",")}`);
|
||||
logger.verbose(`filterPackages changed: ${changedPackages.join(",")}`);
|
||||
}
|
||||
|
||||
if (deps) {
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
import { Config } from "../types/Config";
|
||||
import { findNpmClient } from "../workspace/findNpmClient";
|
||||
import { PackageInfo } from "workspace-tools";
|
||||
import { RunContext } from "../types/RunContext";
|
||||
import { spawn, ChildProcess } from "child_process";
|
||||
import { taskLogger, NpmLogWritable } from "../logger";
|
||||
import { taskWrapper } from "./taskWrapper";
|
||||
import path from "path";
|
||||
|
||||
let npmCmd: string;
|
||||
let bail = false;
|
||||
let activeProcesses = new Set<ChildProcess>();
|
||||
|
||||
export function npmTask(
|
||||
task: string,
|
||||
info: PackageInfo,
|
||||
config: Config,
|
||||
context: RunContext,
|
||||
root: string
|
||||
) {
|
||||
const { node, args, npmClient } = config;
|
||||
|
||||
const logger = taskLogger(info.name, task);
|
||||
|
||||
// cached npmCmd
|
||||
npmCmd = npmCmd || findNpmClient(npmClient);
|
||||
|
||||
const npmArgs = [...node, "run", task, "--", ...args];
|
||||
|
||||
if (bail) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return taskWrapper(
|
||||
info,
|
||||
task,
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!info.scripts || !info.scripts![task]) {
|
||||
logger.info(`Empty script detected, skipping`);
|
||||
return resolve();
|
||||
}
|
||||
|
||||
logger.verbose(`Running ${[npmCmd, ...npmArgs].join(" ")}`);
|
||||
|
||||
const cp = spawn(npmCmd, npmArgs, {
|
||||
cwd: path.dirname(info.packageJsonPath),
|
||||
stdio: "pipe",
|
||||
env: {
|
||||
...process.env,
|
||||
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
|
||||
LAGE_PACKAGE_NAME: info.name,
|
||||
},
|
||||
});
|
||||
|
||||
activeProcesses.add(cp);
|
||||
|
||||
const stdoutLogger = new NpmLogWritable(info.name, task);
|
||||
cp.stdout.pipe(stdoutLogger);
|
||||
|
||||
const stderrLogger = new NpmLogWritable(info.name, task);
|
||||
cp.stderr.pipe(stderrLogger);
|
||||
|
||||
cp.on("exit", handleChildProcessExit);
|
||||
|
||||
function handleChildProcessExit(code: number) {
|
||||
if (code === 0) {
|
||||
activeProcesses.delete(cp);
|
||||
return resolve();
|
||||
}
|
||||
|
||||
bail = true;
|
||||
cp.stdout.destroy();
|
||||
cp.stdin.destroy();
|
||||
reject();
|
||||
}
|
||||
}),
|
||||
config,
|
||||
context,
|
||||
root
|
||||
);
|
||||
}
|
||||
|
||||
export function killAllActiveProcesses() {
|
||||
for (const cp of activeProcesses) {
|
||||
cp.kill("SIGKILL");
|
||||
}
|
||||
}
|
|
@ -5,16 +5,16 @@ import {
|
|||
Task,
|
||||
} from "@microsoft/task-scheduler";
|
||||
import { Config } from "../types/Config";
|
||||
import { npmTask } from "./npmTask";
|
||||
import { filterPackages } from "./filterPackages";
|
||||
import { Workspace } from "../types/Workspace";
|
||||
import { setTaskLogMaxLengths } from "../logger";
|
||||
import {
|
||||
getScopedPackages,
|
||||
getChangedPackages,
|
||||
getTransitiveProviders,
|
||||
} from "workspace-tools";
|
||||
import { Priority } from "../types/Priority";
|
||||
import { NpmScriptTask } from "./NpmScriptTask";
|
||||
import { getTaskId } from "./taskId";
|
||||
|
||||
/** Returns a map that maps task name to the priorities config for that task */
|
||||
function getPriorityMap(priorities: Priority[]) {
|
||||
|
@ -50,29 +50,32 @@ export async function runTasks(options: {
|
|||
concurrency: config.concurrency,
|
||||
});
|
||||
|
||||
const taskNames = Object.keys(config.pipeline);
|
||||
|
||||
for (const [task, taskDeps] of Object.entries(config.pipeline)) {
|
||||
for (const [taskName, taskDeps] of Object.entries(config.pipeline)) {
|
||||
const deps = taskDeps.filter((dep) => !dep.startsWith("^"));
|
||||
const topoDeps = taskDeps
|
||||
.filter((dep) => dep.startsWith("^"))
|
||||
.map((dep) => dep.slice(1));
|
||||
|
||||
pipeline = pipeline.addTask({
|
||||
name: task,
|
||||
name: taskName,
|
||||
deps,
|
||||
topoDeps,
|
||||
priorities: priorityMap.get(task),
|
||||
priorities: priorityMap.get(taskName),
|
||||
run: async (_location, _stdout, _stderr, pkg) => {
|
||||
const scripts = workspace.allPackages[pkg].scripts;
|
||||
if (scripts && scripts[task]) {
|
||||
return (await npmTask(
|
||||
task,
|
||||
workspace.allPackages[pkg],
|
||||
const info = workspace.allPackages[pkg];
|
||||
const scripts = info.scripts;
|
||||
if (scripts && scripts[taskName]) {
|
||||
const npmTask = new NpmScriptTask(
|
||||
taskName,
|
||||
workspace.root,
|
||||
info,
|
||||
config,
|
||||
context,
|
||||
workspace.root
|
||||
)) as boolean;
|
||||
context
|
||||
);
|
||||
|
||||
context.tasks.set(getTaskId(info.name, taskName), npmTask);
|
||||
|
||||
return await npmTask.run();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
@ -95,6 +98,7 @@ export async function runTasks(options: {
|
|||
|
||||
const hasSince = typeof since !== "undefined";
|
||||
let changedPackages: string[] | undefined = undefined;
|
||||
|
||||
if (hasSince) {
|
||||
changedPackages = getChangedPackages(workspace.root, since, config.ignore);
|
||||
}
|
||||
|
@ -106,15 +110,6 @@ export async function runTasks(options: {
|
|||
changedPackages,
|
||||
});
|
||||
|
||||
// Set up the longest names of tasks and scripts for nice logging
|
||||
setTaskLogMaxLengths(
|
||||
Object.keys(workspace.allPackages).reduce(
|
||||
(l, pkg) => (l < pkg.length ? pkg.length : l),
|
||||
0
|
||||
),
|
||||
taskNames.reduce((l, task) => (l < task.length ? task.length : l), 0)
|
||||
);
|
||||
|
||||
await pipeline.go({
|
||||
packages: filteredPackages,
|
||||
tasks: config.command,
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
import { cacheHash, cacheFetch, cachePut } from "../cache/backfill";
|
||||
import { formatDuration } from "../logger/formatDuration";
|
||||
import { taskLogger } from "../logger";
|
||||
import { RunContext } from "../types/RunContext";
|
||||
import { Config } from "../types/Config";
|
||||
import { PackageInfo } from "workspace-tools";
|
||||
import { controller } from "./abortSignal";
|
||||
|
||||
export async function taskWrapper(
|
||||
info: PackageInfo,
|
||||
task: string,
|
||||
fn: () => Promise<unknown>,
|
||||
config: Config,
|
||||
context: RunContext,
|
||||
root: string
|
||||
): Promise<unknown> {
|
||||
const { profiler, measures } = context;
|
||||
const pkg = info.name;
|
||||
const logger = taskLogger(pkg, task);
|
||||
const start = process.hrtime();
|
||||
|
||||
let cacheHit = false;
|
||||
let hash: string | null = null;
|
||||
|
||||
if (config.cache) {
|
||||
hash = await cacheHash(task, info, root, config);
|
||||
|
||||
if (hash && !config.resetCache) {
|
||||
cacheHit = await cacheFetch(hash, info, config);
|
||||
}
|
||||
|
||||
logger.verbose(`hash: ${hash}, cache hit? ${cacheHit}`);
|
||||
}
|
||||
|
||||
if (!cacheHit) {
|
||||
logger.info("▶️ start");
|
||||
|
||||
try {
|
||||
await profiler.run(() => fn(), `${pkg}.${task}`);
|
||||
const duration = process.hrtime(start);
|
||||
|
||||
measures.taskStats.push({
|
||||
pkg,
|
||||
task,
|
||||
start,
|
||||
duration,
|
||||
status: "success",
|
||||
});
|
||||
|
||||
logger.info(`✔️ done - took ${formatDuration(duration)}`);
|
||||
|
||||
if (config.cache && hash) {
|
||||
logger.verbose(`hash put ${hash}`);
|
||||
await cachePut(hash, info, config);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
handleFailure();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const duration = process.hrtime(start);
|
||||
measures.taskStats.push({ pkg, task, start, duration, status: "skipped" });
|
||||
logger.info("⏭️ skip");
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleFailure() {
|
||||
logger.info("❌ fail");
|
||||
if (!measures.failedTask) {
|
||||
measures.failedTask = { pkg, task };
|
||||
}
|
||||
const duration = process.hrtime(start);
|
||||
measures.taskStats.push({ pkg, task, start, duration, status: "failed" });
|
||||
controller.abort();
|
||||
}
|
||||
}
|
|
@ -121,4 +121,18 @@ export interface CliOptions {
|
|||
* has changed with the `--since` flag.
|
||||
*/
|
||||
ignore: string[];
|
||||
|
||||
/**
|
||||
* Specify whether to use the JSON Reporter to create a parsable log output
|
||||
*
|
||||
* Example: `lage --reporter json`
|
||||
*/
|
||||
reporter: string;
|
||||
|
||||
/**
|
||||
* Specify whether to make the console logger to group the logs per package task
|
||||
*
|
||||
* Example: `lage --grouped`
|
||||
*/
|
||||
grouped: boolean;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
import Profiler from "p-profiler";
|
||||
import { NpmScriptTask } from "../task/NpmScriptTask";
|
||||
|
||||
interface TaskStats {
|
||||
pkg: string;
|
||||
task: string;
|
||||
start: [number, number];
|
||||
duration: [number, number];
|
||||
status: "failed" | "skipped" | "success" | "not started";
|
||||
}
|
||||
|
||||
interface Measures {
|
||||
export interface Measures {
|
||||
start: [number, number];
|
||||
duration: [number, number];
|
||||
failedTask?: { pkg: string; task: string };
|
||||
taskStats: TaskStats[];
|
||||
}
|
||||
|
||||
export interface RunContext {
|
||||
measures: Measures;
|
||||
tasks: Map<string, NpmScriptTask>;
|
||||
profiler: Profiler;
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
export type TaskId = string;
|
||||
|
||||
export type TaskLogs = Map<TaskId, string[]>;
|
||||
|
||||
export type TaskLogger = {
|
||||
info: (message: string, ...args: any) => void;
|
||||
warn: (message: string, ...args: any) => any;
|
||||
error: (message: string, ...args: any) => void;
|
||||
verbose: (message: string, ...args: any) => void;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export type TaskId = string;
|
|
@ -1,4 +1,5 @@
|
|||
import { Monorepo } from "../mock/monorepo";
|
||||
import { parseNdJson } from "./parseNdJson";
|
||||
|
||||
describe("basics", () => {
|
||||
it("basic test case", () => {
|
||||
|
@ -13,12 +14,47 @@ describe("basics", () => {
|
|||
|
||||
const results = repo.run("test");
|
||||
const output = results.stdout + results.stderr;
|
||||
const jsonOutput = parseNdJson(output);
|
||||
|
||||
expect(output).toContain("b build success");
|
||||
expect(output).toContain("a build success");
|
||||
expect(output).toContain("a test success");
|
||||
expect(output).toContain("b test success");
|
||||
expect(
|
||||
jsonOutput.find((entry) =>
|
||||
filterEntry(entry.data, "b", "build", "completed")
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
jsonOutput.find((entry) =>
|
||||
filterEntry(entry.data, "b", "test", "completed")
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
jsonOutput.find((entry) =>
|
||||
filterEntry(entry.data, "a", "build", "completed")
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
jsonOutput.find((entry) =>
|
||||
filterEntry(entry.data, "a", "test", "completed")
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
jsonOutput.find((entry) =>
|
||||
filterEntry(entry.data, "a", "lint", "completed")
|
||||
)
|
||||
).toBeFalsy();
|
||||
|
||||
repo.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
function filterEntry(taskData, pkg, task, status) {
|
||||
return (
|
||||
taskData &&
|
||||
taskData.package === pkg &&
|
||||
taskData.task === task &&
|
||||
taskData.status === status
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { Monorepo } from "../mock/monorepo";
|
||||
import { getTaskId } from "../../src/task/taskId";
|
||||
import { parseNdJson } from "./parseNdJson";
|
||||
|
||||
describe("bigapp test", () => {
|
||||
// This test follows the model as documented here:
|
||||
// https://microsoft.github.io/lage/guide/levels.html
|
||||
it("with apps and libs and all, y'all", () => {
|
||||
const repo = new Monorepo("bigapp");
|
||||
|
||||
repo.init();
|
||||
repo.install();
|
||||
|
||||
repo.addPackage("FooApp1", ["FooCore"]);
|
||||
repo.addPackage("FooApp2", ["FooCore"]);
|
||||
repo.addPackage("FooCore", ["BuildTool"]);
|
||||
repo.addPackage("BarPage", ["BarCore"]);
|
||||
repo.addPackage("BarCore", ["BuildTool"]);
|
||||
repo.addPackage("BuildTool");
|
||||
repo.linkPackages();
|
||||
|
||||
const results = repo.run("test");
|
||||
const output = results.stdout + results.stderr;
|
||||
const jsonOutput = parseNdJson(output);
|
||||
|
||||
const indices: { [taskId: string]: number } = {};
|
||||
|
||||
for (const pkg of [
|
||||
"FooApp1",
|
||||
"FooApp2",
|
||||
"FooCore",
|
||||
"BarCore",
|
||||
"BarPage",
|
||||
"BuildTool",
|
||||
]) {
|
||||
for (const task of ["build", "test"]) {
|
||||
indices[getTaskId(pkg, task)] = jsonOutput.findIndex((e) =>
|
||||
filterEntry(e.data, pkg, task, "completed")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
expect(indices[getTaskId("BuildTool", "build")]).toBeLessThan(
|
||||
indices[getTaskId("BuildTool", "test")]
|
||||
);
|
||||
|
||||
expect(indices[getTaskId("BuildTool", "build")]).toBeLessThan(
|
||||
indices[getTaskId("FooCore", "build")]
|
||||
);
|
||||
|
||||
expect(indices[getTaskId("BuildTool", "build")]).toBeLessThan(
|
||||
indices[getTaskId("FooApp1", "build")]
|
||||
);
|
||||
|
||||
expect(indices[getTaskId("BuildTool", "build")]).toBeLessThan(
|
||||
indices[getTaskId("FooApp2", "build")]
|
||||
);
|
||||
|
||||
expect(indices[getTaskId("BuildTool", "build")]).toBeLessThan(
|
||||
indices[getTaskId("BarPage", "build")]
|
||||
);
|
||||
|
||||
expect(indices[getTaskId("BuildTool", "build")]).toBeLessThan(
|
||||
indices[getTaskId("BarCore", "build")]
|
||||
);
|
||||
|
||||
expect(indices[getTaskId("BarCore", "build")]).toBeLessThan(
|
||||
indices[getTaskId("BarPage", "build")]
|
||||
);
|
||||
|
||||
expect(indices[getTaskId("FooCore", "build")]).toBeLessThan(
|
||||
indices[getTaskId("FooApp2", "build")]
|
||||
);
|
||||
|
||||
expect(indices[getTaskId("FooCore", "build")]).toBeLessThan(
|
||||
indices[getTaskId("FooCore", "test")]
|
||||
);
|
||||
|
||||
expect(indices[getTaskId("BarPage", "build")]).toBeLessThan(
|
||||
indices[getTaskId("BarPage", "test")]
|
||||
);
|
||||
|
||||
repo.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
function filterEntry(taskData, pkg, task, status) {
|
||||
return (
|
||||
taskData &&
|
||||
taskData.package === pkg &&
|
||||
taskData.task === task &&
|
||||
taskData.status === status
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export function parseNdJson(ndjson: string) {
|
||||
return ndjson.split("\n").map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
}
|
|
@ -78,6 +78,8 @@ export class Monorepo {
|
|||
}
|
||||
|
||||
generateRepoFiles() {
|
||||
const lagePath = path.join(this.nodeModulesPath, "lage/lib/index");
|
||||
|
||||
this.commitFiles({
|
||||
"package.json": {
|
||||
name: this.name,
|
||||
|
@ -85,9 +87,9 @@ export class Monorepo {
|
|||
private: true,
|
||||
workspaces: ["packages/*"],
|
||||
scripts: {
|
||||
build: "lage build",
|
||||
test: "lage test",
|
||||
lint: "lage lint",
|
||||
build: `node "${lagePath}" build --reporter json`,
|
||||
test: `node "${lagePath}" test --reporter json`,
|
||||
lint: `node "${lagePath}" lint --reporter json`,
|
||||
},
|
||||
devDependencies: {
|
||||
lage: path.resolve(__dirname, "..", ".."),
|
||||
|
@ -101,34 +103,6 @@ export class Monorepo {
|
|||
}
|
||||
};`,
|
||||
});
|
||||
|
||||
this.commitFiles(
|
||||
{
|
||||
"node_modules/.bin/lage": `#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')")
|
||||
|
||||
case \`uname\` in
|
||||
*CYGWIN*) basedir=\`cygpath -w "$basedir"\`;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
"$basedir/node" "$basedir/../lage/bin/lage.js" "$@"
|
||||
ret=$?
|
||||
else
|
||||
node "$basedir/../lage/bin/lage.js" "$@"
|
||||
ret=$?
|
||||
fi
|
||||
exit $ret`,
|
||||
"node_modules/.bin/lage.cmd": `@IF EXIST "%~dp0\node.exe" (
|
||||
"%~dp0\\node.exe" "%~dp0\\..\\lage\\bin\\lage.js" %*
|
||||
) ELSE (
|
||||
@SETLOCAL
|
||||
@SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
node "%~dp0\\..\\lage\\bin\\lage.js" %*
|
||||
)`,
|
||||
},
|
||||
{ executable: true }
|
||||
);
|
||||
}
|
||||
|
||||
addPackage(name: string, internalDeps: string[] = []) {
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"lage/src/*": ["../src/*"]
|
||||
}
|
||||
},
|
||||
"include": [".", "../src"]
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче