@toolproof-core/schema 1.0.3 → 1.0.5
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/generated/normalized/Genesis.json +16 -265
- package/dist/generated/resources/Genesis.json +18 -305
- package/dist/generated/schemas/Genesis.json +14 -152
- package/dist/generated/schemas/standalone/Goal.json +0 -33
- package/dist/generated/schemas/standalone/Job.json +0 -42
- package/dist/generated/schemas/standalone/RawStrategy.json +14 -52
- package/dist/generated/schemas/standalone/ResourceType.json +0 -34
- package/dist/generated/schemas/standalone/RunnableStrategy.json +14 -57
- package/dist/generated/schemas/standalone/StrategyRun.json +14 -71
- package/dist/generated/types/standalone/Resource_Genesis.d.ts +1 -1
- package/dist/generated/types/standalone/Resource_Job.d.ts +1 -1
- package/dist/generated/types/standalone/Resource_RawStrategy.d.ts +1 -1
- package/dist/generated/types/standalone/Resource_ResourceType.d.ts +1 -1
- package/dist/generated/types/standalone/Resource_RunnableStrategy.d.ts +1 -1
- package/dist/generated/types/types.d.ts +119 -1126
- package/dist/index.d.ts +1 -4
- package/dist/index.js +0 -2
- package/dist/scripts/_lib/config.d.ts +6 -6
- package/dist/scripts/_lib/config.js +10 -12
- package/dist/scripts/extractSchemasFromResourceTypeShells.js +109 -103
- package/dist/scripts/generateDependencies.js +15 -14
- package/dist/scripts/generateSchemaShims.js +77 -85
- package/dist/scripts/generateStandaloneSchema.js +47 -37
- package/dist/scripts/generateStandaloneType.js +85 -79
- package/dist/scripts/generateTypes.js +350 -470
- package/dist/scripts/normalizeAnchorsToPointers.d.ts +1 -1
- package/dist/scripts/normalizeAnchorsToPointers.js +61 -33
- package/dist/scripts/wrapResourceTypesWithResourceShells.js +14 -16
- package/package.json +7 -8
- package/src/Genesis.json +1837 -1999
- package/src/generated/{dependencyMap.json → dependencies/dependencyMap.json} +9 -19
- package/src/generated/normalized/Genesis.json +16 -265
- package/src/generated/resources/Genesis.json +18 -305
- package/src/generated/schemas/Genesis.json +14 -152
- package/src/generated/schemas/standalone/Goal.json +0 -33
- package/src/generated/schemas/standalone/Job.json +0 -42
- package/src/generated/schemas/standalone/RawStrategy.json +14 -52
- package/src/generated/schemas/standalone/ResourceType.json +0 -34
- package/src/generated/schemas/standalone/RunnableStrategy.json +14 -57
- package/src/generated/schemas/standalone/StrategyRun.json +14 -71
- package/src/generated/types/standalone/Resource_Genesis.d.ts +1 -1
- package/src/generated/types/standalone/Resource_Job.d.ts +1 -1
- package/src/generated/types/standalone/Resource_RawStrategy.d.ts +1 -1
- package/src/generated/types/standalone/Resource_ResourceType.d.ts +1 -1
- package/src/generated/types/standalone/Resource_RunnableStrategy.d.ts +1 -1
- package/src/generated/types/types.d.ts +119 -1126
- package/src/index.ts +66 -93
- package/src/scripts/_lib/config.ts +203 -205
- package/src/scripts/extractSchemasFromResourceTypeShells.ts +261 -218
- package/src/scripts/generateDependencies.ts +121 -120
- package/src/scripts/generateSchemaShims.ts +127 -135
- package/src/scripts/generateStandaloneSchema.ts +185 -175
- package/src/scripts/generateStandaloneType.ts +127 -119
- package/src/scripts/generateTypes.ts +532 -615
- package/src/scripts/normalizeAnchorsToPointers.ts +141 -123
- package/src/scripts/wrapResourceTypesWithResourceShells.ts +82 -84
- package/dist/generated/dependencyMap.json +0 -292
|
@@ -1,503 +1,385 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { compile } from 'json-schema-to-typescript';
|
|
4
4
|
import { getConfig } from './_lib/config.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// This supports location-independent $id values where folder segments were removed
|
|
13
|
-
function buildSchemaIndex(root) {
|
|
14
|
-
const index = new Map();
|
|
15
|
-
const stack = [root];
|
|
16
|
-
while (stack.length) {
|
|
17
|
-
const current = stack.pop();
|
|
18
|
-
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
19
|
-
for (const entry of entries) {
|
|
20
|
-
const full = path.join(current, entry.name);
|
|
21
|
-
if (entry.isDirectory()) {
|
|
22
|
-
// Skip dist or types output folders if present inside schemas (defensive)
|
|
23
|
-
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
24
|
-
continue;
|
|
25
|
-
stack.push(full);
|
|
26
|
-
}
|
|
27
|
-
else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
28
|
-
// Ignore temp files that can be left behind after crashes
|
|
29
|
-
if (entry.name === '.combined-schema.json')
|
|
30
|
-
continue;
|
|
31
|
-
if (entry.name.startsWith('.normalized.'))
|
|
32
|
-
continue;
|
|
33
|
-
if (entry.name.startsWith('.'))
|
|
34
|
-
continue;
|
|
35
|
-
const baseName = entry.name; // keep extension for direct mapping
|
|
36
|
-
if (index.has(baseName)) {
|
|
37
|
-
// Hard fail on collisions so they can be fixed explicitly
|
|
38
|
-
const existing = index.get(baseName);
|
|
39
|
-
throw new Error(`Schema basename collision detected for "${baseName}"\n` +
|
|
40
|
-
`First: ${existing}\n` +
|
|
41
|
-
`Second: ${full}\n` +
|
|
42
|
-
`Please rename one of the schemas to ensure unique basenames.`);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
index.set(baseName, full);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
5
|
+
// PURE: Format a JSON path for error messages.
|
|
6
|
+
function formatPath(pathSegments) {
|
|
7
|
+
let out = '';
|
|
8
|
+
for (const seg of pathSegments) {
|
|
9
|
+
if (typeof seg === 'number') {
|
|
10
|
+
out += `[${seg}]`;
|
|
11
|
+
continue;
|
|
48
12
|
}
|
|
13
|
+
out = out ? `${out}.${seg}` : seg;
|
|
49
14
|
}
|
|
50
|
-
return
|
|
15
|
+
return out || '<root>';
|
|
51
16
|
}
|
|
52
|
-
//
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
59
|
-
for (const entry of entries) {
|
|
60
|
-
const full = path.join(current, entry.name);
|
|
61
|
-
if (entry.isDirectory()) {
|
|
62
|
-
if (entry.name === 'documentation' || entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
63
|
-
continue;
|
|
64
|
-
stack.push(full);
|
|
65
|
-
}
|
|
66
|
-
else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
67
|
-
// Ignore temp files that can be left behind after crashes
|
|
68
|
-
if (entry.name === '.combined-schema.json')
|
|
69
|
-
continue;
|
|
70
|
-
if (entry.name.startsWith('.normalized.'))
|
|
71
|
-
continue;
|
|
72
|
-
if (entry.name.startsWith('.'))
|
|
73
|
-
continue;
|
|
74
|
-
// produce path relative to root with posix separators
|
|
75
|
-
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
76
|
-
files.push(rel);
|
|
77
|
-
}
|
|
17
|
+
// PURE: Validate schema array-keyword shapes; returns a list of issues (no mutation, no I/O).
|
|
18
|
+
function validateSchemaArrayKeywords(node, parentKey, pathSegments = []) {
|
|
19
|
+
if (Array.isArray(node)) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
for (let i = 0; i < node.length; i++) {
|
|
22
|
+
issues.push(...validateSchemaArrayKeywords(node[i], parentKey, pathSegments.concat([i])));
|
|
78
23
|
}
|
|
24
|
+
return issues;
|
|
79
25
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
return config.isSchemaUrl(url);
|
|
101
|
-
},
|
|
102
|
-
read: (file) => {
|
|
103
|
-
let url = (typeof file.url === 'string' ? file.url : file.url?.href) || '';
|
|
104
|
-
// Strip accidental wrapping quotes
|
|
105
|
-
if ((url.startsWith("'") && url.endsWith("'")) || (url.startsWith('"') && url.endsWith('"'))) {
|
|
106
|
-
url = url.slice(1, -1);
|
|
26
|
+
if (!node || typeof node !== 'object')
|
|
27
|
+
return [];
|
|
28
|
+
// IMPORTANT:
|
|
29
|
+
// In meta-schemas we often have `properties: { required: { ...schema... } }`.
|
|
30
|
+
// Here, the key "required" is a *property name*, not the JSON-Schema keyword.
|
|
31
|
+
// So when we are iterating a property-name map, do not treat keys as schema keywords.
|
|
32
|
+
const isPropertyNameMap = parentKey === 'properties' ||
|
|
33
|
+
parentKey === 'patternProperties' ||
|
|
34
|
+
parentKey === '$defs' ||
|
|
35
|
+
parentKey === 'dependentSchemas' ||
|
|
36
|
+
parentKey === 'dependentRequired';
|
|
37
|
+
const arrayKeys = ['anyOf', 'allOf', 'oneOf', 'required', 'enum'];
|
|
38
|
+
const issues = [];
|
|
39
|
+
for (const [k, v] of Object.entries(node)) {
|
|
40
|
+
// Keyword sanity checks for schema objects (regardless of where they appear).
|
|
41
|
+
// These reduce the chance of feeding obviously malformed shapes into the generator.
|
|
42
|
+
if (k === 'properties' || k === 'patternProperties' || k === '$defs' || k === 'dependentSchemas') {
|
|
43
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) {
|
|
44
|
+
issues.push(`${formatPath(pathSegments.concat([k]))}: expected \`${k}\` to be an object map, got ${v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v}`);
|
|
107
45
|
}
|
|
108
|
-
// Strip hash part (anchors) for path resolution
|
|
109
|
-
const noHash = url.split('#')[0];
|
|
110
|
-
// Extract schema name using config
|
|
111
|
-
const schemaName = config.extractSchemaName(noHash);
|
|
112
|
-
const fileName = schemaName.endsWith('.json') ? schemaName : `${schemaName}.json`;
|
|
113
|
-
// Resolve by basename only (location-independent IDs)
|
|
114
|
-
const indexed = schemaIndex.get(fileName);
|
|
115
|
-
if (indexed) {
|
|
116
|
-
return fs.readFileSync(indexed, 'utf8');
|
|
117
|
-
}
|
|
118
|
-
throw new Error(`Toolproof resolver: could not locate schema for URL "${url}". ` +
|
|
119
|
-
`Tried basename "${fileName}" in schema index. Ensure unique basenames and correct $id.`);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
// Files to include in the combined schema (auto-discovered, excludes documentation)
|
|
123
|
-
const toCompile = listAllSchemaFiles(inputDir);
|
|
124
|
-
// Collect schema refs and defs; we'll compile files individually to avoid combined-schema traversal issues.
|
|
125
|
-
const definitions = {};
|
|
126
|
-
const includedNames = [];
|
|
127
|
-
for (const fileName of toCompile) {
|
|
128
|
-
const p = path.join(inputDir, fileName);
|
|
129
|
-
if (!fs.existsSync(p)) {
|
|
130
|
-
console.warn(`Schema file missing, skipping: ${p}`);
|
|
131
|
-
continue;
|
|
132
46
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// This helps json-schema-to-typescript dedupe declarations instead of emitting FooJson and FooJson1
|
|
137
|
-
let refValue = `./${fileName}`;
|
|
138
|
-
try {
|
|
139
|
-
const raw = fs.readFileSync(p, 'utf8');
|
|
140
|
-
const parsed = JSON.parse(raw);
|
|
141
|
-
if (parsed && typeof parsed.$id === 'string' && parsed.$id.trim()) {
|
|
142
|
-
// Sanitize IDs that may have been emitted with surrounding quotes
|
|
143
|
-
// e.g. quotes around schema URL
|
|
144
|
-
let cleaned = parsed.$id.trim();
|
|
145
|
-
if ((cleaned.startsWith("'") && cleaned.endsWith("'")) || (cleaned.startsWith('"') && cleaned.endsWith('"'))) {
|
|
146
|
-
cleaned = cleaned.slice(1, -1);
|
|
147
|
-
}
|
|
148
|
-
refValue = cleaned;
|
|
149
|
-
idToCanonical[refValue] = base + 'Json';
|
|
47
|
+
if (k === 'dependentRequired') {
|
|
48
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) {
|
|
49
|
+
issues.push(`${formatPath(pathSegments.concat([k]))}: expected \`dependentRequired\` to be an object map, got ${v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v}`);
|
|
150
50
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
let entryName = defName;
|
|
157
|
-
if (definitions[entryName]) {
|
|
158
|
-
entryName = `${base}_${defName}`;
|
|
51
|
+
else {
|
|
52
|
+
for (const [depKey, depVal] of Object.entries(v)) {
|
|
53
|
+
if (!Array.isArray(depVal)) {
|
|
54
|
+
issues.push(`${formatPath(pathSegments.concat([k, depKey]))}: expected dependentRequired entries to be string arrays, got ${depVal === null ? 'null' : typeof depVal}`);
|
|
55
|
+
continue;
|
|
159
56
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
57
|
+
for (let i = 0; i < depVal.length; i++) {
|
|
58
|
+
if (typeof depVal[i] !== 'string') {
|
|
59
|
+
issues.push(`${formatPath(pathSegments.concat([k, depKey, i]))}: expected dependentRequired entries to be strings, got ${depVal[i] === null ? 'null' : typeof depVal[i]}`);
|
|
60
|
+
}
|
|
163
61
|
}
|
|
164
62
|
}
|
|
165
63
|
}
|
|
166
64
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const schemaPath = path.join(inputDir, fileName);
|
|
183
|
-
if (!fs.existsSync(schemaPath))
|
|
184
|
-
continue;
|
|
185
|
-
// Load and defensively normalize array-expected keywords to avoid generator crashes
|
|
186
|
-
let toCompilePath = schemaPath;
|
|
187
|
-
try {
|
|
188
|
-
const rawSchema = fs.readFileSync(schemaPath, 'utf8');
|
|
189
|
-
const parsedSchema = JSON.parse(rawSchema);
|
|
190
|
-
function normalizeArrays(node, parentKey) {
|
|
191
|
-
if (Array.isArray(node)) {
|
|
192
|
-
for (let i = 0; i < node.length; i++)
|
|
193
|
-
normalizeArrays(node[i], parentKey);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (!node || typeof node !== 'object')
|
|
197
|
-
return;
|
|
198
|
-
// IMPORTANT:
|
|
199
|
-
// In meta-schemas we often have `properties: { required: { ...schema... } }`.
|
|
200
|
-
// Here, the key "required" is a *property name*, not the JSON-Schema keyword
|
|
201
|
-
// "required". Blindly normalizing would wrap that schema object into an array,
|
|
202
|
-
// causing json-schema-to-typescript to emit the schema-shape instead of `string[]`.
|
|
203
|
-
//
|
|
204
|
-
// So: only coerce array-keywords when we're not inside a map of property names.
|
|
205
|
-
const isPropertyNameMap = parentKey === 'properties'
|
|
206
|
-
|| parentKey === 'patternProperties'
|
|
207
|
-
|| parentKey === '$defs'
|
|
208
|
-
|| parentKey === 'definitions'
|
|
209
|
-
|| parentKey === 'dependentSchemas';
|
|
210
|
-
if (!isPropertyNameMap) {
|
|
211
|
-
const arrayKeys = ['anyOf', 'allOf', 'oneOf', 'required', 'enum'];
|
|
212
|
-
for (const k of arrayKeys) {
|
|
213
|
-
if (k in node) {
|
|
214
|
-
const v = node[k];
|
|
215
|
-
if (!Array.isArray(v))
|
|
216
|
-
node[k] = [v];
|
|
217
|
-
}
|
|
65
|
+
if (!isPropertyNameMap && arrayKeys.includes(k) && Object.prototype.hasOwnProperty.call(node, k)) {
|
|
66
|
+
if (!Array.isArray(v)) {
|
|
67
|
+
issues.push(`${formatPath(pathSegments.concat([k]))}: expected \`${k}\` to be an array, got ${v === null ? 'null' : typeof v}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if ((k === 'anyOf' || k === 'allOf' || k === 'oneOf') && v.length === 0) {
|
|
71
|
+
issues.push(`${formatPath(pathSegments.concat([k]))}: expected \`${k}\` to be a non-empty array`);
|
|
72
|
+
}
|
|
73
|
+
if (k === 'anyOf' || k === 'allOf' || k === 'oneOf') {
|
|
74
|
+
for (let i = 0; i < v.length; i++) {
|
|
75
|
+
const item = v[i];
|
|
76
|
+
const ok = typeof item === 'boolean' ||
|
|
77
|
+
(item !== null && typeof item === 'object' && !Array.isArray(item));
|
|
78
|
+
if (!ok) {
|
|
79
|
+
issues.push(`${formatPath(pathSegments.concat([k, i]))}: expected a schema (object or boolean), got ${item === null ? 'null' : Array.isArray(item) ? 'array' : typeof item}`);
|
|
218
80
|
}
|
|
219
81
|
}
|
|
220
|
-
for (const [k, v] of Object.entries(node))
|
|
221
|
-
normalizeArrays(v, k);
|
|
222
82
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// move those sibling object keywords into an extra `allOf` item.
|
|
234
|
-
function normalizeAllOfSiblingObjectKeywords(node) {
|
|
235
|
-
if (Array.isArray(node)) {
|
|
236
|
-
for (const item of node)
|
|
237
|
-
normalizeAllOfSiblingObjectKeywords(item);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (!node || typeof node !== 'object')
|
|
241
|
-
return;
|
|
242
|
-
const hasAllOf = Array.isArray(node.allOf) && node.allOf.length > 0;
|
|
243
|
-
const looksLikeObjectSchema = node.type === 'object' ||
|
|
244
|
-
node.properties !== undefined ||
|
|
245
|
-
node.required !== undefined ||
|
|
246
|
-
node.unevaluatedProperties !== undefined ||
|
|
247
|
-
node.additionalProperties !== undefined;
|
|
248
|
-
if (hasAllOf && looksLikeObjectSchema) {
|
|
249
|
-
const siblingKeys = [
|
|
250
|
-
'properties',
|
|
251
|
-
'required',
|
|
252
|
-
'additionalProperties',
|
|
253
|
-
'unevaluatedProperties',
|
|
254
|
-
'propertyNames',
|
|
255
|
-
'patternProperties',
|
|
256
|
-
'dependentRequired',
|
|
257
|
-
'dependentSchemas',
|
|
258
|
-
'minProperties',
|
|
259
|
-
'maxProperties'
|
|
260
|
-
];
|
|
261
|
-
const hasSiblingObjectKeywords = siblingKeys.some((k) => k in node);
|
|
262
|
-
if (hasSiblingObjectKeywords) {
|
|
263
|
-
const overlay = {};
|
|
264
|
-
if (node.type === 'object')
|
|
265
|
-
overlay.type = 'object';
|
|
266
|
-
for (const k of siblingKeys) {
|
|
267
|
-
if (k in node) {
|
|
268
|
-
overlay[k] = node[k];
|
|
269
|
-
delete node[k];
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// Prepend so it participates in the intersection early.
|
|
273
|
-
node.allOf = [overlay, ...node.allOf];
|
|
83
|
+
if (k === 'required') {
|
|
84
|
+
// `required` must be string[] when used as a JSON-Schema keyword.
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
for (let i = 0; i < v.length; i++) {
|
|
87
|
+
if (typeof v[i] !== 'string') {
|
|
88
|
+
issues.push(`${formatPath(pathSegments.concat([k, i]))}: expected \`required\` entries to be strings, got ${v[i] === null ? 'null' : typeof v[i]}`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (seen.has(v[i])) {
|
|
92
|
+
issues.push(`${formatPath(pathSegments.concat([k]))}: duplicate required entry \`${v[i]}\``);
|
|
274
93
|
}
|
|
94
|
+
seen.add(v[i]);
|
|
275
95
|
}
|
|
276
|
-
for (const v of Object.values(node))
|
|
277
|
-
normalizeAllOfSiblingObjectKeywords(v);
|
|
278
96
|
}
|
|
279
|
-
// Normalize expected arrays to prevent traversal crashes
|
|
280
|
-
normalizeArrays(parsedSchema);
|
|
281
|
-
// Normalize `allOf` + sibling object keywords so the TS generator doesn't drop them.
|
|
282
|
-
normalizeAllOfSiblingObjectKeywords(parsedSchema);
|
|
283
|
-
const tmpPath = path.join(inputDir, `.normalized.${path.basename(fileName)}`);
|
|
284
|
-
fs.writeFileSync(tmpPath, JSON.stringify(parsedSchema, null, 2), 'utf8');
|
|
285
|
-
toCompilePath = tmpPath;
|
|
286
97
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
98
|
+
}
|
|
99
|
+
issues.push(...validateSchemaArrayKeywords(v, k, pathSegments.concat([k])));
|
|
100
|
+
}
|
|
101
|
+
return issues;
|
|
102
|
+
}
|
|
103
|
+
// PURE: Move sibling object keywords into `allOf` overlays (returns a new tree; does not mutate inputs).
|
|
104
|
+
function normalizeAllOfSiblingObjectKeywords(node) {
|
|
105
|
+
if (Array.isArray(node)) {
|
|
106
|
+
let changed = false;
|
|
107
|
+
const out = node.map((item) => {
|
|
108
|
+
const next = normalizeAllOfSiblingObjectKeywords(item);
|
|
109
|
+
if (next !== item)
|
|
110
|
+
changed = true;
|
|
111
|
+
return next;
|
|
112
|
+
});
|
|
113
|
+
return changed ? out : node;
|
|
114
|
+
}
|
|
115
|
+
if (!node || typeof node !== 'object')
|
|
116
|
+
return node;
|
|
117
|
+
const hasAllOf = Array.isArray(node.allOf) && node.allOf.length > 0;
|
|
118
|
+
const looksLikeObjectSchema = node.type === 'object' ||
|
|
119
|
+
node.properties !== undefined ||
|
|
120
|
+
node.required !== undefined ||
|
|
121
|
+
node.unevaluatedProperties !== undefined ||
|
|
122
|
+
node.additionalProperties !== undefined;
|
|
123
|
+
const siblingKeys = [
|
|
124
|
+
'properties',
|
|
125
|
+
'required',
|
|
126
|
+
'additionalProperties',
|
|
127
|
+
'unevaluatedProperties',
|
|
128
|
+
'propertyNames',
|
|
129
|
+
'patternProperties',
|
|
130
|
+
'dependentRequired',
|
|
131
|
+
'dependentSchemas',
|
|
132
|
+
'minProperties',
|
|
133
|
+
'maxProperties'
|
|
134
|
+
];
|
|
135
|
+
let localNode = node;
|
|
136
|
+
if (hasAllOf && looksLikeObjectSchema) {
|
|
137
|
+
const hasSiblingObjectKeywords = siblingKeys.some((k) => k in localNode);
|
|
138
|
+
if (hasSiblingObjectKeywords) {
|
|
139
|
+
const overlay = {};
|
|
140
|
+
if (localNode.type === 'object')
|
|
141
|
+
overlay.type = 'object';
|
|
142
|
+
const nextNode = { ...localNode };
|
|
143
|
+
for (const k of siblingKeys) {
|
|
144
|
+
if (k in nextNode) {
|
|
145
|
+
overlay[k] = nextNode[k];
|
|
146
|
+
delete nextNode[k];
|
|
311
147
|
}
|
|
312
|
-
catch { }
|
|
313
148
|
}
|
|
149
|
+
nextNode.allOf = [overlay, ...nextNode.allOf];
|
|
150
|
+
localNode = nextNode;
|
|
314
151
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// strips standalone `[k: string]: unknown;` lines.
|
|
327
|
-
ts = ts.replace(/^(\s*)(\$defs\??:\s*)\{\s*\r?\n\s*\};/gm, (_m, indent, head) => `${indent}${head}Record<string, unknown>;`);
|
|
328
|
-
// `properties?: { }` or `properties: { }` -> map type
|
|
329
|
-
ts = ts.replace(/^(\s*)(properties\??:\s*)\{\s*\r?\n\s*\};/gm, (_m, indent, head) => `${indent}${head}Record<string, unknown>;`);
|
|
330
|
-
// `allOf?: { }[];` (and similar for anyOf/oneOf) -> array of schema-ish objects
|
|
331
|
-
ts = ts.replace(/^(\s*)((?:allOf|anyOf|oneOf)\??:\s*)\{\s*\r?\n\s*\}\[\];/gm, (_m, indent, head) => `${indent}${head}Array<{[k: string]: unknown}>;`);
|
|
332
|
-
// Older broken shapes from earlier normalization (keep for backwards compatibility)
|
|
333
|
-
ts = ts.replace(/((?:allOf|anyOf|oneOf)\??:\s*)\[\{type:\s*"array";\s*items:\s*\{type:\s*"object"\}\}\];/g, '$1Array<{[k: string]: unknown}>;');
|
|
334
|
-
ts = ts.replace(/required\?:\s*\[\{type:\s*"array";\s*items:\s*\{type:\s*"string"\};\s*uniqueItems:\s*true\}\];/g, 'required?: string[];');
|
|
335
|
-
// Similarly fix IdentityProp/MeritProp which have the same issue
|
|
336
|
-
ts = ts.replace(/^(export interface IdentityProp[\s\S]*?)(required:\s*\[\{type:\s*"array";\s*contains:\s*\{const:\s*"identity"\};\s*items:\s*\{type:\s*"string"\};\s*uniqueItems:\s*true\}\];)/gm, '$1required?: string[];');
|
|
337
|
-
ts = ts.replace(/^(export interface MeritProp[\s\S]*?)(required:\s*\[\{type:\s*"array";\s*items:\s*\{type:\s*"string"\};\s*uniqueItems:\s*true\}\];)/gm, '$1required?: string[];');
|
|
338
|
-
// Prune verbose type/interface names produced from absolute $id URLs.
|
|
339
|
-
// Deterministic pruning based on original $id -> baseName map
|
|
340
|
-
// This avoids heuristic truncation that dropped prefixes.
|
|
341
|
-
function idToGeneratedIdentifier(id) {
|
|
342
|
-
// json-schema-to-typescript seems to create a PascalCase of the URL with protocol prefix
|
|
343
|
-
// Simplified reconstruction: 'https://' => 'Https', then capitalize path & host segments
|
|
344
|
-
const noProto = id.replace(/^https?:\/\//i, '');
|
|
345
|
-
const tokens = noProto
|
|
346
|
-
.split(/[\/#?.=&_-]+/)
|
|
347
|
-
.filter(Boolean)
|
|
348
|
-
.map((t) => t.replace(/[^A-Za-z0-9]/g, ''))
|
|
349
|
-
.filter(Boolean)
|
|
350
|
-
.map((t) => t.charAt(0).toUpperCase() + t.slice(1));
|
|
351
|
-
return 'Https' + tokens.join('') + 'Json';
|
|
152
|
+
}
|
|
153
|
+
let changed = localNode !== node;
|
|
154
|
+
const out = localNode === node ? {} : { ...localNode };
|
|
155
|
+
for (const [k, v] of Object.entries(localNode)) {
|
|
156
|
+
const next = normalizeAllOfSiblingObjectKeywords(v);
|
|
157
|
+
if (next !== v) {
|
|
158
|
+
if (!changed) {
|
|
159
|
+
changed = true;
|
|
160
|
+
Object.assign(out, localNode);
|
|
161
|
+
}
|
|
162
|
+
out[k] = next;
|
|
352
163
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const longName = idToGeneratedIdentifier(id);
|
|
356
|
-
if (longName === canonical)
|
|
357
|
-
continue; // already minimal
|
|
358
|
-
const re = new RegExp(`\\b${longName}\\b`, 'g');
|
|
359
|
-
ts = ts.replace(re, canonical);
|
|
164
|
+
else if (changed) {
|
|
165
|
+
out[k] = v;
|
|
360
166
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (!seen2.has(name)) {
|
|
377
|
-
seen2.add(name);
|
|
378
|
-
kept.push(' | ' + name);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
else if (trimmed.length) {
|
|
382
|
-
kept.push(line);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return `export type ${typeName} =\n` + kept.join('\n') + ';';
|
|
386
|
-
});
|
|
387
|
-
// If nothing was emitted, compile Genesis.json as a final fallback.
|
|
388
|
-
if (!ts || !ts.trim()) {
|
|
389
|
-
const primary = path.join(inputDir, 'Genesis.json');
|
|
390
|
-
if (fs.existsSync(primary)) {
|
|
391
|
-
ts = await compileFromFile(primary, {
|
|
392
|
-
bannerComment: '',
|
|
393
|
-
declareExternallyReferenced: true,
|
|
394
|
-
unreachableDefinitions: true,
|
|
395
|
-
$refOptions: {
|
|
396
|
-
resolve: {
|
|
397
|
-
file: { order: 2 },
|
|
398
|
-
http: false,
|
|
399
|
-
https: false,
|
|
400
|
-
toolproof: toolproofResolver
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
}
|
|
167
|
+
}
|
|
168
|
+
return changed ? out : node;
|
|
169
|
+
}
|
|
170
|
+
// PURE: Clone + apply all generator-normalizations.
|
|
171
|
+
function normalizeSchemaForGenerator(schema) {
|
|
172
|
+
return normalizeAllOfSiblingObjectKeywords(schema);
|
|
173
|
+
}
|
|
174
|
+
// PURE: Convert a regex-ish pattern into a TS template literal when recognizable.
|
|
175
|
+
function deriveTemplateFromPattern(pattern) {
|
|
176
|
+
// Common (and currently canonical for identities): ^PREFIX-.+$ => `PREFIX-${string}`
|
|
177
|
+
const m1 = /^\^([^$]+)\.\+\$/.exec(pattern);
|
|
178
|
+
if (m1) {
|
|
179
|
+
const prefix = m1[1];
|
|
180
|
+
if (!/[`]/.test(prefix)) {
|
|
181
|
+
return '`' + prefix + '${string}`';
|
|
405
182
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
// PURE: Extract Identity template-literal aliases from a parsed schema.
|
|
187
|
+
function loadPatternTemplatesFromSchema(schema) {
|
|
188
|
+
const map = {};
|
|
189
|
+
const defs = schema?.$defs && typeof schema.$defs === 'object' ? schema.$defs : {};
|
|
190
|
+
for (const [defName, defVal] of Object.entries(defs)) {
|
|
191
|
+
const isPatternType = /Identity$/.test(defName);
|
|
192
|
+
if (!isPatternType)
|
|
193
|
+
continue;
|
|
194
|
+
const v = defVal;
|
|
195
|
+
if (v && v.type === 'string' && typeof v.pattern === 'string') {
|
|
196
|
+
const tmpl = deriveTemplateFromPattern(v.pattern);
|
|
197
|
+
if (tmpl)
|
|
198
|
+
map[defName] = tmpl;
|
|
409
199
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return '`' + prefix + '${string}`';
|
|
200
|
+
}
|
|
201
|
+
return map;
|
|
202
|
+
}
|
|
203
|
+
// PURE: Remove duplicate union members from emitted `export type X = | ...` blocks.
|
|
204
|
+
function removeDuplicateUnionEntries(ts) {
|
|
205
|
+
return ts.replace(/export type ([A-Za-z0-9_]+) =([\s\S]*?);/g, (_m, typeName, body) => {
|
|
206
|
+
const lines = body.split(/\r?\n/);
|
|
207
|
+
const seen = new Set();
|
|
208
|
+
const kept = [];
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
const trimmed = line.trim();
|
|
211
|
+
const match = /^\|\s*([A-Za-z0-9_]+)\b/.exec(trimmed);
|
|
212
|
+
if (match) {
|
|
213
|
+
const name = match[1];
|
|
214
|
+
if (!seen.has(name)) {
|
|
215
|
+
seen.add(name);
|
|
216
|
+
kept.push(' | ' + name);
|
|
428
217
|
}
|
|
218
|
+
continue;
|
|
429
219
|
}
|
|
430
|
-
|
|
220
|
+
if (trimmed.length)
|
|
221
|
+
kept.push(line);
|
|
431
222
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
223
|
+
return `export type ${typeName} =\n` + kept.join('\n') + ';';
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// PURE: Escape a string for use inside a RegExp pattern.
|
|
227
|
+
function escapeRegExpLiteral(value) {
|
|
228
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
229
|
+
}
|
|
230
|
+
// PURE: Fix a known json-schema-to-typescript edge case where a JSON-like recursive value union
|
|
231
|
+
// accidentally includes a direct self-reference (`| JsonData`) instead of the intended array case (`| JsonData[]`).
|
|
232
|
+
function fixJsonDataSelfReference(ts) {
|
|
233
|
+
const lines = ts.split(/\r?\n/);
|
|
234
|
+
const startIndex = lines.findIndex((l) => /^export\s+type\s+JsonData\s*=\s*$/.test(l.trimEnd()));
|
|
235
|
+
if (startIndex < 0)
|
|
236
|
+
return ts;
|
|
237
|
+
// This type currently ends with a union member object whose closing line is `};`.
|
|
238
|
+
// Find that terminator after the type header.
|
|
239
|
+
let endIndex = -1;
|
|
240
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
241
|
+
if (/^\s*};\s*$/.test(lines[i])) {
|
|
242
|
+
endIndex = i;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (endIndex < 0)
|
|
247
|
+
return ts;
|
|
248
|
+
for (let i = startIndex + 1; i < endIndex; i++) {
|
|
249
|
+
if (/^\s*\|\s*JsonData\s*$/.test(lines[i])) {
|
|
250
|
+
lines[i] = lines[i].replace(/\bJsonData\b/, 'JsonData[]');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return lines.join('\n');
|
|
254
|
+
}
|
|
255
|
+
// PURE: Apply deterministic post-processing to the generator output (no I/O).
|
|
256
|
+
function postProcessEmittedTypes(ts, parsedSchema) {
|
|
257
|
+
// Remove permissive index signatures that make interfaces open-ended.
|
|
258
|
+
// Keep meaningful map-like signatures (e.g., `[k: string]: ResourceRoleValue`) intact.
|
|
259
|
+
// Robust single-pass: delete the entire line (with optional trailing newline) where the signature appears.
|
|
260
|
+
// This avoids introducing extra blank lines while handling CRLF/LF and varying indentation.
|
|
261
|
+
ts = ts.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*(?:\r?\n)?/gm, '');
|
|
262
|
+
// Fix meta-schema types where json-schema-to-typescript can still incorrectly interpret
|
|
263
|
+
// schema definitions as literal values (or emit overly-restrictive `{}` objects).
|
|
264
|
+
// We do this as a broad post-pass on the emitted TS because the generator output varies
|
|
265
|
+
// (e.g. `allOf?: { }[];` vs `allOf?: [{type:"array"; ...}]`).
|
|
266
|
+
// `$defs?: { }` or `$defs: { }` -> map type (preserve required/optional marker)
|
|
267
|
+
// NOTE: We emit `Record<...>` instead of an index signature because later cleanup
|
|
268
|
+
// strips standalone `[k: string]: unknown;` lines.
|
|
269
|
+
ts = ts.replace(/^(\s*)(\$defs\??:\s*)\{\s*\r?\n\s*\};/gm, (_m, indent, head) => `${indent}${head}Record<string, unknown>;`);
|
|
270
|
+
// `properties?: { }` or `properties: { }` -> map type
|
|
271
|
+
ts = ts.replace(/^(\s*)(properties\??:\s*)\{\s*\r?\n\s*\};/gm, (_m, indent, head) => `${indent}${head}Record<string, unknown>;`);
|
|
272
|
+
// `allOf?: { }[];` (and similar for anyOf/oneOf) -> array of schema-ish objects
|
|
273
|
+
ts = ts.replace(/^(\s*)((?:allOf|anyOf|oneOf)\??:\s*)\{\s*\r?\n\s*\}\[\];/gm, (_m, indent, head) => `${indent}${head}Array<{[k: string]: unknown}>;`);
|
|
274
|
+
ts = removeDuplicateUnionEntries(ts);
|
|
275
|
+
// Strip URL-derived root schema interface names produced by json-schema-to-typescript
|
|
276
|
+
// (e.g. `HttpsSchemasToolproofComV2GenesisJson`). These are naming artifacts and not
|
|
277
|
+
// meaningful domain types.
|
|
278
|
+
const schemaId = parsedSchema?.$id;
|
|
279
|
+
if (typeof schemaId === 'string') {
|
|
280
|
+
const m = /\/([^/]+)\.json$/i.exec(schemaId);
|
|
281
|
+
const rootBaseName = m?.[1];
|
|
282
|
+
if (rootBaseName) {
|
|
283
|
+
const rootJsonSuffix = `${rootBaseName}Json`;
|
|
284
|
+
const rootJsonSuffixEsc = escapeRegExpLiteral(rootJsonSuffix);
|
|
285
|
+
// Remove the (typically empty) URL-derived root interface, if present.
|
|
286
|
+
ts = ts.replace(new RegExp(`^export interface Https[A-Za-z0-9_]*${rootJsonSuffixEsc}\\s*\\{\\s*\\}\\s*(?:\\r?\\n)?`, 'gm'), '');
|
|
287
|
+
// Replace remaining references to that URL-derived name in doc/comments.
|
|
288
|
+
ts = ts.replace(new RegExp(`\\bHttps[A-Za-z0-9_]*${rootJsonSuffixEsc}\\b`, 'g'), rootBaseName);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!ts || !ts.trim()) {
|
|
292
|
+
throw new Error('Type generator emitted no output for Genesis schema.');
|
|
293
|
+
}
|
|
294
|
+
// Overlay Identity aliases with template literal types inferred from Genesis.json patterns.
|
|
295
|
+
// For each $defs entry ending with "Identity" that provides a `pattern`,
|
|
296
|
+
// derive a TS template literal; otherwise fall back to plain `string`.
|
|
297
|
+
const patternTemplates = loadPatternTemplatesFromSchema(parsedSchema);
|
|
298
|
+
// Replace any exported Identity aliases to use the inferred template literals where available.
|
|
299
|
+
// Handle the common `= string;` output from json-schema-to-typescript.
|
|
300
|
+
ts = ts.replace(/^(export\s+type\s+)([A-Za-z_][A-Za-z0-9_]*Identity)(\s*=\s*)string\s*;$/gm, (_m, p1, typeName, p3) => {
|
|
301
|
+
const tmpl = patternTemplates[typeName];
|
|
302
|
+
return `${p1}${typeName}${p3}${tmpl ?? 'string'};`;
|
|
303
|
+
});
|
|
304
|
+
// Post-process map-like interfaces to enforce key constraints via template literal Identity types.
|
|
305
|
+
// Replace index-signature interfaces with Record<KeyType, ValueType> so object literal keys are checked.
|
|
306
|
+
const resourceRoleKeyType = 'ResourceRoleIdentity';
|
|
307
|
+
const jobStepKeyType = 'JobStepIdentity';
|
|
308
|
+
const strategyThreadKeyType = 'StrategyThreadIdentity';
|
|
309
|
+
ts = ts.replace(/export interface RoleMap\s*{[^}]*}/g, `export type RoleMap = Record<${resourceRoleKeyType}, ResourceRoleValue>;`);
|
|
310
|
+
// Normalize StrategyState & related socket maps to identity-keyed Records.
|
|
311
|
+
// These are emitted as `[k: string]` by json-schema-to-typescript but are identity-keyed in practice.
|
|
312
|
+
// Per schema: JobStepSocket: Record<ResourceRoleIdentity, Resource>
|
|
313
|
+
// StrategyState: Record<JobStepIdentity, JobStepSocket>
|
|
314
|
+
const jobStepSocketValueType = 'Resource';
|
|
315
|
+
ts = ts.replace(/export interface JobStepSocket\s*\{\s*\[k:\s*string\]:\s*[^;]+;\s*\}/g, `export type JobStepSocket = Record<${resourceRoleKeyType}, ${jobStepSocketValueType}>;`);
|
|
316
|
+
ts = ts.replace(/export interface StrategyState\s*\{\s*\[k:\s*string\]:\s*JobStepSocket;\s*\}/g, `export type StrategyState = Record<${jobStepKeyType}, JobStepSocket>;`);
|
|
317
|
+
ts = ts.replace(/(strategyStateUpdate\??:\s*)\{\s*\[k:\s*string\]:\s*JobStepSocket;\s*\};/g, `$1Record<${jobStepKeyType}, JobStepSocket>;`);
|
|
318
|
+
// Ensure key constraints for strategyThreadMap are preserved as template-literal identity keys.
|
|
319
|
+
// json-schema-to-typescript emits `[k: string]: StepArray;`, but we want keys to be `StrategyThreadIdentity`.
|
|
320
|
+
ts = ts.replace(/export interface StrategyThreadMap\s*\{\s*\[k:\s*string\]:\s*StepArray;\s*\}/g, `export type StrategyThreadMap = Record<${strategyThreadKeyType}, StepArray>;`);
|
|
321
|
+
ts = fixJsonDataSelfReference(ts);
|
|
322
|
+
return ts;
|
|
323
|
+
}
|
|
324
|
+
// PURE: Add banner + formatting/guards to build the final .d.ts content.
|
|
325
|
+
function finalizeOutputDts(emittedType) {
|
|
326
|
+
const banner = '// Auto-generated from JSON schemas. Do not edit.\n';
|
|
327
|
+
let output = banner + '\n' + emittedType + '\n';
|
|
328
|
+
// Final guard: strip any lingering `[k: string]: unknown;` that might have been
|
|
329
|
+
// reintroduced by later transforms.
|
|
330
|
+
output = output.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*$/gm, '');
|
|
331
|
+
// Cosmetic post-format: remove lone blank lines before closing braces and collapse excessive blank lines
|
|
332
|
+
// - Remove a single blank line before `};` and `}`
|
|
333
|
+
// - Collapse 3+ consecutive newlines into a maximum of 2
|
|
334
|
+
output = output
|
|
335
|
+
.replace(/\r?\n\s*\r?\n(\s*};)/g, '\n$1')
|
|
336
|
+
.replace(/\r?\n\s*\r?\n(\s*})/g, '\n$1')
|
|
337
|
+
.replace(/(\r?\n){3,}/g, '\n\n');
|
|
338
|
+
// As an additional safeguard, make sure the final .d.ts is treated as a module.
|
|
339
|
+
// If no export/interface/module is present, append an empty export.
|
|
340
|
+
if (!/\bexport\b|\bdeclare\s+module\b|\bdeclare\s+namespace\b/.test(output)) {
|
|
341
|
+
output += '\nexport {}\n';
|
|
342
|
+
}
|
|
343
|
+
return output;
|
|
344
|
+
}
|
|
345
|
+
// IMPURE: Main script entrypoint (config + filesystem I/O + console + process exit).
|
|
346
|
+
async function main() {
|
|
347
|
+
try {
|
|
348
|
+
const config = getConfig();
|
|
349
|
+
const inputDir = config.getSchemasDir();
|
|
350
|
+
const srcLibTypesDir = config.getTypesSrcDir();
|
|
351
|
+
const srcLibOutputPath = config.getTypesSrcPath('types.d.ts');
|
|
352
|
+
fs.mkdirSync(srcLibTypesDir, { recursive: true });
|
|
353
|
+
const schemaFileName = config.getSourceFile();
|
|
354
|
+
const schemaPath = path.join(inputDir, schemaFileName);
|
|
355
|
+
if (!fs.existsSync(schemaPath)) {
|
|
356
|
+
throw new Error(`Root schema not found: ${schemaPath}\n` +
|
|
357
|
+
`Run the schema extraction step first (e.g. pnpm run extractSchemasFromResourceTypeShells).`);
|
|
358
|
+
}
|
|
359
|
+
const rawSchema = fs.readFileSync(schemaPath, 'utf8');
|
|
360
|
+
const parsedSchema = JSON.parse(rawSchema);
|
|
361
|
+
const validationIssues = validateSchemaArrayKeywords(parsedSchema);
|
|
362
|
+
if (validationIssues.length) {
|
|
363
|
+
throw new Error('Genesis schema is not in canonical form for type generation:\n' +
|
|
364
|
+
validationIssues.map((s) => `- ${s}`).join('\n'));
|
|
365
|
+
}
|
|
366
|
+
const normalizedSchema = normalizeSchemaForGenerator(parsedSchema);
|
|
367
|
+
let ts;
|
|
368
|
+
const rootName = path.basename(schemaFileName, path.extname(schemaFileName));
|
|
369
|
+
ts = await compile(normalizedSchema, rootName, {
|
|
370
|
+
bannerComment: '',
|
|
371
|
+
declareExternallyReferenced: true,
|
|
372
|
+
unreachableDefinitions: true,
|
|
373
|
+
$refOptions: {
|
|
374
|
+
resolve: {
|
|
375
|
+
file: { order: 2 },
|
|
376
|
+
http: false,
|
|
377
|
+
https: false
|
|
453
378
|
}
|
|
454
379
|
}
|
|
455
|
-
catch {
|
|
456
|
-
// ignore failures; we'll just fall back to string
|
|
457
|
-
}
|
|
458
|
-
return map;
|
|
459
|
-
}
|
|
460
|
-
const patternTemplates = loadPatternTemplates();
|
|
461
|
-
// Replace any exported Identity/Ref aliases to use the inferred template literals where available.
|
|
462
|
-
// Handle the common `= string;` output from json-schema-to-typescript.
|
|
463
|
-
ts = ts.replace(/^(export\s+type\s+)([A-Za-z_][A-Za-z0-9_]*(?:Identity|Ref))(\s*=\s*)string\s*;$/gm, (_m, p1, typeName, p3) => {
|
|
464
|
-
const tmpl = patternTemplates[typeName];
|
|
465
|
-
return `${p1}${typeName}${p3}${tmpl ?? 'string'};`;
|
|
466
380
|
});
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const resourceRoleKeyType = 'ResourceRoleIdentity';
|
|
470
|
-
const resourceKeyType = 'ResourceIdentity';
|
|
471
|
-
const executionKeyType = 'ExecutionIdentity';
|
|
472
|
-
const strategyThreadKeyType = 'StrategyThreadIdentity';
|
|
473
|
-
ts = ts.replace(/export interface RoleMap\s*{[^}]*}/g, `export type RoleMap = Record<${resourceRoleKeyType}, ResourceRoleValue>;`);
|
|
474
|
-
ts = ts.replace(/export interface RoleBindingMap\s*{[^}]*}/g, `export type RoleBindingMap = Record<${resourceRoleKeyType}, ${resourceKeyType}>;`);
|
|
475
|
-
// Normalize StrategyState & related socket maps to identity-keyed Records.
|
|
476
|
-
// These are emitted as `[k: string]` by json-schema-to-typescript but are identity-keyed in practice.
|
|
477
|
-
const executionSocketValueType = 'ResourceMissing | ResourcePotentialInput | ResourcePotentialOutput | Resource';
|
|
478
|
-
ts = ts.replace(/export interface ExecutionSocket\s*\{\s*\[k:\s*string\]:\s*[^;]+;\s*\}/g, `export type ExecutionSocket = Record<${resourceRoleKeyType}, ${executionSocketValueType}>;`);
|
|
479
|
-
ts = ts.replace(/export interface StrategyState\s*\{\s*\[k:\s*string\]:\s*ExecutionSocket;\s*\}/g, `export type StrategyState = Record<${executionKeyType}, ExecutionSocket>;`);
|
|
480
|
-
ts = ts.replace(/(strategyStateUpdate\??:\s*)\{\s*\[k:\s*string\]:\s*ExecutionSocket;\s*\};/g, `$1Record<${executionKeyType}, ExecutionSocket>;`);
|
|
481
|
-
// Ensure key constraints for strategyThreadMap are preserved as template-literal identity keys.
|
|
482
|
-
// json-schema-to-typescript emits `[k: string]: Step[];`, but we want keys to be `StrategyThreadIdentity`.
|
|
483
|
-
ts = ts.replace(/export interface StrategyThreadMap\s*\{\s*\[k:\s*string\]:\s*Step\[\];\s*\}/g, `export type StrategyThreadMap = Record<${strategyThreadKeyType}, Step[]>;`);
|
|
484
|
-
parts.push(ts);
|
|
485
|
-
let output = parts.join('\n');
|
|
486
|
-
// Final guard: strip any lingering `[k: string]: unknown;` that might have been
|
|
487
|
-
// reintroduced by later transforms.
|
|
488
|
-
output = output.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*$/gm, '');
|
|
489
|
-
// Cosmetic post-format: remove lone blank lines before closing braces and collapse excessive blank lines
|
|
490
|
-
// - Remove a single blank line before `};` and `}`
|
|
491
|
-
// - Collapse 3+ consecutive newlines into a maximum of 2
|
|
492
|
-
output = output
|
|
493
|
-
.replace(/\r?\n\s*\r?\n(\s*};)/g, '\n$1')
|
|
494
|
-
.replace(/\r?\n\s*\r?\n(\s*})/g, '\n$1')
|
|
495
|
-
.replace(/(\r?\n){3,}/g, '\n\n');
|
|
496
|
-
// As an additional safeguard, make sure the final .d.ts is treated as a module.
|
|
497
|
-
// If no export/interface/module is present, append an empty export.
|
|
498
|
-
if (!/\bexport\b|\bdeclare\s+module\b|\bdeclare\s+namespace\b/.test(output)) {
|
|
499
|
-
output += '\nexport {}\n';
|
|
500
|
-
}
|
|
381
|
+
ts = postProcessEmittedTypes(ts, parsedSchema);
|
|
382
|
+
const output = finalizeOutputDts(ts);
|
|
501
383
|
// Write only under configured types output folder
|
|
502
384
|
try {
|
|
503
385
|
fs.writeFileSync(srcLibOutputPath, output, 'utf8');
|
|
@@ -540,11 +422,9 @@ async function main() {
|
|
|
540
422
|
console.warn('Failed to write types.js to dist/_lib:', e);
|
|
541
423
|
}
|
|
542
424
|
}
|
|
543
|
-
|
|
544
|
-
|
|
425
|
+
catch (err) {
|
|
426
|
+
console.error(err);
|
|
427
|
+
process.exitCode = 1;
|
|
545
428
|
}
|
|
546
429
|
}
|
|
547
|
-
main()
|
|
548
|
-
console.error(err);
|
|
549
|
-
process.exitCode = 1;
|
|
550
|
-
});
|
|
430
|
+
void main();
|