typed-openapi 0.1.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.
@@ -0,0 +1,171 @@
1
+ import type { OpenAPIObject, ResponseObject } from "openapi3-ts/oas31";
2
+ import { OperationObject, ParameterObject } from "openapi3-ts/oas31";
3
+ import { capitalize, pick } from "pastable/server";
4
+ import { Box } from "./box";
5
+ import { createBoxFactory } from "./box-factory";
6
+ import { openApiSchemaToTs } from "./openapi-schema-to-ts";
7
+ import { createRefResolver } from "./ref-resolver";
8
+ import { tsFactory } from "./ts-factory";
9
+ import { AnyBox, BoxRef, OpenapiSchemaConvertContext } from "./types";
10
+ import { pathToVariableName } from "./string-utils";
11
+
12
+ const factory = tsFactory;
13
+
14
+ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
15
+ const refs = createRefResolver(doc, factory);
16
+ const ctx: OpenapiSchemaConvertContext = { refs, factory };
17
+ const endpointList = [] as Array<Endpoint>;
18
+
19
+ Object.entries(doc.paths ?? {}).forEach(([path, pathItemObj]) => {
20
+ const pathItem = pick(pathItemObj, ["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
21
+ Object.entries(pathItem).forEach(([method, operation]) => {
22
+ if (operation.deprecated) return;
23
+
24
+ const endpoint = {
25
+ operation,
26
+ method: method as Method,
27
+ path,
28
+ response: openApiSchemaToTs({ schema: {}, ctx }),
29
+ meta: {
30
+ alias: getAlias({ path, method, operation } as Endpoint),
31
+ areParametersRequired: false,
32
+ hasParameters: false,
33
+ },
34
+ } as Endpoint;
35
+
36
+ // Build a list of parameters by type + fill an object with all of them
37
+ const lists = { query: [] as ParameterObject[], path: [] as ParameterObject[], header: [] as ParameterObject[] };
38
+ const paramObjects = (operation.parameters ?? []).reduce(
39
+ (acc, paramOrRef) => {
40
+ const param = refs.unwrap(paramOrRef);
41
+ const schema = openApiSchemaToTs({ schema: refs.unwrap(param.schema ?? {}), ctx });
42
+ lists.query.push(param);
43
+
44
+ if (param.required) endpoint.meta.areParametersRequired = true;
45
+ endpoint.meta.hasParameters = true;
46
+
47
+ if (param.in === "query") acc.query[param.name] = schema;
48
+ if (param.in === "path") acc.path[param.name] = schema;
49
+ if (param.in === "header") acc.header[param.name] = schema;
50
+
51
+ return acc;
52
+ },
53
+ { query: {} as Record<string, Box>, path: {} as Record<string, Box>, header: {} as Record<string, Box> },
54
+ );
55
+
56
+ // Filter out empty objects
57
+ const params = Object.entries(paramObjects).reduce((acc, [key, value]) => {
58
+ if (Object.keys(value).length) {
59
+ // @ts-expect-error
60
+ acc[key] = value;
61
+ }
62
+ return acc;
63
+ }, {} as { query?: Record<string, Box>; path?: Record<string, Box>; header?: Record<string, Box>; body?: Box });
64
+
65
+ // Body
66
+ if (operation.requestBody) {
67
+ const requestBody = refs.unwrap(operation.requestBody ?? {});
68
+ const content = requestBody.content;
69
+ const matchingMediaType = Object.keys(content).find(isAllowedParamMediaTypes);
70
+
71
+ if (matchingMediaType && content[matchingMediaType]) {
72
+ params.body = openApiSchemaToTs({
73
+ schema: content[matchingMediaType]?.schema ?? {} ?? {},
74
+ ctx,
75
+ });
76
+ }
77
+ }
78
+
79
+ // Make parameters optional if none of them are required
80
+ if (params) {
81
+ const t = createBoxFactory({}, ctx);
82
+ if (params.query && lists.query.length && lists.query.every((param) => !param.required)) {
83
+ if (!params.query) params.query = {};
84
+ params.query = t.reference("Partial", [t.object(params.query)]) as any;
85
+ }
86
+ if (params.path && lists.path.length && lists.path.every((param) => !param.required)) {
87
+ params.path = t.reference("Partial", [t.object(params.path)]) as any;
88
+ }
89
+ if (params.header && lists.header.length && lists.header.every((param) => !param.required)) {
90
+ params.header = t.reference("Partial", [t.object(params.header)]) as any;
91
+ }
92
+
93
+ // No need to pass empty objects, it's confusing
94
+ endpoint.parameters = Object.keys(params).length ? (params as any as EndpointParameters) : undefined;
95
+ }
96
+
97
+ let responseObject;
98
+ // Match the default response or first 2xx-3xx response found
99
+ if (operation.responses.default) {
100
+ responseObject = refs.unwrap(operation.responses.default);
101
+ } else {
102
+ Object.entries(operation.responses).map(([status, responseOrRef]) => {
103
+ const statusCode = Number(status);
104
+ if (statusCode >= 200 && statusCode < 300) {
105
+ responseObject = refs.unwrap<ResponseObject>(responseOrRef);
106
+ }
107
+ });
108
+ }
109
+
110
+ const content = responseObject?.content;
111
+ if (content) {
112
+ const matchingMediaType = Object.keys(content).find(isResponseMediaType);
113
+ if (matchingMediaType && content[matchingMediaType]) {
114
+ endpoint.response = openApiSchemaToTs({
115
+ schema: content[matchingMediaType]?.schema ?? {} ?? {},
116
+ ctx,
117
+ });
118
+ }
119
+ }
120
+
121
+ endpointList.push(endpoint);
122
+ });
123
+ });
124
+
125
+ return { doc, refs, endpointList, factory };
126
+ };
127
+
128
+ const allowedParamMediaTypes = [
129
+ "application/octet-stream",
130
+ "multipart/form-data",
131
+ "application/x-www-form-urlencoded",
132
+ "*/*",
133
+ ] as const;
134
+ const isAllowedParamMediaTypes = (
135
+ mediaType: string,
136
+ ): mediaType is (typeof allowedParamMediaTypes)[number] | `application/${string}json${string}` | `text/${string}` =>
137
+ (mediaType.includes("application/") && mediaType.includes("json")) ||
138
+ allowedParamMediaTypes.includes(mediaType as any) ||
139
+ mediaType.includes("text/");
140
+
141
+ const isResponseMediaType = (mediaType: string) => mediaType === "application/json";
142
+ const getAlias = ({ path, method, operation }: Endpoint) =>
143
+ method + "_" + capitalize(operation.operationId ?? pathToVariableName(path));
144
+
145
+ type MutationMethod = "post" | "put" | "patch" | "delete";
146
+ type Method = "get" | "head" | "options" | MutationMethod;
147
+
148
+ export type EndpointParameters = {
149
+ body?: Box;
150
+ query?: Box<BoxRef> | Record<string, AnyBox>;
151
+ header?: Box<BoxRef> | Record<string, AnyBox>;
152
+ path?: Box<BoxRef> | Record<string, AnyBox>;
153
+ };
154
+
155
+ type DefaultEndpoint = {
156
+ parameters?: EndpointParameters | undefined;
157
+ response: AnyBox;
158
+ };
159
+
160
+ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
161
+ operation: OperationObject;
162
+ method: Method;
163
+ path: string;
164
+ parameters?: TConfig["parameters"];
165
+ meta: {
166
+ alias: string;
167
+ hasParameters: boolean;
168
+ areParametersRequired: boolean;
169
+ };
170
+ response: TConfig["response"];
171
+ };
@@ -0,0 +1,157 @@
1
+ import { isPrimitiveType } from "./asserts";
2
+ import { Box } from "./box";
3
+ import { createBoxFactory } from "./box-factory";
4
+ import { isReferenceObject } from "./is-reference-object";
5
+ import { AnyBoxDef, OpenapiSchemaConvertArgs } from "./types";
6
+ import { wrapWithQuotesIfNeeded } from "./string-utils";
7
+
8
+ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: OpenapiSchemaConvertArgs): Box<AnyBoxDef> => {
9
+ const meta = {} as OpenapiSchemaConvertArgs["meta"];
10
+
11
+ if (!schema) {
12
+ throw new Error("Schema is required");
13
+ }
14
+
15
+ const t = createBoxFactory(schema, ctx);
16
+ const getTs = () => {
17
+ if (isReferenceObject(schema)) {
18
+ const refInfo = ctx.refs.getInfosByRef(schema.$ref);
19
+
20
+ return t.reference(refInfo.normalized);
21
+ }
22
+
23
+ if (Array.isArray(schema.type)) {
24
+ if (schema.type.length === 1) {
25
+ return openApiSchemaToTs({ schema: { ...schema, type: schema.type[0]! }, ctx, meta });
26
+ }
27
+
28
+ return t.union(schema.type.map((prop) => openApiSchemaToTs({ schema: { ...schema, type: prop }, ctx, meta })));
29
+ }
30
+
31
+ if (schema.type === "null") {
32
+ return t.reference("null");
33
+ }
34
+
35
+ if (schema.oneOf) {
36
+ if (schema.oneOf.length === 1) {
37
+ return openApiSchemaToTs({ schema: schema.oneOf[0]!, ctx, meta });
38
+ }
39
+
40
+ return t.union(schema.oneOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
41
+ }
42
+
43
+ // anyOf = oneOf but with 1 or more = `T extends oneOf ? T | T[] : never`
44
+ if (schema.anyOf) {
45
+ if (schema.anyOf.length === 1) {
46
+ return openApiSchemaToTs({ schema: schema.anyOf[0]!, ctx, meta });
47
+ }
48
+
49
+ const oneOf = t.union(schema.anyOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
50
+ return t.union([oneOf, t.array(oneOf)]);
51
+ }
52
+
53
+ if (schema.allOf) {
54
+ if (schema.allOf.length === 1) {
55
+ return openApiSchemaToTs({ schema: schema.allOf[0]!, ctx, meta });
56
+ }
57
+
58
+ const types = schema.allOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta }));
59
+ return t.intersection(types);
60
+ }
61
+
62
+ const schemaType = schema.type ? (schema.type.toLowerCase() as NonNullable<typeof schema.type>) : undefined;
63
+ if (schemaType && isPrimitiveType(schemaType)) {
64
+ if (schema.enum) {
65
+ if (schema.enum.length === 1) {
66
+ const value = schema.enum[0];
67
+ return t.literal(value === null ? "null" : `"${value}"`);
68
+ }
69
+
70
+ if (schemaType === "string") {
71
+ return t.union(schema.enum.map((value) => t.literal(`"${value}"`)));
72
+ }
73
+
74
+ if (schema.enum.some((e) => typeof e === "string")) {
75
+ return t.never();
76
+ }
77
+
78
+ return t.union(schema.enum.map((value) => t.literal(value === null ? "null" : value)));
79
+ }
80
+
81
+ if (schemaType === "string") return t.string();
82
+ if (schemaType === "boolean") return t.boolean();
83
+ if (schemaType === "number" || schemaType === "integer") return t.number();
84
+ if (schemaType === "null") return t.reference("null");
85
+ }
86
+
87
+ if (schemaType === "array") {
88
+ if (schema.items) {
89
+ let arrayOfType = openApiSchemaToTs({ schema: schema.items, ctx, meta });
90
+ if (typeof arrayOfType === "string") {
91
+ arrayOfType = t.reference(arrayOfType);
92
+ }
93
+
94
+ return t.array(arrayOfType);
95
+ }
96
+
97
+ return t.array(t.any());
98
+ }
99
+
100
+ if (schemaType === "object" || schema.properties || schema.additionalProperties) {
101
+ if (!schema.properties) {
102
+ return t.unknown();
103
+ }
104
+
105
+ let additionalProperties;
106
+ if (schema.additionalProperties) {
107
+ let additionalPropertiesType;
108
+ if (
109
+ (typeof schema.additionalProperties === "boolean" && schema.additionalProperties) ||
110
+ (typeof schema.additionalProperties === "object" && Object.keys(schema.additionalProperties).length === 0)
111
+ ) {
112
+ additionalPropertiesType = t.any();
113
+ } else if (typeof schema.additionalProperties === "object") {
114
+ additionalPropertiesType = openApiSchemaToTs({
115
+ schema: schema.additionalProperties,
116
+ ctx,
117
+ meta,
118
+ });
119
+ }
120
+
121
+ additionalProperties = t.object({ [t.string().value]: additionalPropertiesType! });
122
+ }
123
+
124
+ const hasRequiredArray = schema.required && schema.required.length > 0;
125
+ const isPartial = !schema.required?.length;
126
+
127
+ const props = Object.fromEntries(
128
+ Object.entries(schema.properties).map(([prop, propSchema]) => {
129
+ let propType = openApiSchemaToTs({ schema: propSchema, ctx, meta });
130
+ if (typeof propType === "string") {
131
+ // TODO Partial ?
132
+ propType = t.reference(propType);
133
+ }
134
+
135
+ const isRequired = Boolean(isPartial ? true : hasRequiredArray ? schema.required?.includes(prop) : false);
136
+ const isOptional = !isPartial && !isRequired;
137
+ return [
138
+ `${wrapWithQuotesIfNeeded(prop)}${isOptional ? "?" : ""}`,
139
+ isOptional ? t.optional(propType) : propType,
140
+ ];
141
+ }),
142
+ );
143
+
144
+ const objectType = additionalProperties
145
+ ? t.intersection([t.object(props), additionalProperties])
146
+ : t.object(props);
147
+
148
+ return isPartial ? t.reference("Partial", [objectType]) : objectType;
149
+ }
150
+
151
+ if (!schemaType) return t.unknown();
152
+
153
+ throw new Error(`Unsupported schema type: ${schemaType}`);
154
+ };
155
+
156
+ return getTs();
157
+ };
@@ -0,0 +1,206 @@
1
+ import type { OpenAPIObject, ReferenceObject, SchemaObject } from "openapi3-ts/oas31";
2
+ import { get } from "pastable/server";
3
+
4
+ import { Box } from "./box";
5
+ import { isReferenceObject } from "./is-reference-object";
6
+ import { openApiSchemaToTs } from "./openapi-schema-to-ts";
7
+ import { normalizeString } from "./string-utils";
8
+ import { AnyBoxDef, GenericFactory } from "./types";
9
+ import { topologicalSort } from "./topological-sort";
10
+
11
+ const autocorrectRef = (ref: string) => (ref[1] === "/" ? ref : "#/" + ref.slice(1));
12
+ const componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"];
13
+
14
+ export type RefInfo = {
15
+ /**
16
+ * The (potentially autocorrected) ref
17
+ * @example "#/components/schemas/MySchema"
18
+ */
19
+ ref: string;
20
+ /**
21
+ * The name of the ref
22
+ * @example "MySchema"
23
+ * */
24
+ name: string;
25
+ normalized: string;
26
+ kind: "schemas" | "responses" | "parameters" | "requestBodies" | "headers";
27
+ };
28
+
29
+ export const createRefResolver = (doc: OpenAPIObject, factory: GenericFactory) => {
30
+ // both used for debugging purpose
31
+ const nameByRef = new Map<string, string>();
32
+ const refByName = new Map<string, string>();
33
+
34
+ const byRef = new Map<string, RefInfo>();
35
+ const byNormalized = new Map<string, RefInfo>();
36
+
37
+ const boxByRef = new Map<string, Box<AnyBoxDef>>();
38
+
39
+ const getSchemaByRef = <T = SchemaObject>(ref: string) => {
40
+ // #components -> #/components
41
+ const correctRef = autocorrectRef(ref);
42
+ const split = correctRef.split("/");
43
+
44
+ // "#/components/schemas/Something.jsonld" -> #/components/schemas
45
+ const path = split.slice(1, -1).join("/")!;
46
+ const normalizedPath = path.replace("#/", "").replace("#", "").replaceAll("/", ".");
47
+ const map = get(doc, normalizedPath) ?? ({} as any);
48
+
49
+ // "#/components/schemas/Something.jsonld" -> "Something.jsonld"
50
+ const name = split[split.length - 1]!;
51
+ const normalized = normalizeString(name);
52
+
53
+ nameByRef.set(correctRef, normalized);
54
+ refByName.set(normalized, correctRef);
55
+
56
+ const infos = { ref: correctRef, name, normalized, kind: normalizedPath.split(".")[1] as RefInfo["kind"] };
57
+ byRef.set(infos.ref, infos);
58
+ byNormalized.set(infos.normalized, infos);
59
+
60
+ // doc.components.schemas["Something.jsonld"]
61
+ const schema = map[name] as T;
62
+ if (!schema) {
63
+ throw new Error(`Unresolved ref "${name}" not found in "${path}"`);
64
+ }
65
+
66
+ return schema;
67
+ };
68
+
69
+ const getInfosByRef = (ref: string) => byRef.get(autocorrectRef(ref))!;
70
+
71
+ const schemaEntries = Object.entries(doc.components ?? {}).filter(([key]) => componentsWithSchemas.includes(key));
72
+
73
+ schemaEntries.forEach(([key, component]) => {
74
+ Object.keys(component).map((name) => {
75
+ const ref = `#/components/${key}/${name}`;
76
+ getSchemaByRef(ref);
77
+ });
78
+ });
79
+
80
+ const directDependencies = new Map<string, Set<string>>();
81
+
82
+ // need to be done after all refs are resolved
83
+ schemaEntries.forEach(([key, component]) => {
84
+ Object.keys(component).map((name) => {
85
+ const ref = `#/components/${key}/${name}`;
86
+ const schema = getSchemaByRef(ref);
87
+ boxByRef.set(ref, openApiSchemaToTs({ schema, ctx: { factory, refs: { getInfosByRef } as any } }));
88
+
89
+ if (!directDependencies.has(ref)) {
90
+ directDependencies.set(ref, new Set<string>());
91
+ }
92
+ setSchemaDependencies(schema, directDependencies.get(ref)!);
93
+ });
94
+ });
95
+
96
+ const transitiveDependencies = getTransitiveDependencies(directDependencies);
97
+
98
+ return {
99
+ get: getSchemaByRef,
100
+ unwrap: <T extends ReferenceObject | {}>(component: T) => {
101
+ return (isReferenceObject(component) ? getSchemaByRef(component.$ref) : component) as Exclude<T, ReferenceObject>;
102
+ },
103
+ getInfosByRef: getInfosByRef,
104
+ infos: byRef,
105
+ /**
106
+ * Get the schemas in the order they should be generated, depending on their dependencies
107
+ * so that a schema is generated before the ones that depend on it
108
+ */
109
+ getOrderedSchemas: () => {
110
+ const schemaOrderedByDependencies = topologicalSort(transitiveDependencies).map((ref) => {
111
+ const infos = getInfosByRef(ref);
112
+ return [boxByRef.get(infos.ref)!, infos] as [schema: Box<AnyBoxDef>, infos: RefInfo];
113
+ });
114
+
115
+ return schemaOrderedByDependencies;
116
+ },
117
+ directDependencies,
118
+ transitiveDependencies,
119
+ };
120
+ };
121
+
122
+ export interface RefResolver extends ReturnType<typeof createRefResolver> {}
123
+
124
+ const setSchemaDependencies = (schema: SchemaObject, deps: Set<string>) => {
125
+ const visit = (schema: SchemaObject | ReferenceObject): void => {
126
+ if (!schema) return;
127
+
128
+ if (isReferenceObject(schema)) {
129
+ deps.add(schema.$ref);
130
+ return;
131
+ }
132
+
133
+ if (schema.allOf) {
134
+ for (const allOf of schema.allOf) {
135
+ visit(allOf);
136
+ }
137
+
138
+ return;
139
+ }
140
+
141
+ if (schema.oneOf) {
142
+ for (const oneOf of schema.oneOf) {
143
+ visit(oneOf);
144
+ }
145
+
146
+ return;
147
+ }
148
+
149
+ if (schema.anyOf) {
150
+ for (const anyOf of schema.anyOf) {
151
+ visit(anyOf);
152
+ }
153
+
154
+ return;
155
+ }
156
+
157
+ if (schema.type === "array") {
158
+ if (!schema.items) return;
159
+ return void visit(schema.items);
160
+ }
161
+
162
+ if (schema.type === "object" || schema.properties || schema.additionalProperties) {
163
+ if (schema.properties) {
164
+ for (const property in schema.properties) {
165
+ visit(schema.properties[property]!);
166
+ }
167
+ }
168
+
169
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
170
+ visit(schema.additionalProperties);
171
+ }
172
+ }
173
+ };
174
+
175
+ visit(schema);
176
+ };
177
+
178
+ const getTransitiveDependencies = (directDependencies: Map<string, Set<string>>) => {
179
+ const transitiveDependencies = new Map<string, Set<string>>();
180
+ const visitedsDeepRefs = new Set<string>();
181
+
182
+ directDependencies.forEach((deps, ref) => {
183
+ if (!transitiveDependencies.has(ref)) {
184
+ transitiveDependencies.set(ref, new Set());
185
+ }
186
+
187
+ const visit = (depRef: string) => {
188
+ transitiveDependencies.get(ref)!.add(depRef);
189
+
190
+ const deps = directDependencies.get(depRef);
191
+ if (deps && ref !== depRef) {
192
+ deps.forEach((transitive) => {
193
+ const key = ref + "__" + transitive;
194
+ if (visitedsDeepRefs.has(key)) return;
195
+
196
+ visitedsDeepRefs.add(key);
197
+ visit(transitive);
198
+ });
199
+ }
200
+ };
201
+
202
+ deps.forEach((dep) => visit(dep));
203
+ });
204
+
205
+ return transitiveDependencies;
206
+ };
@@ -0,0 +1,42 @@
1
+ import { capitalize, kebabToCamel } from "pastable/server";
2
+
3
+ export const toSchemasRef = (name: string) => `#/components/schemas/${name}`;
4
+
5
+ export function normalizeString(text: string) {
6
+ const prefixed = prefixStringStartingWithNumberIfNeeded(text);
7
+ return prefixed
8
+ .normalize("NFKD") // The normalize() using NFKD method returns the Unicode Normalization Form of a given string.
9
+ .trim() // Remove whitespace from both sides of a string (optional)
10
+ .replace(/\s+/g, "_") // Replace spaces with _
11
+ .replace(/-+/g, "_") // Replace - with _
12
+ .replace(/[^\w\-]+/g, "_") // Remove all non-word chars
13
+ .replace(/--+/g, "-"); // Replace multiple - with single -
14
+ }
15
+
16
+ const onlyWordRegex = /^\w+$/;
17
+ export const wrapWithQuotesIfNeeded = (str: string) => {
18
+ if (str[0] === '"' && str[str.length - 1] === '"') return str;
19
+ if (onlyWordRegex.test(str)) {
20
+ return str;
21
+ }
22
+
23
+ return `"${str}"`;
24
+ };
25
+
26
+ const prefixStringStartingWithNumberIfNeeded = (str: string) => {
27
+ const firstAsNumber = Number(str[0]);
28
+ if (typeof firstAsNumber === "number" && !Number.isNaN(firstAsNumber)) {
29
+ return "_" + str;
30
+ }
31
+
32
+ return str;
33
+ };
34
+
35
+ const pathParamWithBracketsRegex = /({\w+})/g;
36
+ const wordPrecededByNonWordCharacter = /[^\w\-]+/g;
37
+
38
+ /** @example turns `/media-objects/{id}` into `MediaObjectsId` */
39
+ export const pathToVariableName = (path: string) =>
40
+ capitalize(kebabToCamel(path).replaceAll("/", "")) // /media-objects/{id} -> MediaObjects{id}
41
+ .replace(pathParamWithBracketsRegex, (group) => capitalize(group.slice(1, -1))) // {id} -> Id
42
+ .replace(wordPrecededByNonWordCharacter, "_"); // "/robots.txt" -> "/robots_txt"
@@ -0,0 +1,35 @@
1
+ /** @see https://gist.github.com/RubyTuesdayDONO/5006455 */
2
+ export function topologicalSort(graph: Map<string, Set<string>>) {
3
+ const sorted: string[] = [], // sorted list of IDs ( returned value )
4
+ visited: Record<string, boolean> = {}; // hash: id of already visited node => true
5
+
6
+ function visit(name: string, ancestors: string[]) {
7
+ if (!Array.isArray(ancestors)) ancestors = [];
8
+ ancestors.push(name);
9
+ visited[name] = true;
10
+
11
+ const deps = graph.get(name);
12
+ if (deps) {
13
+ deps.forEach((dep) => {
14
+ if (ancestors.includes(dep)) {
15
+ // if already in ancestors, a closed chain (recursive relation) exists
16
+ return;
17
+ // throw new Error(
18
+ // 'Circular dependency "' + dep + '" is required by "' + name + '": ' + ancestors.join(" -> ")
19
+ // );
20
+ }
21
+
22
+ // if already exists, do nothing
23
+ if (visited[dep]) return;
24
+ visit(dep, ancestors.slice(0)); // recursive call
25
+ });
26
+ }
27
+
28
+ if (!sorted.includes(name)) sorted.push(name);
29
+ }
30
+
31
+ // 2. topological sort
32
+ graph.forEach((_, name) => visit(name, []));
33
+
34
+ return sorted;
35
+ }
@@ -0,0 +1,24 @@
1
+ import { createFactory, unwrap } from "./box-factory";
2
+ import { wrapWithQuotesIfNeeded } from "./string-utils";
3
+
4
+ export const tsFactory = createFactory({
5
+ union: (types) => types.map(unwrap).join(" | "),
6
+ intersection: (types) => types.map(unwrap).join(" & "),
7
+ array: (type) => `Array<${unwrap(type)}>`,
8
+ optional: (type) => `${unwrap(type)} | undefined`,
9
+ reference: (name, typeArgs) => `${name}${typeArgs ? `<${typeArgs.map(unwrap).join(", ")}>` : ""}`,
10
+ literal: (value) => value.toString(),
11
+ string: () => "string" as const,
12
+ number: () => "number" as const,
13
+ boolean: () => "boolean" as const,
14
+ unknown: () => "unknown" as const,
15
+ any: () => "any" as const,
16
+ never: () => "never" as const,
17
+ object: (props) => {
18
+ const propsString = Object.entries(props)
19
+ .map(([prop, type]) => `${wrapWithQuotesIfNeeded(prop)}: ${unwrap(type)}`)
20
+ .join(", ");
21
+
22
+ return `{ ${propsString} }`;
23
+ },
24
+ });