@outfitter/tooling 0.2.3 → 0.3.0

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 (35) hide show
  1. package/README.md +69 -1
  2. package/biome.json +1 -1
  3. package/dist/cli/check-changeset.d.ts +12 -10
  4. package/dist/cli/check-changeset.js +7 -1
  5. package/dist/cli/check-exports.d.ts +2 -2
  6. package/dist/cli/check-exports.js +3 -1
  7. package/dist/cli/check-readme-imports.d.ts +3 -2
  8. package/dist/cli/check-readme-imports.js +5 -1
  9. package/dist/cli/check-tsdoc.d.ts +2 -0
  10. package/dist/cli/check-tsdoc.js +36 -0
  11. package/dist/cli/index.js +208 -51
  12. package/dist/cli/pre-push.d.ts +8 -1
  13. package/dist/cli/pre-push.js +6 -1
  14. package/dist/index.d.ts +79 -4
  15. package/dist/index.js +17 -6
  16. package/dist/registry/build.d.ts +1 -1
  17. package/dist/registry/build.js +8 -5
  18. package/dist/registry/index.d.ts +2 -2
  19. package/dist/registry/index.js +1 -1
  20. package/dist/registry/schema.d.ts +1 -1
  21. package/dist/registry/schema.js +1 -1
  22. package/dist/shared/@outfitter/{tooling-8sd32ts6.js → tooling-2n2dpsaa.js} +48 -2
  23. package/dist/shared/@outfitter/tooling-cj5vsa9k.js +184 -0
  24. package/dist/shared/@outfitter/{tooling-tf22zt9p.js → tooling-enjcenja.js} +5 -2
  25. package/dist/shared/@outfitter/tooling-njw4z34x.d.ts +140 -0
  26. package/dist/shared/@outfitter/tooling-qk5xgmxr.js +405 -0
  27. package/dist/shared/@outfitter/{tooling-q0d60xb3.d.ts → tooling-wesswf21.d.ts} +2 -1
  28. package/dist/shared/@outfitter/{tooling-g83d0kjv.js → tooling-wv09k6hr.js} +3 -3
  29. package/dist/shared/{chunk-6a7tjcgm.js → chunk-7tdgbqb0.js} +5 -1
  30. package/dist/shared/chunk-cmde0fwx.js +421 -0
  31. package/dist/version.d.ts +1 -1
  32. package/package.json +14 -6
  33. package/registry/registry.json +6 -6
  34. package/dist/shared/@outfitter/tooling-3w8vr2w3.js +0 -94
  35. package/dist/shared/chunk-8aenrm6f.js +0 -18
@@ -0,0 +1,405 @@
1
+ // @bun
2
+ import {
3
+ __require
4
+ } from "./tooling-dvwh9qve.js";
5
+
6
+ // packages/tooling/src/cli/check-tsdoc.ts
7
+ import { resolve } from "path";
8
+ import ts from "typescript";
9
+ import { z } from "zod";
10
+ var coverageLevelSchema = z.enum([
11
+ "documented",
12
+ "partial",
13
+ "undocumented"
14
+ ]);
15
+ var declarationCoverageSchema = z.object({
16
+ name: z.string(),
17
+ kind: z.string(),
18
+ level: coverageLevelSchema,
19
+ file: z.string(),
20
+ line: z.number()
21
+ });
22
+ var coverageSummarySchema = z.object({
23
+ documented: z.number(),
24
+ partial: z.number(),
25
+ undocumented: z.number(),
26
+ total: z.number(),
27
+ percentage: z.number()
28
+ });
29
+ var packageCoverageSchema = z.object({
30
+ name: z.string(),
31
+ path: z.string(),
32
+ declarations: z.array(declarationCoverageSchema),
33
+ documented: z.number(),
34
+ partial: z.number(),
35
+ undocumented: z.number(),
36
+ total: z.number(),
37
+ percentage: z.number()
38
+ });
39
+ var tsDocCheckResultSchema = z.object({
40
+ ok: z.boolean(),
41
+ packages: z.array(packageCoverageSchema),
42
+ summary: coverageSummarySchema
43
+ });
44
+ function isExportedDeclaration(node) {
45
+ if (ts.isExportDeclaration(node))
46
+ return false;
47
+ if (ts.isExportAssignment(node))
48
+ return false;
49
+ const isDeclaration = ts.isFunctionDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node) || ts.isEnumDeclaration(node) || ts.isVariableStatement(node);
50
+ if (!isDeclaration)
51
+ return false;
52
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
53
+ if (!modifiers)
54
+ return false;
55
+ return modifiers.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword);
56
+ }
57
+ function getDeclarationName(node) {
58
+ if (ts.isVariableStatement(node)) {
59
+ const decl = node.declarationList.declarations[0];
60
+ if (decl && ts.isIdentifier(decl.name)) {
61
+ return decl.name.text;
62
+ }
63
+ return;
64
+ }
65
+ if (ts.isFunctionDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node) || ts.isEnumDeclaration(node)) {
66
+ return node.name?.text;
67
+ }
68
+ return;
69
+ }
70
+ function getDeclarationKind(node) {
71
+ if (ts.isFunctionDeclaration(node))
72
+ return "function";
73
+ if (ts.isInterfaceDeclaration(node))
74
+ return "interface";
75
+ if (ts.isTypeAliasDeclaration(node))
76
+ return "type";
77
+ if (ts.isClassDeclaration(node))
78
+ return "class";
79
+ if (ts.isEnumDeclaration(node))
80
+ return "enum";
81
+ if (ts.isVariableStatement(node))
82
+ return "variable";
83
+ return "unknown";
84
+ }
85
+ function hasJSDocComment(node, sourceFile) {
86
+ const sourceText = sourceFile.getFullText();
87
+ const ranges = ts.getLeadingCommentRanges(sourceText, node.getFullStart());
88
+ if (!ranges)
89
+ return false;
90
+ return ranges.some((range) => {
91
+ if (range.kind !== ts.SyntaxKind.MultiLineCommentTrivia)
92
+ return false;
93
+ const text = sourceText.slice(range.pos, range.end);
94
+ return text.startsWith("/**");
95
+ });
96
+ }
97
+ function memberHasJSDoc(member, sourceFile) {
98
+ const sourceText = sourceFile.getFullText();
99
+ const ranges = ts.getLeadingCommentRanges(sourceText, member.getFullStart());
100
+ if (!ranges)
101
+ return false;
102
+ return ranges.some((range) => {
103
+ if (range.kind !== ts.SyntaxKind.MultiLineCommentTrivia)
104
+ return false;
105
+ const text = sourceText.slice(range.pos, range.end);
106
+ return text.startsWith("/**");
107
+ });
108
+ }
109
+ function classifyDeclaration(node, sourceFile) {
110
+ const hasDoc = hasJSDocComment(node, sourceFile);
111
+ if (!hasDoc)
112
+ return "undocumented";
113
+ if (ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node)) {
114
+ const members = node.members;
115
+ if (members.length > 0) {
116
+ const allMembersDocumented = members.every((member) => memberHasJSDoc(member, sourceFile));
117
+ if (!allMembersDocumented)
118
+ return "partial";
119
+ }
120
+ }
121
+ return "documented";
122
+ }
123
+ function analyzeSourceFile(sourceFile) {
124
+ const results = [];
125
+ for (const statement of sourceFile.statements) {
126
+ if (!isExportedDeclaration(statement))
127
+ continue;
128
+ const name = getDeclarationName(statement);
129
+ if (!name)
130
+ continue;
131
+ const kind = getDeclarationKind(statement);
132
+ const level = classifyDeclaration(statement, sourceFile);
133
+ const { line } = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
134
+ results.push({
135
+ name,
136
+ kind,
137
+ level,
138
+ file: sourceFile.fileName,
139
+ line: line + 1
140
+ });
141
+ }
142
+ return results;
143
+ }
144
+ function calculateCoverage(declarations) {
145
+ const total = declarations.length;
146
+ if (total === 0) {
147
+ return {
148
+ documented: 0,
149
+ partial: 0,
150
+ undocumented: 0,
151
+ total: 0,
152
+ percentage: 100
153
+ };
154
+ }
155
+ const documented = declarations.filter((d) => d.level === "documented").length;
156
+ const partial = declarations.filter((d) => d.level === "partial").length;
157
+ const undocumented = declarations.filter((d) => d.level === "undocumented").length;
158
+ const score = documented + partial * 0.5;
159
+ const percentage = Math.round(score / total * 100);
160
+ return { documented, partial, undocumented, total, percentage };
161
+ }
162
+ var COLORS = {
163
+ reset: "\x1B[0m",
164
+ red: "\x1B[31m",
165
+ green: "\x1B[32m",
166
+ yellow: "\x1B[33m",
167
+ blue: "\x1B[34m",
168
+ dim: "\x1B[2m",
169
+ bold: "\x1B[1m"
170
+ };
171
+ function resolveJsonMode(options = {}) {
172
+ return options.json ?? process.env["OUTFITTER_JSON"] === "1";
173
+ }
174
+ function bar(percentage, width = 20) {
175
+ const filled = Math.round(percentage / 100 * width);
176
+ const empty = width - filled;
177
+ const color = percentage >= 80 ? COLORS.green : percentage >= 50 ? COLORS.yellow : COLORS.red;
178
+ return `${color}${"\u2588".repeat(filled)}${COLORS.dim}${"\u2591".repeat(empty)}${COLORS.reset}`;
179
+ }
180
+ function discoverPackages(cwd) {
181
+ const packages = [];
182
+ const seenEntryPoints = new Set;
183
+ for (const pattern of ["packages/*/src/index.ts", "apps/*/src/index.ts"]) {
184
+ const glob = new Bun.Glob(pattern);
185
+ for (const match of glob.scanSync({ cwd, dot: false })) {
186
+ const parts = match.split("/");
187
+ const rootDir = parts[0];
188
+ const pkgDir = parts[1];
189
+ if (!rootDir || !pkgDir)
190
+ continue;
191
+ const entryPoint = resolve(cwd, match);
192
+ if (seenEntryPoints.has(entryPoint)) {
193
+ continue;
194
+ }
195
+ seenEntryPoints.add(entryPoint);
196
+ const pkgRoot = resolve(cwd, rootDir, pkgDir);
197
+ let pkgName = pkgDir;
198
+ try {
199
+ const pkgJson = JSON.parse(__require("fs").readFileSync(resolve(pkgRoot, "package.json"), "utf-8"));
200
+ if (pkgJson.name)
201
+ pkgName = pkgJson.name;
202
+ } catch {}
203
+ packages.push({
204
+ name: pkgName,
205
+ path: pkgRoot,
206
+ entryPoint
207
+ });
208
+ }
209
+ }
210
+ if (packages.length === 0) {
211
+ const entryPoint = resolve(cwd, "src/index.ts");
212
+ try {
213
+ __require("fs").accessSync(entryPoint);
214
+ let pkgName = "root";
215
+ try {
216
+ const pkgJson = JSON.parse(__require("fs").readFileSync(resolve(cwd, "package.json"), "utf-8"));
217
+ if (pkgJson.name)
218
+ pkgName = pkgJson.name;
219
+ } catch {}
220
+ packages.push({
221
+ name: pkgName,
222
+ path: cwd,
223
+ entryPoint
224
+ });
225
+ seenEntryPoints.add(entryPoint);
226
+ } catch {}
227
+ }
228
+ return packages.sort((a, b) => a.name.localeCompare(b.name));
229
+ }
230
+ function collectReExportedSourceFiles(sourceFile, program, pkgPath) {
231
+ const result = [];
232
+ const seen = new Set;
233
+ for (const statement of sourceFile.statements) {
234
+ if (!ts.isExportDeclaration(statement))
235
+ continue;
236
+ if (!statement.moduleSpecifier)
237
+ continue;
238
+ if (!ts.isStringLiteral(statement.moduleSpecifier))
239
+ continue;
240
+ const specifier = statement.moduleSpecifier.text;
241
+ if (!specifier.startsWith("."))
242
+ continue;
243
+ const resolvedModule = ts.resolveModuleName(specifier, sourceFile.fileName, program.getCompilerOptions(), ts.sys);
244
+ const resolvedFileName = resolvedModule.resolvedModule?.resolvedFileName;
245
+ if (!resolvedFileName)
246
+ continue;
247
+ if (!resolvedFileName.startsWith(pkgPath))
248
+ continue;
249
+ if (seen.has(resolvedFileName))
250
+ continue;
251
+ seen.add(resolvedFileName);
252
+ const sf = program.getSourceFile(resolvedFileName);
253
+ if (sf)
254
+ result.push(sf);
255
+ }
256
+ return result;
257
+ }
258
+ function analyzePackage(pkg, workspaceCwd) {
259
+ try {
260
+ __require("fs").accessSync(pkg.entryPoint);
261
+ } catch {
262
+ return {
263
+ name: pkg.name,
264
+ path: pkg.path,
265
+ declarations: [],
266
+ documented: 0,
267
+ partial: 0,
268
+ undocumented: 0,
269
+ total: 0,
270
+ percentage: 0
271
+ };
272
+ }
273
+ let tsconfigPath = resolve(pkg.path, "tsconfig.json");
274
+ try {
275
+ __require("fs").accessSync(tsconfigPath);
276
+ } catch {
277
+ tsconfigPath = resolve(workspaceCwd, "tsconfig.json");
278
+ }
279
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
280
+ const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, pkg.path);
281
+ const program = ts.createProgram({
282
+ rootNames: [pkg.entryPoint],
283
+ options: parsedConfig.options,
284
+ host: ts.createCompilerHost(parsedConfig.options)
285
+ });
286
+ const sourceFile = program.getSourceFile(pkg.entryPoint);
287
+ if (!sourceFile) {
288
+ return {
289
+ name: pkg.name,
290
+ path: pkg.path,
291
+ declarations: [],
292
+ documented: 0,
293
+ partial: 0,
294
+ undocumented: 0,
295
+ total: 0,
296
+ percentage: 0
297
+ };
298
+ }
299
+ const declarations = analyzeSourceFile(sourceFile);
300
+ const reExportedFiles = collectReExportedSourceFiles(sourceFile, program, pkg.path);
301
+ for (const sf of reExportedFiles) {
302
+ declarations.push(...analyzeSourceFile(sf));
303
+ }
304
+ const stats = calculateCoverage(declarations);
305
+ return {
306
+ name: pkg.name,
307
+ path: pkg.path,
308
+ declarations,
309
+ ...stats
310
+ };
311
+ }
312
+ function analyzeCheckTsdoc(options = {}) {
313
+ const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
314
+ const minCoverage = options.minCoverage ?? 0;
315
+ let packages;
316
+ if (options.paths && options.paths.length > 0) {
317
+ packages = options.paths.map((p) => {
318
+ const absPath = resolve(cwd, p);
319
+ const entryPoint = resolve(absPath, "src/index.ts");
320
+ let name = p;
321
+ try {
322
+ const pkgJson = JSON.parse(__require("fs").readFileSync(resolve(absPath, "package.json"), "utf-8"));
323
+ if (pkgJson.name)
324
+ name = pkgJson.name;
325
+ } catch {}
326
+ return { name, path: absPath, entryPoint };
327
+ });
328
+ } else {
329
+ packages = discoverPackages(cwd);
330
+ }
331
+ if (packages.length === 0) {
332
+ return null;
333
+ }
334
+ const packageResults = [];
335
+ for (const pkg of packages) {
336
+ packageResults.push(analyzePackage(pkg, cwd));
337
+ }
338
+ const allDeclarations = packageResults.flatMap((p) => p.declarations);
339
+ const summary = calculateCoverage(allDeclarations);
340
+ const ok = !options.strict || summary.percentage >= minCoverage;
341
+ return {
342
+ ok,
343
+ packages: packageResults,
344
+ summary
345
+ };
346
+ }
347
+ function printCheckTsdocHuman(result, options) {
348
+ process.stdout.write(`
349
+ ${COLORS.bold}TSDoc Coverage Report${COLORS.reset}
350
+
351
+ `);
352
+ for (const pkg of result.packages) {
353
+ const color = pkg.percentage >= 80 ? COLORS.green : pkg.percentage >= 50 ? COLORS.yellow : COLORS.red;
354
+ process.stdout.write(` ${color}${pkg.percentage.toString().padStart(3)}%${COLORS.reset} ${bar(pkg.percentage)} ${pkg.name}
355
+ `);
356
+ if (pkg.total > 0) {
357
+ const parts = [];
358
+ if (pkg.documented > 0)
359
+ parts.push(`${COLORS.green}${pkg.documented} documented${COLORS.reset}`);
360
+ if (pkg.partial > 0)
361
+ parts.push(`${COLORS.yellow}${pkg.partial} partial${COLORS.reset}`);
362
+ if (pkg.undocumented > 0)
363
+ parts.push(`${COLORS.red}${pkg.undocumented} undocumented${COLORS.reset}`);
364
+ process.stdout.write(` ${COLORS.dim}${pkg.total} declarations:${COLORS.reset} ${parts.join(", ")}
365
+ `);
366
+ } else {
367
+ process.stdout.write(` ${COLORS.dim}no exported declarations${COLORS.reset}
368
+ `);
369
+ }
370
+ }
371
+ const { summary } = result;
372
+ process.stdout.write(`
373
+ ${COLORS.bold}Summary:${COLORS.reset} ${summary.percentage}% coverage (${summary.documented} documented, ${summary.partial} partial, ${summary.undocumented} undocumented of ${summary.total} total)
374
+ `);
375
+ const minCoverage = options?.minCoverage ?? 0;
376
+ if (options?.strict && summary.percentage < minCoverage) {
377
+ process.stderr.write(`
378
+ ${COLORS.red}Coverage ${summary.percentage}% is below minimum threshold of ${minCoverage}%${COLORS.reset}
379
+ `);
380
+ }
381
+ process.stdout.write(`
382
+ `);
383
+ }
384
+ async function runCheckTsdoc(options = {}) {
385
+ const result = analyzeCheckTsdoc(options);
386
+ if (!result) {
387
+ process.stderr.write(`No packages found with src/index.ts entry points.
388
+ ` + `Searched: packages/*/src/index.ts, apps/*/src/index.ts, src/index.ts
389
+ ` + `Use --package <path> to specify a package path explicitly.
390
+ `);
391
+ process.exit(1);
392
+ }
393
+ if (resolveJsonMode(options)) {
394
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
395
+ `);
396
+ } else {
397
+ printCheckTsdocHuman(result, {
398
+ strict: options.strict,
399
+ minCoverage: options.minCoverage
400
+ });
401
+ }
402
+ process.exit(result.ok ? 0 : 1);
403
+ }
404
+
405
+ export { coverageLevelSchema, declarationCoverageSchema, coverageSummarySchema, packageCoverageSchema, tsDocCheckResultSchema, isExportedDeclaration, getDeclarationName, getDeclarationKind, classifyDeclaration, analyzeSourceFile, calculateCoverage, resolveJsonMode, analyzeCheckTsdoc, printCheckTsdocHuman, runCheckTsdoc };
@@ -49,10 +49,11 @@ declare function compareExports(input: CompareInput): PackageResult;
49
49
  interface CheckExportsOptions {
50
50
  readonly json?: boolean;
51
51
  }
52
+ declare function resolveJsonMode(options?: CheckExportsOptions): boolean;
52
53
  /**
53
54
  * Run check-exports across all workspace packages.
54
55
  *
55
56
  * Reads the bunup workspace config to discover packages and their * settings, then compares expected vs actual exports in each package.json.
56
57
  */
57
58
  declare function runCheckExports(options?: CheckExportsOptions): Promise<void>;
58
- export { ExportMap, ExportDrift, PackageResult, CheckResult, CompareInput, entryToSubpath, compareExports, CheckExportsOptions, runCheckExports };
59
+ export { ExportMap, ExportDrift, PackageResult, CheckResult, CompareInput, entryToSubpath, compareExports, CheckExportsOptions, resolveJsonMode, runCheckExports };
@@ -11,13 +11,13 @@ var BlockSchema = z.object({
11
11
  name: z.string().min(1),
12
12
  description: z.string().min(1),
13
13
  files: z.array(FileEntrySchema).optional(),
14
- dependencies: z.record(z.string()).optional(),
15
- devDependencies: z.record(z.string()).optional(),
14
+ dependencies: z.record(z.string(), z.string()).optional(),
15
+ devDependencies: z.record(z.string(), z.string()).optional(),
16
16
  extends: z.array(z.string()).optional()
17
17
  });
18
18
  var RegistrySchema = z.object({
19
19
  version: z.string(),
20
- blocks: z.record(BlockSchema)
20
+ blocks: z.record(z.string(), BlockSchema)
21
21
  });
22
22
 
23
23
  export { FileEntrySchema, BlockSchema, RegistrySchema };
@@ -84,6 +84,9 @@ var COLORS = {
84
84
  blue: "\x1B[34m",
85
85
  dim: "\x1B[2m"
86
86
  };
87
+ function resolveJsonMode(options = {}) {
88
+ return options.json ?? process.env["OUTFITTER_JSON"] === "1";
89
+ }
87
90
  async function runCheckReadmeImports(options = {}) {
88
91
  const cwd = process.cwd();
89
92
  const readmeGlob = new Bun.Glob("**/README.md");
@@ -150,7 +153,7 @@ async function runCheckReadmeImports(options = {}) {
150
153
  hasInvalid = true;
151
154
  }
152
155
  }
153
- if (options.json) {
156
+ if (resolveJsonMode(options)) {
154
157
  const output = {
155
158
  ok: !hasInvalid,
156
159
  files: results
@@ -187,6 +190,7 @@ async function runCheckReadmeImports(options = {}) {
187
190
  }
188
191
  export {
189
192
  runCheckReadmeImports,
193
+ resolveJsonMode,
190
194
  parseSpecifier,
191
195
  isExportedSubpath,
192
196
  extractImports