@themeparks/typelib 1.0.5 → 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 +6 -3
- 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/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
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@ TypeScript definition system for ThemeParks.wiki
|
|
|
6
6
|
|
|
7
7
|
`@themeparks/typelib` is a TypeScript types package that generates types from JSON schemas and provides runtime type validation. It is designed for the internal ThemeParks.wiki systems and generating client libraries. You likely do not want to interact with this library directly.
|
|
8
8
|
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Node.js >= 24.0.0
|
|
12
|
+
|
|
9
13
|
## Installation
|
|
10
14
|
|
|
11
15
|
```bash
|
|
@@ -17,30 +21,32 @@ npm install @themeparks/typelib
|
|
|
17
21
|
### Using pre-built types
|
|
18
22
|
|
|
19
23
|
```typescript
|
|
20
|
-
import {
|
|
24
|
+
import { Entity, LiveData, EntitySchedule } from '@themeparks/typelib';
|
|
21
25
|
|
|
22
26
|
// Use generated types
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const entity: Entity = {
|
|
28
|
+
id: 'park-123',
|
|
29
|
+
name: 'Example Park',
|
|
30
|
+
entityType: 'PARK',
|
|
31
|
+
timezone: 'America/New_York',
|
|
27
32
|
};
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
const liveData: LiveData = {
|
|
35
|
+
id: 'attraction-456',
|
|
36
|
+
status: 'OPERATING',
|
|
37
|
+
};
|
|
32
38
|
```
|
|
33
39
|
|
|
34
|
-
###
|
|
40
|
+
### Enums and conversion functions
|
|
35
41
|
|
|
36
42
|
```typescript
|
|
37
|
-
import {
|
|
38
|
-
import { resolve } from 'path';
|
|
43
|
+
import { EntityTypeEnum, StringToEntityType } from '@themeparks/typelib';
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// Native TypeScript enums
|
|
46
|
+
const type = EntityTypeEnum.ATTRACTION;
|
|
47
|
+
|
|
48
|
+
// Convert strings to enum values
|
|
49
|
+
const parsed = StringToEntityType('attraction');
|
|
44
50
|
```
|
|
45
51
|
|
|
46
52
|
### Runtime Schema Registry
|
|
@@ -55,38 +61,69 @@ Retrieve a registered schema.
|
|
|
55
61
|
import { registerTypeSchema, getTypeSchema } from '@themeparks/typelib';
|
|
56
62
|
|
|
57
63
|
// Schemas are automatically registered when importing types
|
|
58
|
-
import {
|
|
64
|
+
import { Entity } from '@themeparks/typelib';
|
|
59
65
|
|
|
60
66
|
// Access the schema at runtime
|
|
61
|
-
const schema = getTypeSchema('
|
|
67
|
+
const schema = getTypeSchema('Entity');
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Deterministic Object Hashing
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { hashObject } from '@themeparks/typelib/hash';
|
|
74
|
+
|
|
75
|
+
// Returns a 64-character hex SHA-256 hash
|
|
76
|
+
const hash = hashObject({ name: 'Example', id: 123 });
|
|
77
|
+
|
|
78
|
+
// Deterministic — key order doesn't matter
|
|
79
|
+
hashObject({ b: 2, a: 1 }) === hashObject({ a: 1, b: 2 }); // true
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Generating types from schemas
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { generateTypes } from '@themeparks/typelib/generate';
|
|
86
|
+
import { resolve } from 'path';
|
|
87
|
+
|
|
88
|
+
await generateTypes({
|
|
89
|
+
schemaDirs: [resolve('./typesrc')],
|
|
90
|
+
outputDir: './src/types'
|
|
91
|
+
});
|
|
62
92
|
```
|
|
63
93
|
|
|
64
94
|
## Schema Format
|
|
65
95
|
|
|
66
|
-
Schemas follow JSON Schema Draft 7 specification:
|
|
96
|
+
Schemas follow JSON Schema Draft 7 specification. Each file defines top-level types as properties:
|
|
67
97
|
|
|
68
98
|
```json
|
|
69
99
|
{
|
|
70
100
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
71
|
-
"title": "
|
|
101
|
+
"title": "Entities",
|
|
72
102
|
"type": "object",
|
|
73
103
|
"properties": {
|
|
74
|
-
"
|
|
104
|
+
"LanguageCode": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"enum": ["en", "en-gb", "de", "fr", "es", "ja", "ko", "zh"],
|
|
107
|
+
"description": "Supported language codes for ThemeParks.wiki"
|
|
108
|
+
},
|
|
109
|
+
"Entity": {
|
|
75
110
|
"type": "object",
|
|
76
|
-
"required": ["id", "name"],
|
|
111
|
+
"required": ["id", "name", "entityType", "timezone"],
|
|
77
112
|
"properties": {
|
|
78
113
|
"id": {
|
|
79
114
|
"type": "string",
|
|
80
|
-
"description": "Unique identifier"
|
|
115
|
+
"description": "Unique identifier for this entity"
|
|
81
116
|
},
|
|
82
117
|
"name": {
|
|
83
|
-
"
|
|
84
|
-
"
|
|
118
|
+
"description": "Entity name",
|
|
119
|
+
"$ref": "#/properties/LocalisedString"
|
|
85
120
|
},
|
|
86
|
-
"
|
|
121
|
+
"entityType": {
|
|
122
|
+
"$ref": "#/properties/EntityType"
|
|
123
|
+
},
|
|
124
|
+
"timezone": {
|
|
87
125
|
"type": "string",
|
|
88
|
-
"
|
|
89
|
-
"description": "Status of the item"
|
|
126
|
+
"description": "Timezone of this entity (IANA)"
|
|
90
127
|
}
|
|
91
128
|
}
|
|
92
129
|
}
|
|
@@ -94,38 +131,29 @@ Schemas follow JSON Schema Draft 7 specification:
|
|
|
94
131
|
}
|
|
95
132
|
```
|
|
96
133
|
|
|
134
|
+
Types can reference each other within the same file using `$ref`, and the generator resolves cross-file references automatically.
|
|
135
|
+
|
|
97
136
|
## Generated Output
|
|
98
137
|
|
|
99
138
|
The generator creates:
|
|
100
|
-
- **Type definitions**
|
|
101
|
-
- **Enum types**
|
|
102
|
-
- **Runtime registration**
|
|
103
|
-
- **Re-export index**
|
|
139
|
+
- **Type definitions** — TypeScript interfaces and types
|
|
140
|
+
- **Enum types** — Native TypeScript enums with `StringTo*` conversion functions
|
|
141
|
+
- **Runtime registration** — Automatic schema registration via `registerTypeSchema`
|
|
142
|
+
- **Re-export index** — Convenient imports from a single file
|
|
104
143
|
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
// Generated: my-types.types.ts
|
|
108
|
-
|
|
109
|
-
export interface MyType {
|
|
110
|
-
/** Unique identifier */
|
|
111
|
-
id: string;
|
|
112
|
-
/** Display name */
|
|
113
|
-
name: string;
|
|
114
|
-
/** Status of the item */
|
|
115
|
-
status?: 'active' | 'inactive' | 'pending';
|
|
116
|
-
}
|
|
144
|
+
## Package Exports
|
|
117
145
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"pending" = 'pending'
|
|
122
|
-
}
|
|
146
|
+
- `@themeparks/typelib` — Types, enums, and schema registry functions
|
|
147
|
+
- `@themeparks/typelib/generate` — Type generation from JSON schemas
|
|
148
|
+
- `@themeparks/typelib/hash` — Deterministic SHA-256 object hashing
|
|
123
149
|
|
|
124
|
-
|
|
125
|
-
registerTypeSchema("MyType", { /* schema */ });
|
|
126
|
-
```
|
|
150
|
+
## Publishing
|
|
127
151
|
|
|
128
|
-
|
|
152
|
+
Builds and publishes to npm automatically via `prepublishOnly`:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Bump version in package.json, then:
|
|
156
|
+
npm publish --access public
|
|
157
|
+
```
|
|
129
158
|
|
|
130
|
-
|
|
131
|
-
- `@themeparks/typelib/generate` - Type generation functionality
|
|
159
|
+
This runs `npm run build` (which regenerates types from schemas, then compiles TypeScript) before publishing. The published package includes `dist/`, `typesrc/`, and `src/`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hash.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { hashObject } from '../hash.js';
|
|
3
|
+
// ── Helper ─────────────────────────────────────────────────────────────────
|
|
4
|
+
/** Assert that two values hash the same */
|
|
5
|
+
function same(a, b) {
|
|
6
|
+
expect(hashObject(a)).toBe(hashObject(b));
|
|
7
|
+
}
|
|
8
|
+
/** Assert that two values hash differently */
|
|
9
|
+
function different(a, b) {
|
|
10
|
+
expect(hashObject(a)).not.toBe(hashObject(b));
|
|
11
|
+
}
|
|
12
|
+
/** Assert that hashObject throws a TypeError with a message matching the pattern */
|
|
13
|
+
function throwsTypeError(value, pattern) {
|
|
14
|
+
expect(() => hashObject(value)).toThrow(TypeError);
|
|
15
|
+
expect(() => hashObject(value)).toThrow(pattern);
|
|
16
|
+
}
|
|
17
|
+
// ── Output format ──────────────────────────────────────────────────────────
|
|
18
|
+
describe('output format', () => {
|
|
19
|
+
it('returns a 64-character lowercase hex string', () => {
|
|
20
|
+
expect(hashObject({ x: 1 })).toMatch(/^[0-9a-f]{64}$/);
|
|
21
|
+
});
|
|
22
|
+
it('is deterministic — same input always produces same output', () => {
|
|
23
|
+
const obj = { a: 1, b: [2, 3], c: { d: 'hello' } };
|
|
24
|
+
expect(hashObject(obj)).toBe(hashObject(obj));
|
|
25
|
+
expect(hashObject(obj)).toBe(hashObject({ a: 1, b: [2, 3], c: { d: 'hello' } }));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
// ── Supported types ────────────────────────────────────────────────────────
|
|
29
|
+
describe('supported scalar types', () => {
|
|
30
|
+
it('hashes null', () => {
|
|
31
|
+
const h = hashObject(null);
|
|
32
|
+
expect(h).toMatch(/^[0-9a-f]{64}$/);
|
|
33
|
+
});
|
|
34
|
+
it('hashes strings', () => {
|
|
35
|
+
different('hello', 'world');
|
|
36
|
+
same('hello', 'hello');
|
|
37
|
+
});
|
|
38
|
+
it('hashes numbers', () => {
|
|
39
|
+
different(1, 2);
|
|
40
|
+
same(42, 42);
|
|
41
|
+
});
|
|
42
|
+
it('hashes booleans', () => {
|
|
43
|
+
different(true, false);
|
|
44
|
+
same(true, true);
|
|
45
|
+
});
|
|
46
|
+
it('differentiates between types with similar string forms', () => {
|
|
47
|
+
different(null, 'null');
|
|
48
|
+
different(0, false);
|
|
49
|
+
different(0, '0');
|
|
50
|
+
different(false, 'false');
|
|
51
|
+
different(1, '1');
|
|
52
|
+
different(null, ''); // null is fine; just checking null != ''
|
|
53
|
+
});
|
|
54
|
+
it('differentiates NaN from null — NaN throws', () => {
|
|
55
|
+
expect(() => hashObject(NaN)).toThrow(TypeError);
|
|
56
|
+
// null should still work fine
|
|
57
|
+
expect(() => hashObject(null)).not.toThrow();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('supported object types', () => {
|
|
61
|
+
it('hashes empty object', () => {
|
|
62
|
+
const h = hashObject({});
|
|
63
|
+
expect(h).toMatch(/^[0-9a-f]{64}$/);
|
|
64
|
+
});
|
|
65
|
+
it('hashes empty array', () => {
|
|
66
|
+
different([], {});
|
|
67
|
+
expect(hashObject([])).toMatch(/^[0-9a-f]{64}$/);
|
|
68
|
+
});
|
|
69
|
+
it('hashes plain objects', () => {
|
|
70
|
+
same({ a: 1 }, { a: 1 });
|
|
71
|
+
different({ a: 1 }, { a: 2 });
|
|
72
|
+
different({ a: 1 }, { b: 1 });
|
|
73
|
+
});
|
|
74
|
+
it('hashes arrays (order is preserved)', () => {
|
|
75
|
+
same([1, 2, 3], [1, 2, 3]);
|
|
76
|
+
different([1, 2, 3], [3, 2, 1]);
|
|
77
|
+
});
|
|
78
|
+
it('hashes nested objects', () => {
|
|
79
|
+
same({ a: { b: { c: 42 } } }, { a: { b: { c: 42 } } });
|
|
80
|
+
different({ a: { b: 1 } }, { a: { b: 2 } });
|
|
81
|
+
});
|
|
82
|
+
it('hashes mixed nested structures', () => {
|
|
83
|
+
same({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] });
|
|
84
|
+
different({ a: [1, { b: 2 }] }, { a: [1, { b: 3 }] });
|
|
85
|
+
});
|
|
86
|
+
it('allows the same object reference in sibling array positions', () => {
|
|
87
|
+
const shared = { x: 1 };
|
|
88
|
+
expect(() => hashObject([shared, shared])).not.toThrow();
|
|
89
|
+
same([shared, shared], [{ x: 1 }, { x: 1 }]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
// ── Key ordering ───────────────────────────────────────────────────────────
|
|
93
|
+
describe('key ordering', () => {
|
|
94
|
+
it('produces the same hash regardless of key insertion order (top level)', () => {
|
|
95
|
+
same({ a: 1, b: 2 }, { b: 2, a: 1 });
|
|
96
|
+
});
|
|
97
|
+
it('produces the same hash for deeply nested objects with different key order', () => {
|
|
98
|
+
same({ x: { c: 3, a: 1, b: 2 } }, { x: { a: 1, b: 2, c: 3 } });
|
|
99
|
+
});
|
|
100
|
+
it('produces the same hash for objects inside arrays with different key order', () => {
|
|
101
|
+
same([{ z: 26, a: 1 }, { m: 13, b: 2 }], [{ a: 1, z: 26 }, { b: 2, m: 13 }]);
|
|
102
|
+
});
|
|
103
|
+
it('distinguishes objects with the same keys but different values', () => {
|
|
104
|
+
different({ a: 1, b: 2 }, { a: 2, b: 1 });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
// ── Unsupported types — must throw TypeError ───────────────────────────────
|
|
108
|
+
describe('unsupported types throw TypeError', () => {
|
|
109
|
+
it('throws on undefined (top-level)', () => {
|
|
110
|
+
throwsTypeError(undefined, /undefined/);
|
|
111
|
+
});
|
|
112
|
+
it('throws on undefined inside an object value', () => {
|
|
113
|
+
throwsTypeError({ a: undefined }, /undefined/);
|
|
114
|
+
});
|
|
115
|
+
it('throws on undefined inside a nested object', () => {
|
|
116
|
+
throwsTypeError({ a: { b: undefined } }, /undefined/);
|
|
117
|
+
});
|
|
118
|
+
it('throws on undefined inside an array', () => {
|
|
119
|
+
throwsTypeError([1, undefined, 3], /undefined/);
|
|
120
|
+
});
|
|
121
|
+
it('throws on Date', () => {
|
|
122
|
+
throwsTypeError(new Date(), /Date/);
|
|
123
|
+
});
|
|
124
|
+
it('throws on Date inside an object', () => {
|
|
125
|
+
throwsTypeError({ created: new Date() }, /Date/);
|
|
126
|
+
});
|
|
127
|
+
it('throws on Map', () => {
|
|
128
|
+
throwsTypeError(new Map([['a', 1]]), /Map/);
|
|
129
|
+
});
|
|
130
|
+
it('throws on Map inside an object', () => {
|
|
131
|
+
throwsTypeError({ data: new Map() }, /Map/);
|
|
132
|
+
});
|
|
133
|
+
it('throws on Set', () => {
|
|
134
|
+
throwsTypeError(new Set([1, 2, 3]), /Set/);
|
|
135
|
+
});
|
|
136
|
+
it('throws on Set inside an object', () => {
|
|
137
|
+
throwsTypeError({ tags: new Set(['a', 'b']) }, /Set/);
|
|
138
|
+
});
|
|
139
|
+
it('throws on Buffer', () => {
|
|
140
|
+
throwsTypeError(Buffer.from('hello'), /TypedArray|Buffer/);
|
|
141
|
+
});
|
|
142
|
+
it('throws on Uint8Array', () => {
|
|
143
|
+
throwsTypeError(new Uint8Array([1, 2, 3]), /TypedArray|Buffer/);
|
|
144
|
+
});
|
|
145
|
+
it('throws on Int32Array', () => {
|
|
146
|
+
throwsTypeError(new Int32Array([1, 2, 3]), /TypedArray|Buffer/);
|
|
147
|
+
});
|
|
148
|
+
it('throws on Float64Array', () => {
|
|
149
|
+
throwsTypeError(new Float64Array([1.0, 2.0]), /TypedArray|Buffer/);
|
|
150
|
+
});
|
|
151
|
+
it('throws on function', () => {
|
|
152
|
+
throwsTypeError(() => 42, /function/);
|
|
153
|
+
});
|
|
154
|
+
it('throws on function inside an object', () => {
|
|
155
|
+
throwsTypeError({ fn: () => { } }, /function/);
|
|
156
|
+
});
|
|
157
|
+
it('throws on BigInt', () => {
|
|
158
|
+
throwsTypeError(BigInt(9007199254740993), /BigInt/);
|
|
159
|
+
});
|
|
160
|
+
it('throws on BigInt inside an object', () => {
|
|
161
|
+
throwsTypeError({ id: BigInt(1) }, /BigInt/);
|
|
162
|
+
});
|
|
163
|
+
it('throws on Symbol', () => {
|
|
164
|
+
throwsTypeError(Symbol('test'), /Symbol/);
|
|
165
|
+
});
|
|
166
|
+
it('throws on circular reference (object)', () => {
|
|
167
|
+
const obj = { a: 1 };
|
|
168
|
+
obj.self = obj;
|
|
169
|
+
throwsTypeError(obj, /circular/);
|
|
170
|
+
});
|
|
171
|
+
it('throws on circular reference (array)', () => {
|
|
172
|
+
const arr = [1, 2];
|
|
173
|
+
arr.push(arr);
|
|
174
|
+
throwsTypeError(arr, /circular/);
|
|
175
|
+
});
|
|
176
|
+
it('throws on circular reference (nested)', () => {
|
|
177
|
+
const inner = { x: 1 };
|
|
178
|
+
const outer = { a: inner, b: inner }; // shared ref is fine (not circular)
|
|
179
|
+
outer.a.parent = outer; // this makes it circular
|
|
180
|
+
throwsTypeError(outer, /circular/);
|
|
181
|
+
});
|
|
182
|
+
it('does NOT throw for shared (non-circular) references', () => {
|
|
183
|
+
// The same object appearing in two sibling branches is fine — it is not circular
|
|
184
|
+
const shared = { x: 1 };
|
|
185
|
+
expect(() => hashObject({ a: shared, b: shared })).not.toThrow();
|
|
186
|
+
// And both branches must hash the same
|
|
187
|
+
same({ a: shared, b: shared }, { a: { x: 1 }, b: { x: 1 } });
|
|
188
|
+
});
|
|
189
|
+
it('throws on NaN', () => {
|
|
190
|
+
throwsTypeError(NaN, /non-finite/);
|
|
191
|
+
});
|
|
192
|
+
it('throws on Infinity', () => {
|
|
193
|
+
throwsTypeError(Infinity, /non-finite/);
|
|
194
|
+
});
|
|
195
|
+
it('throws on -Infinity', () => {
|
|
196
|
+
throwsTypeError(-Infinity, /non-finite/);
|
|
197
|
+
});
|
|
198
|
+
it('throws on NaN inside an object', () => {
|
|
199
|
+
throwsTypeError({ value: NaN }, /non-finite/);
|
|
200
|
+
});
|
|
201
|
+
it('throws on Symbol inside an object', () => {
|
|
202
|
+
throwsTypeError({ s: Symbol('test') }, /Symbol/);
|
|
203
|
+
});
|
|
204
|
+
it('throws on Buffer inside an object', () => {
|
|
205
|
+
throwsTypeError({ data: Buffer.from('hello') }, /TypedArray|Buffer/);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate_types.d.ts","sourceRoot":"","sources":["../src/generate_types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generate_types.d.ts","sourceRoot":"","sources":["../src/generate_types.ts"],"names":[],"mappings":"AA8XA;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,EAChC,UAAgC,EAChC,SAA8B,EAC9B,kBAA0C,EAC7C;;;;CAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CrB"}
|
package/dist/generate_types.js
CHANGED
|
@@ -66,7 +66,10 @@ function resolveReference(ref, schema, registry, tracker) {
|
|
|
66
66
|
// If still not found, check registry
|
|
67
67
|
const registryType = registry[segment];
|
|
68
68
|
if (registryType) {
|
|
69
|
-
tracker.imports.
|
|
69
|
+
if (!tracker.imports.has(registryType.sourceFile)) {
|
|
70
|
+
tracker.imports.set(registryType.sourceFile, new Set());
|
|
71
|
+
}
|
|
72
|
+
tracker.imports.get(registryType.sourceFile).add(segment);
|
|
70
73
|
tracker.referencePath.pop();
|
|
71
74
|
return { typeName: segment, sourceFile: registryType.sourceFile };
|
|
72
75
|
}
|
|
@@ -291,7 +294,7 @@ async function generateTypeFile(schemaPath, registry, outputDir, typeRegistryImp
|
|
|
291
294
|
const currentFile = schemaPath.split(/[\/\\]/).pop().replace('.json', '');
|
|
292
295
|
const imports = Array.from(tracker.imports.entries())
|
|
293
296
|
.filter(([file]) => file !== currentFile)
|
|
294
|
-
.map(([file,
|
|
297
|
+
.map(([file, typeNames]) => `import { ${Array.from(typeNames).join(', ')} } from './${file}.types.js';`);
|
|
295
298
|
if (imports.length > 0) {
|
|
296
299
|
// Replace header with imports and re-add header once
|
|
297
300
|
output = output.replace(FILE_HEADER, FILE_HEADER + imports.join('\n') + '\n\n');
|
|
@@ -364,6 +367,6 @@ export async function generateTypes({ schemaDirs = DEFAULT_SCHEMA_DIRS, outputDi
|
|
|
364
367
|
}
|
|
365
368
|
catch (err) {
|
|
366
369
|
error('Error generating types:', err);
|
|
367
|
-
|
|
370
|
+
throw err;
|
|
368
371
|
}
|
|
369
372
|
}
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
* Return a deterministic 64-char hex SHA-256 hash of the given value.
|
|
23
|
+
*
|
|
24
|
+
* Throws TypeError if the value contains any unsupported type.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hashObject(value: unknown): string;
|
|
27
|
+
//# sourceMappingURL=hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAoGH;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKjD"}
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
/**
|
|
23
|
+
* Recursively normalise a value for hashing:
|
|
24
|
+
* - Sort object keys alphabetically
|
|
25
|
+
* - Throw TypeError on any unsupported type
|
|
26
|
+
* - Track seen objects to detect circular references
|
|
27
|
+
*/
|
|
28
|
+
function normalise(value, seen) {
|
|
29
|
+
// null is fine
|
|
30
|
+
if (value === null)
|
|
31
|
+
return null;
|
|
32
|
+
// Primitives
|
|
33
|
+
if (typeof value === 'string')
|
|
34
|
+
return value;
|
|
35
|
+
if (typeof value === 'number') {
|
|
36
|
+
if (!Number.isFinite(value)) {
|
|
37
|
+
throw new TypeError(`hashObject: non-finite number is not supported (got: ${value})`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === 'boolean')
|
|
42
|
+
return value;
|
|
43
|
+
// Explicitly unsupported primitives
|
|
44
|
+
if (value === undefined) {
|
|
45
|
+
throw new TypeError('hashObject: undefined is not supported — use null or omit the key instead');
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === 'bigint') {
|
|
48
|
+
throw new TypeError(`hashObject: BigInt is not supported — convert to string or number first (got: ${value}n)`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'symbol') {
|
|
51
|
+
throw new TypeError(`hashObject: Symbol is not supported (got: ${String(value)})`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === 'function') {
|
|
54
|
+
throw new TypeError(`hashObject: function is not supported (got: ${value.name || 'anonymous'})`);
|
|
55
|
+
}
|
|
56
|
+
// Object types
|
|
57
|
+
if (typeof value === 'object') {
|
|
58
|
+
// Circular reference detection
|
|
59
|
+
if (seen.has(value)) {
|
|
60
|
+
throw new TypeError('hashObject: circular reference detected');
|
|
61
|
+
}
|
|
62
|
+
// Explicitly unsupported object types — check before Array/plain-object
|
|
63
|
+
if (value instanceof Date) {
|
|
64
|
+
throw new TypeError(`hashObject: Date is not supported — convert to ISO string first (got: ${value.toISOString()})`);
|
|
65
|
+
}
|
|
66
|
+
if (value instanceof Map) {
|
|
67
|
+
throw new TypeError('hashObject: Map is not supported — convert to a plain object first');
|
|
68
|
+
}
|
|
69
|
+
if (value instanceof Set) {
|
|
70
|
+
throw new TypeError('hashObject: Set is not supported — convert to an Array first');
|
|
71
|
+
}
|
|
72
|
+
if (ArrayBuffer.isView(value)) {
|
|
73
|
+
throw new TypeError(`hashObject: TypedArray/Buffer is not supported — convert to a plain array or base64 string first (got: ${value.constructor.name})`);
|
|
74
|
+
}
|
|
75
|
+
seen.add(value);
|
|
76
|
+
let result;
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
result = value.map((item) => normalise(item, seen));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Plain object — sort keys for determinism
|
|
82
|
+
const sorted = {};
|
|
83
|
+
for (const key of Object.keys(value).sort()) {
|
|
84
|
+
sorted[key] = normalise(value[key], seen);
|
|
85
|
+
}
|
|
86
|
+
result = sorted;
|
|
87
|
+
}
|
|
88
|
+
seen.delete(value); // allow the same object to appear in sibling branches
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
// Should be unreachable, but TypeScript needs the exhaustive case
|
|
92
|
+
throw new TypeError(`hashObject: unsupported type: ${typeof value}`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Return a deterministic 64-char hex SHA-256 hash of the given value.
|
|
96
|
+
*
|
|
97
|
+
* Throws TypeError if the value contains any unsupported type.
|
|
98
|
+
*/
|
|
99
|
+
export function hashObject(value) {
|
|
100
|
+
const normalised = normalise(value, new Set());
|
|
101
|
+
return createHash('sha256')
|
|
102
|
+
.update(JSON.stringify(normalised))
|
|
103
|
+
.digest('hex');
|
|
104
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACvE,cAAc,kBAAkB,CAAC
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACvE,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
// Main stub. Expose type register functions and all generated types.
|
|
2
2
|
export { registerTypeSchema, getTypeSchema } from './type_register.js';
|
|
3
3
|
export * from './types/index.js';
|
|
4
|
-
// export the generate types function, for use in build scripts or other projects
|
|
5
|
-
export { generateTypes } from './generate_types.js';
|
package/dist/type_register.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Register
|
|
3
|
-
* @param name The type name (e.g. "
|
|
4
|
-
* @param schema The
|
|
2
|
+
* Register a JSON schema for runtime type checking
|
|
3
|
+
* @param name The type name (e.g. "Entity")
|
|
4
|
+
* @param schema The JSON schema definition
|
|
5
5
|
*/
|
|
6
6
|
export declare function registerTypeSchema(name: string, schema: any): void;
|
|
7
7
|
/**
|
|
8
|
-
* Retrieve the
|
|
8
|
+
* Retrieve the JSON schema for a registered type
|
|
9
9
|
* @param name The type name
|
|
10
|
-
* @returns The
|
|
10
|
+
* @returns The JSON schema or undefined if not found
|
|
11
11
|
*/
|
|
12
12
|
export declare function getTypeSchema(name: string): any | undefined;
|
|
13
13
|
//# sourceMappingURL=type_register.d.ts.map
|
package/dist/type_register.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// Store
|
|
1
|
+
// Store JSON schemas for runtime type information
|
|
2
2
|
const typeMetadataRegistry = new Map();
|
|
3
3
|
/**
|
|
4
|
-
* Register
|
|
5
|
-
* @param name The type name (e.g. "
|
|
6
|
-
* @param schema The
|
|
4
|
+
* Register a JSON schema for runtime type checking
|
|
5
|
+
* @param name The type name (e.g. "Entity")
|
|
6
|
+
* @param schema The JSON schema definition
|
|
7
7
|
*/
|
|
8
8
|
export function registerTypeSchema(name, schema) {
|
|
9
9
|
typeMetadataRegistry.set(name, schema);
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Retrieve the
|
|
12
|
+
* Retrieve the JSON schema for a registered type
|
|
13
13
|
* @param name The type name
|
|
14
|
-
* @returns The
|
|
14
|
+
* @returns The JSON schema or undefined if not found
|
|
15
15
|
*/
|
|
16
16
|
export function getTypeSchema(name) {
|
|
17
17
|
return typeMetadataRegistry.get(name);
|