@xlr-lib/xlr-converters 0.1.1--canary.9.190

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.
Files changed (38) hide show
  1. package/bin/run +8 -0
  2. package/dist/cjs/index.cjs +1736 -0
  3. package/dist/cjs/index.cjs.map +1 -0
  4. package/dist/index.legacy-esm.js +1703 -0
  5. package/dist/index.mjs +1703 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +35 -0
  8. package/src/__tests__/__snapshots__/annotations.test.ts.snap +22 -0
  9. package/src/__tests__/__snapshots__/common-to-ts.test.ts.snap +50 -0
  10. package/src/__tests__/__snapshots__/export-to-ts.test.ts.snap +30 -0
  11. package/src/__tests__/__snapshots__/player.test.ts.snap +8024 -0
  12. package/src/__tests__/__snapshots__/ts-to-common.test.ts.snap +1657 -0
  13. package/src/__tests__/annotations.test.ts +40 -0
  14. package/src/__tests__/common-to-ts.test.ts +356 -0
  15. package/src/__tests__/documentation.test.ts +116 -0
  16. package/src/__tests__/export-to-ts.test.ts +68 -0
  17. package/src/__tests__/heritage-additional-properties.test.ts +119 -0
  18. package/src/__tests__/player.test.ts +866 -0
  19. package/src/__tests__/ts-to-common.test.ts +776 -0
  20. package/src/__tests__/ts-utils.test.ts +383 -0
  21. package/src/helpers/converter.ts +44 -0
  22. package/src/helpers/writeManifest.ts +45 -0
  23. package/src/index.ts +7 -0
  24. package/src/ts/annotations.ts +237 -0
  25. package/src/ts/documentation.ts +243 -0
  26. package/src/ts/ts-to-xlr.ts +1245 -0
  27. package/src/ts/ts-utils.ts +247 -0
  28. package/src/ts/xlr-to-ts.ts +636 -0
  29. package/src/types.ts +15 -0
  30. package/types/helpers/converter.d.ts +5 -0
  31. package/types/helpers/writeManifest.d.ts +3 -0
  32. package/types/index.d.ts +8 -0
  33. package/types/ts/annotations.d.ts +7 -0
  34. package/types/ts/documentation.d.ts +14 -0
  35. package/types/ts/ts-to-xlr.d.ts +56 -0
  36. package/types/ts/ts-utils.d.ts +68 -0
  37. package/types/ts/xlr-to-ts.d.ts +45 -0
  38. package/types/types.d.ts +8 -0
@@ -0,0 +1,40 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { setupTestEnv } from "@xlr-lib/test-utils";
3
+ import { decorateNode } from "../ts/annotations";
4
+
5
+ describe("Annotations", () => {
6
+ test("JSDoc comments to strings", () => {
7
+ const sc = `
8
+ /**
9
+ * An asset is the smallest unit of user interaction in a player view
10
+ * @example Example usage of interface Asset
11
+ * @see Asset for implementation details
12
+ * @default default value
13
+ */
14
+ export interface Asset<T extends string = string> {
15
+ id: string;
16
+ [key: string]: unknown;
17
+ }
18
+ `;
19
+
20
+ const { sf } = setupTestEnv(sc);
21
+ expect(decorateNode(sf.statements[0])).toMatchSnapshot();
22
+ });
23
+
24
+ test("JSDoc @meta", () => {
25
+ const sc = `
26
+ /**
27
+ * An asset is the smallest unit of user interaction in a player view
28
+ * @meta category:views
29
+ * @meta screenshot:/path/image.png
30
+ */
31
+ export interface Asset<T extends string = string> {
32
+ id: string;
33
+ [key: string]: unknown;
34
+ }
35
+ `;
36
+
37
+ const { sf } = setupTestEnv(sc);
38
+ expect(decorateNode(sf.statements[0])).toMatchSnapshot();
39
+ });
40
+ });
@@ -0,0 +1,356 @@
1
+ import { test, describe, expect, vi } from "vitest";
2
+ import type { NamedType } from "@xlr-lib/xlr";
3
+ import ts from "typescript";
4
+ import { TSWriter } from "../ts/xlr-to-ts";
5
+
6
+ describe("Type Exports", () => {
7
+ vi.setConfig({
8
+ testTimeout: 2 * 60 * 10000,
9
+ });
10
+
11
+ test("Basic Type Conversion", () => {
12
+ const xlr = {
13
+ name: "ActionAsset",
14
+ type: "object",
15
+ source: "common-to-ts.test.ts",
16
+ properties: {
17
+ value: {
18
+ required: false,
19
+ node: {
20
+ type: "string",
21
+ title: "ActionAsset.value",
22
+ description:
23
+ "The transition value of the action in the state machine",
24
+ },
25
+ },
26
+ label: {
27
+ required: false,
28
+ node: {
29
+ type: "ref",
30
+ ref: "AssetWrapper<AnyTextAsset>",
31
+ genericArguments: [
32
+ {
33
+ type: "ref",
34
+ ref: "AnyTextAsset",
35
+ },
36
+ ],
37
+ title: "ActionAsset.label",
38
+ description: "A text-like asset for the action's label",
39
+ },
40
+ },
41
+ exp: {
42
+ required: false,
43
+ node: {
44
+ type: "ref",
45
+ ref: "Expression",
46
+ title: "ActionAsset.exp",
47
+ description:
48
+ "An optional expression to execute before transitioning",
49
+ },
50
+ },
51
+ accessibility: {
52
+ required: false,
53
+ node: {
54
+ type: "string",
55
+ title: "ActionAsset.accessibility",
56
+ description:
57
+ "An optional string that describes the action for screen-readers",
58
+ },
59
+ },
60
+ metaData: {
61
+ required: false,
62
+ node: {
63
+ type: "object",
64
+ properties: {
65
+ beacon: {
66
+ required: false,
67
+ node: {
68
+ name: "BeaconDataType",
69
+ source:
70
+ "/private/var/tmp/_bazel_kreddy8/6fc13ccb395252816f0c23d8394e8532/sandbox/darwin-sandbox/181/execroot/player/node_modules/@player-ui/beacon-plugin/dist/index.d.ts",
71
+ type: "or",
72
+ or: [
73
+ {
74
+ type: "string",
75
+ title: "BeaconDataType",
76
+ },
77
+ {
78
+ type: "record",
79
+ keyType: {
80
+ type: "string",
81
+ },
82
+ valueType: {
83
+ type: "any",
84
+ },
85
+ title: "BeaconDataType",
86
+ },
87
+ ],
88
+ title: "ActionAsset.metaData.beacon",
89
+ description: "Additional data to beacon",
90
+ },
91
+ },
92
+ skipValidation: {
93
+ required: false,
94
+ node: {
95
+ type: "boolean",
96
+ title: "ActionAsset.metaData.skipValidation",
97
+ description:
98
+ "Force transition to the next view without checking for validation",
99
+ },
100
+ },
101
+ },
102
+ additionalProperties: false,
103
+ title: "ActionAsset.metaData",
104
+ description:
105
+ "Additional optional data to assist with the action interactions on the page",
106
+ },
107
+ },
108
+ },
109
+ additionalProperties: false,
110
+ title: "ActionAsset",
111
+ description:
112
+ "User actions can be represented in several places.\nEach view typically has one or more actions that allow the user to navigate away from that view.\nIn addition, several asset types can have actions that apply to that asset only.",
113
+ genericTokens: [
114
+ {
115
+ symbol: "AnyTextAsset",
116
+ constraints: {
117
+ type: "ref",
118
+ ref: "Asset",
119
+ },
120
+ default: {
121
+ type: "ref",
122
+ ref: "Asset",
123
+ },
124
+ },
125
+ ],
126
+ extends: {
127
+ type: "ref",
128
+ ref: "Asset<'action'>",
129
+ genericArguments: [
130
+ {
131
+ type: "string",
132
+ const: "action",
133
+ },
134
+ ],
135
+ },
136
+ } as NamedType;
137
+
138
+ const converter = new TSWriter(ts.factory);
139
+
140
+ const { type: tsNode, referencedTypes: referencedImports } =
141
+ converter.convertNamedType(xlr);
142
+
143
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
144
+ const resultFile = ts.createSourceFile(
145
+ "output.d.ts",
146
+ "",
147
+ ts.ScriptTarget.ES2017,
148
+ false, // setParentNodes
149
+ ts.ScriptKind.TS,
150
+ );
151
+
152
+ const nodeText = printer.printNode(
153
+ ts.EmitHint.Unspecified,
154
+ tsNode,
155
+ resultFile,
156
+ );
157
+
158
+ expect(nodeText).toMatchSnapshot();
159
+ expect(referencedImports).toMatchInlineSnapshot(`
160
+ Set {
161
+ "AssetWrapper",
162
+ "Expression",
163
+ "Asset",
164
+ }
165
+ `);
166
+ });
167
+
168
+ test("Template Type Conversion", () => {
169
+ const xlr = {
170
+ name: "BindingRef",
171
+ title: "BindingRef",
172
+ type: "template",
173
+ format: "{{.*}}",
174
+ } as NamedType;
175
+
176
+ const converter = new TSWriter(ts.factory);
177
+
178
+ const { type: tsNode, referencedTypes: referencedImports } =
179
+ converter.convertNamedType(xlr);
180
+
181
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
182
+ const resultFile = ts.createSourceFile(
183
+ "output.d.ts",
184
+ "",
185
+ ts.ScriptTarget.ES2017,
186
+ false, // setParentNodes
187
+ ts.ScriptKind.TS,
188
+ );
189
+
190
+ const nodeText = printer.printNode(
191
+ ts.EmitHint.Unspecified,
192
+ tsNode,
193
+ resultFile,
194
+ );
195
+
196
+ expect(nodeText).toMatchSnapshot();
197
+ expect(referencedImports).toBeUndefined();
198
+ });
199
+
200
+ test("Static Type Conversion Objects", () => {
201
+ const xlr = {
202
+ name: "test",
203
+ source: "test.ts",
204
+ type: "object",
205
+ properties: {
206
+ foo: {
207
+ required: true,
208
+ node: {
209
+ type: "string",
210
+ const: "foo",
211
+ },
212
+ },
213
+ bar: {
214
+ required: true,
215
+ node: {
216
+ type: "number",
217
+ const: 1,
218
+ },
219
+ },
220
+ bax: {
221
+ required: true,
222
+ node: {
223
+ type: "boolean",
224
+ const: false,
225
+ },
226
+ },
227
+ },
228
+ additionalProperties: false,
229
+ } as NamedType;
230
+
231
+ const converter = new TSWriter(ts.factory);
232
+
233
+ const { type: tsNode, referencedTypes: referencedImports } =
234
+ converter.convertNamedType(xlr);
235
+
236
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
237
+ const resultFile = ts.createSourceFile(
238
+ "output.d.ts",
239
+ "",
240
+ ts.ScriptTarget.ES2017,
241
+ false, // setParentNodes
242
+ ts.ScriptKind.TS,
243
+ );
244
+
245
+ const nodeText = printer.printNode(
246
+ ts.EmitHint.Unspecified,
247
+ tsNode,
248
+ resultFile,
249
+ );
250
+
251
+ expect(nodeText).toMatchSnapshot();
252
+ expect(referencedImports).toBeUndefined();
253
+ });
254
+
255
+ test("Static Type Conversion Arrays", () => {
256
+ const xlr = {
257
+ name: "test",
258
+ source: "test.ts",
259
+ type: "array",
260
+ elementType: {
261
+ type: "any",
262
+ },
263
+ const: [
264
+ {
265
+ type: "string",
266
+ const: "foo",
267
+ },
268
+ {
269
+ type: "number",
270
+ const: 1,
271
+ },
272
+ {
273
+ type: "boolean",
274
+ const: false,
275
+ },
276
+ ],
277
+ } as NamedType;
278
+
279
+ const converter = new TSWriter(ts.factory);
280
+
281
+ const { type: tsNode, referencedTypes: referencedImports } =
282
+ converter.convertNamedType(xlr);
283
+
284
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
285
+ const resultFile = ts.createSourceFile(
286
+ "output.d.ts",
287
+ "",
288
+ ts.ScriptTarget.ES2017,
289
+ false, // setParentNodes
290
+ ts.ScriptKind.TS,
291
+ );
292
+
293
+ const nodeText = printer.printNode(
294
+ ts.EmitHint.Unspecified,
295
+ tsNode,
296
+ resultFile,
297
+ );
298
+
299
+ expect(nodeText).toMatchSnapshot();
300
+ expect(referencedImports).toBeUndefined();
301
+ });
302
+
303
+ test("Dynamic results Conversion", () => {
304
+ const xlr = {
305
+ source: "test.ts",
306
+ name: "size",
307
+ type: "ref",
308
+ ref: "ExpressionHandler",
309
+ genericArguments: [
310
+ {
311
+ type: "tuple",
312
+ elementTypes: [
313
+ {
314
+ name: "val",
315
+ type: {
316
+ type: "unknown",
317
+ },
318
+ },
319
+ ],
320
+ additionalItems: false,
321
+ minItems: 1,
322
+ },
323
+ {
324
+ type: "number",
325
+ },
326
+ ],
327
+ } as NamedType;
328
+
329
+ const converter = new TSWriter(ts.factory);
330
+
331
+ const { type: tsNode, referencedTypes: referencedImports } =
332
+ converter.convertNamedType(xlr);
333
+
334
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
335
+ const resultFile = ts.createSourceFile(
336
+ "output.d.ts",
337
+ "",
338
+ ts.ScriptTarget.ES2017,
339
+ false, // setParentNodes
340
+ ts.ScriptKind.TS,
341
+ );
342
+
343
+ const nodeText = printer.printNode(
344
+ ts.EmitHint.Unspecified,
345
+ tsNode,
346
+ resultFile,
347
+ );
348
+
349
+ expect(nodeText).toMatchSnapshot();
350
+ expect(referencedImports).toMatchInlineSnapshot(`
351
+ Set {
352
+ "ExpressionHandler",
353
+ }
354
+ `);
355
+ });
356
+ });
@@ -0,0 +1,116 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import type { FunctionType, OrType, TupleType } from "@xlr-lib/xlr";
3
+ import { createDocString } from "../ts/documentation";
4
+
5
+ describe("docs", () => {
6
+ test("or", () => {
7
+ const type1: OrType = {
8
+ type: "or",
9
+ or: [
10
+ {
11
+ type: "string",
12
+ },
13
+ {
14
+ type: "array",
15
+ elementType: {
16
+ type: "string",
17
+ },
18
+ },
19
+ ],
20
+ };
21
+
22
+ expect(createDocString(type1)).toMatchInlineSnapshot(
23
+ `"string | Array<string>"`,
24
+ );
25
+ });
26
+
27
+ test("function", () => {
28
+ const type1: FunctionType = {
29
+ type: "function",
30
+ name: "testABC",
31
+ parameters: [
32
+ {
33
+ name: "a",
34
+ type: {
35
+ type: "string",
36
+ },
37
+ },
38
+ {
39
+ name: "b",
40
+ type: {
41
+ type: "array",
42
+ elementType: {
43
+ type: "string",
44
+ },
45
+ },
46
+ },
47
+ ],
48
+ returnType: {
49
+ type: "string",
50
+ },
51
+ };
52
+
53
+ expect(createDocString(type1)).toMatchInlineSnapshot(
54
+ `"function testABC(a: string, b: Array<string>): string"`,
55
+ );
56
+ });
57
+
58
+ test("tuple", () => {
59
+ const type1: TupleType = {
60
+ type: "tuple",
61
+ name: "testABC",
62
+ elementTypes: [
63
+ {
64
+ name: "a",
65
+ type: {
66
+ type: "string",
67
+ },
68
+ },
69
+ {
70
+ type: {
71
+ type: "array",
72
+ elementType: {
73
+ type: "string",
74
+ },
75
+ },
76
+ },
77
+ ],
78
+ minItems: 2,
79
+ additionalItems: false,
80
+ };
81
+
82
+ expect(createDocString(type1)).toMatchInlineSnapshot(
83
+ `"[a: string, Array<string>]"`,
84
+ );
85
+ });
86
+
87
+ test("const", () => {
88
+ const type1: OrType = {
89
+ type: "or",
90
+ or: [
91
+ {
92
+ type: "string",
93
+ const: "abc",
94
+ },
95
+ {
96
+ type: "number",
97
+ const: 123,
98
+ },
99
+ {
100
+ type: "boolean",
101
+ const: true,
102
+ },
103
+ {
104
+ type: "array",
105
+ elementType: {
106
+ type: "string",
107
+ },
108
+ },
109
+ ],
110
+ };
111
+
112
+ expect(createDocString(type1)).toMatchInlineSnapshot(
113
+ `""abc" | 123 | true | Array<string>"`,
114
+ );
115
+ });
116
+ });
@@ -0,0 +1,68 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import type { NamedType } from "@xlr-lib/xlr";
3
+ import { exportTypesToTypeScript } from "../ts/xlr-to-ts";
4
+
5
+ const IMPORT_MAP = new Map([["@player-ui/types", ["Asset", "Binding"]]]);
6
+
7
+ const ASSET_TYPE: NamedType = {
8
+ name: "Asset",
9
+ type: "object",
10
+ source: "test",
11
+ properties: {
12
+ id: { required: true, node: { type: "string" } },
13
+ type: { required: true, node: { type: "string" } },
14
+ },
15
+ additionalProperties: false,
16
+ };
17
+
18
+ const ACTION_ASSET_TYPE: NamedType = {
19
+ name: "ActionAsset",
20
+ type: "object",
21
+ source: "test",
22
+ properties: {
23
+ id: { required: true, node: { type: "string" } },
24
+ type: { required: true, node: { type: "string", const: "action" } },
25
+ value: { required: false, node: { type: "string" } },
26
+ },
27
+ additionalProperties: false,
28
+ };
29
+
30
+ describe("exportTypesToTypeScript", () => {
31
+ test("exports named types as a .d.ts string", () => {
32
+ const result = exportTypesToTypeScript(
33
+ [ASSET_TYPE, ACTION_ASSET_TYPE],
34
+ IMPORT_MAP,
35
+ );
36
+ expect(typeof result).toBe("string");
37
+ expect(result).toContain("export interface Asset");
38
+ expect(result).toContain("export interface ActionAsset");
39
+ expect(result).toMatchSnapshot();
40
+ });
41
+
42
+ test("emits import declarations for referenced types", () => {
43
+ const typeWithRef: NamedType = {
44
+ name: "Wrapper",
45
+ type: "object",
46
+ source: "test",
47
+ properties: {
48
+ asset: {
49
+ required: true,
50
+ node: { type: "ref", ref: "Asset" },
51
+ },
52
+ },
53
+ additionalProperties: false,
54
+ };
55
+
56
+ const result = exportTypesToTypeScript([typeWithRef], IMPORT_MAP);
57
+ expect(result).toContain("@player-ui/types");
58
+ expect(result).toContain("Asset");
59
+ expect(result).toMatchSnapshot();
60
+ });
61
+
62
+ test("skips import declarations for unreferenced packages", () => {
63
+ const result = exportTypesToTypeScript([ASSET_TYPE], IMPORT_MAP);
64
+ // Asset itself is defined, not imported — so the import declaration should be absent
65
+ expect(result).not.toContain("@player-ui/types");
66
+ expect(result).toMatchSnapshot();
67
+ });
68
+ });
@@ -0,0 +1,119 @@
1
+ import { test, expect } from "vitest";
2
+ import { setupTestEnv } from "@xlr-lib/test-utils";
3
+ import { TsConverter } from "..";
4
+
5
+ /**
6
+ * Tests for handleHeritageClauses: when an interface extends a base that has
7
+ * no index signature, but the current interface declares [key: string]: unknown,
8
+ * the merged XLR must include the current interface's additionalProperties
9
+ * (so the merged type allows extra keys).
10
+ */
11
+ test("merged type includes current interface additionalProperties when extending base without index signature", () => {
12
+ const sc = `
13
+ interface NavigationBaseState {
14
+ state_type: string;
15
+ onStart?: string;
16
+ }
17
+
18
+ export interface NavigationFlowEndState extends NavigationBaseState {
19
+ outcome: string;
20
+ [key: string]: unknown;
21
+ }
22
+ `;
23
+
24
+ const { sf, tc } = setupTestEnv(sc);
25
+ const converter = new TsConverter(tc);
26
+ const types = converter.convertSourceFile(sf).data.types;
27
+
28
+ const endStateType = types.find(
29
+ (t: { name?: string }) => t.name === "NavigationFlowEndState",
30
+ );
31
+ expect(endStateType).toBeDefined();
32
+ expect(
33
+ (endStateType as { additionalProperties?: unknown }).additionalProperties,
34
+ ).not.toBe(false);
35
+ expect(
36
+ (
37
+ (endStateType as { additionalProperties?: { type?: string } })
38
+ .additionalProperties as { type?: string }
39
+ )?.type,
40
+ ).toBe("unknown");
41
+ });
42
+
43
+ test("merged type includes additionalProperties when only extended type has index signature", () => {
44
+ const sc = `
45
+ interface Base {
46
+ required: string;
47
+ }
48
+
49
+ interface WithIndex {
50
+ [key: string]: unknown;
51
+ }
52
+
53
+ export interface Child extends Base, WithIndex {
54
+ extra: number;
55
+ }
56
+ `;
57
+
58
+ const { sf, tc } = setupTestEnv(sc);
59
+ const converter = new TsConverter(tc);
60
+ const types = converter.convertSourceFile(sf).data.types;
61
+
62
+ const childType = types.find((t: { name?: string }) => t.name === "Child");
63
+ expect(childType).toBeDefined();
64
+ const additionalProperties = (childType as { additionalProperties?: unknown })
65
+ .additionalProperties;
66
+ expect(additionalProperties).not.toBe(false);
67
+ expect((additionalProperties as { type?: string })?.type).toBe("unknown");
68
+ });
69
+
70
+ test("merged type has no additionalProperties when no interface has index signature", () => {
71
+ const sc = `
72
+ interface Base {
73
+ required: string;
74
+ }
75
+
76
+ export interface Child extends Base {
77
+ extra: number;
78
+ }
79
+ `;
80
+
81
+ const { sf, tc } = setupTestEnv(sc);
82
+ const converter = new TsConverter(tc);
83
+ const types = converter.convertSourceFile(sf).data.types;
84
+
85
+ const childType = types.find((t: { name?: string }) => t.name === "Child");
86
+ expect(childType).toBeDefined();
87
+ const additionalProperties = (childType as { additionalProperties?: unknown })
88
+ .additionalProperties;
89
+ expect(additionalProperties).toBe(false);
90
+ });
91
+
92
+ test("merged type combines additionalProperties from current and extended when both have index signatures", () => {
93
+ const sc = `
94
+ interface Base {
95
+ required: string;
96
+ [key: string]: unknown;
97
+ }
98
+
99
+ export interface Child extends Base {
100
+ extra: number;
101
+ [key: string]: unknown;
102
+ }
103
+ `;
104
+
105
+ const { sf, tc } = setupTestEnv(sc);
106
+ const converter = new TsConverter(tc);
107
+ const types = converter.convertSourceFile(sf).data.types;
108
+
109
+ const childType = types.find((t: { name?: string }) => t.name === "Child");
110
+ expect(childType).toBeDefined();
111
+ const additionalProperties = (childType as { additionalProperties?: unknown })
112
+ .additionalProperties;
113
+ expect(additionalProperties).not.toBe(false);
114
+ // When both have index sigs we merge (e.g. or of both); extra keys are allowed
115
+ const ap = additionalProperties as { type?: string; or?: unknown[] };
116
+ expect(
117
+ ap.type === "unknown" || (ap.type === "or" && Array.isArray(ap.or)),
118
+ ).toBe(true);
119
+ });