@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 +25 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +260 -0
- package/package.json +26 -0
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
|
package/dist/index.d.ts
ADDED
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
|
+
}
|