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