typespec-typescript-emitter 0.2.0 → 0.3.1
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/.husky/pre-commit +3 -0
- package/.prettierignore +1 -0
- package/CHANGELOG.md +37 -5
- package/README.md +49 -32
- package/dist/src/emit_routes.d.ts +5 -1
- package/dist/src/emit_routes.js +68 -33
- package/dist/src/emit_routes.js.map +1 -1
- package/dist/src/emit_types.d.ts +8 -6
- package/dist/src/emit_types.js +49 -155
- package/dist/src/emit_types.js.map +1 -1
- package/dist/src/emit_types_resolve.d.ts +8 -0
- package/dist/src/emit_types_resolve.js +122 -0
- package/dist/src/emit_types_resolve.js.map +1 -0
- package/dist/src/emit_types_typeguards.d.ts +17 -0
- package/dist/src/emit_types_typeguards.js +107 -0
- package/dist/src/emit_types_typeguards.js.map +1 -0
- package/dist/src/emitter.d.ts +5 -0
- package/dist/src/emitter.js +23 -10
- package/dist/src/emitter.js.map +1 -1
- package/dist/src/lib.d.ts +2 -0
- package/dist/src/lib.js +2 -0
- package/dist/src/lib.js.map +1 -1
- package/eslint.config.js +6 -1
- package/package.json +6 -7
- package/src/emit_routes.ts +107 -37
- package/src/emit_types.ts +55 -187
- package/src/emit_types_resolve.ts +167 -0
- package/src/emit_types_typeguards.ts +137 -0
- package/src/emitter.ts +38 -8
- package/src/lib.ts +4 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Model, Type } from "@typespec/compiler";
|
|
2
|
+
import { resolveScalar } from "./emit_types_resolve.js";
|
|
3
|
+
|
|
4
|
+
export const getTypeguardModel = (
|
|
5
|
+
m: Model,
|
|
6
|
+
accessor: string,
|
|
7
|
+
nestingLevel = 1,
|
|
8
|
+
knownGuards?: Array<{ filename: string; name: string }>,
|
|
9
|
+
): [string, string[]] => {
|
|
10
|
+
const imports: string[] = [];
|
|
11
|
+
return [
|
|
12
|
+
Array.from(m.properties)
|
|
13
|
+
.map((property) => {
|
|
14
|
+
const guard = getTypeguard(
|
|
15
|
+
property[1].type,
|
|
16
|
+
`${accessor}['${property[1].name}']`,
|
|
17
|
+
nestingLevel + 1,
|
|
18
|
+
knownGuards,
|
|
19
|
+
);
|
|
20
|
+
imports.push(...guard[1]);
|
|
21
|
+
let ret = " ".repeat(nestingLevel);
|
|
22
|
+
ret += property[1].optional
|
|
23
|
+
? `${accessor}['${property[1].name}'] === undefined || `
|
|
24
|
+
: `${accessor}['${property[1].name}'] !== undefined && `;
|
|
25
|
+
ret += `(${guard[0]})`;
|
|
26
|
+
return ret;
|
|
27
|
+
})
|
|
28
|
+
.filter((x) => !!x)
|
|
29
|
+
.join(" &&\n"),
|
|
30
|
+
imports,
|
|
31
|
+
];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates the function body for a typeguard
|
|
36
|
+
* @param t Type to create guards for
|
|
37
|
+
* @param accessor String by which the type-to-test can be accessed by the code
|
|
38
|
+
* @param nestingLevel
|
|
39
|
+
* @param knownGuards Array of names of known typeguards; if type is found in those, no new typeguard will be created and instead a reference to the existing one is produced
|
|
40
|
+
* @returns Tuple: [function body of the typeguard, array of import filenames (not unique!)]
|
|
41
|
+
*/
|
|
42
|
+
export const getTypeguard = (
|
|
43
|
+
t: Type,
|
|
44
|
+
accessor: string,
|
|
45
|
+
nestingLevel = 1,
|
|
46
|
+
knownGuards?: Array<{ filename: string; name: string }>,
|
|
47
|
+
): [string, string[]] => {
|
|
48
|
+
switch (t.kind) {
|
|
49
|
+
case "Model":
|
|
50
|
+
if (t.name === "Array") {
|
|
51
|
+
const guard = getTypeguard(
|
|
52
|
+
t.indexer!.value,
|
|
53
|
+
"v",
|
|
54
|
+
nestingLevel,
|
|
55
|
+
knownGuards,
|
|
56
|
+
);
|
|
57
|
+
if (guard[0].endsWith("\n"))
|
|
58
|
+
guard[0] = guard[0].substring(0, guard[0].length - 1);
|
|
59
|
+
return [
|
|
60
|
+
`Array.isArray(${accessor}) && ${accessor}.every((v) => ${guard[0]})`,
|
|
61
|
+
guard[1],
|
|
62
|
+
];
|
|
63
|
+
} else if (knownGuards && knownGuards.some((x) => x.name === t.name)) {
|
|
64
|
+
return [
|
|
65
|
+
`is${t.name}(${accessor})`,
|
|
66
|
+
[
|
|
67
|
+
`import {is${t.name}} from './${knownGuards.find((x) => x.name === t.name)!.filename}';`,
|
|
68
|
+
],
|
|
69
|
+
];
|
|
70
|
+
} else if (t.name && !knownGuards && t.namespace?.models.has(t.name)) {
|
|
71
|
+
return [`is${t.name}(${accessor})`, []];
|
|
72
|
+
} else {
|
|
73
|
+
const guard = getTypeguardModel(t, accessor, nestingLevel, knownGuards);
|
|
74
|
+
return [
|
|
75
|
+
`(\n${guard[0]}\n${" ".repeat(Math.max(nestingLevel - 1, 0))})`,
|
|
76
|
+
guard[1],
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
case "Boolean":
|
|
80
|
+
return [`typeof ${accessor} === 'boolean'`, []];
|
|
81
|
+
case "Intrinsic":
|
|
82
|
+
return [`${accessor} === ${t.name}`, []];
|
|
83
|
+
case "Number":
|
|
84
|
+
return [`typeof ${accessor} === 'number'`, []];
|
|
85
|
+
case "Scalar":
|
|
86
|
+
if (
|
|
87
|
+
// TODO: figure out how to support all the varieties of dates
|
|
88
|
+
// TODO: support byte arrays
|
|
89
|
+
resolveScalar(t) !== "Date" &&
|
|
90
|
+
resolveScalar(t) !== "Uint8Array"
|
|
91
|
+
)
|
|
92
|
+
return [`typeof ${accessor} === '${resolveScalar(t)}'`, []];
|
|
93
|
+
break;
|
|
94
|
+
case "String":
|
|
95
|
+
return [`typeof ${accessor} === 'string'`, []];
|
|
96
|
+
case "Tuple": {
|
|
97
|
+
// TODO: ['string1', 'string2'] gets resolved as [string, string] instead of literals. Why?
|
|
98
|
+
const imports: string[] = [];
|
|
99
|
+
return [
|
|
100
|
+
t.values
|
|
101
|
+
.map((v, i) => {
|
|
102
|
+
const guard = getTypeguard(
|
|
103
|
+
v,
|
|
104
|
+
`${accessor}[${i}]`,
|
|
105
|
+
nestingLevel,
|
|
106
|
+
knownGuards,
|
|
107
|
+
);
|
|
108
|
+
imports.push(...guard[1]);
|
|
109
|
+
return `(${guard[0]})`;
|
|
110
|
+
})
|
|
111
|
+
.join(" && "),
|
|
112
|
+
imports,
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
case "Union": {
|
|
116
|
+
const imports: string[] = [];
|
|
117
|
+
return [
|
|
118
|
+
Array.from(t.variants)
|
|
119
|
+
.map((v) => {
|
|
120
|
+
const guard = getTypeguard(
|
|
121
|
+
v[1].type,
|
|
122
|
+
`${accessor}`,
|
|
123
|
+
nestingLevel,
|
|
124
|
+
knownGuards,
|
|
125
|
+
);
|
|
126
|
+
imports.push(...guard[1]);
|
|
127
|
+
return `(${guard[0]})`;
|
|
128
|
+
})
|
|
129
|
+
.join(" || "),
|
|
130
|
+
imports,
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
default:
|
|
134
|
+
console.warn("Could not resolve type:", t.kind);
|
|
135
|
+
}
|
|
136
|
+
return ["true", []]; // fallback to not break everything in case of errors
|
|
137
|
+
};
|
package/src/emitter.ts
CHANGED
|
@@ -9,20 +9,46 @@ import emitRoutes from "./emit_routes.js";
|
|
|
9
9
|
import emitTypes from "./emit_types.js";
|
|
10
10
|
import { EmitterOptions } from "./lib.js";
|
|
11
11
|
|
|
12
|
+
declare global {
|
|
13
|
+
interface String {
|
|
14
|
+
addLine(str: string, tabs?: number, continued?: boolean): string;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
String.prototype.addLine = function (
|
|
19
|
+
this: string,
|
|
20
|
+
str: string,
|
|
21
|
+
tabs?: number,
|
|
22
|
+
continued?: boolean,
|
|
23
|
+
): string {
|
|
24
|
+
return `${this}${" ".repeat(tabs ?? 0)}${str}${continued ? "" : "\n"}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
12
27
|
export async function $onEmit(context: EmitContext) {
|
|
13
28
|
if (!context.program.compilerOptions.noEmit) {
|
|
14
29
|
const options: EmitterOptions = {
|
|
15
30
|
"root-namespace": context.options["root-namespace"],
|
|
16
31
|
"out-dir": context.options["out-dir"] ?? context.emitterOutputDir,
|
|
17
32
|
"enable-types": context.options["enable-types"] ?? true,
|
|
33
|
+
"enable-typeguards":
|
|
34
|
+
(context.options["enable-types"] ?? true) &&
|
|
35
|
+
(context.options["enable-typeguards"] ?? false),
|
|
18
36
|
"enable-routes": context.options["enable-routes"] ?? false,
|
|
37
|
+
"typeguards-in-routes":
|
|
38
|
+
(context.options["enable-types"] ?? true) &&
|
|
39
|
+
(context.options["enable-typeguards"] ?? false) &&
|
|
40
|
+
(context.options["enable-routes"] ?? false) &&
|
|
41
|
+
(context.options["typeguards-in-routes"] ?? false),
|
|
19
42
|
};
|
|
20
43
|
|
|
21
44
|
console.log(`Writing routes to ${options["out-dir"]}`);
|
|
22
45
|
|
|
23
46
|
let targetNamespaceFound = false;
|
|
24
47
|
let routesObject = "";
|
|
25
|
-
let typeFiles: ReturnType<typeof emitTypes> = {
|
|
48
|
+
let typeFiles: ReturnType<typeof emitTypes> = {
|
|
49
|
+
files: {},
|
|
50
|
+
typeguardedNames: [],
|
|
51
|
+
};
|
|
26
52
|
navigateProgram(context.program, {
|
|
27
53
|
namespace(n) {
|
|
28
54
|
if (
|
|
@@ -30,15 +56,18 @@ export async function $onEmit(context: EmitContext) {
|
|
|
30
56
|
n.name === context.options["root-namespace"]
|
|
31
57
|
) {
|
|
32
58
|
targetNamespaceFound = true;
|
|
59
|
+
if (options["enable-types"] || options["enable-typeguards"])
|
|
60
|
+
typeFiles = emitTypes(context, n, options);
|
|
33
61
|
if (options["enable-routes"]) {
|
|
34
62
|
const servers = getServers(context.program, n);
|
|
35
63
|
routesObject = emitRoutes(
|
|
36
64
|
context,
|
|
37
65
|
n,
|
|
38
66
|
servers && servers[0] ? servers[0].url : "",
|
|
67
|
+
options,
|
|
68
|
+
typeFiles.typeguardedNames,
|
|
39
69
|
);
|
|
40
70
|
}
|
|
41
|
-
if (options["enable-types"]) typeFiles = emitTypes(context, n);
|
|
42
71
|
}
|
|
43
72
|
},
|
|
44
73
|
});
|
|
@@ -59,13 +88,14 @@ export async function $onEmit(context: EmitContext) {
|
|
|
59
88
|
}
|
|
60
89
|
|
|
61
90
|
// type files
|
|
62
|
-
if (options["enable-types"]) {
|
|
63
|
-
const typeFileArr = Object.entries(typeFiles);
|
|
91
|
+
if (options["enable-types"] || options["enable-typeguards"]) {
|
|
92
|
+
const typeFileArr = Object.entries(typeFiles.files);
|
|
64
93
|
for (let i = 0; i < typeFileArr.length; i++) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
if (typeFileArr[i][1])
|
|
95
|
+
await emitFile(context.program, {
|
|
96
|
+
path: resolvePath(options["out-dir"], `${typeFileArr[i][0]}.ts`),
|
|
97
|
+
content: typeFileArr[i][1],
|
|
98
|
+
});
|
|
69
99
|
}
|
|
70
100
|
}
|
|
71
101
|
}
|
package/src/lib.ts
CHANGED
|
@@ -4,7 +4,9 @@ export interface EmitterOptions {
|
|
|
4
4
|
"root-namespace": string;
|
|
5
5
|
"out-dir": string;
|
|
6
6
|
"enable-types": boolean;
|
|
7
|
+
"enable-typeguards": boolean;
|
|
7
8
|
"enable-routes": boolean;
|
|
9
|
+
"typeguards-in-routes": boolean;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {
|
|
@@ -14,7 +16,9 @@ const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {
|
|
|
14
16
|
"root-namespace": { type: "string" },
|
|
15
17
|
"out-dir": { type: "string", format: "absolute-path" },
|
|
16
18
|
"enable-types": { type: "boolean" },
|
|
19
|
+
"enable-typeguards": { type: "boolean" },
|
|
17
20
|
"enable-routes": { type: "boolean" },
|
|
21
|
+
"typeguards-in-routes": { type: "boolean" },
|
|
18
22
|
},
|
|
19
23
|
required: ["root-namespace"],
|
|
20
24
|
};
|