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.
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} +7 -4
  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} +260 -206
  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 +52 -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 +12 -10
  51. package/src/bin.ts +2 -2
  52. package/src/codegen/{language.ts → codegenerator.ts} +15 -6
  53. package/src/codegen/factory.ts +23 -0
  54. package/src/codegen/languages/{typescript.ts → typescript/index.ts} +269 -203
  55. package/src/commands/index.ts +1 -1
  56. package/src/commands/install.ts +21 -35
  57. package/src/packageInfo.ts +1 -1
  58. package/src/storage.ts +14 -5
  59. package/tsconfig.json +3 -10
  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,19 +1,20 @@
1
- import type Storage from '../../storage';
2
- import type { InstallerOptions } from '../language';
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 { PackageJson } from 'type-fest';
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 '../../logger';
25
- import CodeGeneratorLanguage from '../language';
25
+ import logger from '../../../logger.js';
26
+ import CodeGenerator from '../../codegenerator.js';
26
27
 
27
- import { docblockEscape, generateTypeName, wordWrap } from './typescript/util';
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
- export default class TSGenerator extends CodeGeneratorLanguage {
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, opts: TSGeneratorOptions = {}) {
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: "Required for the `@readme/api-core` library that the codegen'd SDK uses for making requests.",
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: options.compilerTarget === 'cjs' ? ScriptTarget.ES5 : ScriptTarget.ES2020,
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
- async installer(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
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
- return execa('npm', [...npmInstall, installDir].filter(Boolean))
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 current OpenAPI definition into a TypeScript library.
163
+ * Compile the TS code we generated into JS for use in CJS and ESM environments.
186
164
  *
187
165
  */
188
- async generator() {
189
- const sdkSource = this.createSourceFile();
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
- [sourceFile.getBaseName()]: sourceFile.getFullText(),
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
- createSourceFile() {
250
+ private createSDKSource(sourceDirectory: Directory) {
293
251
  const { operations } = this.loadOperationsAndMethods();
294
252
 
295
- const sourceFile = this.project.createSourceFile('index.ts', '');
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 IFEE.
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 CJS targets have `createSDK` exported with `module.exports`, but the TS type side
438
- // of things to work right we need to set this as `export =`. Thankfully `ts-morph` will
439
- // handle this accordingly and still create our JS file with `module.exports` and not
440
- // `export =` -- only TS types will have this export style.
441
- isExportEquals: this.compilerTarget === 'cjs' && this.outputJS,
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 = this.project.createSourceFile('schemas.ts', '');
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
- sourceFile.addVariableStatement({
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
- * This is the conversion prefix that we add to all `$ref` pointers we find in
467
- * generated JSON Schema.
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 = this.project.createSourceFile('types.ts', '');
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.addTagToDocblock(docblock, {
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.addTagToDocblock(docblock, {
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.addTagToDocblock(docblock, {
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 `::convert::${typeName}` as SchemaObject;
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('::convert::')) {
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('::convert::', '');
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 `::convert::${typeName}` as SchemaObject;
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('::convert::')) {
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('::convert::', '');
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
  }