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,35 +1,32 @@
|
|
|
1
|
-
import type Storage from '
|
|
2
|
-
import type { InstallerOptions } from '
|
|
1
|
+
import type Storage from '../../../storage.js';
|
|
2
|
+
import type { InstallerOptions } from '../../codegenerator.js';
|
|
3
3
|
import type Oas from 'oas';
|
|
4
4
|
import type Operation from 'oas/operation';
|
|
5
5
|
import type { HttpMethods, SchemaObject } from 'oas/rmoas.types';
|
|
6
6
|
import type { SemVer } from 'semver';
|
|
7
7
|
import type {
|
|
8
8
|
ClassDeclaration,
|
|
9
|
+
Directory,
|
|
9
10
|
JSDocStructure,
|
|
10
11
|
JSDocTagStructure,
|
|
11
12
|
OptionalKind,
|
|
12
13
|
ParameterDeclarationStructure,
|
|
13
14
|
} from 'ts-morph';
|
|
14
|
-
import type {
|
|
15
|
+
import type { Options } from 'tsup';
|
|
16
|
+
import type { JsonObject, PackageJson, TsConfigJson } from 'type-fest';
|
|
15
17
|
|
|
16
|
-
import fs from 'node:fs';
|
|
17
18
|
import path from 'node:path';
|
|
18
19
|
|
|
20
|
+
import corePkg from '@readme/api-core/package.json' assert { type: 'json' };
|
|
19
21
|
import execa from 'execa';
|
|
20
22
|
import setWith from 'lodash.setwith';
|
|
21
23
|
import semver from 'semver';
|
|
22
24
|
import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationKind } from 'ts-morph';
|
|
23
25
|
|
|
24
|
-
import logger from '
|
|
25
|
-
import
|
|
26
|
+
import logger from '../../../logger.js';
|
|
27
|
+
import CodeGenerator from '../../codegenerator.js';
|
|
26
28
|
|
|
27
|
-
import { docblockEscape, generateTypeName, wordWrap } from './
|
|
28
|
-
|
|
29
|
-
export interface TSGeneratorOptions {
|
|
30
|
-
compilerTarget?: 'cjs' | 'esm';
|
|
31
|
-
outputJS?: boolean;
|
|
32
|
-
}
|
|
29
|
+
import { docblockEscape, generateTypeName, wordWrap } from './util.js';
|
|
33
30
|
|
|
34
31
|
interface OperationTypeHousing {
|
|
35
32
|
operation: Operation;
|
|
@@ -45,13 +42,24 @@ interface OperationTypeHousing {
|
|
|
45
42
|
};
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
/**
|
|
46
|
+
* This is the conversion prefix that we add to all `$ref` pointers we find in generated JSON
|
|
47
|
+
* Schema.
|
|
48
|
+
*
|
|
49
|
+
* Because the pointer name is a string we want to have it reference the schema constant we're
|
|
50
|
+
* adding into the codegen'd schema file. As there's no way, not even using `eval()` in this case,
|
|
51
|
+
* to convert a string to a constant we're prefixing them with this so we can later remove it and
|
|
52
|
+
* rewrite the value to a literal. eg. `'Pet'` becomes `Pet`.
|
|
53
|
+
*
|
|
54
|
+
* And because our TypeScript type name generator properly ignores `:`, this is safe to prepend to
|
|
55
|
+
* all generated type names.
|
|
56
|
+
*/
|
|
57
|
+
const REF_PLACEHOLDER = '::convert::';
|
|
58
|
+
const REF_PLACEHOLDER_REGEX = /"::convert::([a-zA-Z_$\\d]*)"/g;
|
|
59
|
+
|
|
60
|
+
export default class TSGenerator extends CodeGenerator {
|
|
49
61
|
project: Project;
|
|
50
62
|
|
|
51
|
-
outputJS: boolean;
|
|
52
|
-
|
|
53
|
-
compilerTarget: 'cjs' | 'esm';
|
|
54
|
-
|
|
55
63
|
types: Map<string, string>;
|
|
56
64
|
|
|
57
65
|
sdk!: ClassDeclaration;
|
|
@@ -70,101 +78,107 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
70
78
|
|
|
71
79
|
usesHTTPMethodRangeInterface = false;
|
|
72
80
|
|
|
73
|
-
constructor(spec: Oas, specPath: string, identifier: string
|
|
74
|
-
const options: { compilerTarget: 'cjs' | 'esm'; outputJS: boolean } = {
|
|
75
|
-
outputJS: false,
|
|
76
|
-
compilerTarget: 'cjs',
|
|
77
|
-
...opts,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
if (!options.outputJS) {
|
|
81
|
-
// TypeScript compilation will always target towards ESM-like imports and exports.
|
|
82
|
-
options.compilerTarget = 'esm';
|
|
83
|
-
}
|
|
84
|
-
|
|
81
|
+
constructor(spec: Oas, specPath: string, identifier: string) {
|
|
85
82
|
super(spec, specPath, identifier);
|
|
86
83
|
|
|
87
84
|
this.requiredPackages = {
|
|
88
|
-
api: {
|
|
89
|
-
|
|
85
|
+
'@readme/api-core': {
|
|
86
|
+
dependencyType: 'production',
|
|
87
|
+
reason: "The core magic of your codegen'd SDK and is what is used for making requests.",
|
|
90
88
|
url: 'https://npm.im/api',
|
|
89
|
+
version:
|
|
90
|
+
// When running unit tests we're installing `@readme/api-core` but because that package
|
|
91
|
+
// source lives in this repository NPM will throw a gnarly "Cannot set properties of null
|
|
92
|
+
// (setting 'dev')" workspace error message because we're creating a funky circular
|
|
93
|
+
// dependency.
|
|
94
|
+
process.env.NODE_ENV === 'test'
|
|
95
|
+
? `file:${path.relative(__dirname, path.dirname(require.resolve('@readme/api-core/package.json')))}`
|
|
96
|
+
: corePkg.version,
|
|
91
97
|
},
|
|
92
98
|
'json-schema-to-ts': {
|
|
99
|
+
dependencyType: 'production',
|
|
93
100
|
reason: 'Required for TypeScript type handling.',
|
|
94
101
|
url: 'https://npm.im/json-schema-to-ts',
|
|
102
|
+
version: '^2.9.2',
|
|
95
103
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
tsup: {
|
|
105
|
+
dependencyType: 'development',
|
|
106
|
+
reason: "Used for compiling your codegen'd SDK into code that can be used in JS environments.",
|
|
107
|
+
url: 'https://tsup.egoist.dev/',
|
|
108
|
+
version: '^7.2.0',
|
|
109
|
+
},
|
|
110
|
+
typescript: {
|
|
111
|
+
dependencyType: 'development',
|
|
112
|
+
reason: 'Required for `tsup`.',
|
|
113
|
+
version: '^5.2.2',
|
|
99
114
|
},
|
|
100
115
|
};
|
|
101
116
|
|
|
102
117
|
this.project = new Project({
|
|
103
|
-
manipulationSettings: {
|
|
104
|
-
indentationText: IndentationText.TwoSpaces,
|
|
105
|
-
quoteKind: QuoteKind.Single,
|
|
106
|
-
},
|
|
107
118
|
compilerOptions: {
|
|
108
|
-
// If we're exporting a TypeScript SDK then we don't need to pollute the codegen directory
|
|
109
|
-
// with unnecessary declaration `.d.ts` files.
|
|
110
|
-
declaration: options.outputJS,
|
|
111
119
|
outDir: 'dist',
|
|
112
120
|
resolveJsonModule: true,
|
|
113
|
-
target:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// function that does some determination to see if the module has a default export or not.
|
|
119
|
-
//
|
|
120
|
-
// Basically without this option CJS code will fail.
|
|
121
|
-
...(options.compilerTarget === 'cjs' ? { esModuleInterop: true } : {}),
|
|
121
|
+
target: ScriptTarget.ES2022,
|
|
122
|
+
},
|
|
123
|
+
manipulationSettings: {
|
|
124
|
+
indentationText: IndentationText.TwoSpaces,
|
|
125
|
+
quoteKind: QuoteKind.Single,
|
|
122
126
|
},
|
|
127
|
+
useInMemoryFileSystem: true,
|
|
123
128
|
});
|
|
124
129
|
|
|
125
|
-
this.compilerTarget = options.compilerTarget;
|
|
126
|
-
this.outputJS = options.outputJS;
|
|
127
|
-
|
|
128
130
|
this.types = new Map();
|
|
129
131
|
this.schemas = {};
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
// eslint-disable-next-line class-methods-use-this
|
|
135
|
+
async install(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
|
|
133
136
|
const installDir = storage.getIdentifierStorageDir();
|
|
134
137
|
|
|
135
|
-
const info = this.spec.getDefinition().info;
|
|
136
|
-
let pkgVersion = semver.coerce(info.version);
|
|
137
|
-
if (!pkgVersion) {
|
|
138
|
-
// If the version that's in `info.version` isn't compatible with semver NPM won't be able to
|
|
139
|
-
// handle it properly so we need to fallback to something it can.
|
|
140
|
-
pkgVersion = semver.coerce('0.0.0') as SemVer;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const pkg: PackageJson = {
|
|
144
|
-
name: `@api/${storage.identifier}`,
|
|
145
|
-
version: pkgVersion.version,
|
|
146
|
-
main: `./index.${this.outputJS ? 'js' : 'ts'}`,
|
|
147
|
-
types: './index.d.ts', // Types are always present regardless if you're getting compiled JS.
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
fs.writeFileSync(path.join(installDir, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
151
|
-
|
|
152
138
|
const npmInstall = ['install', '--save', opts.dryRun ? '--dry-run' : ''].filter(Boolean);
|
|
153
139
|
|
|
154
|
-
// This will install packages required for the SDK within its installed directory in `.apis/`.
|
|
155
|
-
await execa('npm', [...npmInstall, ...Object.keys(this.requiredPackages)].filter(Boolean), {
|
|
156
|
-
cwd: installDir,
|
|
157
|
-
}).then(res => {
|
|
158
|
-
if (opts.dryRun) {
|
|
159
|
-
(opts.logger ? opts.logger : logger)(res.command);
|
|
160
|
-
(opts.logger ? opts.logger : logger)(res.stdout);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
|
|
164
140
|
// This will install the installed SDK as a dependency within the current working directory,
|
|
165
141
|
// adding `@api/<sdk identifier>` as a dependency there so you can load it with
|
|
166
142
|
// `require('@api/<sdk identifier>)`.
|
|
167
|
-
|
|
143
|
+
await execa('npm', [...npmInstall, installDir].filter(Boolean))
|
|
144
|
+
.then(res => {
|
|
145
|
+
if (opts.dryRun) {
|
|
146
|
+
(opts.logger ? opts.logger : logger)(res.command);
|
|
147
|
+
(opts.logger ? opts.logger : logger)(res.stdout);
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
.catch(err => {
|
|
151
|
+
// If `npm install` throws this error it always happens **after** our dependencies have been
|
|
152
|
+
// installed and is an annoying quirk that sometimes occurs when installing a package within
|
|
153
|
+
// our workspace as we're creating a circular dependency on `@readme/api-core`.
|
|
154
|
+
if (
|
|
155
|
+
process.env.NODE_ENV === 'test' &&
|
|
156
|
+
err.message.includes("npm ERR! Cannot set properties of null (setting 'dev')")
|
|
157
|
+
) {
|
|
158
|
+
(opts.logger ? opts.logger : logger)("npm threw an error but we're ignoring it");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (opts.dryRun) {
|
|
163
|
+
(opts.logger ? opts.logger : logger)(err.message);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw err;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Compile the TS code we generated into JS for use in CJS and ESM environments.
|
|
173
|
+
*
|
|
174
|
+
*/
|
|
175
|
+
// eslint-disable-next-line class-methods-use-this
|
|
176
|
+
async compile(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
|
|
177
|
+
const installDir = storage.getIdentifierStorageDir();
|
|
178
|
+
|
|
179
|
+
await execa('npx', ['tsup'], {
|
|
180
|
+
cwd: installDir,
|
|
181
|
+
})
|
|
168
182
|
.then(res => {
|
|
169
183
|
if (opts.dryRun) {
|
|
170
184
|
(opts.logger ? opts.logger : logger)(res.command);
|
|
@@ -182,39 +196,19 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
182
196
|
}
|
|
183
197
|
|
|
184
198
|
/**
|
|
185
|
-
*
|
|
199
|
+
* Generate the current OpenAPI definition into a TypeScript library.
|
|
186
200
|
*
|
|
187
201
|
*/
|
|
188
|
-
async
|
|
189
|
-
const
|
|
202
|
+
async generate() {
|
|
203
|
+
const srcDirectory = this.project.createDirectory('src');
|
|
204
|
+
const sdkSource = this.createSDKSource(srcDirectory);
|
|
205
|
+
|
|
206
|
+
this.createPackageJSON();
|
|
207
|
+
this.createTSConfig();
|
|
190
208
|
|
|
191
209
|
if (Object.keys(this.schemas).length) {
|
|
192
|
-
this.createSchemasFile();
|
|
193
|
-
this.createTypesFile();
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Export all of our available types so they can be used in SDK implementations. Types are
|
|
197
|
-
* exported individually because TS has no way right now of allowing us to do
|
|
198
|
-
* `export type * from './types'` on a non-named entry.
|
|
199
|
-
*
|
|
200
|
-
* Types in the main entry point are only being exported for TS outputs as JS users won't be
|
|
201
|
-
* able to use them and it clashes with the default SDK export present.
|
|
202
|
-
*
|
|
203
|
-
* @see {@link https://github.com/microsoft/TypeScript/issues/37238}
|
|
204
|
-
* @see {@link https://github.com/readmeio/api/issues/588}
|
|
205
|
-
*/
|
|
206
|
-
if (!this.outputJS) {
|
|
207
|
-
const types = Array.from(this.types.keys());
|
|
208
|
-
types.sort();
|
|
209
|
-
|
|
210
|
-
sdkSource.addExportDeclarations([
|
|
211
|
-
{
|
|
212
|
-
isTypeOnly: true,
|
|
213
|
-
namedExports: types,
|
|
214
|
-
moduleSpecifier: './types',
|
|
215
|
-
},
|
|
216
|
-
]);
|
|
217
|
-
}
|
|
210
|
+
this.createSchemasFile(srcDirectory);
|
|
211
|
+
this.createTypesFile(srcDirectory);
|
|
218
212
|
} else {
|
|
219
213
|
// If we don't have any schemas then we shouldn't import a `types` file that doesn't exist.
|
|
220
214
|
sdkSource
|
|
@@ -232,46 +226,19 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
232
226
|
?.replaceWithText("import type { ConfigOptions, FetchResponse } from '@readme/api-core';");
|
|
233
227
|
}
|
|
234
228
|
|
|
235
|
-
if (this.outputJS) {
|
|
236
|
-
return this.project
|
|
237
|
-
.emitToMemory()
|
|
238
|
-
.getFiles()
|
|
239
|
-
.map(sourceFile => {
|
|
240
|
-
const file = path.basename(sourceFile.filePath);
|
|
241
|
-
if (file === 'schemas.js' || file === 'types.js') {
|
|
242
|
-
// If we're generating a JS SDK then we don't need to generate these two files as the
|
|
243
|
-
// user will have `.d.ts` files for them instead.
|
|
244
|
-
return {};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
let code = sourceFile.text;
|
|
248
|
-
if (file === 'index.js' && this.compilerTarget === 'cjs') {
|
|
249
|
-
/**
|
|
250
|
-
* There's an annoying quirk with `ts-morph` where if we're exporting a default export
|
|
251
|
-
* to a CJS environment, it'll export it as `exports.default`. Because we don't want
|
|
252
|
-
* folks in these environments to have to load their SDKs with
|
|
253
|
-
* `require('@api/sdk').default` we're overriding that here to change it to being the
|
|
254
|
-
* module exports.
|
|
255
|
-
*
|
|
256
|
-
* `ts-morph` unfortunately doesn't give us any options for programatically doing this
|
|
257
|
-
* so we need to resort to modifying the emitted JS code.
|
|
258
|
-
*/
|
|
259
|
-
code = code
|
|
260
|
-
.replace(/Object\.defineProperty\(exports, '__esModule', { value: true }\);\n/, '')
|
|
261
|
-
.replace('exports.default = createSDK;', 'module.exports = createSDK;');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
[file]: code,
|
|
266
|
-
};
|
|
267
|
-
})
|
|
268
|
-
.reduce((prev, next) => Object.assign(prev, next));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
229
|
return [
|
|
272
|
-
...this.project.getSourceFiles().map(sourceFile =>
|
|
273
|
-
|
|
274
|
-
|
|
230
|
+
...this.project.getSourceFiles().map(sourceFile => {
|
|
231
|
+
// `getFilePath` will always return a string that contains a preceeding directory separator
|
|
232
|
+
// however when we're creating these codegen'd files that may cause us to create that file
|
|
233
|
+
// in the root directory (because it's preceeded by a `/`). We don't want that to happen so
|
|
234
|
+
// we're slicing off that first character.
|
|
235
|
+
let filePath = sourceFile.getFilePath().toString();
|
|
236
|
+
filePath = filePath.substring(1);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
[filePath]: sourceFile.getFullText(),
|
|
240
|
+
};
|
|
241
|
+
}),
|
|
275
242
|
|
|
276
243
|
// Because we're returning the raw source files for TS generation we also need to separately
|
|
277
244
|
// emit out our declaration files so we can put those into a separate file in the installed
|
|
@@ -289,10 +256,10 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
289
256
|
* Create our main SDK source file.
|
|
290
257
|
*
|
|
291
258
|
*/
|
|
292
|
-
|
|
259
|
+
private createSDKSource(sourceDirectory: Directory) {
|
|
293
260
|
const { operations } = this.loadOperationsAndMethods();
|
|
294
261
|
|
|
295
|
-
const sourceFile =
|
|
262
|
+
const sourceFile = sourceDirectory.createSourceFile('index.ts', '');
|
|
296
263
|
|
|
297
264
|
sourceFile.addImportDeclarations([
|
|
298
265
|
// This import will be automatically removed later if the SDK ends up not having any types.
|
|
@@ -302,7 +269,6 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
302
269
|
defaultImport: 'type { ConfigOptions, FetchResponse, HTTPMethodRange }',
|
|
303
270
|
moduleSpecifier: '@readme/api-core',
|
|
304
271
|
},
|
|
305
|
-
{ defaultImport: 'Oas', moduleSpecifier: 'oas' },
|
|
306
272
|
{ defaultImport: 'APICore', moduleSpecifier: '@readme/api-core' },
|
|
307
273
|
{ defaultImport: 'definition', moduleSpecifier: this.specPath },
|
|
308
274
|
]);
|
|
@@ -310,16 +276,12 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
310
276
|
// @todo add TOS, License, info.* to a docblock at the top of the SDK.
|
|
311
277
|
this.sdk = sourceFile.addClass({
|
|
312
278
|
name: 'SDK',
|
|
313
|
-
properties: [
|
|
314
|
-
{ name: 'spec', type: 'Oas' },
|
|
315
|
-
{ name: 'core', type: 'APICore' },
|
|
316
|
-
],
|
|
279
|
+
properties: [{ name: 'core', type: 'APICore' }],
|
|
317
280
|
});
|
|
318
281
|
|
|
319
282
|
this.sdk.addConstructor({
|
|
320
283
|
statements: writer => {
|
|
321
|
-
writer.
|
|
322
|
-
writer.write('this.core = new APICore(this.spec, ').quote(this.userAgent).write(');');
|
|
284
|
+
writer.write('this.core = new APICore(definition, ').quote(this.userAgent).write(');');
|
|
323
285
|
return writer;
|
|
324
286
|
},
|
|
325
287
|
});
|
|
@@ -425,7 +387,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
425
387
|
{
|
|
426
388
|
name: 'createSDK',
|
|
427
389
|
initializer: writer => {
|
|
428
|
-
// `ts-morph` doesn't have any way to cleanly create an
|
|
390
|
+
// `ts-morph` doesn't have any way to cleanly create an IIFE.
|
|
429
391
|
writer.writeLine('(() => { return new SDK(); })()');
|
|
430
392
|
return writer;
|
|
431
393
|
},
|
|
@@ -434,49 +396,158 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
434
396
|
});
|
|
435
397
|
|
|
436
398
|
sourceFile.addExportAssignment({
|
|
437
|
-
// Because
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
isExportEquals:
|
|
399
|
+
// Because we're exporting `createSDK` as an IIFE constant we need to have it exported as
|
|
400
|
+
// `export default createSDK`. `addExportAssignment` by default wants it exported as
|
|
401
|
+
// `export = createSDK`, which will throw TS errors because we may also be exporting types in
|
|
402
|
+
// the `./types.ts` file.
|
|
403
|
+
isExportEquals: false,
|
|
442
404
|
expression: 'createSDK',
|
|
443
405
|
});
|
|
444
406
|
|
|
445
407
|
return sourceFile;
|
|
446
408
|
}
|
|
447
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Create the `tsconfig.json` file that will allow this SDK to be compiled for use.
|
|
412
|
+
*
|
|
413
|
+
*/
|
|
414
|
+
createTSConfig() {
|
|
415
|
+
const sourceFile = this.project.createSourceFile('tsconfig.json', '');
|
|
416
|
+
|
|
417
|
+
const config: TsConfigJson = {
|
|
418
|
+
compilerOptions: {
|
|
419
|
+
module: 'NodeNext',
|
|
420
|
+
resolveJsonModule: true,
|
|
421
|
+
},
|
|
422
|
+
include: ['./src/**/*'],
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
sourceFile.addStatements(JSON.stringify(config, null, 2));
|
|
426
|
+
|
|
427
|
+
return sourceFile;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Create the `package.json` file that will ultimately make this SDK available to use.
|
|
432
|
+
*
|
|
433
|
+
*/
|
|
434
|
+
createPackageJSON() {
|
|
435
|
+
const sourceFile = this.project.createSourceFile('package.json', '');
|
|
436
|
+
|
|
437
|
+
const hasTypes = !!Object.keys(this.schemas).length;
|
|
438
|
+
|
|
439
|
+
const info = this.spec.getDefinition().info;
|
|
440
|
+
let pkgVersion = semver.coerce(info.version);
|
|
441
|
+
if (!pkgVersion) {
|
|
442
|
+
// If the version that's in `info.version` isn't compatible with semver NPM won't be able to
|
|
443
|
+
// handle it properly so we need to fallback to something it can.
|
|
444
|
+
pkgVersion = semver.coerce('0.0.0') as SemVer;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const tsupOptions: Options = {
|
|
448
|
+
cjsInterop: true,
|
|
449
|
+
clean: true,
|
|
450
|
+
dts: true,
|
|
451
|
+
entry: [
|
|
452
|
+
'./src/index.ts',
|
|
453
|
+
// If this SDK has schemas and generated types then we should also export those too so
|
|
454
|
+
// they're available to use.
|
|
455
|
+
hasTypes ? './src/types.ts' : '',
|
|
456
|
+
].filter(Boolean),
|
|
457
|
+
format: ['esm', 'cjs'],
|
|
458
|
+
minify: false,
|
|
459
|
+
shims: true,
|
|
460
|
+
sourcemap: true,
|
|
461
|
+
splitting: true,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const dependencies = Object.entries(this.requiredPackages)
|
|
465
|
+
.map(([dep, { dependencyType, version }]) => (dependencyType === 'production' ? { [dep]: version } : {}))
|
|
466
|
+
.reduce((prev, next) => Object.assign(prev, next));
|
|
467
|
+
|
|
468
|
+
const devDependencies = Object.entries(this.requiredPackages)
|
|
469
|
+
.map(([dep, { dependencyType, version }]) => (dependencyType === 'development' ? { [dep]: version } : {}))
|
|
470
|
+
.reduce((prev, next) => Object.assign(prev, next));
|
|
471
|
+
|
|
472
|
+
const pkg: PackageJson = {
|
|
473
|
+
name: `@api/${this.identifier}`,
|
|
474
|
+
version: pkgVersion.version,
|
|
475
|
+
main: './dist/index.js',
|
|
476
|
+
types: './dist/index.d.ts',
|
|
477
|
+
module: './dist/index.mts',
|
|
478
|
+
exports: {
|
|
479
|
+
'.': {
|
|
480
|
+
import: './dist/index.mjs',
|
|
481
|
+
require: './dist/index.js',
|
|
482
|
+
},
|
|
483
|
+
...(hasTypes
|
|
484
|
+
? {
|
|
485
|
+
'./types': {
|
|
486
|
+
import: './dist/types.d.mts',
|
|
487
|
+
require: './dist/types.d.ts',
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
: {}),
|
|
491
|
+
},
|
|
492
|
+
files: ['dist'],
|
|
493
|
+
scripts: {
|
|
494
|
+
prepare: 'tsup',
|
|
495
|
+
},
|
|
496
|
+
dependencies,
|
|
497
|
+
devDependencies,
|
|
498
|
+
tsup: tsupOptions as JsonObject,
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
sourceFile.addStatements(JSON.stringify(pkg, null, 2));
|
|
502
|
+
|
|
503
|
+
return sourceFile;
|
|
504
|
+
}
|
|
505
|
+
|
|
448
506
|
/**
|
|
449
507
|
* Create our main schemas file. This is where all of the JSON Schema that our TypeScript typing
|
|
450
508
|
* infrastructure sources its data from. Without this there are no types.
|
|
451
509
|
*
|
|
452
510
|
*/
|
|
453
|
-
createSchemasFile() {
|
|
454
|
-
const sourceFile =
|
|
511
|
+
private createSchemasFile(sourceDirectory: Directory) {
|
|
512
|
+
const sourceFile = sourceDirectory.createSourceFile('schemas.ts', '');
|
|
513
|
+
const schemasDir = sourceDirectory.createDirectory('schemas');
|
|
455
514
|
|
|
456
515
|
const sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort());
|
|
457
516
|
|
|
458
517
|
Array.from(sortedSchemas).forEach(([schemaName, schema]) => {
|
|
459
|
-
|
|
518
|
+
const schemaFile = schemasDir.createSourceFile(`${schemaName}.ts`);
|
|
519
|
+
|
|
520
|
+
// Because we're chunking our schemas into a `schemas/` directory we need to add imports
|
|
521
|
+
// for these schemas into our main `schemas.ts` file.`
|
|
522
|
+
sourceFile.addImportDeclaration({
|
|
523
|
+
defaultImport: schemaName,
|
|
524
|
+
moduleSpecifier: `./schemas/${schemaName}`,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
let str = JSON.stringify(schema);
|
|
528
|
+
const referencedSchemas = str.match(REF_PLACEHOLDER_REGEX)?.map(s => s.replace(REF_PLACEHOLDER_REGEX, '$1'));
|
|
529
|
+
if (referencedSchemas) {
|
|
530
|
+
referencedSchemas.sort();
|
|
531
|
+
referencedSchemas.forEach(ref => {
|
|
532
|
+
// Because this schema is referenced from another file we need to create an `import`
|
|
533
|
+
// declaration for it.
|
|
534
|
+
schemaFile.addImportDeclaration({
|
|
535
|
+
defaultImport: ref,
|
|
536
|
+
moduleSpecifier: `./${ref}`,
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Load the schema into the schema file within the `schemas/` directory.
|
|
542
|
+
schemaFile.addVariableStatement({
|
|
460
543
|
declarationKind: VariableDeclarationKind.Const,
|
|
461
544
|
declarations: [
|
|
462
545
|
{
|
|
463
546
|
name: schemaName,
|
|
464
547
|
initializer: writer => {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
*
|
|
469
|
-
* Because the pointer name is a string we want to have it reference the schema
|
|
470
|
-
* constant we're adding into the codegen'd schema file. As there's no way, not even
|
|
471
|
-
* using `eval()` in this case, to convert a string to a constant we're prefixing
|
|
472
|
-
* them with this so we can later remove it and rewrite the value to a literal.
|
|
473
|
-
* eg. `'Pet'` becomes `Pet`.
|
|
474
|
-
*
|
|
475
|
-
* And because our TypeScript type name generator properly ignores `:`, this is safe
|
|
476
|
-
* to prepend to all generated type names.
|
|
477
|
-
*/
|
|
478
|
-
let str = JSON.stringify(schema);
|
|
479
|
-
str = str.replace(/"::convert::([a-zA-Z_$\\d]*)"/g, '$1');
|
|
548
|
+
// We can't have `::convert::<schemaName>` variables within these schema files so we
|
|
549
|
+
// need to clean them up.
|
|
550
|
+
str = str.replace(REF_PLACEHOLDER_REGEX, '$1');
|
|
480
551
|
|
|
481
552
|
writer.writeLine(`${str} as const`);
|
|
482
553
|
return writer;
|
|
@@ -484,8 +555,11 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
484
555
|
},
|
|
485
556
|
],
|
|
486
557
|
});
|
|
558
|
+
|
|
559
|
+
schemaFile.addStatements(`export default ${schemaName}`);
|
|
487
560
|
});
|
|
488
561
|
|
|
562
|
+
// Export all of our schemas from inside the main `schemas.ts` file.
|
|
489
563
|
sourceFile.addStatements(`export { ${Array.from(sortedSchemas.keys()).join(', ')} }`);
|
|
490
564
|
|
|
491
565
|
return sourceFile;
|
|
@@ -498,8 +572,8 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
498
572
|
*
|
|
499
573
|
* @see {@link https://npm.im/json-schema-to-ts}
|
|
500
574
|
*/
|
|
501
|
-
createTypesFile() {
|
|
502
|
-
const sourceFile =
|
|
575
|
+
private createTypesFile(sourceDirectory: Directory) {
|
|
576
|
+
const sourceFile = sourceDirectory.createSourceFile('types.ts', '');
|
|
503
577
|
|
|
504
578
|
sourceFile.addImportDeclarations([
|
|
505
579
|
{ defaultImport: 'type { FromSchema }', moduleSpecifier: 'json-schema-to-ts' },
|
|
@@ -513,25 +587,11 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
513
587
|
return sourceFile;
|
|
514
588
|
}
|
|
515
589
|
|
|
516
|
-
/**
|
|
517
|
-
* Add a new JSDoc `@tag` to an existing docblock.
|
|
518
|
-
*
|
|
519
|
-
*/
|
|
520
|
-
static addTagToDocblock(docblock: OptionalKind<JSDocStructure>, tag: OptionalKind<JSDocTagStructure>) {
|
|
521
|
-
const tags = docblock.tags ?? [];
|
|
522
|
-
tags.push(tag);
|
|
523
|
-
|
|
524
|
-
return {
|
|
525
|
-
...docblock,
|
|
526
|
-
tags,
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
|
|
530
590
|
/**
|
|
531
591
|
* Create operation accessors on the SDK.
|
|
532
592
|
*
|
|
533
593
|
*/
|
|
534
|
-
createOperationAccessor(
|
|
594
|
+
private createOperationAccessor(
|
|
535
595
|
operation: Operation,
|
|
536
596
|
operationId: string,
|
|
537
597
|
paramTypes?: OperationTypeHousing['types']['params'],
|
|
@@ -556,7 +616,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
556
616
|
};
|
|
557
617
|
|
|
558
618
|
if (summary && description) {
|
|
559
|
-
docblock = TSGenerator
|
|
619
|
+
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
560
620
|
tagName: 'summary',
|
|
561
621
|
text: docblockEscape(wordWrap(summary)),
|
|
562
622
|
});
|
|
@@ -609,7 +669,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
609
669
|
}
|
|
610
670
|
|
|
611
671
|
if (Number(statusPrefix) >= 4) {
|
|
612
|
-
docblock = TSGenerator
|
|
672
|
+
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
613
673
|
tagName: 'throws',
|
|
614
674
|
text: `FetchError<${status}, ${responseType}>${
|
|
615
675
|
responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
|
|
@@ -626,7 +686,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
626
686
|
// 400 and 500 status code families are thrown as exceptions so adding them as a possible
|
|
627
687
|
// return type isn't valid.
|
|
628
688
|
if (Number(status) >= 400) {
|
|
629
|
-
docblock = TSGenerator
|
|
689
|
+
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
630
690
|
tagName: 'throws',
|
|
631
691
|
text: `FetchError<${status}, ${responseType}>${
|
|
632
692
|
responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
|
|
@@ -736,7 +796,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
736
796
|
* along with every HTTP method that's in use.
|
|
737
797
|
*
|
|
738
798
|
*/
|
|
739
|
-
loadOperationsAndMethods() {
|
|
799
|
+
private loadOperationsAndMethods() {
|
|
740
800
|
const operations: Record</* operationId */ string, OperationTypeHousing> = {};
|
|
741
801
|
const methods = new Set<HttpMethods>();
|
|
742
802
|
|
|
@@ -777,7 +837,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
777
837
|
* usable TypeScript types.
|
|
778
838
|
*
|
|
779
839
|
*/
|
|
780
|
-
prepareParameterTypesForOperation(operation: Operation, operationId: string) {
|
|
840
|
+
private prepareParameterTypesForOperation(operation: Operation, operationId: string) {
|
|
781
841
|
const schemas = operation.getParametersAsJSONSchema({
|
|
782
842
|
includeDiscriminatorMappingRefs: false,
|
|
783
843
|
mergeIntoBodyAndMetadata: true,
|
|
@@ -789,7 +849,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
789
849
|
const typeName = generateTypeName(s['x-readme-ref-name']);
|
|
790
850
|
this.addSchemaToExport(s, typeName, typeName);
|
|
791
851
|
|
|
792
|
-
return
|
|
852
|
+
return `${REF_PLACEHOLDER}${typeName}` as SchemaObject;
|
|
793
853
|
}
|
|
794
854
|
|
|
795
855
|
return s;
|
|
@@ -808,10 +868,10 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
808
868
|
.map(([paramType, schema]: [string, string | SchemaObject]) => {
|
|
809
869
|
let typeName;
|
|
810
870
|
|
|
811
|
-
if (typeof schema === 'string' && schema.startsWith(
|
|
871
|
+
if (typeof schema === 'string' && schema.startsWith(REF_PLACEHOLDER)) {
|
|
812
872
|
// If this schema is a string and has our conversion prefix then we've already created
|
|
813
873
|
// a type for it.
|
|
814
|
-
typeName = schema.replace(
|
|
874
|
+
typeName = schema.replace(REF_PLACEHOLDER, '');
|
|
815
875
|
} else {
|
|
816
876
|
typeName = generateTypeName(operationId, paramType, 'param');
|
|
817
877
|
this.addSchemaToExport(schema as SchemaObject, typeName, `${generateTypeName(operationId)}.${paramType}`);
|
|
@@ -830,7 +890,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
830
890
|
* Compile the response schemas for an API operation into usable TypeScript types.
|
|
831
891
|
*
|
|
832
892
|
*/
|
|
833
|
-
prepareResponseTypesForOperation(operation: Operation, operationId: string) {
|
|
893
|
+
private prepareResponseTypesForOperation(operation: Operation, operationId: string) {
|
|
834
894
|
const responseStatusCodes = operation.getResponseStatusCodes();
|
|
835
895
|
if (!responseStatusCodes.length) {
|
|
836
896
|
return undefined;
|
|
@@ -847,7 +907,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
847
907
|
const typeName = generateTypeName(s['x-readme-ref-name']);
|
|
848
908
|
this.addSchemaToExport(s, typeName, `${typeName}`);
|
|
849
909
|
|
|
850
|
-
return
|
|
910
|
+
return `${REF_PLACEHOLDER}${typeName}` as SchemaObject;
|
|
851
911
|
}
|
|
852
912
|
|
|
853
913
|
return s;
|
|
@@ -868,10 +928,10 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
868
928
|
.map(([status, { description, schema }]) => {
|
|
869
929
|
let typeName;
|
|
870
930
|
|
|
871
|
-
if (typeof schema === 'string' && schema.startsWith(
|
|
931
|
+
if (typeof schema === 'string' && schema.startsWith(REF_PLACEHOLDER)) {
|
|
872
932
|
// If this schema is a string and has our conversion prefix then we've already created
|
|
873
933
|
// a type for it.
|
|
874
|
-
typeName = schema.replace(
|
|
934
|
+
typeName = schema.replace(REF_PLACEHOLDER, '');
|
|
875
935
|
} else {
|
|
876
936
|
typeName = generateTypeName(operationId, 'response', status);
|
|
877
937
|
|
|
@@ -899,7 +959,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
899
959
|
* Add a given schema into our schema dataset that we'll be be exporting as types.
|
|
900
960
|
*
|
|
901
961
|
*/
|
|
902
|
-
addSchemaToExport(schema: SchemaObject, typeName: string, pointer: string) {
|
|
962
|
+
private addSchemaToExport(schema: SchemaObject, typeName: string, pointer: string) {
|
|
903
963
|
if (this.types.has(typeName)) {
|
|
904
964
|
return;
|
|
905
965
|
}
|
|
@@ -907,4 +967,18 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
907
967
|
setWith(this.schemas, pointer, schema, Object);
|
|
908
968
|
this.types.set(typeName, `FromSchema<typeof schemas.${pointer}>`);
|
|
909
969
|
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Add a new JSDoc `@tag` to an existing docblock.
|
|
973
|
+
*
|
|
974
|
+
*/
|
|
975
|
+
static #addTagToDocblock(docblock: OptionalKind<JSDocStructure>, tag: OptionalKind<JSDocTagStructure>) {
|
|
976
|
+
const tags = docblock.tags ?? [];
|
|
977
|
+
tags.push(tag);
|
|
978
|
+
|
|
979
|
+
return {
|
|
980
|
+
...docblock,
|
|
981
|
+
tags,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
910
984
|
}
|