@bb-labs/bldr 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -3
  2. package/dist/index.js +196 -12
  3. package/package.json +7 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # @bb-labs/builder
1
+ # bldr
2
2
 
3
- A TypeScript build tool with watch mode and automatic dist synchronization.
3
+ A TypeScript build tool with watch mode, automatic dist synchronization, and split-terminal UI.
4
4
 
5
5
  ## Installation
6
6
 
@@ -21,5 +21,22 @@ bun add -d @bb-labs/builder typescript tsc-alias
21
21
 
22
22
  ## Options
23
23
 
24
- - `--watch`, `-w`: Watch mode
24
+ - `--watch`, `-w`: Watch mode with split-terminal UI showing tsc, tsc-alias, and bldr outputs side by side
25
25
  - `--project`, `-p <path>`: Custom tsconfig.json path
26
+
27
+ ## Testing
28
+
29
+ Run `bun run test:dev` to test the build tool with the included test files in `test-src/`. This will:
30
+
31
+ - Build files from `test-src/` to `test-dist/`
32
+ - Show the split-terminal UI with real-time updates
33
+ - Watch for changes and rebuild automatically
34
+
35
+ Add or modify files in `test-src/` to see the build tool in action!
36
+
37
+ ## Features
38
+
39
+ - **Split Terminal UI**: In watch mode, displays three panels side by side showing output from TypeScript compiler, tsc-alias, and bldr itself
40
+ - **Clean Builds**: Always cleans output directory before building
41
+ - **Path Alias Support**: Integrates with tsc-alias for path mapping
42
+ - **Process Management**: Properly handles child process cleanup on exit
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs";
5
5
  import chokidar from "chokidar";
6
+ import blessed from "blessed";
6
7
  // We use the TypeScript API to correctly resolve tsconfig + "extends".
7
8
  import ts from "typescript";
8
9
  function parseArgs(argv) {
@@ -65,21 +66,39 @@ function run(cmd, args, cwd) {
65
66
  return new Promise((resolve, reject) => {
66
67
  const p = spawn(cmd, args, {
67
68
  cwd,
68
- stdio: "inherit",
69
+ stdio: ["inherit", "pipe", "pipe"], // pipe both stdout and stderr to capture all output
69
70
  shell: process.platform === "win32",
70
71
  });
72
+ let stdout = "";
73
+ let stderr = "";
74
+ if (p.stdout) {
75
+ p.stdout.on("data", (data) => {
76
+ stdout += data.toString();
77
+ });
78
+ }
79
+ if (p.stderr) {
80
+ p.stderr.on("data", (data) => {
81
+ stderr += data.toString();
82
+ });
83
+ }
71
84
  p.on("exit", (code) => {
72
85
  if (code === 0)
73
86
  resolve();
74
- else
75
- reject(new Error(`${cmd} ${args.join(" ")} failed with exit code ${code}`));
87
+ else {
88
+ // Give a small delay to ensure all output data is captured
89
+ setTimeout(() => {
90
+ const allOutput = (stdout + stderr).trim();
91
+ const errorMsg = allOutput || `${cmd} ${args.join(" ")} failed with exit code ${code}`;
92
+ reject(new Error(errorMsg));
93
+ }, 100);
94
+ }
76
95
  });
77
96
  });
78
97
  }
79
98
  function spawnLongRunning(cmd, args, cwd) {
80
99
  const p = spawn(cmd, args, {
81
100
  cwd,
82
- stdio: "inherit",
101
+ stdio: ["inherit", "pipe", "pipe"], // pipe stdout and stderr
83
102
  shell: process.platform === "win32",
84
103
  });
85
104
  return p;
@@ -133,6 +152,81 @@ async function main() {
133
152
  }
134
153
  const rootDirAbs = path.isAbsolute(cfg.rootDir) ? cfg.rootDir : path.resolve(cfg.configDir, cfg.rootDir);
135
154
  const outDirAbs = path.isAbsolute(cfg.outDir) ? cfg.outDir : path.resolve(cfg.configDir, cfg.outDir);
155
+ // Create terminal UI only in watch mode
156
+ let screen, bldrBox, tscBox, aliasBox, logToBldr;
157
+ if (args.watch) {
158
+ screen = blessed.screen({
159
+ smartCSR: true,
160
+ title: "bldr - TypeScript Build Tool",
161
+ });
162
+ bldrBox = blessed.box({
163
+ top: 0,
164
+ left: 0,
165
+ width: "33%",
166
+ height: "100%",
167
+ label: " bldr ",
168
+ border: { type: "line" },
169
+ scrollable: true,
170
+ alwaysScroll: true,
171
+ scrollbar: { ch: " " },
172
+ });
173
+ tscBox = blessed.box({
174
+ top: 0,
175
+ left: "33%",
176
+ width: "34%",
177
+ height: "100%",
178
+ label: " tsc ",
179
+ border: { type: "line" },
180
+ scrollable: true,
181
+ alwaysScroll: true,
182
+ scrollbar: { ch: " " },
183
+ });
184
+ aliasBox = blessed.box({
185
+ top: 0,
186
+ left: "67%",
187
+ width: "33%",
188
+ height: "100%",
189
+ label: " tsc-alias ",
190
+ border: { type: "line" },
191
+ scrollable: true,
192
+ alwaysScroll: true,
193
+ scrollbar: { ch: " " },
194
+ });
195
+ screen.append(bldrBox);
196
+ screen.append(tscBox);
197
+ screen.append(aliasBox);
198
+ // Handle screen events
199
+ screen.key(["escape", "q", "C-c"], () => {
200
+ shutdown();
201
+ });
202
+ screen.key(["C-l"], () => {
203
+ bldrBox.setScrollPerc(100);
204
+ tscBox.setScrollPerc(100);
205
+ aliasBox.setScrollPerc(100);
206
+ screen.render();
207
+ });
208
+ // Create a function to log to the bldr box
209
+ logToBldr = (text) => {
210
+ if (screen && bldrBox) {
211
+ bldrBox.insertBottom(text);
212
+ bldrBox.setScrollPerc(100);
213
+ screen.render();
214
+ }
215
+ };
216
+ // Override console.log and console.error for bldr messages
217
+ const originalLog = console.log;
218
+ const originalError = console.error;
219
+ console.log = (...args) => {
220
+ logToBldr(args.join(" "));
221
+ // Don't call originalLog to avoid double output
222
+ };
223
+ console.error = (...args) => {
224
+ logToBldr(args.join(" "));
225
+ // Don't call originalError to avoid double output
226
+ };
227
+ screen.render();
228
+ }
229
+ // Initial messages will now go to the bldr box via overridden console.log
136
230
  console.log([
137
231
  `\n[bldr] project: ${rel(cwd, tsconfigPath)}`,
138
232
  `[bldr] rootDir : ${rel(cwd, rootDirAbs)}`,
@@ -144,6 +238,42 @@ async function main() {
144
238
  await rmIfExists(outDirAbs);
145
239
  const tscArgs = ["-p", tsconfigPath];
146
240
  const aliasArgs = ["-p", tsconfigPath];
241
+ // Error handling function for watch mode
242
+ const handleFatalError = async (errorMessage) => {
243
+ // Always restore normal console logging for watch mode
244
+ if (args.watch) {
245
+ // Destroy UI first to avoid conflicts
246
+ if (screen) {
247
+ screen.destroy();
248
+ screen = null;
249
+ bldrBox = null;
250
+ tscBox = null;
251
+ aliasBox = null;
252
+ }
253
+ // Write directly to stderr to ensure visibility
254
+ process.stderr.write(`[bldr] ${errorMessage}\n`);
255
+ }
256
+ else {
257
+ // In build mode, just print normally
258
+ console.error(`[bldr] ${errorMessage}`);
259
+ }
260
+ // Kill all processes
261
+ if (tscWatch && !tscWatch.killed) {
262
+ tscWatch.kill();
263
+ }
264
+ if (aliasWatch && !aliasWatch.killed) {
265
+ aliasWatch.kill();
266
+ }
267
+ if (srcWatcher) {
268
+ try {
269
+ await srcWatcher.close();
270
+ }
271
+ catch (e) {
272
+ // ignore
273
+ }
274
+ }
275
+ process.exit(1);
276
+ };
147
277
  if (!args.watch) {
148
278
  // One-shot build: tsc -> tsc-alias
149
279
  try {
@@ -156,22 +286,69 @@ async function main() {
156
286
  await rmIfExists(outDirAbs);
157
287
  throw error;
158
288
  }
289
+ console.log(`[bldr] build completed successfully!`);
159
290
  return;
160
291
  }
161
292
  // Watch mode:
162
- // 1) Do a clean initial pass so dist is correct immediately.
293
+ // 2) Do a clean initial pass so dist is correct immediately.
163
294
  try {
164
295
  await run("tsc", tscArgs, cwd);
165
296
  await run("tsc-alias", aliasArgs, cwd);
166
297
  }
167
298
  catch (error) {
168
- console.error(`[bldr] initial build failed: ${error}`);
169
- // Continue in watch mode even if initial build fails
170
- console.log(`[bldr] continuing in watch mode...`);
299
+ await handleFatalError(`[bldr] initial build failed: ${error}`);
171
300
  }
172
- // 2) Start watchers
301
+ // 3) Start watchers
173
302
  const tscWatch = spawnLongRunning("tsc", [...tscArgs, "-w"], cwd);
174
303
  const aliasWatch = spawnLongRunning("tsc-alias", [...aliasArgs, "-w"], cwd);
304
+ // Pipe output to boxes
305
+ if (tscWatch.stdout) {
306
+ tscWatch.stdout.on("data", (data) => {
307
+ const text = data.toString().trim();
308
+ if (text) {
309
+ tscBox.insertBottom(text);
310
+ tscBox.setScrollPerc(100);
311
+ screen.render();
312
+ }
313
+ });
314
+ }
315
+ if (tscWatch.stderr) {
316
+ tscWatch.stderr.on("data", (data) => {
317
+ const text = data.toString().trim();
318
+ if (text) {
319
+ tscBox.insertBottom(text);
320
+ tscBox.setScrollPerc(100);
321
+ screen.render();
322
+ }
323
+ });
324
+ }
325
+ if (aliasWatch.stdout) {
326
+ aliasWatch.stdout.on("data", (data) => {
327
+ const text = data.toString().trim();
328
+ if (text) {
329
+ aliasBox.insertBottom(text);
330
+ aliasBox.setScrollPerc(100);
331
+ screen.render();
332
+ }
333
+ });
334
+ }
335
+ if (aliasWatch.stderr) {
336
+ aliasWatch.stderr.on("data", (data) => {
337
+ const text = data.toString().trim();
338
+ if (text) {
339
+ aliasBox.insertBottom(text);
340
+ aliasBox.setScrollPerc(100);
341
+ screen.render();
342
+ }
343
+ });
344
+ }
345
+ // If any child process errors or exits, shut down all processes
346
+ tscWatch.on("error", (error) => {
347
+ handleFatalError(`[bldr] tsc watcher error: ${error}`);
348
+ });
349
+ aliasWatch.on("error", (error) => {
350
+ handleFatalError(`[bldr] tsc-alias watcher error: ${error}`);
351
+ });
175
352
  // 3) dist sync watcher: remove stale outputs on deletes/dir deletes
176
353
  const cleaner = makeDistCleaner(rootDirAbs, outDirAbs);
177
354
  const srcWatcher = chokidar.watch(rootDirAbs, {
@@ -225,9 +402,16 @@ async function main() {
225
402
  // TypeScript compiler handles additions and changes automatically
226
403
  const shutdown = async () => {
227
404
  console.log("\n[bldr] shutting down...");
405
+ // Destroy screen if it exists
406
+ if (screen) {
407
+ screen.destroy();
408
+ screen = null;
409
+ }
228
410
  // Close file watcher
229
411
  try {
230
- await srcWatcher.close();
412
+ if (srcWatcher) {
413
+ await srcWatcher.close();
414
+ }
231
415
  console.log("[bldr] src watcher closed");
232
416
  }
233
417
  catch (error) {
@@ -235,7 +419,7 @@ async function main() {
235
419
  }
236
420
  // Kill child processes
237
421
  const killPromises = [];
238
- if (!tscWatch.killed) {
422
+ if (tscWatch && !tscWatch.killed) {
239
423
  killPromises.push(new Promise((resolve) => {
240
424
  tscWatch.on("exit", () => resolve());
241
425
  tscWatch.kill();
@@ -249,7 +433,7 @@ async function main() {
249
433
  }));
250
434
  console.log("[bldr] killing tsc watcher...");
251
435
  }
252
- if (!aliasWatch.killed) {
436
+ if (aliasWatch && !aliasWatch.killed) {
253
437
  killPromises.push(new Promise((resolve) => {
254
438
  aliasWatch.on("exit", () => resolve());
255
439
  aliasWatch.kill();
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@bb-labs/bldr",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
+ "type": "module",
4
5
  "bin": {
5
6
  "bldr": "./dist/index.js"
6
7
  },
@@ -9,9 +10,12 @@
9
10
  ],
10
11
  "scripts": {
11
12
  "build": "tsc",
12
- "postbuild": "chmod +x dist/index.js"
13
+ "postbuild": "chmod +x dist/index.js",
14
+ "test:build": "node dist/index.js --project test-src/tsconfig.json",
15
+ "test:watch": "node dist/index.js --watch --project test-src/tsconfig.json"
13
16
  },
14
17
  "devDependencies": {
18
+ "@types/blessed": "^0.1.27",
15
19
  "@types/bun": "latest",
16
20
  "@types/node": "latest"
17
21
  },
@@ -20,6 +24,7 @@
20
24
  },
21
25
  "dependencies": {
22
26
  "@bb-labs/tsconfigs": "^0.0.1",
27
+ "blessed": "^0.1.81",
23
28
  "chokidar": "^5.0.0",
24
29
  "tsc-alias": "^1.8.8"
25
30
  }