@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.
- package/README.md +508 -0
- package/dist/types/adapters/fetch.d.ts +27 -0
- package/dist/types/adapters/node.d.ts +47 -0
- package/dist/types/app/binding.d.ts +122 -0
- package/dist/types/app/effect.d.ts +77 -0
- package/dist/types/app/index.d.ts +31 -0
- package/dist/types/app/subscription.d.ts +82 -0
- package/dist/types/bundle.d.ts +43 -0
- package/dist/types/cli.d.ts +15 -0
- package/dist/types/client/http.d.ts +242 -0
- package/dist/types/client/outcome.d.ts +289 -0
- package/dist/types/compat.d.ts +36 -0
- package/dist/types/compile.d.ts +196 -0
- package/dist/types/describe.d.ts +115 -0
- package/dist/types/diff.d.ts +91 -0
- package/dist/types/errors.d.ts +205 -0
- package/dist/types/http/dispatch.d.ts +148 -0
- package/dist/types/http/serve.d.ts +154 -0
- package/dist/types/http/wire.d.ts +334 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/ledger.d.ts +207 -0
- package/dist/types/local/index.d.ts +127 -0
- package/dist/types/messages.d.ts +63 -0
- package/dist/types/path.d.ts +119 -0
- package/dist/types/pipeline.d.ts +157 -0
- package/dist/types/port/client.d.ts +142 -0
- package/dist/types/port/frame.d.ts +195 -0
- package/dist/types/port/serve.d.ts +102 -0
- package/dist/types/project/index.d.ts +34 -0
- package/dist/types/project/markdown.d.ts +28 -0
- package/dist/types/project/openapi.d.ts +102 -0
- package/dist/types/project/tools.d.ts +57 -0
- package/dist/types/project/typescript.d.ts +59 -0
- package/dist/types/public.d.ts +73 -0
- package/dist/types/revision.d.ts +36 -0
- package/dist/types/stream/client.d.ts +104 -0
- package/dist/types/stream/server.d.ts +106 -0
- package/dist/types/stream/sse.d.ts +62 -0
- package/docs/APP-INTEGRATION.md +301 -0
- package/docs/CONTRACT-FORMAT.md +1923 -0
- package/package.json +110 -0
- package/schemas/jaren-contract-port.draft-07.schema.json +241 -0
- package/schemas/jaren-contract-port.schema.json +241 -0
- package/schemas/jaren-contract.draft-07.schema.json +287 -0
- package/schemas/jaren-contract.schema.json +287 -0
- package/src/adapters/fetch.js +109 -0
- package/src/adapters/node.js +238 -0
- package/src/app/binding.js +426 -0
- package/src/app/effect.js +190 -0
- package/src/app/index.js +26 -0
- package/src/app/subscription.js +130 -0
- package/src/bundle.js +168 -0
- package/src/cli.js +264 -0
- package/src/client/http.js +1150 -0
- package/src/client/outcome.js +364 -0
- package/src/compat.js +62 -0
- package/src/compile.js +1162 -0
- package/src/describe.js +109 -0
- package/src/diff.js +610 -0
- package/src/errors.js +236 -0
- package/src/http/dispatch.js +1054 -0
- package/src/http/serve.js +301 -0
- package/src/http/wire.js +469 -0
- package/src/index.js +33 -0
- package/src/ledger.js +225 -0
- package/src/local/index.js +363 -0
- package/src/messages.js +68 -0
- package/src/path.js +471 -0
- package/src/pipeline.js +241 -0
- package/src/port/client.js +518 -0
- package/src/port/frame.js +196 -0
- package/src/port/serve.js +442 -0
- package/src/project/index.js +29 -0
- package/src/project/markdown.js +244 -0
- package/src/project/openapi.js +564 -0
- package/src/project/openapi.jslt.json +149 -0
- package/src/project/tools.js +139 -0
- package/src/project/typescript.js +152 -0
- package/src/project/typescript.jtlt.json +72 -0
- package/src/public.js +206 -0
- package/src/revision.js +90 -0
- package/src/stream/client.js +212 -0
- package/src/stream/server.js +306 -0
- 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();
|