@opra/cli 0.25.5 → 0.26.0
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/cjs/api-exporter/api-exporter.js +13 -7
- package/cjs/api-exporter/process-resources.js +93 -48
- package/cjs/api-exporter/process-types.js +49 -39
- package/cjs/api-exporter/ts-file.js +45 -32
- package/esm/api-exporter/api-exporter.js +14 -8
- package/esm/api-exporter/process-resources.js +92 -47
- package/esm/api-exporter/process-types.js +49 -39
- package/esm/api-exporter/ts-file.js +44 -30
- package/package.json +2 -2
- package/types/api-exporter/api-exporter.d.ts +2 -2
- package/types/api-exporter/process-resources.d.ts +3 -5
- package/types/api-exporter/process-types.d.ts +2 -2
- package/types/api-exporter/ts-file.d.ts +8 -7
|
@@ -33,7 +33,7 @@ class ApiExporter {
|
|
|
33
33
|
async execute() {
|
|
34
34
|
this.logger.log(chalk_1.default.cyan('Fetching service metadata from'), chalk_1.default.whiteBright(this.client.serviceUrl));
|
|
35
35
|
this.document = await this.client.getMetadata();
|
|
36
|
-
this.logger.log(chalk_1.default.cyan('Retrieved service info:\n'), chalk_1.default.white('Title:'), chalk_1.default.whiteBright(this.document.info.title), '\n', chalk_1.default.white('Version:'), chalk_1.default.whiteBright(this.document.info.version), '\n'
|
|
36
|
+
this.logger.log(chalk_1.default.cyan('Retrieved service info:\n'), chalk_1.default.white('Title:'), chalk_1.default.whiteBright(this.document.info.title), '\n', chalk_1.default.white('Version:'), chalk_1.default.whiteBright(this.document.info.version), '\n');
|
|
37
37
|
this.serviceClassName = (this.serviceClassName || this.document.info.title || 'Service1').replace(/[^\w_$]*/g, '');
|
|
38
38
|
this.serviceClassName = this.serviceClassName.charAt(0).toUpperCase() + this.serviceClassName.substring(1);
|
|
39
39
|
this.fileHeader += `/*
|
|
@@ -45,27 +45,33 @@ class ApiExporter {
|
|
|
45
45
|
this.cleanDirectory(this.outDir);
|
|
46
46
|
this.logger.log(chalk_1.default.cyan(`Generating service interface ( ${chalk_1.default.whiteBright(this.serviceClassName)} )`));
|
|
47
47
|
node_fs_1.default.mkdirSync(this.outDir, { recursive: true });
|
|
48
|
+
this.logger.log(chalk_1.default.cyan('Processing types'));
|
|
48
49
|
await this.processTypes();
|
|
49
|
-
|
|
50
|
+
this.logger.log(chalk_1.default.cyan('Processing resources'));
|
|
51
|
+
const rootTs = this.addFile('/' + this.serviceClassName + '.ts');
|
|
52
|
+
await this.processResource(this.document.root, this.serviceClassName, rootTs);
|
|
50
53
|
const { importExt } = this;
|
|
51
54
|
// Write files
|
|
52
55
|
for (const file of Object.values(this.files)) {
|
|
53
|
-
const
|
|
56
|
+
const filename = node_path_1.default.join(this.outDir, file.filename);
|
|
57
|
+
const targetDir = node_path_1.default.dirname(filename);
|
|
54
58
|
node_fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
55
|
-
await this.writer.writeFile(
|
|
59
|
+
await this.writer.writeFile(filename, file.generate({ importExt }));
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
getFile(filePath) {
|
|
59
|
-
return this.files[
|
|
63
|
+
return this.files[filePath];
|
|
60
64
|
}
|
|
61
65
|
addFile(filePath, returnExists) {
|
|
66
|
+
if (!(filePath.startsWith('.') || filePath.startsWith('/')))
|
|
67
|
+
filePath = './' + filePath;
|
|
62
68
|
let file = this.getFile(filePath);
|
|
63
69
|
if (file) {
|
|
64
70
|
if (returnExists)
|
|
65
71
|
return file;
|
|
66
72
|
throw new Error(`File "${filePath}" already exists`);
|
|
67
73
|
}
|
|
68
|
-
file = new ts_file_js_1.TsFile(
|
|
74
|
+
file = new ts_file_js_1.TsFile(filePath);
|
|
69
75
|
file.header = this.fileHeader;
|
|
70
76
|
this.files[file.filename] = file;
|
|
71
77
|
return file;
|
|
@@ -97,7 +103,7 @@ class ApiExporter {
|
|
|
97
103
|
}
|
|
98
104
|
exports.ApiExporter = ApiExporter;
|
|
99
105
|
(() => {
|
|
100
|
-
ApiExporter.prototype.
|
|
106
|
+
ApiExporter.prototype.processResource = process_resources_js_1.processResource;
|
|
101
107
|
ApiExporter.prototype.processTypes = process_types_js_1.processTypes;
|
|
102
108
|
ApiExporter.prototype.generateTypeFile = process_types_js_1.generateTypeFile;
|
|
103
109
|
ApiExporter.prototype.generateComplexTypeDefinition = process_types_js_1.generateComplexTypeDefinition;
|
|
@@ -1,62 +1,107 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.processResource = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
6
5
|
const node_path_1 = tslib_1.__importDefault(require("node:path"));
|
|
7
6
|
const common_1 = require("@opra/common");
|
|
8
7
|
const string_utils_js_1 = require("../utils/string-utils.js");
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* @param targetDir
|
|
12
|
-
*/
|
|
13
|
-
async function processResources(targetDir = '') {
|
|
14
|
-
this.logger.log(chalk_1.default.cyan('Processing resources'));
|
|
15
|
-
const { document } = this;
|
|
16
|
-
const serviceTs = this.addFile(node_path_1.default.join(targetDir, this.serviceClassName + '.ts'));
|
|
17
|
-
serviceTs.addImportPackage('@opra/client', ['HttpServiceBase']);
|
|
8
|
+
async function processResource(resource, className, tsFile) {
|
|
9
|
+
tsFile.addImport('@opra/client', ['HttpServiceBase', 'kClient', 'kContext']);
|
|
18
10
|
const indexTs = this.addFile('/index.ts', true);
|
|
19
|
-
indexTs.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
indexTs.addExport(tsFile.filename);
|
|
12
|
+
tsFile.content = `\n
|
|
13
|
+
/**
|
|
14
|
+
* ${(0, string_utils_js_1.wrapJSDocString)(resource.description || '')}
|
|
15
|
+
* @class ${className}
|
|
16
|
+
* @url ${node_path_1.default.posix.join(this.client.serviceUrl, '#resources/' + className)}
|
|
17
|
+
*/
|
|
18
|
+
export class ${className} extends HttpServiceBase {\n`;
|
|
19
|
+
if (resource instanceof common_1.Container) {
|
|
20
|
+
for (const child of resource.resources.values()) {
|
|
21
|
+
// Determine class name of child resource
|
|
22
|
+
let childClassName = '';
|
|
23
|
+
if (child instanceof common_1.Container)
|
|
24
|
+
childClassName = child.name.charAt(0).toUpperCase() + child.name.substring(1) + 'Container';
|
|
25
|
+
else
|
|
26
|
+
childClassName = child.name + 'Resource';
|
|
27
|
+
// Create TsFile for child resource
|
|
28
|
+
const dir = node_path_1.default.dirname(tsFile.filename);
|
|
29
|
+
const basename = dir === '/'
|
|
30
|
+
? 'root'
|
|
31
|
+
: node_path_1.default.basename(tsFile.filename, node_path_1.default.extname(tsFile.filename));
|
|
32
|
+
const childFile = this.addFile(node_path_1.default.join(dir, basename, child.name + '.ts'));
|
|
33
|
+
await this.processResource(child, childClassName, childFile);
|
|
34
|
+
tsFile.addImport(childFile.filename, [childClassName]);
|
|
35
|
+
tsFile.content += `
|
|
23
36
|
/**
|
|
24
|
-
* ${(0, string_utils_js_1.wrapJSDocString)(
|
|
25
|
-
* @url ${node_path_1.default.posix.join(this.client.serviceUrl, '#resources/' +
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const operations = Object.keys(resource.operations)
|
|
32
|
-
.map(x => `'${x}'`).join(' | ');
|
|
33
|
-
if (!operations.length) {
|
|
34
|
-
this.logger.warn(chalk_1.default.yellow('WARN: ') +
|
|
35
|
-
`Ignoring "${chalk_1.default.whiteBright(resource.name)}" resource. No operations available.`);
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
serviceTs.content += jsDoc + `
|
|
39
|
-
get ${resource.name}(): Pick<HttpCollectionNode<${typeName}>, ${operations}> {
|
|
40
|
-
return this.$client.collection('${resource.name}');
|
|
37
|
+
* ${(0, string_utils_js_1.wrapJSDocString)(child.description || '')}
|
|
38
|
+
* @url ${node_path_1.default.posix.join(this.client.serviceUrl, '#resources/' + child.name)}
|
|
39
|
+
*/
|
|
40
|
+
get ${child.name}(): ${childClassName} {
|
|
41
|
+
if (!this[kContext].resources.${child.name})
|
|
42
|
+
this[kContext].resources.${child.name} = new ${childClassName}(this[kClient]);
|
|
43
|
+
return this[kContext].resources.${child.name};
|
|
41
44
|
}\n`;
|
|
42
45
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
}
|
|
47
|
+
else if (resource instanceof common_1.Collection) {
|
|
48
|
+
tsFile.addImport('@opra/client', ['HttpCollectionNode']);
|
|
49
|
+
const typeName = resource.type.name || '';
|
|
50
|
+
tsFile.addImport(`/types/${typeName}`, [typeName], true);
|
|
51
|
+
for (const [operation, endpoint] of resource.operations.entries()) {
|
|
52
|
+
tsFile.content += `
|
|
53
|
+
/**
|
|
54
|
+
* ${(0, string_utils_js_1.wrapJSDocString)(endpoint.description || '')}
|
|
55
|
+
*/
|
|
56
|
+
get ${operation}(): HttpCollectionNode<${typeName}>['${operation}'] {
|
|
57
|
+
if (!this[kContext].node)
|
|
58
|
+
this[kContext].node = this[kClient].collection('${resource.name}');
|
|
59
|
+
return this[kContext].node.${operation};
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (resource instanceof common_1.Singleton) {
|
|
65
|
+
tsFile.addImport('@opra/client', ['HttpSingletonNode']);
|
|
66
|
+
const typeName = resource.type.name || '';
|
|
67
|
+
tsFile.addImport(`/types/${typeName}`, [typeName], true);
|
|
68
|
+
for (const [operation, endpoint] of resource.operations.entries()) {
|
|
69
|
+
tsFile.content += `
|
|
70
|
+
/**
|
|
71
|
+
* ${(0, string_utils_js_1.wrapJSDocString)(endpoint.description || '')}
|
|
72
|
+
*/
|
|
73
|
+
get ${operation}(): HttpSingletonNode<${typeName}>['${operation}'] {
|
|
74
|
+
if (!this[kContext].node)
|
|
75
|
+
this[kContext].node = this[kClient].singleton('${resource.name}');
|
|
76
|
+
return this[kContext].node.${operation};
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (resource.actions.size) {
|
|
82
|
+
tsFile.addImport('@opra/client', ['HttpRequestObservable']);
|
|
83
|
+
for (const [action, endpoint] of resource.actions.entries()) {
|
|
84
|
+
const typeName = endpoint.returnType?.name || 'any';
|
|
85
|
+
const actionPath = resource.getFullPath() + '/' + action;
|
|
86
|
+
let params = '';
|
|
87
|
+
for (const prm of endpoint.parameters.values()) {
|
|
88
|
+
params += ` ${prm.name}: ${prm.type.name || 'any'}`;
|
|
89
|
+
if (prm.isArray)
|
|
90
|
+
params += '[]';
|
|
91
|
+
params += ';\n';
|
|
53
92
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
93
|
+
params = params ? '{\n' + params + ' }\n ' : '{}';
|
|
94
|
+
tsFile.content += `
|
|
95
|
+
/**
|
|
96
|
+
* ${(0, string_utils_js_1.wrapJSDocString)(endpoint.description || '')}
|
|
97
|
+
*/
|
|
98
|
+
${action}(params: ${params}): HttpRequestObservable<${typeName}> {
|
|
99
|
+
return this[kClient].action('${actionPath}', params);
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
58
102
|
}
|
|
59
103
|
}
|
|
60
|
-
|
|
104
|
+
tsFile.content += `}\n`;
|
|
105
|
+
return tsFile.content;
|
|
61
106
|
}
|
|
62
|
-
exports.
|
|
107
|
+
exports.processResource = processResource;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateMappedTypeDefinition = exports.generateUnionTypeDefinition = exports.generateEnumTypeDefinition = exports.generateSimpleTypeDefinition = exports.generateComplexTypeDefinition = exports.resolveTypeNameOrDef = exports.generateTypeFile = exports.processTypes = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
6
5
|
const node_path_1 = tslib_1.__importDefault(require("node:path"));
|
|
7
6
|
const common_1 = require("@opra/common");
|
|
8
7
|
const string_utils_js_1 = require("../utils/string-utils.js");
|
|
@@ -11,14 +10,10 @@ const internalTypeNames = ['any', 'boolean', 'bigint', 'number', 'null', 'string
|
|
|
11
10
|
*
|
|
12
11
|
* @param targetDir
|
|
13
12
|
*/
|
|
14
|
-
async function processTypes(
|
|
15
|
-
this.logger.log(chalk_1.default.cyan('Processing types'));
|
|
13
|
+
async function processTypes() {
|
|
16
14
|
const { document } = this;
|
|
17
|
-
const typesTs = this.addFile(node_path_1.default.join(targetDir, 'types.ts'));
|
|
18
15
|
for (const dataType of document.types.values()) {
|
|
19
|
-
|
|
20
|
-
if (expFile)
|
|
21
|
-
typesTs.addExportFile(expFile.filename);
|
|
16
|
+
await this.generateTypeFile(dataType);
|
|
22
17
|
}
|
|
23
18
|
}
|
|
24
19
|
exports.processTypes = processTypes;
|
|
@@ -27,42 +22,56 @@ exports.processTypes = processTypes;
|
|
|
27
22
|
* @param dataType
|
|
28
23
|
* @param targetDir
|
|
29
24
|
*/
|
|
30
|
-
async function generateTypeFile(dataType
|
|
25
|
+
async function generateTypeFile(dataType) {
|
|
31
26
|
const typeName = dataType.name;
|
|
32
27
|
if (typeName === 'any')
|
|
33
28
|
return;
|
|
34
29
|
if (!typeName)
|
|
35
30
|
throw new TypeError(`DataType has no name`);
|
|
36
|
-
|
|
31
|
+
const typesTs = this.addFile('/types.ts', true);
|
|
32
|
+
let filePath = '';
|
|
37
33
|
if (dataType instanceof common_1.SimpleType)
|
|
38
34
|
filePath = '/simple-types.ts';
|
|
39
|
-
else
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
else {
|
|
36
|
+
if (dataType instanceof common_1.EnumType)
|
|
37
|
+
filePath = node_path_1.default.join('/enums', typeName + '.ts');
|
|
38
|
+
else
|
|
39
|
+
filePath = node_path_1.default.join('/types', typeName + '.ts');
|
|
43
40
|
}
|
|
44
|
-
|
|
45
|
-
throw new TypeError(`Unimplemented DataType (${dataType.kind})`);
|
|
46
|
-
const file = this.addFile(node_path_1.default.join(targetDir, filePath), true);
|
|
41
|
+
const file = this.addFile(filePath, true);
|
|
47
42
|
if (file.exportTypes.includes(typeName))
|
|
48
43
|
return file;
|
|
49
44
|
file.exportTypes.push(typeName);
|
|
50
45
|
const indexTs = this.addFile('/index.ts', true);
|
|
51
|
-
indexTs.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
indexTs.addExport(file.filename);
|
|
47
|
+
// Export EnumType
|
|
48
|
+
if (dataType instanceof common_1.EnumType) {
|
|
49
|
+
file.content += `
|
|
50
|
+
/**\n * ${(0, string_utils_js_1.wrapJSDocString)(dataType.description || '')}
|
|
51
|
+
* @enum ${typeName}
|
|
55
52
|
* @url ${node_path_1.default.posix.join(this.client.serviceUrl, '#types/' + typeName)}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
file.content += `export type ${typeName} = ` + await this.generateSimpleTypeDefinition(file, dataType);
|
|
53
|
+
*/
|
|
54
|
+
export enum ${typeName} ` + await this.generateEnumTypeDefinition(file, dataType);
|
|
59
55
|
}
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
// Export ComplexType
|
|
57
|
+
if (dataType instanceof common_1.ComplexType) {
|
|
58
|
+
file.content += `
|
|
59
|
+
/**\n * ${(0, string_utils_js_1.wrapJSDocString)(dataType.description || '')}
|
|
60
|
+
* @interface ${typeName}
|
|
61
|
+
* @url ${node_path_1.default.posix.join(this.client.serviceUrl, '#types/' + typeName)}
|
|
62
|
+
*/
|
|
63
|
+
export interface ${typeName} ${await this.generateComplexTypeDefinition(file, dataType, true)}`;
|
|
62
64
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
// Export SimpleType
|
|
66
|
+
if (dataType instanceof common_1.SimpleType) {
|
|
67
|
+
file.content += `
|
|
68
|
+
/**\n * ${(0, string_utils_js_1.wrapJSDocString)(dataType.description || '')}
|
|
69
|
+
* @interface ${typeName}
|
|
70
|
+
* @url ${node_path_1.default.posix.join(this.client.serviceUrl, '#types/' + typeName)}
|
|
71
|
+
*/
|
|
72
|
+
export type ${typeName} = ` + await this.generateSimpleTypeDefinition(file, dataType);
|
|
65
73
|
}
|
|
74
|
+
typesTs.addExport(file.filename);
|
|
66
75
|
return file;
|
|
67
76
|
}
|
|
68
77
|
exports.generateTypeFile = generateTypeFile;
|
|
@@ -79,11 +88,9 @@ async function resolveTypeNameOrDef(file, dataType, forInterface) {
|
|
|
79
88
|
const f = await this.generateTypeFile(dataType);
|
|
80
89
|
if (!f)
|
|
81
90
|
return '';
|
|
82
|
-
file.
|
|
91
|
+
file.addImport(f.filename, [dataType.name], true);
|
|
83
92
|
return dataType.name;
|
|
84
93
|
}
|
|
85
|
-
if (dataType instanceof common_1.ComplexType)
|
|
86
|
-
return this.generateComplexTypeDefinition(file, dataType, forInterface);
|
|
87
94
|
if (dataType instanceof common_1.SimpleType)
|
|
88
95
|
return this.generateSimpleTypeDefinition(file, dataType);
|
|
89
96
|
if (dataType instanceof common_1.EnumType)
|
|
@@ -92,6 +99,8 @@ async function resolveTypeNameOrDef(file, dataType, forInterface) {
|
|
|
92
99
|
return this.generateUnionTypeDefinition(file, dataType, forInterface);
|
|
93
100
|
if (dataType instanceof common_1.MappedType)
|
|
94
101
|
return this.generateMappedTypeDefinition(file, dataType, forInterface);
|
|
102
|
+
if (dataType instanceof common_1.ComplexType)
|
|
103
|
+
return this.generateComplexTypeDefinition(file, dataType, forInterface);
|
|
95
104
|
return '';
|
|
96
105
|
}
|
|
97
106
|
exports.resolveTypeNameOrDef = resolveTypeNameOrDef;
|
|
@@ -113,6 +122,8 @@ async function generateComplexTypeDefinition(file, dataType, forInterface) {
|
|
|
113
122
|
let jsDoc = '';
|
|
114
123
|
if (field.description)
|
|
115
124
|
jsDoc += ` * ${field.description}\n`;
|
|
125
|
+
if (field.type.name)
|
|
126
|
+
jsDoc += ` * @type ${field.type.name}\n`;
|
|
116
127
|
if (field.default)
|
|
117
128
|
jsDoc += ` * @default ` + field.default + '\n';
|
|
118
129
|
// if (field.format)
|
|
@@ -122,7 +133,7 @@ async function generateComplexTypeDefinition(file, dataType, forInterface) {
|
|
|
122
133
|
if (field.deprecated)
|
|
123
134
|
jsDoc += ` * @deprecated ` + (typeof field.deprecated === 'string' ? field.deprecated : '') + '\n';
|
|
124
135
|
if (jsDoc)
|
|
125
|
-
out += `/**\n${jsDoc}*/\n`;
|
|
136
|
+
out += `/**\n${jsDoc} */\n`;
|
|
126
137
|
// Print field name
|
|
127
138
|
out += `${field.name}${field.required ? '' : '?'}: `;
|
|
128
139
|
if (field.fixed)
|
|
@@ -165,16 +176,15 @@ exports.generateSimpleTypeDefinition = generateSimpleTypeDefinition;
|
|
|
165
176
|
*/
|
|
166
177
|
async function generateEnumTypeDefinition(file, dataType) {
|
|
167
178
|
let out = '{\n\t';
|
|
168
|
-
for (const [
|
|
179
|
+
for (const [value, info] of Object.entries(dataType.values)) {
|
|
169
180
|
// Print JSDoc
|
|
170
181
|
let jsDoc = '';
|
|
171
|
-
if (dataType.
|
|
172
|
-
jsDoc += ` * ${dataType.
|
|
182
|
+
if (dataType.values[value].description)
|
|
183
|
+
jsDoc += ` * ${dataType.values[value].description}\n`;
|
|
173
184
|
if (jsDoc)
|
|
174
|
-
out += `/**\n${jsDoc}*/\n`;
|
|
175
|
-
out += `${
|
|
176
|
-
|
|
177
|
-
out += ' = ' + (typeof v === 'number' ? v : ('"' + (String(v)).replace('"', '\\"')) + '"');
|
|
185
|
+
out += `/**\n${jsDoc} */\n`;
|
|
186
|
+
out += `${info.key || value} = ` +
|
|
187
|
+
(typeof value === 'number' ? value : ('\'' + (String(value)).replace('\'', '\\\'')) + '\'');
|
|
178
188
|
out += ',\n\n';
|
|
179
189
|
}
|
|
180
190
|
return out + '\b}';
|
|
@@ -199,7 +209,7 @@ exports.generateUnionTypeDefinition = generateUnionTypeDefinition;
|
|
|
199
209
|
* @param forInterface
|
|
200
210
|
*/
|
|
201
211
|
async function generateMappedTypeDefinition(file, dataType, forInterface) {
|
|
202
|
-
const typeName = await this.resolveTypeNameOrDef(file, dataType.
|
|
212
|
+
const typeName = await this.resolveTypeNameOrDef(file, dataType.base, forInterface);
|
|
203
213
|
const keys = (dataType.pick || dataType.omit || []);
|
|
204
214
|
if (!keys.length)
|
|
205
215
|
return typeName;
|
|
@@ -1,41 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.TsFile = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
6
6
|
const putil_flattentext_1 = tslib_1.__importDefault(require("putil-flattentext"));
|
|
7
7
|
class TsFile {
|
|
8
8
|
constructor(filename) {
|
|
9
9
|
this.filename = filename;
|
|
10
|
-
this.
|
|
10
|
+
this.imports = {};
|
|
11
11
|
this.exportFiles = {};
|
|
12
12
|
this.exportTypes = [];
|
|
13
13
|
this.header = '';
|
|
14
14
|
this.content = '';
|
|
15
|
-
this.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
this.addImportFile = (filename, types) => {
|
|
23
|
-
filename = path_1.default.resolve(this.dirname, filename);
|
|
15
|
+
this.addImport = (filename, items, typeImport) => {
|
|
16
|
+
if (isLocalFile(filename)) {
|
|
17
|
+
filename = path_1.default.relative(this.dirname, path_1.default.resolve(this.dirname, filename));
|
|
18
|
+
if (!filename.startsWith('.'))
|
|
19
|
+
filename = './' + filename;
|
|
20
|
+
}
|
|
24
21
|
if (filename.endsWith('.d.ts'))
|
|
25
22
|
filename = filename.substring(0, filename.length - 5);
|
|
26
|
-
if (filename.endsWith('.ts'))
|
|
23
|
+
if (filename.endsWith('.ts') || filename.endsWith('.js'))
|
|
27
24
|
filename = filename.substring(0, filename.length - 3);
|
|
28
|
-
this.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
const imp = (this.imports[filename] = this.imports[filename] || { items: [], typeImport });
|
|
26
|
+
if (!typeImport)
|
|
27
|
+
imp.typeImport = false;
|
|
28
|
+
items?.forEach(x => {
|
|
29
|
+
if (!imp.items.includes(x))
|
|
30
|
+
imp.items.push(x);
|
|
32
31
|
});
|
|
33
32
|
};
|
|
34
|
-
this.
|
|
35
|
-
|
|
33
|
+
this.addExport = (filename, types) => {
|
|
34
|
+
if (isLocalFile(filename)) {
|
|
35
|
+
filename = path_1.default.relative(this.dirname, path_1.default.resolve(this.dirname, filename));
|
|
36
|
+
if (!filename.startsWith('.'))
|
|
37
|
+
filename = './' + filename;
|
|
38
|
+
}
|
|
36
39
|
if (filename.endsWith('.d.ts'))
|
|
37
40
|
filename = filename.substring(0, filename.length - 5);
|
|
38
|
-
if (filename.endsWith('.ts'))
|
|
41
|
+
if (filename.endsWith('.ts') || filename.endsWith('.js'))
|
|
39
42
|
filename = filename.substring(0, filename.length - 3);
|
|
40
43
|
this.exportFiles[filename] = this.exportFiles[filename] || [];
|
|
41
44
|
types?.forEach(x => {
|
|
@@ -46,20 +49,28 @@ class TsFile {
|
|
|
46
49
|
this.dirname = path_1.default.dirname(filename);
|
|
47
50
|
}
|
|
48
51
|
generate(options) {
|
|
49
|
-
const dirname = path_1.default.dirname(this.filename);
|
|
50
52
|
let output = '/* #!oprimp_auto_generated!# !! Do NOT remove this line */\n' +
|
|
51
53
|
(this.header ? (0, putil_flattentext_1.default)(this.header) + '\n\n' : '\n');
|
|
52
|
-
const importStr = Object.keys(this.
|
|
53
|
-
.sort((a, b) =>
|
|
54
|
+
const importStr = Object.keys(this.imports)
|
|
55
|
+
.sort((a, b) => {
|
|
56
|
+
if (a.startsWith('@'))
|
|
57
|
+
return -1;
|
|
58
|
+
if (b.startsWith('@'))
|
|
59
|
+
return 1;
|
|
60
|
+
if (!a.startsWith('.'))
|
|
61
|
+
return -1;
|
|
62
|
+
if (!b.startsWith('.'))
|
|
63
|
+
return 1;
|
|
64
|
+
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
65
|
+
})
|
|
54
66
|
.map(filename => {
|
|
55
|
-
const
|
|
67
|
+
const imp = this.imports[filename];
|
|
56
68
|
let relFile = filename;
|
|
57
|
-
if (
|
|
58
|
-
relFile = relativePath(dirname, filename);
|
|
69
|
+
if (!isPackageName(filename)) {
|
|
59
70
|
if (options?.importExt)
|
|
60
71
|
relFile += '.js';
|
|
61
72
|
}
|
|
62
|
-
return `import ${
|
|
73
|
+
return `import${imp.typeImport ? ' type' : ''} ${imp.items.length ? '{ ' + imp.items.join(', ') + ' } from ' : ''}'${relFile}';`;
|
|
63
74
|
})
|
|
64
75
|
.join('\n');
|
|
65
76
|
if (importStr)
|
|
@@ -70,8 +81,7 @@ class TsFile {
|
|
|
70
81
|
.map(filename => {
|
|
71
82
|
const types = this.exportFiles[filename];
|
|
72
83
|
let relFile = filename;
|
|
73
|
-
if (
|
|
74
|
-
relFile = relativePath(dirname, filename);
|
|
84
|
+
if (!isPackageName(filename)) {
|
|
75
85
|
if (options?.importExt)
|
|
76
86
|
relFile += '.js';
|
|
77
87
|
}
|
|
@@ -84,8 +94,11 @@ class TsFile {
|
|
|
84
94
|
}
|
|
85
95
|
}
|
|
86
96
|
exports.TsFile = TsFile;
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
function isLocalFile(s) {
|
|
98
|
+
return typeof s === 'string' &&
|
|
99
|
+
(s.startsWith('.') || s.startsWith('/'));
|
|
100
|
+
}
|
|
101
|
+
const PACKAGENAME_PATTERN = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
102
|
+
function isPackageName(s) {
|
|
103
|
+
return PACKAGENAME_PATTERN.test(s);
|
|
90
104
|
}
|
|
91
|
-
exports.relativePath = relativePath;
|
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { OpraHttpClient } from '@opra/client';
|
|
6
6
|
import { FileWriter } from './file-writer.js';
|
|
7
|
-
import {
|
|
7
|
+
import { processResource } from './process-resources.js';
|
|
8
8
|
import { generateComplexTypeDefinition, generateEnumTypeDefinition, generateMappedTypeDefinition, generateSimpleTypeDefinition, generateTypeFile, generateUnionTypeDefinition, processTypes, resolveTypeNameOrDef } from './process-types.js';
|
|
9
9
|
import { TsFile } from './ts-file.js';
|
|
10
10
|
export class ApiExporter {
|
|
@@ -29,7 +29,7 @@ export class ApiExporter {
|
|
|
29
29
|
async execute() {
|
|
30
30
|
this.logger.log(chalk.cyan('Fetching service metadata from'), chalk.whiteBright(this.client.serviceUrl));
|
|
31
31
|
this.document = await this.client.getMetadata();
|
|
32
|
-
this.logger.log(chalk.cyan('Retrieved service info:\n'), chalk.white('Title:'), chalk.whiteBright(this.document.info.title), '\n', chalk.white('Version:'), chalk.whiteBright(this.document.info.version), '\n'
|
|
32
|
+
this.logger.log(chalk.cyan('Retrieved service info:\n'), chalk.white('Title:'), chalk.whiteBright(this.document.info.title), '\n', chalk.white('Version:'), chalk.whiteBright(this.document.info.version), '\n');
|
|
33
33
|
this.serviceClassName = (this.serviceClassName || this.document.info.title || 'Service1').replace(/[^\w_$]*/g, '');
|
|
34
34
|
this.serviceClassName = this.serviceClassName.charAt(0).toUpperCase() + this.serviceClassName.substring(1);
|
|
35
35
|
this.fileHeader += `/*
|
|
@@ -41,27 +41,33 @@ export class ApiExporter {
|
|
|
41
41
|
this.cleanDirectory(this.outDir);
|
|
42
42
|
this.logger.log(chalk.cyan(`Generating service interface ( ${chalk.whiteBright(this.serviceClassName)} )`));
|
|
43
43
|
fs.mkdirSync(this.outDir, { recursive: true });
|
|
44
|
+
this.logger.log(chalk.cyan('Processing types'));
|
|
44
45
|
await this.processTypes();
|
|
45
|
-
|
|
46
|
+
this.logger.log(chalk.cyan('Processing resources'));
|
|
47
|
+
const rootTs = this.addFile('/' + this.serviceClassName + '.ts');
|
|
48
|
+
await this.processResource(this.document.root, this.serviceClassName, rootTs);
|
|
46
49
|
const { importExt } = this;
|
|
47
50
|
// Write files
|
|
48
51
|
for (const file of Object.values(this.files)) {
|
|
49
|
-
const
|
|
52
|
+
const filename = path.join(this.outDir, file.filename);
|
|
53
|
+
const targetDir = path.dirname(filename);
|
|
50
54
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
51
|
-
await this.writer.writeFile(
|
|
55
|
+
await this.writer.writeFile(filename, file.generate({ importExt }));
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
getFile(filePath) {
|
|
55
|
-
return this.files[
|
|
59
|
+
return this.files[filePath];
|
|
56
60
|
}
|
|
57
61
|
addFile(filePath, returnExists) {
|
|
62
|
+
if (!(filePath.startsWith('.') || filePath.startsWith('/')))
|
|
63
|
+
filePath = './' + filePath;
|
|
58
64
|
let file = this.getFile(filePath);
|
|
59
65
|
if (file) {
|
|
60
66
|
if (returnExists)
|
|
61
67
|
return file;
|
|
62
68
|
throw new Error(`File "${filePath}" already exists`);
|
|
63
69
|
}
|
|
64
|
-
file = new TsFile(
|
|
70
|
+
file = new TsFile(filePath);
|
|
65
71
|
file.header = this.fileHeader;
|
|
66
72
|
this.files[file.filename] = file;
|
|
67
73
|
return file;
|
|
@@ -92,7 +98,7 @@ export class ApiExporter {
|
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
(() => {
|
|
95
|
-
ApiExporter.prototype.
|
|
101
|
+
ApiExporter.prototype.processResource = processResource;
|
|
96
102
|
ApiExporter.prototype.processTypes = processTypes;
|
|
97
103
|
ApiExporter.prototype.generateTypeFile = generateTypeFile;
|
|
98
104
|
ApiExporter.prototype.generateComplexTypeDefinition = generateComplexTypeDefinition;
|
|
@@ -1,57 +1,102 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import { Collection, Singleton } from '@opra/common';
|
|
2
|
+
import { Collection, Container, Singleton } from '@opra/common';
|
|
4
3
|
import { wrapJSDocString } from '../utils/string-utils.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* @param targetDir
|
|
8
|
-
*/
|
|
9
|
-
export async function processResources(targetDir = '') {
|
|
10
|
-
this.logger.log(chalk.cyan('Processing resources'));
|
|
11
|
-
const { document } = this;
|
|
12
|
-
const serviceTs = this.addFile(path.join(targetDir, this.serviceClassName + '.ts'));
|
|
13
|
-
serviceTs.addImportPackage('@opra/client', ['HttpServiceBase']);
|
|
4
|
+
export async function processResource(resource, className, tsFile) {
|
|
5
|
+
tsFile.addImport('@opra/client', ['HttpServiceBase', 'kClient', 'kContext']);
|
|
14
6
|
const indexTs = this.addFile('/index.ts', true);
|
|
15
|
-
indexTs.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
indexTs.addExport(tsFile.filename);
|
|
8
|
+
tsFile.content = `\n
|
|
9
|
+
/**
|
|
10
|
+
* ${wrapJSDocString(resource.description || '')}
|
|
11
|
+
* @class ${className}
|
|
12
|
+
* @url ${path.posix.join(this.client.serviceUrl, '#resources/' + className)}
|
|
13
|
+
*/
|
|
14
|
+
export class ${className} extends HttpServiceBase {\n`;
|
|
15
|
+
if (resource instanceof Container) {
|
|
16
|
+
for (const child of resource.resources.values()) {
|
|
17
|
+
// Determine class name of child resource
|
|
18
|
+
let childClassName = '';
|
|
19
|
+
if (child instanceof Container)
|
|
20
|
+
childClassName = child.name.charAt(0).toUpperCase() + child.name.substring(1) + 'Container';
|
|
21
|
+
else
|
|
22
|
+
childClassName = child.name + 'Resource';
|
|
23
|
+
// Create TsFile for child resource
|
|
24
|
+
const dir = path.dirname(tsFile.filename);
|
|
25
|
+
const basename = dir === '/'
|
|
26
|
+
? 'root'
|
|
27
|
+
: path.basename(tsFile.filename, path.extname(tsFile.filename));
|
|
28
|
+
const childFile = this.addFile(path.join(dir, basename, child.name + '.ts'));
|
|
29
|
+
await this.processResource(child, childClassName, childFile);
|
|
30
|
+
tsFile.addImport(childFile.filename, [childClassName]);
|
|
31
|
+
tsFile.content += `
|
|
19
32
|
/**
|
|
20
|
-
* ${wrapJSDocString(
|
|
21
|
-
* @url ${path.posix.join(this.client.serviceUrl, '#resources/' +
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const operations = Object.keys(resource.operations)
|
|
28
|
-
.map(x => `'${x}'`).join(' | ');
|
|
29
|
-
if (!operations.length) {
|
|
30
|
-
this.logger.warn(chalk.yellow('WARN: ') +
|
|
31
|
-
`Ignoring "${chalk.whiteBright(resource.name)}" resource. No operations available.`);
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
serviceTs.content += jsDoc + `
|
|
35
|
-
get ${resource.name}(): Pick<HttpCollectionNode<${typeName}>, ${operations}> {
|
|
36
|
-
return this.$client.collection('${resource.name}');
|
|
33
|
+
* ${wrapJSDocString(child.description || '')}
|
|
34
|
+
* @url ${path.posix.join(this.client.serviceUrl, '#resources/' + child.name)}
|
|
35
|
+
*/
|
|
36
|
+
get ${child.name}(): ${childClassName} {
|
|
37
|
+
if (!this[kContext].resources.${child.name})
|
|
38
|
+
this[kContext].resources.${child.name} = new ${childClassName}(this[kClient]);
|
|
39
|
+
return this[kContext].resources.${child.name};
|
|
37
40
|
}\n`;
|
|
38
41
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
}
|
|
43
|
+
else if (resource instanceof Collection) {
|
|
44
|
+
tsFile.addImport('@opra/client', ['HttpCollectionNode']);
|
|
45
|
+
const typeName = resource.type.name || '';
|
|
46
|
+
tsFile.addImport(`/types/${typeName}`, [typeName], true);
|
|
47
|
+
for (const [operation, endpoint] of resource.operations.entries()) {
|
|
48
|
+
tsFile.content += `
|
|
49
|
+
/**
|
|
50
|
+
* ${wrapJSDocString(endpoint.description || '')}
|
|
51
|
+
*/
|
|
52
|
+
get ${operation}(): HttpCollectionNode<${typeName}>['${operation}'] {
|
|
53
|
+
if (!this[kContext].node)
|
|
54
|
+
this[kContext].node = this[kClient].collection('${resource.name}');
|
|
55
|
+
return this[kContext].node.${operation};
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (resource instanceof Singleton) {
|
|
61
|
+
tsFile.addImport('@opra/client', ['HttpSingletonNode']);
|
|
62
|
+
const typeName = resource.type.name || '';
|
|
63
|
+
tsFile.addImport(`/types/${typeName}`, [typeName], true);
|
|
64
|
+
for (const [operation, endpoint] of resource.operations.entries()) {
|
|
65
|
+
tsFile.content += `
|
|
66
|
+
/**
|
|
67
|
+
* ${wrapJSDocString(endpoint.description || '')}
|
|
68
|
+
*/
|
|
69
|
+
get ${operation}(): HttpSingletonNode<${typeName}>['${operation}'] {
|
|
70
|
+
if (!this[kContext].node)
|
|
71
|
+
this[kContext].node = this[kClient].singleton('${resource.name}');
|
|
72
|
+
return this[kContext].node.${operation};
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (resource.actions.size) {
|
|
78
|
+
tsFile.addImport('@opra/client', ['HttpRequestObservable']);
|
|
79
|
+
for (const [action, endpoint] of resource.actions.entries()) {
|
|
80
|
+
const typeName = endpoint.returnType?.name || 'any';
|
|
81
|
+
const actionPath = resource.getFullPath() + '/' + action;
|
|
82
|
+
let params = '';
|
|
83
|
+
for (const prm of endpoint.parameters.values()) {
|
|
84
|
+
params += ` ${prm.name}: ${prm.type.name || 'any'}`;
|
|
85
|
+
if (prm.isArray)
|
|
86
|
+
params += '[]';
|
|
87
|
+
params += ';\n';
|
|
49
88
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
89
|
+
params = params ? '{\n' + params + ' }\n ' : '{}';
|
|
90
|
+
tsFile.content += `
|
|
91
|
+
/**
|
|
92
|
+
* ${wrapJSDocString(endpoint.description || '')}
|
|
93
|
+
*/
|
|
94
|
+
${action}(params: ${params}): HttpRequestObservable<${typeName}> {
|
|
95
|
+
return this[kClient].action('${actionPath}', params);
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
54
98
|
}
|
|
55
99
|
}
|
|
56
|
-
|
|
100
|
+
tsFile.content += `}\n`;
|
|
101
|
+
return tsFile.content;
|
|
57
102
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { ComplexType, EnumType, MappedType, SimpleType, UnionType } from '@opra/common';
|
|
4
3
|
import { wrapJSDocString } from '../utils/string-utils.js';
|
|
@@ -7,14 +6,10 @@ const internalTypeNames = ['any', 'boolean', 'bigint', 'number', 'null', 'string
|
|
|
7
6
|
*
|
|
8
7
|
* @param targetDir
|
|
9
8
|
*/
|
|
10
|
-
export async function processTypes(
|
|
11
|
-
this.logger.log(chalk.cyan('Processing types'));
|
|
9
|
+
export async function processTypes() {
|
|
12
10
|
const { document } = this;
|
|
13
|
-
const typesTs = this.addFile(path.join(targetDir, 'types.ts'));
|
|
14
11
|
for (const dataType of document.types.values()) {
|
|
15
|
-
|
|
16
|
-
if (expFile)
|
|
17
|
-
typesTs.addExportFile(expFile.filename);
|
|
12
|
+
await this.generateTypeFile(dataType);
|
|
18
13
|
}
|
|
19
14
|
}
|
|
20
15
|
/**
|
|
@@ -22,42 +17,56 @@ export async function processTypes(targetDir = '') {
|
|
|
22
17
|
* @param dataType
|
|
23
18
|
* @param targetDir
|
|
24
19
|
*/
|
|
25
|
-
export async function generateTypeFile(dataType
|
|
20
|
+
export async function generateTypeFile(dataType) {
|
|
26
21
|
const typeName = dataType.name;
|
|
27
22
|
if (typeName === 'any')
|
|
28
23
|
return;
|
|
29
24
|
if (!typeName)
|
|
30
25
|
throw new TypeError(`DataType has no name`);
|
|
31
|
-
|
|
26
|
+
const typesTs = this.addFile('/types.ts', true);
|
|
27
|
+
let filePath = '';
|
|
32
28
|
if (dataType instanceof SimpleType)
|
|
33
29
|
filePath = '/simple-types.ts';
|
|
34
|
-
else
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
else {
|
|
31
|
+
if (dataType instanceof EnumType)
|
|
32
|
+
filePath = path.join('/enums', typeName + '.ts');
|
|
33
|
+
else
|
|
34
|
+
filePath = path.join('/types', typeName + '.ts');
|
|
38
35
|
}
|
|
39
|
-
|
|
40
|
-
throw new TypeError(`Unimplemented DataType (${dataType.kind})`);
|
|
41
|
-
const file = this.addFile(path.join(targetDir, filePath), true);
|
|
36
|
+
const file = this.addFile(filePath, true);
|
|
42
37
|
if (file.exportTypes.includes(typeName))
|
|
43
38
|
return file;
|
|
44
39
|
file.exportTypes.push(typeName);
|
|
45
40
|
const indexTs = this.addFile('/index.ts', true);
|
|
46
|
-
indexTs.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
indexTs.addExport(file.filename);
|
|
42
|
+
// Export EnumType
|
|
43
|
+
if (dataType instanceof EnumType) {
|
|
44
|
+
file.content += `
|
|
45
|
+
/**\n * ${wrapJSDocString(dataType.description || '')}
|
|
46
|
+
* @enum ${typeName}
|
|
50
47
|
* @url ${path.posix.join(this.client.serviceUrl, '#types/' + typeName)}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
file.content += `export type ${typeName} = ` + await this.generateSimpleTypeDefinition(file, dataType);
|
|
48
|
+
*/
|
|
49
|
+
export enum ${typeName} ` + await this.generateEnumTypeDefinition(file, dataType);
|
|
54
50
|
}
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
// Export ComplexType
|
|
52
|
+
if (dataType instanceof ComplexType) {
|
|
53
|
+
file.content += `
|
|
54
|
+
/**\n * ${wrapJSDocString(dataType.description || '')}
|
|
55
|
+
* @interface ${typeName}
|
|
56
|
+
* @url ${path.posix.join(this.client.serviceUrl, '#types/' + typeName)}
|
|
57
|
+
*/
|
|
58
|
+
export interface ${typeName} ${await this.generateComplexTypeDefinition(file, dataType, true)}`;
|
|
57
59
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
// Export SimpleType
|
|
61
|
+
if (dataType instanceof SimpleType) {
|
|
62
|
+
file.content += `
|
|
63
|
+
/**\n * ${wrapJSDocString(dataType.description || '')}
|
|
64
|
+
* @interface ${typeName}
|
|
65
|
+
* @url ${path.posix.join(this.client.serviceUrl, '#types/' + typeName)}
|
|
66
|
+
*/
|
|
67
|
+
export type ${typeName} = ` + await this.generateSimpleTypeDefinition(file, dataType);
|
|
60
68
|
}
|
|
69
|
+
typesTs.addExport(file.filename);
|
|
61
70
|
return file;
|
|
62
71
|
}
|
|
63
72
|
/**
|
|
@@ -73,11 +82,9 @@ export async function resolveTypeNameOrDef(file, dataType, forInterface) {
|
|
|
73
82
|
const f = await this.generateTypeFile(dataType);
|
|
74
83
|
if (!f)
|
|
75
84
|
return '';
|
|
76
|
-
file.
|
|
85
|
+
file.addImport(f.filename, [dataType.name], true);
|
|
77
86
|
return dataType.name;
|
|
78
87
|
}
|
|
79
|
-
if (dataType instanceof ComplexType)
|
|
80
|
-
return this.generateComplexTypeDefinition(file, dataType, forInterface);
|
|
81
88
|
if (dataType instanceof SimpleType)
|
|
82
89
|
return this.generateSimpleTypeDefinition(file, dataType);
|
|
83
90
|
if (dataType instanceof EnumType)
|
|
@@ -86,6 +93,8 @@ export async function resolveTypeNameOrDef(file, dataType, forInterface) {
|
|
|
86
93
|
return this.generateUnionTypeDefinition(file, dataType, forInterface);
|
|
87
94
|
if (dataType instanceof MappedType)
|
|
88
95
|
return this.generateMappedTypeDefinition(file, dataType, forInterface);
|
|
96
|
+
if (dataType instanceof ComplexType)
|
|
97
|
+
return this.generateComplexTypeDefinition(file, dataType, forInterface);
|
|
89
98
|
return '';
|
|
90
99
|
}
|
|
91
100
|
/**
|
|
@@ -106,6 +115,8 @@ export async function generateComplexTypeDefinition(file, dataType, forInterface
|
|
|
106
115
|
let jsDoc = '';
|
|
107
116
|
if (field.description)
|
|
108
117
|
jsDoc += ` * ${field.description}\n`;
|
|
118
|
+
if (field.type.name)
|
|
119
|
+
jsDoc += ` * @type ${field.type.name}\n`;
|
|
109
120
|
if (field.default)
|
|
110
121
|
jsDoc += ` * @default ` + field.default + '\n';
|
|
111
122
|
// if (field.format)
|
|
@@ -115,7 +126,7 @@ export async function generateComplexTypeDefinition(file, dataType, forInterface
|
|
|
115
126
|
if (field.deprecated)
|
|
116
127
|
jsDoc += ` * @deprecated ` + (typeof field.deprecated === 'string' ? field.deprecated : '') + '\n';
|
|
117
128
|
if (jsDoc)
|
|
118
|
-
out += `/**\n${jsDoc}*/\n`;
|
|
129
|
+
out += `/**\n${jsDoc} */\n`;
|
|
119
130
|
// Print field name
|
|
120
131
|
out += `${field.name}${field.required ? '' : '?'}: `;
|
|
121
132
|
if (field.fixed)
|
|
@@ -156,16 +167,15 @@ export async function generateSimpleTypeDefinition(file, dataType) {
|
|
|
156
167
|
*/
|
|
157
168
|
export async function generateEnumTypeDefinition(file, dataType) {
|
|
158
169
|
let out = '{\n\t';
|
|
159
|
-
for (const [
|
|
170
|
+
for (const [value, info] of Object.entries(dataType.values)) {
|
|
160
171
|
// Print JSDoc
|
|
161
172
|
let jsDoc = '';
|
|
162
|
-
if (dataType.
|
|
163
|
-
jsDoc += ` * ${dataType.
|
|
173
|
+
if (dataType.values[value].description)
|
|
174
|
+
jsDoc += ` * ${dataType.values[value].description}\n`;
|
|
164
175
|
if (jsDoc)
|
|
165
|
-
out += `/**\n${jsDoc}*/\n`;
|
|
166
|
-
out += `${
|
|
167
|
-
|
|
168
|
-
out += ' = ' + (typeof v === 'number' ? v : ('"' + (String(v)).replace('"', '\\"')) + '"');
|
|
176
|
+
out += `/**\n${jsDoc} */\n`;
|
|
177
|
+
out += `${info.key || value} = ` +
|
|
178
|
+
(typeof value === 'number' ? value : ('\'' + (String(value)).replace('\'', '\\\'')) + '\'');
|
|
169
179
|
out += ',\n\n';
|
|
170
180
|
}
|
|
171
181
|
return out + '\b}';
|
|
@@ -188,7 +198,7 @@ export async function generateUnionTypeDefinition(file, dataType, forInterface)
|
|
|
188
198
|
* @param forInterface
|
|
189
199
|
*/
|
|
190
200
|
export async function generateMappedTypeDefinition(file, dataType, forInterface) {
|
|
191
|
-
const typeName = await this.resolveTypeNameOrDef(file, dataType.
|
|
201
|
+
const typeName = await this.resolveTypeNameOrDef(file, dataType.base, forInterface);
|
|
192
202
|
const keys = (dataType.pick || dataType.omit || []);
|
|
193
203
|
if (!keys.length)
|
|
194
204
|
return typeName;
|
|
@@ -3,35 +3,38 @@ import flattenText from 'putil-flattentext';
|
|
|
3
3
|
export class TsFile {
|
|
4
4
|
constructor(filename) {
|
|
5
5
|
this.filename = filename;
|
|
6
|
-
this.
|
|
6
|
+
this.imports = {};
|
|
7
7
|
this.exportFiles = {};
|
|
8
8
|
this.exportTypes = [];
|
|
9
9
|
this.header = '';
|
|
10
10
|
this.content = '';
|
|
11
|
-
this.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
this.addImportFile = (filename, types) => {
|
|
19
|
-
filename = path.resolve(this.dirname, filename);
|
|
11
|
+
this.addImport = (filename, items, typeImport) => {
|
|
12
|
+
if (isLocalFile(filename)) {
|
|
13
|
+
filename = path.relative(this.dirname, path.resolve(this.dirname, filename));
|
|
14
|
+
if (!filename.startsWith('.'))
|
|
15
|
+
filename = './' + filename;
|
|
16
|
+
}
|
|
20
17
|
if (filename.endsWith('.d.ts'))
|
|
21
18
|
filename = filename.substring(0, filename.length - 5);
|
|
22
|
-
if (filename.endsWith('.ts'))
|
|
19
|
+
if (filename.endsWith('.ts') || filename.endsWith('.js'))
|
|
23
20
|
filename = filename.substring(0, filename.length - 3);
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
const imp = (this.imports[filename] = this.imports[filename] || { items: [], typeImport });
|
|
22
|
+
if (!typeImport)
|
|
23
|
+
imp.typeImport = false;
|
|
24
|
+
items?.forEach(x => {
|
|
25
|
+
if (!imp.items.includes(x))
|
|
26
|
+
imp.items.push(x);
|
|
28
27
|
});
|
|
29
28
|
};
|
|
30
|
-
this.
|
|
31
|
-
|
|
29
|
+
this.addExport = (filename, types) => {
|
|
30
|
+
if (isLocalFile(filename)) {
|
|
31
|
+
filename = path.relative(this.dirname, path.resolve(this.dirname, filename));
|
|
32
|
+
if (!filename.startsWith('.'))
|
|
33
|
+
filename = './' + filename;
|
|
34
|
+
}
|
|
32
35
|
if (filename.endsWith('.d.ts'))
|
|
33
36
|
filename = filename.substring(0, filename.length - 5);
|
|
34
|
-
if (filename.endsWith('.ts'))
|
|
37
|
+
if (filename.endsWith('.ts') || filename.endsWith('.js'))
|
|
35
38
|
filename = filename.substring(0, filename.length - 3);
|
|
36
39
|
this.exportFiles[filename] = this.exportFiles[filename] || [];
|
|
37
40
|
types?.forEach(x => {
|
|
@@ -42,20 +45,28 @@ export class TsFile {
|
|
|
42
45
|
this.dirname = path.dirname(filename);
|
|
43
46
|
}
|
|
44
47
|
generate(options) {
|
|
45
|
-
const dirname = path.dirname(this.filename);
|
|
46
48
|
let output = '/* #!oprimp_auto_generated!# !! Do NOT remove this line */\n' +
|
|
47
49
|
(this.header ? flattenText(this.header) + '\n\n' : '\n');
|
|
48
|
-
const importStr = Object.keys(this.
|
|
49
|
-
.sort((a, b) =>
|
|
50
|
+
const importStr = Object.keys(this.imports)
|
|
51
|
+
.sort((a, b) => {
|
|
52
|
+
if (a.startsWith('@'))
|
|
53
|
+
return -1;
|
|
54
|
+
if (b.startsWith('@'))
|
|
55
|
+
return 1;
|
|
56
|
+
if (!a.startsWith('.'))
|
|
57
|
+
return -1;
|
|
58
|
+
if (!b.startsWith('.'))
|
|
59
|
+
return 1;
|
|
60
|
+
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
61
|
+
})
|
|
50
62
|
.map(filename => {
|
|
51
|
-
const
|
|
63
|
+
const imp = this.imports[filename];
|
|
52
64
|
let relFile = filename;
|
|
53
|
-
if (
|
|
54
|
-
relFile = relativePath(dirname, filename);
|
|
65
|
+
if (!isPackageName(filename)) {
|
|
55
66
|
if (options?.importExt)
|
|
56
67
|
relFile += '.js';
|
|
57
68
|
}
|
|
58
|
-
return `import ${
|
|
69
|
+
return `import${imp.typeImport ? ' type' : ''} ${imp.items.length ? '{ ' + imp.items.join(', ') + ' } from ' : ''}'${relFile}';`;
|
|
59
70
|
})
|
|
60
71
|
.join('\n');
|
|
61
72
|
if (importStr)
|
|
@@ -66,8 +77,7 @@ export class TsFile {
|
|
|
66
77
|
.map(filename => {
|
|
67
78
|
const types = this.exportFiles[filename];
|
|
68
79
|
let relFile = filename;
|
|
69
|
-
if (
|
|
70
|
-
relFile = relativePath(dirname, filename);
|
|
80
|
+
if (!isPackageName(filename)) {
|
|
71
81
|
if (options?.importExt)
|
|
72
82
|
relFile += '.js';
|
|
73
83
|
}
|
|
@@ -79,7 +89,11 @@ export class TsFile {
|
|
|
79
89
|
return output;
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
function isLocalFile(s) {
|
|
93
|
+
return typeof s === 'string' &&
|
|
94
|
+
(s.startsWith('.') || s.startsWith('/'));
|
|
95
|
+
}
|
|
96
|
+
const PACKAGENAME_PATTERN = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
97
|
+
function isPackageName(s) {
|
|
98
|
+
return PACKAGENAME_PATTERN.test(s);
|
|
85
99
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opra/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Opra CLI tools",
|
|
5
5
|
"author": "Panates",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"clean:cover": "rimraf ../../coverage/client"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@opra/client": "^0.
|
|
31
|
+
"@opra/client": "^0.26.0",
|
|
32
32
|
"chalk": "^5.3.0",
|
|
33
33
|
"commander": "^11.0.0",
|
|
34
34
|
"js-string-escape": "^1.0.1",
|
|
@@ -2,7 +2,7 @@ import { OpraHttpClient } from '@opra/client';
|
|
|
2
2
|
import { ApiDocument } from '@opra/common';
|
|
3
3
|
import { IFileWriter } from '../interfaces/file-writer.interface.js';
|
|
4
4
|
import { ILogger } from '../interfaces/logger.interface.js';
|
|
5
|
-
import {
|
|
5
|
+
import { processResource } from './process-resources.js';
|
|
6
6
|
import { generateComplexTypeDefinition, generateEnumTypeDefinition, generateMappedTypeDefinition, generateSimpleTypeDefinition, generateTypeFile, generateUnionTypeDefinition, processTypes, resolveTypeNameOrDef } from './process-types.js';
|
|
7
7
|
import { TsFile } from './ts-file.js';
|
|
8
8
|
export declare namespace ApiExporter {
|
|
@@ -28,7 +28,7 @@ export declare class ApiExporter {
|
|
|
28
28
|
protected writer: IFileWriter;
|
|
29
29
|
protected files: Record<string, TsFile>;
|
|
30
30
|
protected importExt?: boolean;
|
|
31
|
-
protected
|
|
31
|
+
protected processResource: typeof processResource;
|
|
32
32
|
protected processTypes: typeof processTypes;
|
|
33
33
|
protected generateTypeFile: typeof generateTypeFile;
|
|
34
34
|
protected generateComplexTypeDefinition: typeof generateComplexTypeDefinition;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
+
import { Resource } from '@opra/common';
|
|
1
2
|
import type { ApiExporter } from './api-exporter.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* @param targetDir
|
|
5
|
-
*/
|
|
6
|
-
export declare function processResources(this: ApiExporter, targetDir?: string): Promise<void>;
|
|
3
|
+
import { TsFile } from './ts-file.js';
|
|
4
|
+
export declare function processResource(this: ApiExporter, resource: Resource, className: string, tsFile: TsFile): Promise<string>;
|
|
@@ -5,13 +5,13 @@ import { TsFile } from './ts-file.js';
|
|
|
5
5
|
*
|
|
6
6
|
* @param targetDir
|
|
7
7
|
*/
|
|
8
|
-
export declare function processTypes(this: ApiExporter
|
|
8
|
+
export declare function processTypes(this: ApiExporter): Promise<void>;
|
|
9
9
|
/**
|
|
10
10
|
*
|
|
11
11
|
* @param dataType
|
|
12
12
|
* @param targetDir
|
|
13
13
|
*/
|
|
14
|
-
export declare function generateTypeFile(this: ApiExporter, dataType: DataType
|
|
14
|
+
export declare function generateTypeFile(this: ApiExporter, dataType: DataType): Promise<TsFile | undefined>;
|
|
15
15
|
/**
|
|
16
16
|
*
|
|
17
17
|
* @param file
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
export declare class TsFile {
|
|
2
|
-
filename: string;
|
|
3
|
-
dirname: string;
|
|
4
|
-
|
|
2
|
+
readonly filename: string;
|
|
3
|
+
readonly dirname: string;
|
|
4
|
+
imports: Record<string, {
|
|
5
|
+
items: string[];
|
|
6
|
+
typeImport?: boolean;
|
|
7
|
+
}>;
|
|
5
8
|
exportFiles: Record<string, string[]>;
|
|
6
9
|
exportTypes: string[];
|
|
7
10
|
header: string;
|
|
8
11
|
content: string;
|
|
9
12
|
constructor(filename: string);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
addExportFile: (filename: string, types?: string[]) => void;
|
|
13
|
+
addImport: (filename: string, items?: string[], typeImport?: boolean) => void;
|
|
14
|
+
addExport: (filename: string, types?: string[]) => void;
|
|
13
15
|
generate(options?: {
|
|
14
16
|
importExt?: boolean;
|
|
15
17
|
}): string;
|
|
16
18
|
}
|
|
17
|
-
export declare function relativePath(from: string, to: string): string;
|