Skip to content

Commit 38c9e5d

Browse files
committed
add formatted logging
1 parent 01975c5 commit 38c9e5d

9 files changed

Lines changed: 293 additions & 55 deletions

File tree

packages/rollup-util/.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
src/
2+
rollup.config.mjs
3+
tsconfig.json

packages/rollup-util/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@calmdownval/rollup-util",
3-
"version": "1.0.0",
3+
"version": "1.0.0-alpha.1",
44
"license": "ISC",
55
"exports": {
66
".": {
@@ -9,7 +9,7 @@
99
}
1010
},
1111
"peerDependencies": {
12-
"@calmdownval/workspaces-util": "1.0.0",
12+
"@calmdownval/workspaces-util": "1.0.0-alpha.1",
1313
"rollup": "4.38.0"
1414
},
1515
"devDependencies": {

packages/rollup-util/src/BuildContext.ts

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { join } from "node:path";
22
import { pathToFileURL } from "node:url";
33

4-
import { discoverModule, discoverWorkspace, getDependencies } from "@calmdownval/workspaces-util";
4+
import { discoverModule, discoverWorkspace, getDependencies, type GraphNode, type Module, type TraversalResult } from "@calmdownval/workspaces-util";
55
import { rollup, type InputOptions, type OutputOptions } from "rollup";
66

77
import type { Configurator } from "./Entity";
8+
import { createStatusReporter, formatTime, print } from "./StatusReporter";
89

910
export interface BuildContext {
1011
readonly cwd: string;
@@ -17,13 +18,14 @@ export type TargetEnv =
1718
| "stag"
1819
| "prod";
1920

20-
export function inEnvs(...envs: TargetEnv[]): Configurator<any> {
21+
export function inEnv(...envs: TargetEnv[]): Configurator<any> {
2122
return (_, context) => envs.includes(context.targetEnv);
2223
}
2324

2425

2526
/** @internal */
2627
export interface BuildTarget {
28+
readonly name: string;
2729
readonly input: InputOptions;
2830
readonly outputs: readonly OutputOptions[];
2931
}
@@ -55,6 +57,7 @@ const ENV_MAP: { readonly [K in string]?: TargetEnv } = {
5557
};
5658

5759
export async function build(cwd: string = process.cwd()) {
60+
const buildStartTime = Date.now();
5861
try {
5962
// get the origin module to build
6063
const originModule = await discoverModule(cwd);
@@ -64,56 +67,99 @@ export async function build(cwd: string = process.cwd()) {
6467

6568
// get an ordered queue of dependencies that need to be built
6669
const workspace = await discoverWorkspace({ cwd });
67-
const moduleQueue = workspace
70+
const tree = workspace
6871
? getDependencies({
6972
workspace,
7073
moduleName: originModule.declaration.name,
74+
exclude: [ "build-logic" ],
7175
})
72-
: [ originModule ];
76+
: singleTree(originModule);
77+
78+
const status = createStatusReporter(tree.root, "queued");
7379

7480
// get the target environment
7581
const targetEnv: TargetEnv = process.env.BUILD_ENV
7682
? ENV_MAP[process.env.BUILD_ENV] ?? "prod"
7783
: "prod";
7884

7985
// build!
80-
for (const currentModule of moduleQueue) {
86+
for (const currentNode of tree.buildOrder) {
87+
const moduleStartTime = Date.now();
88+
status.update(currentNode, { kind: "pending" });
89+
8190
const context: BuildContext = {
82-
cwd: currentModule.baseDir,
83-
moduleName: currentModule.declaration.name,
91+
cwd: currentNode.module.baseDir,
92+
moduleName: currentNode.module.declaration.name,
8493
targetEnv,
8594
};
8695

87-
// import the build.targets.mjs definition file
88-
currentTasks = [];
89-
process.chdir(context.cwd);
96+
let target: BuildTarget | null = null;
9097
try {
91-
const url = pathToFileURL(join(context.cwd, "build.targets.mjs")).href;
98+
// import the build.config.mjs definition file
99+
currentTasks = [];
100+
process.chdir(context.cwd);
101+
102+
const url = pathToFileURL(join(context.cwd, "build.config.mjs")).href;
92103
await import(url);
93-
}
94-
catch {
95-
// likely no such file exists, skip it...?
96-
continue;
97-
}
98104

99-
// run queued tasks
100-
const targets = (
101-
await Promise.all(
102-
currentTasks.map(task => task(context)),
105+
// process queued tasks
106+
const targets = (
107+
await Promise.all(
108+
currentTasks.map(task => task(context)),
109+
)
103110
)
104-
)
105-
.flat(1);
106-
107-
// build the targets sequentially
108-
for (const target of targets) {
109-
const bundle = await rollup(target.input);
110-
for (const output of target.outputs) {
111-
await bundle.write(output);
111+
.flat(1);
112+
113+
// build targets sequentially
114+
for (target of targets) {
115+
const bundle = await rollup({
116+
...target.input,
117+
onLog(level, log) {
118+
if (level !== "debug") {
119+
status.log(currentNode, log.message);
120+
}
121+
}
122+
});
123+
124+
for (const output of target.outputs) {
125+
await bundle.write(output);
126+
}
112127
}
128+
129+
const moduleTimeTaken = Date.now() - moduleStartTime;
130+
status.update(currentNode, {
131+
kind: "success",
132+
timeMs: moduleTimeTaken,
133+
});
134+
}
135+
catch (ex) {
136+
const moduleTimeTaken = Date.now() - moduleStartTime;
137+
status.update(currentNode, {
138+
kind: "error",
139+
timeMs: moduleTimeTaken,
140+
});
141+
142+
status.log(currentNode, (ex as Error).toString());
113143
}
114144
}
115145
}
116146
finally {
117147
currentTasks = null;
148+
149+
const buildTimeTaken = Date.now() - buildStartTime;
150+
print(`\nDone in ${formatTime(buildTimeTaken)}!\n`);
118151
}
119152
}
153+
154+
function singleTree(module: Module): TraversalResult {
155+
const node: GraphNode = {
156+
module,
157+
dependencies: [],
158+
dependents: [],
159+
};
160+
161+
return {
162+
buildOrder: [ node ],
163+
root: node,
164+
};
165+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import type { GraphNode, Module } from "@calmdownval/workspaces-util";
2+
3+
export interface StatusReporter {
4+
readonly lineCount: number;
5+
readonly root: GraphNode;
6+
lastLogNode?: GraphNode;
7+
8+
clear(): void;
9+
log(node: GraphNode, message: string): void;
10+
update(node: GraphNode, status: StatusInfo): void;
11+
}
12+
13+
export interface StatusInfo {
14+
kind: StatusKind;
15+
message?: string;
16+
timeMs?: number;
17+
}
18+
19+
export type StatusKind =
20+
| "error"
21+
| "pending"
22+
| "queued"
23+
| "success"
24+
| "unknown";
25+
26+
interface StatusNode extends GraphNode {
27+
readonly module: Module;
28+
readonly dependencies: StatusNode[];
29+
status?: StatusInfo;
30+
}
31+
32+
export function createStatusReporter(root: GraphNode, defaultStatus: StatusKind = "unknown"): StatusReporter {
33+
const visit = (node: StatusNode) => {
34+
node.status = { kind: defaultStatus };
35+
node.dependencies.forEach(visit);
36+
};
37+
38+
visit(root);
39+
40+
const format = formatStatusTree(root);
41+
print("\n");
42+
print(format.output);
43+
44+
return {
45+
lineCount: format.lineCount,
46+
root,
47+
clear: onClear,
48+
log: onLog,
49+
update: onUpdate,
50+
};
51+
}
52+
53+
function onClear(this: StatusReporter) {
54+
print(`\u001b[${this.lineCount + 1}A\r\u001b[0J`);
55+
}
56+
57+
function onLog(this: StatusReporter, node: GraphNode, message: string) {
58+
this.clear();
59+
if (this.lastLogNode !== node) {
60+
const header = node.module.declaration.name;
61+
print(`\n\u001b[1m${header}\u001b[0m\n┌${"─".repeat(header.length - 1)}\n`);
62+
63+
this.lastLogNode = node;
64+
}
65+
66+
print("│ ")
67+
print(message.replaceAll(/\r?\n/g, "\n│ "));
68+
69+
print("\n\n");
70+
print(formatStatusTree(this.root).output);
71+
}
72+
73+
function onUpdate(this: StatusReporter, node: GraphNode, status: StatusInfo) {
74+
(node as StatusNode).status = status;
75+
76+
this.clear();
77+
print("\n");
78+
print(formatStatusTree(this.root).output);
79+
}
80+
81+
82+
const Icon: Record<StatusKind, string> = {
83+
error: "❌",
84+
pending: "🔨",
85+
queued: "💤",
86+
success: "✅",
87+
unknown: "❓",
88+
};
89+
90+
function formatStatusTree(
91+
node: StatusNode,
92+
prefix: string = "",
93+
li0: string = "",
94+
li1: string = "",
95+
) {
96+
const { length } = node.dependencies;
97+
const branch = prefix + li0;
98+
const name = node.module.declaration.name;
99+
100+
let icon = Icon.unknown;
101+
let status = "";
102+
if (node.status) {
103+
icon = Icon[node.status.kind];
104+
105+
if (node.status.message) {
106+
status += ` · ${node.status.message}`;
107+
}
108+
109+
if (node.status.timeMs !== undefined) {
110+
status += ` (${formatTime(node.status.timeMs)})`;
111+
}
112+
}
113+
114+
let lineCount = 1;
115+
let output = `${icon} \u001b[0;90m${branch}\u001b[0m\u001b[1m${name}\u001b[0m${status}\n`;
116+
let index = 0;
117+
let isLast;
118+
let result;
119+
120+
for (; index < length; index += 1) {
121+
isLast = index + 1 === length;
122+
result = formatStatusTree(
123+
node.dependencies[index],
124+
prefix + li1,
125+
isLast ? "└─ " : "├─ ",
126+
isLast ? " " : "│ ",
127+
);
128+
129+
lineCount += result.lineCount;
130+
output += result.output;
131+
}
132+
133+
return {
134+
lineCount,
135+
output,
136+
};
137+
}
138+
139+
export function print(message: string) {
140+
process.stdout.write(message, "utf8");
141+
}
142+
143+
export function formatTime(timeMs: number): string {
144+
if (timeMs >= 1_000) {
145+
return `${(Math.round(timeMs / 100) / 10).toFixed(1)}s`;
146+
}
147+
148+
return `${timeMs.toFixed(0)}ms`;
149+
}

packages/rollup-util/src/TargetDeclaration.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export type TargetDeclaration<
3232
): TargetDeclaration<TName, TConfig, TPipelines & { [K in NameOf<TPipeline>]: TPipeline }>;
3333

3434
build(
35-
block?: (target: Target<TName, TConfig, TPipelines>) => void,
35+
block: (target: Target<TName, TConfig, TPipelines>) => void,
3636
): void;
3737
}>;
3838

@@ -130,12 +130,12 @@ const DEFAULT_CONFIG: OutputOptions = {
130130

131131
function onBuild(
132132
this: AnyTargetDeclaration,
133-
block?: (target: AnyTarget) => void,
133+
block: (target: AnyTarget) => void,
134134
): void {
135135
const target = this.finalize();
136136
buildTask(async context => {
137-
block?.(target);
138-
if (await isDisabled(target, context)) {
137+
block(target);
138+
if (!hasEntries(target) || await isDisabled(target, context)) {
139139
return [];
140140
}
141141

@@ -165,6 +165,7 @@ function onBuild(
165165
}
166166

167167
return {
168+
name: `${target.name} · ${pipeline.name}`,
168169
outputs: pipelineOutputs,
169170
input: {
170171
input: target.entries,
@@ -175,6 +176,12 @@ function onBuild(
175176
});
176177
}
177178

179+
function hasEntries(
180+
target: AnyTargetDeclaration,
181+
): boolean {
182+
return !!target.entries && Object.keys(target.entries).length > 0;
183+
}
184+
178185
async function isDisabled(
179186
entity: AnyEntity,
180187
context: BuildContext,

packages/rollup-util/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { build, inEnvs } from "./BuildContext";
1+
export { build, inEnv } from "./BuildContext";
22
export { declarePlugin } from "./PluginDeclaration";
33
export { declareTarget } from "./TargetDeclaration";
44

packages/workspaces-util/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@calmdownval/workspaces-util",
3-
"version": "1.0.0",
3+
"version": "1.0.0-alpha.1",
44
"license": "ISC",
55
"exports": {
66
".": {

0 commit comments

Comments
 (0)