@themeparks/typelib 1.0.4 → 1.1.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/README.md +82 -54
- package/dist/__tests__/hash.test.d.ts +2 -0
- package/dist/__tests__/hash.test.d.ts.map +1 -0
- package/dist/__tests__/hash.test.js +207 -0
- package/dist/generate_types.d.ts.map +1 -1
- package/dist/generate_types.js +10 -4
- package/dist/hash.d.ts +27 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +104 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/type_register.d.ts +5 -5
- package/dist/type_register.js +6 -6
- package/dist/types/entities.types.d.ts +1 -1
- package/dist/types/entities.types.d.ts.map +1 -1
- package/package.json +18 -4
- package/src/__tests__/hash.test.ts +269 -0
- package/src/generate_types.ts +437 -0
- package/src/hash.ts +130 -0
- package/src/index.ts +4 -0
- package/src/run_generate_types.ts +8 -0
- package/src/type_register.ts +20 -0
- package/src/types/entities.types.ts +345 -0
- package/src/types/index.ts +13 -0
- package/src/types/livedata.types.ts +498 -0
- package/src/types/pricedata.types.ts +77 -0
- package/src/types/schedule.types.ts +137 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// Script to generate TypeScript interface files from JSON schemas
|
|
2
|
+
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
// Default directories - can be overridden in generateTypes()
|
|
7
|
+
const DEFAULT_SCHEMA_DIRS = ['./typesrc'];
|
|
8
|
+
const DEFAULT_OUTPUT_DIR = './src/types';
|
|
9
|
+
const FILE_HEADER = '// THIS FILE IS GENERATED - DO NOT EDIT DIRECTLY\n\n';
|
|
10
|
+
|
|
11
|
+
// Simple logger implementation to replace missing './logger.js'
|
|
12
|
+
const log = (...args: any[]) => console.log('[TypeGenerator 📜]', ...args);
|
|
13
|
+
const error = (...args: any[]) => console.error('[TypeGenerator 📜]', ...args);
|
|
14
|
+
|
|
15
|
+
interface JSONSchema {
|
|
16
|
+
$schema?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
type?: string | string[];
|
|
19
|
+
properties?: {
|
|
20
|
+
[key: string]: JSONSchema;
|
|
21
|
+
};
|
|
22
|
+
items?: JSONSchema;
|
|
23
|
+
required?: string[];
|
|
24
|
+
description?: string;
|
|
25
|
+
$ref?: string;
|
|
26
|
+
[key: string]: any; // Allow string indexing for reference lookup
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface TypeRegistry {
|
|
30
|
+
[typeName: string]: {
|
|
31
|
+
sourceFile: string;
|
|
32
|
+
schema: JSONSchema;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ImportTracker {
|
|
37
|
+
imports: Map<string, Set<string>>; // Map<sourceFile, Set<typeName>>
|
|
38
|
+
referencePath: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function buildTypeRegistry(schemaDirs: string[]): Promise<TypeRegistry> {
|
|
42
|
+
const registry: TypeRegistry = {};
|
|
43
|
+
|
|
44
|
+
// Process all schema directories
|
|
45
|
+
for (const schemaDir of schemaDirs) {
|
|
46
|
+
try {
|
|
47
|
+
const files = await fs.readdir(schemaDir);
|
|
48
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
49
|
+
|
|
50
|
+
for (const file of jsonFiles) {
|
|
51
|
+
const schemaPath = join(schemaDir, file);
|
|
52
|
+
const schemaContent = await fs.readFile(schemaPath, 'utf-8');
|
|
53
|
+
const schema: JSONSchema = JSON.parse(schemaContent);
|
|
54
|
+
|
|
55
|
+
// Register all top-level types from this file
|
|
56
|
+
for (const [name, typeSchema] of Object.entries(schema.properties || {})) {
|
|
57
|
+
// If type already exists, log a warning but allow override (later directories take precedence)
|
|
58
|
+
if (registry[name]) {
|
|
59
|
+
log(`Warning: Type "${name}" already exists, overriding with version from ${schemaPath}`);
|
|
60
|
+
}
|
|
61
|
+
registry[name] = {
|
|
62
|
+
sourceFile: file.replace('.json', ''),
|
|
63
|
+
schema: typeSchema
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// Skip directories that don't exist or can't be read
|
|
69
|
+
log(`Skipping schema directory ${schemaDir}: ${(err as Error).message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return registry;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveReference(ref: string, schema: JSONSchema, registry: TypeRegistry, tracker: ImportTracker): { typeName: string; sourceFile?: string } {
|
|
77
|
+
const refPath = ref.split('/').slice(1);
|
|
78
|
+
|
|
79
|
+
// Check if we're in a circular reference
|
|
80
|
+
if (tracker.referencePath.includes(ref)) {
|
|
81
|
+
throw new Error(`Circular reference detected: ${tracker.referencePath.join(' -> ')} -> ${ref}`);
|
|
82
|
+
}
|
|
83
|
+
tracker.referencePath.push(ref);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// First try to resolve in current schema
|
|
87
|
+
let referenced: JSONSchema = schema;
|
|
88
|
+
for (const segment of refPath) {
|
|
89
|
+
if (segment === 'properties' && referenced.properties) {
|
|
90
|
+
referenced = referenced.properties;
|
|
91
|
+
} else if (!referenced[segment]) {
|
|
92
|
+
// If segment not found, try alternate paths
|
|
93
|
+
if (segment === 'definitions' && ref.startsWith('#/definitions/')) {
|
|
94
|
+
// Handle #/definitions/* by looking in properties instead
|
|
95
|
+
const typeName = refPath[refPath.length - 1];
|
|
96
|
+
if (schema.properties?.[typeName]) {
|
|
97
|
+
tracker.referencePath.pop();
|
|
98
|
+
return { typeName };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If still not found, check registry
|
|
103
|
+
const registryType = registry[segment];
|
|
104
|
+
if (registryType) {
|
|
105
|
+
if (!tracker.imports.has(registryType.sourceFile)) {
|
|
106
|
+
tracker.imports.set(registryType.sourceFile, new Set());
|
|
107
|
+
}
|
|
108
|
+
tracker.imports.get(registryType.sourceFile)!.add(segment);
|
|
109
|
+
tracker.referencePath.pop();
|
|
110
|
+
return { typeName: segment, sourceFile: registryType.sourceFile };
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Invalid reference path: ${ref}`);
|
|
113
|
+
} else {
|
|
114
|
+
referenced = referenced[segment];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If we're referencing a property definition, get its type
|
|
119
|
+
const typeName = refPath[refPath.length - 1];
|
|
120
|
+
tracker.referencePath.pop();
|
|
121
|
+
return { typeName };
|
|
122
|
+
|
|
123
|
+
} catch (err) {
|
|
124
|
+
tracker.referencePath.pop();
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getTypeFromSchema(schema: JSONSchema, rootSchema: JSONSchema, registry: TypeRegistry, tracker: ImportTracker, typeName?: string): string {
|
|
130
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
131
|
+
// Handle enums specially - create union type of string literals
|
|
132
|
+
return schema.enum.map(value => `'${value}'`).join(' | ');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (schema.oneOf || schema.anyOf) {
|
|
136
|
+
// Handle oneOf/anyOf - create union type
|
|
137
|
+
const variants = schema.oneOf || schema.anyOf;
|
|
138
|
+
if (variants && Array.isArray(variants)) {
|
|
139
|
+
const types = variants.map(variant => getTypeFromSchema(variant, rootSchema, registry, tracker));
|
|
140
|
+
return types.join(' | ');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (schema.$ref) {
|
|
145
|
+
try {
|
|
146
|
+
const resolved = resolveReference(schema.$ref, rootSchema, registry, tracker);
|
|
147
|
+
return resolved.typeName;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
error(`Error resolving reference: ${schema.$ref}`, err);
|
|
150
|
+
return 'any';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (Array.isArray(schema.type)) {
|
|
155
|
+
// Handle union types like ["string", "null"]
|
|
156
|
+
return schema.type.map((t: string) => t === 'null' ? 'null' : t).join(' | ');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
switch (schema.type) {
|
|
160
|
+
case 'string':
|
|
161
|
+
return schema.nullable ? 'string | null' : 'string';
|
|
162
|
+
case 'number':
|
|
163
|
+
return schema.nullable ? 'number | null' : 'number';
|
|
164
|
+
case 'integer':
|
|
165
|
+
return schema.nullable ? 'number | null' : 'number';
|
|
166
|
+
case 'boolean':
|
|
167
|
+
return schema.nullable ? 'boolean | null' : 'boolean';
|
|
168
|
+
case 'array': {
|
|
169
|
+
let arrayType = schema.items
|
|
170
|
+
? `${getTypeFromSchema(schema.items, rootSchema, registry, tracker)}[]`
|
|
171
|
+
: 'any[]';
|
|
172
|
+
// Handle nullable arrays
|
|
173
|
+
if (schema.nullable) {
|
|
174
|
+
arrayType += ' | null';
|
|
175
|
+
}
|
|
176
|
+
return arrayType;
|
|
177
|
+
}
|
|
178
|
+
case 'object': {
|
|
179
|
+
if (!schema.properties) {
|
|
180
|
+
// Check if propertyNames constraint exists
|
|
181
|
+
let keyType = 'string';
|
|
182
|
+
if (schema.propertyNames?.$ref) {
|
|
183
|
+
const resolved = resolveReference(schema.propertyNames.$ref, rootSchema, registry, tracker);
|
|
184
|
+
keyType = resolved.typeName;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check if additionalProperties has a type constraint
|
|
188
|
+
let valueType = 'any';
|
|
189
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
190
|
+
valueType = getTypeFromSchema(schema.additionalProperties, rootSchema, registry, tracker);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Use Partial for propertyNames constraints to allow partial translations
|
|
194
|
+
const recordType = schema.propertyNames?.$ref
|
|
195
|
+
? `Partial<Record<${keyType}, ${valueType}>>`
|
|
196
|
+
: `Record<${keyType}, ${valueType}>`;
|
|
197
|
+
if (typeName) {
|
|
198
|
+
return `extends ${recordType} {}`;
|
|
199
|
+
}
|
|
200
|
+
return schema.nullable ? `${recordType} | null` : recordType;
|
|
201
|
+
}
|
|
202
|
+
const props: string[] = [];
|
|
203
|
+
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
|
|
204
|
+
const isRequired = schema.required?.includes(propName);
|
|
205
|
+
const propType = getTypeFromSchema(propSchema, rootSchema, registry, tracker);
|
|
206
|
+
const description = propSchema.description
|
|
207
|
+
? `\n /** ${propSchema.description} */\n `
|
|
208
|
+
: '';
|
|
209
|
+
props.push(`${description}${propName}${isRequired ? '' : '?'}: ${propType};`);
|
|
210
|
+
}
|
|
211
|
+
const objType = `{\n ${props.join('\n ')}\n}`;
|
|
212
|
+
return schema.nullable ? `(${objType}) | null` : objType;
|
|
213
|
+
}
|
|
214
|
+
default:
|
|
215
|
+
return 'any';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function generateRuntimeSchemaExport(schema: JSONSchema, typeRegistryImport: string): string {
|
|
220
|
+
// Generate the runtime schema registration code
|
|
221
|
+
let output = '\n// Runtime Schema Registration\n';
|
|
222
|
+
output += `import { registerTypeSchema } from "${typeRegistryImport}";\n\n`;
|
|
223
|
+
|
|
224
|
+
// For each top-level type, register its schema
|
|
225
|
+
for (const [name, typeSchema] of Object.entries(schema.properties || {})) {
|
|
226
|
+
// remove any properties that have "__private" set to true. Recusively.
|
|
227
|
+
const cleanSchema = JSON.parse(JSON.stringify(typeSchema, (key, value) => {
|
|
228
|
+
// if any value is an object with "__private" set to true, remove it
|
|
229
|
+
if (value && typeof value === 'object' && value.__private) {
|
|
230
|
+
return undefined; // Remove this property
|
|
231
|
+
}
|
|
232
|
+
return value;
|
|
233
|
+
}));
|
|
234
|
+
output += `registerTypeSchema("${name}", ${JSON.stringify(cleanSchema, null, 2)});\n\n`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return output;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function generateTypeFile(schemaPath: string, registry: TypeRegistry, outputDir: string, typeRegistryImport: string): Promise<void> {
|
|
241
|
+
log(`Generating types for schema: ${schemaPath}`);
|
|
242
|
+
const schemaContent = await fs.readFile(schemaPath, 'utf-8');
|
|
243
|
+
const schema: JSONSchema = JSON.parse(schemaContent);
|
|
244
|
+
|
|
245
|
+
let output = FILE_HEADER;
|
|
246
|
+
|
|
247
|
+
// Track imports needed for this file
|
|
248
|
+
const tracker: ImportTracker = {
|
|
249
|
+
imports: new Map<string, Set<string>>(),
|
|
250
|
+
referencePath: []
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Generate interfaces and types for top-level properties
|
|
254
|
+
for (const [name, typeSchema] of Object.entries(schema.properties || {})) {
|
|
255
|
+
const description = typeSchema.description
|
|
256
|
+
? `/** ${typeSchema.description} */\n`
|
|
257
|
+
: '';
|
|
258
|
+
|
|
259
|
+
if (typeSchema.type === 'object' && typeSchema.properties) {
|
|
260
|
+
// Handle objects with const values (like HttpStatusCode)
|
|
261
|
+
const hasConstValues = Object.entries(typeSchema.properties).some(([_, prop]) => prop.const !== undefined);
|
|
262
|
+
if (hasConstValues) {
|
|
263
|
+
output += `export enum ${name}Enum {\n`;
|
|
264
|
+
Object.entries(typeSchema.properties).forEach(([key, prop]) => {
|
|
265
|
+
output += ` ${key} = ${prop.const},\n`;
|
|
266
|
+
});
|
|
267
|
+
output += `}\n\n`;
|
|
268
|
+
output += `${description}export type ${name} = keyof typeof ${name}Enum;\n\n`;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (typeSchema.oneOf || typeSchema.anyOf) {
|
|
274
|
+
// Handle oneOf/anyOf - create type alias with union
|
|
275
|
+
const typeStr = getTypeFromSchema(typeSchema, schema, registry, tracker);
|
|
276
|
+
output += `${description}export type ${name} = ${typeStr};\n\n`;
|
|
277
|
+
} else if (typeSchema.enum && Array.isArray(typeSchema.enum)) {
|
|
278
|
+
// generate a native enum for enum types
|
|
279
|
+
output += `export enum ${name}Enum {\n`;
|
|
280
|
+
typeSchema.enum.forEach((value: string) => {
|
|
281
|
+
output += ` "${value}" = '${value}',\n`;
|
|
282
|
+
});
|
|
283
|
+
output += `}\n\n`;
|
|
284
|
+
|
|
285
|
+
// Also generate enum type
|
|
286
|
+
output += `${description}export type ${name} = keyof typeof ${name}Enum;\n\n`;
|
|
287
|
+
|
|
288
|
+
// Also generate a StringTo${name} function
|
|
289
|
+
output += `// Function to convert string to ${name}Enum\n`;
|
|
290
|
+
output += `export function StringTo${name}(value: string): ${name}Enum {\n`;
|
|
291
|
+
output += ` const lowerValue = value.toLowerCase();\n`;
|
|
292
|
+
output += ` switch (lowerValue) {\n`;
|
|
293
|
+
typeSchema.enum.forEach((value: string) => {
|
|
294
|
+
if (value) {
|
|
295
|
+
output += ` case '${value.toLowerCase()}':\n`;
|
|
296
|
+
if (value.includes(' ') || value.includes('-')) {
|
|
297
|
+
// Handle spaces and hyphens in enum values
|
|
298
|
+
output += ` return ${name}Enum["${value}"];\n`;
|
|
299
|
+
} else {
|
|
300
|
+
output += ` return ${name}Enum.${value};\n`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
output += ` }\n`;
|
|
305
|
+
output += ` throw new Error('Unknown ${name} value: ' + value);\n`;
|
|
306
|
+
output += `}\n\n`;
|
|
307
|
+
} else if (typeSchema.type === 'object' || typeSchema.allOf) {
|
|
308
|
+
// Handle inheritance with allOf
|
|
309
|
+
if (typeSchema.allOf) {
|
|
310
|
+
const baseType = typeSchema.allOf.find((t: JSONSchema) => t.$ref)?.$ref?.split('/').pop();
|
|
311
|
+
const interfaceContent = typeSchema.properties?.data
|
|
312
|
+
? getTypeFromSchema(typeSchema.properties.data, schema, registry, tracker)
|
|
313
|
+
: '{}';
|
|
314
|
+
|
|
315
|
+
if (baseType === 'AdminResponse') {
|
|
316
|
+
// For types extending AdminResponse, use the data type as generic parameter
|
|
317
|
+
output += `${description}export interface ${name} extends ${baseType}<${interfaceContent}> {}\n\n`;
|
|
318
|
+
} else if (baseType) {
|
|
319
|
+
output += `${description}export interface ${name} extends ${baseType} ${getTypeFromSchema(typeSchema, schema, registry, tracker)}\n\n`;
|
|
320
|
+
} else {
|
|
321
|
+
output += `${description}export interface ${name} ${getTypeFromSchema(typeSchema, schema, registry, tracker, name)}\n\n`;
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
// Generate regular interface type
|
|
325
|
+
if (name === 'AdminResponse') {
|
|
326
|
+
// Make AdminResponse generic with data type as parameter
|
|
327
|
+
output += `${description}export interface ${name}<T = any> {\n success: boolean;\n data: T;\n}\n\n`;
|
|
328
|
+
} else {
|
|
329
|
+
const typeStr = getTypeFromSchema(typeSchema, schema, registry, tracker, name);
|
|
330
|
+
if (typeStr.startsWith('extends')) {
|
|
331
|
+
output += `${description}export interface ${name} ${typeStr}\n\n`;
|
|
332
|
+
} else {
|
|
333
|
+
output += `${description}export type ${name} = ${typeStr}\n\n`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Add imports
|
|
341
|
+
const currentFile = schemaPath.split(/[\/\\]/).pop()!.replace('.json', '');
|
|
342
|
+
const imports = Array.from(tracker.imports.entries())
|
|
343
|
+
.filter(([file]) => file !== currentFile)
|
|
344
|
+
.map(([file, typeNames]) => `import { ${Array.from(typeNames).join(', ')} } from './${file}.types.js';`);
|
|
345
|
+
|
|
346
|
+
if (imports.length > 0) {
|
|
347
|
+
// Replace header with imports and re-add header once
|
|
348
|
+
output = output.replace(FILE_HEADER, FILE_HEADER + imports.join('\n') + '\n\n');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
output += generateRuntimeSchemaExport(schema, typeRegistryImport);
|
|
352
|
+
|
|
353
|
+
// Write to output file
|
|
354
|
+
// Get just the filename without path and convert to .types.ts
|
|
355
|
+
const baseFileName = schemaPath.split(/[\/\\]/).pop()!.replace('.json', '.types.ts');
|
|
356
|
+
const fileName = join(outputDir, baseFileName);
|
|
357
|
+
await fs.writeFile(fileName, output);
|
|
358
|
+
log(`Generated ${fileName}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function generateIndexFile(files: string[], outputDir: string): Promise<void> {
|
|
362
|
+
let output = FILE_HEADER;
|
|
363
|
+
|
|
364
|
+
// Add imports to trigger schema registration
|
|
365
|
+
output += '// Import all type files to trigger schema registration\n';
|
|
366
|
+
files.forEach(file => {
|
|
367
|
+
const baseFileName = file.replace('.json', '.types.js');
|
|
368
|
+
output += `import './${baseFileName}';\n`;
|
|
369
|
+
});
|
|
370
|
+
output += '\n';
|
|
371
|
+
|
|
372
|
+
// Add re-exports
|
|
373
|
+
output += '// Re-export types\n';
|
|
374
|
+
files.forEach(file => {
|
|
375
|
+
const baseFileName = file.replace('.json', '.types.js');
|
|
376
|
+
output += `export * from './${baseFileName}';\n`;
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await fs.writeFile(join(outputDir, 'index.ts'), output);
|
|
380
|
+
log('Generated index.ts');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Generate TypeScript types from JSON schemas
|
|
385
|
+
* @param schemaDirs Array of directories containing JSON schema files (default: ['./typesrc'])
|
|
386
|
+
* @param outputDir Directory to output generated TypeScript files (default: './src/types')
|
|
387
|
+
* @param typeRegistryImport Import path for the type registry (default: '@themeparks/typelib')
|
|
388
|
+
*/
|
|
389
|
+
export async function generateTypes({
|
|
390
|
+
schemaDirs = DEFAULT_SCHEMA_DIRS,
|
|
391
|
+
outputDir = DEFAULT_OUTPUT_DIR,
|
|
392
|
+
typeRegistryImport = "@themeparks/typelib"
|
|
393
|
+
} = {}): Promise<void> {
|
|
394
|
+
try {
|
|
395
|
+
log(`Generating types from schema directories: ${schemaDirs.join(', ')}`);
|
|
396
|
+
log(`Output directory: ${outputDir}`);
|
|
397
|
+
|
|
398
|
+
// First build the type registry from all schema directories
|
|
399
|
+
const registry = await buildTypeRegistry(schemaDirs);
|
|
400
|
+
|
|
401
|
+
// make sure output directory exists
|
|
402
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
403
|
+
|
|
404
|
+
// Collect all JSON files from all schema directories
|
|
405
|
+
const allJsonFiles: string[] = [];
|
|
406
|
+
const processedFiles = new Set<string>(); // Track processed filenames to avoid duplicates
|
|
407
|
+
|
|
408
|
+
for (const schemaDir of schemaDirs) {
|
|
409
|
+
try {
|
|
410
|
+
const files = await fs.readdir(schemaDir);
|
|
411
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
412
|
+
|
|
413
|
+
// Generate type files with cross-file reference support
|
|
414
|
+
for (const file of jsonFiles) {
|
|
415
|
+
const schemaPath = join(schemaDir, file);
|
|
416
|
+
await generateTypeFile(schemaPath, registry, outputDir, typeRegistryImport);
|
|
417
|
+
|
|
418
|
+
// Track this filename for the index file (avoid duplicates)
|
|
419
|
+
if (!processedFiles.has(file)) {
|
|
420
|
+
allJsonFiles.push(file);
|
|
421
|
+
processedFiles.add(file);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
log(`Skipping schema directory ${schemaDir}: ${(err as Error).message}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Generate index.ts with all processed files
|
|
430
|
+
await generateIndexFile(allJsonFiles, outputDir);
|
|
431
|
+
|
|
432
|
+
log('Type generation complete!');
|
|
433
|
+
} catch (err) {
|
|
434
|
+
error('Error generating types:', err);
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic object hashing using node:crypto SHA-256.
|
|
3
|
+
*
|
|
4
|
+
* ## Supported types
|
|
5
|
+
* Plain objects, arrays, strings, numbers, booleans, null.
|
|
6
|
+
*
|
|
7
|
+
* ## Unsupported types (throws TypeError)
|
|
8
|
+
* undefined, Date, Map, Set, TypedArrays (Buffer, Uint8Array, etc.),
|
|
9
|
+
* Function, BigInt, Symbol, and circular references.
|
|
10
|
+
*
|
|
11
|
+
* Unsupported types throw rather than silently producing a hash that
|
|
12
|
+
* diverges from the previous object-hash implementation.
|
|
13
|
+
*
|
|
14
|
+
* ## Key ordering
|
|
15
|
+
* Object keys are sorted recursively before serialisation, so
|
|
16
|
+
* `{ b: 2, a: 1 }` and `{ a: 1, b: 2 }` produce the same hash.
|
|
17
|
+
*
|
|
18
|
+
* ## Array ordering
|
|
19
|
+
* Array element order is preserved — `[1, 2]` and `[2, 1]` hash differently.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createHash } from 'node:crypto';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Recursively normalise a value for hashing:
|
|
26
|
+
* - Sort object keys alphabetically
|
|
27
|
+
* - Throw TypeError on any unsupported type
|
|
28
|
+
* - Track seen objects to detect circular references
|
|
29
|
+
*/
|
|
30
|
+
function normalise(value: unknown, seen: Set<object>): unknown {
|
|
31
|
+
// null is fine
|
|
32
|
+
if (value === null) return null;
|
|
33
|
+
|
|
34
|
+
// Primitives
|
|
35
|
+
if (typeof value === 'string') return value;
|
|
36
|
+
if (typeof value === 'number') {
|
|
37
|
+
if (!Number.isFinite(value)) {
|
|
38
|
+
throw new TypeError(
|
|
39
|
+
`hashObject: non-finite number is not supported (got: ${value})`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === 'boolean') return value;
|
|
45
|
+
|
|
46
|
+
// Explicitly unsupported primitives
|
|
47
|
+
if (value === undefined) {
|
|
48
|
+
throw new TypeError(
|
|
49
|
+
'hashObject: undefined is not supported — use null or omit the key instead',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === 'bigint') {
|
|
53
|
+
throw new TypeError(
|
|
54
|
+
`hashObject: BigInt is not supported — convert to string or number first (got: ${value}n)`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === 'symbol') {
|
|
58
|
+
throw new TypeError(
|
|
59
|
+
`hashObject: Symbol is not supported (got: ${String(value)})`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === 'function') {
|
|
63
|
+
throw new TypeError(
|
|
64
|
+
`hashObject: function is not supported (got: ${(value as Function).name || 'anonymous'})`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Object types
|
|
69
|
+
if (typeof value === 'object') {
|
|
70
|
+
// Circular reference detection
|
|
71
|
+
if (seen.has(value)) {
|
|
72
|
+
throw new TypeError('hashObject: circular reference detected');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Explicitly unsupported object types — check before Array/plain-object
|
|
76
|
+
if (value instanceof Date) {
|
|
77
|
+
throw new TypeError(
|
|
78
|
+
`hashObject: Date is not supported — convert to ISO string first (got: ${value.toISOString()})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (value instanceof Map) {
|
|
82
|
+
throw new TypeError(
|
|
83
|
+
'hashObject: Map is not supported — convert to a plain object first',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (value instanceof Set) {
|
|
87
|
+
throw new TypeError(
|
|
88
|
+
'hashObject: Set is not supported — convert to an Array first',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (ArrayBuffer.isView(value)) {
|
|
92
|
+
throw new TypeError(
|
|
93
|
+
`hashObject: TypedArray/Buffer is not supported — convert to a plain array or base64 string first (got: ${value.constructor.name})`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
seen.add(value);
|
|
98
|
+
|
|
99
|
+
let result: unknown;
|
|
100
|
+
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
result = value.map((item) => normalise(item, seen));
|
|
103
|
+
} else {
|
|
104
|
+
// Plain object — sort keys for determinism
|
|
105
|
+
const sorted: Record<string, unknown> = {};
|
|
106
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
107
|
+
sorted[key] = normalise((value as Record<string, unknown>)[key], seen);
|
|
108
|
+
}
|
|
109
|
+
result = sorted;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
seen.delete(value); // allow the same object to appear in sibling branches
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Should be unreachable, but TypeScript needs the exhaustive case
|
|
117
|
+
throw new TypeError(`hashObject: unsupported type: ${typeof value}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Return a deterministic 64-char hex SHA-256 hash of the given value.
|
|
122
|
+
*
|
|
123
|
+
* Throws TypeError if the value contains any unsupported type.
|
|
124
|
+
*/
|
|
125
|
+
export function hashObject(value: unknown): string {
|
|
126
|
+
const normalised = normalise(value, new Set());
|
|
127
|
+
return createHash('sha256')
|
|
128
|
+
.update(JSON.stringify(normalised))
|
|
129
|
+
.digest('hex');
|
|
130
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Store JSON schemas for runtime type information
|
|
2
|
+
const typeMetadataRegistry = new Map<string, any>();
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Register a JSON schema for runtime type checking
|
|
6
|
+
* @param name The type name (e.g. "Entity")
|
|
7
|
+
* @param schema The JSON schema definition
|
|
8
|
+
*/
|
|
9
|
+
export function registerTypeSchema(name: string, schema: any): void {
|
|
10
|
+
typeMetadataRegistry.set(name, schema);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Retrieve the JSON schema for a registered type
|
|
15
|
+
* @param name The type name
|
|
16
|
+
* @returns The JSON schema or undefined if not found
|
|
17
|
+
*/
|
|
18
|
+
export function getTypeSchema(name: string): any | undefined {
|
|
19
|
+
return typeMetadataRegistry.get(name);
|
|
20
|
+
}
|