@outfitter/tooling 0.2.1 → 0.2.3

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 (53) hide show
  1. package/.markdownlint-cli2.jsonc +55 -55
  2. package/README.md +30 -3
  3. package/biome.json +79 -72
  4. package/dist/cli/check-boundary-invocations.d.ts +34 -0
  5. package/dist/cli/check-boundary-invocations.js +14 -0
  6. package/dist/cli/check-bunup-registry.d.ts +36 -0
  7. package/dist/cli/check-bunup-registry.js +12 -0
  8. package/dist/cli/check-changeset.d.ts +64 -0
  9. package/dist/cli/check-changeset.js +14 -0
  10. package/dist/cli/check-clean-tree.d.ts +36 -0
  11. package/dist/cli/check-clean-tree.js +14 -0
  12. package/dist/cli/check-exports.d.ts +2 -0
  13. package/dist/cli/check-exports.js +12 -0
  14. package/dist/cli/check-readme-imports.d.ts +60 -0
  15. package/dist/cli/check-readme-imports.js +194 -0
  16. package/dist/cli/check.js +2 -1
  17. package/dist/cli/fix.js +2 -1
  18. package/dist/cli/index.js +1106 -17
  19. package/dist/cli/init.js +2 -1
  20. package/dist/cli/pre-push.d.ts +34 -1
  21. package/dist/cli/pre-push.js +15 -2
  22. package/dist/cli/upgrade-bun.js +2 -1
  23. package/dist/index.d.ts +110 -35
  24. package/dist/index.js +23 -8
  25. package/dist/registry/build.d.ts +6 -0
  26. package/dist/registry/build.js +32 -13
  27. package/dist/registry/index.js +1 -0
  28. package/dist/registry/schema.js +1 -0
  29. package/dist/shared/@outfitter/{tooling-xx1146e3.js → tooling-0x5q15ec.js} +2 -1
  30. package/dist/shared/@outfitter/tooling-1y8w5ahg.js +70 -0
  31. package/dist/shared/@outfitter/tooling-3w8vr2w3.js +94 -0
  32. package/dist/shared/@outfitter/tooling-8sd32ts6.js +277 -0
  33. package/dist/shared/@outfitter/{tooling-s4eqq91d.js → tooling-9errkcvk.js} +2 -1
  34. package/dist/shared/@outfitter/tooling-9vs606gq.d.ts +3 -0
  35. package/dist/shared/@outfitter/{tooling-75j500dv.js → tooling-9yzd08v1.js} +10 -6
  36. package/dist/shared/@outfitter/tooling-ctmgnap5.js +19 -0
  37. package/dist/shared/@outfitter/tooling-dvwh9qve.js +4 -0
  38. package/dist/shared/@outfitter/{tooling-xaxdr9da.js → tooling-mxwc1n8w.js} +13 -3
  39. package/dist/shared/@outfitter/tooling-q0d60xb3.d.ts +58 -0
  40. package/dist/shared/@outfitter/tooling-r9976n43.js +100 -0
  41. package/dist/shared/@outfitter/tooling-t17gnh9b.js +78 -0
  42. package/dist/shared/@outfitter/tooling-tf22zt9p.js +226 -0
  43. package/dist/shared/chunk-3s189drz.js +4 -0
  44. package/dist/shared/chunk-6a7tjcgm.js +193 -0
  45. package/dist/shared/chunk-8aenrm6f.js +18 -0
  46. package/dist/version.d.ts +2 -0
  47. package/dist/version.js +8 -0
  48. package/lefthook.yml +5 -7
  49. package/package.json +122 -121
  50. package/registry/registry.json +78 -76
  51. package/tsconfig.preset.bun.json +5 -5
  52. package/tsconfig.preset.json +33 -33
  53. package/dist/shared/@outfitter/tooling-qm7jeg0d.js +0 -99
@@ -0,0 +1,226 @@
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.sort();
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
+ async function runCheckExports(options = {}) {
145
+ const cwd = process.cwd();
146
+ const configPath = resolve(cwd, "bunup.config.ts");
147
+ let workspaces;
148
+ try {
149
+ const configModule = await import(configPath);
150
+ const rawConfig = configModule.default;
151
+ if (!Array.isArray(rawConfig)) {
152
+ process.stderr.write(`bunup.config.ts must export a workspace array
153
+ `);
154
+ process.exit(1);
155
+ }
156
+ workspaces = rawConfig;
157
+ } catch {
158
+ process.stderr.write(`Could not load bunup.config.ts from ${cwd}
159
+ `);
160
+ process.exit(1);
161
+ }
162
+ const results = [];
163
+ for (const workspace of workspaces) {
164
+ const packageRoot = resolve(cwd, workspace.root);
165
+ const pkgPath = resolve(packageRoot, "package.json");
166
+ let pkg;
167
+ try {
168
+ pkg = await Bun.file(pkgPath).json();
169
+ } catch {
170
+ results.push({ name: workspace.name, status: "ok" });
171
+ continue;
172
+ }
173
+ const actual = typeof pkg.exports === "object" && pkg.exports !== null ? pkg.exports : {};
174
+ const expected = computeExpectedExports(packageRoot, workspace, pkg);
175
+ results.push(compareExports({
176
+ name: workspace.name,
177
+ actual,
178
+ expected,
179
+ path: workspace.root
180
+ }));
181
+ }
182
+ const checkResult = {
183
+ ok: results.every((r) => r.status === "ok"),
184
+ packages: results
185
+ };
186
+ if (options.json) {
187
+ process.stdout.write(`${JSON.stringify(checkResult, null, 2)}
188
+ `);
189
+ } else {
190
+ const drifted = results.filter((r) => r.status === "drift");
191
+ if (drifted.length === 0) {
192
+ process.stdout.write(`${COLORS.green}All ${results.length} packages have exports in sync.${COLORS.reset}
193
+ `);
194
+ } else {
195
+ process.stderr.write(`${COLORS.red}Export drift detected in ${drifted.length} package(s):${COLORS.reset}
196
+
197
+ `);
198
+ for (const result of drifted) {
199
+ const drift = result.drift;
200
+ if (!drift)
201
+ continue;
202
+ process.stderr.write(` ${COLORS.yellow}${result.name}${COLORS.reset} ${COLORS.dim}(${drift.path})${COLORS.reset}
203
+ `);
204
+ for (const key of drift.added) {
205
+ process.stderr.write(` ${COLORS.green}+ ${key}${COLORS.reset} ${COLORS.dim}(missing from package.json)${COLORS.reset}
206
+ `);
207
+ }
208
+ for (const key of drift.removed) {
209
+ process.stderr.write(` ${COLORS.red}- ${key}${COLORS.reset} ${COLORS.dim}(not in source)${COLORS.reset}
210
+ `);
211
+ }
212
+ for (const entry of drift.changed) {
213
+ process.stderr.write(` ${COLORS.yellow}~ ${entry.key}${COLORS.reset} ${COLORS.dim}(value mismatch)${COLORS.reset}
214
+ `);
215
+ }
216
+ process.stderr.write(`
217
+ `);
218
+ }
219
+ process.stderr.write(`Run ${COLORS.blue}bun run build${COLORS.reset} to regenerate exports.
220
+ `);
221
+ }
222
+ }
223
+ process.exit(checkResult.ok ? 0 : 1);
224
+ }
225
+
226
+ export { entryToSubpath, compareExports, runCheckExports };
@@ -0,0 +1,4 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ export { __require };
@@ -0,0 +1,193 @@
1
+ import"./chunk-3s189drz.js";
2
+
3
+ // src/cli/check-readme-imports.ts
4
+ import { resolve } from "node:path";
5
+ function parseSpecifier(specifier) {
6
+ if (!specifier.startsWith("@outfitter/")) {
7
+ return null;
8
+ }
9
+ const parts = specifier.split("/");
10
+ if (parts.length < 2) {
11
+ return null;
12
+ }
13
+ const packageName = `${parts[0]}/${parts[1]}`;
14
+ const rest = parts.slice(2);
15
+ if (rest.length === 0) {
16
+ return { packageName, subpath: "." };
17
+ }
18
+ return { packageName, subpath: `./${rest.join("/")}` };
19
+ }
20
+ function extractImports(content, file) {
21
+ const lines = content.split(`
22
+ `);
23
+ const results = [];
24
+ const seen = new Set;
25
+ const CODE_FENCE_OPEN = /^```(?:typescript|ts|javascript|js)\s*$/;
26
+ const CODE_FENCE_CLOSE = /^```\s*$/;
27
+ const IMPORT_RE = /from\s+["'](@outfitter\/[^"']+)["']/;
28
+ const NON_CONTRACTUAL = /<!--\s*non-contractual\s*-->/;
29
+ let inCodeBlock = false;
30
+ let skipBlock = false;
31
+ for (let i = 0;i < lines.length; i++) {
32
+ const line = lines[i];
33
+ if (!inCodeBlock) {
34
+ if (CODE_FENCE_OPEN.test(line)) {
35
+ inCodeBlock = true;
36
+ skipBlock = false;
37
+ for (let j = i - 1;j >= 0; j--) {
38
+ const prev = lines[j].trim();
39
+ if (prev === "")
40
+ continue;
41
+ if (NON_CONTRACTUAL.test(prev)) {
42
+ skipBlock = true;
43
+ }
44
+ break;
45
+ }
46
+ }
47
+ continue;
48
+ }
49
+ if (CODE_FENCE_CLOSE.test(line) && !CODE_FENCE_OPEN.test(line)) {
50
+ inCodeBlock = false;
51
+ skipBlock = false;
52
+ continue;
53
+ }
54
+ if (skipBlock)
55
+ continue;
56
+ const match = IMPORT_RE.exec(line);
57
+ if (!match?.[1])
58
+ continue;
59
+ const fullSpecifier = match[1];
60
+ if (seen.has(fullSpecifier))
61
+ continue;
62
+ seen.add(fullSpecifier);
63
+ const parsed = parseSpecifier(fullSpecifier);
64
+ if (!parsed)
65
+ continue;
66
+ results.push({
67
+ packageName: parsed.packageName,
68
+ subpath: parsed.subpath,
69
+ fullSpecifier,
70
+ file,
71
+ line: i + 1
72
+ });
73
+ }
74
+ return results;
75
+ }
76
+ function isExportedSubpath(subpath, exports) {
77
+ return subpath in exports;
78
+ }
79
+ var COLORS = {
80
+ reset: "\x1B[0m",
81
+ red: "\x1B[31m",
82
+ green: "\x1B[32m",
83
+ yellow: "\x1B[33m",
84
+ blue: "\x1B[34m",
85
+ dim: "\x1B[2m"
86
+ };
87
+ async function runCheckReadmeImports(options = {}) {
88
+ const cwd = process.cwd();
89
+ const readmeGlob = new Bun.Glob("**/README.md");
90
+ const readmeDirs = ["packages", "docs/packages"];
91
+ const readmePaths = [];
92
+ for (const dir of readmeDirs) {
93
+ const fullDir = resolve(cwd, dir);
94
+ try {
95
+ for (const match of readmeGlob.scanSync({ cwd: fullDir, dot: false })) {
96
+ const segments = match.split("/");
97
+ if (segments.length === 2 && segments[1] === "README.md") {
98
+ readmePaths.push(resolve(fullDir, match));
99
+ }
100
+ }
101
+ } catch {}
102
+ }
103
+ if (readmePaths.length === 0) {
104
+ process.stdout.write(`No README.md files found.
105
+ `);
106
+ return;
107
+ }
108
+ const exportsCache = new Map;
109
+ async function getExportsForPackage(packageName) {
110
+ if (exportsCache.has(packageName)) {
111
+ return exportsCache.get(packageName) ?? null;
112
+ }
113
+ const shortName = packageName.replace("@outfitter/", "");
114
+ const pkgPath = resolve(cwd, "packages", shortName, "package.json");
115
+ try {
116
+ const pkg = await Bun.file(pkgPath).json();
117
+ const exports = typeof pkg.exports === "object" && pkg.exports !== null ? pkg.exports : {};
118
+ exportsCache.set(packageName, exports);
119
+ return exports;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+ const results = [];
125
+ let hasInvalid = false;
126
+ for (const readmePath of readmePaths.sort()) {
127
+ const content = await Bun.file(readmePath).text();
128
+ const relativePath = readmePath.replace(`${cwd}/`, "");
129
+ const imports = extractImports(content, relativePath);
130
+ if (imports.length === 0)
131
+ continue;
132
+ const valid = [];
133
+ const invalid = [];
134
+ for (const imp of imports) {
135
+ const exports = await getExportsForPackage(imp.packageName);
136
+ if (exports === null) {
137
+ invalid.push(imp);
138
+ continue;
139
+ }
140
+ if (isExportedSubpath(imp.subpath, exports)) {
141
+ valid.push(imp);
142
+ } else {
143
+ invalid.push(imp);
144
+ }
145
+ }
146
+ if (valid.length > 0 || invalid.length > 0) {
147
+ results.push({ file: relativePath, valid, invalid });
148
+ }
149
+ if (invalid.length > 0) {
150
+ hasInvalid = true;
151
+ }
152
+ }
153
+ if (options.json) {
154
+ const output = {
155
+ ok: !hasInvalid,
156
+ files: results
157
+ };
158
+ process.stdout.write(`${JSON.stringify(output, null, 2)}
159
+ `);
160
+ } else {
161
+ const totalValid = results.reduce((n, r) => n + r.valid.length, 0);
162
+ const totalInvalid = results.reduce((n, r) => n + r.invalid.length, 0);
163
+ if (!hasInvalid) {
164
+ process.stdout.write(`${COLORS.green}All ${totalValid} README import examples match package exports.${COLORS.reset}
165
+ `);
166
+ } else {
167
+ process.stderr.write(`${COLORS.red}Invalid README import examples found:${COLORS.reset}
168
+
169
+ `);
170
+ for (const result of results) {
171
+ if (result.invalid.length === 0)
172
+ continue;
173
+ process.stderr.write(` ${COLORS.yellow}${result.file}${COLORS.reset}
174
+ `);
175
+ for (const imp of result.invalid) {
176
+ process.stderr.write(` ${COLORS.red}line ${imp.line}:${COLORS.reset} ${imp.fullSpecifier} ${COLORS.dim}(subpath ${imp.subpath} not in ${imp.packageName} exports)${COLORS.reset}
177
+ `);
178
+ }
179
+ process.stderr.write(`
180
+ `);
181
+ }
182
+ process.stderr.write(`${totalInvalid} invalid, ${totalValid} valid across ${results.length} file(s).
183
+ `);
184
+ }
185
+ }
186
+ process.exit(hasInvalid ? 1 : 0);
187
+ }
188
+ export {
189
+ runCheckReadmeImports,
190
+ parseSpecifier,
191
+ isExportedSubpath,
192
+ extractImports
193
+ };
@@ -0,0 +1,18 @@
1
+ // src/version.ts
2
+ import { readFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ var DEFAULT_VERSION = "0.0.0";
5
+ function readPackageVersion() {
6
+ try {
7
+ const require2 = createRequire(import.meta.url);
8
+ const pkgPath = require2.resolve("@outfitter/tooling/package.json");
9
+ const packageJson = JSON.parse(readFileSync(pkgPath, "utf8"));
10
+ if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
11
+ return packageJson.version;
12
+ }
13
+ } catch {}
14
+ return DEFAULT_VERSION;
15
+ }
16
+ var VERSION = readPackageVersion();
17
+
18
+ export { VERSION };
@@ -0,0 +1,2 @@
1
+ import { VERSION } from "./shared/@outfitter/tooling-9vs606gq";
2
+ export { VERSION };
@@ -0,0 +1,8 @@
1
+ // @bun
2
+ import {
3
+ VERSION
4
+ } from "./shared/@outfitter/tooling-ctmgnap5.js";
5
+ import"./shared/@outfitter/tooling-dvwh9qve.js";
6
+ export {
7
+ VERSION
8
+ };
package/lefthook.yml CHANGED
@@ -20,11 +20,9 @@ pre-commit:
20
20
  pre-push:
21
21
  parallel: false
22
22
  commands:
23
- build:
24
- run: bun run build
25
-
26
- test:
27
- # TDD-aware: skips tests on RED phase branches (*-tests, */tests, *_tests)
28
- # Override with `run: bun run test` if you don't want TDD support
29
- # Requires: @outfitter/tooling must be a devDependency in your project
23
+ verify:
24
+ # TDD-aware: skips strict verification on explicit RED phase branches
25
+ # (*-tests, */tests, *_tests). Otherwise runs verify:ci (or a strict
26
+ # fallback: typecheck/check-or-lint/build/test).
27
+ # Requires: @outfitter/tooling as a dev dependency.
30
28
  run: bunx @outfitter/tooling pre-push