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