@openpkg-ts/cli 0.9.3 → 0.11.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 (2) hide show
  1. package/dist/index.js +134 -15
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -5,32 +5,49 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { parseArgs } from "node:util";
7
7
  import {
8
+ calculateNextVersion,
9
+ categorizeBreakingChanges,
8
10
  createDocs,
9
11
  diffSpecs,
10
12
  extractSpec,
13
+ getAvailableVersions,
14
+ getValidationErrors,
11
15
  listExports,
16
+ loadConfig,
17
+ mergeConfig,
12
18
  recommendSemverBump
13
19
  } from "@openpkg-ts/sdk";
14
20
  var HELP = `openpkg - extract TypeScript API specs and generate docs
15
21
 
16
22
  Usage:
17
- openpkg spec <entry.ts> [-o spec.json]
23
+ openpkg spec <entry.ts> [-o spec.json] [--follow-external <pkg,...>]
18
24
  openpkg docs <entry.ts | spec.json> [-f md|html|json] [-o out]
19
25
  openpkg list <entry.ts> [--json]
20
- openpkg diff <old.json> <new.json>
26
+ openpkg validate <spec.json>
27
+ openpkg diff <old.json> <new.json> [--json]
21
28
 
22
29
  Commands:
23
- spec Extract an OpenPkg spec from a TypeScript entry point
24
- docs Generate docs from an entry point or an existing spec file
25
- list List exports (name, kind, location)
26
- diff Compare two spec files and recommend a semver bump
30
+ spec Extract an OpenPkg spec from a TypeScript entry point
31
+ docs Generate docs from an entry point or an existing spec file
32
+ list List exports (name, kind, location)
33
+ validate Validate a spec file against the OpenPkg meta-schema
34
+ diff Compare two spec files and recommend a semver bump
27
35
 
28
36
  Options:
29
- -o, --output Write to file instead of stdout
30
- -f, --format docs output format: md (default), html, json
31
- --json list output as JSON
32
- -h, --help Show this help
33
- -v, --version Show version
37
+ -o, --output Write to file instead of stdout
38
+ -f, --format docs output format: md (default), html, json
39
+ --json list/diff output as JSON
40
+ --follow-external Expand types from these packages (comma-separated,
41
+ globs ok: "@ai-sdk/*"). Default: stub externals.
42
+ --follow-external-all Expand every external package (use with care)
43
+ --only Only extract these exports (comma-separated, * ok)
44
+ --ignore Ignore these exports (comma-separated, * ok)
45
+ -h, --help Show this help
46
+ -v, --version Show version
47
+
48
+ Config: reads openpkg.config.json (or package.json "openpkg" field) from the
49
+ cwd. Flags override the file. Example openpkg.config.json:
50
+ { "followExternal": ["@acme/payment-kit", "@ai-sdk/*"] }
34
51
  `;
35
52
  function fail(message) {
36
53
  console.error(`error: ${message}`);
@@ -59,17 +76,59 @@ function reportDiagnostics(diagnostics) {
59
76
  process.exit(1);
60
77
  }
61
78
  }
79
+ function toList(value) {
80
+ if (!value)
81
+ return;
82
+ const items = value.split(",").map((s) => s.trim()).filter(Boolean);
83
+ return items.length > 0 ? items : undefined;
84
+ }
85
+ function reportStubbedExternals(spec) {
86
+ const counts = new Map;
87
+ for (const t of spec.types ?? []) {
88
+ if (!t.external)
89
+ continue;
90
+ const pkg = t.schema?.["x-ts-package"];
91
+ const key = typeof pkg === "string" ? pkg : "(unknown origin)";
92
+ counts.set(key, (counts.get(key) ?? 0) + 1);
93
+ }
94
+ if (counts.size === 0)
95
+ return;
96
+ const summary = [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([pkg, n]) => `${pkg} (${n})`).join(", ");
97
+ console.error(`external types stubbed from: ${summary}`);
98
+ console.error(" → add package names to followExternal (config or --follow-external) to expand them");
99
+ }
62
100
  async function specCommand(args) {
63
101
  const { values, positionals } = parseArgs({
64
102
  args,
65
- options: { output: { type: "string", short: "o" } },
103
+ options: {
104
+ output: { type: "string", short: "o" },
105
+ "follow-external": { type: "string" },
106
+ "follow-external-all": { type: "boolean" },
107
+ only: { type: "string" },
108
+ ignore: { type: "string" }
109
+ },
66
110
  allowPositionals: true
67
111
  });
68
112
  const entryFile = positionals[0];
69
113
  if (!entryFile)
70
114
  fail("spec requires an entry file (openpkg spec src/index.ts)");
71
- const { spec, diagnostics } = await extractSpec({ entryFile });
115
+ const fileConfig = loadConfig(process.cwd());
116
+ const cliConfig = {
117
+ followExternal: values["follow-external-all"] ? true : toList(values["follow-external"]),
118
+ only: toList(values.only),
119
+ ignore: toList(values.ignore)
120
+ };
121
+ const config = mergeConfig(fileConfig, cliConfig);
122
+ const { spec, diagnostics } = await extractSpec({
123
+ entryFile,
124
+ followExternal: config.followExternal,
125
+ only: config.only,
126
+ ignore: config.ignore,
127
+ externals: config.externals
128
+ });
72
129
  reportDiagnostics(diagnostics);
130
+ if (!config.followExternal)
131
+ reportStubbedExternals(spec);
73
132
  write(JSON.stringify(spec, null, 2), values.output);
74
133
  }
75
134
  async function docsCommand(args) {
@@ -130,12 +189,67 @@ function readSpecFile(file) {
130
189
  fail(`failed to read spec file ${file}: ${err instanceof Error ? err.message : String(err)}`);
131
190
  }
132
191
  }
192
+ function pickVersion(spec) {
193
+ const declared = spec?.openpkg;
194
+ if (typeof declared === "string" && getAvailableVersions().includes(declared)) {
195
+ return declared;
196
+ }
197
+ return "latest";
198
+ }
199
+ function readValidSpecFile(file) {
200
+ const parsed = readSpecFile(file);
201
+ const errors = getValidationErrors(parsed, pickVersion(parsed));
202
+ if (errors.length > 0) {
203
+ const details = errors.map((e) => ` ${e.instancePath || "/"} ${e.message}`).join(`
204
+ `);
205
+ fail(`invalid spec ${file}:
206
+ ${details}`);
207
+ }
208
+ return parsed;
209
+ }
210
+ function validateCommand(args) {
211
+ const [file] = args;
212
+ if (!file)
213
+ fail("validate requires a spec file (openpkg validate spec.json)");
214
+ const spec = readSpecFile(file);
215
+ const errors = getValidationErrors(spec, pickVersion(spec));
216
+ if (errors.length === 0) {
217
+ console.log(`${file}: valid`);
218
+ return;
219
+ }
220
+ for (const e of errors) {
221
+ console.error(`${e.instancePath || "/"} ${e.message}`);
222
+ }
223
+ process.exit(1);
224
+ }
133
225
  function diffCommand(args) {
134
- const [oldFile, newFile] = args;
226
+ const { values, positionals } = parseArgs({
227
+ args,
228
+ options: { json: { type: "boolean" } },
229
+ allowPositionals: true
230
+ });
231
+ const [oldFile, newFile] = positionals;
135
232
  if (!oldFile || !newFile)
136
233
  fail("diff requires two spec files (openpkg diff old.json new.json)");
137
- const diff = diffSpecs(readSpecFile(oldFile), readSpecFile(newFile));
234
+ const oldSpec = readValidSpecFile(oldFile);
235
+ const newSpec = readValidSpecFile(newFile);
236
+ const diff = diffSpecs(oldSpec, newSpec);
138
237
  const recommendation = recommendSemverBump(diff);
238
+ const oldVersion = newSpec?.meta?.version;
239
+ const nextVersion = oldVersion ? calculateNextVersion(oldVersion, recommendation.bump) : undefined;
240
+ if (values.json) {
241
+ console.log(JSON.stringify({
242
+ breaking: diff.breaking,
243
+ nonBreaking: diff.nonBreaking,
244
+ docsOnly: diff.docsOnly,
245
+ categorized: categorizeBreakingChanges(diff.breaking, oldSpec, newSpec),
246
+ recommendation,
247
+ ...nextVersion ? { nextVersion } : {}
248
+ }, null, 2));
249
+ if (diff.breaking.length)
250
+ process.exitCode = 2;
251
+ return;
252
+ }
139
253
  const section = (title, items) => {
140
254
  if (!items.length)
141
255
  return;
@@ -151,6 +265,8 @@ function diffCommand(args) {
151
265
  }
152
266
  console.log(`
153
267
  Recommended bump: ${recommendation.bump} (${recommendation.reason})`);
268
+ if (nextVersion)
269
+ console.log(`Next version: ${nextVersion}`);
154
270
  if (diff.breaking.length)
155
271
  process.exitCode = 2;
156
272
  }
@@ -166,6 +282,9 @@ async function main() {
166
282
  case "list":
167
283
  await listCommand(rest);
168
284
  break;
285
+ case "validate":
286
+ validateCommand(rest);
287
+ break;
169
288
  case "diff":
170
289
  diffCommand(rest);
171
290
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpkg-ts/cli",
3
- "version": "0.9.3",
3
+ "version": "0.11.0",
4
4
  "description": "CLI for OpenPkg - extract TypeScript API specs and generate docs",
5
5
  "keywords": [
6
6
  "openpkg",
@@ -35,7 +35,7 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@openpkg-ts/sdk": "^0.46.0"
38
+ "@openpkg-ts/sdk": "^0.48.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/bun": "latest",