api 7.0.0-alpha.3 → 7.0.0-alpha.6
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/bin/api.js +2 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +6 -33
- package/dist/bin.js.map +1 -0
- package/dist/codegen/{language.d.ts → codegenerator.d.ts} +9 -5
- package/dist/codegen/codegenerator.d.ts.map +1 -0
- package/dist/codegen/{language.js → codegenerator.js} +4 -6
- package/dist/codegen/codegenerator.js.map +1 -0
- package/dist/codegen/factory.d.ts +7 -0
- package/dist/codegen/factory.d.ts.map +1 -0
- package/dist/codegen/factory.js +14 -0
- package/dist/codegen/factory.js.map +1 -0
- package/dist/codegen/languages/typescript/index.d.ts +90 -0
- package/dist/codegen/languages/typescript/index.d.ts.map +1 -0
- package/dist/codegen/languages/{typescript.js → typescript/index.js} +277 -216
- package/dist/codegen/languages/typescript/index.js.map +1 -0
- package/dist/codegen/languages/typescript/util.d.ts +1 -0
- package/dist/codegen/languages/typescript/util.d.ts.map +1 -0
- package/dist/codegen/languages/typescript/util.js +10 -18
- package/dist/codegen/languages/typescript/util.js.map +1 -0
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +4 -8
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +56 -67
- package/dist/commands/install.js.map +1 -0
- package/dist/fetcher.d.ts +1 -0
- package/dist/fetcher.d.ts.map +1 -0
- package/dist/fetcher.js +12 -17
- package/dist/fetcher.js.map +1 -0
- package/dist/lib/prompt.d.ts +1 -0
- package/dist/lib/prompt.d.ts.map +1 -0
- package/dist/lib/prompt.js +4 -9
- package/dist/lib/prompt.js.map +1 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +4 -9
- package/dist/logger.js.map +1 -0
- package/dist/packageInfo.d.ts +2 -1
- package/dist/packageInfo.d.ts.map +1 -0
- package/dist/packageInfo.js +3 -5
- package/dist/packageInfo.js.map +1 -0
- package/dist/storage.d.ts +2 -1
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +43 -40
- package/dist/storage.js.map +1 -0
- package/package.json +11 -10
- package/src/bin.ts +2 -2
- package/src/codegen/{language.ts → codegenerator.ts} +16 -6
- package/src/codegen/factory.ts +23 -0
- package/src/codegen/languages/{typescript.ts → typescript/index.ts} +287 -213
- package/src/commands/index.ts +1 -1
- package/src/commands/install.ts +27 -36
- package/src/packageInfo.ts +1 -1
- package/src/storage.ts +14 -5
- package/tsconfig.json +3 -9
- package/bin/api +0 -2
- package/dist/codegen/index.d.ts +0 -4
- package/dist/codegen/index.js +0 -23
- package/dist/codegen/languages/typescript.d.ts +0 -111
- package/src/codegen/index.ts +0 -31
|
@@ -1,151 +1,147 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import corePkg from '@readme/api-core/package.json' assert { type: 'json' };
|
|
3
|
+
import execa from 'execa';
|
|
4
|
+
import setWith from 'lodash.setwith';
|
|
5
|
+
import semver from 'semver';
|
|
6
|
+
import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationKind } from 'ts-morph';
|
|
7
|
+
import logger from '../../../logger.js';
|
|
8
|
+
import CodeGenerator from '../../codegenerator.js';
|
|
9
|
+
import { docblockEscape, generateTypeName, wordWrap } from './util.js';
|
|
10
|
+
/**
|
|
11
|
+
* This is the conversion prefix that we add to all `$ref` pointers we find in generated JSON
|
|
12
|
+
* Schema.
|
|
13
|
+
*
|
|
14
|
+
* Because the pointer name is a string we want to have it reference the schema constant we're
|
|
15
|
+
* adding into the codegen'd schema file. As there's no way, not even using `eval()` in this case,
|
|
16
|
+
* to convert a string to a constant we're prefixing them with this so we can later remove it and
|
|
17
|
+
* rewrite the value to a literal. eg. `'Pet'` becomes `Pet`.
|
|
18
|
+
*
|
|
19
|
+
* And because our TypeScript type name generator properly ignores `:`, this is safe to prepend to
|
|
20
|
+
* all generated type names.
|
|
21
|
+
*/
|
|
22
|
+
const REF_PLACEHOLDER = '::convert::';
|
|
23
|
+
const REF_PLACEHOLDER_REGEX = /"::convert::([a-zA-Z_$\\d]*)"/g;
|
|
24
|
+
export default class TSGenerator extends CodeGenerator {
|
|
16
25
|
project;
|
|
17
|
-
outputJS;
|
|
18
|
-
compilerTarget;
|
|
19
26
|
types;
|
|
20
27
|
sdk;
|
|
21
28
|
schemas;
|
|
22
29
|
usesHTTPMethodRangeInterface = false;
|
|
23
|
-
constructor(spec, specPath, identifier
|
|
24
|
-
const options = {
|
|
25
|
-
outputJS: false,
|
|
26
|
-
compilerTarget: 'cjs',
|
|
27
|
-
...opts,
|
|
28
|
-
};
|
|
29
|
-
if (!options.outputJS) {
|
|
30
|
-
// TypeScript compilation will always target towards ESM-like imports and exports.
|
|
31
|
-
options.compilerTarget = 'esm';
|
|
32
|
-
}
|
|
30
|
+
constructor(spec, specPath, identifier) {
|
|
33
31
|
super(spec, specPath, identifier);
|
|
34
32
|
this.requiredPackages = {
|
|
35
|
-
api: {
|
|
36
|
-
|
|
33
|
+
'@readme/api-core': {
|
|
34
|
+
dependencyType: 'production',
|
|
35
|
+
reason: "The core magic of your codegen'd SDK and is what is used for making requests.",
|
|
37
36
|
url: 'https://npm.im/api',
|
|
37
|
+
version:
|
|
38
|
+
// When running unit tests we're installing `@readme/api-core` but because that package
|
|
39
|
+
// source lives in this repository NPM will throw a gnarly "Cannot set properties of null
|
|
40
|
+
// (setting 'dev')" workspace error message because we're creating a funky circular
|
|
41
|
+
// dependency.
|
|
42
|
+
process.env.NODE_ENV === 'test'
|
|
43
|
+
? `file:${path.relative(__dirname, path.dirname(require.resolve('@readme/api-core/package.json')))}`
|
|
44
|
+
: corePkg.version,
|
|
38
45
|
},
|
|
39
46
|
'json-schema-to-ts': {
|
|
47
|
+
dependencyType: 'production',
|
|
40
48
|
reason: 'Required for TypeScript type handling.',
|
|
41
49
|
url: 'https://npm.im/json-schema-to-ts',
|
|
50
|
+
version: '^2.9.2',
|
|
42
51
|
},
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
tsup: {
|
|
53
|
+
dependencyType: 'development',
|
|
54
|
+
reason: "Used for compiling your codegen'd SDK into code that can be used in JS environments.",
|
|
55
|
+
url: 'https://tsup.egoist.dev/',
|
|
56
|
+
version: '^7.2.0',
|
|
46
57
|
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
quoteKind: ts_morph_1.QuoteKind.Single,
|
|
58
|
+
typescript: {
|
|
59
|
+
dependencyType: 'development',
|
|
60
|
+
reason: 'Required for `tsup`.',
|
|
61
|
+
version: '^5.2.2',
|
|
52
62
|
},
|
|
63
|
+
};
|
|
64
|
+
this.project = new Project({
|
|
53
65
|
compilerOptions: {
|
|
54
|
-
// If we're exporting a TypeScript SDK then we don't need to pollute the codegen directory
|
|
55
|
-
// with unnecessary declaration `.d.ts` files.
|
|
56
|
-
declaration: options.outputJS,
|
|
57
66
|
outDir: 'dist',
|
|
58
67
|
resolveJsonModule: true,
|
|
59
|
-
target:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
// Basically without this option CJS code will fail.
|
|
66
|
-
...(options.compilerTarget === 'cjs' ? { esModuleInterop: true } : {}),
|
|
68
|
+
target: ScriptTarget.ES2022,
|
|
69
|
+
},
|
|
70
|
+
manipulationSettings: {
|
|
71
|
+
indentationText: IndentationText.TwoSpaces,
|
|
72
|
+
quoteKind: QuoteKind.Single,
|
|
67
73
|
},
|
|
74
|
+
useInMemoryFileSystem: true,
|
|
68
75
|
});
|
|
69
|
-
this.compilerTarget = options.compilerTarget;
|
|
70
|
-
this.outputJS = options.outputJS;
|
|
71
76
|
this.types = new Map();
|
|
72
77
|
this.schemas = {};
|
|
73
78
|
}
|
|
74
|
-
|
|
79
|
+
// eslint-disable-next-line class-methods-use-this
|
|
80
|
+
async install(storage, opts = {}) {
|
|
75
81
|
const installDir = storage.getIdentifierStorageDir();
|
|
76
|
-
const info = this.spec.getDefinition().info;
|
|
77
|
-
let pkgVersion = semver_1.default.coerce(info.version);
|
|
78
|
-
if (!pkgVersion) {
|
|
79
|
-
// If the version that's in `info.version` isn't compatible with semver NPM won't be able to
|
|
80
|
-
// handle it properly so we need to fallback to something it can.
|
|
81
|
-
pkgVersion = semver_1.default.coerce('0.0.0');
|
|
82
|
-
}
|
|
83
|
-
const pkg = {
|
|
84
|
-
name: `@api/${storage.identifier}`,
|
|
85
|
-
version: pkgVersion.version,
|
|
86
|
-
main: `./index.${this.outputJS ? 'js' : 'ts'}`,
|
|
87
|
-
types: './index.d.ts', // Types are always present regardless if you're getting compiled JS.
|
|
88
|
-
};
|
|
89
|
-
node_fs_1.default.writeFileSync(node_path_1.default.join(installDir, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
90
82
|
const npmInstall = ['install', '--save', opts.dryRun ? '--dry-run' : ''].filter(Boolean);
|
|
91
|
-
// This will install packages required for the SDK within its installed directory in `.apis/`.
|
|
92
|
-
await (0, execa_1.default)('npm', [...npmInstall, ...Object.keys(this.requiredPackages)].filter(Boolean), {
|
|
93
|
-
cwd: installDir,
|
|
94
|
-
}).then(res => {
|
|
95
|
-
if (opts.dryRun) {
|
|
96
|
-
(opts.logger ? opts.logger : logger_1.default)(res.command);
|
|
97
|
-
(opts.logger ? opts.logger : logger_1.default)(res.stdout);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
83
|
// This will install the installed SDK as a dependency within the current working directory,
|
|
101
84
|
// adding `@api/<sdk identifier>` as a dependency there so you can load it with
|
|
102
85
|
// `require('@api/<sdk identifier>)`.
|
|
103
|
-
|
|
86
|
+
await execa('npm', [...npmInstall, installDir].filter(Boolean))
|
|
104
87
|
.then(res => {
|
|
105
88
|
if (opts.dryRun) {
|
|
106
|
-
(opts.logger ? opts.logger :
|
|
107
|
-
(opts.logger ? opts.logger :
|
|
89
|
+
(opts.logger ? opts.logger : logger)(res.command);
|
|
90
|
+
(opts.logger ? opts.logger : logger)(res.stdout);
|
|
108
91
|
}
|
|
109
92
|
})
|
|
110
93
|
.catch(err => {
|
|
94
|
+
// If `npm install` throws this error it always happens **after** our dependencies have been
|
|
95
|
+
// installed and is an annoying quirk that sometimes occurs when installing a package within
|
|
96
|
+
// our workspace as we're creating a circular dependency on `@readme/api-core`.
|
|
97
|
+
if (process.env.NODE_ENV === 'test' &&
|
|
98
|
+
err.message.includes("npm ERR! Cannot set properties of null (setting 'dev')")) {
|
|
99
|
+
(opts.logger ? opts.logger : logger)("npm threw an error but we're ignoring it");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
111
102
|
if (opts.dryRun) {
|
|
112
|
-
(opts.logger ? opts.logger :
|
|
103
|
+
(opts.logger ? opts.logger : logger)(err.message);
|
|
113
104
|
return;
|
|
114
105
|
}
|
|
115
106
|
throw err;
|
|
116
107
|
});
|
|
117
108
|
}
|
|
118
109
|
/**
|
|
119
|
-
* Compile the
|
|
110
|
+
* Compile the TS code we generated into JS for use in CJS and ESM environments.
|
|
120
111
|
*
|
|
121
112
|
*/
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!this.outputJS) {
|
|
139
|
-
const types = Array.from(this.types.keys());
|
|
140
|
-
types.sort();
|
|
141
|
-
sdkSource.addExportDeclarations([
|
|
142
|
-
{
|
|
143
|
-
isTypeOnly: true,
|
|
144
|
-
namedExports: types,
|
|
145
|
-
moduleSpecifier: './types',
|
|
146
|
-
},
|
|
147
|
-
]);
|
|
113
|
+
// eslint-disable-next-line class-methods-use-this
|
|
114
|
+
async compile(storage, opts = {}) {
|
|
115
|
+
const installDir = storage.getIdentifierStorageDir();
|
|
116
|
+
await execa('npx', ['tsup'], {
|
|
117
|
+
cwd: installDir,
|
|
118
|
+
})
|
|
119
|
+
.then(res => {
|
|
120
|
+
if (opts.dryRun) {
|
|
121
|
+
(opts.logger ? opts.logger : logger)(res.command);
|
|
122
|
+
(opts.logger ? opts.logger : logger)(res.stdout);
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.catch(err => {
|
|
126
|
+
if (opts.dryRun) {
|
|
127
|
+
(opts.logger ? opts.logger : logger)(err.message);
|
|
128
|
+
return;
|
|
148
129
|
}
|
|
130
|
+
throw err;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Generate the current OpenAPI definition into a TypeScript library.
|
|
135
|
+
*
|
|
136
|
+
*/
|
|
137
|
+
async generate() {
|
|
138
|
+
const srcDirectory = this.project.createDirectory('src');
|
|
139
|
+
const sdkSource = this.createSDKSource(srcDirectory);
|
|
140
|
+
this.createPackageJSON();
|
|
141
|
+
this.createTSConfig();
|
|
142
|
+
if (Object.keys(this.schemas).length) {
|
|
143
|
+
this.createSchemasFile(srcDirectory);
|
|
144
|
+
this.createTypesFile(srcDirectory);
|
|
149
145
|
}
|
|
150
146
|
else {
|
|
151
147
|
// If we don't have any schemas then we shouldn't import a `types` file that doesn't exist.
|
|
@@ -162,43 +158,18 @@ class TSGenerator extends language_1.default {
|
|
|
162
158
|
.find(id => id.getText().includes('HTTPMethodRange'))
|
|
163
159
|
?.replaceWithText("import type { ConfigOptions, FetchResponse } from '@readme/api-core';");
|
|
164
160
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// user will have `.d.ts` files for them instead.
|
|
174
|
-
return {};
|
|
175
|
-
}
|
|
176
|
-
let code = sourceFile.text;
|
|
177
|
-
if (file === 'index.js' && this.compilerTarget === 'cjs') {
|
|
178
|
-
/**
|
|
179
|
-
* There's an annoying quirk with `ts-morph` where if we're exporting a default export
|
|
180
|
-
* to a CJS environment, it'll export it as `exports.default`. Because we don't want
|
|
181
|
-
* folks in these environments to have to load their SDKs with
|
|
182
|
-
* `require('@api/sdk').default` we're overriding that here to change it to being the
|
|
183
|
-
* module exports.
|
|
184
|
-
*
|
|
185
|
-
* `ts-morph` unfortunately doesn't give us any options for programatically doing this
|
|
186
|
-
* so we need to resort to modifying the emitted JS code.
|
|
187
|
-
*/
|
|
188
|
-
code = code
|
|
189
|
-
.replace(/Object\.defineProperty\(exports, '__esModule', { value: true }\);\n/, '')
|
|
190
|
-
.replace('exports.default = createSDK;', 'module.exports = createSDK;');
|
|
191
|
-
}
|
|
161
|
+
return [
|
|
162
|
+
...this.project.getSourceFiles().map(sourceFile => {
|
|
163
|
+
// `getFilePath` will always return a string that contains a preceeding directory separator
|
|
164
|
+
// however when we're creating these codegen'd files that may cause us to create that file
|
|
165
|
+
// in the root directory (because it's preceeded by a `/`). We don't want that to happen so
|
|
166
|
+
// we're slicing off that first character.
|
|
167
|
+
let filePath = sourceFile.getFilePath().toString();
|
|
168
|
+
filePath = filePath.substring(1);
|
|
192
169
|
return {
|
|
193
|
-
[
|
|
170
|
+
[filePath]: sourceFile.getFullText(),
|
|
194
171
|
};
|
|
195
|
-
})
|
|
196
|
-
.reduce((prev, next) => Object.assign(prev, next));
|
|
197
|
-
}
|
|
198
|
-
return [
|
|
199
|
-
...this.project.getSourceFiles().map(sourceFile => ({
|
|
200
|
-
[sourceFile.getBaseName()]: sourceFile.getFullText(),
|
|
201
|
-
})),
|
|
172
|
+
}),
|
|
202
173
|
// Because we're returning the raw source files for TS generation we also need to separately
|
|
203
174
|
// emit out our declaration files so we can put those into a separate file in the installed
|
|
204
175
|
// SDK directory.
|
|
@@ -206,7 +177,7 @@ class TSGenerator extends language_1.default {
|
|
|
206
177
|
.emitToMemory({ emitOnlyDtsFiles: true })
|
|
207
178
|
.getFiles()
|
|
208
179
|
.map(sourceFile => ({
|
|
209
|
-
[
|
|
180
|
+
[path.basename(sourceFile.filePath)]: sourceFile.text,
|
|
210
181
|
})),
|
|
211
182
|
].reduce((prev, next) => Object.assign(prev, next));
|
|
212
183
|
}
|
|
@@ -214,9 +185,9 @@ class TSGenerator extends language_1.default {
|
|
|
214
185
|
* Create our main SDK source file.
|
|
215
186
|
*
|
|
216
187
|
*/
|
|
217
|
-
|
|
188
|
+
createSDKSource(sourceDirectory) {
|
|
218
189
|
const { operations } = this.loadOperationsAndMethods();
|
|
219
|
-
const sourceFile =
|
|
190
|
+
const sourceFile = sourceDirectory.createSourceFile('index.ts', '');
|
|
220
191
|
sourceFile.addImportDeclarations([
|
|
221
192
|
// This import will be automatically removed later if the SDK ends up not having any types.
|
|
222
193
|
{ defaultImport: 'type * as types', moduleSpecifier: './types' },
|
|
@@ -225,22 +196,17 @@ class TSGenerator extends language_1.default {
|
|
|
225
196
|
defaultImport: 'type { ConfigOptions, FetchResponse, HTTPMethodRange }',
|
|
226
197
|
moduleSpecifier: '@readme/api-core',
|
|
227
198
|
},
|
|
228
|
-
{ defaultImport: 'Oas', moduleSpecifier: 'oas' },
|
|
229
199
|
{ defaultImport: 'APICore', moduleSpecifier: '@readme/api-core' },
|
|
230
200
|
{ defaultImport: 'definition', moduleSpecifier: this.specPath },
|
|
231
201
|
]);
|
|
232
202
|
// @todo add TOS, License, info.* to a docblock at the top of the SDK.
|
|
233
203
|
this.sdk = sourceFile.addClass({
|
|
234
204
|
name: 'SDK',
|
|
235
|
-
properties: [
|
|
236
|
-
{ name: 'spec', type: 'Oas' },
|
|
237
|
-
{ name: 'core', type: 'APICore' },
|
|
238
|
-
],
|
|
205
|
+
properties: [{ name: 'core', type: 'APICore' }],
|
|
239
206
|
});
|
|
240
207
|
this.sdk.addConstructor({
|
|
241
208
|
statements: writer => {
|
|
242
|
-
writer.
|
|
243
|
-
writer.write('this.core = new APICore(this.spec, ').quote(this.userAgent).write(');');
|
|
209
|
+
writer.write('this.core = new APICore(definition, ').quote(this.userAgent).write(');');
|
|
244
210
|
return writer;
|
|
245
211
|
},
|
|
246
212
|
});
|
|
@@ -252,12 +218,12 @@ class TSGenerator extends language_1.default {
|
|
|
252
218
|
statements: writer => writer.writeLine('this.core.setConfig(config);'),
|
|
253
219
|
docs: [
|
|
254
220
|
{
|
|
255
|
-
description: writer => writer.writeLine(
|
|
221
|
+
description: writer => writer.writeLine(wordWrap('Optionally configure various options that the SDK allows.')),
|
|
256
222
|
tags: [
|
|
257
223
|
{ tagName: 'param', text: 'config Object of supported SDK options and toggles.' },
|
|
258
224
|
{
|
|
259
225
|
tagName: 'param',
|
|
260
|
-
text:
|
|
226
|
+
text: wordWrap('config.timeout Override the default `fetch` request timeout of 30 seconds. This number should be represented in milliseconds.'),
|
|
261
227
|
},
|
|
262
228
|
],
|
|
263
229
|
},
|
|
@@ -273,7 +239,7 @@ class TSGenerator extends language_1.default {
|
|
|
273
239
|
},
|
|
274
240
|
docs: [
|
|
275
241
|
{
|
|
276
|
-
description: writer => writer.writeLine(
|
|
242
|
+
description: writer => writer.writeLine(wordWrap(`If the API you're using requires authentication you can supply the required credentials through this method and the library will magically determine how they should be used within your API request.
|
|
277
243
|
|
|
278
244
|
With the exception of OpenID and MutualTLS, it supports all forms of authentication supported by the OpenAPI specification.
|
|
279
245
|
|
|
@@ -305,7 +271,7 @@ sdk.auth('myApiKey');`)),
|
|
|
305
271
|
statements: writer => writer.writeLine('this.core.setServer(url, variables);'),
|
|
306
272
|
docs: [
|
|
307
273
|
{
|
|
308
|
-
description: writer => writer.writeLine(
|
|
274
|
+
description: writer => writer.writeLine(wordWrap(`If the API you're using offers alternate server URLs, and server variables, you can tell the SDK which one to use with this method. To use it you can supply either one of the server URLs that are contained within the OpenAPI definition (along with any server variables), or you can pass it a fully qualified URL to use (that may or may not exist within the OpenAPI definition).
|
|
309
275
|
|
|
310
276
|
@example <caption>Server URL with server variables</caption>
|
|
311
277
|
sdk.server('https://{region}.api.example.com/{basePath}', {
|
|
@@ -329,12 +295,12 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
329
295
|
});
|
|
330
296
|
// Export our SDK into the source file.
|
|
331
297
|
sourceFile.addVariableStatement({
|
|
332
|
-
declarationKind:
|
|
298
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
333
299
|
declarations: [
|
|
334
300
|
{
|
|
335
301
|
name: 'createSDK',
|
|
336
302
|
initializer: writer => {
|
|
337
|
-
// `ts-morph` doesn't have any way to cleanly create an
|
|
303
|
+
// `ts-morph` doesn't have any way to cleanly create an IIFE.
|
|
338
304
|
writer.writeLine('(() => { return new SDK(); })()');
|
|
339
305
|
return writer;
|
|
340
306
|
},
|
|
@@ -342,52 +308,147 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
342
308
|
],
|
|
343
309
|
});
|
|
344
310
|
sourceFile.addExportAssignment({
|
|
345
|
-
// Because
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
isExportEquals:
|
|
311
|
+
// Because we're exporting `createSDK` as an IIFE constant we need to have it exported as
|
|
312
|
+
// `export default createSDK`. `addExportAssignment` by default wants it exported as
|
|
313
|
+
// `export = createSDK`, which will throw TS errors because we may also be exporting types in
|
|
314
|
+
// the `./types.ts` file.
|
|
315
|
+
isExportEquals: false,
|
|
350
316
|
expression: 'createSDK',
|
|
351
317
|
});
|
|
352
318
|
return sourceFile;
|
|
353
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Create the `tsconfig.json` file that will allow this SDK to be compiled for use.
|
|
322
|
+
*
|
|
323
|
+
*/
|
|
324
|
+
createTSConfig() {
|
|
325
|
+
const sourceFile = this.project.createSourceFile('tsconfig.json', '');
|
|
326
|
+
const config = {
|
|
327
|
+
compilerOptions: {
|
|
328
|
+
module: 'NodeNext',
|
|
329
|
+
resolveJsonModule: true,
|
|
330
|
+
},
|
|
331
|
+
include: ['./src/**/*'],
|
|
332
|
+
};
|
|
333
|
+
sourceFile.addStatements(JSON.stringify(config, null, 2));
|
|
334
|
+
return sourceFile;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Create the `package.json` file that will ultimately make this SDK available to use.
|
|
338
|
+
*
|
|
339
|
+
*/
|
|
340
|
+
createPackageJSON() {
|
|
341
|
+
const sourceFile = this.project.createSourceFile('package.json', '');
|
|
342
|
+
const hasTypes = !!Object.keys(this.schemas).length;
|
|
343
|
+
const info = this.spec.getDefinition().info;
|
|
344
|
+
let pkgVersion = semver.coerce(info.version);
|
|
345
|
+
if (!pkgVersion) {
|
|
346
|
+
// If the version that's in `info.version` isn't compatible with semver NPM won't be able to
|
|
347
|
+
// handle it properly so we need to fallback to something it can.
|
|
348
|
+
pkgVersion = semver.coerce('0.0.0');
|
|
349
|
+
}
|
|
350
|
+
const tsupOptions = {
|
|
351
|
+
cjsInterop: true,
|
|
352
|
+
clean: true,
|
|
353
|
+
dts: true,
|
|
354
|
+
entry: [
|
|
355
|
+
'./src/index.ts',
|
|
356
|
+
// If this SDK has schemas and generated types then we should also export those too so
|
|
357
|
+
// they're available to use.
|
|
358
|
+
hasTypes ? './src/types.ts' : '',
|
|
359
|
+
].filter(Boolean),
|
|
360
|
+
format: ['esm', 'cjs'],
|
|
361
|
+
minify: false,
|
|
362
|
+
shims: true,
|
|
363
|
+
sourcemap: true,
|
|
364
|
+
splitting: true,
|
|
365
|
+
};
|
|
366
|
+
const dependencies = Object.entries(this.requiredPackages)
|
|
367
|
+
.map(([dep, { dependencyType, version }]) => (dependencyType === 'production' ? { [dep]: version } : {}))
|
|
368
|
+
.reduce((prev, next) => Object.assign(prev, next));
|
|
369
|
+
const devDependencies = Object.entries(this.requiredPackages)
|
|
370
|
+
.map(([dep, { dependencyType, version }]) => (dependencyType === 'development' ? { [dep]: version } : {}))
|
|
371
|
+
.reduce((prev, next) => Object.assign(prev, next));
|
|
372
|
+
const pkg = {
|
|
373
|
+
name: `@api/${this.identifier}`,
|
|
374
|
+
version: pkgVersion.version,
|
|
375
|
+
main: './dist/index.js',
|
|
376
|
+
types: './dist/index.d.ts',
|
|
377
|
+
module: './dist/index.mts',
|
|
378
|
+
exports: {
|
|
379
|
+
'.': {
|
|
380
|
+
import: './dist/index.mjs',
|
|
381
|
+
require: './dist/index.js',
|
|
382
|
+
},
|
|
383
|
+
...(hasTypes
|
|
384
|
+
? {
|
|
385
|
+
'./types': {
|
|
386
|
+
import: './dist/types.d.mts',
|
|
387
|
+
require: './dist/types.d.ts',
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
: {}),
|
|
391
|
+
},
|
|
392
|
+
files: ['dist'],
|
|
393
|
+
scripts: {
|
|
394
|
+
prepare: 'tsup',
|
|
395
|
+
},
|
|
396
|
+
dependencies,
|
|
397
|
+
devDependencies,
|
|
398
|
+
tsup: tsupOptions,
|
|
399
|
+
};
|
|
400
|
+
sourceFile.addStatements(JSON.stringify(pkg, null, 2));
|
|
401
|
+
return sourceFile;
|
|
402
|
+
}
|
|
354
403
|
/**
|
|
355
404
|
* Create our main schemas file. This is where all of the JSON Schema that our TypeScript typing
|
|
356
405
|
* infrastructure sources its data from. Without this there are no types.
|
|
357
406
|
*
|
|
358
407
|
*/
|
|
359
|
-
createSchemasFile() {
|
|
360
|
-
const sourceFile =
|
|
408
|
+
createSchemasFile(sourceDirectory) {
|
|
409
|
+
const sourceFile = sourceDirectory.createSourceFile('schemas.ts', '');
|
|
410
|
+
const schemasDir = sourceDirectory.createDirectory('schemas');
|
|
361
411
|
const sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort());
|
|
362
412
|
Array.from(sortedSchemas).forEach(([schemaName, schema]) => {
|
|
363
|
-
|
|
364
|
-
|
|
413
|
+
const schemaFile = schemasDir.createSourceFile(`${schemaName}.ts`);
|
|
414
|
+
// Because we're chunking our schemas into a `schemas/` directory we need to add imports
|
|
415
|
+
// for these schemas into our main `schemas.ts` file.`
|
|
416
|
+
sourceFile.addImportDeclaration({
|
|
417
|
+
defaultImport: schemaName,
|
|
418
|
+
moduleSpecifier: `./schemas/${schemaName}`,
|
|
419
|
+
});
|
|
420
|
+
let str = JSON.stringify(schema);
|
|
421
|
+
const referencedSchemas = str.match(REF_PLACEHOLDER_REGEX)?.map(s => s.replace(REF_PLACEHOLDER_REGEX, '$1'));
|
|
422
|
+
if (referencedSchemas) {
|
|
423
|
+
referencedSchemas.sort();
|
|
424
|
+
referencedSchemas.forEach(ref => {
|
|
425
|
+
// Because this schema is referenced from another file we need to create an `import`
|
|
426
|
+
// declaration for it.
|
|
427
|
+
schemaFile.addImportDeclaration({
|
|
428
|
+
defaultImport: ref,
|
|
429
|
+
moduleSpecifier: `./${ref}`,
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
// Load the schema into the schema file within the `schemas/` directory.
|
|
434
|
+
schemaFile.addVariableStatement({
|
|
435
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
365
436
|
declarations: [
|
|
366
437
|
{
|
|
367
438
|
name: schemaName,
|
|
368
439
|
initializer: writer => {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
*
|
|
373
|
-
* Because the pointer name is a string we want to have it reference the schema
|
|
374
|
-
* constant we're adding into the codegen'd schema file. As there's no way, not even
|
|
375
|
-
* using `eval()` in this case, to convert a string to a constant we're prefixing
|
|
376
|
-
* them with this so we can later remove it and rewrite the value to a literal.
|
|
377
|
-
* eg. `'Pet'` becomes `Pet`.
|
|
378
|
-
*
|
|
379
|
-
* And because our TypeScript type name generator properly ignores `:`, this is safe
|
|
380
|
-
* to prepend to all generated type names.
|
|
381
|
-
*/
|
|
382
|
-
let str = JSON.stringify(schema);
|
|
383
|
-
str = str.replace(/"::convert::([a-zA-Z_$\\d]*)"/g, '$1');
|
|
440
|
+
// We can't have `::convert::<schemaName>` variables within these schema files so we
|
|
441
|
+
// need to clean them up.
|
|
442
|
+
str = str.replace(REF_PLACEHOLDER_REGEX, '$1');
|
|
384
443
|
writer.writeLine(`${str} as const`);
|
|
385
444
|
return writer;
|
|
386
445
|
},
|
|
387
446
|
},
|
|
388
447
|
],
|
|
389
448
|
});
|
|
449
|
+
schemaFile.addStatements(`export default ${schemaName}`);
|
|
390
450
|
});
|
|
451
|
+
// Export all of our schemas from inside the main `schemas.ts` file.
|
|
391
452
|
sourceFile.addStatements(`export { ${Array.from(sortedSchemas.keys()).join(', ')} }`);
|
|
392
453
|
return sourceFile;
|
|
393
454
|
}
|
|
@@ -398,8 +459,8 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
398
459
|
*
|
|
399
460
|
* @see {@link https://npm.im/json-schema-to-ts}
|
|
400
461
|
*/
|
|
401
|
-
createTypesFile() {
|
|
402
|
-
const sourceFile =
|
|
462
|
+
createTypesFile(sourceDirectory) {
|
|
463
|
+
const sourceFile = sourceDirectory.createSourceFile('types.ts', '');
|
|
403
464
|
sourceFile.addImportDeclarations([
|
|
404
465
|
{ defaultImport: 'type { FromSchema }', moduleSpecifier: 'json-schema-to-ts' },
|
|
405
466
|
{ defaultImport: '* as schemas', moduleSpecifier: './schemas' },
|
|
@@ -409,18 +470,6 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
409
470
|
});
|
|
410
471
|
return sourceFile;
|
|
411
472
|
}
|
|
412
|
-
/**
|
|
413
|
-
* Add a new JSDoc `@tag` to an existing docblock.
|
|
414
|
-
*
|
|
415
|
-
*/
|
|
416
|
-
static addTagToDocblock(docblock, tag) {
|
|
417
|
-
const tags = docblock.tags ?? [];
|
|
418
|
-
tags.push(tag);
|
|
419
|
-
return {
|
|
420
|
-
...docblock,
|
|
421
|
-
tags,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
473
|
/**
|
|
425
474
|
* Create operation accessors on the SDK.
|
|
426
475
|
*
|
|
@@ -435,18 +484,18 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
435
484
|
// what we surface the main docblock description.
|
|
436
485
|
docblock.description = writer => {
|
|
437
486
|
if (description) {
|
|
438
|
-
writer.writeLine(
|
|
487
|
+
writer.writeLine(docblockEscape(wordWrap(description)));
|
|
439
488
|
}
|
|
440
489
|
else if (summary) {
|
|
441
|
-
writer.writeLine(
|
|
490
|
+
writer.writeLine(docblockEscape(wordWrap(summary)));
|
|
442
491
|
}
|
|
443
492
|
writer.newLineIfLastNot();
|
|
444
493
|
return writer;
|
|
445
494
|
};
|
|
446
495
|
if (summary && description) {
|
|
447
|
-
docblock = TSGenerator
|
|
496
|
+
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
448
497
|
tagName: 'summary',
|
|
449
|
-
text:
|
|
498
|
+
text: docblockEscape(wordWrap(summary)),
|
|
450
499
|
});
|
|
451
500
|
}
|
|
452
501
|
}
|
|
@@ -488,9 +537,9 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
488
537
|
return `FetchResponse<number, ${responseType}>`;
|
|
489
538
|
}
|
|
490
539
|
if (Number(statusPrefix) >= 4) {
|
|
491
|
-
docblock = TSGenerator
|
|
540
|
+
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
492
541
|
tagName: 'throws',
|
|
493
|
-
text: `FetchError<${status}, ${responseType}>${responseDescription ?
|
|
542
|
+
text: `FetchError<${status}, ${responseType}>${responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''}`,
|
|
494
543
|
});
|
|
495
544
|
return false;
|
|
496
545
|
}
|
|
@@ -500,9 +549,9 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
500
549
|
// 400 and 500 status code families are thrown as exceptions so adding them as a possible
|
|
501
550
|
// return type isn't valid.
|
|
502
551
|
if (Number(status) >= 400) {
|
|
503
|
-
docblock = TSGenerator
|
|
552
|
+
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
504
553
|
tagName: 'throws',
|
|
505
|
-
text: `FetchError<${status}, ${responseType}>${responseDescription ?
|
|
554
|
+
text: `FetchError<${status}, ${responseType}>${responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''}`,
|
|
506
555
|
});
|
|
507
556
|
return false;
|
|
508
557
|
}
|
|
@@ -641,9 +690,9 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
641
690
|
// As our schemas are dereferenced in the `oas` library we don't want to pollute our
|
|
642
691
|
// codegen'd schemas file with duplicate schemas.
|
|
643
692
|
if ('x-readme-ref-name' in s && typeof s['x-readme-ref-name'] !== 'undefined') {
|
|
644
|
-
const typeName =
|
|
693
|
+
const typeName = generateTypeName(s['x-readme-ref-name']);
|
|
645
694
|
this.addSchemaToExport(s, typeName, typeName);
|
|
646
|
-
return
|
|
695
|
+
return `${REF_PLACEHOLDER}${typeName}`;
|
|
647
696
|
}
|
|
648
697
|
return s;
|
|
649
698
|
},
|
|
@@ -657,14 +706,14 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
657
706
|
return Object.entries(res)
|
|
658
707
|
.map(([paramType, schema]) => {
|
|
659
708
|
let typeName;
|
|
660
|
-
if (typeof schema === 'string' && schema.startsWith(
|
|
709
|
+
if (typeof schema === 'string' && schema.startsWith(REF_PLACEHOLDER)) {
|
|
661
710
|
// If this schema is a string and has our conversion prefix then we've already created
|
|
662
711
|
// a type for it.
|
|
663
|
-
typeName = schema.replace(
|
|
712
|
+
typeName = schema.replace(REF_PLACEHOLDER, '');
|
|
664
713
|
}
|
|
665
714
|
else {
|
|
666
|
-
typeName =
|
|
667
|
-
this.addSchemaToExport(schema, typeName, `${
|
|
715
|
+
typeName = generateTypeName(operationId, paramType, 'param');
|
|
716
|
+
this.addSchemaToExport(schema, typeName, `${generateTypeName(operationId)}.${paramType}`);
|
|
668
717
|
}
|
|
669
718
|
return {
|
|
670
719
|
// Types are prefixed with `types.` because that's how we're importing them from
|
|
@@ -691,9 +740,9 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
691
740
|
// As our schemas are dereferenced in the `oas` library we don't want to pollute our
|
|
692
741
|
// codegen'd schemas file with duplicate schemas.
|
|
693
742
|
if ('x-readme-ref-name' in s && typeof s['x-readme-ref-name'] !== 'undefined') {
|
|
694
|
-
const typeName =
|
|
743
|
+
const typeName = generateTypeName(s['x-readme-ref-name']);
|
|
695
744
|
this.addSchemaToExport(s, typeName, `${typeName}`);
|
|
696
|
-
return
|
|
745
|
+
return `${REF_PLACEHOLDER}${typeName}`;
|
|
697
746
|
}
|
|
698
747
|
return s;
|
|
699
748
|
},
|
|
@@ -709,17 +758,17 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
709
758
|
const res = Object.entries(schemas)
|
|
710
759
|
.map(([status, { description, schema }]) => {
|
|
711
760
|
let typeName;
|
|
712
|
-
if (typeof schema === 'string' && schema.startsWith(
|
|
761
|
+
if (typeof schema === 'string' && schema.startsWith(REF_PLACEHOLDER)) {
|
|
713
762
|
// If this schema is a string and has our conversion prefix then we've already created
|
|
714
763
|
// a type for it.
|
|
715
|
-
typeName = schema.replace(
|
|
764
|
+
typeName = schema.replace(REF_PLACEHOLDER, '');
|
|
716
765
|
}
|
|
717
766
|
else {
|
|
718
|
-
typeName =
|
|
767
|
+
typeName = generateTypeName(operationId, 'response', status);
|
|
719
768
|
// Because `status` will usually be a number here we need to set the pointer for it
|
|
720
769
|
// within an `[]` as if we do `FromSchema<typeof schemas.operation.response.200>`,
|
|
721
770
|
// TypeScript will throw a compilation error.
|
|
722
|
-
this.addSchemaToExport(schema, typeName, `${
|
|
771
|
+
this.addSchemaToExport(schema, typeName, `${generateTypeName(operationId)}.response['${status}']`);
|
|
723
772
|
}
|
|
724
773
|
return {
|
|
725
774
|
// Types are prefixed with `types.` because that's how we're importing them from
|
|
@@ -741,8 +790,20 @@ sdk.server('https://eu.api.example.com/v14');`)),
|
|
|
741
790
|
if (this.types.has(typeName)) {
|
|
742
791
|
return;
|
|
743
792
|
}
|
|
744
|
-
(
|
|
793
|
+
setWith(this.schemas, pointer, schema, Object);
|
|
745
794
|
this.types.set(typeName, `FromSchema<typeof schemas.${pointer}>`);
|
|
746
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* Add a new JSDoc `@tag` to an existing docblock.
|
|
798
|
+
*
|
|
799
|
+
*/
|
|
800
|
+
static #addTagToDocblock(docblock, tag) {
|
|
801
|
+
const tags = docblock.tags ?? [];
|
|
802
|
+
tags.push(tag);
|
|
803
|
+
return {
|
|
804
|
+
...docblock,
|
|
805
|
+
tags,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
747
808
|
}
|
|
748
|
-
|
|
809
|
+
//# sourceMappingURL=index.js.map
|