@opra/cli 0.25.4 → 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.
@@ -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', chalk_1.default.white('Resources:'), chalk_1.default.whiteBright(this.document.resources.size), 'resources found\n', chalk_1.default.white('Types:'), chalk_1.default.whiteBright(this.document.types.size), 'types found\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
- await this.processSources();
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 targetDir = node_path_1.default.dirname(file.filename);
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(file.filename, file.generate({ importExt }));
59
+ await this.writer.writeFile(filename, file.generate({ importExt }));
56
60
  }
57
61
  }
58
62
  getFile(filePath) {
59
- return this.files[node_path_1.default.resolve(node_path_1.default.join(this.outDir, filePath))];
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(node_path_1.default.resolve(node_path_1.default.join(this.outDir, filePath)));
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.processSources = process_resources_js_1.processResources;
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.processResources = void 0;
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.addExportFile(serviceTs.filename);
20
- serviceTs.content = `\nexport class ${this.serviceClassName} extends HttpServiceBase {\n`;
21
- for (const resource of document.resources.values()) {
22
- const jsDoc = `
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)(resource.description || resource.name)}
25
- * @url ${node_path_1.default.posix.join(this.client.serviceUrl, '#resources/' + resource.name)}
26
- */`;
27
- if (resource instanceof common_1.Collection) {
28
- const typeName = resource.type.name || '';
29
- serviceTs.addImportPackage('@opra/client', ['HttpCollectionNode']);
30
- serviceTs.addImportFile(`types/${typeName}-type`, [typeName]);
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
- else if (resource instanceof common_1.Singleton) {
44
- const typeName = resource.type.name || '';
45
- serviceTs.addImportPackage('@opra/client', ['HttpSingletonNode']);
46
- serviceTs.addImportFile(`types/${typeName}-type`, [typeName]);
47
- const operations = Object.keys(resource.operations)
48
- .map(x => `'${x}'`).join(' | ');
49
- if (!operations.length) {
50
- this.logger.warn(chalk_1.default.yellow('WARN: ') +
51
- `Ignoring "${chalk_1.default.whiteBright(resource.name)}" resource. No operations available.`);
52
- continue;
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
- serviceTs.content += jsDoc + `
55
- get ${resource.name}(): Pick<HttpSingletonNode<${typeName}>, ${operations}> {
56
- return this.$client.singleton('${resource.name}');
57
- }\n`;
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
- serviceTs.content += '}';
104
+ tsFile.content += `}\n`;
105
+ return tsFile.content;
61
106
  }
62
- exports.processResources = processResources;
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(targetDir = '') {
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
- const expFile = await this.generateTypeFile(dataType, targetDir);
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, targetDir = '') {
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
- let filePath;
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 if (dataType instanceof common_1.ComplexType)
40
- filePath = `/types/${typeName}-type.ts`;
41
- else if (dataType instanceof common_1.EnumType) {
42
- filePath = `/enums/${typeName}-enum.ts`;
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
- else
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.addExportFile(file.filename);
52
- file.content += `\n/**\n * ${(0, string_utils_js_1.wrapJSDocString)(dataType.description || typeName)}
53
- * @interface ${typeName}
54
- * @kind ${dataType.kind}
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
- */\n`;
57
- if (dataType instanceof common_1.SimpleType) {
58
- file.content += `export type ${typeName} = ` + await this.generateSimpleTypeDefinition(file, dataType);
53
+ */
54
+ export enum ${typeName} ` + await this.generateEnumTypeDefinition(file, dataType);
59
55
  }
60
- else if (dataType instanceof common_1.EnumType) {
61
- file.content += `export enum ${typeName} ` + await this.generateEnumTypeDefinition(file, dataType);
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
- else if (dataType instanceof common_1.ComplexType) {
64
- file.content += `export interface ${typeName} ${await this.generateComplexTypeDefinition(file, dataType, true)}`;
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.addImportFile(f.filename, [dataType.name]);
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 [k, v] of Object.entries(dataType.values)) {
179
+ for (const [value, info] of Object.entries(dataType.values)) {
169
180
  // Print JSDoc
170
181
  let jsDoc = '';
171
- if (dataType.meanings[k])
172
- jsDoc += ` * ${dataType.meanings[k]}\n`;
182
+ if (dataType.values[value].description)
183
+ jsDoc += ` * ${dataType.values[value].description}\n`;
173
184
  if (jsDoc)
174
- out += `/**\n${jsDoc}*/\n`;
175
- out += `${k}`;
176
- if (v)
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.type, forInterface);
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.relativePath = exports.TsFile = void 0;
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.importFiles = {};
10
+ this.imports = {};
11
11
  this.exportFiles = {};
12
12
  this.exportTypes = [];
13
13
  this.header = '';
14
14
  this.content = '';
15
- this.addImportPackage = (name, types) => {
16
- this.importFiles[name] = this.importFiles[name] || [];
17
- types?.forEach(x => {
18
- if (!this.importFiles[name].includes(x))
19
- this.importFiles[name].push(x);
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.importFiles[filename] = this.importFiles[filename] || [];
29
- types?.forEach(x => {
30
- if (!this.importFiles[filename].includes(x))
31
- this.importFiles[filename].push(x);
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.addExportFile = (filename, types) => {
35
- filename = path_1.default.resolve(this.dirname, filename);
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.importFiles)
53
- .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
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 types = this.importFiles[filename];
67
+ const imp = this.imports[filename];
56
68
  let relFile = filename;
57
- if (path_1.default.isAbsolute(filename)) {
58
- relFile = relativePath(dirname, filename);
69
+ if (!isPackageName(filename)) {
59
70
  if (options?.importExt)
60
71
  relFile += '.js';
61
72
  }
62
- return `import ${types.length ? '{ ' + types.join(', ') + ' } from ' : ''}'${relFile}';`;
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 (path_1.default.isAbsolute(filename)) {
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 relativePath(from, to) {
88
- const s = path_1.default.relative(from, to);
89
- return s.startsWith('.') ? s : ('./' + s);
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 { processResources } from './process-resources.js';
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', chalk.white('Resources:'), chalk.whiteBright(this.document.resources.size), 'resources found\n', chalk.white('Types:'), chalk.whiteBright(this.document.types.size), 'types found\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
- await this.processSources();
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 targetDir = path.dirname(file.filename);
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(file.filename, file.generate({ importExt }));
55
+ await this.writer.writeFile(filename, file.generate({ importExt }));
52
56
  }
53
57
  }
54
58
  getFile(filePath) {
55
- return this.files[path.resolve(path.join(this.outDir, filePath))];
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(path.resolve(path.join(this.outDir, filePath)));
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.processSources = processResources;
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.addExportFile(serviceTs.filename);
16
- serviceTs.content = `\nexport class ${this.serviceClassName} extends HttpServiceBase {\n`;
17
- for (const resource of document.resources.values()) {
18
- const jsDoc = `
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(resource.description || resource.name)}
21
- * @url ${path.posix.join(this.client.serviceUrl, '#resources/' + resource.name)}
22
- */`;
23
- if (resource instanceof Collection) {
24
- const typeName = resource.type.name || '';
25
- serviceTs.addImportPackage('@opra/client', ['HttpCollectionNode']);
26
- serviceTs.addImportFile(`types/${typeName}-type`, [typeName]);
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
- else if (resource instanceof Singleton) {
40
- const typeName = resource.type.name || '';
41
- serviceTs.addImportPackage('@opra/client', ['HttpSingletonNode']);
42
- serviceTs.addImportFile(`types/${typeName}-type`, [typeName]);
43
- const operations = Object.keys(resource.operations)
44
- .map(x => `'${x}'`).join(' | ');
45
- if (!operations.length) {
46
- this.logger.warn(chalk.yellow('WARN: ') +
47
- `Ignoring "${chalk.whiteBright(resource.name)}" resource. No operations available.`);
48
- continue;
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
- serviceTs.content += jsDoc + `
51
- get ${resource.name}(): Pick<HttpSingletonNode<${typeName}>, ${operations}> {
52
- return this.$client.singleton('${resource.name}');
53
- }\n`;
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
- serviceTs.content += '}';
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(targetDir = '') {
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
- const expFile = await this.generateTypeFile(dataType, targetDir);
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, targetDir = '') {
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
- let filePath;
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 if (dataType instanceof ComplexType)
35
- filePath = `/types/${typeName}-type.ts`;
36
- else if (dataType instanceof EnumType) {
37
- filePath = `/enums/${typeName}-enum.ts`;
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
- else
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.addExportFile(file.filename);
47
- file.content += `\n/**\n * ${wrapJSDocString(dataType.description || typeName)}
48
- * @interface ${typeName}
49
- * @kind ${dataType.kind}
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
- */\n`;
52
- if (dataType instanceof SimpleType) {
53
- file.content += `export type ${typeName} = ` + await this.generateSimpleTypeDefinition(file, dataType);
48
+ */
49
+ export enum ${typeName} ` + await this.generateEnumTypeDefinition(file, dataType);
54
50
  }
55
- else if (dataType instanceof EnumType) {
56
- file.content += `export enum ${typeName} ` + await this.generateEnumTypeDefinition(file, dataType);
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
- else if (dataType instanceof ComplexType) {
59
- file.content += `export interface ${typeName} ${await this.generateComplexTypeDefinition(file, dataType, true)}`;
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.addImportFile(f.filename, [dataType.name]);
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 [k, v] of Object.entries(dataType.values)) {
170
+ for (const [value, info] of Object.entries(dataType.values)) {
160
171
  // Print JSDoc
161
172
  let jsDoc = '';
162
- if (dataType.meanings[k])
163
- jsDoc += ` * ${dataType.meanings[k]}\n`;
173
+ if (dataType.values[value].description)
174
+ jsDoc += ` * ${dataType.values[value].description}\n`;
164
175
  if (jsDoc)
165
- out += `/**\n${jsDoc}*/\n`;
166
- out += `${k}`;
167
- if (v)
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.type, forInterface);
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.importFiles = {};
6
+ this.imports = {};
7
7
  this.exportFiles = {};
8
8
  this.exportTypes = [];
9
9
  this.header = '';
10
10
  this.content = '';
11
- this.addImportPackage = (name, types) => {
12
- this.importFiles[name] = this.importFiles[name] || [];
13
- types?.forEach(x => {
14
- if (!this.importFiles[name].includes(x))
15
- this.importFiles[name].push(x);
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.importFiles[filename] = this.importFiles[filename] || [];
25
- types?.forEach(x => {
26
- if (!this.importFiles[filename].includes(x))
27
- this.importFiles[filename].push(x);
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.addExportFile = (filename, types) => {
31
- filename = path.resolve(this.dirname, filename);
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.importFiles)
49
- .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
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 types = this.importFiles[filename];
63
+ const imp = this.imports[filename];
52
64
  let relFile = filename;
53
- if (path.isAbsolute(filename)) {
54
- relFile = relativePath(dirname, filename);
65
+ if (!isPackageName(filename)) {
55
66
  if (options?.importExt)
56
67
  relFile += '.js';
57
68
  }
58
- return `import ${types.length ? '{ ' + types.join(', ') + ' } from ' : ''}'${relFile}';`;
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 (path.isAbsolute(filename)) {
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
- export function relativePath(from, to) {
83
- const s = path.relative(from, to);
84
- return s.startsWith('.') ? s : ('./' + s);
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.25.4",
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.25.4",
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 { processResources } from './process-resources.js';
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 processSources: typeof processResources;
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, targetDir?: string): Promise<void>;
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, targetDir?: string): Promise<TsFile | undefined>;
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
- importFiles: Record<string, string[]>;
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
- addImportPackage: (name: string, types?: string[]) => void;
11
- addImportFile: (filename: string, types?: string[]) => void;
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;