@outfitter/tooling 0.3.3 → 0.3.4
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/.markdownlint-cli2.jsonc +55 -55
- package/README.md +21 -21
- package/dist/bun-version-compat.d.ts +2 -0
- package/dist/bun-version-compat.js +10 -0
- package/dist/cli/check-boundary-invocations.d.ts +34 -0
- package/dist/cli/check-boundary-invocations.js +14 -0
- package/dist/cli/check-bunup-registry.d.ts +36 -0
- package/dist/cli/check-bunup-registry.js +12 -0
- package/dist/cli/check-changeset.d.ts +66 -0
- package/dist/cli/check-changeset.js +20 -0
- package/dist/cli/check-clean-tree.d.ts +36 -0
- package/dist/cli/check-clean-tree.js +14 -0
- package/dist/cli/check-exports.d.ts +2 -0
- package/dist/cli/check-exports.js +14 -0
- package/dist/cli/check-markdown-links.d.ts +42 -0
- package/dist/cli/check-markdown-links.js +13 -0
- package/dist/cli/check-readme-imports.d.ts +60 -0
- package/dist/{shared/chunk-7tdgbqb0.js → cli/check-readme-imports.js} +7 -6
- package/dist/cli/check-tsdoc.d.ts +2 -0
- package/dist/cli/check-tsdoc.js +36 -0
- package/dist/cli/check.d.ts +19 -0
- package/dist/cli/check.js +10 -0
- package/dist/cli/fix.d.ts +19 -0
- package/dist/cli/fix.js +10 -0
- package/dist/cli/index.js +49 -1218
- package/dist/cli/init.d.ts +31 -0
- package/dist/cli/init.js +12 -0
- package/dist/cli/pre-push.d.ts +60 -0
- package/dist/cli/pre-push.js +27 -0
- package/dist/cli/upgrade-bun.d.ts +8 -0
- package/dist/cli/upgrade-bun.js +9 -0
- package/dist/index.d.ts +6 -186
- package/dist/index.js +4 -42
- package/dist/registry/build.d.ts +4 -0
- package/dist/registry/build.js +279 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +1 -0
- package/dist/registry/schema.d.ts +2 -0
- package/dist/registry/schema.js +28 -0
- package/dist/shared/@outfitter/tooling-1hez6j9d.js +21 -0
- package/dist/shared/@outfitter/tooling-6cxfdx0q.js +187 -0
- package/dist/shared/{chunk-cmde0fwx.js → @outfitter/tooling-875svjnz.js} +16 -31
- package/dist/shared/@outfitter/tooling-9ram55dd.js +69 -0
- package/dist/shared/@outfitter/tooling-9vs606gq.d.ts +3 -0
- package/dist/shared/@outfitter/tooling-a4bfx4be.js +21 -0
- package/dist/shared/@outfitter/tooling-amrbp7cm.js +102 -0
- package/dist/shared/@outfitter/tooling-ctmgnap5.js +19 -0
- package/dist/shared/@outfitter/tooling-d363b88r.js +349 -0
- package/dist/shared/@outfitter/tooling-gcdvsqqp.js +73 -0
- package/dist/shared/@outfitter/tooling-h04te11c.js +231 -0
- package/dist/shared/@outfitter/tooling-ja1zg5yc.js +214 -0
- package/dist/shared/@outfitter/tooling-jnrs9rqd.js +4 -0
- package/dist/shared/@outfitter/tooling-mkynjra9.js +23 -0
- package/dist/shared/@outfitter/tooling-njw4z34x.d.ts +140 -0
- package/dist/shared/@outfitter/tooling-pq47jv6t.js +213 -0
- package/dist/shared/@outfitter/tooling-sjm8nebx.d.ts +109 -0
- package/dist/shared/@outfitter/tooling-vjmhvpjq.d.ts +29 -0
- package/dist/shared/@outfitter/tooling-wesswf21.d.ts +59 -0
- package/dist/shared/@outfitter/tooling-wwm97f47.js +81 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +8 -0
- package/package.json +134 -131
- package/registry/registry.json +17 -10
- package/tsconfig.preset.bun.json +5 -5
- package/tsconfig.preset.json +33 -33
- package/biome.json +0 -81
- package/dist/shared/chunk-3s189drz.js +0 -4
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
analyzeSourceFile,
|
|
4
|
+
calculateCoverage
|
|
5
|
+
} from "./tooling-875svjnz.js";
|
|
6
|
+
|
|
7
|
+
// packages/tooling/src/cli/pre-push.ts
|
|
8
|
+
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
|
+
import ts from "typescript";
|
|
11
|
+
var COLORS = {
|
|
12
|
+
reset: "\x1B[0m",
|
|
13
|
+
red: "\x1B[31m",
|
|
14
|
+
green: "\x1B[32m",
|
|
15
|
+
yellow: "\x1B[33m",
|
|
16
|
+
blue: "\x1B[34m"
|
|
17
|
+
};
|
|
18
|
+
function log(msg) {
|
|
19
|
+
process.stdout.write(`${msg}
|
|
20
|
+
`);
|
|
21
|
+
}
|
|
22
|
+
function getCurrentBranch() {
|
|
23
|
+
const result = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
24
|
+
return result.stdout.toString().trim();
|
|
25
|
+
}
|
|
26
|
+
function runGit(args) {
|
|
27
|
+
try {
|
|
28
|
+
const result = Bun.spawnSync(["git", ...args], { stderr: "ignore" });
|
|
29
|
+
if (result.exitCode !== 0) {
|
|
30
|
+
return { ok: false, lines: [] };
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
lines: result.stdout.toString().split(`
|
|
35
|
+
`).map((line) => line.trim()).filter(Boolean)
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
return { ok: false, lines: [] };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isRedPhaseBranch(branch) {
|
|
42
|
+
return branch.endsWith("-tests") || branch.endsWith("/tests") || branch.endsWith("_tests");
|
|
43
|
+
}
|
|
44
|
+
function isScaffoldBranch(branch) {
|
|
45
|
+
return branch.endsWith("-scaffold") || branch.endsWith("/scaffold") || branch.endsWith("_scaffold");
|
|
46
|
+
}
|
|
47
|
+
function isReleaseBranch(branch) {
|
|
48
|
+
return branch.startsWith("changeset-release/");
|
|
49
|
+
}
|
|
50
|
+
var TEST_PATH_PATTERNS = [
|
|
51
|
+
/(^|\/)__tests__\//,
|
|
52
|
+
/(^|\/)__snapshots__\//,
|
|
53
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/,
|
|
54
|
+
/\.snap$/,
|
|
55
|
+
/(^|\/)(vitest|jest|bun)\.config\.[cm]?[jt]s$/,
|
|
56
|
+
/(^|\/)tsconfig\.test\.json$/,
|
|
57
|
+
/(^|\/)\.env\.test(\.|$)/
|
|
58
|
+
];
|
|
59
|
+
function isTestOnlyPath(path) {
|
|
60
|
+
const normalized = path.replaceAll("\\", "/");
|
|
61
|
+
return TEST_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
62
|
+
}
|
|
63
|
+
function areFilesTestOnly(paths) {
|
|
64
|
+
return paths.length > 0 && paths.every((path) => isTestOnlyPath(path));
|
|
65
|
+
}
|
|
66
|
+
function canBypassRedPhaseByChangedFiles(changedFiles) {
|
|
67
|
+
return changedFiles.deterministic && areFilesTestOnly(changedFiles.files);
|
|
68
|
+
}
|
|
69
|
+
function hasPackageSourceChanges(changedFiles) {
|
|
70
|
+
const packageSrcPattern = /^packages\/[^/]+\/src\//;
|
|
71
|
+
return changedFiles.files.some((f) => packageSrcPattern.test(f));
|
|
72
|
+
}
|
|
73
|
+
async function printTsdocSummary() {
|
|
74
|
+
const glob = new Bun.Glob("packages/*/src/index.ts");
|
|
75
|
+
const cwd = process.cwd();
|
|
76
|
+
const allDeclarations = [];
|
|
77
|
+
for (const entry of glob.scanSync({ cwd })) {
|
|
78
|
+
const filePath = resolve(cwd, entry);
|
|
79
|
+
const content = await Bun.file(filePath).text();
|
|
80
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
81
|
+
allDeclarations.push(...analyzeSourceFile(sourceFile));
|
|
82
|
+
}
|
|
83
|
+
if (allDeclarations.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
const coverage = calculateCoverage(allDeclarations);
|
|
86
|
+
const parts = [];
|
|
87
|
+
if (coverage.documented > 0)
|
|
88
|
+
parts.push(`${coverage.documented} documented`);
|
|
89
|
+
if (coverage.partial > 0)
|
|
90
|
+
parts.push(`${coverage.partial} partial`);
|
|
91
|
+
if (coverage.undocumented > 0)
|
|
92
|
+
parts.push(`${coverage.undocumented} undocumented`);
|
|
93
|
+
log(`${COLORS.blue}TSDoc${COLORS.reset}: ${coverage.percentage}% coverage (${parts.join(", ")} of ${coverage.total} total)`);
|
|
94
|
+
}
|
|
95
|
+
function resolveBaseRef() {
|
|
96
|
+
const candidates = [
|
|
97
|
+
"origin/main",
|
|
98
|
+
"main",
|
|
99
|
+
"origin/trunk",
|
|
100
|
+
"trunk",
|
|
101
|
+
"origin/master",
|
|
102
|
+
"master"
|
|
103
|
+
];
|
|
104
|
+
for (const candidate of candidates) {
|
|
105
|
+
const resolved = runGit(["rev-parse", "--verify", "--quiet", candidate]);
|
|
106
|
+
if (resolved.ok) {
|
|
107
|
+
return candidate;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
function changedFilesFromRange(range) {
|
|
113
|
+
const result = runGit(["diff", "--name-only", "--diff-filter=d", range]);
|
|
114
|
+
return {
|
|
115
|
+
ok: result.ok,
|
|
116
|
+
files: result.lines
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function getChangedFilesForPush() {
|
|
120
|
+
const upstream = runGit([
|
|
121
|
+
"rev-parse",
|
|
122
|
+
"--abbrev-ref",
|
|
123
|
+
"--symbolic-full-name",
|
|
124
|
+
"@{upstream}"
|
|
125
|
+
]);
|
|
126
|
+
if (upstream.ok && upstream.lines[0]) {
|
|
127
|
+
const rangeResult = changedFilesFromRange(`${upstream.lines[0]}...HEAD`);
|
|
128
|
+
if (rangeResult.ok) {
|
|
129
|
+
return {
|
|
130
|
+
files: rangeResult.files,
|
|
131
|
+
deterministic: true,
|
|
132
|
+
source: "upstream"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const baseRef = resolveBaseRef();
|
|
137
|
+
if (baseRef) {
|
|
138
|
+
const rangeResult = changedFilesFromRange(`${baseRef}...HEAD`);
|
|
139
|
+
if (rangeResult.ok) {
|
|
140
|
+
return {
|
|
141
|
+
files: rangeResult.files,
|
|
142
|
+
deterministic: true,
|
|
143
|
+
source: "baseRef"
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
files: [],
|
|
149
|
+
deterministic: false,
|
|
150
|
+
source: "undetermined"
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function maybeSkipForRedPhase(reason, branch) {
|
|
154
|
+
const changedFiles = getChangedFilesForPush();
|
|
155
|
+
if (!changedFiles.deterministic) {
|
|
156
|
+
log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: could not determine full push diff range`);
|
|
157
|
+
log("Running strict verification.");
|
|
158
|
+
log("");
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (!canBypassRedPhaseByChangedFiles(changedFiles)) {
|
|
162
|
+
log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: changed files are not test-only`);
|
|
163
|
+
if (changedFiles.files.length > 0) {
|
|
164
|
+
log(`Changed files (${changedFiles.source}): ${changedFiles.files.join(", ")}`);
|
|
165
|
+
} else {
|
|
166
|
+
log(`No changed files detected in ${changedFiles.source} range. Running strict verification.`);
|
|
167
|
+
}
|
|
168
|
+
log("");
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (reason === "branch") {
|
|
172
|
+
log(`${COLORS.yellow}TDD RED phase${COLORS.reset} detected: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
173
|
+
} else {
|
|
174
|
+
log(`${COLORS.yellow}Scaffold branch${COLORS.reset} with RED phase branch in context: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
175
|
+
}
|
|
176
|
+
log(`${COLORS.yellow}Skipping strict verification${COLORS.reset} - changed files are test-only`);
|
|
177
|
+
log(`Diff source: ${changedFiles.source}`);
|
|
178
|
+
log("");
|
|
179
|
+
log("Remember: GREEN phase (implementation) must make these tests pass!");
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
function hasRedPhaseBranchInContext(currentBranch) {
|
|
183
|
+
let branches = [];
|
|
184
|
+
try {
|
|
185
|
+
const gtResult = Bun.spawnSync(["gt", "ls"], { stderr: "pipe" });
|
|
186
|
+
if (gtResult.exitCode === 0) {
|
|
187
|
+
branches = gtResult.stdout.toString().split(`
|
|
188
|
+
`).map((line) => line.replace(/^[\u2502\u251C\u2514\u2500\u25C9\u25EF ]*/g, "").replace(/ \(.*/, "")).filter(Boolean);
|
|
189
|
+
}
|
|
190
|
+
} catch {}
|
|
191
|
+
if (branches.length === 0) {
|
|
192
|
+
const gitResult = Bun.spawnSync([
|
|
193
|
+
"git",
|
|
194
|
+
"branch",
|
|
195
|
+
"--list",
|
|
196
|
+
"cli/*",
|
|
197
|
+
"types/*",
|
|
198
|
+
"contracts/*"
|
|
199
|
+
]);
|
|
200
|
+
branches = gitResult.stdout.toString().split(`
|
|
201
|
+
`).map((line) => line.replace(/^[* ]+/, "")).filter(Boolean);
|
|
202
|
+
}
|
|
203
|
+
for (const branch of branches) {
|
|
204
|
+
if (branch === currentBranch)
|
|
205
|
+
continue;
|
|
206
|
+
if (isRedPhaseBranch(branch))
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
function createVerificationPlan(scripts) {
|
|
212
|
+
if (scripts["verify:ci"]) {
|
|
213
|
+
return { ok: true, scripts: ["verify:ci"], source: "verify:ci" };
|
|
214
|
+
}
|
|
215
|
+
const requiredScripts = ["typecheck", "build", "test"];
|
|
216
|
+
const missingRequired = requiredScripts.filter((name) => !scripts[name]);
|
|
217
|
+
const checkOrLint = scripts["check"] ? "check" : scripts["lint"] ? "lint" : undefined;
|
|
218
|
+
if (!checkOrLint || missingRequired.length > 0) {
|
|
219
|
+
const missing = checkOrLint ? missingRequired : [...missingRequired, "check|lint"];
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
error: `Missing required scripts for strict pre-push verification: ${missing.join(", ")}`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
ok: true,
|
|
227
|
+
scripts: ["typecheck", checkOrLint, "build", "test"],
|
|
228
|
+
source: "fallback"
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function readPackageScripts(cwd = process.cwd()) {
|
|
232
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
233
|
+
if (!existsSync(packageJsonPath)) {
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
238
|
+
const scripts = parsed.scripts ?? {};
|
|
239
|
+
const normalized = {};
|
|
240
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
241
|
+
if (typeof value === "string") {
|
|
242
|
+
normalized[name] = value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return normalized;
|
|
246
|
+
} catch {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function runScript(scriptName) {
|
|
251
|
+
log("");
|
|
252
|
+
log(`Running: ${COLORS.blue}bun run ${scriptName}${COLORS.reset}`);
|
|
253
|
+
const result = Bun.spawnSync(["bun", "run", scriptName], {
|
|
254
|
+
stdio: ["inherit", "inherit", "inherit"]
|
|
255
|
+
});
|
|
256
|
+
return result.exitCode === 0;
|
|
257
|
+
}
|
|
258
|
+
function checkBunVersion(projectRoot = process.cwd()) {
|
|
259
|
+
const versionFile = join(projectRoot, ".bun-version");
|
|
260
|
+
if (!existsSync(versionFile)) {
|
|
261
|
+
return { matches: true };
|
|
262
|
+
}
|
|
263
|
+
const expected = readFileSync(versionFile, "utf-8").trim();
|
|
264
|
+
const actual = Bun.version;
|
|
265
|
+
if (expected === actual) {
|
|
266
|
+
return { matches: true };
|
|
267
|
+
}
|
|
268
|
+
return { matches: false, expected, actual };
|
|
269
|
+
}
|
|
270
|
+
async function runPrePush(options = {}) {
|
|
271
|
+
log(`${COLORS.blue}Pre-push verify${COLORS.reset} (TDD-aware)`);
|
|
272
|
+
log("");
|
|
273
|
+
if (options.force) {
|
|
274
|
+
log(`${COLORS.yellow}Force flag set${COLORS.reset} - skipping strict verification`);
|
|
275
|
+
process.exitCode = 0;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const versionCheck = checkBunVersion();
|
|
279
|
+
if (!versionCheck.matches) {
|
|
280
|
+
log(`${COLORS.red}Bun version mismatch${COLORS.reset}: running ${versionCheck.actual}, pinned ${versionCheck.expected}`);
|
|
281
|
+
log("Fix: bunx @outfitter/tooling upgrade-bun");
|
|
282
|
+
log("");
|
|
283
|
+
process.exitCode = 1;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const branch = getCurrentBranch();
|
|
287
|
+
if (isReleaseBranch(branch)) {
|
|
288
|
+
log(`${COLORS.yellow}Release branch detected${COLORS.reset}: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
289
|
+
log(`${COLORS.yellow}Skipping strict verification${COLORS.reset} for automated changeset release push`);
|
|
290
|
+
process.exitCode = 0;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (isRedPhaseBranch(branch)) {
|
|
294
|
+
if (maybeSkipForRedPhase("branch", branch)) {
|
|
295
|
+
process.exitCode = 0;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (isScaffoldBranch(branch)) {
|
|
300
|
+
if (hasRedPhaseBranchInContext(branch)) {
|
|
301
|
+
if (maybeSkipForRedPhase("context", branch)) {
|
|
302
|
+
process.exitCode = 0;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const plan = createVerificationPlan(readPackageScripts());
|
|
308
|
+
if (!plan.ok) {
|
|
309
|
+
log(`${COLORS.red}Strict pre-push verification is not configured${COLORS.reset}`);
|
|
310
|
+
log(plan.error);
|
|
311
|
+
log("");
|
|
312
|
+
log("Add one of:");
|
|
313
|
+
log(" - verify:ci");
|
|
314
|
+
log(" - typecheck + (check or lint) + build + test");
|
|
315
|
+
process.exitCode = 1;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
log(`Running strict verification for branch: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
319
|
+
if (plan.source === "verify:ci") {
|
|
320
|
+
log("Using `verify:ci` script.");
|
|
321
|
+
} else {
|
|
322
|
+
log(`Using fallback scripts: ${plan.scripts.join(" -> ")}`);
|
|
323
|
+
}
|
|
324
|
+
for (const scriptName of plan.scripts) {
|
|
325
|
+
if (runScript(scriptName)) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
log("");
|
|
329
|
+
log(`${COLORS.red}Verification failed${COLORS.reset} on script: ${scriptName}`);
|
|
330
|
+
log("");
|
|
331
|
+
log("If this is intentional TDD RED phase work, name your branch:");
|
|
332
|
+
log(" - feature-tests");
|
|
333
|
+
log(" - feature/tests");
|
|
334
|
+
log(" - feature_tests");
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const changedFiles = getChangedFilesForPush();
|
|
339
|
+
if (hasPackageSourceChanges(changedFiles)) {
|
|
340
|
+
try {
|
|
341
|
+
await printTsdocSummary();
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
log("");
|
|
345
|
+
log(`${COLORS.green}Strict verification passed${COLORS.reset}`);
|
|
346
|
+
process.exitCode = 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export { isRedPhaseBranch, isScaffoldBranch, isReleaseBranch, isTestOnlyPath, areFilesTestOnly, canBypassRedPhaseByChangedFiles, hasPackageSourceChanges, createVerificationPlan, checkBunVersion, runPrePush };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/tooling/src/cli/check-clean-tree.ts
|
|
3
|
+
function parseGitDiff(diffOutput) {
|
|
4
|
+
return diffOutput.split(`
|
|
5
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
6
|
+
}
|
|
7
|
+
function parseUntrackedFiles(lsOutput) {
|
|
8
|
+
return lsOutput.split(`
|
|
9
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
function isCleanTree(status) {
|
|
12
|
+
return status.modified.length === 0 && status.untracked.length === 0;
|
|
13
|
+
}
|
|
14
|
+
var COLORS = {
|
|
15
|
+
reset: "\x1B[0m",
|
|
16
|
+
red: "\x1B[31m",
|
|
17
|
+
green: "\x1B[32m",
|
|
18
|
+
dim: "\x1B[2m"
|
|
19
|
+
};
|
|
20
|
+
async function runCheckCleanTree(options = {}) {
|
|
21
|
+
const pathArgs = options.paths ?? [];
|
|
22
|
+
const diffResult = Bun.spawnSync(["git", "diff", "HEAD", "--name-only", "--", ...pathArgs], { stderr: "pipe" });
|
|
23
|
+
if (diffResult.exitCode !== 0) {
|
|
24
|
+
process.stderr.write(`Failed to run git diff
|
|
25
|
+
`);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const modified = parseGitDiff(diffResult.stdout.toString());
|
|
30
|
+
const lsResult = Bun.spawnSync(["git", "ls-files", "--others", "--exclude-standard", "--", ...pathArgs], { stderr: "pipe" });
|
|
31
|
+
if (lsResult.exitCode !== 0) {
|
|
32
|
+
process.stderr.write(`Failed to run git ls-files
|
|
33
|
+
`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const untracked = parseUntrackedFiles(lsResult.stdout.toString());
|
|
38
|
+
const clean = modified.length === 0 && untracked.length === 0;
|
|
39
|
+
const status = { clean, modified, untracked };
|
|
40
|
+
if (status.clean) {
|
|
41
|
+
process.stdout.write(`${COLORS.green}Working tree is clean.${COLORS.reset}
|
|
42
|
+
`);
|
|
43
|
+
process.exitCode = 0;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
process.stderr.write(`${COLORS.red}Working tree is dirty after verification:${COLORS.reset}
|
|
47
|
+
|
|
48
|
+
`);
|
|
49
|
+
if (modified.length > 0) {
|
|
50
|
+
process.stderr.write(`Modified files:
|
|
51
|
+
`);
|
|
52
|
+
for (const file of modified) {
|
|
53
|
+
process.stderr.write(` ${COLORS.dim}M${COLORS.reset} ${file}
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (untracked.length > 0) {
|
|
58
|
+
process.stderr.write(`Untracked files:
|
|
59
|
+
`);
|
|
60
|
+
for (const file of untracked) {
|
|
61
|
+
process.stderr.write(` ${COLORS.dim}?${COLORS.reset} ${file}
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
process.stderr.write(`
|
|
66
|
+
This likely means a build step produced uncommitted changes.
|
|
67
|
+
`);
|
|
68
|
+
process.stderr.write(`Commit these changes or add them to .gitignore.
|
|
69
|
+
`);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { parseGitDiff, parseUntrackedFiles, isCleanTree, runCheckCleanTree };
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/tooling/src/cli/check-exports.ts
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
function entryToSubpath(entry) {
|
|
5
|
+
const stripped = entry.replace(/^src\//, "").replace(/\.[cm]?[jt]sx?$/, "");
|
|
6
|
+
if (stripped === "index") {
|
|
7
|
+
return ".";
|
|
8
|
+
}
|
|
9
|
+
if (stripped.endsWith("/index")) {
|
|
10
|
+
return `./${stripped.slice(0, -"/index".length)}`;
|
|
11
|
+
}
|
|
12
|
+
return `./${stripped}`;
|
|
13
|
+
}
|
|
14
|
+
function compareExports(input) {
|
|
15
|
+
const { name, actual, expected, path } = input;
|
|
16
|
+
const actualKeys = new Set(Object.keys(actual));
|
|
17
|
+
const expectedKeys = new Set(Object.keys(expected));
|
|
18
|
+
const added = [];
|
|
19
|
+
const removed = [];
|
|
20
|
+
const changed = [];
|
|
21
|
+
for (const key of expectedKeys) {
|
|
22
|
+
if (!actualKeys.has(key)) {
|
|
23
|
+
added.push(key);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const key of actualKeys) {
|
|
27
|
+
if (!expectedKeys.has(key)) {
|
|
28
|
+
removed.push(key);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const key of actualKeys) {
|
|
32
|
+
if (expectedKeys.has(key)) {
|
|
33
|
+
const actualValue = actual[key];
|
|
34
|
+
const expectedValue = expected[key];
|
|
35
|
+
if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) {
|
|
36
|
+
changed.push({ key, expected: expectedValue, actual: actualValue });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
added.sort();
|
|
41
|
+
removed.sort();
|
|
42
|
+
changed.sort((a, b) => a.key.localeCompare(b.key));
|
|
43
|
+
if (added.length === 0 && removed.length === 0 && changed.length === 0) {
|
|
44
|
+
return { name, status: "ok" };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
status: "drift",
|
|
49
|
+
drift: {
|
|
50
|
+
package: name,
|
|
51
|
+
path: path ?? "",
|
|
52
|
+
added,
|
|
53
|
+
removed,
|
|
54
|
+
changed
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function matchesExclude(subpath, excludes) {
|
|
59
|
+
return excludes.some((pattern) => new Bun.Glob(pattern).match(subpath));
|
|
60
|
+
}
|
|
61
|
+
var CLI_EXCLUSION_PATTERNS = [
|
|
62
|
+
"**/cli.ts",
|
|
63
|
+
"**/cli/index.ts",
|
|
64
|
+
"**/bin.ts",
|
|
65
|
+
"**/bin/index.ts"
|
|
66
|
+
];
|
|
67
|
+
function isCliEntrypoint(entry) {
|
|
68
|
+
return CLI_EXCLUSION_PATTERNS.some((pattern) => new Bun.Glob(pattern).match(entry));
|
|
69
|
+
}
|
|
70
|
+
function buildExportValue(entry) {
|
|
71
|
+
const distPath = entry.replace(/^src\//, "").replace(/\.[cm]?[jt]sx?$/, "");
|
|
72
|
+
return {
|
|
73
|
+
import: {
|
|
74
|
+
types: `./dist/${distPath}.d.ts`,
|
|
75
|
+
default: `./dist/${distPath}.js`
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function discoverEntries(packageRoot) {
|
|
80
|
+
const glob = new Bun.Glob("src/**/*.ts");
|
|
81
|
+
const entries = [];
|
|
82
|
+
for (const match of glob.scanSync({ cwd: packageRoot, dot: false })) {
|
|
83
|
+
if (match.includes("__tests__") || match.endsWith(".test.ts")) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
entries.push(match);
|
|
87
|
+
}
|
|
88
|
+
return entries.toSorted();
|
|
89
|
+
}
|
|
90
|
+
function addConfigFileExports(expected, pkg) {
|
|
91
|
+
const CONFIG_RE = /\.(json|jsonc|yml|yaml|toml)$/;
|
|
92
|
+
const configFiles = (pkg.files ?? []).filter((file) => CONFIG_RE.test(file) && file !== "package.json");
|
|
93
|
+
for (const file of configFiles) {
|
|
94
|
+
expected[`./${file}`] = `./${file}`;
|
|
95
|
+
let base = file.replace(CONFIG_RE, "");
|
|
96
|
+
const match = base.match(/^(.+)\.preset(?:\.(.+))?$/);
|
|
97
|
+
if (match?.[1]) {
|
|
98
|
+
base = match[2] ? `${match[1]}-${match[2]}` : match[1];
|
|
99
|
+
}
|
|
100
|
+
if (base !== file) {
|
|
101
|
+
expected[`./${base}`] = `./${file}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function computeExpectedExports(packageRoot, workspace, pkg) {
|
|
106
|
+
const entries = discoverEntries(packageRoot);
|
|
107
|
+
const exportsConfig = typeof workspace.config?.exports === "object" ? workspace.config.exports : undefined;
|
|
108
|
+
const excludes = exportsConfig?.exclude ?? [];
|
|
109
|
+
const customExports = exportsConfig?.customExports ?? {};
|
|
110
|
+
const expected = {};
|
|
111
|
+
const subpathEntries = new Map;
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (isCliEntrypoint(entry))
|
|
114
|
+
continue;
|
|
115
|
+
const subpath = entryToSubpath(entry);
|
|
116
|
+
if (matchesExclude(subpath, excludes))
|
|
117
|
+
continue;
|
|
118
|
+
const existing = subpathEntries.get(subpath);
|
|
119
|
+
if (existing) {
|
|
120
|
+
if (!existing.endsWith("/index.ts") && entry.endsWith("/index.ts")) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
subpathEntries.set(subpath, entry);
|
|
125
|
+
}
|
|
126
|
+
for (const [subpath, entry] of subpathEntries) {
|
|
127
|
+
expected[subpath] = buildExportValue(entry);
|
|
128
|
+
}
|
|
129
|
+
for (const [key, value] of Object.entries(customExports)) {
|
|
130
|
+
expected[`./${key.replace(/^\.\//, "")}`] = value;
|
|
131
|
+
}
|
|
132
|
+
addConfigFileExports(expected, pkg);
|
|
133
|
+
expected["./package.json"] = "./package.json";
|
|
134
|
+
return expected;
|
|
135
|
+
}
|
|
136
|
+
var COLORS = {
|
|
137
|
+
reset: "\x1B[0m",
|
|
138
|
+
red: "\x1B[31m",
|
|
139
|
+
green: "\x1B[32m",
|
|
140
|
+
yellow: "\x1B[33m",
|
|
141
|
+
blue: "\x1B[34m",
|
|
142
|
+
dim: "\x1B[2m"
|
|
143
|
+
};
|
|
144
|
+
function resolveJsonMode(options = {}) {
|
|
145
|
+
return options.json ?? process.env["OUTFITTER_JSON"] === "1";
|
|
146
|
+
}
|
|
147
|
+
async function runCheckExports(options = {}) {
|
|
148
|
+
const cwd = process.cwd();
|
|
149
|
+
const configPath = resolve(cwd, "bunup.config.ts");
|
|
150
|
+
let workspaces;
|
|
151
|
+
try {
|
|
152
|
+
const configModule = await import(configPath);
|
|
153
|
+
const rawConfig = configModule.default;
|
|
154
|
+
if (!Array.isArray(rawConfig)) {
|
|
155
|
+
process.stderr.write(`bunup.config.ts must export a workspace array
|
|
156
|
+
`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
workspaces = rawConfig;
|
|
161
|
+
} catch {
|
|
162
|
+
process.stderr.write(`Could not load bunup.config.ts from ${cwd}
|
|
163
|
+
`);
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const results = [];
|
|
168
|
+
for (const workspace of workspaces) {
|
|
169
|
+
const packageRoot = resolve(cwd, workspace.root);
|
|
170
|
+
const pkgPath = resolve(packageRoot, "package.json");
|
|
171
|
+
let pkg;
|
|
172
|
+
try {
|
|
173
|
+
pkg = await Bun.file(pkgPath).json();
|
|
174
|
+
} catch {
|
|
175
|
+
results.push({ name: workspace.name, status: "ok" });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const actual = typeof pkg.exports === "object" && pkg.exports !== null ? pkg.exports : {};
|
|
179
|
+
const expected = computeExpectedExports(packageRoot, workspace, pkg);
|
|
180
|
+
results.push(compareExports({
|
|
181
|
+
name: workspace.name,
|
|
182
|
+
actual,
|
|
183
|
+
expected,
|
|
184
|
+
path: workspace.root
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
const checkResult = {
|
|
188
|
+
ok: results.every((r) => r.status === "ok"),
|
|
189
|
+
packages: results
|
|
190
|
+
};
|
|
191
|
+
if (resolveJsonMode(options)) {
|
|
192
|
+
process.stdout.write(`${JSON.stringify(checkResult, null, 2)}
|
|
193
|
+
`);
|
|
194
|
+
} else {
|
|
195
|
+
const drifted = results.filter((r) => r.status === "drift");
|
|
196
|
+
if (drifted.length === 0) {
|
|
197
|
+
process.stdout.write(`${COLORS.green}All ${results.length} packages have exports in sync.${COLORS.reset}
|
|
198
|
+
`);
|
|
199
|
+
} else {
|
|
200
|
+
process.stderr.write(`${COLORS.red}Export drift detected in ${drifted.length} package(s):${COLORS.reset}
|
|
201
|
+
|
|
202
|
+
`);
|
|
203
|
+
for (const result of drifted) {
|
|
204
|
+
const drift = result.drift;
|
|
205
|
+
if (!drift)
|
|
206
|
+
continue;
|
|
207
|
+
process.stderr.write(` ${COLORS.yellow}${result.name}${COLORS.reset} ${COLORS.dim}(${drift.path})${COLORS.reset}
|
|
208
|
+
`);
|
|
209
|
+
for (const key of drift.added) {
|
|
210
|
+
process.stderr.write(` ${COLORS.green}+ ${key}${COLORS.reset} ${COLORS.dim}(missing from package.json)${COLORS.reset}
|
|
211
|
+
`);
|
|
212
|
+
}
|
|
213
|
+
for (const key of drift.removed) {
|
|
214
|
+
process.stderr.write(` ${COLORS.red}- ${key}${COLORS.reset} ${COLORS.dim}(not in source)${COLORS.reset}
|
|
215
|
+
`);
|
|
216
|
+
}
|
|
217
|
+
for (const entry of drift.changed) {
|
|
218
|
+
process.stderr.write(` ${COLORS.yellow}~ ${entry.key}${COLORS.reset} ${COLORS.dim}(value mismatch)${COLORS.reset}
|
|
219
|
+
`);
|
|
220
|
+
}
|
|
221
|
+
process.stderr.write(`
|
|
222
|
+
`);
|
|
223
|
+
}
|
|
224
|
+
process.stderr.write(`Run ${COLORS.blue}bun run build${COLORS.reset} to regenerate exports.
|
|
225
|
+
`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
process.exitCode = checkResult.ok ? 0 : 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export { entryToSubpath, compareExports, resolveJsonMode, runCheckExports };
|