vovk-cli 0.0.1-draft.255 → 0.0.1-draft.256
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/client-templates/cjs/index.d.cts.ejs +5 -2
- package/client-templates/mixins/mixins.d.ts.ejs +57 -20
- package/client-templates/mjs/index.d.mts.ejs +5 -2
- package/client-templates/schemaTs/schema.ts.ejs +5 -0
- package/client-templates/ts/index.ts.ejs +5 -2
- package/dist/generate/generate.mjs +7 -26
- package/dist/generate/getClientTemplateFiles.d.mts +1 -0
- package/dist/generate/getClientTemplateFiles.mjs +1 -0
- package/dist/generate/writeOneClientFile.mjs +4 -3
- package/dist/utils/compileJSONSchemaToTypeScriptType.d.mts +3 -0
- package/dist/utils/compileJSONSchemaToTypeScriptType.mjs +225 -0
- package/package.json +2 -1
- package/dist/utils/convertJSONSchemaToTypeScriptDef.d.mts +0 -5
- package/dist/utils/convertJSONSchemaToTypeScriptDef.mjs +0 -271
|
@@ -7,7 +7,7 @@ import type { createRPC } from '<%= t.imports.module.createRPC %>';
|
|
|
7
7
|
import type { Controllers as Controllers<%= i %> } from "<%= t.segmentMeta[segment.segmentName].segmentImportPath %>";
|
|
8
8
|
<% }}) %>
|
|
9
9
|
<% if (t.hasMixins) { %>
|
|
10
|
-
import type { Controllers as MixinControllers } from "./mixins";
|
|
10
|
+
import type { Controllers as MixinControllers, Mixins } from "./mixins";
|
|
11
11
|
<% } %>
|
|
12
12
|
|
|
13
13
|
type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
|
|
@@ -16,4 +16,7 @@ type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
|
|
|
16
16
|
export const <%= rpcModuleName %>: ReturnType<typeof createRPC<<%= segment.segmentType === 'mixin' ? `MixinControllers` : `Controllers${i}` %>["<%= rpcModuleName %>"], Options>>;
|
|
17
17
|
<% })
|
|
18
18
|
}) %>
|
|
19
|
-
export { schema } from './schema.cjs';
|
|
19
|
+
export { schema } from './schema.cjs';
|
|
20
|
+
<% if (t.hasMixins) { %>
|
|
21
|
+
export { Mixins };
|
|
22
|
+
<% } %>
|
|
@@ -1,27 +1,64 @@
|
|
|
1
1
|
<%- `// auto-generated by Vovk.ts ${new Date().toISOString()}` %>
|
|
2
2
|
import type { VovkRequest, VovkStreamAsyncIterable, KnownAny } from 'vovk';
|
|
3
3
|
|
|
4
|
-
<% Object.values(t.schema.segments).filter((segment) => segment.emitSchema && segment.segmentType === 'mixin')
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
}
|
|
4
|
+
<% const mixins = Object.values(t.schema.segments).filter((segment) => segment.emitSchema && segment.segmentType === 'mixin'); %>
|
|
5
|
+
|
|
6
|
+
export namespace Mixins {
|
|
7
|
+
<% for (const segment of mixins) { %>
|
|
8
|
+
<% console.log(segment); if(segment.meta?.components?.schemas) { %>
|
|
9
|
+
export namespace <%= t._.upperFirst(t._.camelCase(segment.segmentName)) %> {
|
|
10
|
+
<% for (const [componentName, componentSchema] of Object.entries(segment.meta.components.schemas)) { %>
|
|
11
|
+
<%- await t.compileJSONSchemaToTypeScriptType(componentSchema, componentName, segment.meta.components) %>
|
|
12
|
+
<% } %>
|
|
13
|
+
}
|
|
14
|
+
<% } %>
|
|
15
|
+
<% } %>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export namespace Types {
|
|
19
|
+
<% for (const segment of mixins) { %>
|
|
20
|
+
export namespace <%= t._.upperFirst(t._.camelCase(segment.segmentName)) %> {
|
|
21
|
+
<% for (const [controllerName, controllerSchema] of Object.entries(segment.controllers)) { %>
|
|
22
|
+
export namespace <%= controllerSchema.rpcModuleName %> {
|
|
23
|
+
<% for (const [handlerName, handlerSchema] of Object.entries(controllerSchema.handlers)) { %>
|
|
24
|
+
export namespace <%= t._.upperFirst(handlerName) %> {
|
|
25
|
+
<%- await t.compileJSONSchemaToTypeScriptType(handlerSchema.validation?.body, 'Body') %>
|
|
26
|
+
<%- await t.compileJSONSchemaToTypeScriptType(handlerSchema.validation?.query, 'Query') %>
|
|
27
|
+
<%- await t.compileJSONSchemaToTypeScriptType(handlerSchema.validation?.params, 'Params') %>
|
|
28
|
+
<%- await t.compileJSONSchemaToTypeScriptType(handlerSchema.validation?.output, 'Output') %>
|
|
29
|
+
<%- await t.compileJSONSchemaToTypeScriptType(handlerSchema.validation?.iteration, 'Iteration') %>
|
|
30
|
+
}
|
|
31
|
+
<% } %>
|
|
32
|
+
}
|
|
33
|
+
<% } %>
|
|
34
|
+
}
|
|
35
|
+
<% } %>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
<% const getType = (segment, controllerSchema, handlerName, validationType) => {
|
|
39
|
+
const segmentNs = t._.upperFirst(t._.camelCase(segment.segmentName));
|
|
40
|
+
const controllerNs = controllerSchema.rpcModuleName;
|
|
41
|
+
const handlerNs = t._.upperFirst(handlerName);
|
|
42
|
+
const typeName = t._.upperFirst(validationType);
|
|
43
|
+
const handlerSchema = controllerSchema.handlers[handlerName];
|
|
44
|
+
if(handlerSchema.validation?.[validationType] ) {
|
|
45
|
+
return 'Types.' + segmentNs + '.' + controllerNs + '.' + handlerNs + '.' + typeName;
|
|
46
|
+
}
|
|
47
|
+
return 'null';
|
|
48
|
+
} %>
|
|
15
49
|
|
|
16
50
|
export type Controllers = {
|
|
17
|
-
<% Object.values(t.schema.segments).filter((segment) => segment.emitSchema && segment.segmentType === 'mixin')
|
|
18
|
-
<% Object.values(segment.controllers)
|
|
51
|
+
<% for (const segment of Object.values(t.schema.segments).filter((segment) => segment.emitSchema && segment.segmentType === 'mixin')) { %>
|
|
52
|
+
<% for (const controllerSchema of Object.values(segment.controllers)) { %>
|
|
19
53
|
<%= controllerSchema.rpcModuleName %>: {
|
|
20
|
-
<% Object.entries(controllerSchema.handlers)
|
|
21
|
-
<%= handlerName %>: (req: VovkRequest
|
|
22
|
-
|
|
54
|
+
<% for (const [handlerName, handlerSchema] of Object.entries(controllerSchema.handlers)) { %>
|
|
55
|
+
<%= handlerName %>: (req: VovkRequest<
|
|
56
|
+
<%- getType(segment, controllerSchema, handlerName, 'body') %>,
|
|
57
|
+
<%- getType(segment, controllerSchema, handlerName, 'query') %>,
|
|
58
|
+
<%- getType(segment, controllerSchema, handlerName, 'params') %>
|
|
59
|
+
>) => <%- handlerSchema.validation?.output ? `Promise<${getType(segment, controllerSchema, handlerName, 'output')}>` : handlerSchema.validation?.iteration ? `Promise<VovkStreamAsyncIterable<${getType(segment, controllerSchema, handlerName, 'iteration')}>` : 'Promise<KnownAny>' %>,
|
|
60
|
+
<% } %>
|
|
23
61
|
};
|
|
24
|
-
<% }
|
|
25
|
-
<% }
|
|
26
|
-
};
|
|
27
|
-
|
|
62
|
+
<% } %>
|
|
63
|
+
<% } %>
|
|
64
|
+
};
|
|
@@ -7,7 +7,7 @@ import type { createRPC } from '<%= t.imports.module.createRPC %>';
|
|
|
7
7
|
import type { Controllers as Controllers<%= i %> } from "<%= t.segmentMeta[segment.segmentName].segmentImportPath %>";
|
|
8
8
|
<% }}) %>
|
|
9
9
|
<% if (t.hasMixins) { %>
|
|
10
|
-
import type { Controllers as MixinControllers } from "./mixins";
|
|
10
|
+
import type { Controllers as MixinControllers, Mixins } from "./mixins";
|
|
11
11
|
<% } %>
|
|
12
12
|
|
|
13
13
|
type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
|
|
@@ -16,4 +16,7 @@ type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
|
|
|
16
16
|
export const <%= rpcModuleName %>: ReturnType<typeof createRPC<<%= segment.segmentType === 'mixin' ? `MixinControllers` : `Controllers${i}` %>["<%= rpcModuleName %>"], Options>>;
|
|
17
17
|
<% })
|
|
18
18
|
}) %>
|
|
19
|
-
export { schema } from './schema.cjs';
|
|
19
|
+
export { schema } from './schema.cjs';
|
|
20
|
+
<% if (t.hasMixins) { %>
|
|
21
|
+
export { Mixins };
|
|
22
|
+
<% } %>
|
|
@@ -4,6 +4,7 @@ import meta from './<%= t.schemaOutDir %>/_meta.json' with { type: "json" };
|
|
|
4
4
|
<% } %>
|
|
5
5
|
<% if(t.hasMixins) { %>
|
|
6
6
|
import mixins from './mixins.json' with { type: "json" };
|
|
7
|
+
import type { Mixins } from './mixins.d.ts';
|
|
7
8
|
<% } %>
|
|
8
9
|
<% Object.values(t.schema.segments).filter((segment) => segment.emitSchema).forEach((segment, i) => { if(segment.segmentType !== 'mixin') { %>
|
|
9
10
|
import segment<%= i %> from './<%= t.schemaOutDir %>/<%= segment.segmentName || t.ROOT_SEGMENT_FILE_NAME %>.json' with { type: "json" };
|
|
@@ -28,3 +29,7 @@ export const schema = {
|
|
|
28
29
|
<% } %>
|
|
29
30
|
}
|
|
30
31
|
};
|
|
32
|
+
|
|
33
|
+
<% if (t.hasMixins) { %>
|
|
34
|
+
export { Mixins };
|
|
35
|
+
<% } %>
|
|
@@ -8,7 +8,7 @@ import type { Controllers as Controllers<%= i %> } from "<%= t.segmentMeta[segme
|
|
|
8
8
|
<% }
|
|
9
9
|
});
|
|
10
10
|
if (t.hasMixins) { %>
|
|
11
|
-
import type { Controllers as MixinControllers } from "./mixins.d.ts";
|
|
11
|
+
import type { Controllers as MixinControllers, Mixins } from "./mixins.d.ts";
|
|
12
12
|
<% }
|
|
13
13
|
if (t.imports.validateOnClient) { %>
|
|
14
14
|
import { validateOnClient } from '<%= t.imports.validateOnClient %>';
|
|
@@ -24,4 +24,7 @@ export const <%= rpcModuleName %> = createRPC<<%= segment.segmentType === 'mixin
|
|
|
24
24
|
);
|
|
25
25
|
<% })
|
|
26
26
|
}) %>
|
|
27
|
-
export { schema };
|
|
27
|
+
export { schema };
|
|
28
|
+
<% if (t.hasMixins) { %>
|
|
29
|
+
export { Mixins };
|
|
30
|
+
<% } %>
|
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
import _ from 'lodash';
|
|
5
|
-
import { openAPIToVovkSchema
|
|
5
|
+
import { openAPIToVovkSchema } from 'vovk';
|
|
6
6
|
import getClientTemplateFiles from './getClientTemplateFiles.mjs';
|
|
7
7
|
import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
|
|
8
8
|
import pickSegmentFullSchema from '../utils/pickSegmentFullSchema.mjs';
|
|
@@ -86,38 +86,19 @@ export async function generate({ isEnsuringClient = false, projectInfo, forceNot
|
|
|
86
86
|
...config.openApiMixins,
|
|
87
87
|
...cliOptionsToOpenAPIMixins(cliGenerateOptions ?? {}),
|
|
88
88
|
};
|
|
89
|
+
/** @deprecated */
|
|
89
90
|
let hasMixins = false;
|
|
90
91
|
if (Object.keys(allOpenAPIMixins).length) {
|
|
91
|
-
const mixins = Object.fromEntries(Object.entries(await normalizeOpenAPIMixins({ mixinModules: allOpenAPIMixins })).map(([mixinName, conf]) =>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
...conf,
|
|
96
|
-
mixinName,
|
|
97
|
-
}).segments[mixinName],
|
|
98
|
-
];
|
|
99
|
-
}));
|
|
92
|
+
const mixins = Object.fromEntries(Object.entries(await normalizeOpenAPIMixins({ mixinModules: allOpenAPIMixins })).map(([mixinName, conf]) => [
|
|
93
|
+
mixinName,
|
|
94
|
+
openAPIToVovkSchema({ ...conf, mixinName }).segments[mixinName],
|
|
95
|
+
]));
|
|
100
96
|
hasMixins = true;
|
|
101
97
|
fullSchema = {
|
|
102
98
|
...fullSchema,
|
|
103
99
|
segments: {
|
|
104
100
|
...fullSchema.segments,
|
|
105
|
-
...
|
|
106
|
-
return [
|
|
107
|
-
segmentName,
|
|
108
|
-
{
|
|
109
|
-
$schema: VovkSchemaIdEnum.SEGMENT,
|
|
110
|
-
emitSchema: true,
|
|
111
|
-
segmentType: 'mixin',
|
|
112
|
-
segmentName,
|
|
113
|
-
forceApiRoot: mixin?.forceApiRoot, // TODO: Merging with existing segments and using apiRoot doesn't make a lot of sense
|
|
114
|
-
controllers: {
|
|
115
|
-
...fullSchema.segments[segmentName]?.controllers,
|
|
116
|
-
...mixin?.controllers,
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
];
|
|
120
|
-
})),
|
|
101
|
+
...mixins,
|
|
121
102
|
},
|
|
122
103
|
};
|
|
123
104
|
}
|
|
@@ -14,6 +14,7 @@ export default function getClientTemplateFiles({ config, cwd, log, configKey, cl
|
|
|
14
14
|
log: ProjectInfo['log'];
|
|
15
15
|
configKey: 'composedClient' | 'segmentedClient';
|
|
16
16
|
cliGenerateOptions?: GenerateOptions;
|
|
17
|
+
/** @deprecated */
|
|
17
18
|
hasMixins: boolean;
|
|
18
19
|
}): Promise<{
|
|
19
20
|
fromTemplates: string[];
|
|
@@ -31,6 +31,7 @@ export default async function getClientTemplateFiles({ config, cwd, log, configK
|
|
|
31
31
|
}
|
|
32
32
|
usedTemplateDefs[templateName] = usedDef;
|
|
33
33
|
}
|
|
34
|
+
// $openapi['github']['components']['schemas']['User'];
|
|
34
35
|
const templateFiles = [];
|
|
35
36
|
const entries = Object.entries(usedTemplateDefs);
|
|
36
37
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -7,7 +7,7 @@ import * as YAML from 'yaml';
|
|
|
7
7
|
import TOML from '@iarna/toml';
|
|
8
8
|
import prettify from '../utils/prettify.mjs';
|
|
9
9
|
import { ROOT_SEGMENT_FILE_NAME } from '../dev/writeOneSegmentSchemaFile.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import { compileJSONSchemaToTypeScriptType } from '../utils/compileJSONSchemaToTypeScriptType.mjs';
|
|
11
11
|
export default async function writeOneClientFile({ cwd, projectInfo, clientTemplateFile, fullSchema, prettifyClient, segmentName, imports, templateContent, matterResult: { data, content }, package: packageJson, isEnsuringClient, outCwdRelativeDir, origin, templateDef, locatedSegments, isNodeNextResolution, hasMixins, isVovkProject, }) {
|
|
12
12
|
const { config, apiRoot } = projectInfo;
|
|
13
13
|
const { templateFilePath, relativeDir } = clientTemplateFile;
|
|
@@ -29,7 +29,7 @@ export default async function writeOneClientFile({ cwd, projectInfo, clientTempl
|
|
|
29
29
|
schema: fullSchema,
|
|
30
30
|
VovkSchemaIdEnum,
|
|
31
31
|
createCodeExamples,
|
|
32
|
-
|
|
32
|
+
compileJSONSchemaToTypeScriptType,
|
|
33
33
|
YAML,
|
|
34
34
|
TOML,
|
|
35
35
|
nodeNextResolutionExt: {
|
|
@@ -74,8 +74,9 @@ export default async function writeOneClientFile({ cwd, projectInfo, clientTempl
|
|
|
74
74
|
}
|
|
75
75
|
// Render the template
|
|
76
76
|
let rendered = templateFilePath.endsWith('.ejs')
|
|
77
|
-
? ejs.render(content, { t }, {
|
|
77
|
+
? await ejs.render(content, { t }, {
|
|
78
78
|
filename: templateFilePath,
|
|
79
|
+
async: true,
|
|
79
80
|
})
|
|
80
81
|
: templateContent;
|
|
81
82
|
// Optionally prettify
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { JSONSchema } from 'json-schema-to-typescript';
|
|
2
|
+
import { OpenAPIObject } from 'openapi3-ts/oas31';
|
|
3
|
+
export declare function compileJSONSchemaToTypeScriptType(schema: JSONSchema, typeName: string, components?: NonNullable<OpenAPIObject['components']>): Promise<string>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { compile } from 'json-schema-to-typescript';
|
|
2
|
+
export async function compileJSONSchemaToTypeScriptType(schema, typeName, components = {}) {
|
|
3
|
+
if (!schema)
|
|
4
|
+
return '';
|
|
5
|
+
const tsType = await compile({ ...schema, components }, typeName, {
|
|
6
|
+
bannerComment: schema.description ? `/**\n * ${schema.description}\n */` : '',
|
|
7
|
+
style: {
|
|
8
|
+
bracketSpacing: true,
|
|
9
|
+
printWidth: 80,
|
|
10
|
+
semi: true,
|
|
11
|
+
singleQuote: true,
|
|
12
|
+
tabWidth: 2,
|
|
13
|
+
useTabs: false,
|
|
14
|
+
trailingComma: 'all',
|
|
15
|
+
},
|
|
16
|
+
// Don't generate separate interfaces for additionalProperties
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
// Enable strict null checks
|
|
19
|
+
strictIndexSignatures: true,
|
|
20
|
+
// Don't add schema as comment
|
|
21
|
+
});
|
|
22
|
+
return tsType;
|
|
23
|
+
}
|
|
24
|
+
/*
|
|
25
|
+
// Extend JSONSchema to include custom x-formData property
|
|
26
|
+
interface ExtendedJSONSchema extends JSONSchema {
|
|
27
|
+
'x-formData'?: boolean;
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface OpenAPIDocument {
|
|
32
|
+
openapi: string;
|
|
33
|
+
components?: {
|
|
34
|
+
schemas?: Record<string, ExtendedJSONSchema>;
|
|
35
|
+
};
|
|
36
|
+
[key: string]: any;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Converts OpenAPI 3.1 component schemas to TypeScript type definitions
|
|
41
|
+
* @param oasDoc - The OpenAPI 3.1 document
|
|
42
|
+
* @param options - Optional configuration for the TypeScript generation
|
|
43
|
+
* @returns A string containing all TypeScript type definitions
|
|
44
|
+
* /
|
|
45
|
+
export async function oasToTypeScript(
|
|
46
|
+
oasDoc: OpenAPIDocument,
|
|
47
|
+
options?: {
|
|
48
|
+
bannerComment?: string;
|
|
49
|
+
style?: {
|
|
50
|
+
bracketSpacing?: boolean;
|
|
51
|
+
printWidth?: number;
|
|
52
|
+
semi?: boolean;
|
|
53
|
+
singleQuote?: boolean;
|
|
54
|
+
tabWidth?: number;
|
|
55
|
+
useTabs?: boolean;
|
|
56
|
+
trailingComma?: 'all' | 'es5' | 'none';
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
): Promise<string> {
|
|
60
|
+
// Validate that this is an OAS 3.1 document
|
|
61
|
+
if (!oasDoc.openapi || !oasDoc.openapi.startsWith('3.1')) {
|
|
62
|
+
throw new Error('Document must be an OpenAPI 3.1 specification');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Extract schemas from components
|
|
66
|
+
const schemas = oasDoc.components?.schemas || {};
|
|
67
|
+
|
|
68
|
+
if (Object.keys(schemas).length === 0) {
|
|
69
|
+
return '// No schemas found in components';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const typeDefinitions: string[] = [];
|
|
73
|
+
|
|
74
|
+
// Process each schema
|
|
75
|
+
for (const [schemaName, schema] of Object.entries(schemas)) {
|
|
76
|
+
try {
|
|
77
|
+
// Check if schema has x-formData: true
|
|
78
|
+
if (schema['x-formData'] === true) {
|
|
79
|
+
// Generate a simple type alias to FormData
|
|
80
|
+
typeDefinitions.push(`export type ${schemaName} = FormData;\n`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle schema references
|
|
85
|
+
const resolvedSchema = resolveSchemaReferences(schema, schemas);
|
|
86
|
+
|
|
87
|
+
// Compile to TypeScript
|
|
88
|
+
const tsType = await compile(resolvedSchema as JSONSchema, schemaName, {
|
|
89
|
+
bannerComment: options?.bannerComment || '',
|
|
90
|
+
style: options?.style || {
|
|
91
|
+
bracketSpacing: true,
|
|
92
|
+
printWidth: 80,
|
|
93
|
+
semi: true,
|
|
94
|
+
singleQuote: true,
|
|
95
|
+
tabWidth: 2,
|
|
96
|
+
useTabs: false,
|
|
97
|
+
trailingComma: 'all',
|
|
98
|
+
},
|
|
99
|
+
// Don't generate separate interfaces for additionalProperties
|
|
100
|
+
additionalProperties: false,
|
|
101
|
+
// Enable strict null checks
|
|
102
|
+
strictIndexSignatures: true,
|
|
103
|
+
// Don't add schema as comment
|
|
104
|
+
$refOptions: {
|
|
105
|
+
resolve: {
|
|
106
|
+
// Resolve internal references
|
|
107
|
+
internal: true,
|
|
108
|
+
// Add our custom resolver
|
|
109
|
+
custom: {
|
|
110
|
+
order: 1,
|
|
111
|
+
canRead: (ref: string) => ref.startsWith('#/components/schemas/'),
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
typeDefinitions.push(tsType);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`Error processing schema "${schemaName}":`, error);
|
|
120
|
+
typeDefinitions.push(`// Error processing schema "${schemaName}": ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return typeDefinitions.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolves $ref references within a schema
|
|
129
|
+
* /
|
|
130
|
+
function resolveSchemaReferences(
|
|
131
|
+
schema: any,
|
|
132
|
+
allSchemas: Record<string, ExtendedJSONSchema>,
|
|
133
|
+
visited = new Set<string>()
|
|
134
|
+
): any {
|
|
135
|
+
if (!schema || typeof schema !== 'object') {
|
|
136
|
+
return schema;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle $ref
|
|
140
|
+
if (schema.$ref && typeof schema.$ref === 'string') {
|
|
141
|
+
const refPath = schema.$ref.replace('#/components/schemas/', '');
|
|
142
|
+
|
|
143
|
+
// Prevent circular references
|
|
144
|
+
if (visited.has(refPath)) {
|
|
145
|
+
return { type: 'object', additionalProperties: true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
visited.add(refPath);
|
|
149
|
+
const referencedSchema = allSchemas[refPath];
|
|
150
|
+
|
|
151
|
+
if (referencedSchema) {
|
|
152
|
+
return resolveSchemaReferences(referencedSchema, allSchemas, visited);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return schema;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle arrays
|
|
159
|
+
if (Array.isArray(schema)) {
|
|
160
|
+
return schema.map((item) => resolveSchemaReferences(item, allSchemas, visited));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Handle objects recursively
|
|
164
|
+
const resolved: any = {};
|
|
165
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
166
|
+
resolved[key] = resolveSchemaReferences(value, allSchemas, visited);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return resolved;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Example usage:
|
|
173
|
+
/*
|
|
174
|
+
const oasDoc = {
|
|
175
|
+
openapi: '3.1.0',
|
|
176
|
+
info: {
|
|
177
|
+
title: 'My API',
|
|
178
|
+
version: '1.0.0'
|
|
179
|
+
},
|
|
180
|
+
components: {
|
|
181
|
+
schemas: {
|
|
182
|
+
User: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
id: { type: 'integer' },
|
|
186
|
+
name: { type: 'string' },
|
|
187
|
+
email: { type: 'string', format: 'email' },
|
|
188
|
+
roles: {
|
|
189
|
+
type: 'array',
|
|
190
|
+
items: { $ref: '#/components/schemas/Role' }
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
required: ['id', 'name', 'email']
|
|
194
|
+
},
|
|
195
|
+
Role: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
properties: {
|
|
198
|
+
id: { type: 'integer' },
|
|
199
|
+
name: { type: 'string' },
|
|
200
|
+
permissions: {
|
|
201
|
+
type: 'array',
|
|
202
|
+
items: { type: 'string' }
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
required: ['id', 'name']
|
|
206
|
+
},
|
|
207
|
+
UploadFileRequest: {
|
|
208
|
+
'x-formData': true,
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
file: { type: 'string', format: 'binary' },
|
|
212
|
+
description: { type: 'string' }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const types = await oasToTypeScript(oasDoc);
|
|
220
|
+
console.log(types);
|
|
221
|
+
// Output will include:
|
|
222
|
+
// export interface User { ... }
|
|
223
|
+
// export interface Role { ... }
|
|
224
|
+
// export type UploadFileRequest = FormData;
|
|
225
|
+
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vovk-cli",
|
|
3
|
-
"version": "0.0.1-draft.
|
|
3
|
+
"version": "0.0.1-draft.256",
|
|
4
4
|
"bin": {
|
|
5
5
|
"vovk": "./dist/index.mjs"
|
|
6
6
|
},
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"glob": "^11.0.2",
|
|
61
61
|
"gray-matter": "^4.0.3",
|
|
62
62
|
"inflection": "^3.0.2",
|
|
63
|
+
"json-schema-to-typescript": "^15.0.4",
|
|
63
64
|
"jsonc-parser": "^3.3.1",
|
|
64
65
|
"lodash": "^4.17.21",
|
|
65
66
|
"loglevel": "^1.9.2",
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
export function convertJSONSchemaToTypeScriptDef(schema, defsPrefix) {
|
|
2
|
-
if (!schema)
|
|
3
|
-
return { $type: 'null', $defs: 'null' };
|
|
4
|
-
// Helper function to escape single quotes in string literals
|
|
5
|
-
const escapeStringLiteral = (str) => {
|
|
6
|
-
return str.replace(/'/g, "\\'");
|
|
7
|
-
};
|
|
8
|
-
// Helper function to escape JSDoc comment closing sequences
|
|
9
|
-
const escapeJSDocComment = (str) => {
|
|
10
|
-
return str.replace(/\*\//g, '*\\/');
|
|
11
|
-
};
|
|
12
|
-
// Helper function to check if a property name is a valid JavaScript identifier
|
|
13
|
-
const isValidIdentifier = (name) => {
|
|
14
|
-
// Check if it matches valid JavaScript identifier pattern and is not a reserved word
|
|
15
|
-
return (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) &&
|
|
16
|
-
![
|
|
17
|
-
'break',
|
|
18
|
-
'case',
|
|
19
|
-
'catch',
|
|
20
|
-
'class',
|
|
21
|
-
'const',
|
|
22
|
-
'continue',
|
|
23
|
-
'debugger',
|
|
24
|
-
'default',
|
|
25
|
-
'delete',
|
|
26
|
-
'do',
|
|
27
|
-
'else',
|
|
28
|
-
'export',
|
|
29
|
-
'extends',
|
|
30
|
-
'false',
|
|
31
|
-
'finally',
|
|
32
|
-
'for',
|
|
33
|
-
'function',
|
|
34
|
-
'if',
|
|
35
|
-
'import',
|
|
36
|
-
'in',
|
|
37
|
-
'instanceof',
|
|
38
|
-
'new',
|
|
39
|
-
'null',
|
|
40
|
-
'return',
|
|
41
|
-
'super',
|
|
42
|
-
'switch',
|
|
43
|
-
'this',
|
|
44
|
-
'throw',
|
|
45
|
-
'true',
|
|
46
|
-
'try',
|
|
47
|
-
'typeof',
|
|
48
|
-
'var',
|
|
49
|
-
'void',
|
|
50
|
-
'while',
|
|
51
|
-
'with',
|
|
52
|
-
'let',
|
|
53
|
-
'static',
|
|
54
|
-
'yield',
|
|
55
|
-
'enum',
|
|
56
|
-
'await',
|
|
57
|
-
'implements',
|
|
58
|
-
'interface',
|
|
59
|
-
'package',
|
|
60
|
-
'private',
|
|
61
|
-
'protected',
|
|
62
|
-
'public',
|
|
63
|
-
].includes(name));
|
|
64
|
-
};
|
|
65
|
-
// Helper function to format property name (with quotes if needed)
|
|
66
|
-
const formatPropertyName = (name) => {
|
|
67
|
-
if (isValidIdentifier(name)) {
|
|
68
|
-
return name;
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
return `'${escapeStringLiteral(name)}'`;
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
// Helper function to extract type name from $ref
|
|
75
|
-
const getRefTypeName = (ref) => {
|
|
76
|
-
if (ref.startsWith('#/definitions/') || ref.startsWith('#/$defs/')) {
|
|
77
|
-
const path = ref.split('/');
|
|
78
|
-
return `${defsPrefix}__${path[path.length - 1].replace(/[^a-zA-Z0-9_$]/g, '_')}`;
|
|
79
|
-
}
|
|
80
|
-
return 'KnownAny'; // Fallback for external references
|
|
81
|
-
};
|
|
82
|
-
// Helper function to get JSDoc from schema
|
|
83
|
-
const getJSDoc = (schema, indentation = '') => {
|
|
84
|
-
if (typeof schema === 'boolean') {
|
|
85
|
-
return '';
|
|
86
|
-
}
|
|
87
|
-
const description = schema.description || schema.title;
|
|
88
|
-
if (!description) {
|
|
89
|
-
return '';
|
|
90
|
-
}
|
|
91
|
-
const safeDescription = escapeJSDocComment(description);
|
|
92
|
-
return `${indentation}/**\n${indentation} * ${safeDescription}\n${indentation} */`;
|
|
93
|
-
};
|
|
94
|
-
// Helper function to convert schema to TypeScript type
|
|
95
|
-
const schemaToType = (schema, indentation = ' ') => {
|
|
96
|
-
if (!schema) {
|
|
97
|
-
return 'null';
|
|
98
|
-
}
|
|
99
|
-
if (typeof schema === 'boolean') {
|
|
100
|
-
return schema ? 'KnownAny' : 'never';
|
|
101
|
-
}
|
|
102
|
-
// Handle $ref references - check this first before other properties
|
|
103
|
-
if (schema.$ref) {
|
|
104
|
-
return getRefTypeName(schema.$ref);
|
|
105
|
-
}
|
|
106
|
-
if ('x-formData' in schema) {
|
|
107
|
-
return `FormData`; // Special case for form data
|
|
108
|
-
}
|
|
109
|
-
if (schema.enum) {
|
|
110
|
-
return schema.enum
|
|
111
|
-
.map((value) => {
|
|
112
|
-
if (typeof value === 'string') {
|
|
113
|
-
return `'${escapeStringLiteral(value)}'`;
|
|
114
|
-
}
|
|
115
|
-
else if (value === null) {
|
|
116
|
-
return 'null';
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
return String(value);
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
.join(' | ');
|
|
123
|
-
}
|
|
124
|
-
if (schema.const !== undefined) {
|
|
125
|
-
if (typeof schema.const === 'string') {
|
|
126
|
-
return `'${escapeStringLiteral(schema.const)}'`;
|
|
127
|
-
}
|
|
128
|
-
else if (schema.const === null) {
|
|
129
|
-
return 'null';
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
return String(schema.const);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (schema.oneOf) {
|
|
136
|
-
return schema.oneOf.map((s) => schemaToType(s, indentation)).join(' | ');
|
|
137
|
-
}
|
|
138
|
-
if (schema.anyOf) {
|
|
139
|
-
return schema.anyOf.map((s) => schemaToType(s, indentation)).join(' | ');
|
|
140
|
-
}
|
|
141
|
-
if (schema.allOf) {
|
|
142
|
-
return schema.allOf.map((s) => schemaToType(s, indentation)).join(' & ');
|
|
143
|
-
}
|
|
144
|
-
if (schema.type === 'object' || schema.properties) {
|
|
145
|
-
const properties = schema.properties || {};
|
|
146
|
-
const required = schema.required || [];
|
|
147
|
-
const propertyEntries = Object.entries(properties);
|
|
148
|
-
if (propertyEntries.length === 0) {
|
|
149
|
-
// Handle additional properties
|
|
150
|
-
if (schema.additionalProperties) {
|
|
151
|
-
if (typeof schema.additionalProperties === 'boolean') {
|
|
152
|
-
return schema.additionalProperties ? 'Record<string, KnownAny>' : '{}';
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
const valueType = schemaToType(schema.additionalProperties, indentation);
|
|
156
|
-
return `Record<string, ${valueType}>`;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return '{}';
|
|
160
|
-
}
|
|
161
|
-
const props = propertyEntries
|
|
162
|
-
.map(([propName, propSchema]) => {
|
|
163
|
-
if (typeof propSchema === 'boolean') {
|
|
164
|
-
const type = propSchema ? 'KnownAny' : 'never';
|
|
165
|
-
const isOptional = !required.includes(propName);
|
|
166
|
-
const jsDoc = getJSDoc(propSchema, indentation);
|
|
167
|
-
return `${jsDoc}\n${indentation}${formatPropertyName(propName)}${isOptional ? '?' : ''}: ${type};`;
|
|
168
|
-
}
|
|
169
|
-
const isOptional = !required.includes(propName);
|
|
170
|
-
const defaultValue = propSchema.default !== undefined ? ` // default: ${JSON.stringify(propSchema.default)}` : '';
|
|
171
|
-
const jsDoc = getJSDoc(propSchema, indentation);
|
|
172
|
-
const propType = schemaToType(propSchema, indentation + ' ');
|
|
173
|
-
return [
|
|
174
|
-
`${jsDoc}`,
|
|
175
|
-
`${indentation}${formatPropertyName(propName)}${isOptional ? '?' : ''}: ${propType};${defaultValue}`,
|
|
176
|
-
]
|
|
177
|
-
.filter(Boolean)
|
|
178
|
-
.join('\n');
|
|
179
|
-
})
|
|
180
|
-
.join('\n');
|
|
181
|
-
// Handle additional properties
|
|
182
|
-
let additionalPropsType = '';
|
|
183
|
-
if (schema.additionalProperties) {
|
|
184
|
-
if (typeof schema.additionalProperties === 'boolean') {
|
|
185
|
-
if (schema.additionalProperties) {
|
|
186
|
-
additionalPropsType = `\n${indentation}[key: string]: KnownAny;`;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
const valueType = schemaToType(schema.additionalProperties, indentation + ' ');
|
|
191
|
-
additionalPropsType = `\n${indentation}[key: string]: ${valueType};`;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return `{\n${props}${additionalPropsType}\n${indentation.slice(2)}}`;
|
|
195
|
-
}
|
|
196
|
-
if (schema.type === 'array' && schema.items) {
|
|
197
|
-
if (Array.isArray(schema.items)) {
|
|
198
|
-
// Tuple
|
|
199
|
-
const tupleTypes = schema.items.map((item) => schemaToType(item, indentation));
|
|
200
|
-
let tupleType = `[${tupleTypes.join(', ')}]`;
|
|
201
|
-
// Handle additional items
|
|
202
|
-
if (schema.additionalItems === true) {
|
|
203
|
-
tupleType += ' & KnownAny[]';
|
|
204
|
-
}
|
|
205
|
-
else if (typeof schema.additionalItems === 'object') {
|
|
206
|
-
const additionalType = schemaToType(schema.additionalItems, indentation);
|
|
207
|
-
tupleType += ` & ${additionalType}[]`;
|
|
208
|
-
}
|
|
209
|
-
return tupleType;
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
// Array
|
|
213
|
-
return `${schemaToType(schema.items, indentation)}[]`;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
// Handle multiple types
|
|
217
|
-
if (Array.isArray(schema.type)) {
|
|
218
|
-
return schema.type
|
|
219
|
-
.map((t) => {
|
|
220
|
-
const singleTypeSchema = { ...schema, type: t };
|
|
221
|
-
singleTypeSchema.type = t;
|
|
222
|
-
return schemaToType(singleTypeSchema, indentation);
|
|
223
|
-
})
|
|
224
|
-
.join(' | ');
|
|
225
|
-
}
|
|
226
|
-
// Handle primitive types
|
|
227
|
-
switch (schema.type) {
|
|
228
|
-
case 'string':
|
|
229
|
-
return 'string';
|
|
230
|
-
case 'number':
|
|
231
|
-
case 'integer':
|
|
232
|
-
return 'number';
|
|
233
|
-
case 'boolean':
|
|
234
|
-
return 'boolean';
|
|
235
|
-
case 'null':
|
|
236
|
-
return 'null';
|
|
237
|
-
case 'array':
|
|
238
|
-
return 'KnownAny[]'; // For arrays with no items defined
|
|
239
|
-
default:
|
|
240
|
-
return 'undefined';
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
const defsToType = (defs) => {
|
|
244
|
-
return Object.entries(defs)
|
|
245
|
-
.map(([defName, defSchema]) => {
|
|
246
|
-
if (defSchema) {
|
|
247
|
-
const jsDoc = getJSDoc(defSchema);
|
|
248
|
-
const defType = schemaToType(defSchema);
|
|
249
|
-
return [jsDoc, `type ${defsPrefix}__${defName.replace(/[^a-zA-Z0-9_$]/g, '_')} = ${defType};`]
|
|
250
|
-
.filter(Boolean)
|
|
251
|
-
.join('\n');
|
|
252
|
-
}
|
|
253
|
-
return '';
|
|
254
|
-
})
|
|
255
|
-
.filter(Boolean)
|
|
256
|
-
.join('\n');
|
|
257
|
-
};
|
|
258
|
-
// Process definitions from both $defs and definitions properties
|
|
259
|
-
const allDefs = {
|
|
260
|
-
...(schema.definitions || {}),
|
|
261
|
-
...(schema.$defs || {}),
|
|
262
|
-
};
|
|
263
|
-
// Generate the main type
|
|
264
|
-
const jsDoc = getJSDoc(schema);
|
|
265
|
-
const mainType = schemaToType(schema);
|
|
266
|
-
const defsType = defsToType(allDefs);
|
|
267
|
-
return {
|
|
268
|
-
$type: jsDoc ? `${jsDoc}\n${mainType}` : mainType,
|
|
269
|
-
$defs: defsType,
|
|
270
|
-
};
|
|
271
|
-
}
|