api 7.0.0-alpha.2 → 7.0.0-alpha.5
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} +7 -4
- 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} +260 -206
- 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 +52 -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 +12 -10
- package/src/bin.ts +2 -2
- package/src/codegen/{language.ts → codegenerator.ts} +15 -6
- package/src/codegen/factory.ts +23 -0
- package/src/codegen/languages/{typescript.ts → typescript/index.ts} +269 -203
- package/src/commands/index.ts +1 -1
- package/src/commands/install.ts +21 -35
- package/src/packageInfo.ts +1 -1
- package/src/storage.ts +14 -5
- package/tsconfig.json +3 -10
- 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,19 +1,20 @@
|
|
|
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
|
|
|
19
20
|
import execa from 'execa';
|
|
@@ -21,15 +22,10 @@ import setWith from 'lodash.setwith';
|
|
|
21
22
|
import semver from 'semver';
|
|
22
23
|
import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationKind } from 'ts-morph';
|
|
23
24
|
|
|
24
|
-
import logger from '
|
|
25
|
-
import
|
|
25
|
+
import logger from '../../../logger.js';
|
|
26
|
+
import CodeGenerator from '../../codegenerator.js';
|
|
26
27
|
|
|
27
|
-
import { docblockEscape, generateTypeName, wordWrap } from './
|
|
28
|
-
|
|
29
|
-
export interface TSGeneratorOptions {
|
|
30
|
-
compilerTarget?: 'cjs' | 'esm';
|
|
31
|
-
outputJS?: boolean;
|
|
32
|
-
}
|
|
28
|
+
import { docblockEscape, generateTypeName, wordWrap } from './util.js';
|
|
33
29
|
|
|
34
30
|
interface OperationTypeHousing {
|
|
35
31
|
operation: Operation;
|
|
@@ -45,13 +41,24 @@ interface OperationTypeHousing {
|
|
|
45
41
|
};
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
/**
|
|
45
|
+
* This is the conversion prefix that we add to all `$ref` pointers we find in generated JSON
|
|
46
|
+
* Schema.
|
|
47
|
+
*
|
|
48
|
+
* Because the pointer name is a string we want to have it reference the schema constant we're
|
|
49
|
+
* adding into the codegen'd schema file. As there's no way, not even using `eval()` in this case,
|
|
50
|
+
* to convert a string to a constant we're prefixing them with this so we can later remove it and
|
|
51
|
+
* rewrite the value to a literal. eg. `'Pet'` becomes `Pet`.
|
|
52
|
+
*
|
|
53
|
+
* And because our TypeScript type name generator properly ignores `:`, this is safe to prepend to
|
|
54
|
+
* all generated type names.
|
|
55
|
+
*/
|
|
56
|
+
const REF_PLACEHOLDER = '::convert::';
|
|
57
|
+
const REF_PLACEHOLDER_REGEX = /"::convert::([a-zA-Z_$\\d]*)"/g;
|
|
58
|
+
|
|
59
|
+
export default class TSGenerator extends CodeGenerator {
|
|
49
60
|
project: Project;
|
|
50
61
|
|
|
51
|
-
outputJS: boolean;
|
|
52
|
-
|
|
53
|
-
compilerTarget: 'cjs' | 'esm';
|
|
54
|
-
|
|
55
62
|
types: Map<string, string>;
|
|
56
63
|
|
|
57
64
|
sdk!: ClassDeclaration;
|
|
@@ -70,101 +77,61 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
70
77
|
|
|
71
78
|
usesHTTPMethodRangeInterface = false;
|
|
72
79
|
|
|
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
|
-
|
|
80
|
+
constructor(spec: Oas, specPath: string, identifier: string) {
|
|
85
81
|
super(spec, specPath, identifier);
|
|
86
82
|
|
|
87
83
|
this.requiredPackages = {
|
|
88
|
-
api: {
|
|
89
|
-
reason: "
|
|
84
|
+
'@readme/api-core': {
|
|
85
|
+
reason: "The core magic of your codegen'd SDK and is what is used for making requests.",
|
|
90
86
|
url: 'https://npm.im/api',
|
|
87
|
+
version:
|
|
88
|
+
// When running unit tests we're installing `@readme/api-core` but because that package
|
|
89
|
+
// source lives in this repository NPM will throw a gnarly "Cannot set properties of null
|
|
90
|
+
// (setting 'dev')" workspace error message because we're creating a funky circular
|
|
91
|
+
// dependency.
|
|
92
|
+
process.env.NODE_ENV === 'test'
|
|
93
|
+
? `file:${path.relative(__dirname, path.dirname(require.resolve('@readme/api-core/package.json')))}`
|
|
94
|
+
: '^7.0.0',
|
|
91
95
|
},
|
|
92
96
|
'json-schema-to-ts': {
|
|
93
97
|
reason: 'Required for TypeScript type handling.',
|
|
94
98
|
url: 'https://npm.im/json-schema-to-ts',
|
|
99
|
+
version: '^2.9.2',
|
|
95
100
|
},
|
|
96
101
|
oas: {
|
|
97
102
|
reason: 'Used within `@readme/api-core` and is also loaded for TypeScript types.',
|
|
98
103
|
url: 'https://npm.im/oas',
|
|
104
|
+
version: '^23.0.0',
|
|
99
105
|
},
|
|
100
106
|
};
|
|
101
107
|
|
|
102
108
|
this.project = new Project({
|
|
103
|
-
manipulationSettings: {
|
|
104
|
-
indentationText: IndentationText.TwoSpaces,
|
|
105
|
-
quoteKind: QuoteKind.Single,
|
|
106
|
-
},
|
|
107
109
|
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
110
|
outDir: 'dist',
|
|
112
111
|
resolveJsonModule: true,
|
|
113
|
-
target:
|
|
114
|
-
|
|
115
|
-
// If we're compiling to a CJS target then we need to include this compiler option
|
|
116
|
-
// otherwise TS will attempt to load our `openapi.json` import with a `.default` property
|
|
117
|
-
// which doesn't exist. `esModuleInterop` wraps imports in a small `__importDefault`
|
|
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 } : {}),
|
|
112
|
+
target: ScriptTarget.ES2022,
|
|
122
113
|
},
|
|
114
|
+
manipulationSettings: {
|
|
115
|
+
indentationText: IndentationText.TwoSpaces,
|
|
116
|
+
quoteKind: QuoteKind.Single,
|
|
117
|
+
},
|
|
118
|
+
useInMemoryFileSystem: true,
|
|
123
119
|
});
|
|
124
120
|
|
|
125
|
-
this.compilerTarget = options.compilerTarget;
|
|
126
|
-
this.outputJS = options.outputJS;
|
|
127
|
-
|
|
128
121
|
this.types = new Map();
|
|
129
122
|
this.schemas = {};
|
|
130
123
|
}
|
|
131
124
|
|
|
132
|
-
|
|
125
|
+
// eslint-disable-next-line class-methods-use-this
|
|
126
|
+
async install(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
|
|
133
127
|
const installDir = storage.getIdentifierStorageDir();
|
|
134
128
|
|
|
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
129
|
const npmInstall = ['install', '--save', opts.dryRun ? '--dry-run' : ''].filter(Boolean);
|
|
153
130
|
|
|
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
131
|
// This will install the installed SDK as a dependency within the current working directory,
|
|
165
132
|
// adding `@api/<sdk identifier>` as a dependency there so you can load it with
|
|
166
133
|
// `require('@api/<sdk identifier>)`.
|
|
167
|
-
|
|
134
|
+
await execa('npm', [...npmInstall, installDir].filter(Boolean))
|
|
168
135
|
.then(res => {
|
|
169
136
|
if (opts.dryRun) {
|
|
170
137
|
(opts.logger ? opts.logger : logger)(res.command);
|
|
@@ -172,6 +139,17 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
172
139
|
}
|
|
173
140
|
})
|
|
174
141
|
.catch(err => {
|
|
142
|
+
// If `npm install` throws this error it always happens **after** our dependencies have been
|
|
143
|
+
// installed and is an annoying quirk that sometimes occurs when installing a package within
|
|
144
|
+
// our workspace as we're creating a circular dependency on `@readme/api-core`.
|
|
145
|
+
if (
|
|
146
|
+
process.env.NODE_ENV === 'test' &&
|
|
147
|
+
err.message.includes("npm ERR! Cannot set properties of null (setting 'dev')")
|
|
148
|
+
) {
|
|
149
|
+
(opts.logger ? opts.logger : logger)("npm threw an error but we're ignoring it");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
175
153
|
if (opts.dryRun) {
|
|
176
154
|
(opts.logger ? opts.logger : logger)(err.message);
|
|
177
155
|
return;
|
|
@@ -182,39 +160,46 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
182
160
|
}
|
|
183
161
|
|
|
184
162
|
/**
|
|
185
|
-
* Compile the
|
|
163
|
+
* Compile the TS code we generated into JS for use in CJS and ESM environments.
|
|
186
164
|
*
|
|
187
165
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
166
|
+
// eslint-disable-next-line class-methods-use-this
|
|
167
|
+
async compile(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
|
|
168
|
+
const installDir = storage.getIdentifierStorageDir();
|
|
169
|
+
|
|
170
|
+
await execa('npx', ['tsup'], {
|
|
171
|
+
cwd: installDir,
|
|
172
|
+
})
|
|
173
|
+
.then(res => {
|
|
174
|
+
if (opts.dryRun) {
|
|
175
|
+
(opts.logger ? opts.logger : logger)(res.command);
|
|
176
|
+
(opts.logger ? opts.logger : logger)(res.stdout);
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
.catch(err => {
|
|
180
|
+
if (opts.dryRun) {
|
|
181
|
+
(opts.logger ? opts.logger : logger)(err.message);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
throw err;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Generate the current OpenAPI definition into a TypeScript library.
|
|
191
|
+
*
|
|
192
|
+
*/
|
|
193
|
+
async generate() {
|
|
194
|
+
const srcDirectory = this.project.createDirectory('src');
|
|
195
|
+
const sdkSource = this.createSDKSource(srcDirectory);
|
|
196
|
+
|
|
197
|
+
this.createPackageJSON();
|
|
198
|
+
this.createTSConfig();
|
|
190
199
|
|
|
191
200
|
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
|
-
}
|
|
201
|
+
this.createSchemasFile(srcDirectory);
|
|
202
|
+
this.createTypesFile(srcDirectory);
|
|
218
203
|
} else {
|
|
219
204
|
// If we don't have any schemas then we shouldn't import a `types` file that doesn't exist.
|
|
220
205
|
sdkSource
|
|
@@ -232,46 +217,19 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
232
217
|
?.replaceWithText("import type { ConfigOptions, FetchResponse } from '@readme/api-core';");
|
|
233
218
|
}
|
|
234
219
|
|
|
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
220
|
return [
|
|
272
|
-
...this.project.getSourceFiles().map(sourceFile =>
|
|
273
|
-
|
|
274
|
-
|
|
221
|
+
...this.project.getSourceFiles().map(sourceFile => {
|
|
222
|
+
// `getFilePath` will always return a string that contains a preceeding directory separator
|
|
223
|
+
// however when we're creating these codegen'd files that may cause us to create that file
|
|
224
|
+
// in the root directory (because it's preceeded by a `/`). We don't want that to happen so
|
|
225
|
+
// we're slicing off that first character.
|
|
226
|
+
let filePath = sourceFile.getFilePath().toString();
|
|
227
|
+
filePath = filePath.substring(1);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
[filePath]: sourceFile.getFullText(),
|
|
231
|
+
};
|
|
232
|
+
}),
|
|
275
233
|
|
|
276
234
|
// Because we're returning the raw source files for TS generation we also need to separately
|
|
277
235
|
// emit out our declaration files so we can put those into a separate file in the installed
|
|
@@ -289,10 +247,10 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
289
247
|
* Create our main SDK source file.
|
|
290
248
|
*
|
|
291
249
|
*/
|
|
292
|
-
|
|
250
|
+
private createSDKSource(sourceDirectory: Directory) {
|
|
293
251
|
const { operations } = this.loadOperationsAndMethods();
|
|
294
252
|
|
|
295
|
-
const sourceFile =
|
|
253
|
+
const sourceFile = sourceDirectory.createSourceFile('index.ts', '');
|
|
296
254
|
|
|
297
255
|
sourceFile.addImportDeclarations([
|
|
298
256
|
// This import will be automatically removed later if the SDK ends up not having any types.
|
|
@@ -425,7 +383,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
425
383
|
{
|
|
426
384
|
name: 'createSDK',
|
|
427
385
|
initializer: writer => {
|
|
428
|
-
// `ts-morph` doesn't have any way to cleanly create an
|
|
386
|
+
// `ts-morph` doesn't have any way to cleanly create an IIFE.
|
|
429
387
|
writer.writeLine('(() => { return new SDK(); })()');
|
|
430
388
|
return writer;
|
|
431
389
|
},
|
|
@@ -434,49 +392,154 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
434
392
|
});
|
|
435
393
|
|
|
436
394
|
sourceFile.addExportAssignment({
|
|
437
|
-
// Because
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
isExportEquals:
|
|
395
|
+
// Because we're exporting `createSDK` as an IIFE constant we need to have it exported as
|
|
396
|
+
// `export default createSDK`. `addExportAssignment` by default wants it exported as
|
|
397
|
+
// `export = createSDK`, which will throw TS errors because we may also be exporting types in
|
|
398
|
+
// the `./types.ts` file.
|
|
399
|
+
isExportEquals: false,
|
|
442
400
|
expression: 'createSDK',
|
|
443
401
|
});
|
|
444
402
|
|
|
445
403
|
return sourceFile;
|
|
446
404
|
}
|
|
447
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Create the `tsconfig.json` file that will allow this SDK to be compiled for use.
|
|
408
|
+
*
|
|
409
|
+
*/
|
|
410
|
+
createTSConfig() {
|
|
411
|
+
const sourceFile = this.project.createSourceFile('tsconfig.json', '');
|
|
412
|
+
|
|
413
|
+
const config: TsConfigJson = {
|
|
414
|
+
compilerOptions: {
|
|
415
|
+
checkJs: true,
|
|
416
|
+
esModuleInterop: true,
|
|
417
|
+
module: 'NodeNext',
|
|
418
|
+
resolveJsonModule: true,
|
|
419
|
+
},
|
|
420
|
+
include: ['./src/**/*'],
|
|
421
|
+
exclude: ['dist'],
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
sourceFile.addStatements(JSON.stringify(config, null, 2));
|
|
425
|
+
|
|
426
|
+
return sourceFile;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Create the `package.json` file that will ultimately make this SDK available to use.
|
|
431
|
+
*
|
|
432
|
+
*/
|
|
433
|
+
createPackageJSON() {
|
|
434
|
+
const sourceFile = this.project.createSourceFile('package.json', '');
|
|
435
|
+
|
|
436
|
+
const hasTypes = !!Object.keys(this.schemas).length;
|
|
437
|
+
|
|
438
|
+
const info = this.spec.getDefinition().info;
|
|
439
|
+
let pkgVersion = semver.coerce(info.version);
|
|
440
|
+
if (!pkgVersion) {
|
|
441
|
+
// If the version that's in `info.version` isn't compatible with semver NPM won't be able to
|
|
442
|
+
// handle it properly so we need to fallback to something it can.
|
|
443
|
+
pkgVersion = semver.coerce('0.0.0') as SemVer;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const tsupOptions: Options = {
|
|
447
|
+
cjsInterop: true,
|
|
448
|
+
clean: true,
|
|
449
|
+
dts: true,
|
|
450
|
+
entry: [
|
|
451
|
+
'./src/index.ts',
|
|
452
|
+
// If this SDK has schemas and generated types then we should also export those too so
|
|
453
|
+
// they're available to use.
|
|
454
|
+
hasTypes ? './src/types.ts' : '',
|
|
455
|
+
].filter(Boolean),
|
|
456
|
+
// TODO: figure this out
|
|
457
|
+
// external: ['@readme/api-core'],
|
|
458
|
+
format: ['esm', 'cjs'],
|
|
459
|
+
minify: false,
|
|
460
|
+
shims: true,
|
|
461
|
+
sourcemap: true,
|
|
462
|
+
splitting: true,
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const dependencies = Object.entries(this.requiredPackages)
|
|
466
|
+
.map(([dep, { version }]) => ({ [dep]: version }))
|
|
467
|
+
.reduce((prev, next) => Object.assign(prev, next));
|
|
468
|
+
|
|
469
|
+
const pkg: PackageJson = {
|
|
470
|
+
name: `@api/${this.identifier}`,
|
|
471
|
+
version: pkgVersion.version,
|
|
472
|
+
main: './dist/index.js',
|
|
473
|
+
types: './dist/index.d.ts',
|
|
474
|
+
module: './dist/index.mts',
|
|
475
|
+
exports: {
|
|
476
|
+
'.': {
|
|
477
|
+
import: './dist/index.mjs',
|
|
478
|
+
require: './dist/index.js',
|
|
479
|
+
},
|
|
480
|
+
...(hasTypes
|
|
481
|
+
? {
|
|
482
|
+
'./types': {
|
|
483
|
+
import: './dist/types.d.mts',
|
|
484
|
+
require: './dist/types.d.ts',
|
|
485
|
+
},
|
|
486
|
+
}
|
|
487
|
+
: {}),
|
|
488
|
+
},
|
|
489
|
+
dependencies,
|
|
490
|
+
tsup: tsupOptions as JsonObject,
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
sourceFile.addStatements(JSON.stringify(pkg, null, 2));
|
|
494
|
+
|
|
495
|
+
return sourceFile;
|
|
496
|
+
}
|
|
497
|
+
|
|
448
498
|
/**
|
|
449
499
|
* Create our main schemas file. This is where all of the JSON Schema that our TypeScript typing
|
|
450
500
|
* infrastructure sources its data from. Without this there are no types.
|
|
451
501
|
*
|
|
452
502
|
*/
|
|
453
|
-
createSchemasFile() {
|
|
454
|
-
const sourceFile =
|
|
503
|
+
private createSchemasFile(sourceDirectory: Directory) {
|
|
504
|
+
const sourceFile = sourceDirectory.createSourceFile('schemas.ts', '');
|
|
505
|
+
const schemasDir = sourceDirectory.createDirectory('schemas');
|
|
455
506
|
|
|
456
507
|
const sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort());
|
|
457
508
|
|
|
458
509
|
Array.from(sortedSchemas).forEach(([schemaName, schema]) => {
|
|
459
|
-
|
|
510
|
+
const schemaFile = schemasDir.createSourceFile(`${schemaName}.ts`);
|
|
511
|
+
|
|
512
|
+
// Because we're chunking our schemas into a `schemas/` directory we need to add imports
|
|
513
|
+
// for these schemas into our main `schemas.ts` file.`
|
|
514
|
+
sourceFile.addImportDeclaration({
|
|
515
|
+
defaultImport: schemaName,
|
|
516
|
+
moduleSpecifier: `./schemas/${schemaName}`,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
let str = JSON.stringify(schema);
|
|
520
|
+
const referencedSchemas = str.match(REF_PLACEHOLDER_REGEX)?.map(s => s.replace(REF_PLACEHOLDER_REGEX, '$1'));
|
|
521
|
+
if (referencedSchemas) {
|
|
522
|
+
referencedSchemas.sort();
|
|
523
|
+
referencedSchemas.forEach(ref => {
|
|
524
|
+
// Because this schema is referenced from another file we need to create an `import`
|
|
525
|
+
// declaration for it.
|
|
526
|
+
schemaFile.addImportDeclaration({
|
|
527
|
+
defaultImport: ref,
|
|
528
|
+
moduleSpecifier: `./${ref}`,
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Load the schema into the schema file within the `schemas/` directory.
|
|
534
|
+
schemaFile.addVariableStatement({
|
|
460
535
|
declarationKind: VariableDeclarationKind.Const,
|
|
461
536
|
declarations: [
|
|
462
537
|
{
|
|
463
538
|
name: schemaName,
|
|
464
539
|
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');
|
|
540
|
+
// We can't have `::convert::<schemaName>` variables within these schema files so we
|
|
541
|
+
// need to clean them up.
|
|
542
|
+
str = str.replace(REF_PLACEHOLDER_REGEX, '$1');
|
|
480
543
|
|
|
481
544
|
writer.writeLine(`${str} as const`);
|
|
482
545
|
return writer;
|
|
@@ -484,8 +547,11 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
484
547
|
},
|
|
485
548
|
],
|
|
486
549
|
});
|
|
550
|
+
|
|
551
|
+
schemaFile.addStatements(`export default ${schemaName}`);
|
|
487
552
|
});
|
|
488
553
|
|
|
554
|
+
// Export all of our schemas from inside the main `schemas.ts` file.
|
|
489
555
|
sourceFile.addStatements(`export { ${Array.from(sortedSchemas.keys()).join(', ')} }`);
|
|
490
556
|
|
|
491
557
|
return sourceFile;
|
|
@@ -498,8 +564,8 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
498
564
|
*
|
|
499
565
|
* @see {@link https://npm.im/json-schema-to-ts}
|
|
500
566
|
*/
|
|
501
|
-
createTypesFile() {
|
|
502
|
-
const sourceFile =
|
|
567
|
+
private createTypesFile(sourceDirectory: Directory) {
|
|
568
|
+
const sourceFile = sourceDirectory.createSourceFile('types.ts', '');
|
|
503
569
|
|
|
504
570
|
sourceFile.addImportDeclarations([
|
|
505
571
|
{ defaultImport: 'type { FromSchema }', moduleSpecifier: 'json-schema-to-ts' },
|
|
@@ -513,25 +579,11 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
513
579
|
return sourceFile;
|
|
514
580
|
}
|
|
515
581
|
|
|
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
582
|
/**
|
|
531
583
|
* Create operation accessors on the SDK.
|
|
532
584
|
*
|
|
533
585
|
*/
|
|
534
|
-
createOperationAccessor(
|
|
586
|
+
private createOperationAccessor(
|
|
535
587
|
operation: Operation,
|
|
536
588
|
operationId: string,
|
|
537
589
|
paramTypes?: OperationTypeHousing['types']['params'],
|
|
@@ -556,7 +608,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
556
608
|
};
|
|
557
609
|
|
|
558
610
|
if (summary && description) {
|
|
559
|
-
docblock = TSGenerator
|
|
611
|
+
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
560
612
|
tagName: 'summary',
|
|
561
613
|
text: docblockEscape(wordWrap(summary)),
|
|
562
614
|
});
|
|
@@ -609,7 +661,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
609
661
|
}
|
|
610
662
|
|
|
611
663
|
if (Number(statusPrefix) >= 4) {
|
|
612
|
-
docblock = TSGenerator
|
|
664
|
+
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
613
665
|
tagName: 'throws',
|
|
614
666
|
text: `FetchError<${status}, ${responseType}>${
|
|
615
667
|
responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
|
|
@@ -626,7 +678,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
626
678
|
// 400 and 500 status code families are thrown as exceptions so adding them as a possible
|
|
627
679
|
// return type isn't valid.
|
|
628
680
|
if (Number(status) >= 400) {
|
|
629
|
-
docblock = TSGenerator
|
|
681
|
+
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
630
682
|
tagName: 'throws',
|
|
631
683
|
text: `FetchError<${status}, ${responseType}>${
|
|
632
684
|
responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
|
|
@@ -736,7 +788,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
736
788
|
* along with every HTTP method that's in use.
|
|
737
789
|
*
|
|
738
790
|
*/
|
|
739
|
-
loadOperationsAndMethods() {
|
|
791
|
+
private loadOperationsAndMethods() {
|
|
740
792
|
const operations: Record</* operationId */ string, OperationTypeHousing> = {};
|
|
741
793
|
const methods = new Set<HttpMethods>();
|
|
742
794
|
|
|
@@ -777,7 +829,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
777
829
|
* usable TypeScript types.
|
|
778
830
|
*
|
|
779
831
|
*/
|
|
780
|
-
prepareParameterTypesForOperation(operation: Operation, operationId: string) {
|
|
832
|
+
private prepareParameterTypesForOperation(operation: Operation, operationId: string) {
|
|
781
833
|
const schemas = operation.getParametersAsJSONSchema({
|
|
782
834
|
includeDiscriminatorMappingRefs: false,
|
|
783
835
|
mergeIntoBodyAndMetadata: true,
|
|
@@ -789,7 +841,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
789
841
|
const typeName = generateTypeName(s['x-readme-ref-name']);
|
|
790
842
|
this.addSchemaToExport(s, typeName, typeName);
|
|
791
843
|
|
|
792
|
-
return
|
|
844
|
+
return `${REF_PLACEHOLDER}${typeName}` as SchemaObject;
|
|
793
845
|
}
|
|
794
846
|
|
|
795
847
|
return s;
|
|
@@ -808,10 +860,10 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
808
860
|
.map(([paramType, schema]: [string, string | SchemaObject]) => {
|
|
809
861
|
let typeName;
|
|
810
862
|
|
|
811
|
-
if (typeof schema === 'string' && schema.startsWith(
|
|
863
|
+
if (typeof schema === 'string' && schema.startsWith(REF_PLACEHOLDER)) {
|
|
812
864
|
// If this schema is a string and has our conversion prefix then we've already created
|
|
813
865
|
// a type for it.
|
|
814
|
-
typeName = schema.replace(
|
|
866
|
+
typeName = schema.replace(REF_PLACEHOLDER, '');
|
|
815
867
|
} else {
|
|
816
868
|
typeName = generateTypeName(operationId, paramType, 'param');
|
|
817
869
|
this.addSchemaToExport(schema as SchemaObject, typeName, `${generateTypeName(operationId)}.${paramType}`);
|
|
@@ -830,7 +882,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
830
882
|
* Compile the response schemas for an API operation into usable TypeScript types.
|
|
831
883
|
*
|
|
832
884
|
*/
|
|
833
|
-
prepareResponseTypesForOperation(operation: Operation, operationId: string) {
|
|
885
|
+
private prepareResponseTypesForOperation(operation: Operation, operationId: string) {
|
|
834
886
|
const responseStatusCodes = operation.getResponseStatusCodes();
|
|
835
887
|
if (!responseStatusCodes.length) {
|
|
836
888
|
return undefined;
|
|
@@ -847,7 +899,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
847
899
|
const typeName = generateTypeName(s['x-readme-ref-name']);
|
|
848
900
|
this.addSchemaToExport(s, typeName, `${typeName}`);
|
|
849
901
|
|
|
850
|
-
return
|
|
902
|
+
return `${REF_PLACEHOLDER}${typeName}` as SchemaObject;
|
|
851
903
|
}
|
|
852
904
|
|
|
853
905
|
return s;
|
|
@@ -868,10 +920,10 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
868
920
|
.map(([status, { description, schema }]) => {
|
|
869
921
|
let typeName;
|
|
870
922
|
|
|
871
|
-
if (typeof schema === 'string' && schema.startsWith(
|
|
923
|
+
if (typeof schema === 'string' && schema.startsWith(REF_PLACEHOLDER)) {
|
|
872
924
|
// If this schema is a string and has our conversion prefix then we've already created
|
|
873
925
|
// a type for it.
|
|
874
|
-
typeName = schema.replace(
|
|
926
|
+
typeName = schema.replace(REF_PLACEHOLDER, '');
|
|
875
927
|
} else {
|
|
876
928
|
typeName = generateTypeName(operationId, 'response', status);
|
|
877
929
|
|
|
@@ -899,7 +951,7 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
899
951
|
* Add a given schema into our schema dataset that we'll be be exporting as types.
|
|
900
952
|
*
|
|
901
953
|
*/
|
|
902
|
-
addSchemaToExport(schema: SchemaObject, typeName: string, pointer: string) {
|
|
954
|
+
private addSchemaToExport(schema: SchemaObject, typeName: string, pointer: string) {
|
|
903
955
|
if (this.types.has(typeName)) {
|
|
904
956
|
return;
|
|
905
957
|
}
|
|
@@ -907,4 +959,18 @@ sdk.server('https://eu.api.example.com/v14');`),
|
|
|
907
959
|
setWith(this.schemas, pointer, schema, Object);
|
|
908
960
|
this.types.set(typeName, `FromSchema<typeof schemas.${pointer}>`);
|
|
909
961
|
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Add a new JSDoc `@tag` to an existing docblock.
|
|
965
|
+
*
|
|
966
|
+
*/
|
|
967
|
+
static #addTagToDocblock(docblock: OptionalKind<JSDocStructure>, tag: OptionalKind<JSDocTagStructure>) {
|
|
968
|
+
const tags = docblock.tags ?? [];
|
|
969
|
+
tags.push(tag);
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
...docblock,
|
|
973
|
+
tags,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
910
976
|
}
|