@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
@@ -1,2 +1,2 @@
1
- import { AddBlockOptions, AddBlockResult, Block, BlockDefinition, BlockSchema, FileEntry, FileEntrySchema, Registry, RegistryBuildConfig, RegistrySchema } from "../shared/@outfitter/tooling-sjm8nebx";
1
+ import { AddBlockOptions, AddBlockResult, Block, BlockDefinition, BlockSchema, FileEntry, FileEntrySchema, Registry, RegistryBuildConfig, RegistrySchema } from "../shared/@outfitter/tooling-sjm8nebx.js";
2
2
  export { RegistrySchema, RegistryBuildConfig, Registry, FileEntrySchema, FileEntry, BlockSchema, BlockDefinition, Block, AddBlockResult, AddBlockOptions };
@@ -3,7 +3,7 @@ import {
3
3
  BlockSchema,
4
4
  FileEntrySchema,
5
5
  RegistrySchema
6
- } from "../shared/@outfitter/tooling-g83d0kjv.js";
6
+ } from "../shared/@outfitter/tooling-wv09k6hr.js";
7
7
  import"../shared/@outfitter/tooling-dvwh9qve.js";
8
8
  export {
9
9
  RegistrySchema,
@@ -1,7 +1,13 @@
1
1
  // @bun
2
+ import {
3
+ analyzeSourceFile,
4
+ calculateCoverage
5
+ } from "./tooling-qk5xgmxr.js";
6
+
2
7
  // packages/tooling/src/cli/pre-push.ts
3
8
  import { existsSync, readFileSync } from "fs";
4
- import { join } from "path";
9
+ import { join, resolve } from "path";
10
+ import ts from "typescript";
5
11
  var COLORS = {
6
12
  reset: "\x1B[0m",
7
13
  red: "\x1B[31m",
@@ -38,6 +44,9 @@ function isRedPhaseBranch(branch) {
38
44
  function isScaffoldBranch(branch) {
39
45
  return branch.endsWith("-scaffold") || branch.endsWith("/scaffold") || branch.endsWith("_scaffold");
40
46
  }
47
+ function isReleaseBranch(branch) {
48
+ return branch.startsWith("changeset-release/");
49
+ }
41
50
  var TEST_PATH_PATTERNS = [
42
51
  /(^|\/)__tests__\//,
43
52
  /(^|\/)__snapshots__\//,
@@ -57,6 +66,32 @@ function areFilesTestOnly(paths) {
57
66
  function canBypassRedPhaseByChangedFiles(changedFiles) {
58
67
  return changedFiles.deterministic && areFilesTestOnly(changedFiles.files);
59
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
+ }
60
95
  function resolveBaseRef() {
61
96
  const candidates = [
62
97
  "origin/main",
@@ -224,6 +259,11 @@ async function runPrePush(options = {}) {
224
259
  log(`${COLORS.blue}Pre-push verify${COLORS.reset} (TDD-aware)`);
225
260
  log("");
226
261
  const branch = getCurrentBranch();
262
+ if (isReleaseBranch(branch)) {
263
+ log(`${COLORS.yellow}Release branch detected${COLORS.reset}: ${COLORS.blue}${branch}${COLORS.reset}`);
264
+ log(`${COLORS.yellow}Skipping strict verification${COLORS.reset} for automated changeset release push`);
265
+ process.exit(0);
266
+ }
227
267
  if (isRedPhaseBranch(branch)) {
228
268
  if (maybeSkipForRedPhase("branch", branch)) {
229
269
  process.exit(0);
@@ -269,9 +309,15 @@ async function runPrePush(options = {}) {
269
309
  log(" - feature_tests");
270
310
  process.exit(1);
271
311
  }
312
+ const changedFiles = getChangedFilesForPush();
313
+ if (hasPackageSourceChanges(changedFiles)) {
314
+ try {
315
+ await printTsdocSummary();
316
+ } catch {}
317
+ }
272
318
  log("");
273
319
  log(`${COLORS.green}Strict verification passed${COLORS.reset}`);
274
320
  process.exit(0);
275
321
  }
276
322
 
277
- export { isRedPhaseBranch, isScaffoldBranch, isTestOnlyPath, areFilesTestOnly, canBypassRedPhaseByChangedFiles, createVerificationPlan, runPrePush };
323
+ export { isRedPhaseBranch, isScaffoldBranch, isReleaseBranch, isTestOnlyPath, areFilesTestOnly, canBypassRedPhaseByChangedFiles, hasPackageSourceChanges, createVerificationPlan, runPrePush };
@@ -0,0 +1,184 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/check-changeset.ts
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { join } from "path";
5
+ function getChangedPackagePaths(files) {
6
+ const packageNames = new Set;
7
+ const pattern = /^packages\/([^/]+)\/src\//;
8
+ for (const file of files) {
9
+ const match = pattern.exec(file);
10
+ if (match?.[1]) {
11
+ packageNames.add(match[1]);
12
+ }
13
+ }
14
+ return [...packageNames].sort();
15
+ }
16
+ function getChangedChangesetFiles(files) {
17
+ const pattern = /^\.changeset\/([^/]+\.md)$/;
18
+ const results = [];
19
+ for (const file of files) {
20
+ const match = pattern.exec(file);
21
+ if (match?.[1] && match[1] !== "README.md") {
22
+ results.push(match[1]);
23
+ }
24
+ }
25
+ return results.sort();
26
+ }
27
+ function checkChangesetRequired(changedPackages, changesetFiles) {
28
+ if (changedPackages.length === 0) {
29
+ return { ok: true, missingFor: [] };
30
+ }
31
+ if (changesetFiles.length > 0) {
32
+ return { ok: true, missingFor: [] };
33
+ }
34
+ return { ok: false, missingFor: changedPackages };
35
+ }
36
+ function parseIgnoredPackagesFromChangesetConfig(jsonContent) {
37
+ try {
38
+ const parsed = JSON.parse(jsonContent);
39
+ if (!Array.isArray(parsed.ignore)) {
40
+ return [];
41
+ }
42
+ return parsed.ignore.filter((entry) => typeof entry === "string");
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+ function parseChangesetFrontmatterPackageNames(markdownContent) {
48
+ const frontmatterMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdownContent);
49
+ if (!frontmatterMatch?.[1]) {
50
+ return [];
51
+ }
52
+ const packages = new Set;
53
+ for (const line of frontmatterMatch[1].split(/\r?\n/)) {
54
+ const trimmed = line.trim();
55
+ const match = /^(["']?)(@[^"':\s]+\/[^"':\s]+)\1\s*:/.exec(trimmed);
56
+ if (match?.[2]) {
57
+ packages.add(match[2]);
58
+ }
59
+ }
60
+ return [...packages].sort();
61
+ }
62
+ function findIgnoredPackageReferences(input) {
63
+ if (input.ignoredPackages.length === 0 || input.changesetFiles.length === 0) {
64
+ return [];
65
+ }
66
+ const ignored = new Set(input.ignoredPackages);
67
+ const results = [];
68
+ for (const file of input.changesetFiles) {
69
+ const content = input.readChangesetFile(file);
70
+ const referencedPackages = parseChangesetFrontmatterPackageNames(content);
71
+ const invalidReferences = referencedPackages.filter((pkg) => ignored.has(pkg));
72
+ if (invalidReferences.length > 0) {
73
+ results.push({ file, packages: invalidReferences.sort() });
74
+ }
75
+ }
76
+ return results.sort((a, b) => a.file.localeCompare(b.file));
77
+ }
78
+ function loadIgnoredPackages(cwd) {
79
+ const configPath = join(cwd, ".changeset", "config.json");
80
+ if (!existsSync(configPath)) {
81
+ return [];
82
+ }
83
+ try {
84
+ return parseIgnoredPackagesFromChangesetConfig(readFileSync(configPath, "utf-8"));
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+ function getIgnoredReferencesForChangedChangesets(cwd, changesetFiles) {
90
+ const ignoredPackages = loadIgnoredPackages(cwd);
91
+ return findIgnoredPackageReferences({
92
+ changesetFiles,
93
+ ignoredPackages,
94
+ readChangesetFile: (filename) => {
95
+ try {
96
+ return readFileSync(join(cwd, ".changeset", filename), "utf-8");
97
+ } catch {
98
+ return "";
99
+ }
100
+ }
101
+ });
102
+ }
103
+ var COLORS = {
104
+ reset: "\x1B[0m",
105
+ red: "\x1B[31m",
106
+ green: "\x1B[32m",
107
+ yellow: "\x1B[33m",
108
+ blue: "\x1B[34m",
109
+ dim: "\x1B[2m"
110
+ };
111
+ async function runCheckChangeset(options = {}) {
112
+ if (options.skip || process.env["NO_CHANGESET"] === "1") {
113
+ process.stdout.write(`${COLORS.dim}check-changeset skipped (NO_CHANGESET=1)${COLORS.reset}
114
+ `);
115
+ process.exit(0);
116
+ }
117
+ if (process.env["GITHUB_EVENT_NAME"] === "push") {
118
+ process.stdout.write(`${COLORS.dim}check-changeset skipped (push event)${COLORS.reset}
119
+ `);
120
+ process.exit(0);
121
+ }
122
+ const cwd = process.cwd();
123
+ let changedFiles;
124
+ try {
125
+ const proc = Bun.spawnSync(["git", "diff", "--name-only", "origin/main...HEAD"], { cwd });
126
+ if (proc.exitCode !== 0) {
127
+ process.exit(0);
128
+ }
129
+ changedFiles = proc.stdout.toString().trim().split(`
130
+ `).filter((line) => line.length > 0);
131
+ } catch {
132
+ process.exit(0);
133
+ }
134
+ const changedPackages = getChangedPackagePaths(changedFiles);
135
+ if (changedPackages.length === 0) {
136
+ process.stdout.write(`${COLORS.green}No package source changes detected.${COLORS.reset}
137
+ `);
138
+ process.exit(0);
139
+ }
140
+ const changesetFiles = getChangedChangesetFiles(changedFiles);
141
+ const check = checkChangesetRequired(changedPackages, changesetFiles);
142
+ if (!check.ok) {
143
+ process.stderr.write(`${COLORS.red}Missing changeset!${COLORS.reset}
144
+
145
+ `);
146
+ process.stderr.write(`The following packages have source changes but no changeset:
147
+
148
+ `);
149
+ for (const pkg of check.missingFor) {
150
+ process.stderr.write(` ${COLORS.yellow}@outfitter/${pkg}${COLORS.reset}
151
+ `);
152
+ }
153
+ process.stderr.write(`
154
+ Run ${COLORS.blue}bun run changeset${COLORS.reset} to add a changeset, ` + `or add the ${COLORS.blue}no-changeset${COLORS.reset} label to skip.
155
+ `);
156
+ process.exit(1);
157
+ }
158
+ const ignoredReferences = getIgnoredReferencesForChangedChangesets(cwd, changesetFiles);
159
+ if (ignoredReferences.length > 0) {
160
+ process.stderr.write(`${COLORS.red}Invalid changeset package reference(s).${COLORS.reset}
161
+
162
+ `);
163
+ process.stderr.write(`Changesets must not reference packages listed in .changeset/config.json ignore:
164
+
165
+ `);
166
+ for (const reference of ignoredReferences) {
167
+ process.stderr.write(` ${COLORS.yellow}${reference.file}${COLORS.reset}
168
+ `);
169
+ for (const pkg of reference.packages) {
170
+ process.stderr.write(` - ${pkg}
171
+ `);
172
+ }
173
+ }
174
+ process.stderr.write(`
175
+ Update the affected changeset files to remove ignored packages before merging.
176
+ `);
177
+ process.exit(1);
178
+ }
179
+ process.stdout.write(`${COLORS.green}Changeset found for ${changedPackages.length} changed package(s).${COLORS.reset}
180
+ `);
181
+ process.exit(0);
182
+ }
183
+
184
+ export { getChangedPackagePaths, getChangedChangesetFiles, checkChangesetRequired, parseIgnoredPackagesFromChangesetConfig, parseChangesetFrontmatterPackageNames, findIgnoredPackageReferences, runCheckChangeset };
@@ -141,6 +141,9 @@ var COLORS = {
141
141
  blue: "\x1B[34m",
142
142
  dim: "\x1B[2m"
143
143
  };
144
+ function resolveJsonMode(options = {}) {
145
+ return options.json ?? process.env["OUTFITTER_JSON"] === "1";
146
+ }
144
147
  async function runCheckExports(options = {}) {
145
148
  const cwd = process.cwd();
146
149
  const configPath = resolve(cwd, "bunup.config.ts");
@@ -183,7 +186,7 @@ async function runCheckExports(options = {}) {
183
186
  ok: results.every((r) => r.status === "ok"),
184
187
  packages: results
185
188
  };
186
- if (options.json) {
189
+ if (resolveJsonMode(options)) {
187
190
  process.stdout.write(`${JSON.stringify(checkResult, null, 2)}
188
191
  `);
189
192
  } else {
@@ -223,4 +226,4 @@ async function runCheckExports(options = {}) {
223
226
  process.exit(checkResult.ok ? 0 : 1);
224
227
  }
225
228
 
226
- export { entryToSubpath, compareExports, runCheckExports };
229
+ export { entryToSubpath, compareExports, resolveJsonMode, runCheckExports };
@@ -0,0 +1,140 @@
1
+ import ts from "typescript";
2
+ import { ZodType } from "zod";
3
+ /** Coverage classification for a single declaration. */
4
+ type CoverageLevel = "documented" | "partial" | "undocumented";
5
+ /** Result for a single exported declaration. */
6
+ interface DeclarationCoverage {
7
+ readonly name: string;
8
+ readonly kind: string;
9
+ readonly level: CoverageLevel;
10
+ readonly file: string;
11
+ readonly line: number;
12
+ }
13
+ /** Coverage summary statistics. */
14
+ interface CoverageSummary {
15
+ readonly documented: number;
16
+ readonly partial: number;
17
+ readonly undocumented: number;
18
+ readonly total: number;
19
+ readonly percentage: number;
20
+ }
21
+ /** Per-package TSDoc coverage stats. */
22
+ interface PackageCoverage {
23
+ readonly name: string;
24
+ readonly path: string;
25
+ readonly declarations: readonly DeclarationCoverage[];
26
+ readonly documented: number;
27
+ readonly partial: number;
28
+ readonly undocumented: number;
29
+ readonly total: number;
30
+ readonly percentage: number;
31
+ }
32
+ /** Aggregated result across all packages. */
33
+ interface TsDocCheckResult {
34
+ readonly ok: boolean;
35
+ readonly packages: readonly PackageCoverage[];
36
+ readonly summary: CoverageSummary;
37
+ }
38
+ /** Zod schema for {@link CoverageLevel}. */
39
+ declare const coverageLevelSchema: ZodType<CoverageLevel>;
40
+ /** Zod schema for {@link DeclarationCoverage}. */
41
+ declare const declarationCoverageSchema: ZodType<DeclarationCoverage>;
42
+ /** Zod schema for {@link CoverageSummary}. */
43
+ declare const coverageSummarySchema: ZodType<CoverageSummary>;
44
+ /** Zod schema for {@link PackageCoverage}. */
45
+ declare const packageCoverageSchema: ZodType<PackageCoverage>;
46
+ /** Zod schema for {@link TsDocCheckResult}. */
47
+ declare const tsDocCheckResultSchema: ZodType<TsDocCheckResult>;
48
+ /** Options for the check-tsdoc command. */
49
+ interface CheckTsDocOptions {
50
+ readonly strict?: boolean | undefined;
51
+ readonly json?: boolean | undefined;
52
+ readonly minCoverage?: number | undefined;
53
+ readonly cwd?: string | undefined;
54
+ readonly paths?: readonly string[] | undefined;
55
+ }
56
+ /**
57
+ * Check whether a node is an exported declaration worth checking.
58
+ *
59
+ * Returns true for function, interface, type alias, class, enum, and variable
60
+ * declarations that carry the `export` keyword. Re-exports (`{ ... } from`)
61
+ * and `*` are excluded since TSDoc belongs at the definition site.
62
+ */
63
+ declare function isExportedDeclaration(node: ts.Node): boolean;
64
+ /**
65
+ * Extract the name of a declaration node.
66
+ *
67
+ * For variable statements, returns the name of the first variable declarator.
68
+ * Returns `undefined` for anonymous declarations (e.g., `function() {}`).
69
+ */
70
+ declare function getDeclarationName(node: ts.Node): string | undefined;
71
+ /**
72
+ * Determine the kind label for a declaration node.
73
+ *
74
+ * Maps AST node types to human-readable kind strings used in coverage reports.
75
+ */
76
+ declare function getDeclarationKind(node: ts.Node): string;
77
+ /**
78
+ * Classify a declaration's TSDoc coverage level.
79
+ *
80
+ * - `"documented"` -- has a JSDoc comment with a description. For interfaces
81
+ * and classes, all members must also have JSDoc comments.
82
+ * - `"partial"` -- the declaration has a JSDoc comment but some members
83
+ * (in interfaces/classes) lack documentation.
84
+ * - `"undocumented"` -- no JSDoc comment at all.
85
+ */
86
+ declare function classifyDeclaration(node: ts.Node, sourceFile: ts.SourceFile): CoverageLevel;
87
+ /**
88
+ * Analyze all exported declarations in a source file for TSDoc coverage.
89
+ *
90
+ * Walks top-level statements, filters to exported declarations, and
91
+ * classifies each for documentation coverage.
92
+ */
93
+ declare function analyzeSourceFile(sourceFile: ts.SourceFile): DeclarationCoverage[];
94
+ /**
95
+ * Calculate aggregate coverage statistics from declaration results.
96
+ *
97
+ * Partial documentation counts as half coverage in the percentage calculation.
98
+ * An empty array returns 100% (no declarations to check).
99
+ */
100
+ declare function calculateCoverage(declarations: readonly DeclarationCoverage[]): {
101
+ documented: number;
102
+ partial: number;
103
+ undocumented: number;
104
+ total: number;
105
+ percentage: number;
106
+ };
107
+ /** Resolve whether JSON output mode is active. */
108
+ declare function resolveJsonMode(options?: CheckTsDocOptions): boolean;
109
+ /**
110
+ * Analyze TSDoc coverage across workspace packages.
111
+ *
112
+ * Pure function that discovers packages, analyzes TSDoc coverage on exported
113
+ * declarations, and returns the aggregated result. Does not print output or
114
+ * call `process.exit()`.
115
+ *
116
+ * @param options - Analysis options (paths, strict mode, coverage threshold)
117
+ * @returns Aggregated coverage result across all packages, or `null` if no packages found
118
+ */
119
+ declare function analyzeCheckTsdoc(options?: CheckTsDocOptions): TsDocCheckResult | null;
120
+ /**
121
+ * Print a TSDoc coverage result in human-readable format.
122
+ *
123
+ * Renders a bar chart per package with summary statistics. Writes to stdout/stderr.
124
+ *
125
+ * @param result - The coverage result to print
126
+ * @param options - Display options (strict mode, coverage threshold for warning)
127
+ */
128
+ declare function printCheckTsdocHuman(result: TsDocCheckResult, options?: {
129
+ strict?: boolean | undefined;
130
+ minCoverage?: number | undefined;
131
+ }): void;
132
+ /**
133
+ * Run check-tsdoc across workspace packages.
134
+ *
135
+ * Discovers packages with `src/index.ts` entry points, analyzes TSDoc
136
+ * coverage on exported declarations, and reports per-package statistics.
137
+ * Calls `process.exit()` on completion.
138
+ */
139
+ declare function runCheckTsdoc(options?: CheckTsDocOptions): Promise<void>;
140
+ export { CoverageLevel, DeclarationCoverage, CoverageSummary, PackageCoverage, TsDocCheckResult, coverageLevelSchema, declarationCoverageSchema, coverageSummarySchema, packageCoverageSchema, tsDocCheckResultSchema, CheckTsDocOptions, isExportedDeclaration, getDeclarationName, getDeclarationKind, classifyDeclaration, analyzeSourceFile, calculateCoverage, resolveJsonMode, analyzeCheckTsdoc, printCheckTsdocHuman, runCheckTsdoc };