@pajecawav/json2code 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +180 -2
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +131 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/core/config.mjs +53 -0
- package/dist/core/config.mjs.map +1 -0
- package/dist/core/discovery/index.d.mts +10 -0
- package/dist/core/discovery/index.d.mts.map +1 -0
- package/dist/core/discovery/index.mjs +83 -0
- package/dist/core/discovery/index.mjs.map +1 -0
- package/dist/core/generate.d.mts +11 -0
- package/dist/core/generate.d.mts.map +1 -0
- package/dist/core/generate.mjs +89 -0
- package/dist/core/generate.mjs.map +1 -0
- package/dist/core/ir/types.d.mts +100 -0
- package/dist/core/ir/types.d.mts.map +1 -0
- package/dist/core/ir/visit.d.mts +8 -0
- package/dist/core/ir/visit.d.mts.map +1 -0
- package/dist/core/ir/visit.mjs +38 -0
- package/dist/core/ir/visit.mjs.map +1 -0
- package/dist/core/linker/index.d.mts +32 -0
- package/dist/core/linker/index.d.mts.map +1 -0
- package/dist/core/linker/index.mjs +104 -0
- package/dist/core/linker/index.mjs.map +1 -0
- package/dist/core/printer/abstract.d.mts +26 -0
- package/dist/core/printer/abstract.d.mts.map +1 -0
- package/dist/core/types.d.mts +76 -0
- package/dist/core/types.d.mts.map +1 -0
- package/dist/core/types.mjs +28 -0
- package/dist/core/types.mjs.map +1 -0
- package/dist/core/walker/abstract.d.mts +18 -0
- package/dist/core/walker/abstract.d.mts.map +1 -0
- package/dist/core/walker/json-schema.d.mts +10 -0
- package/dist/core/walker/json-schema.d.mts.map +1 -0
- package/dist/core/walker/json-schema.mjs +311 -0
- package/dist/core/walker/json-schema.mjs.map +1 -0
- package/dist/core/walker/ref-resolver.d.mts +22 -0
- package/dist/core/walker/ref-resolver.d.mts.map +1 -0
- package/dist/core/walker/ref-resolver.mjs +66 -0
- package/dist/core/walker/ref-resolver.mjs.map +1 -0
- package/dist/core/writer/index.d.mts +19 -0
- package/dist/core/writer/index.d.mts.map +1 -0
- package/dist/core/writer/index.mjs +84 -0
- package/dist/core/writer/index.mjs.map +1 -0
- package/dist/diagnostics/errors.d.mts +39 -0
- package/dist/diagnostics/errors.d.mts.map +1 -0
- package/dist/diagnostics/errors.mjs +50 -0
- package/dist/diagnostics/errors.mjs.map +1 -0
- package/dist/diagnostics/logger.d.mts +10 -0
- package/dist/diagnostics/logger.d.mts.map +1 -0
- package/dist/diagnostics/logger.mjs +18 -0
- package/dist/diagnostics/logger.mjs.map +1 -0
- package/dist/index.d.mts +24 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +26 -0
- package/dist/index.mjs.map +1 -0
- package/dist/package.mjs +6 -0
- package/dist/package.mjs.map +1 -0
- package/dist/targets/index.d.mts +9 -0
- package/dist/targets/index.d.mts.map +1 -0
- package/dist/targets/index.mjs +30 -0
- package/dist/targets/index.mjs.map +1 -0
- package/dist/targets/shared.mjs +137 -0
- package/dist/targets/shared.mjs.map +1 -0
- package/dist/targets/typescript/printer.d.mts +13 -0
- package/dist/targets/typescript/printer.d.mts.map +1 -0
- package/dist/targets/typescript/printer.mjs +47 -0
- package/dist/targets/typescript/printer.mjs.map +1 -0
- package/dist/targets/valibot/printer.d.mts +21 -0
- package/dist/targets/valibot/printer.d.mts.map +1 -0
- package/dist/targets/valibot/printer.mjs +198 -0
- package/dist/targets/valibot/printer.mjs.map +1 -0
- package/dist/targets/zod/lazy.d.mts +29 -0
- package/dist/targets/zod/lazy.d.mts.map +1 -0
- package/dist/targets/zod/lazy.mjs +100 -0
- package/dist/targets/zod/lazy.mjs.map +1 -0
- package/dist/targets/zod/printer.d.mts +21 -0
- package/dist/targets/zod/printer.d.mts.map +1 -0
- package/dist/targets/zod/printer.mjs +193 -0
- package/dist/targets/zod/printer.mjs.map +1 -0
- package/package.json +71 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pajecawav
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,181 @@
|
|
|
1
|
-
#
|
|
1
|
+
# json2code
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generate TypeScript code from JSON Schema — type aliases for the `typescript` target, [zod v4](https://zod.dev) schemas for the `zod` target, [valibot v1](https://valibot.dev) schemas for the `valibot` target. One pass, multiple targets, deterministic output.
|
|
4
|
+
|
|
5
|
+
- **Mirror layout** — `schemas/user.json` → `src/generated/typescript/user.ts`, imports between files resolved from `$ref`
|
|
6
|
+
- **Cycles without topological sort** — recursive schemas work in every target (`z.lazy` / `v.lazy` with SCC analysis)
|
|
7
|
+
- **Standard Schema** — the `zod` and `valibot` targets emit schemas that implement the [Standard Schema](https://standardschema.dev) interface
|
|
8
|
+
- **Extensible** — bring your own walker (any source format) and printer (any output layout) via `defineGenerator`
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add -D json2code
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quickstart
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
project/
|
|
20
|
+
├── schemas/
|
|
21
|
+
│ ├── common.json
|
|
22
|
+
│ └── user.json # { "$ref": "common.json#/$defs/Id" } …
|
|
23
|
+
└── json2code.config.ts
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// json2code.config.ts
|
|
28
|
+
import { defineConfig } from "json2code";
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
input: ["schemas"],
|
|
32
|
+
output: "src/generated",
|
|
33
|
+
targets: ["typescript", "zod"],
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
json2code # uses the config file
|
|
39
|
+
json2code schemas -o src/generated -t typescript,zod,valibot # or all via flags
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// src/generated/zod/user.ts (excerpt)
|
|
44
|
+
import { z } from "zod";
|
|
45
|
+
import { Id } from "./common.js";
|
|
46
|
+
|
|
47
|
+
export const User = z.object({ id: Id, email: z.email().optional() });
|
|
48
|
+
export type User = z.infer<typeof User>;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
json2code [input...] -o <dir> [flags]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Flag | Description |
|
|
58
|
+
| ------------------------------ | ------------------------------------------------------------------------------------- |
|
|
59
|
+
| `[input...]` | Globs and/or folders (default: `schemas/**/*.json`) |
|
|
60
|
+
| `-o, --output <dir>` | Output directory |
|
|
61
|
+
| `-t, --target <list>` | Comma-separated targets: `typescript`, `zod`, `valibot` (default: `typescript`) |
|
|
62
|
+
| `--config <path>` | Config file path (default: `json2code.config.*` in cwd) |
|
|
63
|
+
| `--import-extension <ext>` | Extension in relative imports: `js` \| `ts` \| `none` (default: `js`) |
|
|
64
|
+
| `--readonly-arrays` | Emit `readonly T[]` in the typescript target |
|
|
65
|
+
| `--naming <style>` | Export name style: `origin` \| `camel` \| `pascal` (default: `pascal`) |
|
|
66
|
+
| `--export-suffix <str>` | Literal appended to every export name, after the naming style (e.g. `Schema`) |
|
|
67
|
+
| `--header <template>` | Header template with `{{version}}` / `{{date}}` placeholders (`\n` escapes a newline) |
|
|
68
|
+
| `--no-header` | Don't emit the file header |
|
|
69
|
+
| `--zod-import-path <spec>` | Import specifier for zod in the zod target (default: `zod`) |
|
|
70
|
+
| `--valibot-import-path <spec>` | Import specifier for valibot in the valibot target (default: `valibot`) |
|
|
71
|
+
| `--no-defaults` | Don't apply JSON Schema `default` in validator targets |
|
|
72
|
+
| `--dry-run` | Print the plan, write nothing |
|
|
73
|
+
| `--clean` | Remove stale `@generated` files from previous runs |
|
|
74
|
+
| `-V, --verbose` | Verbose logging |
|
|
75
|
+
|
|
76
|
+
CLI flags override values from the config file.
|
|
77
|
+
|
|
78
|
+
Objects render multi-line with JSDoc comments derived from JSON Schema descriptions:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
export type User = {
|
|
82
|
+
/** Unique identifier */
|
|
83
|
+
id: string;
|
|
84
|
+
email?: string;
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## API
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { generate } from "json2code";
|
|
92
|
+
|
|
93
|
+
await generate({
|
|
94
|
+
input: ["schemas/**/*.json"],
|
|
95
|
+
output: "src/generated",
|
|
96
|
+
targets: ["typescript", "zod"],
|
|
97
|
+
options: {
|
|
98
|
+
importExtension: "js",
|
|
99
|
+
readonlyArrays: false,
|
|
100
|
+
importPaths: { zod: "zod", valibot: "valibot" },
|
|
101
|
+
defaults: true,
|
|
102
|
+
naming: "pascal", // or "origin" / "camel"
|
|
103
|
+
exportSuffix: "", // e.g. "Schema" → export const UserSchema = ...
|
|
104
|
+
header: "// generated by json2code v{{version}} on {{date}}", // or false
|
|
105
|
+
},
|
|
106
|
+
clean: true,
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Returns `{ written, deleted, diagnostics }`. Generation is deterministic: identical input produces byte-identical output, so checking generated code into VCS is diff-friendly. (The built-in `{{date}}` header placeholder is the one exception — it changes per day.)
|
|
111
|
+
|
|
112
|
+
## Custom generators
|
|
113
|
+
|
|
114
|
+
A target is a walker (source → IR) plus a printer (IR → code). Both are replaceable:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { defineGenerator, generate } from "json2code";
|
|
118
|
+
|
|
119
|
+
const bundle = defineGenerator({
|
|
120
|
+
name: "bundle",
|
|
121
|
+
// optional; default — JsonSchemaWalker
|
|
122
|
+
walker: {
|
|
123
|
+
name: "my-walker",
|
|
124
|
+
async walk(files, ctx) {
|
|
125
|
+
return files.map(f => ({
|
|
126
|
+
docId: f.docId,
|
|
127
|
+
sourcePath: f.path,
|
|
128
|
+
exports: new Map([["Stub", { kind: "unknown" }]]),
|
|
129
|
+
exportOrder: ["Stub"],
|
|
130
|
+
rootExportName: "Stub",
|
|
131
|
+
}));
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
// required; receives ALL docs at once so cross-file logic is possible
|
|
135
|
+
printer: {
|
|
136
|
+
name: "bundle-printer",
|
|
137
|
+
async print(docs, ctx) {
|
|
138
|
+
return [
|
|
139
|
+
{
|
|
140
|
+
path: "bundle/all.ts",
|
|
141
|
+
content: docs
|
|
142
|
+
.flatMap(d => d.exportOrder.map(n => `export type ${n} = unknown;`))
|
|
143
|
+
.join("\n"),
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
// optional; default — mirror the input structure under `<name>/`
|
|
149
|
+
mapOutput: docId => `bundle/${docId}`,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await generate({ input: ["schemas"], output: "out", targets: [bundle] });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Contracts (`AbstractWalker`, `AbstractPrinter`, IR types) are exported from the package root. Until v1.0 they may change with a minor bump.
|
|
156
|
+
|
|
157
|
+
## Diagnostics
|
|
158
|
+
|
|
159
|
+
Errors are aggregated across files and reported with context: `[file] → [$ref] → reason`.
|
|
160
|
+
|
|
161
|
+
| Code | Meaning |
|
|
162
|
+
| --------------------- | ---------------------------------------------------- |
|
|
163
|
+
| `J2C_FILE_NOT_FOUND` | No input files matched |
|
|
164
|
+
| `J2C_INVALID_JSON` | Schema is not valid JSON |
|
|
165
|
+
| `J2C_INVALID_REF` | `$ref` target does not exist |
|
|
166
|
+
| `J2C_EXTERNAL_REF` | External/URL refs are not supported |
|
|
167
|
+
| `J2C_NAME_COLLISION` | Two files map to the same export name |
|
|
168
|
+
| `J2C_UNKNOWN_KEYWORD` | Unsupported keyword → mapped to `unknown` + warning |
|
|
169
|
+
| `J2C_OUTPUT_DIRTY` | Output contains non-generated files blocking a write |
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
pnpm build # tsdown → dist/
|
|
175
|
+
pnpm test # vitest
|
|
176
|
+
pnpm lint # tsc + oxlint + prettier + publint
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Json2CodeError } from "./diagnostics/errors.mjs";
|
|
3
|
+
import { NAMING_STYLES } from "./core/types.mjs";
|
|
4
|
+
import { version } from "./package.mjs";
|
|
5
|
+
import { generate } from "./core/generate.mjs";
|
|
6
|
+
import { loadFileConfig } from "./core/config.mjs";
|
|
7
|
+
import { defineCommand, runMain } from "citty";
|
|
8
|
+
//#region src/cli.ts
|
|
9
|
+
const command = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "json2code",
|
|
12
|
+
version,
|
|
13
|
+
description: "Generate TypeScript code from JSON Schema."
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
output: {
|
|
17
|
+
type: "string",
|
|
18
|
+
alias: "o",
|
|
19
|
+
description: "Output directory"
|
|
20
|
+
},
|
|
21
|
+
target: {
|
|
22
|
+
type: "string",
|
|
23
|
+
alias: "t",
|
|
24
|
+
description: "Target(s), comma-separated (typescript,zod,valibot)"
|
|
25
|
+
},
|
|
26
|
+
config: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Path to config file (default: json2code.config.* in cwd)"
|
|
29
|
+
},
|
|
30
|
+
"import-extension": {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Import extension: js|ts|none"
|
|
33
|
+
},
|
|
34
|
+
"readonly-arrays": {
|
|
35
|
+
type: "boolean",
|
|
36
|
+
description: "Emit readonly arrays"
|
|
37
|
+
},
|
|
38
|
+
"zod-import-path": {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "Zod import specifier"
|
|
41
|
+
},
|
|
42
|
+
"valibot-import-path": {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Valibot import specifier"
|
|
45
|
+
},
|
|
46
|
+
"no-defaults": {
|
|
47
|
+
type: "boolean",
|
|
48
|
+
description: "Don't apply JSON Schema `default` in validator targets"
|
|
49
|
+
},
|
|
50
|
+
naming: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: `Export name style: ${NAMING_STYLES.join("|")} (default: pascal)`
|
|
53
|
+
},
|
|
54
|
+
"export-suffix": {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Literal appended to every export name (e.g. Schema → UsersUserSchema)"
|
|
57
|
+
},
|
|
58
|
+
header: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Header template ({{version}}, {{date}} placeholders, \"\\n\" for newlines); empty disables"
|
|
61
|
+
},
|
|
62
|
+
"no-header": {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
description: "Omit the file header"
|
|
65
|
+
},
|
|
66
|
+
"dry-run": {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
description: "Don't write, show the plan"
|
|
69
|
+
},
|
|
70
|
+
clean: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "Remove stale generated files"
|
|
73
|
+
},
|
|
74
|
+
verbose: {
|
|
75
|
+
type: "boolean",
|
|
76
|
+
alias: "V",
|
|
77
|
+
description: "Verbose logging"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
async run({ args }) {
|
|
81
|
+
const { config: file } = await loadFileConfig(args.config);
|
|
82
|
+
const input = args._.length > 0 ? [...args._] : file.input ? [...file.input] : ["schemas/**/*.json"];
|
|
83
|
+
const output = args.output ?? file.output;
|
|
84
|
+
if (!output) {
|
|
85
|
+
console.error("error: missing output directory (-o or `output` in the config file)");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const targets = args.target ? args.target.split(",").map((s) => s.trim()).filter(Boolean) : file.targets ?? ["typescript"];
|
|
89
|
+
const namingArg = args.naming ?? file.options?.naming;
|
|
90
|
+
if (namingArg !== void 0 && !NAMING_STYLES.includes(namingArg)) {
|
|
91
|
+
console.error(`error: invalid naming style "${namingArg}" (expected one of: ${NAMING_STYLES.join(", ")})`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const rawHeader = args.header;
|
|
95
|
+
const headerArg = rawHeader === false || args["no-header"] === true ? false : typeof rawHeader === "string" ? rawHeader.replaceAll("\\n", "\n") : file.options?.header;
|
|
96
|
+
const config = {
|
|
97
|
+
input,
|
|
98
|
+
output,
|
|
99
|
+
targets,
|
|
100
|
+
options: {
|
|
101
|
+
importExtension: args["import-extension"] ?? file.options?.importExtension,
|
|
102
|
+
readonlyArrays: args["readonly-arrays"] ?? file.options?.readonlyArrays,
|
|
103
|
+
importPaths: {
|
|
104
|
+
...file.options?.importPaths,
|
|
105
|
+
...args["zod-import-path"] ? { zod: args["zod-import-path"] } : {},
|
|
106
|
+
...args["valibot-import-path"] ? { valibot: args["valibot-import-path"] } : {}
|
|
107
|
+
},
|
|
108
|
+
defaults: !(args["no-defaults"] ?? false) && (file.options?.defaults ?? true),
|
|
109
|
+
naming: namingArg,
|
|
110
|
+
exportSuffix: args["export-suffix"] ?? file.options?.exportSuffix,
|
|
111
|
+
header: headerArg
|
|
112
|
+
},
|
|
113
|
+
clean: args.clean ?? file.clean,
|
|
114
|
+
dryRun: args["dry-run"] ?? file.dryRun,
|
|
115
|
+
verbose: args.verbose ?? file.verbose
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
await generate(config);
|
|
119
|
+
process.exit(0);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (e instanceof Json2CodeError) process.exit(1);
|
|
122
|
+
console.error(e);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
await runMain(command);
|
|
128
|
+
//#endregion
|
|
129
|
+
export {};
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["pkg.version"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { defineCommand, runMain } from \"citty\";\nimport pkg from \"../package.json\" with { type: \"json\" };\nimport { loadFileConfig } from \"./core/config.js\";\nimport { generate } from \"./core/generate.js\";\nimport type { GenerateConfig, ImportExtension, NamingStyle, TargetName } from \"./core/types.js\";\nimport { NAMING_STYLES } from \"./core/types.js\";\nimport { Json2CodeError } from \"./diagnostics/errors.js\";\n\nconst command = defineCommand({\n\tmeta: {\n\t\tname: \"json2code\",\n\t\tversion: pkg.version,\n\t\tdescription: \"Generate TypeScript code from JSON Schema.\",\n\t},\n\targs: {\n\t\toutput: { type: \"string\", alias: \"o\", description: \"Output directory\" },\n\t\ttarget: {\n\t\t\ttype: \"string\",\n\t\t\talias: \"t\",\n\t\t\tdescription: \"Target(s), comma-separated (typescript,zod,valibot)\",\n\t\t},\n\t\tconfig: {\n\t\t\ttype: \"string\",\n\t\t\tdescription: \"Path to config file (default: json2code.config.* in cwd)\",\n\t\t},\n\t\t\"import-extension\": {\n\t\t\ttype: \"string\",\n\t\t\tdescription: \"Import extension: js|ts|none\",\n\t\t},\n\t\t\"readonly-arrays\": { type: \"boolean\", description: \"Emit readonly arrays\" },\n\t\t\"zod-import-path\": { type: \"string\", description: \"Zod import specifier\" },\n\t\t\"valibot-import-path\": { type: \"string\", description: \"Valibot import specifier\" },\n\t\t\"no-defaults\": {\n\t\t\ttype: \"boolean\",\n\t\t\tdescription: \"Don't apply JSON Schema `default` in validator targets\",\n\t\t},\n\t\tnaming: {\n\t\t\ttype: \"string\",\n\t\t\tdescription: `Export name style: ${NAMING_STYLES.join(\"|\")} (default: pascal)`,\n\t\t},\n\t\t\"export-suffix\": {\n\t\t\ttype: \"string\",\n\t\t\tdescription: \"Literal appended to every export name (e.g. Schema → UsersUserSchema)\",\n\t\t},\n\t\theader: {\n\t\t\ttype: \"string\",\n\t\t\tdescription:\n\t\t\t\t'Header template ({{version}}, {{date}} placeholders, \"\\\\n\" for newlines); empty disables',\n\t\t},\n\t\t\"no-header\": { type: \"boolean\", description: \"Omit the file header\" },\n\t\t\"dry-run\": { type: \"boolean\", description: \"Don't write, show the plan\" },\n\t\tclean: { type: \"boolean\", description: \"Remove stale generated files\" },\n\t\tverbose: { type: \"boolean\", alias: \"V\", description: \"Verbose logging\" },\n\t},\n\tasync run({ args }) {\n\t\tconst { config: file } = await loadFileConfig(args.config);\n\t\tconst input =\n\t\t\targs._.length > 0 ? [...args._] : file.input ? [...file.input] : [\"schemas/**/*.json\"];\n\t\tconst output = args.output ?? file.output;\n\t\tif (!output) {\n\t\t\tconsole.error(\"error: missing output directory (-o or `output` in the config file)\");\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconst targets = (\n\t\t\targs.target\n\t\t\t\t? args.target\n\t\t\t\t\t\t.split(\",\")\n\t\t\t\t\t\t.map(s => s.trim())\n\t\t\t\t\t\t.filter(Boolean)\n\t\t\t\t: (file.targets ?? [\"typescript\"])\n\t\t) as TargetName[];\n\t\tconst namingArg = (args.naming as NamingStyle | undefined) ?? file.options?.naming;\n\t\tif (namingArg !== undefined && !NAMING_STYLES.includes(namingArg)) {\n\t\t\tconsole.error(\n\t\t\t\t`error: invalid naming style \"${namingArg}\" (expected one of: ${NAMING_STYLES.join(\", \")})`,\n\t\t\t);\n\t\t\tprocess.exit(1);\n\t\t}\n\t\t// citty maps `--no-header` to `header === false`; `--header <text>` gives a string\n\t\tconst rawHeader = args.header as string | boolean | undefined;\n\t\tconst headerArg =\n\t\t\trawHeader === false || args[\"no-header\"] === true\n\t\t\t\t? false\n\t\t\t\t: typeof rawHeader === \"string\"\n\t\t\t\t\t? rawHeader.replaceAll(\"\\\\n\", \"\\n\")\n\t\t\t\t\t: file.options?.header;\n\t\tconst config: GenerateConfig = {\n\t\t\tinput,\n\t\t\toutput,\n\t\t\ttargets,\n\t\t\toptions: {\n\t\t\t\timportExtension:\n\t\t\t\t\t(args[\"import-extension\"] as ImportExtension | undefined) ??\n\t\t\t\t\tfile.options?.importExtension,\n\t\t\t\treadonlyArrays: args[\"readonly-arrays\"] ?? file.options?.readonlyArrays,\n\t\t\t\timportPaths: {\n\t\t\t\t\t...file.options?.importPaths,\n\t\t\t\t\t...(args[\"zod-import-path\"] ? { zod: args[\"zod-import-path\"] } : {}),\n\t\t\t\t\t...(args[\"valibot-import-path\"]\n\t\t\t\t\t\t? { valibot: args[\"valibot-import-path\"] }\n\t\t\t\t\t\t: {}),\n\t\t\t\t},\n\t\t\t\tdefaults: !(args[\"no-defaults\"] ?? false) && (file.options?.defaults ?? true),\n\t\t\t\tnaming: namingArg,\n\t\t\t\texportSuffix: args[\"export-suffix\"] ?? file.options?.exportSuffix,\n\t\t\t\theader: headerArg,\n\t\t\t},\n\t\t\tclean: args.clean ?? file.clean,\n\t\t\tdryRun: args[\"dry-run\"] ?? file.dryRun,\n\t\t\tverbose: args.verbose ?? file.verbose,\n\t\t};\n\t\ttry {\n\t\t\tawait generate(config);\n\t\t\tprocess.exit(0);\n\t\t} catch (e) {\n\t\t\tif (e instanceof Json2CodeError) {\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t\tconsole.error(e);\n\t\t\tprocess.exit(1);\n\t\t}\n\t},\n});\n\nawait runMain(command);\n"],"mappings":";;;;;;;;AASA,MAAM,UAAU,cAAc;CAC7B,MAAM;EACL,MAAM;EACGA;EACT,aAAa;CACd;CACA,MAAM;EACL,QAAQ;GAAE,MAAM;GAAU,OAAO;GAAK,aAAa;EAAmB;EACtE,QAAQ;GACP,MAAM;GACN,OAAO;GACP,aAAa;EACd;EACA,QAAQ;GACP,MAAM;GACN,aAAa;EACd;EACA,oBAAoB;GACnB,MAAM;GACN,aAAa;EACd;EACA,mBAAmB;GAAE,MAAM;GAAW,aAAa;EAAuB;EAC1E,mBAAmB;GAAE,MAAM;GAAU,aAAa;EAAuB;EACzE,uBAAuB;GAAE,MAAM;GAAU,aAAa;EAA2B;EACjF,eAAe;GACd,MAAM;GACN,aAAa;EACd;EACA,QAAQ;GACP,MAAM;GACN,aAAa,sBAAsB,cAAc,KAAK,GAAG,EAAE;EAC5D;EACA,iBAAiB;GAChB,MAAM;GACN,aAAa;EACd;EACA,QAAQ;GACP,MAAM;GACN,aACC;EACF;EACA,aAAa;GAAE,MAAM;GAAW,aAAa;EAAuB;EACpE,WAAW;GAAE,MAAM;GAAW,aAAa;EAA6B;EACxE,OAAO;GAAE,MAAM;GAAW,aAAa;EAA+B;EACtE,SAAS;GAAE,MAAM;GAAW,OAAO;GAAK,aAAa;EAAkB;CACxE;CACA,MAAM,IAAI,EAAE,QAAQ;EACnB,MAAM,EAAE,QAAQ,SAAS,MAAM,eAAe,KAAK,MAAM;EACzD,MAAM,QACL,KAAK,EAAE,SAAS,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,mBAAmB;EACtF,MAAM,SAAS,KAAK,UAAU,KAAK;EACnC,IAAI,CAAC,QAAQ;GACZ,QAAQ,MAAM,qEAAqE;GACnF,QAAQ,KAAK,CAAC;EACf;EACA,MAAM,UACL,KAAK,SACF,KAAK,OACJ,MAAM,GAAG,CAAC,CACV,KAAI,MAAK,EAAE,KAAK,CAAC,CAAC,CAClB,OAAO,OAAO,IACd,KAAK,WAAW,CAAC,YAAY;EAElC,MAAM,YAAa,KAAK,UAAsC,KAAK,SAAS;EAC5E,IAAI,cAAc,KAAA,KAAa,CAAC,cAAc,SAAS,SAAS,GAAG;GAClE,QAAQ,MACP,gCAAgC,UAAU,sBAAsB,cAAc,KAAK,IAAI,EAAE,EAC1F;GACA,QAAQ,KAAK,CAAC;EACf;EAEA,MAAM,YAAY,KAAK;EACvB,MAAM,YACL,cAAc,SAAS,KAAK,iBAAiB,OAC1C,QACA,OAAO,cAAc,WACpB,UAAU,WAAW,OAAO,IAAI,IAChC,KAAK,SAAS;EACnB,MAAM,SAAyB;GAC9B;GACA;GACA;GACA,SAAS;IACR,iBACE,KAAK,uBACN,KAAK,SAAS;IACf,gBAAgB,KAAK,sBAAsB,KAAK,SAAS;IACzD,aAAa;KACZ,GAAG,KAAK,SAAS;KACjB,GAAI,KAAK,qBAAqB,EAAE,KAAK,KAAK,mBAAmB,IAAI,CAAC;KAClE,GAAI,KAAK,yBACN,EAAE,SAAS,KAAK,uBAAuB,IACvC,CAAC;IACL;IACA,UAAU,EAAE,KAAK,kBAAkB,WAAW,KAAK,SAAS,YAAY;IACxE,QAAQ;IACR,cAAc,KAAK,oBAAoB,KAAK,SAAS;IACrD,QAAQ;GACT;GACA,OAAO,KAAK,SAAS,KAAK;GAC1B,QAAQ,KAAK,cAAc,KAAK;GAChC,SAAS,KAAK,WAAW,KAAK;EAC/B;EACA,IAAI;GACH,MAAM,SAAS,MAAM;GACrB,QAAQ,KAAK,CAAC;EACf,SAAS,GAAG;GACX,IAAI,aAAa,gBAChB,QAAQ,KAAK,CAAC;GAEf,QAAQ,MAAM,CAAC;GACf,QAAQ,KAAK,CAAC;EACf;CACD;AACD,CAAC;AAED,MAAM,QAAQ,OAAO"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { access, constants } from "node:fs/promises";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { createJiti } from "jiti";
|
|
5
|
+
//#region src/core/config.ts
|
|
6
|
+
const CONFIG_CANDIDATES = [
|
|
7
|
+
"json2code.config.ts",
|
|
8
|
+
"json2code.config.mts",
|
|
9
|
+
"json2code.config.js",
|
|
10
|
+
"json2code.config.mjs",
|
|
11
|
+
"json2code.config.json"
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Load the config file: `explicitPath` if given, otherwise the first
|
|
15
|
+
* `json2code.config.{ts,mts,js,mjs,json}` found in `cwd`. Returns an empty
|
|
16
|
+
* config when nothing is found.
|
|
17
|
+
*/
|
|
18
|
+
async function loadFileConfig(explicitPath, cwd = process.cwd()) {
|
|
19
|
+
const jiti = createJiti(import.meta.url);
|
|
20
|
+
if (explicitPath) {
|
|
21
|
+
const file = path.resolve(cwd, explicitPath);
|
|
22
|
+
return {
|
|
23
|
+
config: await readConfig(jiti, file),
|
|
24
|
+
path: file
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
for (const name of CONFIG_CANDIDATES) {
|
|
28
|
+
const file = path.resolve(cwd, name);
|
|
29
|
+
if (await accessible(file)) return {
|
|
30
|
+
config: await readConfig(jiti, file),
|
|
31
|
+
path: file
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { config: {} };
|
|
35
|
+
}
|
|
36
|
+
async function accessible(file) {
|
|
37
|
+
try {
|
|
38
|
+
await access(file, constants.R_OK);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function readConfig(jiti, file) {
|
|
45
|
+
const mod = await jiti.import(file);
|
|
46
|
+
const raw = typeof mod === "object" && mod !== null && "default" in mod ? mod.default : mod;
|
|
47
|
+
if (typeof raw !== "object" || raw === null) throw new TypeError(`Config file ${path.basename(file)} must export a config object.`);
|
|
48
|
+
return raw;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { loadFileConfig };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=config.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.mjs","names":[],"sources":["../../src/core/config.ts"],"sourcesContent":["import { access, constants } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { createJiti } from \"jiti\";\nimport type { GenerateOptions, TargetSpec } from \"./types.js\";\n\n/** Everything a `json2code.config.*` file may set. All fields optional. */\nexport interface FileConfig {\n\treadonly input?: readonly string[];\n\treadonly output?: string;\n\treadonly targets?: readonly TargetSpec[];\n\treadonly options?: Partial<GenerateOptions>;\n\treadonly clean?: boolean;\n\treadonly dryRun?: boolean;\n\treadonly verbose?: boolean;\n}\n\nconst CONFIG_CANDIDATES = [\n\t\"json2code.config.ts\",\n\t\"json2code.config.mts\",\n\t\"json2code.config.js\",\n\t\"json2code.config.mjs\",\n\t\"json2code.config.json\",\n] as const;\n\n/**\n * Load the config file: `explicitPath` if given, otherwise the first\n * `json2code.config.{ts,mts,js,mjs,json}` found in `cwd`. Returns an empty\n * config when nothing is found.\n */\nexport async function loadFileConfig(\n\texplicitPath?: string,\n\tcwd: string = process.cwd(),\n): Promise<{ config: FileConfig; path?: string }> {\n\tconst jiti = createJiti(import.meta.url);\n\tif (explicitPath) {\n\t\tconst file = path.resolve(cwd, explicitPath);\n\t\treturn { config: await readConfig(jiti, file), path: file };\n\t}\n\tfor (const name of CONFIG_CANDIDATES) {\n\t\tconst file = path.resolve(cwd, name);\n\t\tif (await accessible(file)) {\n\t\t\treturn { config: await readConfig(jiti, file), path: file };\n\t\t}\n\t}\n\treturn { config: {} };\n}\n\nasync function accessible(file: string): Promise<boolean> {\n\ttry {\n\t\tawait access(file, constants.R_OK);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nasync function readConfig(jiti: ReturnType<typeof createJiti>, file: string): Promise<FileConfig> {\n\tconst mod: unknown = await jiti.import(file);\n\tconst raw = typeof mod === \"object\" && mod !== null && \"default\" in mod ? mod.default : mod;\n\tif (typeof raw !== \"object\" || raw === null) {\n\t\tthrow new TypeError(`Config file ${path.basename(file)} must export a config object.`);\n\t}\n\treturn raw;\n}\n"],"mappings":";;;;;AAiBA,MAAM,oBAAoB;CACzB;CACA;CACA;CACA;CACA;AACD;;;;;;AAOA,eAAsB,eACrB,cACA,MAAc,QAAQ,IAAI,GACuB;CACjD,MAAM,OAAO,WAAW,YAAY,GAAG;CACvC,IAAI,cAAc;EACjB,MAAM,OAAO,KAAK,QAAQ,KAAK,YAAY;EAC3C,OAAO;GAAE,QAAQ,MAAM,WAAW,MAAM,IAAI;GAAG,MAAM;EAAK;CAC3D;CACA,KAAK,MAAM,QAAQ,mBAAmB;EACrC,MAAM,OAAO,KAAK,QAAQ,KAAK,IAAI;EACnC,IAAI,MAAM,WAAW,IAAI,GACxB,OAAO;GAAE,QAAQ,MAAM,WAAW,MAAM,IAAI;GAAG,MAAM;EAAK;CAE5D;CACA,OAAO,EAAE,QAAQ,CAAC,EAAE;AACrB;AAEA,eAAe,WAAW,MAAgC;CACzD,IAAI;EACH,MAAM,OAAO,MAAM,UAAU,IAAI;EACjC,OAAO;CACR,QAAQ;EACP,OAAO;CACR;AACD;AAEA,eAAe,WAAW,MAAqC,MAAmC;CACjG,MAAM,MAAe,MAAM,KAAK,OAAO,IAAI;CAC3C,MAAM,MAAM,OAAO,QAAQ,YAAY,QAAQ,QAAQ,aAAa,MAAM,IAAI,UAAU;CACxF,IAAI,OAAO,QAAQ,YAAY,QAAQ,MACtC,MAAM,IAAI,UAAU,eAAe,KAAK,SAAS,IAAI,EAAE,8BAA8B;CAEtF,OAAO;AACR"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Logger } from "../../diagnostics/logger.mjs";
|
|
2
|
+
import { SchemaFile } from "../types.mjs";
|
|
3
|
+
//#region src/core/discovery/index.d.ts
|
|
4
|
+
export interface DiscoveryResult {
|
|
5
|
+
readonly files: readonly SchemaFile[];
|
|
6
|
+
readonly rootDir: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function discover(input: string | string[], cwd: string, logger: Logger): Promise<DiscoveryResult>;
|
|
9
|
+
//#endregion
|
|
10
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/core/discovery/index.ts"],"mappings":";;;iBASiB;WACP,gBAAgB;WAChB;;wBAGY,SACrB,0BACA,aACA,QAAQ,SACN,QAAQ"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { DiagnosticCode, Json2CodeError, createDiagnostic } from "../../diagnostics/errors.mjs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import fg from "fast-glob";
|
|
5
|
+
//#region src/core/discovery/index.ts
|
|
6
|
+
const GLOB_CHARS = /[*?[\]{}]/;
|
|
7
|
+
async function discover(input, cwd, logger) {
|
|
8
|
+
const entries = Array.isArray(input) ? input : [input];
|
|
9
|
+
const matched = [];
|
|
10
|
+
for (const entry of entries) matched.push(...await expandInput(entry, cwd));
|
|
11
|
+
const unique = [...new Set(matched)].toSorted();
|
|
12
|
+
if (unique.length === 0) throw new Json2CodeError([createDiagnostic(DiagnosticCode.FileNotFound, "error", `No JSON Schema files matched input: ${entries.join(", ")}`)]);
|
|
13
|
+
const rootDir = commonDir(unique);
|
|
14
|
+
logger.debug(`discovery rootDir=${rootDir}, ${unique.length} files`);
|
|
15
|
+
const files = [];
|
|
16
|
+
for (const abs of unique) {
|
|
17
|
+
const docId = toDocId(abs, rootDir);
|
|
18
|
+
const content = await readFile(abs, "utf8");
|
|
19
|
+
files.push({
|
|
20
|
+
path: abs,
|
|
21
|
+
docId,
|
|
22
|
+
content
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
files,
|
|
27
|
+
rootDir
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function expandInput(entry, cwd) {
|
|
31
|
+
const abs = path.resolve(cwd, entry);
|
|
32
|
+
if (!GLOB_CHARS.test(entry)) {
|
|
33
|
+
const kind = await pathKind(abs);
|
|
34
|
+
if (kind === "dir") return fg("**/*.json", {
|
|
35
|
+
cwd: abs,
|
|
36
|
+
absolute: true,
|
|
37
|
+
onlyFiles: true,
|
|
38
|
+
ignore: ["**/node_modules/**"]
|
|
39
|
+
});
|
|
40
|
+
if (kind === "file") return [abs];
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return fg(entry, {
|
|
44
|
+
cwd,
|
|
45
|
+
absolute: true,
|
|
46
|
+
onlyFiles: true,
|
|
47
|
+
ignore: ["**/node_modules/**"]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function pathKind(p) {
|
|
51
|
+
try {
|
|
52
|
+
const s = await stat(p);
|
|
53
|
+
return s.isFile() ? "file" : s.isDirectory() ? "dir" : "missing";
|
|
54
|
+
} catch {
|
|
55
|
+
return "missing";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Longest common directory of a set of absolute paths. */
|
|
59
|
+
function commonDir(paths) {
|
|
60
|
+
const dirs = paths.map((p) => path.dirname(p).split(path.sep));
|
|
61
|
+
const first = dirs[0];
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < first.length) {
|
|
64
|
+
const seg = first[i];
|
|
65
|
+
if (seg === void 0) break;
|
|
66
|
+
let ok = true;
|
|
67
|
+
for (let j = 1; j < dirs.length; j++) if (dirs[j][i] !== seg) {
|
|
68
|
+
ok = false;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (!ok) break;
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
const common = first.slice(0, i).join(path.sep);
|
|
75
|
+
return common === "" ? path.sep : common;
|
|
76
|
+
}
|
|
77
|
+
function toDocId(absPath, rootDir) {
|
|
78
|
+
return path.relative(rootDir, absPath).split(path.sep).join("/");
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { discover };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/core/discovery/index.ts"],"sourcesContent":["import { readFile, stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport { createDiagnostic, DiagnosticCode, Json2CodeError } from \"../../diagnostics/errors.js\";\nimport type { Logger } from \"../../diagnostics/logger.js\";\nimport type { SchemaFile } from \"../types.js\";\n\nconst GLOB_CHARS = /[*?[\\]{}]/;\n\nexport interface DiscoveryResult {\n\treadonly files: readonly SchemaFile[];\n\treadonly rootDir: string;\n}\n\nexport async function discover(\n\tinput: string | string[],\n\tcwd: string,\n\tlogger: Logger,\n): Promise<DiscoveryResult> {\n\tconst entries = Array.isArray(input) ? input : [input];\n\tconst matched: string[] = [];\n\n\tfor (const entry of entries) {\n\t\tmatched.push(...(await expandInput(entry, cwd)));\n\t}\n\n\tconst unique = [...new Set(matched)].toSorted();\n\tif (unique.length === 0) {\n\t\tthrow new Json2CodeError([\n\t\t\tcreateDiagnostic(\n\t\t\t\tDiagnosticCode.FileNotFound,\n\t\t\t\t\"error\",\n\t\t\t\t`No JSON Schema files matched input: ${entries.join(\", \")}`,\n\t\t\t),\n\t\t]);\n\t}\n\n\tconst rootDir = commonDir(unique);\n\tlogger.debug(`discovery rootDir=${rootDir}, ${unique.length} files`);\n\n\tconst files: SchemaFile[] = [];\n\tfor (const abs of unique) {\n\t\tconst docId = toDocId(abs, rootDir);\n\t\tconst content = await readFile(abs, \"utf8\");\n\t\tfiles.push({ path: abs, docId, content });\n\t}\n\treturn { files, rootDir };\n}\n\nasync function expandInput(entry: string, cwd: string): Promise<string[]> {\n\tconst abs = path.resolve(cwd, entry);\n\tconst isGlob = GLOB_CHARS.test(entry);\n\tif (!isGlob) {\n\t\tconst kind = await pathKind(abs);\n\t\tif (kind === \"dir\") {\n\t\t\treturn fg(\"**/*.json\", {\n\t\t\t\tcwd: abs,\n\t\t\t\tabsolute: true,\n\t\t\t\tonlyFiles: true,\n\t\t\t\tignore: [\"**/node_modules/**\"],\n\t\t\t});\n\t\t}\n\t\tif (kind === \"file\") {\n\t\t\treturn [abs];\n\t\t}\n\t\treturn [];\n\t}\n\treturn fg(entry, { cwd, absolute: true, onlyFiles: true, ignore: [\"**/node_modules/**\"] });\n}\n\nasync function pathKind(p: string): Promise<\"file\" | \"dir\" | \"missing\"> {\n\ttry {\n\t\tconst s = await stat(p);\n\t\treturn s.isFile() ? \"file\" : s.isDirectory() ? \"dir\" : \"missing\";\n\t} catch {\n\t\treturn \"missing\";\n\t}\n}\n\n/** Longest common directory of a set of absolute paths. */\nfunction commonDir(paths: readonly string[]): string {\n\tconst dirs = paths.map(p => path.dirname(p).split(path.sep));\n\tconst first = dirs[0]!;\n\tlet i = 0;\n\twhile (i < first.length) {\n\t\tconst seg = first[i];\n\t\tif (seg === undefined) {\n\t\t\tbreak;\n\t\t}\n\t\tlet ok = true;\n\t\tfor (let j = 1; j < dirs.length; j++) {\n\t\t\tif (dirs[j]![i] !== seg) {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!ok) {\n\t\t\tbreak;\n\t\t}\n\t\ti++;\n\t}\n\tconst common = first.slice(0, i).join(path.sep);\n\treturn common === \"\" ? path.sep : common;\n}\n\nfunction toDocId(absPath: string, rootDir: string): string {\n\tconst rel = path.relative(rootDir, absPath);\n\treturn rel.split(path.sep).join(\"/\");\n}\n"],"mappings":";;;;;AAOA,MAAM,aAAa;AAOnB,eAAsB,SACrB,OACA,KACA,QAC2B;CAC3B,MAAM,UAAU,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;CACrD,MAAM,UAAoB,CAAC;CAE3B,KAAK,MAAM,SAAS,SACnB,QAAQ,KAAK,GAAI,MAAM,YAAY,OAAO,GAAG,CAAE;CAGhD,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,SAAS;CAC9C,IAAI,OAAO,WAAW,GACrB,MAAM,IAAI,eAAe,CACxB,iBACC,eAAe,cACf,SACA,uCAAuC,QAAQ,KAAK,IAAI,GACzD,CACD,CAAC;CAGF,MAAM,UAAU,UAAU,MAAM;CAChC,OAAO,MAAM,qBAAqB,QAAQ,IAAI,OAAO,OAAO,OAAO;CAEnE,MAAM,QAAsB,CAAC;CAC7B,KAAK,MAAM,OAAO,QAAQ;EACzB,MAAM,QAAQ,QAAQ,KAAK,OAAO;EAClC,MAAM,UAAU,MAAM,SAAS,KAAK,MAAM;EAC1C,MAAM,KAAK;GAAE,MAAM;GAAK;GAAO;EAAQ,CAAC;CACzC;CACA,OAAO;EAAE;EAAO;CAAQ;AACzB;AAEA,eAAe,YAAY,OAAe,KAAgC;CACzE,MAAM,MAAM,KAAK,QAAQ,KAAK,KAAK;CAEnC,IAAI,CADW,WAAW,KAAK,KACrB,GAAG;EACZ,MAAM,OAAO,MAAM,SAAS,GAAG;EAC/B,IAAI,SAAS,OACZ,OAAO,GAAG,aAAa;GACtB,KAAK;GACL,UAAU;GACV,WAAW;GACX,QAAQ,CAAC,oBAAoB;EAC9B,CAAC;EAEF,IAAI,SAAS,QACZ,OAAO,CAAC,GAAG;EAEZ,OAAO,CAAC;CACT;CACA,OAAO,GAAG,OAAO;EAAE;EAAK,UAAU;EAAM,WAAW;EAAM,QAAQ,CAAC,oBAAoB;CAAE,CAAC;AAC1F;AAEA,eAAe,SAAS,GAAgD;CACvE,IAAI;EACH,MAAM,IAAI,MAAM,KAAK,CAAC;EACtB,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,YAAY,IAAI,QAAQ;CACxD,QAAQ;EACP,OAAO;CACR;AACD;;AAGA,SAAS,UAAU,OAAkC;CACpD,MAAM,OAAO,MAAM,KAAI,MAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC;CAC3D,MAAM,QAAQ,KAAK;CACnB,IAAI,IAAI;CACR,OAAO,IAAI,MAAM,QAAQ;EACxB,MAAM,MAAM,MAAM;EAClB,IAAI,QAAQ,KAAA,GACX;EAED,IAAI,KAAK;EACT,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAChC,IAAI,KAAK,EAAE,CAAE,OAAO,KAAK;GACxB,KAAK;GACL;EACD;EAED,IAAI,CAAC,IACJ;EAED;CACD;CACA,MAAM,SAAS,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,GAAG;CAC9C,OAAO,WAAW,KAAK,KAAK,MAAM;AACnC;AAEA,SAAS,QAAQ,SAAiB,SAAyB;CAE1D,OADY,KAAK,SAAS,SAAS,OAC1B,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG;AACpC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Diagnostic } from "../diagnostics/errors.mjs";
|
|
2
|
+
import { GenerateConfig } from "./types.mjs";
|
|
3
|
+
//#region src/core/generate.d.ts
|
|
4
|
+
export interface GenerateResult {
|
|
5
|
+
readonly written: readonly string[];
|
|
6
|
+
readonly deleted: readonly string[];
|
|
7
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
8
|
+
}
|
|
9
|
+
export declare function generate(config: GenerateConfig): Promise<GenerateResult>;
|
|
10
|
+
//#endregion
|
|
11
|
+
//# sourceMappingURL=generate.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.mts","names":[],"sources":["../../src/core/generate.ts"],"mappings":";;;iBA0BiB;WACP;WACA;WACA,sBAAsB;;wBAKV,SAAS,QAAQ,iBAAiB,QAAQ"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { DiagnosticCode, Json2CodeError, createDiagnostic, formatDiagnostic, partitionDiagnostics } from "../diagnostics/errors.mjs";
|
|
2
|
+
import { createLogger } from "../diagnostics/logger.mjs";
|
|
3
|
+
import { resolveTarget } from "../targets/index.mjs";
|
|
4
|
+
import { discover } from "./discovery/index.mjs";
|
|
5
|
+
import { Linker } from "./linker/index.mjs";
|
|
6
|
+
import { DEFAULT_OPTIONS, NAMING_STYLES } from "./types.mjs";
|
|
7
|
+
import { JsonSchemaWalker } from "./walker/json-schema.mjs";
|
|
8
|
+
import { writeFiles } from "./writer/index.mjs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
//#region src/core/generate.ts
|
|
11
|
+
const defaultWalker = new JsonSchemaWalker();
|
|
12
|
+
async function generate(config) {
|
|
13
|
+
const logger = createLogger(config.verbose ?? false);
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const output = path.resolve(cwd, config.output);
|
|
16
|
+
const options = mergeOptions(config.options);
|
|
17
|
+
const allDiagnostics = [];
|
|
18
|
+
const allFiles = [];
|
|
19
|
+
const { files, rootDir } = await discover(config.input, cwd, logger);
|
|
20
|
+
if (files.length === 0) throw new Json2CodeError([createDiagnostic(DiagnosticCode.FileNotFound, "error", "No JSON Schema files matched the input.")]);
|
|
21
|
+
const resolved = {
|
|
22
|
+
...options,
|
|
23
|
+
output,
|
|
24
|
+
rootDir
|
|
25
|
+
};
|
|
26
|
+
const walkCache = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const spec of normalizeTargets(config.targets)) {
|
|
28
|
+
const def = resolveTarget(spec);
|
|
29
|
+
const walker = def.walker ?? defaultWalker;
|
|
30
|
+
let docsPromise = walkCache.get(walker);
|
|
31
|
+
if (!docsPromise) {
|
|
32
|
+
const wctx = {
|
|
33
|
+
logger,
|
|
34
|
+
options: resolved,
|
|
35
|
+
diagnostics: allDiagnostics
|
|
36
|
+
};
|
|
37
|
+
docsPromise = walker.walk(files, wctx);
|
|
38
|
+
walkCache.set(walker, docsPromise);
|
|
39
|
+
}
|
|
40
|
+
const docs = await docsPromise;
|
|
41
|
+
const mapOutput = def.mapOutput ?? ((docId) => Linker.defaultMapOutput(def.name, docId));
|
|
42
|
+
const pctx = new Linker({
|
|
43
|
+
options: resolved,
|
|
44
|
+
targetName: def.name,
|
|
45
|
+
mapOutput,
|
|
46
|
+
diagnostics: allDiagnostics
|
|
47
|
+
}).createPrintContext(logger, allDiagnostics);
|
|
48
|
+
const printed = await def.printer.print(docs, pctx);
|
|
49
|
+
for (const f of printed) allFiles.push(f);
|
|
50
|
+
}
|
|
51
|
+
const { errors, warnings } = partitionDiagnostics(allDiagnostics);
|
|
52
|
+
for (const w of warnings) logger.warn(formatDiagnostic(w));
|
|
53
|
+
if (errors.length > 0) {
|
|
54
|
+
for (const e of errors) logger.error(formatDiagnostic(e));
|
|
55
|
+
throw new Json2CodeError(errors);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...await writeFiles(allFiles, {
|
|
59
|
+
output,
|
|
60
|
+
dryRun: config.dryRun ?? false,
|
|
61
|
+
clean: config.clean ?? false,
|
|
62
|
+
logger,
|
|
63
|
+
header: options.header
|
|
64
|
+
}),
|
|
65
|
+
diagnostics: allDiagnostics
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function normalizeTargets(targets) {
|
|
69
|
+
return targets.length > 0 ? targets : ["typescript"];
|
|
70
|
+
}
|
|
71
|
+
function mergeOptions(opts) {
|
|
72
|
+
const naming = opts?.naming ?? DEFAULT_OPTIONS.naming;
|
|
73
|
+
if (!NAMING_STYLES.includes(naming)) throw new TypeError(`Invalid naming style: ${JSON.stringify(naming)} (expected one of ${NAMING_STYLES.join(", ")})`);
|
|
74
|
+
const defined = Object.fromEntries(Object.entries(opts ?? {}).filter(([, v]) => v !== void 0));
|
|
75
|
+
const importPaths = {
|
|
76
|
+
...DEFAULT_OPTIONS.importPaths,
|
|
77
|
+
...defined.importPaths
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
...DEFAULT_OPTIONS,
|
|
81
|
+
...defined,
|
|
82
|
+
importPaths,
|
|
83
|
+
naming
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { generate };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=generate.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.mjs","names":[],"sources":["../../src/core/generate.ts"],"sourcesContent":["import path from \"node:path\";\nimport {\n\tcreateDiagnostic,\n\tDiagnosticCode,\n\tformatDiagnostic,\n\tJson2CodeError,\n\tpartitionDiagnostics,\n\ttype Diagnostic,\n} from \"../diagnostics/errors.js\";\nimport { createLogger } from \"../diagnostics/logger.js\";\nimport { resolveTarget } from \"../targets/index.js\";\nimport { discover } from \"./discovery/index.js\";\nimport type { SourceDoc } from \"./ir/types.js\";\nimport { Linker } from \"./linker/index.js\";\nimport type {\n\tGenerateConfig,\n\tGenerateOptions,\n\tPrintedFile,\n\tResolvedOptions,\n\tTargetSpec,\n} from \"./types.js\";\nimport { DEFAULT_OPTIONS, NAMING_STYLES } from \"./types.js\";\nimport type { AbstractWalker, WalkerContext } from \"./walker/abstract.js\";\nimport { JsonSchemaWalker } from \"./walker/json-schema.js\";\nimport { writeFiles } from \"./writer/index.js\";\n\nexport interface GenerateResult {\n\treadonly written: readonly string[];\n\treadonly deleted: readonly string[];\n\treadonly diagnostics: readonly Diagnostic[];\n}\n\nconst defaultWalker = new JsonSchemaWalker();\n\nexport async function generate(config: GenerateConfig): Promise<GenerateResult> {\n\tconst logger = createLogger(config.verbose ?? false);\n\tconst cwd = process.cwd();\n\tconst output = path.resolve(cwd, config.output);\n\tconst options = mergeOptions(config.options);\n\n\tconst allDiagnostics: Diagnostic[] = [];\n\tconst allFiles: PrintedFile[] = [];\n\n\tconst { files, rootDir } = await discover(config.input, cwd, logger);\n\tif (files.length === 0) {\n\t\tthrow new Json2CodeError([\n\t\t\tcreateDiagnostic(\n\t\t\t\tDiagnosticCode.FileNotFound,\n\t\t\t\t\"error\",\n\t\t\t\t\"No JSON Schema files matched the input.\",\n\t\t\t),\n\t\t]);\n\t}\n\tconst resolved: ResolvedOptions = { ...options, output, rootDir };\n\n\tconst walkCache = new Map<AbstractWalker, Promise<SourceDoc[]>>();\n\tfor (const spec of normalizeTargets(config.targets)) {\n\t\tconst def = resolveTarget(spec);\n\t\tconst walker = def.walker ?? defaultWalker;\n\t\t// share one walk per walker instance: multi-target runs don't duplicate diagnostics\n\t\tlet docsPromise = walkCache.get(walker);\n\t\tif (!docsPromise) {\n\t\t\tconst wctx: WalkerContext = { logger, options: resolved, diagnostics: allDiagnostics };\n\t\t\tdocsPromise = walker.walk(files, wctx);\n\t\t\twalkCache.set(walker, docsPromise);\n\t\t}\n\t\tconst docs = await docsPromise;\n\n\t\tconst mapOutput =\n\t\t\tdef.mapOutput ?? ((docId: string) => Linker.defaultMapOutput(def.name, docId));\n\t\tconst linker = new Linker({\n\t\t\toptions: resolved,\n\t\t\ttargetName: def.name,\n\t\t\tmapOutput,\n\t\t\tdiagnostics: allDiagnostics,\n\t\t});\n\t\tconst pctx = linker.createPrintContext(logger, allDiagnostics);\n\t\tconst printed = await def.printer.print(docs, pctx);\n\t\tfor (const f of printed) {\n\t\t\tallFiles.push(f);\n\t\t}\n\t}\n\n\tconst { errors, warnings } = partitionDiagnostics(allDiagnostics);\n\tfor (const w of warnings) {\n\t\tlogger.warn(formatDiagnostic(w));\n\t}\n\tif (errors.length > 0) {\n\t\tfor (const e of errors) {\n\t\t\tlogger.error(formatDiagnostic(e));\n\t\t}\n\t\tthrow new Json2CodeError(errors);\n\t}\n\n\tconst result = await writeFiles(allFiles, {\n\t\toutput,\n\t\tdryRun: config.dryRun ?? false,\n\t\tclean: config.clean ?? false,\n\t\tlogger,\n\t\theader: options.header,\n\t});\n\treturn { ...result, diagnostics: allDiagnostics };\n}\n\nfunction normalizeTargets(targets: TargetSpec[]): TargetSpec[] {\n\treturn targets.length > 0 ? targets : [\"typescript\"];\n}\n\nfunction mergeOptions(opts?: Partial<GenerateOptions>): GenerateOptions {\n\tconst naming = opts?.naming ?? DEFAULT_OPTIONS.naming;\n\tif (!NAMING_STYLES.includes(naming)) {\n\t\tthrow new TypeError(\n\t\t\t`Invalid naming style: ${JSON.stringify(naming)} (expected one of ${NAMING_STYLES.join(\", \")})`,\n\t\t);\n\t}\n\t// drop explicitly-undefined keys so they don't override defaults when spreading\n\tconst defined = Object.fromEntries(\n\t\tObject.entries(opts ?? {}).filter(([, v]) => v !== undefined),\n\t) as Partial<GenerateOptions>;\n\t// importPaths is nested: merge per-target keys so a partial object keeps defaults\n\tconst importPaths = { ...DEFAULT_OPTIONS.importPaths, ...defined.importPaths };\n\treturn {\n\t\t...DEFAULT_OPTIONS,\n\t\t...defined,\n\t\timportPaths,\n\t\tnaming,\n\t};\n}\n"],"mappings":";;;;;;;;;;AAgCA,MAAM,gBAAgB,IAAI,iBAAiB;AAE3C,eAAsB,SAAS,QAAiD;CAC/E,MAAM,SAAS,aAAa,OAAO,WAAW,KAAK;CACnD,MAAM,MAAM,QAAQ,IAAI;CACxB,MAAM,SAAS,KAAK,QAAQ,KAAK,OAAO,MAAM;CAC9C,MAAM,UAAU,aAAa,OAAO,OAAO;CAE3C,MAAM,iBAA+B,CAAC;CACtC,MAAM,WAA0B,CAAC;CAEjC,MAAM,EAAE,OAAO,YAAY,MAAM,SAAS,OAAO,OAAO,KAAK,MAAM;CACnE,IAAI,MAAM,WAAW,GACpB,MAAM,IAAI,eAAe,CACxB,iBACC,eAAe,cACf,SACA,yCACD,CACD,CAAC;CAEF,MAAM,WAA4B;EAAE,GAAG;EAAS;EAAQ;CAAQ;CAEhE,MAAM,4BAAY,IAAI,IAA0C;CAChE,KAAK,MAAM,QAAQ,iBAAiB,OAAO,OAAO,GAAG;EACpD,MAAM,MAAM,cAAc,IAAI;EAC9B,MAAM,SAAS,IAAI,UAAU;EAE7B,IAAI,cAAc,UAAU,IAAI,MAAM;EACtC,IAAI,CAAC,aAAa;GACjB,MAAM,OAAsB;IAAE;IAAQ,SAAS;IAAU,aAAa;GAAe;GACrF,cAAc,OAAO,KAAK,OAAO,IAAI;GACrC,UAAU,IAAI,QAAQ,WAAW;EAClC;EACA,MAAM,OAAO,MAAM;EAEnB,MAAM,YACL,IAAI,eAAe,UAAkB,OAAO,iBAAiB,IAAI,MAAM,KAAK;EAO7E,MAAM,OAAO,IANM,OAAO;GACzB,SAAS;GACT,YAAY,IAAI;GAChB;GACA,aAAa;EACd,CACkB,CAAC,CAAC,mBAAmB,QAAQ,cAAc;EAC7D,MAAM,UAAU,MAAM,IAAI,QAAQ,MAAM,MAAM,IAAI;EAClD,KAAK,MAAM,KAAK,SACf,SAAS,KAAK,CAAC;CAEjB;CAEA,MAAM,EAAE,QAAQ,aAAa,qBAAqB,cAAc;CAChE,KAAK,MAAM,KAAK,UACf,OAAO,KAAK,iBAAiB,CAAC,CAAC;CAEhC,IAAI,OAAO,SAAS,GAAG;EACtB,KAAK,MAAM,KAAK,QACf,OAAO,MAAM,iBAAiB,CAAC,CAAC;EAEjC,MAAM,IAAI,eAAe,MAAM;CAChC;CASA,OAAO;EAAE,GAAG,MAPS,WAAW,UAAU;GACzC;GACA,QAAQ,OAAO,UAAU;GACzB,OAAO,OAAO,SAAS;GACvB;GACA,QAAQ,QAAQ;EACjB,CAAC;EACmB,aAAa;CAAe;AACjD;AAEA,SAAS,iBAAiB,SAAqC;CAC9D,OAAO,QAAQ,SAAS,IAAI,UAAU,CAAC,YAAY;AACpD;AAEA,SAAS,aAAa,MAAkD;CACvE,MAAM,SAAS,MAAM,UAAU,gBAAgB;CAC/C,IAAI,CAAC,cAAc,SAAS,MAAM,GACjC,MAAM,IAAI,UACT,yBAAyB,KAAK,UAAU,MAAM,EAAE,oBAAoB,cAAc,KAAK,IAAI,EAAE,EAC9F;CAGD,MAAM,UAAU,OAAO,YACtB,OAAO,QAAQ,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,OAAO,MAAM,KAAA,CAAS,CAC7D;CAEA,MAAM,cAAc;EAAE,GAAG,gBAAgB;EAAa,GAAG,QAAQ;CAAY;CAC7E,OAAO;EACN,GAAG;EACH,GAAG;EACH;EACA;CACD;AACD"}
|