cddl2ts 0.7.1 → 0.7.2

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/build/index.js CHANGED
@@ -57,6 +57,21 @@ export function transform(assignments, options) {
57
57
  }
58
58
  return print(ast).code;
59
59
  }
60
+ function getAssignmentComments(assignment) {
61
+ return assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
62
+ }
63
+ function exportWithComments(declaration) {
64
+ const expr = b.exportDeclaration(false, declaration);
65
+ expr.comments = declaration.comments;
66
+ declaration.comments = [];
67
+ return expr;
68
+ }
69
+ function isExtensibleRecordProperty(prop) {
70
+ return !isUnNamedProperty(prop) &&
71
+ prop.Occurrence.m === Infinity &&
72
+ !prop.HasCut &&
73
+ RECORD_KEY_TYPES.has(prop.Name);
74
+ }
60
75
  function parseAssignment(assignment) {
61
76
  if (isVariable(assignment)) {
62
77
  const propType = Array.isArray(assignment.PropertyType)
@@ -72,8 +87,8 @@ function parseAssignment(assignment) {
72
87
  typeParameters = b.tsUnionType(propType.map(parseUnionType));
73
88
  }
74
89
  const expr = b.tsTypeAliasDeclaration(id, typeParameters);
75
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
76
- return b.exportDeclaration(false, expr);
90
+ expr.comments = getAssignmentComments(assignment);
91
+ return exportWithComments(expr);
77
92
  }
78
93
  if (isGroup(assignment)) {
79
94
  const id = b.identifier(pascalCase(assignment.Name));
@@ -154,8 +169,8 @@ function parseAssignment(assignment) {
154
169
  value = b.tsIntersectionType(intersections);
155
170
  }
156
171
  const expr = b.tsTypeAliasDeclaration(id, value);
157
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
158
- return b.exportDeclaration(false, expr);
172
+ expr.comments = getAssignmentComments(assignment);
173
+ return exportWithComments(expr);
159
174
  }
160
175
  const props = properties;
161
176
  /**
@@ -167,8 +182,8 @@ function parseAssignment(assignment) {
167
182
  if (propType.length === 1 && RECORD_KEY_TYPES.has(prop.Name)) {
168
183
  const value = parseUnionType(assignment);
169
184
  const expr = b.tsTypeAliasDeclaration(id, value);
170
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
171
- return b.exportDeclaration(false, expr);
185
+ expr.comments = getAssignmentComments(assignment);
186
+ return exportWithComments(expr);
172
187
  }
173
188
  }
174
189
  // Check if extended interfaces are likely unions or conflicting types
@@ -385,14 +400,14 @@ function parseAssignment(assignment) {
385
400
  value = b.tsIntersectionType(intersections);
386
401
  }
387
402
  const expr = b.tsTypeAliasDeclaration(id, value);
388
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
389
- return b.exportDeclaration(false, expr);
403
+ expr.comments = getAssignmentComments(assignment);
404
+ return exportWithComments(expr);
390
405
  }
391
406
  // Fallback to interface if no mixins (pure object)
392
407
  const objectType = parseObjectType(props);
393
408
  const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType));
394
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
395
- return b.exportDeclaration(false, expr);
409
+ expr.comments = getAssignmentComments(assignment);
410
+ return exportWithComments(expr);
396
411
  }
397
412
  if (isCDDLArray(assignment)) {
398
413
  const id = b.identifier(pascalCase(assignment.Name));
@@ -408,8 +423,8 @@ function parseAssignment(assignment) {
408
423
  });
409
424
  const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)));
410
425
  const expr = b.tsTypeAliasDeclaration(id, value);
411
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
412
- return b.exportDeclaration(false, expr);
426
+ expr.comments = getAssignmentComments(assignment);
427
+ return exportWithComments(expr);
413
428
  }
414
429
  // Standard array
415
430
  const firstType = assignmentValues.Type;
@@ -422,8 +437,8 @@ function parseAssignment(assignment) {
422
437
  ? obj[0]
423
438
  : b.tsParenthesizedType(b.tsUnionType(obj)));
424
439
  const expr = b.tsTypeAliasDeclaration(id, value);
425
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
426
- return b.exportDeclaration(false, expr);
440
+ expr.comments = getAssignmentComments(assignment);
441
+ return exportWithComments(expr);
427
442
  }
428
443
  throw new Error(`Unknown assignment type "${assignment.Type}"`);
429
444
  }
@@ -444,9 +459,22 @@ function parseObjectType(props) {
444
459
  if (isUnNamedProperty(prop)) {
445
460
  continue;
446
461
  }
447
- const id = b.identifier(camelcase(prop.Name));
448
462
  const cddlType = Array.isArray(prop.Type) ? prop.Type : [prop.Type];
449
463
  const comments = prop.Comments.map((c) => ` ${c.Content}`);
464
+ if (isExtensibleRecordProperty(prop)) {
465
+ const keyIdentifier = b.identifier('key');
466
+ keyIdentifier.typeAnnotation = b.tsTypeAnnotation(NATIVE_TYPES[prop.Name]);
467
+ const indexSignature = b.tsIndexSignature([keyIdentifier], b.tsTypeAnnotation(b.tsUnionType([
468
+ ...cddlType.map((t) => parseUnionType(t)),
469
+ b.tsUndefinedKeyword()
470
+ ])));
471
+ indexSignature.comments = comments.length
472
+ ? [b.commentBlock(`*\n *${comments.join('\n *')}\n `)]
473
+ : [];
474
+ propItems.push(indexSignature);
475
+ continue;
476
+ }
477
+ const id = b.identifier(camelcase(prop.Name));
450
478
  if (prop.Operator && prop.Operator.Type === 'default') {
451
479
  const defaultValue = parseDefaultValue(prop.Operator);
452
480
  defaultValue && comments.length && comments.push(''); // add empty line if we have previous comments
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cddl2ts",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "A Node.js package that can generate a TypeScript definition based on a CDDL file",
5
5
  "author": "Christian Bromann <mail@bromann.dev>",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  "camelcase": "^9.0.0",
33
33
  "recast": "^0.23.11",
34
34
  "yargs": "^18.0.0",
35
- "cddl": "0.19.1"
35
+ "cddl": "0.19.2"
36
36
  },
37
37
  "scripts": {
38
38
  "release": "release-it --config .release-it.ts --VV",
package/src/index.ts CHANGED
@@ -90,6 +90,27 @@ export function transform (assignments: Assignment[], options?: TransformOptions
90
90
  return print(ast).code
91
91
  }
92
92
 
93
+ function getAssignmentComments (assignment: Assignment) {
94
+ return assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
95
+ }
96
+
97
+ function exportWithComments (
98
+ declaration: types.namedTypes.TSTypeAliasDeclaration
99
+ | types.namedTypes.TSInterfaceDeclaration
100
+ ) {
101
+ const expr = b.exportDeclaration(false, declaration)
102
+ expr.comments = declaration.comments
103
+ declaration.comments = []
104
+ return expr
105
+ }
106
+
107
+ function isExtensibleRecordProperty (prop: Property) {
108
+ return !isUnNamedProperty(prop) &&
109
+ prop.Occurrence.m === Infinity &&
110
+ !prop.HasCut &&
111
+ RECORD_KEY_TYPES.has(prop.Name)
112
+ }
113
+
93
114
  function parseAssignment (assignment: Assignment) {
94
115
  if (isVariable(assignment)) {
95
116
  const propType = Array.isArray(assignment.PropertyType)
@@ -107,8 +128,8 @@ function parseAssignment (assignment: Assignment) {
107
128
  }
108
129
 
109
130
  const expr = b.tsTypeAliasDeclaration(id, typeParameters)
110
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
111
- return b.exportDeclaration(false, expr)
131
+ expr.comments = getAssignmentComments(assignment)
132
+ return exportWithComments(expr)
112
133
  }
113
134
 
114
135
  if (isGroup(assignment)) {
@@ -201,8 +222,8 @@ function parseAssignment (assignment: Assignment) {
201
222
  }
202
223
 
203
224
  const expr = b.tsTypeAliasDeclaration(id, value)
204
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
205
- return b.exportDeclaration(false, expr)
225
+ expr.comments = getAssignmentComments(assignment)
226
+ return exportWithComments(expr)
206
227
  }
207
228
 
208
229
  const props = properties as Property[]
@@ -216,8 +237,8 @@ function parseAssignment (assignment: Assignment) {
216
237
  if (propType.length === 1 && RECORD_KEY_TYPES.has(prop.Name)) {
217
238
  const value = parseUnionType(assignment)
218
239
  const expr = b.tsTypeAliasDeclaration(id, value)
219
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
220
- return b.exportDeclaration(false, expr)
240
+ expr.comments = getAssignmentComments(assignment)
241
+ return exportWithComments(expr)
221
242
  }
222
243
  }
223
244
 
@@ -431,16 +452,16 @@ function parseAssignment (assignment: Assignment) {
431
452
  }
432
453
 
433
454
  const expr = b.tsTypeAliasDeclaration(id, value)
434
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
435
- return b.exportDeclaration(false, expr)
455
+ expr.comments = getAssignmentComments(assignment)
456
+ return exportWithComments(expr)
436
457
  }
437
458
 
438
459
  // Fallback to interface if no mixins (pure object)
439
460
  const objectType = parseObjectType(props)
440
461
 
441
462
  const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType))
442
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
443
- return b.exportDeclaration(false, expr)
463
+ expr.comments = getAssignmentComments(assignment)
464
+ return exportWithComments(expr)
444
465
  }
445
466
 
446
467
  if (isCDDLArray(assignment)) {
@@ -459,8 +480,8 @@ function parseAssignment (assignment: Assignment) {
459
480
  })
460
481
  const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)))
461
482
  const expr = b.tsTypeAliasDeclaration(id, value)
462
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
463
- return b.exportDeclaration(false, expr)
483
+ expr.comments = getAssignmentComments(assignment)
484
+ return exportWithComments(expr)
464
485
  }
465
486
 
466
487
  // Standard array
@@ -477,8 +498,8 @@ function parseAssignment (assignment: Assignment) {
477
498
  : b.tsParenthesizedType(b.tsUnionType(obj))
478
499
  )
479
500
  const expr = b.tsTypeAliasDeclaration(id, value)
480
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
481
- return b.exportDeclaration(false, expr)
501
+ expr.comments = getAssignmentComments(assignment)
502
+ return exportWithComments(expr)
482
503
  }
483
504
 
484
505
  throw new Error(`Unknown assignment type "${(assignment as any).Type}"`)
@@ -502,10 +523,31 @@ function parseObjectType (props: Property[]): ObjectBody {
502
523
  continue
503
524
  }
504
525
 
505
- const id = b.identifier(camelcase(prop.Name))
506
526
  const cddlType: PropertyType[] = Array.isArray(prop.Type) ? prop.Type : [prop.Type]
507
527
  const comments: string[] = prop.Comments.map((c) => ` ${c.Content}`)
508
528
 
529
+ if (isExtensibleRecordProperty(prop)) {
530
+ const keyIdentifier = b.identifier('key')
531
+ keyIdentifier.typeAnnotation = b.tsTypeAnnotation(NATIVE_TYPES[prop.Name])
532
+
533
+ const indexSignature = b.tsIndexSignature(
534
+ [keyIdentifier],
535
+ b.tsTypeAnnotation(
536
+ b.tsUnionType([
537
+ ...cddlType.map((t) => parseUnionType(t)),
538
+ b.tsUndefinedKeyword()
539
+ ])
540
+ )
541
+ )
542
+ indexSignature.comments = comments.length
543
+ ? [b.commentBlock(`*\n *${comments.join('\n *')}\n `)]
544
+ : []
545
+ propItems.push(indexSignature)
546
+ continue
547
+ }
548
+
549
+ const id = b.identifier(camelcase(prop.Name))
550
+
509
551
  if (prop.Operator && prop.Operator.Type === 'default') {
510
552
  const defaultValue = parseDefaultValue(prop.Operator)
511
553
  defaultValue && comments.length && comments.push('') // add empty line if we have previous comments
@@ -0,0 +1,16 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`extensible metadata > should render extensible metadata as an interface with an index signature 1`] = `
4
+ "export type MetadataScalar = null | boolean | number | number | string;
5
+
6
+ export interface MessageMetadata {
7
+ provider?: string;
8
+ model?: string;
9
+ modelType?: string;
10
+ runId?: string;
11
+ threadId?: string;
12
+ systemFingerprint?: string;
13
+ serviceTier?: string;
14
+ [key: string]: MetadataScalar | undefined;
15
+ }"
16
+ `;
@@ -12,52 +12,52 @@ export type DirectProxyConfiguration = Extensible & {
12
12
  proxyType: "direct";
13
13
  };
14
14
 
15
- export // 1. Simple Group Choice
16
- type ManualProxyConfiguration = Extensible & {
15
+ // 1. Simple Group Choice
16
+ export type ManualProxyConfiguration = Extensible & {
17
17
  proxyType: "manual";
18
18
  };
19
19
 
20
- export // 2. Nested Group Choice
21
- type SimpleGroupChoice = Int | string;
20
+ // 2. Nested Group Choice
21
+ export type SimpleGroupChoice = Int | string;
22
22
 
23
- export // 3. Group Choice with Multiple Items (Sequence) - interpreted as tuple
24
- type NestedGroupChoice = (Int | Tstr);
23
+ // 3. Group Choice with Multiple Items (Sequence) - interpreted as tuple
24
+ export type NestedGroupChoice = (Int | Tstr);
25
25
 
26
- export // 4. Map Group Choice - interpreted as map (interface) union
27
- type SequenceGroupChoice = [number, string] | number;
26
+ // 4. Map Group Choice - interpreted as map (interface) union
27
+ export type SequenceGroupChoice = [number, string] | number;
28
28
 
29
- export // 5. Type Choice inside Group - should be value union
30
- type MapGroupChoice = {
29
+ // 5. Type Choice inside Group - should be value union
30
+ export type MapGroupChoice = {
31
31
  a: 1;
32
32
  } | {
33
33
  b: 2;
34
34
  };
35
35
 
36
- export // 6. Array with Group Choice - should be array of union?
37
- type TypeChoiceInsideGroup = number | string;
36
+ // 6. Array with Group Choice - should be array of union?
37
+ export type TypeChoiceInsideGroup = number | string;
38
38
 
39
- export // 7. Map with nested group (bare and parens) - should be object like
40
- type ArrayGroupChoice = Int | string[];
39
+ // 7. Map with nested group (bare and parens) - should be object like
40
+ export type ArrayGroupChoice = Int | string[];
41
41
 
42
42
  export type MapWithBareGroup = number;
43
43
 
44
- export // 8. Group wrapped in map with multiple properties
45
- type MapWithParensGroup = number;
44
+ // 8. Group wrapped in map with multiple properties
45
+ export type MapWithParensGroup = number;
46
46
 
47
- export // 9. Property type choice without operators
48
- type MapGroupWrap = ;
47
+ // 9. Property type choice without operators
48
+ export type MapGroupWrap = ;
49
49
 
50
- export // 10. Nested choice with group reference
51
- interface PropChoiceNoOp {
50
+ // 10. Nested choice with group reference
51
+ export interface PropChoiceNoOp {
52
52
  a: number | number;
53
53
  b: string;
54
54
  }
55
55
 
56
- export // 11. Complex type (group) followed by property without comma
57
- type NestedChoiceRef = number | string;
56
+ // 11. Complex type (group) followed by property without comma
57
+ export type NestedChoiceRef = number | string;
58
58
 
59
- export // 12. Multi-item group choice (verifies isChoice persistence)
60
- interface MapNoComma {
59
+ // 12. Multi-item group choice (verifies isChoice persistence)
60
+ export interface MapNoComma {
61
61
  a: number;
62
62
  b: string;
63
63
  }
@@ -95,8 +95,8 @@ export interface SomeGroup {
95
95
  export type ScriptListLocalValue = ScriptLocalValue[];
96
96
  export type ScriptMappingLocalValue = (ScriptLocalValue | ScriptLocalValue)[];
97
97
 
98
- export // some comments here
99
- type Extensible = Record<string, any>;",
98
+ // some comments here
99
+ export type Extensible = Record<string, any>;",
100
100
  ],
101
101
  ]
102
102
  `;
@@ -388,9 +388,9 @@ export interface BrowsingContextPrintParameters {
388
388
  shrinkToFit?: boolean;
389
389
  }
390
390
 
391
- export // Minimum size is 1pt x 1pt. Conversion follows from
391
+ // Minimum size is 1pt x 1pt. Conversion follows from
392
392
  // https://www.w3.org/TR/css3-values/#absolute-lengths
393
- interface BrowsingContextPrintMarginParameters {
393
+ export interface BrowsingContextPrintMarginParameters {
394
394
  /**
395
395
  * @default 1
396
396
  */
@@ -0,0 +1,49 @@
1
+ import url from 'node:url'
2
+ import path from 'node:path'
3
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
4
+
5
+ import cli from '../src/cli.js'
6
+
7
+ const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
8
+ const cddlFile = path.join(__dirname, '..', '..', '..', 'examples', 'commons', 'extensible_metadata.cddl')
9
+
10
+ vi.mock('../src/constants', () => ({
11
+ pkg: {
12
+ name: 'cddl2ts',
13
+ version: '0.0.0'
14
+ }
15
+ }))
16
+
17
+ describe('extensible metadata', () => {
18
+ let exitOrig = process.exit
19
+ let logOrig = console.log
20
+ let errorOrig = console.error
21
+
22
+ beforeEach(() => {
23
+ process.exit = vi.fn() as any
24
+ console.log = vi.fn()
25
+ console.error = vi.fn()
26
+ })
27
+
28
+ afterEach(() => {
29
+ process.exit = exitOrig
30
+ console.log = logOrig
31
+ console.error = errorOrig
32
+ })
33
+
34
+ it('should render extensible metadata as an interface with an index signature', async () => {
35
+ await cli([cddlFile])
36
+
37
+ expect(process.exit).not.toHaveBeenCalledWith(1)
38
+ expect(console.error).not.toHaveBeenCalled()
39
+
40
+ const output = vi.mocked(console.log).mock.calls.flat().join('\n')
41
+
42
+ expect(output).toContain('export interface MessageMetadata {')
43
+ expect(output).toContain('provider?: string;')
44
+ expect(output).toContain('modelType?: string;')
45
+ expect(output).toContain('[key: string]: MetadataScalar | undefined;')
46
+ expect(output).not.toContain('text?: MetadataScalar;')
47
+ expect(output).toMatchSnapshot()
48
+ })
49
+ })
@@ -39,8 +39,8 @@ describe('named group choice', () => {
39
39
 
40
40
  const output = vi.mocked(console.log).mock.calls.flat().join('\n')
41
41
 
42
- // The export keyword is separated from the type definition by comments
43
- expect(output).toMatch(/export\s+(\/\/.*\n)+\s*type Choice = OptionA \| OptionB/)
42
+ // Leading comments should render before the exported declaration.
43
+ expect(output).toMatch(/(\/\/.*\n)+export type Choice = OptionA \| OptionB/)
44
44
  expect(output).toContain('export interface OptionA {')
45
45
  expect(output).toContain('export interface OptionB {')
46
46
  })
@@ -14,11 +14,11 @@ import { transform } from '../src/index.js'
14
14
 
15
15
  const COMMENTS: Comment[] = []
16
16
 
17
- function comment (content: string): Comment {
17
+ function comment (content: string, leading = false): Comment {
18
18
  return {
19
19
  Type: 'comment',
20
20
  Content: content,
21
- Leading: false
21
+ Leading: leading
22
22
  }
23
23
  }
24
24
 
@@ -240,6 +240,40 @@ describe('transform edge cases', () => {
240
240
  expect(output).toContain('enabled?: boolean')
241
241
  })
242
242
 
243
+ it('should place leading comments before exported declarations', () => {
244
+ const output = transform([
245
+ variable('metadata-scalar', ['null', 'bool', 'int', 'float', 'text'], [
246
+ comment('Flat scalar value used by concise metadata bags.', true)
247
+ ])
248
+ ])
249
+
250
+ expect(output).toContain(`// Flat scalar value used by concise metadata bags.\nexport type MetadataScalar = null | boolean | number | number | string;`)
251
+ expect(output).not.toContain('export // Flat scalar value used by concise metadata bags.')
252
+ })
253
+
254
+ it('should emit extensible object properties as index signatures', () => {
255
+ const output = transform([
256
+ variable('metadata-scalar', ['null', 'bool', 'int', 'float', 'text']),
257
+ group('message-metadata', [
258
+ property('provider', 'text', {
259
+ Occurrence: { n: 0, m: 1 }
260
+ }),
261
+ property('model', 'text', {
262
+ Occurrence: { n: 0, m: 1 }
263
+ }),
264
+ property('text', groupRef('metadata-scalar'), {
265
+ Occurrence: { n: 0, m: Infinity }
266
+ })
267
+ ])
268
+ ])
269
+
270
+ expect(output).toContain('export interface MessageMetadata {')
271
+ expect(output).toContain('provider?: string;')
272
+ expect(output).toContain('model?: string;')
273
+ expect(output).toContain('[key: string]: MetadataScalar | undefined;')
274
+ expect(output).not.toContain('text?: MetadataScalar;')
275
+ })
276
+
243
277
  it('should throw clear errors for unsupported inputs', () => {
244
278
  expect(() => transform([
245
279
  variable('unknown-native', 'nope')