@bb-labs/bldr 0.0.1

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.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @bb-labs/builder
2
+
3
+ A TypeScript build tool with watch mode and automatic dist synchronization.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add -d @bb-labs/builder typescript tsc-alias
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```json
14
+ {
15
+ "scripts": {
16
+ "build": "bldr",
17
+ "dev": "bldr --watch"
18
+ }
19
+ }
20
+ ```
21
+
22
+ ## Options
23
+
24
+ - `--watch`, `-w`: Watch mode
25
+ - `--project`, `-p <path>`: Custom tsconfig.json path
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env bun
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import fs from "node:fs";
5
+ import chokidar from "chokidar";
6
+ // We use the TypeScript API to correctly resolve tsconfig + "extends".
7
+ import ts from "typescript";
8
+ function parseArgs(argv) {
9
+ const args = { watch: false };
10
+ for (let i = 0; i < argv.length; i++) {
11
+ const a = argv[i];
12
+ if (a === "-w" || a === "--watch")
13
+ args.watch = true;
14
+ else if (a === "-p" || a === "--project")
15
+ args.project = argv[++i];
16
+ }
17
+ return args;
18
+ }
19
+ function findClosestTsconfig(startDir) {
20
+ let dir = startDir;
21
+ while (true) {
22
+ const candidate = path.join(dir, "tsconfig.json");
23
+ if (fs.existsSync(candidate))
24
+ return candidate;
25
+ const parent = path.dirname(dir);
26
+ if (parent === dir)
27
+ break;
28
+ dir = parent;
29
+ }
30
+ throw new Error(`Could not find tsconfig.json starting from ${startDir}. Pass -p <path-to-tsconfig>.`);
31
+ }
32
+ function readTsConfig(tsconfigPath) {
33
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
34
+ if (configFile.error) {
35
+ const msg = ts.formatDiagnosticsWithColorAndContext([configFile.error], {
36
+ getCanonicalFileName: (f) => f,
37
+ getCurrentDirectory: ts.sys.getCurrentDirectory,
38
+ getNewLine: () => ts.sys.newLine,
39
+ });
40
+ throw new Error(msg);
41
+ }
42
+ const configDir = path.dirname(tsconfigPath);
43
+ // parseJsonConfigFileContent resolves "extends" and normalizes options.
44
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, configDir,
45
+ /*existingOptions*/ undefined, tsconfigPath);
46
+ if (parsed.errors?.length) {
47
+ const msg = ts.formatDiagnosticsWithColorAndContext(parsed.errors, {
48
+ getCanonicalFileName: (f) => f,
49
+ getCurrentDirectory: ts.sys.getCurrentDirectory,
50
+ getNewLine: () => ts.sys.newLine,
51
+ });
52
+ throw new Error(msg);
53
+ }
54
+ // These are absolute paths in parsed.options (when provided).
55
+ const outDir = parsed.options.outDir;
56
+ const rootDir = parsed.options.rootDir;
57
+ return {
58
+ tsconfigPath,
59
+ configDir,
60
+ outDir,
61
+ rootDir,
62
+ };
63
+ }
64
+ function run(cmd, args, cwd) {
65
+ return new Promise((resolve, reject) => {
66
+ const p = spawn(cmd, args, {
67
+ cwd,
68
+ stdio: "inherit",
69
+ shell: process.platform === "win32",
70
+ });
71
+ p.on("exit", (code) => {
72
+ if (code === 0)
73
+ resolve();
74
+ else
75
+ reject(new Error(`${cmd} ${args.join(" ")} failed with exit code ${code}`));
76
+ });
77
+ });
78
+ }
79
+ function spawnLongRunning(cmd, args, cwd) {
80
+ const p = spawn(cmd, args, {
81
+ cwd,
82
+ stdio: "inherit",
83
+ shell: process.platform === "win32",
84
+ });
85
+ return p;
86
+ }
87
+ function rel(from, to) {
88
+ const r = path.relative(from, to);
89
+ return r.startsWith(".") ? r : `./${r}`;
90
+ }
91
+ async function rmIfExists(p) {
92
+ try {
93
+ await fs.promises.rm(p, { force: true, recursive: true });
94
+ }
95
+ catch {
96
+ // ignore
97
+ }
98
+ }
99
+ function isTsLike(filePath) {
100
+ return (filePath.endsWith(".ts") || filePath.endsWith(".tsx") || filePath.endsWith(".mts") || filePath.endsWith(".cts"));
101
+ }
102
+ function stripTsExt(p) {
103
+ return p.replace(/\.(ts|tsx|mts|cts)$/i, "");
104
+ }
105
+ function makeDistCleaner(rootDirAbs, outDirAbs) {
106
+ const EMITTED_SUFFIXES = [".js", ".js.map", ".d.ts", ".d.ts.map"];
107
+ function toOutPath(srcPathAbs) {
108
+ const relPath = path.relative(rootDirAbs, srcPathAbs);
109
+ return path.join(outDirAbs, relPath);
110
+ }
111
+ async function removeEmittedForSource(srcFileAbs) {
112
+ const outLike = toOutPath(srcFileAbs); // dist/foo.tsx
113
+ const base = stripTsExt(outLike); // dist/foo
114
+ await Promise.all(EMITTED_SUFFIXES.map((s) => rmIfExists(base + s)));
115
+ }
116
+ async function removeOutDirForSourceDir(srcDirAbs) {
117
+ const outDir = toOutPath(srcDirAbs);
118
+ await rmIfExists(outDir);
119
+ }
120
+ return { removeEmittedForSource, removeOutDirForSourceDir };
121
+ }
122
+ async function main() {
123
+ const args = parseArgs(process.argv.slice(2));
124
+ const cwd = process.cwd();
125
+ const tsconfigPath = args.project ? path.resolve(cwd, args.project) : findClosestTsconfig(cwd);
126
+ const cfg = readTsConfig(tsconfigPath);
127
+ if (!cfg.outDir || !cfg.rootDir) {
128
+ throw new Error([
129
+ `tsconfig must specify both "compilerOptions.outDir" and "compilerOptions.rootDir" for dist sync.`,
130
+ `Found: rootDir=${String(cfg.rootDir)} outDir=${String(cfg.outDir)}`,
131
+ `File: ${tsconfigPath}`,
132
+ ].join("\n"));
133
+ }
134
+ const rootDirAbs = path.isAbsolute(cfg.rootDir) ? cfg.rootDir : path.resolve(cfg.configDir, cfg.rootDir);
135
+ const outDirAbs = path.isAbsolute(cfg.outDir) ? cfg.outDir : path.resolve(cfg.configDir, cfg.outDir);
136
+ console.log([
137
+ `\n[tsbuild] project: ${rel(cwd, tsconfigPath)}`,
138
+ `[tsbuild] rootDir : ${rel(cwd, rootDirAbs)}`,
139
+ `[tsbuild] outDir : ${rel(cwd, outDirAbs)}`,
140
+ `[tsbuild] mode : ${args.watch ? "watch" : "build"}\n`,
141
+ ].join("\n"));
142
+ // Clean the output directory first
143
+ console.log(`[tsbuild] cleaning output directory...`);
144
+ await rmIfExists(outDirAbs);
145
+ const tscArgs = ["-p", tsconfigPath];
146
+ const aliasArgs = ["-p", tsconfigPath];
147
+ if (!args.watch) {
148
+ // One-shot build: tsc -> tsc-alias
149
+ try {
150
+ await run("tsc", tscArgs, cwd);
151
+ await run("tsc-alias", aliasArgs, cwd);
152
+ }
153
+ catch (error) {
154
+ // Clean up partial output on build failure
155
+ console.error(`[tsbuild] build failed, cleaning output directory...`);
156
+ await rmIfExists(outDirAbs);
157
+ throw error;
158
+ }
159
+ return;
160
+ }
161
+ // Watch mode:
162
+ // 1) Do a clean initial pass so dist is correct immediately.
163
+ try {
164
+ await run("tsc", tscArgs, cwd);
165
+ await run("tsc-alias", aliasArgs, cwd);
166
+ }
167
+ catch (error) {
168
+ console.error(`[tsbuild] initial build failed: ${error}`);
169
+ // Continue in watch mode even if initial build fails
170
+ console.log(`[tsbuild] continuing in watch mode...`);
171
+ }
172
+ // 2) Start watchers
173
+ const tscWatch = spawnLongRunning("tsc", [...tscArgs, "-w"], cwd);
174
+ const aliasWatch = spawnLongRunning("tsc-alias", [...aliasArgs, "-w"], cwd);
175
+ // 3) dist sync watcher: remove stale outputs on deletes/dir deletes
176
+ const cleaner = makeDistCleaner(rootDirAbs, outDirAbs);
177
+ const srcWatcher = chokidar.watch(rootDirAbs, {
178
+ ignoreInitial: true,
179
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
180
+ atomic: true, // Handle atomic writes (renames/moves) properly
181
+ interval: 100, // Poll interval for most files
182
+ binaryInterval: 300, // Poll interval for binary files
183
+ });
184
+ // Queue for handling rapid operations to prevent race conditions
185
+ const operationQueue = new Map();
186
+ const MAX_CONCURRENT_OPERATIONS = 10;
187
+ async function queuedOperation(key, operation) {
188
+ // Wait for any existing operation on this key
189
+ const existing = operationQueue.get(key);
190
+ if (existing) {
191
+ await existing;
192
+ }
193
+ // Limit concurrent operations
194
+ while (operationQueue.size >= MAX_CONCURRENT_OPERATIONS) {
195
+ await Promise.race(operationQueue.values());
196
+ }
197
+ const promise = operation().finally(() => operationQueue.delete(key));
198
+ operationQueue.set(key, promise);
199
+ return promise;
200
+ }
201
+ srcWatcher.on("unlink", async (absPath) => {
202
+ try {
203
+ await queuedOperation(absPath, async () => {
204
+ if (isTsLike(absPath)) {
205
+ await cleaner.removeEmittedForSource(absPath);
206
+ }
207
+ else {
208
+ // Mirror non-TS deletes too (assets)
209
+ await rmIfExists(path.join(outDirAbs, path.relative(rootDirAbs, absPath)));
210
+ }
211
+ });
212
+ }
213
+ catch (error) {
214
+ console.error(`[tsbuild] error handling file deletion ${absPath}: ${error}`);
215
+ }
216
+ });
217
+ srcWatcher.on("unlinkDir", async (absDir) => {
218
+ try {
219
+ await cleaner.removeOutDirForSourceDir(absDir);
220
+ }
221
+ catch (error) {
222
+ console.error(`[tsbuild] error handling directory deletion ${absDir}: ${error}`);
223
+ }
224
+ });
225
+ // TypeScript compiler handles additions and changes automatically
226
+ const shutdown = async () => {
227
+ console.log("\n[tsbuild] shutting down...");
228
+ try {
229
+ srcWatcher.close();
230
+ console.log("[tsbuild] src watcher closed");
231
+ }
232
+ catch (error) {
233
+ console.error(`[tsbuild] error closing src watcher: ${error}`);
234
+ }
235
+ try {
236
+ tscWatch.kill();
237
+ console.log("[tsbuild] tsc watcher killed");
238
+ }
239
+ catch (error) {
240
+ console.error(`[tsbuild] error killing tsc watcher: ${error}`);
241
+ }
242
+ try {
243
+ aliasWatch.kill();
244
+ console.log("[tsbuild] tsc-alias watcher killed");
245
+ }
246
+ catch (error) {
247
+ console.error(`[tsbuild] error killing tsc-alias watcher: ${error}`);
248
+ }
249
+ process.exit(0);
250
+ };
251
+ process.on("SIGINT", shutdown);
252
+ process.on("SIGTERM", shutdown);
253
+ // If any child exits, shut down.
254
+ tscWatch.on("exit", () => shutdown());
255
+ aliasWatch.on("exit", () => shutdown());
256
+ }
257
+ main().catch((e) => {
258
+ console.error(String(e?.stack || e));
259
+ process.exit(1);
260
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@bb-labs/bldr",
3
+ "version": "0.0.1",
4
+ "bin": {
5
+ "bldr": "./dist/index.js"
6
+ },
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "postbuild": "chmod +x dist/index.js"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "latest",
16
+ "@types/node": "latest"
17
+ },
18
+ "peerDependencies": {
19
+ "typescript": "^5"
20
+ },
21
+ "dependencies": {
22
+ "@bb-labs/tsconfigs": "^0.0.1",
23
+ "chokidar": "^5.0.0",
24
+ "tsc-alias": "^1.8.8"
25
+ }
26
+ }