@jarenjs/contract 0.43.1

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 (84) hide show
  1. package/README.md +508 -0
  2. package/dist/types/adapters/fetch.d.ts +27 -0
  3. package/dist/types/adapters/node.d.ts +47 -0
  4. package/dist/types/app/binding.d.ts +122 -0
  5. package/dist/types/app/effect.d.ts +77 -0
  6. package/dist/types/app/index.d.ts +31 -0
  7. package/dist/types/app/subscription.d.ts +82 -0
  8. package/dist/types/bundle.d.ts +43 -0
  9. package/dist/types/cli.d.ts +15 -0
  10. package/dist/types/client/http.d.ts +242 -0
  11. package/dist/types/client/outcome.d.ts +289 -0
  12. package/dist/types/compat.d.ts +36 -0
  13. package/dist/types/compile.d.ts +196 -0
  14. package/dist/types/describe.d.ts +115 -0
  15. package/dist/types/diff.d.ts +91 -0
  16. package/dist/types/errors.d.ts +205 -0
  17. package/dist/types/http/dispatch.d.ts +148 -0
  18. package/dist/types/http/serve.d.ts +154 -0
  19. package/dist/types/http/wire.d.ts +334 -0
  20. package/dist/types/index.d.ts +39 -0
  21. package/dist/types/ledger.d.ts +207 -0
  22. package/dist/types/local/index.d.ts +127 -0
  23. package/dist/types/messages.d.ts +63 -0
  24. package/dist/types/path.d.ts +119 -0
  25. package/dist/types/pipeline.d.ts +157 -0
  26. package/dist/types/port/client.d.ts +142 -0
  27. package/dist/types/port/frame.d.ts +195 -0
  28. package/dist/types/port/serve.d.ts +102 -0
  29. package/dist/types/project/index.d.ts +34 -0
  30. package/dist/types/project/markdown.d.ts +28 -0
  31. package/dist/types/project/openapi.d.ts +102 -0
  32. package/dist/types/project/tools.d.ts +57 -0
  33. package/dist/types/project/typescript.d.ts +59 -0
  34. package/dist/types/public.d.ts +73 -0
  35. package/dist/types/revision.d.ts +36 -0
  36. package/dist/types/stream/client.d.ts +104 -0
  37. package/dist/types/stream/server.d.ts +106 -0
  38. package/dist/types/stream/sse.d.ts +62 -0
  39. package/docs/APP-INTEGRATION.md +301 -0
  40. package/docs/CONTRACT-FORMAT.md +1923 -0
  41. package/package.json +110 -0
  42. package/schemas/jaren-contract-port.draft-07.schema.json +241 -0
  43. package/schemas/jaren-contract-port.schema.json +241 -0
  44. package/schemas/jaren-contract.draft-07.schema.json +287 -0
  45. package/schemas/jaren-contract.schema.json +287 -0
  46. package/src/adapters/fetch.js +109 -0
  47. package/src/adapters/node.js +238 -0
  48. package/src/app/binding.js +426 -0
  49. package/src/app/effect.js +190 -0
  50. package/src/app/index.js +26 -0
  51. package/src/app/subscription.js +130 -0
  52. package/src/bundle.js +168 -0
  53. package/src/cli.js +264 -0
  54. package/src/client/http.js +1150 -0
  55. package/src/client/outcome.js +364 -0
  56. package/src/compat.js +62 -0
  57. package/src/compile.js +1162 -0
  58. package/src/describe.js +109 -0
  59. package/src/diff.js +610 -0
  60. package/src/errors.js +236 -0
  61. package/src/http/dispatch.js +1054 -0
  62. package/src/http/serve.js +301 -0
  63. package/src/http/wire.js +469 -0
  64. package/src/index.js +33 -0
  65. package/src/ledger.js +225 -0
  66. package/src/local/index.js +363 -0
  67. package/src/messages.js +68 -0
  68. package/src/path.js +471 -0
  69. package/src/pipeline.js +241 -0
  70. package/src/port/client.js +518 -0
  71. package/src/port/frame.js +196 -0
  72. package/src/port/serve.js +442 -0
  73. package/src/project/index.js +29 -0
  74. package/src/project/markdown.js +244 -0
  75. package/src/project/openapi.js +564 -0
  76. package/src/project/openapi.jslt.json +149 -0
  77. package/src/project/tools.js +139 -0
  78. package/src/project/typescript.js +152 -0
  79. package/src/project/typescript.jtlt.json +72 -0
  80. package/src/public.js +206 -0
  81. package/src/revision.js +90 -0
  82. package/src/stream/client.js +212 -0
  83. package/src/stream/server.js +306 -0
  84. package/src/stream/sse.js +67 -0
package/src/cli.js ADDED
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ //@ts-check
3
+ /**
4
+ * @file The `jaren-contract` command: compile a contract document and
5
+ * print or write one of its projections — `describe` (the `describe()`
6
+ * summary), `public` (the public projection), `openapi`, `types`
7
+ * (TypeScript declarations), `docs` (Markdown) — with `--check` to fail
8
+ * CI when a written artifact has drifted from what the document projects
9
+ * today; plus `diff --from a.json --to b.json`, the classified change
10
+ * report (docs/CONTRACT-FORMAT.md §13), whose `--fail-on <classes>`
11
+ * makes it a compatibility gate. Exit codes: 0 current/written (diff:
12
+ * no failing class), 1 drift under `--check` (diff: a `--fail-on` class
13
+ * is non-empty), 2 a usage error, an unreadable document or a compile
14
+ * refusal (printed as `code docPath message`).
15
+ */
16
+
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+
20
+ import { compileContract } from './compile.js';
21
+ import { ContractCompileError, ContractHostError } from './errors.js';
22
+ import { diffContracts } from './diff.js';
23
+ import { publicProjection } from './public.js';
24
+ import { toOpenApi } from './project/openapi.js';
25
+ import { toTypeScript } from './project/typescript.js';
26
+ import { toMarkdown } from './project/markdown.js';
27
+
28
+ const USAGE = `jaren-contract — projections of a jaren-contract document
29
+
30
+ Usage:
31
+ jaren-contract <command> --contract <file> [--out <dir|file>] [--check] [options]
32
+ jaren-contract diff --from <file> --to <file> [--fail-on <class,…>]
33
+
34
+ Commands:
35
+ describe The describe() summary (JSON): every operation's resolved binding and policy
36
+ public The public projection (JSON): itself a $contract document — what a client needs, what the revision hashes
37
+ openapi An OpenAPI 3.1 document (JSON)
38
+ types TypeScript declarations (.d.ts): operation types, Operations, Client, Handlers
39
+ docs Markdown reference documentation
40
+ diff The classified changes from --from to --to (JSON: breaking, additive, neutral, unknown)
41
+
42
+ Options:
43
+ --contract <file> The $contract document (required except for diff)
44
+ --out <dir|file> Write here instead of printing; a directory gets <id>.<ext>
45
+ --check Do not write; exit 1 when the file at --out differs (for CI)
46
+ --info-title <text> openapi: the info.title (default: the contract id)
47
+ --info-version <text> openapi: the info.version (default: the contract version)
48
+ --lenient openapi: drop and report keywords the projection would refuse
49
+ --from <file> diff: the contract consumers hold today
50
+ --to <file> diff: the contract they would meet
51
+ --fail-on <class,…> diff: exit 1 when any named class (breaking, additive, neutral, unknown) is non-empty
52
+ --help This text
53
+
54
+ Exit codes: 0 current or written (diff: no failing class), 1 drift under
55
+ --check (diff: a --fail-on class is non-empty), 2 a usage error, an
56
+ unreadable document, or a compile refusal (printed as code docPath message).
57
+
58
+ Examples:
59
+ jaren-contract describe --contract shop.json
60
+ jaren-contract openapi --contract shop.json --out api/ --info-title Shop
61
+ jaren-contract types --contract shop.json --out src/shop.d.ts --check
62
+ jaren-contract diff --from api/v1.json --to api/v2.json --fail-on breaking
63
+ `;
64
+
65
+ const COMMANDS = /** @type {const} */ ({
66
+ describe: { extension: '.describe.json' },
67
+ public: { extension: '.public.json' },
68
+ openapi: { extension: '.openapi.json' },
69
+ types: { extension: '.d.ts' },
70
+ docs: { extension: '.md' },
71
+ });
72
+
73
+ /**
74
+ * @param {string[]} argv
75
+ */
76
+ function parseArgs(argv) {
77
+ const options = {
78
+ command: /** @type {string | null} */ (null), contract: /** @type {string | null} */ (null),
79
+ out: /** @type {string | null} */ (null), check: false, help: false, lenient: false,
80
+ infoTitle: /** @type {string | null} */ (null), infoVersion: /** @type {string | null} */ (null),
81
+ from: /** @type {string | null} */ (null), to: /** @type {string | null} */ (null),
82
+ failOn: /** @type {string | null} */ (null),
83
+ };
84
+ for (let i = 2; i < argv.length; i++) {
85
+ switch (argv[i]) {
86
+ case '--contract': options.contract = argv[++i] ?? null; break;
87
+ case '--from': options.from = argv[++i] ?? null; break;
88
+ case '--to': options.to = argv[++i] ?? null; break;
89
+ case '--fail-on': options.failOn = argv[++i] ?? null; break;
90
+ case '--out': options.out = argv[++i] ?? null; break;
91
+ case '--check': options.check = true; break;
92
+ case '--lenient': options.lenient = true; break;
93
+ case '--info-title': options.infoTitle = argv[++i] ?? null; break;
94
+ case '--info-version': options.infoVersion = argv[++i] ?? null; break;
95
+ case '--help': case '-h': options.help = true; break;
96
+ default:
97
+ if (argv[i].startsWith('-')) throw new Error(`unknown option: ${argv[i]}`);
98
+ if (options.command !== null) throw new Error(`unexpected argument: ${argv[i]}`);
99
+ options.command = argv[i];
100
+ }
101
+ }
102
+ return options;
103
+ }
104
+
105
+ /**
106
+ * Write, or in `--check` mode compare and report. Returns true when the
107
+ * file on disk already matches (the same messages as jaren-emit).
108
+ * @param {string} file
109
+ * @param {string} content
110
+ * @param {boolean} check
111
+ */
112
+ function writeOrCheck(file, content, check) {
113
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
114
+ if (existing === content) return true;
115
+ if (check) {
116
+ console.error(existing === null ? `missing: ${file}` : `out of date: ${file}`);
117
+ return false;
118
+ }
119
+ fs.mkdirSync(path.dirname(file), { recursive: true });
120
+ fs.writeFileSync(file, content);
121
+ console.log(`wrote ${file}`);
122
+ return true;
123
+ }
124
+
125
+ /**
126
+ * The output file: `--out` itself, or `<out>/<id><extension>` when it is
127
+ * (or is spelled as) a directory.
128
+ * @param {string} out
129
+ * @param {string} id
130
+ * @param {string} extension
131
+ */
132
+ function outputFile(out, id, extension) {
133
+ const isDir = out.endsWith('/') || out.endsWith(path.sep)
134
+ || (fs.existsSync(out) && fs.statSync(out).isDirectory());
135
+ return isDir ? path.join(out, id + extension) : out;
136
+ }
137
+
138
+ /**
139
+ * @param {string} message
140
+ * @param {boolean} [usage]
141
+ */
142
+ function fail(message, usage = false) {
143
+ console.error(`jaren-contract: ${message}`);
144
+ if (usage) console.error(`\n${USAGE}`);
145
+ process.exit(2);
146
+ }
147
+
148
+ /** The class names `--fail-on` may list. */
149
+ const DIFF_CLASSES = ['breaking', 'additive', 'neutral', 'unknown'];
150
+
151
+ /**
152
+ * Read a contract document, exit 2 on an unreadable file.
153
+ * @param {string} file
154
+ * @param {string} flag
155
+ */
156
+ function readDocument(file, flag) {
157
+ try {
158
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
159
+ }
160
+ catch (error) {
161
+ fail(`cannot read ${flag} '${file}': ${/** @type {Error} */ (error).message}`);
162
+ return null; // unreachable — fail() exits
163
+ }
164
+ }
165
+
166
+ /**
167
+ * The `diff` command: classify the changes and print them; under
168
+ * `--fail-on` exit 1 when a named class is non-empty.
169
+ * @param {ReturnType<typeof parseArgs>} options
170
+ */
171
+ function runDiff(options) {
172
+ if (options.from === null || options.to === null) return fail('diff needs --from <file> and --to <file>', true);
173
+ /** @type {string[]} */
174
+ let failOn = [];
175
+ if (options.failOn !== null) {
176
+ failOn = options.failOn.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
177
+ for (const name of failOn) {
178
+ if (!DIFF_CLASSES.includes(name)) return fail(`--fail-on '${name}' is not a change class (${DIFF_CLASSES.join(', ')})`, true);
179
+ }
180
+ }
181
+ const from = readDocument(options.from, '--from');
182
+ const to = readDocument(options.to, '--to');
183
+ let diff;
184
+ try {
185
+ diff = diffContracts(from, to);
186
+ }
187
+ catch (error) {
188
+ if (error instanceof ContractCompileError) return fail(`${error.code} ${error.docPath ?? ''} ${error.reason}`);
189
+ throw error;
190
+ }
191
+ process.stdout.write(JSON.stringify(diff, null, 2) + '\n');
192
+ const failing = failOn.filter((name) => diff[/** @type {keyof typeof diff} */ (name)].length > 0);
193
+ if (failing.length > 0) {
194
+ console.error(`diff: ${failing.map((name) => `${diff[/** @type {keyof typeof diff} */ (name)].length} ${name}`).join(', ')} change(s)`);
195
+ process.exit(1);
196
+ }
197
+ }
198
+
199
+ function main() {
200
+ let options;
201
+ try {
202
+ options = parseArgs(process.argv);
203
+ }
204
+ catch (error) {
205
+ return fail(/** @type {Error} */ (error).message, true);
206
+ }
207
+ if (options.help) {
208
+ console.log(USAGE);
209
+ return;
210
+ }
211
+ if (options.command === 'diff') return runDiff(options);
212
+ if (options.command === null || !Object.hasOwn(COMMANDS, options.command)) {
213
+ return fail(options.command === null ? 'a command is required' : `unknown command '${options.command}'`, true);
214
+ }
215
+ const command = /** @type {keyof typeof COMMANDS} */ (options.command);
216
+ if (options.contract === null) return fail('--contract <file> is required', true);
217
+ if (options.check && options.out === null) return fail('--check needs --out', true);
218
+
219
+ let doc;
220
+ try {
221
+ doc = JSON.parse(fs.readFileSync(options.contract, 'utf8'));
222
+ }
223
+ catch (error) {
224
+ return fail(`cannot read contract '${options.contract}': ${/** @type {Error} */ (error).message}`);
225
+ }
226
+ let rendered;
227
+ try {
228
+ const contract = compileContract(doc);
229
+ switch (command) {
230
+ case 'describe': rendered = JSON.stringify(contract.describe(), null, 2) + '\n'; break;
231
+ case 'public': rendered = JSON.stringify(publicProjection(contract), null, 2) + '\n'; break;
232
+ case 'openapi': {
233
+ /** @type {Record<string, unknown>} */
234
+ const info = {};
235
+ if (options.infoTitle !== null) info.title = options.infoTitle;
236
+ if (options.infoVersion !== null) info.version = options.infoVersion;
237
+ const { document, dropped } = toOpenApi(contract, { info, lenient: options.lenient });
238
+ for (const d of dropped) console.error(`dropped ${d.keyword} at ${d.docPath}: ${d.reason}`);
239
+ rendered = JSON.stringify(document, null, 2) + '\n';
240
+ break;
241
+ }
242
+ case 'types': rendered = toTypeScript(contract); break;
243
+ case 'docs': rendered = toMarkdown(contract); break;
244
+ }
245
+ if (options.out === null) {
246
+ process.stdout.write(rendered);
247
+ return;
248
+ }
249
+ const file = outputFile(options.out, contract.id === null ? 'contract' : contract.id, COMMANDS[command].extension);
250
+ if (!writeOrCheck(file, rendered, options.check)) {
251
+ console.error('\nGenerated output is out of date. Run jaren-contract without --check.');
252
+ process.exit(1);
253
+ }
254
+ }
255
+ catch (error) {
256
+ if (error instanceof ContractCompileError) {
257
+ return fail(`${error.code} ${error.docPath ?? ''} ${error.reason}`);
258
+ }
259
+ if (error instanceof ContractHostError) return fail(`${error.code} ${error.reason}`);
260
+ throw error;
261
+ }
262
+ }
263
+
264
+ main();