@toolproof-core/schema 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/types/Resource_Genesis.d.ts +3 -0
- package/dist/generated/types/Resource_Genesis.js +1 -0
- package/dist/generated/types/Resource_Job.d.ts +3 -0
- package/dist/generated/types/Resource_Job.js +1 -0
- package/dist/generated/types/Resource_RawStrategy.d.ts +3 -0
- package/dist/generated/types/Resource_RawStrategy.js +1 -0
- package/dist/generated/types/Resource_ResourceType.d.ts +3 -0
- package/dist/generated/types/Resource_ResourceType.js +1 -0
- package/dist/generated/types/Resource_RunnableStrategy.d.ts +3 -0
- package/dist/generated/types/Resource_RunnableStrategy.js +1 -0
- package/dist/generated/types/types.d.ts +1784 -0
- package/dist/generated/types/types.js +1 -0
- package/dist/scripts/_lib/config.d.ts +53 -0
- package/dist/scripts/_lib/config.js +138 -0
- package/dist/scripts/extractSchemas.d.ts +1 -0
- package/dist/scripts/extractSchemas.js +210 -0
- package/dist/scripts/extractSubSchemaWithDefs.d.ts +1 -0
- package/dist/scripts/extractSubSchemaWithDefs.js +187 -0
- package/dist/scripts/generateDependencies.d.ts +1 -0
- package/dist/scripts/generateDependencies.js +106 -0
- package/dist/scripts/generateResourceShells.d.ts +1 -0
- package/dist/scripts/generateResourceShells.js +91 -0
- package/dist/scripts/generateResourceTypeType.d.ts +1 -0
- package/dist/scripts/generateResourceTypeType.js +93 -0
- package/dist/scripts/generateSchemaShims.d.ts +1 -0
- package/dist/scripts/generateSchemaShims.js +105 -0
- package/dist/scripts/generateTypes.d.ts +1 -0
- package/dist/scripts/generateTypes.js +550 -0
- package/dist/scripts/rewriteAnchors.d.ts +1 -0
- package/dist/scripts/rewriteAnchors.js +96 -0
- package/package.json +45 -0
- package/src/Genesis.json +2043 -0
- package/src/Roadmap.json +102 -0
- package/src/generated/dependencies.json +299 -0
- package/src/generated/resourceTypes/Genesis.json +2043 -0
- package/src/generated/resourceTypes/Genesis.ts +2 -0
- package/src/generated/resources/Genesis.json +2962 -0
- package/src/generated/resources/Genesis.ts +2 -0
- package/src/generated/schemas/Genesis.json +1489 -0
- package/src/generated/schemas/Genesis.ts +2 -0
- package/src/generated/schemas/Goal.json +86 -0
- package/src/generated/schemas/Goal.ts +2 -0
- package/src/generated/schemas/Job.json +236 -0
- package/src/generated/schemas/Job.ts +2 -0
- package/src/generated/schemas/RawStrategy.json +667 -0
- package/src/generated/schemas/RawStrategy.ts +2 -0
- package/src/generated/schemas/ResourceType.json +140 -0
- package/src/generated/schemas/ResourceType.ts +2 -0
- package/src/generated/schemas/RunnableStrategy.json +737 -0
- package/src/generated/schemas/RunnableStrategy.ts +2 -0
- package/src/generated/schemas/StrategyRun.json +1025 -0
- package/src/generated/schemas/StrategyRun.ts +2 -0
- package/src/generated/types/Resource_Genesis.d.ts +3 -0
- package/src/generated/types/Resource_Genesis.js +1 -0
- package/src/generated/types/Resource_Job.d.ts +3 -0
- package/src/generated/types/Resource_Job.js +1 -0
- package/src/generated/types/Resource_RawStrategy.d.ts +3 -0
- package/src/generated/types/Resource_RawStrategy.js +1 -0
- package/src/generated/types/Resource_ResourceType.d.ts +3 -0
- package/src/generated/types/Resource_ResourceType.js +1 -0
- package/src/generated/types/Resource_RunnableStrategy.d.ts +3 -0
- package/src/generated/types/Resource_RunnableStrategy.js +1 -0
- package/src/generated/types/types.d.ts +1784 -0
- package/src/generated/types/types.js +1 -0
- package/src/index.ts +1 -0
- package/src/scripts/_lib/config.ts +181 -0
- package/src/scripts/extractSchemas.ts +229 -0
- package/src/scripts/extractSubSchemaWithDefs.ts +196 -0
- package/src/scripts/generateDependencies.ts +120 -0
- package/src/scripts/generateResourceShells.ts +105 -0
- package/src/scripts/generateResourceTypeType.ts +110 -0
- package/src/scripts/generateSchemaShims.ts +115 -0
- package/src/scripts/generateTypes.ts +615 -0
- package/src/scripts/rewriteAnchors.ts +123 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { compileFromFile } from 'json-schema-to-typescript';
|
|
4
|
+
import { getConfig } from './_lib/config.js';
|
|
5
|
+
|
|
6
|
+
const config = getConfig();
|
|
7
|
+
const projectRoot = config.getRoot();
|
|
8
|
+
const inputDir = config.getOutputDir();
|
|
9
|
+
// We emit under src/genesis/generated/types and dist/genesis/generated/types
|
|
10
|
+
const srcLibTypesDir = config.getTypesSrcDir();
|
|
11
|
+
const srcLibOutputPath = config.getTypesSrcPath('types.d.ts');
|
|
12
|
+
|
|
13
|
+
// Build an index of all schema files by their basename
|
|
14
|
+
// This supports location-independent $id values where folder segments were removed
|
|
15
|
+
function buildSchemaIndex(root: string): Map<string, string> {
|
|
16
|
+
const index = new Map<string, string>();
|
|
17
|
+
const stack = [root];
|
|
18
|
+
while (stack.length) {
|
|
19
|
+
const current = stack.pop()!;
|
|
20
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const full = path.join(current, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
// Skip dist or types output folders if present inside schemas (defensive)
|
|
25
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
26
|
+
stack.push(full);
|
|
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') continue;
|
|
30
|
+
if (entry.name.startsWith('.normalized.')) continue;
|
|
31
|
+
if (entry.name.startsWith('.')) continue;
|
|
32
|
+
const baseName = entry.name; // keep extension for direct mapping
|
|
33
|
+
if (index.has(baseName)) {
|
|
34
|
+
// Hard fail on collisions so they can be fixed explicitly
|
|
35
|
+
const existing = index.get(baseName)!;
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Schema basename collision detected for "${baseName}"\n` +
|
|
38
|
+
`First: ${existing}\n` +
|
|
39
|
+
`Second: ${full}\n` +
|
|
40
|
+
`Please rename one of the schemas to ensure unique basenames.`
|
|
41
|
+
);
|
|
42
|
+
} else {
|
|
43
|
+
index.set(baseName, full);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return index;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// List all schema files (relative to inputDir), excluding documentation and temp files
|
|
52
|
+
function listAllSchemaFiles(root: string): string[] {
|
|
53
|
+
const files: string[] = [];
|
|
54
|
+
const stack = [root];
|
|
55
|
+
while (stack.length) {
|
|
56
|
+
const current = stack.pop()!;
|
|
57
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const full = path.join(current, entry.name);
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
if (entry.name === 'documentation' || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
62
|
+
stack.push(full);
|
|
63
|
+
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
64
|
+
// Ignore temp files that can be left behind after crashes
|
|
65
|
+
if (entry.name === '.combined-schema.json') continue;
|
|
66
|
+
if (entry.name.startsWith('.normalized.')) continue;
|
|
67
|
+
if (entry.name.startsWith('.')) continue;
|
|
68
|
+
// produce path relative to root with posix separators
|
|
69
|
+
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
70
|
+
files.push(rel);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
files.sort(); // deterministic order
|
|
75
|
+
return files;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main() {
|
|
79
|
+
// Ensure types output folder exists
|
|
80
|
+
fs.mkdirSync(srcLibTypesDir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
const parts: string[] = [];
|
|
83
|
+
parts.push('// Auto-generated from JSON schemas. Do not edit.\n');
|
|
84
|
+
// Precompute index for location-independent IDs (filename only after version segment)
|
|
85
|
+
const schemaIndex = buildSchemaIndex(inputDir);
|
|
86
|
+
const idToCanonical: Record<string, string> = {};
|
|
87
|
+
|
|
88
|
+
// Custom resolver to map our absolute schema IDs to local files, preventing HTTP fetches.
|
|
89
|
+
const toolproofResolver = {
|
|
90
|
+
order: 1,
|
|
91
|
+
canRead: (file: any) => {
|
|
92
|
+
let url: string = (typeof file.url === 'string' ? file.url : file.url?.href) || '';
|
|
93
|
+
// Strip accidental wrapping quotes
|
|
94
|
+
if ((url.startsWith("'") && url.endsWith("'")) || (url.startsWith('"') && url.endsWith('"'))) {
|
|
95
|
+
url = url.slice(1, -1);
|
|
96
|
+
}
|
|
97
|
+
return config.isSchemaUrl(url);
|
|
98
|
+
},
|
|
99
|
+
read: (file: any) => {
|
|
100
|
+
let url: string = (typeof file.url === 'string' ? file.url : file.url?.href) || '';
|
|
101
|
+
// Strip accidental wrapping quotes
|
|
102
|
+
if ((url.startsWith("'") && url.endsWith("'")) || (url.startsWith('"') && url.endsWith('"'))) {
|
|
103
|
+
url = url.slice(1, -1);
|
|
104
|
+
}
|
|
105
|
+
// Strip hash part (anchors) for path resolution
|
|
106
|
+
const noHash = url.split('#')[0];
|
|
107
|
+
|
|
108
|
+
// Extract schema name using config
|
|
109
|
+
const schemaName = config.extractSchemaName(noHash);
|
|
110
|
+
const fileName = schemaName.endsWith('.json') ? schemaName : `${schemaName}.json`;
|
|
111
|
+
|
|
112
|
+
// Resolve by basename only (location-independent IDs)
|
|
113
|
+
const indexed = schemaIndex.get(fileName);
|
|
114
|
+
if (indexed) {
|
|
115
|
+
return fs.readFileSync(indexed, 'utf8');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Toolproof resolver: could not locate schema for URL "${url}". ` +
|
|
120
|
+
`Tried basename "${fileName}" in schema index. Ensure unique basenames and correct $id.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
} as any;
|
|
124
|
+
|
|
125
|
+
// Files to include in the combined schema (auto-discovered, excludes documentation)
|
|
126
|
+
const toCompile = listAllSchemaFiles(inputDir);
|
|
127
|
+
|
|
128
|
+
// Collect schema refs and defs; we'll compile files individually to avoid combined-schema traversal issues.
|
|
129
|
+
const definitions: Record<string, unknown> = {};
|
|
130
|
+
const includedNames: string[] = [];
|
|
131
|
+
|
|
132
|
+
for (const fileName of toCompile) {
|
|
133
|
+
const p = path.join(inputDir, fileName);
|
|
134
|
+
if (!fs.existsSync(p)) {
|
|
135
|
+
console.warn(`Schema file missing, skipping: ${p}`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Definition key: basename without extension, with path segments removed
|
|
139
|
+
const base = path.basename(fileName, '.json');
|
|
140
|
+
// Prefer the schema's declared $id so all references point to the same absolute URI
|
|
141
|
+
// This helps json-schema-to-typescript dedupe declarations instead of emitting FooJson and FooJson1
|
|
142
|
+
let refValue = `./${fileName}`;
|
|
143
|
+
try {
|
|
144
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
145
|
+
const parsed = JSON.parse(raw);
|
|
146
|
+
if (parsed && typeof parsed.$id === 'string' && parsed.$id.trim()) {
|
|
147
|
+
// Sanitize IDs that may have been emitted with surrounding quotes
|
|
148
|
+
// e.g. quotes around schema URL
|
|
149
|
+
let cleaned = parsed.$id.trim();
|
|
150
|
+
if ((cleaned.startsWith("'") && cleaned.endsWith("'")) || (cleaned.startsWith('"') && cleaned.endsWith('"'))) {
|
|
151
|
+
cleaned = cleaned.slice(1, -1);
|
|
152
|
+
}
|
|
153
|
+
refValue = cleaned;
|
|
154
|
+
idToCanonical[refValue] = base + 'Json';
|
|
155
|
+
}
|
|
156
|
+
// Promote this file's $defs to top-level combined $defs so each gets its own exported type
|
|
157
|
+
if (parsed && parsed.$defs && typeof parsed.$defs === 'object') {
|
|
158
|
+
const defNames = Object.keys(parsed.$defs).filter((k) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(k));
|
|
159
|
+
for (const defName of defNames) {
|
|
160
|
+
// Prefer bare def name; if collision, namespace with file base
|
|
161
|
+
let entryName = defName;
|
|
162
|
+
if (definitions[entryName]) {
|
|
163
|
+
entryName = `${base}_${defName}`;
|
|
164
|
+
}
|
|
165
|
+
if (!definitions[entryName]) {
|
|
166
|
+
definitions[entryName] = { $ref: `${refValue}#/$defs/${defName}` } as any;
|
|
167
|
+
includedNames.push(entryName);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// If parsing fails, fall back to relative ref; proceed gracefully
|
|
173
|
+
}
|
|
174
|
+
definitions[base] = { $ref: refValue };
|
|
175
|
+
includedNames.push(base);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (includedNames.length === 0) {
|
|
179
|
+
console.warn('No schema files found to compile. Nothing to do.');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Instead of building a combined schema (which can trip json-schema-to-typescript traversals),
|
|
184
|
+
// compile each discovered schema file individually and concatenate the outputs.
|
|
185
|
+
try {
|
|
186
|
+
let ts = '';
|
|
187
|
+
for (const fileName of toCompile) {
|
|
188
|
+
const schemaPath = path.join(inputDir, fileName);
|
|
189
|
+
if (!fs.existsSync(schemaPath)) continue;
|
|
190
|
+
// Load and defensively normalize array-expected keywords to avoid generator crashes
|
|
191
|
+
let toCompilePath = schemaPath;
|
|
192
|
+
try {
|
|
193
|
+
const rawSchema = fs.readFileSync(schemaPath, 'utf8');
|
|
194
|
+
const parsedSchema: any = JSON.parse(rawSchema);
|
|
195
|
+
function normalizeArrays(node: any, parentKey?: string) {
|
|
196
|
+
if (Array.isArray(node)) {
|
|
197
|
+
for (let i = 0; i < node.length; i++) normalizeArrays(node[i], parentKey);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!node || typeof node !== 'object') return;
|
|
201
|
+
|
|
202
|
+
// IMPORTANT:
|
|
203
|
+
// In meta-schemas we often have `properties: { required: { ...schema... } }`.
|
|
204
|
+
// Here, the key "required" is a *property name*, not the JSON-Schema keyword
|
|
205
|
+
// "required". Blindly normalizing would wrap that schema object into an array,
|
|
206
|
+
// causing json-schema-to-typescript to emit the schema-shape instead of `string[]`.
|
|
207
|
+
//
|
|
208
|
+
// So: only coerce array-keywords when we're not inside a map of property names.
|
|
209
|
+
const isPropertyNameMap = parentKey === 'properties'
|
|
210
|
+
|| parentKey === 'patternProperties'
|
|
211
|
+
|| parentKey === '$defs'
|
|
212
|
+
|| parentKey === 'definitions'
|
|
213
|
+
|| parentKey === 'dependentSchemas';
|
|
214
|
+
|
|
215
|
+
if (!isPropertyNameMap) {
|
|
216
|
+
const arrayKeys = ['anyOf', 'allOf', 'oneOf', 'required', 'enum'];
|
|
217
|
+
for (const k of arrayKeys) {
|
|
218
|
+
if (k in node) {
|
|
219
|
+
const v = node[k];
|
|
220
|
+
if (!Array.isArray(v)) node[k] = [v];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const [k, v] of Object.entries(node)) normalizeArrays(v, k);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// json-schema-to-typescript has a long-standing quirk:
|
|
229
|
+
// when a schema uses `allOf`, sibling object keywords like `properties`/`required`
|
|
230
|
+
// can be ignored in the emitted TS (it treats `allOf` as the whole schema).
|
|
231
|
+
//
|
|
232
|
+
// Example (Genesis $defs/Job):
|
|
233
|
+
// { type: 'object', allOf: [Documented, RolesWrapper], properties: { identity: ... }, required: [...] }
|
|
234
|
+
// can become:
|
|
235
|
+
// type Job = Documented & RolesWrapper
|
|
236
|
+
//
|
|
237
|
+
// To avoid cluttering the source JSON schemas, we normalize to a temp file:
|
|
238
|
+
// move those sibling object keywords into an extra `allOf` item.
|
|
239
|
+
function normalizeAllOfSiblingObjectKeywords(node: any) {
|
|
240
|
+
if (Array.isArray(node)) {
|
|
241
|
+
for (const item of node) normalizeAllOfSiblingObjectKeywords(item);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!node || typeof node !== 'object') return;
|
|
245
|
+
|
|
246
|
+
const hasAllOf = Array.isArray(node.allOf) && node.allOf.length > 0;
|
|
247
|
+
const looksLikeObjectSchema =
|
|
248
|
+
node.type === 'object' ||
|
|
249
|
+
node.properties !== undefined ||
|
|
250
|
+
node.required !== undefined ||
|
|
251
|
+
node.unevaluatedProperties !== undefined ||
|
|
252
|
+
node.additionalProperties !== undefined;
|
|
253
|
+
|
|
254
|
+
if (hasAllOf && looksLikeObjectSchema) {
|
|
255
|
+
const siblingKeys = [
|
|
256
|
+
'properties',
|
|
257
|
+
'required',
|
|
258
|
+
'additionalProperties',
|
|
259
|
+
'unevaluatedProperties',
|
|
260
|
+
'propertyNames',
|
|
261
|
+
'patternProperties',
|
|
262
|
+
'dependentRequired',
|
|
263
|
+
'dependentSchemas',
|
|
264
|
+
'minProperties',
|
|
265
|
+
'maxProperties'
|
|
266
|
+
] as const;
|
|
267
|
+
|
|
268
|
+
const hasSiblingObjectKeywords = siblingKeys.some((k) => k in node);
|
|
269
|
+
if (hasSiblingObjectKeywords) {
|
|
270
|
+
const overlay: any = {};
|
|
271
|
+
if (node.type === 'object') overlay.type = 'object';
|
|
272
|
+
|
|
273
|
+
for (const k of siblingKeys) {
|
|
274
|
+
if (k in node) {
|
|
275
|
+
overlay[k] = node[k];
|
|
276
|
+
delete node[k];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Prepend so it participates in the intersection early.
|
|
281
|
+
node.allOf = [overlay, ...node.allOf];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const v of Object.values(node)) normalizeAllOfSiblingObjectKeywords(v);
|
|
286
|
+
}
|
|
287
|
+
// Normalize expected arrays to prevent traversal crashes
|
|
288
|
+
normalizeArrays(parsedSchema);
|
|
289
|
+
|
|
290
|
+
// Normalize `allOf` + sibling object keywords so the TS generator doesn't drop them.
|
|
291
|
+
normalizeAllOfSiblingObjectKeywords(parsedSchema);
|
|
292
|
+
const tmpPath = path.join(inputDir, `.normalized.${path.basename(fileName)}`);
|
|
293
|
+
fs.writeFileSync(tmpPath, JSON.stringify(parsedSchema, null, 2), 'utf8');
|
|
294
|
+
toCompilePath = tmpPath;
|
|
295
|
+
} catch (e) {
|
|
296
|
+
// If normalization fails, fall back to original path
|
|
297
|
+
}
|
|
298
|
+
const part = await compileFromFile(toCompilePath, {
|
|
299
|
+
bannerComment: '',
|
|
300
|
+
//
|
|
301
|
+
declareExternallyReferenced: true,
|
|
302
|
+
unreachableDefinitions: true,
|
|
303
|
+
// Forward ref parser options so absolute $id/$ref URLs resolve from local files
|
|
304
|
+
$refOptions: {
|
|
305
|
+
// Don’t go to the network; we provide a local resolver for our domain
|
|
306
|
+
resolve: {
|
|
307
|
+
file: { order: 2 },
|
|
308
|
+
http: false,
|
|
309
|
+
https: false,
|
|
310
|
+
toolproof: toolproofResolver
|
|
311
|
+
}
|
|
312
|
+
} as any
|
|
313
|
+
});
|
|
314
|
+
ts += '\n' + part + '\n';
|
|
315
|
+
// Cleanup temp normalized file
|
|
316
|
+
if (toCompilePath !== schemaPath) {
|
|
317
|
+
try { fs.unlinkSync(toCompilePath); } catch { }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Remove permissive index signatures that make interfaces open-ended.
|
|
322
|
+
// Keep meaningful map-like signatures (e.g., `[k: string]: ResourceRoleValue`) intact.
|
|
323
|
+
// Robust single-pass: delete the entire line (with optional trailing newline) where the signature appears.
|
|
324
|
+
// This avoids introducing extra blank lines while handling CRLF/LF and varying indentation.
|
|
325
|
+
ts = ts.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*(?:\r?\n)?/gm, '');
|
|
326
|
+
|
|
327
|
+
// Fix meta-schema types where json-schema-to-typescript can still incorrectly interpret
|
|
328
|
+
// schema definitions as literal values (or emit overly-restrictive `{}` objects).
|
|
329
|
+
// We do this as a broad post-pass on the emitted TS because the generator output varies
|
|
330
|
+
// (e.g. `allOf?: { }[];` vs `allOf?: [{type:"array"; ...}]`).
|
|
331
|
+
|
|
332
|
+
// `$defs?: { }` or `$defs: { }` -> map type (preserve required/optional marker)
|
|
333
|
+
// NOTE: We emit `Record<...>` instead of an index signature because later cleanup
|
|
334
|
+
// strips standalone `[k: string]: unknown;` lines.
|
|
335
|
+
ts = ts.replace(
|
|
336
|
+
/^(\s*)(\$defs\??:\s*)\{\s*\r?\n\s*\};/gm,
|
|
337
|
+
(_m, indent, head) => `${indent}${head}Record<string, unknown>;`
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// `properties?: { }` or `properties: { }` -> map type
|
|
341
|
+
ts = ts.replace(
|
|
342
|
+
/^(\s*)(properties\??:\s*)\{\s*\r?\n\s*\};/gm,
|
|
343
|
+
(_m, indent, head) => `${indent}${head}Record<string, unknown>;`
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// `allOf?: { }[];` (and similar for anyOf/oneOf) -> array of schema-ish objects
|
|
347
|
+
ts = ts.replace(
|
|
348
|
+
/^(\s*)((?:allOf|anyOf|oneOf)\??:\s*)\{\s*\r?\n\s*\}\[\];/gm,
|
|
349
|
+
(_m, indent, head) => `${indent}${head}Array<{[k: string]: unknown}>;`
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// Older broken shapes from earlier normalization (keep for backwards compatibility)
|
|
353
|
+
ts = ts.replace(
|
|
354
|
+
/((?:allOf|anyOf|oneOf)\??:\s*)\[\{type:\s*"array";\s*items:\s*\{type:\s*"object"\}\}\];/g,
|
|
355
|
+
'$1Array<{[k: string]: unknown}>;'
|
|
356
|
+
);
|
|
357
|
+
ts = ts.replace(
|
|
358
|
+
/required\?:\s*\[\{type:\s*"array";\s*items:\s*\{type:\s*"string"\};\s*uniqueItems:\s*true\}\];/g,
|
|
359
|
+
'required?: string[];'
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Similarly fix IdentityProp/MeritProp which have the same issue
|
|
363
|
+
ts = ts.replace(
|
|
364
|
+
/^(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,
|
|
365
|
+
'$1required?: string[];'
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
ts = ts.replace(
|
|
369
|
+
/^(export interface MeritProp[\s\S]*?)(required:\s*\[\{type:\s*"array";\s*items:\s*\{type:\s*"string"\};\s*uniqueItems:\s*true\}\];)/gm,
|
|
370
|
+
'$1required?: string[];'
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// Prune verbose type/interface names produced from absolute $id URLs.
|
|
374
|
+
// Deterministic pruning based on original $id -> baseName map
|
|
375
|
+
// This avoids heuristic truncation that dropped prefixes.
|
|
376
|
+
function idToGeneratedIdentifier(id: string): string {
|
|
377
|
+
// json-schema-to-typescript seems to create a PascalCase of the URL with protocol prefix
|
|
378
|
+
// Simplified reconstruction: 'https://' => 'Https', then capitalize path & host segments
|
|
379
|
+
const noProto = id.replace(/^https?:\/\//i, '');
|
|
380
|
+
const tokens = noProto
|
|
381
|
+
.split(/[\/#?.=&_-]+/)
|
|
382
|
+
.filter(Boolean)
|
|
383
|
+
.map((t) => t.replace(/[^A-Za-z0-9]/g, ''))
|
|
384
|
+
.filter(Boolean)
|
|
385
|
+
.map((t) => t.charAt(0).toUpperCase() + t.slice(1));
|
|
386
|
+
return 'Https' + tokens.join('') + 'Json';
|
|
387
|
+
}
|
|
388
|
+
// Perform replacements for known IDs
|
|
389
|
+
for (const [id, canonical] of Object.entries(idToCanonical)) {
|
|
390
|
+
const longName = idToGeneratedIdentifier(id);
|
|
391
|
+
if (longName === canonical) continue; // already minimal
|
|
392
|
+
const re = new RegExp(`\\b${longName}\\b`, 'g');
|
|
393
|
+
ts = ts.replace(re, canonical);
|
|
394
|
+
}
|
|
395
|
+
// Remove version prefixes inside any remaining long identifiers: Https...V0... -> remove V0 if followed by capital
|
|
396
|
+
ts = ts.replace(/(Https[A-Za-z0-9_]*?)V\d+([A-Z])/g, '$1$2');
|
|
397
|
+
|
|
398
|
+
// Final cleanup: aggressively strip the domain prefix `HttpsSchemasToolproofCom` from ALL identifiers.
|
|
399
|
+
// This is safe because those long names are only artifacts of json-schema-to-typescript; base names don't start with that sequence.
|
|
400
|
+
ts = ts.replace(/\bHttpsSchemasToolproofCom(?=[A-Z])/g, '');
|
|
401
|
+
// Remove accidental duplicate union entries in any exported union types after shortening.
|
|
402
|
+
ts = ts.replace(/export type ([A-Za-z0-9_]+) =([\s\S]*?);/g, (m, typeName, body) => {
|
|
403
|
+
const lines = body.split(/\n/);
|
|
404
|
+
const seen2 = new Set<string>();
|
|
405
|
+
const kept: string[] = [];
|
|
406
|
+
for (const line of lines) {
|
|
407
|
+
const trimmed = line.trim();
|
|
408
|
+
const match = /^\|\s*([A-Za-z0-9_]+)\b/.exec(trimmed);
|
|
409
|
+
if (match) {
|
|
410
|
+
const name = match[1];
|
|
411
|
+
if (!seen2.has(name)) {
|
|
412
|
+
seen2.add(name);
|
|
413
|
+
kept.push(' | ' + name);
|
|
414
|
+
}
|
|
415
|
+
} else if (trimmed.length) {
|
|
416
|
+
kept.push(line);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return `export type ${typeName} =\n` + kept.join('\n') + ';';
|
|
420
|
+
});
|
|
421
|
+
// If nothing was emitted, compile Genesis.json as a final fallback.
|
|
422
|
+
if (!ts || !ts.trim()) {
|
|
423
|
+
const primary = path.join(inputDir, 'Genesis.json');
|
|
424
|
+
if (fs.existsSync(primary)) {
|
|
425
|
+
ts = await compileFromFile(primary, {
|
|
426
|
+
bannerComment: '',
|
|
427
|
+
declareExternallyReferenced: true,
|
|
428
|
+
unreachableDefinitions: true,
|
|
429
|
+
$refOptions: {
|
|
430
|
+
resolve: {
|
|
431
|
+
file: { order: 2 },
|
|
432
|
+
http: false,
|
|
433
|
+
https: false,
|
|
434
|
+
toolproof: toolproofResolver
|
|
435
|
+
}
|
|
436
|
+
} as any
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Still nothing? ensure we emit a module so downstream imports don't fail.
|
|
441
|
+
if (!ts || !ts.trim()) {
|
|
442
|
+
ts = '// (No concrete types emitted by generator)\nexport {}\n';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Overlay Identity/Ref aliases with template literal types inferred from Genesis.json patterns.
|
|
446
|
+
// For each $defs entry ending with "Identity" or "Ref", and that provides a `pattern`,
|
|
447
|
+
// derive a TS template literal; otherwise fall back to plain `string`.
|
|
448
|
+
function deriveTemplateFromPattern(pattern: string): string | undefined {
|
|
449
|
+
|
|
450
|
+
// Common form: ^PREFIX-.+$ => PREFIX-${string}
|
|
451
|
+
const m1 = /^\^([^$]+)\.\+\$/.exec(pattern);
|
|
452
|
+
if (m1) {
|
|
453
|
+
const prefix = m1[1];
|
|
454
|
+
if (!/[`]/.test(prefix)) {
|
|
455
|
+
return '`' + prefix + '${string}`';
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Slightly stricter forms: ^PREFIX-[A-Za-z0-9]+$ => PREFIX-${string}
|
|
459
|
+
const m2 = /^\^([A-Za-z0-9._:-]+-)\[?\^?[A-Za-z0-9]+\]?\+?\$/.exec(pattern);
|
|
460
|
+
if (m2) {
|
|
461
|
+
const prefix = m2[1];
|
|
462
|
+
if (!/[`]/.test(prefix)) {
|
|
463
|
+
return '`' + prefix + '${string}`';
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function loadPatternTemplates(): Record<string, string> {
|
|
470
|
+
const map: Record<string, string> = {};
|
|
471
|
+
try {
|
|
472
|
+
const genesisPath = path.join(inputDir, config.getSourceFile());
|
|
473
|
+
if (fs.existsSync(genesisPath)) {
|
|
474
|
+
const raw = fs.readFileSync(genesisPath, 'utf8');
|
|
475
|
+
const parsed = JSON.parse(raw);
|
|
476
|
+
const defs = parsed?.$defs && typeof parsed.$defs === 'object' ? parsed.$defs : {};
|
|
477
|
+
for (const [defName, defVal] of Object.entries(defs)) {
|
|
478
|
+
// Process pattern-bearing string types.
|
|
479
|
+
// Supports "...Identity" / "...Ref" conventions.
|
|
480
|
+
const isPatternType = /(?:Identity|Ref)$/.test(defName);
|
|
481
|
+
if (!isPatternType) continue;
|
|
482
|
+
|
|
483
|
+
const v: any = defVal;
|
|
484
|
+
if (v && v.type === 'string' && typeof v.pattern === 'string') {
|
|
485
|
+
const tmpl = deriveTemplateFromPattern(v.pattern);
|
|
486
|
+
if (tmpl) map[defName] = tmpl;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} catch {
|
|
491
|
+
// ignore failures; we'll just fall back to string
|
|
492
|
+
}
|
|
493
|
+
return map;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const patternTemplates = loadPatternTemplates();
|
|
497
|
+
|
|
498
|
+
// Replace any exported Identity/Ref aliases to use the inferred template literals where available.
|
|
499
|
+
// Handle the common `= string;` output from json-schema-to-typescript.
|
|
500
|
+
ts = ts.replace(
|
|
501
|
+
/^(export\s+type\s+)([A-Za-z_][A-Za-z0-9_]*(?:Identity|Ref))(\s*=\s*)string\s*;$/gm,
|
|
502
|
+
(_m, p1, typeName, p3) => {
|
|
503
|
+
const tmpl = patternTemplates[typeName];
|
|
504
|
+
return `${p1}${typeName}${p3}${tmpl ?? 'string'};`;
|
|
505
|
+
}
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// Post-process map-like interfaces to enforce key constraints via template literal Identity types.
|
|
509
|
+
// Replace index-signature interfaces with Record<KeyType, ValueType> so object literal keys are checked.
|
|
510
|
+
const resourceRoleKeyType = 'ResourceRoleIdentity';
|
|
511
|
+
const resourceKeyType = 'ResourceIdentity';
|
|
512
|
+
const executionKeyType = 'ExecutionIdentity';
|
|
513
|
+
const strategyThreadKeyType = 'StrategyThreadIdentity';
|
|
514
|
+
|
|
515
|
+
ts = ts.replace(
|
|
516
|
+
/export interface RoleMap\s*{[^}]*}/g,
|
|
517
|
+
`export type RoleMap = Record<${resourceRoleKeyType}, ResourceRoleValue>;`
|
|
518
|
+
);
|
|
519
|
+
ts = ts.replace(
|
|
520
|
+
/export interface RoleBindingMap\s*{[^}]*}/g,
|
|
521
|
+
`export type RoleBindingMap = Record<${resourceRoleKeyType}, ${resourceKeyType}>;`
|
|
522
|
+
);
|
|
523
|
+
// Normalize StrategyState & related socket maps to identity-keyed Records.
|
|
524
|
+
// These are emitted as `[k: string]` by json-schema-to-typescript but are identity-keyed in practice.
|
|
525
|
+
const executionSocketValueType = 'ResourceMissing | ResourcePotentialInput | ResourcePotentialOutput | Resource';
|
|
526
|
+
ts = ts.replace(
|
|
527
|
+
/export interface ExecutionSocket\s*\{\s*\[k:\s*string\]:\s*[^;]+;\s*\}/g,
|
|
528
|
+
`export type ExecutionSocket = Record<${resourceRoleKeyType}, ${executionSocketValueType}>;`
|
|
529
|
+
);
|
|
530
|
+
ts = ts.replace(
|
|
531
|
+
/export interface StrategyState\s*\{\s*\[k:\s*string\]:\s*ExecutionSocket;\s*\}/g,
|
|
532
|
+
`export type StrategyState = Record<${executionKeyType}, ExecutionSocket>;`
|
|
533
|
+
);
|
|
534
|
+
ts = ts.replace(
|
|
535
|
+
/(strategyStateUpdate\??:\s*)\{\s*\[k:\s*string\]:\s*ExecutionSocket;\s*\};/g,
|
|
536
|
+
`$1Record<${executionKeyType}, ExecutionSocket>;`
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
// Ensure key constraints for strategyThreadMap are preserved as template-literal identity keys.
|
|
540
|
+
// json-schema-to-typescript emits `[k: string]: Step[];`, but we want keys to be `StrategyThreadIdentity`.
|
|
541
|
+
ts = ts.replace(
|
|
542
|
+
/export interface StrategyThreadMap\s*\{\s*\[k:\s*string\]:\s*Step\[\];\s*\}/g,
|
|
543
|
+
`export type StrategyThreadMap = Record<${strategyThreadKeyType}, Step[]>;`
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
parts.push(ts);
|
|
547
|
+
|
|
548
|
+
let output = parts.join('\n');
|
|
549
|
+
|
|
550
|
+
// Final guard: strip any lingering `[k: string]: unknown;` that might have been
|
|
551
|
+
// reintroduced by later transforms.
|
|
552
|
+
output = output.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*$/gm, '');
|
|
553
|
+
|
|
554
|
+
// Cosmetic post-format: remove lone blank lines before closing braces and collapse excessive blank lines
|
|
555
|
+
// - Remove a single blank line before `};` and `}`
|
|
556
|
+
// - Collapse 3+ consecutive newlines into a maximum of 2
|
|
557
|
+
output = output
|
|
558
|
+
.replace(/\r?\n\s*\r?\n(\s*};)/g, '\n$1')
|
|
559
|
+
.replace(/\r?\n\s*\r?\n(\s*})/g, '\n$1')
|
|
560
|
+
.replace(/(\r?\n){3,}/g, '\n\n');
|
|
561
|
+
|
|
562
|
+
// As an additional safeguard, make sure the final .d.ts is treated as a module.
|
|
563
|
+
// If no export/interface/module is present, append an empty export.
|
|
564
|
+
if (!/\bexport\b|\bdeclare\s+module\b|\bdeclare\s+namespace\b/.test(output)) {
|
|
565
|
+
output += '\nexport {}\n';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Write only under configured types output folder
|
|
569
|
+
try {
|
|
570
|
+
fs.writeFileSync(srcLibOutputPath, output, 'utf8');
|
|
571
|
+
console.log('Wrote', srcLibOutputPath);
|
|
572
|
+
} catch (e) {
|
|
573
|
+
console.warn('Failed to write types to src/_lib:', e);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Also write a copy into dist so consumers get the generated declarations
|
|
577
|
+
const distLibTypesDir = config.getTypesDistDir();
|
|
578
|
+
const distLibOutputPath = config.getTypesDistPath('types.d.ts');
|
|
579
|
+
try {
|
|
580
|
+
fs.mkdirSync(distLibTypesDir, { recursive: true });
|
|
581
|
+
fs.writeFileSync(distLibOutputPath, output, 'utf8');
|
|
582
|
+
console.log('Wrote', distLibOutputPath);
|
|
583
|
+
} catch (e) {
|
|
584
|
+
// If copying to dist fails, log but don't crash the generator.
|
|
585
|
+
console.warn('Failed to write types to dist:', e);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Ensure there is a runtime-resolvable module next to `types.d.ts`
|
|
589
|
+
// Some consumers and TS NodeNext resolution expect a concrete .js next to .d.ts
|
|
590
|
+
// The file is intentionally empty as all exports are types-only.
|
|
591
|
+
try {
|
|
592
|
+
const srcLibTypesJsPath = config.getTypesSrcPath('types.js');
|
|
593
|
+
if (!fs.existsSync(srcLibTypesJsPath)) {
|
|
594
|
+
fs.writeFileSync(srcLibTypesJsPath, 'export {}\n', 'utf8');
|
|
595
|
+
console.log('Wrote', srcLibTypesJsPath);
|
|
596
|
+
}
|
|
597
|
+
} catch (e) {
|
|
598
|
+
console.warn('Failed to write types.js to src/_lib:', e);
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
const distLibTypesJsPath = config.getTypesDistPath('types.js');
|
|
602
|
+
fs.writeFileSync(distLibTypesJsPath, 'export {}\n', 'utf8');
|
|
603
|
+
console.log('Wrote', distLibTypesJsPath);
|
|
604
|
+
} catch (e) {
|
|
605
|
+
console.warn('Failed to write types.js to dist/_lib:', e);
|
|
606
|
+
}
|
|
607
|
+
} finally {
|
|
608
|
+
// No temp combined schema used.
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
main().catch((err) => {
|
|
613
|
+
console.error(err);
|
|
614
|
+
process.exitCode = 1;
|
|
615
|
+
});
|