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