osury 1.0.1 → 1.2.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.
- package/dist/osury.mjs +4041 -0
- package/package.json +6 -20
- package/bin/osury.mjs +0 -547
- package/src/BackendReScript.res.mjs +0 -157
- package/src/Codegen.res.mjs +0 -160
- package/src/CodegenHelpers.res.mjs +0 -233
- package/src/CodegenShims.res.mjs +0 -44
- package/src/CodegenTransforms.res.mjs +0 -794
- package/src/CodegenTypes.res.mjs +0 -187
- package/src/DomainBackend.res.mjs +0 -41
- package/src/DomainConfig.res.mjs +0 -206
- package/src/DomainGen.res.mjs +0 -53
- package/src/DomainIR.res.mjs +0 -2
- package/src/Errors.res.mjs +0 -106
- package/src/IR.res.mjs +0 -2
- package/src/IRGen.res.mjs +0 -367
- package/src/OpenAPIParser.res.mjs +0 -724
- package/src/Schema.gen.tsx +0 -28
- package/src/Schema.res.mjs +0 -877
package/package.json
CHANGED
|
@@ -2,29 +2,13 @@
|
|
|
2
2
|
"name": "osury",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "Generate ReScript types with Sury schemas from OpenAPI specifications",
|
|
5
|
-
"version": "1.0
|
|
5
|
+
"version": "1.2.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
|
-
"osury": "
|
|
8
|
+
"osury": "dist/osury.mjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"
|
|
12
|
-
"src/BackendReScript.res.mjs",
|
|
13
|
-
"src/Codegen.res.mjs",
|
|
14
|
-
"src/CodegenHelpers.res.mjs",
|
|
15
|
-
"src/CodegenShims.res.mjs",
|
|
16
|
-
"src/CodegenTransforms.res.mjs",
|
|
17
|
-
"src/CodegenTypes.res.mjs",
|
|
18
|
-
"src/Errors.res.mjs",
|
|
19
|
-
"src/IR.res.mjs",
|
|
20
|
-
"src/IRGen.res.mjs",
|
|
21
|
-
"src/OpenAPIParser.res.mjs",
|
|
22
|
-
"src/Schema.res.mjs",
|
|
23
|
-
"src/Schema.gen.tsx",
|
|
24
|
-
"src/DomainConfig.res.mjs",
|
|
25
|
-
"src/DomainIR.res.mjs",
|
|
26
|
-
"src/DomainGen.res.mjs",
|
|
27
|
-
"src/DomainBackend.res.mjs",
|
|
11
|
+
"dist/",
|
|
28
12
|
"README.md"
|
|
29
13
|
],
|
|
30
14
|
"scripts": {
|
|
@@ -34,10 +18,12 @@
|
|
|
34
18
|
"codegen": "node scripts/codegen.mjs",
|
|
35
19
|
"demo": "npm --prefix demo run dev",
|
|
36
20
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
37
|
-
"
|
|
21
|
+
"build:bin": "node scripts/build-bin.mjs",
|
|
22
|
+
"prepublishOnly": "npm run res:build && npm test && npm run build:bin"
|
|
38
23
|
},
|
|
39
24
|
"devDependencies": {
|
|
40
25
|
"@rescript/core": "^1.6.1",
|
|
26
|
+
"esbuild": "^0.28.0",
|
|
41
27
|
"gentype": "^4.5.0",
|
|
42
28
|
"jest": "^30.2.0",
|
|
43
29
|
"rescript": "^12.1.0",
|
package/bin/osury.mjs
DELETED
|
@@ -1,547 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import * as OpenAPIParser from "../src/OpenAPIParser.res.mjs";
|
|
4
|
-
import * as Codegen from "../src/Codegen.res.mjs";
|
|
5
|
-
import * as DomainConfig from "../src/DomainConfig.res.mjs";
|
|
6
|
-
import * as DomainGen from "../src/DomainGen.res.mjs";
|
|
7
|
-
import * as DomainBackend from "../src/DomainBackend.res.mjs";
|
|
8
|
-
import fs from "fs";
|
|
9
|
-
import path from "path";
|
|
10
|
-
import { createRequire } from "module";
|
|
11
|
-
import { performance } from "perf_hooks";
|
|
12
|
-
|
|
13
|
-
// ─── Package info ────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
16
|
-
const pkg = require("../package.json");
|
|
17
|
-
const VERSION = pkg.version;
|
|
18
|
-
|
|
19
|
-
// ─── Color support ───────────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
const noColor =
|
|
22
|
-
process.env.NO_COLOR != null ||
|
|
23
|
-
process.argv.includes("--no-color") ||
|
|
24
|
-
!process.stderr.isTTY;
|
|
25
|
-
|
|
26
|
-
const fmt = (code, text) =>
|
|
27
|
-
noColor ? text : `\x1b[${code}m${text}\x1b[0m`;
|
|
28
|
-
|
|
29
|
-
const c = {
|
|
30
|
-
bold: (t) => fmt("1", t),
|
|
31
|
-
dim: (t) => fmt("2", t),
|
|
32
|
-
italic: (t) => fmt("3", t),
|
|
33
|
-
red: (t) => fmt("31", t),
|
|
34
|
-
green: (t) => fmt("32", t),
|
|
35
|
-
yellow: (t) => fmt("33", t),
|
|
36
|
-
blue: (t) => fmt("34", t),
|
|
37
|
-
magenta: (t) => fmt("35", t),
|
|
38
|
-
cyan: (t) => fmt("36", t),
|
|
39
|
-
white: (t) => fmt("37", t),
|
|
40
|
-
gray: (t) => fmt("90", t),
|
|
41
|
-
boldRed: (t) => fmt("1;31", t),
|
|
42
|
-
boldGreen: (t) => fmt("1;32", t),
|
|
43
|
-
boldYellow: (t) => fmt("1;33", t),
|
|
44
|
-
boldCyan: (t) => fmt("1;36", t),
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// ─── Symbols ─────────────────────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
const sym = {
|
|
50
|
-
success: c.green("✓"),
|
|
51
|
-
error: c.red("✗"),
|
|
52
|
-
warning: c.yellow("⚠"),
|
|
53
|
-
arrow: c.dim("→"),
|
|
54
|
-
bullet: c.dim("·"),
|
|
55
|
-
bar: c.dim("│"),
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// ─── Output helpers ──────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
const log = (...args) => console.log(...args);
|
|
61
|
-
const err = (...args) => console.error(...args);
|
|
62
|
-
const blank = () => log();
|
|
63
|
-
|
|
64
|
-
function header() {
|
|
65
|
-
blank();
|
|
66
|
-
log(` ${c.bold("osury")} ${c.dim(`v${VERSION}`)}`);
|
|
67
|
-
log(` ${c.dim("OpenAPI 3.x → ReScript + Sury")}`);
|
|
68
|
-
blank();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function elapsed(startMs) {
|
|
72
|
-
const ms = performance.now() - startMs;
|
|
73
|
-
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
74
|
-
return `${(ms / 1000).toFixed(2)}s`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ─── Help ────────────────────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
function printHelp() {
|
|
80
|
-
header();
|
|
81
|
-
log(` ${c.bold("Usage")}`);
|
|
82
|
-
log(` ${c.cyan("$")} osury ${c.cyan("<input.json>")} ${c.dim("[output.res]")}`);
|
|
83
|
-
log(` ${c.cyan("$")} osury generate ${c.cyan("<input.json>")} -o ${c.cyan("<output.res>")}`);
|
|
84
|
-
log(` ${c.cyan("$")} osury domain ${c.dim("[options]")}`);
|
|
85
|
-
blank();
|
|
86
|
-
log(` ${c.bold("Options")}`);
|
|
87
|
-
log(` ${c.cyan("-o")}, ${c.cyan("--output")} Output file path ${c.dim("(default: ./Generated.res)")}`);
|
|
88
|
-
log(` ${c.cyan("-h")}, ${c.cyan("--help")} Show this help`);
|
|
89
|
-
log(` ${c.cyan("-v")}, ${c.cyan("--version")} Show version`);
|
|
90
|
-
log(` ${c.cyan("--no-color")} Disable colored output`);
|
|
91
|
-
blank();
|
|
92
|
-
log(` ${c.bold("Domain options")}`);
|
|
93
|
-
log(` ${c.cyan("--config")} Config file path ${c.dim("(default: domain.config.json)")}`);
|
|
94
|
-
log(` ${c.cyan("--api-module")} API module name ${c.dim("(default: Api)")}`);
|
|
95
|
-
log(` ${c.cyan("-o")} Output directory ${c.dim("(default: src/domains/)")}`);
|
|
96
|
-
blank();
|
|
97
|
-
log(` ${c.bold("Examples")}`);
|
|
98
|
-
log(` ${c.cyan("$")} osury openapi.json`);
|
|
99
|
-
log(` ${c.cyan("$")} osury openapi.json src/API.res`);
|
|
100
|
-
log(` ${c.cyan("$")} osury generate schema.json -o src/Schema.res`);
|
|
101
|
-
log(` ${c.cyan("$")} osury domain --config domain.config.json --api-module Api`);
|
|
102
|
-
blank();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ─── Arg parsing ─────────────────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
function parseArgs(args) {
|
|
108
|
-
const options = { input: null, output: "./Generated.res" };
|
|
109
|
-
|
|
110
|
-
let i = 0;
|
|
111
|
-
while (i < args.length) {
|
|
112
|
-
const arg = args[i];
|
|
113
|
-
|
|
114
|
-
if (arg === "-h" || arg === "--help") {
|
|
115
|
-
printHelp();
|
|
116
|
-
process.exit(0);
|
|
117
|
-
} else if (arg === "-v" || arg === "--version") {
|
|
118
|
-
log(`osury v${VERSION}`);
|
|
119
|
-
process.exit(0);
|
|
120
|
-
} else if (arg === "--no-color") {
|
|
121
|
-
// already handled above
|
|
122
|
-
} else if (arg === "-o" || arg === "--output") {
|
|
123
|
-
i++;
|
|
124
|
-
if (i >= args.length) {
|
|
125
|
-
header();
|
|
126
|
-
err(` ${sym.error} ${c.boldRed("Missing value for --output")}`);
|
|
127
|
-
blank();
|
|
128
|
-
err(` Expected: osury input.json ${c.cyan("-o <path>")}`);
|
|
129
|
-
blank();
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
options.output = args[i];
|
|
133
|
-
} else if (arg === "generate") {
|
|
134
|
-
// skip command word
|
|
135
|
-
} else if (!options.input) {
|
|
136
|
-
options.input = arg;
|
|
137
|
-
} else if (options.output === "./Generated.res") {
|
|
138
|
-
options.output = arg;
|
|
139
|
-
}
|
|
140
|
-
i++;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return options;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ─── "Did you mean?" ─────────────────────────────────────────────────────────
|
|
147
|
-
|
|
148
|
-
function levenshtein(a, b) {
|
|
149
|
-
const m = a.length, n = b.length;
|
|
150
|
-
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
151
|
-
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
152
|
-
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
153
|
-
for (let i = 1; i <= m; i++)
|
|
154
|
-
for (let j = 1; j <= n; j++)
|
|
155
|
-
dp[i][j] = a[i - 1] === b[j - 1]
|
|
156
|
-
? dp[i - 1][j - 1]
|
|
157
|
-
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
158
|
-
return dp[m][n];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function findSimilarFiles(target) {
|
|
162
|
-
const dir = path.dirname(target);
|
|
163
|
-
const resolvedDir = dir === "." ? process.cwd() : dir;
|
|
164
|
-
const base = path.basename(target).toLowerCase();
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const files = fs.readdirSync(resolvedDir);
|
|
168
|
-
return files
|
|
169
|
-
.filter((f) => {
|
|
170
|
-
const fl = f.toLowerCase();
|
|
171
|
-
if (
|
|
172
|
-
!fl.endsWith(".json") &&
|
|
173
|
-
!fl.endsWith(".yaml") &&
|
|
174
|
-
!fl.endsWith(".yml")
|
|
175
|
-
)
|
|
176
|
-
return false;
|
|
177
|
-
// Levenshtein distance ≤ 40% of target name length
|
|
178
|
-
return levenshtein(base, fl) <= Math.ceil(base.length * 0.4);
|
|
179
|
-
})
|
|
180
|
-
.sort((a, b) => levenshtein(a.toLowerCase(), base) - levenshtein(b.toLowerCase(), base))
|
|
181
|
-
.slice(0, 3);
|
|
182
|
-
} catch {
|
|
183
|
-
return [];
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ─── Error formatting ────────────────────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
function formatErrorKind(kind) {
|
|
190
|
-
if (!kind || !kind.TAG) return "Unknown error";
|
|
191
|
-
switch (kind.TAG) {
|
|
192
|
-
case "UnknownType":
|
|
193
|
-
return `Unknown type ${c.bold(`"${kind._0}"`)}`;
|
|
194
|
-
case "MissingRequiredField":
|
|
195
|
-
return `Missing required field ${c.bold(`"${kind._0}"`)}`;
|
|
196
|
-
case "InvalidRef":
|
|
197
|
-
return `Invalid reference ${c.bold(`"${kind._0}"`)}`;
|
|
198
|
-
case "UnsupportedFeature":
|
|
199
|
-
return `Unsupported feature ${c.bold(`"${kind._0}"`)}`;
|
|
200
|
-
case "InvalidFormat":
|
|
201
|
-
return `Invalid format ${c.bold(`"${kind._0}"`)}`;
|
|
202
|
-
case "CircularReference":
|
|
203
|
-
return `Circular reference ${c.bold(`"${kind._0}"`)}`;
|
|
204
|
-
case "AmbiguousUnion":
|
|
205
|
-
return `Ambiguous union (anyOf/oneOf cannot be distinguished)`;
|
|
206
|
-
case "MissingDiscriminator":
|
|
207
|
-
return `Missing discriminator for union ${c.bold(`"${kind._0}"`)}`;
|
|
208
|
-
case "InvalidJson":
|
|
209
|
-
return `Invalid JSON: ${kind._0}`;
|
|
210
|
-
default:
|
|
211
|
-
return kind.TAG + (kind._0 ? `: ${kind._0}` : "");
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function formatParseError(error, index) {
|
|
216
|
-
const pathStr =
|
|
217
|
-
error.location?.path?.length > 0
|
|
218
|
-
? c.cyan("#/" + error.location.path.join("/"))
|
|
219
|
-
: c.cyan("#");
|
|
220
|
-
|
|
221
|
-
const lines = [];
|
|
222
|
-
lines.push(` ${c.dim(`${index + 1}.`)} ${pathStr}`);
|
|
223
|
-
lines.push(` ${formatErrorKind(error.kind)}`);
|
|
224
|
-
|
|
225
|
-
if (error.hint) {
|
|
226
|
-
lines.push(` ${c.dim("Hint:")} ${c.italic(error.hint)}`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return lines.join("\n");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ─── Warning formatting ─────────────────────────────────────────────────────
|
|
233
|
-
|
|
234
|
-
function formatWarning(warning) {
|
|
235
|
-
// Warnings from collectUnionWarnings look like:
|
|
236
|
-
// "⚠ floatOrDict: anyOf [float, Dict] without discriminator, @tag("_tag") may not work at runtime"
|
|
237
|
-
// "⚠ modelInfoOrDict: anyOf without discriminator, simplified to modelInfo"
|
|
238
|
-
const cleaned = warning.replace(/^⚠\s*/, "");
|
|
239
|
-
return ` ${sym.warning} ${c.yellow(cleaned)}`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ─── Main generate ───────────────────────────────────────────────────────────
|
|
243
|
-
|
|
244
|
-
function generate(inputPath, outputPath) {
|
|
245
|
-
const start = performance.now();
|
|
246
|
-
|
|
247
|
-
header();
|
|
248
|
-
|
|
249
|
-
// ── Check input file exists ──
|
|
250
|
-
if (!fs.existsSync(inputPath)) {
|
|
251
|
-
err(` ${sym.error} ${c.boldRed("File not found:")} ${c.cyan(inputPath)}`);
|
|
252
|
-
|
|
253
|
-
const similar = findSimilarFiles(inputPath);
|
|
254
|
-
if (similar.length > 0) {
|
|
255
|
-
blank();
|
|
256
|
-
err(` ${c.dim("Did you mean?")}`);
|
|
257
|
-
similar.forEach((f) => {
|
|
258
|
-
err(` ${sym.bullet} ${c.cyan(f)}`);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
blank();
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ── Read & parse JSON ──
|
|
267
|
-
let doc;
|
|
268
|
-
try {
|
|
269
|
-
const raw = fs.readFileSync(inputPath, "utf8");
|
|
270
|
-
doc = JSON.parse(raw);
|
|
271
|
-
} catch (e) {
|
|
272
|
-
err(` ${sym.error} ${c.boldRed("Invalid JSON")} in ${c.cyan(inputPath)}`);
|
|
273
|
-
blank();
|
|
274
|
-
|
|
275
|
-
if (e instanceof SyntaxError) {
|
|
276
|
-
// Extract useful part of the message
|
|
277
|
-
const msg = e.message.replace(/^Unexpected/, "Unexpected");
|
|
278
|
-
err(` ${c.red(msg)}`);
|
|
279
|
-
} else {
|
|
280
|
-
err(` ${c.red(e.message)}`);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
blank();
|
|
284
|
-
err(` ${c.dim("Tip:")} Validate your JSON at ${c.cyan("https://jsonlint.com")}`);
|
|
285
|
-
blank();
|
|
286
|
-
process.exit(1);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ── Parse OpenAPI document ──
|
|
290
|
-
const result = OpenAPIParser.parseDocument(doc);
|
|
291
|
-
|
|
292
|
-
if (result.TAG !== "Ok") {
|
|
293
|
-
const errors = result._0;
|
|
294
|
-
const count = errors.length;
|
|
295
|
-
|
|
296
|
-
err(
|
|
297
|
-
` ${sym.error} ${c.boldRed(`${count} parse error${count !== 1 ? "s" : ""}`)} in ${c.cyan(inputPath)}`
|
|
298
|
-
);
|
|
299
|
-
blank();
|
|
300
|
-
|
|
301
|
-
errors.forEach((error, i) => {
|
|
302
|
-
err(formatParseError(error, i));
|
|
303
|
-
if (i < errors.length - 1) blank();
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
blank();
|
|
307
|
-
process.exit(1);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const schemas = result._0;
|
|
311
|
-
const schemaCount = schemas.length;
|
|
312
|
-
|
|
313
|
-
log(` ${sym.success} Parsed ${c.bold(String(schemaCount))} schema${schemaCount !== 1 ? "s" : ""} from ${c.cyan(inputPath)}`);
|
|
314
|
-
|
|
315
|
-
// ── Generate code ──
|
|
316
|
-
const genResult = Codegen.generateModuleWithDiagnostics(schemas);
|
|
317
|
-
|
|
318
|
-
if (genResult.TAG !== "Ok") {
|
|
319
|
-
const errors = genResult._0;
|
|
320
|
-
const count = errors.length;
|
|
321
|
-
|
|
322
|
-
err(
|
|
323
|
-
` ${sym.error} ${c.boldRed(`${count} codegen error${count !== 1 ? "s" : ""}`)} in ${c.cyan(inputPath)}`
|
|
324
|
-
);
|
|
325
|
-
blank();
|
|
326
|
-
|
|
327
|
-
errors.forEach((error, i) => {
|
|
328
|
-
err(formatParseError(error, i));
|
|
329
|
-
if (i < errors.length - 1) blank();
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
blank();
|
|
333
|
-
process.exit(1);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const { code, warnings } = genResult._0;
|
|
337
|
-
|
|
338
|
-
// ── Print warnings ──
|
|
339
|
-
if (warnings.length > 0) {
|
|
340
|
-
blank();
|
|
341
|
-
warnings.forEach((w) => {
|
|
342
|
-
log(formatWarning(w));
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// ── Ensure output directory exists ──
|
|
347
|
-
const outputDir = path.dirname(outputPath);
|
|
348
|
-
if (outputDir && outputDir !== "." && !fs.existsSync(outputDir)) {
|
|
349
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ── Count generated types ──
|
|
353
|
-
const typeCount = (code.match(/^type\s/gm) || []).length;
|
|
354
|
-
|
|
355
|
-
// ── Write files ──
|
|
356
|
-
fs.writeFileSync(outputPath, code);
|
|
357
|
-
|
|
358
|
-
const dictShimPath = path.join(outputDir || ".", "Dict.gen.ts");
|
|
359
|
-
fs.writeFileSync(dictShimPath, Codegen.generateDictShim());
|
|
360
|
-
|
|
361
|
-
const jsonShimPath = path.join(outputDir || ".", "JSON.gen.ts");
|
|
362
|
-
fs.writeFileSync(jsonShimPath, Codegen.generateJsonShim());
|
|
363
|
-
|
|
364
|
-
const nullableResPath = path.join(outputDir || ".", "Nullable.res");
|
|
365
|
-
fs.writeFileSync(nullableResPath, Codegen.generateNullableModule());
|
|
366
|
-
|
|
367
|
-
const nullableShimPath = path.join(outputDir || ".", "Nullable.shim.ts");
|
|
368
|
-
fs.writeFileSync(nullableShimPath, Codegen.generateNullableShim());
|
|
369
|
-
|
|
370
|
-
// ── Success output ──
|
|
371
|
-
blank();
|
|
372
|
-
log(` ${sym.success} Generated ${c.bold(String(typeCount))} type${typeCount !== 1 ? "s" : ""}`);
|
|
373
|
-
blank();
|
|
374
|
-
log(` ${c.dim("Files written:")}`);
|
|
375
|
-
log(` ${sym.bullet} ${c.cyan(outputPath)} ${c.dim("(main module)")}`);
|
|
376
|
-
log(` ${sym.bullet} ${c.cyan(dictShimPath)} ${c.dim("(TS shim)")}`);
|
|
377
|
-
log(` ${sym.bullet} ${c.cyan(jsonShimPath)} ${c.dim("(TS shim)")}`);
|
|
378
|
-
log(` ${sym.bullet} ${c.cyan(nullableResPath)} ${c.dim("(ReScript module)")}`);
|
|
379
|
-
log(` ${sym.bullet} ${c.cyan(nullableShimPath)} ${c.dim("(TS shim)")}`);
|
|
380
|
-
blank();
|
|
381
|
-
log(` ${c.dim(`Done in ${elapsed(start)}`)}`);
|
|
382
|
-
blank();
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// ─── Domain arg parsing ─────────────────────────────────────────────────────
|
|
386
|
-
|
|
387
|
-
function parseDomainArgs(args) {
|
|
388
|
-
const options = {
|
|
389
|
-
config: "domain.config.json",
|
|
390
|
-
apiModule: "Api",
|
|
391
|
-
outputDir: "src/domains/",
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
let i = 0;
|
|
395
|
-
while (i < args.length) {
|
|
396
|
-
const arg = args[i];
|
|
397
|
-
|
|
398
|
-
if (arg === "-h" || arg === "--help") {
|
|
399
|
-
printHelp();
|
|
400
|
-
process.exit(0);
|
|
401
|
-
} else if (arg === "--config") {
|
|
402
|
-
i++;
|
|
403
|
-
if (i >= args.length) {
|
|
404
|
-
header();
|
|
405
|
-
err(` ${sym.error} ${c.boldRed("Missing value for --config")}`);
|
|
406
|
-
blank();
|
|
407
|
-
process.exit(1);
|
|
408
|
-
}
|
|
409
|
-
options.config = args[i];
|
|
410
|
-
} else if (arg === "--api-module") {
|
|
411
|
-
i++;
|
|
412
|
-
if (i >= args.length) {
|
|
413
|
-
header();
|
|
414
|
-
err(` ${sym.error} ${c.boldRed("Missing value for --api-module")}`);
|
|
415
|
-
blank();
|
|
416
|
-
process.exit(1);
|
|
417
|
-
}
|
|
418
|
-
options.apiModule = args[i];
|
|
419
|
-
} else if (arg === "-o" || arg === "--output") {
|
|
420
|
-
i++;
|
|
421
|
-
if (i >= args.length) {
|
|
422
|
-
header();
|
|
423
|
-
err(` ${sym.error} ${c.boldRed("Missing value for --output")}`);
|
|
424
|
-
blank();
|
|
425
|
-
process.exit(1);
|
|
426
|
-
}
|
|
427
|
-
options.outputDir = args[i];
|
|
428
|
-
} else if (arg === "--no-color") {
|
|
429
|
-
// already handled
|
|
430
|
-
}
|
|
431
|
-
i++;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return options;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// ─── Domain generate ────────────────────────────────────────────────────────
|
|
438
|
-
|
|
439
|
-
function generateDomain(options) {
|
|
440
|
-
const start = performance.now();
|
|
441
|
-
|
|
442
|
-
header();
|
|
443
|
-
log(` ${c.dim("Mode:")} domain module generation`);
|
|
444
|
-
blank();
|
|
445
|
-
|
|
446
|
-
// ── Check config file exists ──
|
|
447
|
-
if (!fs.existsSync(options.config)) {
|
|
448
|
-
err(` ${sym.error} ${c.boldRed("Config not found:")} ${c.cyan(options.config)}`);
|
|
449
|
-
blank();
|
|
450
|
-
process.exit(1);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// ── Read & parse config JSON ──
|
|
454
|
-
let configJson;
|
|
455
|
-
try {
|
|
456
|
-
const raw = fs.readFileSync(options.config, "utf8");
|
|
457
|
-
configJson = JSON.parse(raw);
|
|
458
|
-
} catch (e) {
|
|
459
|
-
err(` ${sym.error} ${c.boldRed("Invalid JSON")} in ${c.cyan(options.config)}`);
|
|
460
|
-
blank();
|
|
461
|
-
if (e instanceof SyntaxError) {
|
|
462
|
-
err(` ${c.red(e.message)}`);
|
|
463
|
-
} else {
|
|
464
|
-
err(` ${c.red(e.message)}`);
|
|
465
|
-
}
|
|
466
|
-
blank();
|
|
467
|
-
process.exit(1);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// ── Parse domain config ──
|
|
471
|
-
const parseResult = DomainConfig.parse(configJson);
|
|
472
|
-
|
|
473
|
-
if (parseResult.TAG !== "Ok") {
|
|
474
|
-
const errors = parseResult._0;
|
|
475
|
-
const count = errors.length;
|
|
476
|
-
|
|
477
|
-
err(
|
|
478
|
-
` ${sym.error} ${c.boldRed(`${count} config error${count !== 1 ? "s" : ""}`)} in ${c.cyan(options.config)}`
|
|
479
|
-
);
|
|
480
|
-
blank();
|
|
481
|
-
|
|
482
|
-
errors.forEach((error, i) => {
|
|
483
|
-
err(formatParseError(error, i));
|
|
484
|
-
if (i < errors.length - 1) blank();
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
blank();
|
|
488
|
-
process.exit(1);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const config = parseResult._0;
|
|
492
|
-
const moduleCount = config.modules.length;
|
|
493
|
-
log(` ${sym.success} Parsed ${c.bold(String(moduleCount))} domain module${moduleCount !== 1 ? "s" : ""} from ${c.cyan(options.config)}`);
|
|
494
|
-
|
|
495
|
-
// ── Generate domain modules ──
|
|
496
|
-
const modules = DomainGen.generate(config, options.apiModule);
|
|
497
|
-
|
|
498
|
-
// ── Ensure output directory exists ──
|
|
499
|
-
if (!fs.existsSync(options.outputDir)) {
|
|
500
|
-
fs.mkdirSync(options.outputDir, { recursive: true });
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// ── Write each module ──
|
|
504
|
-
const writtenFiles = [];
|
|
505
|
-
modules.forEach((mod) => {
|
|
506
|
-
const code = DomainBackend.printModule(mod);
|
|
507
|
-
const outputPath = path.join(options.outputDir, mod.output);
|
|
508
|
-
fs.writeFileSync(outputPath, code);
|
|
509
|
-
writtenFiles.push(outputPath);
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
// ── Success output ──
|
|
513
|
-
blank();
|
|
514
|
-
log(` ${sym.success} Generated ${c.bold(String(writtenFiles.length))} domain module${writtenFiles.length !== 1 ? "s" : ""}`);
|
|
515
|
-
blank();
|
|
516
|
-
log(` ${c.dim("Files written:")}`);
|
|
517
|
-
writtenFiles.forEach((f) => {
|
|
518
|
-
log(` ${sym.bullet} ${c.cyan(f)}`);
|
|
519
|
-
});
|
|
520
|
-
blank();
|
|
521
|
-
log(` ${c.dim(`Done in ${elapsed(start)}`)}`);
|
|
522
|
-
blank();
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
526
|
-
|
|
527
|
-
const rawArgs = process.argv.slice(2);
|
|
528
|
-
|
|
529
|
-
// Check for "domain" subcommand
|
|
530
|
-
if (rawArgs[0] === "domain") {
|
|
531
|
-
const domainOptions = parseDomainArgs(rawArgs.slice(1));
|
|
532
|
-
generateDomain(domainOptions);
|
|
533
|
-
} else {
|
|
534
|
-
const options = parseArgs(rawArgs);
|
|
535
|
-
|
|
536
|
-
if (!options.input) {
|
|
537
|
-
header();
|
|
538
|
-
err(` ${sym.error} ${c.boldRed("No input file specified")}`);
|
|
539
|
-
blank();
|
|
540
|
-
err(` ${c.dim("Usage:")} osury ${c.cyan("<input.json>")} ${c.dim("[output.res]")}`);
|
|
541
|
-
err(` ${c.dim("Help:")} osury ${c.cyan("--help")}`);
|
|
542
|
-
blank();
|
|
543
|
-
process.exit(1);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
generate(options.input, options.output);
|
|
547
|
-
}
|