api 6.0.0 → 6.1.1

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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/cache.d.ts +4 -1
  3. package/dist/cli/codegen/languages/typescript/util.d.ts +0 -1
  4. package/dist/cli/codegen/languages/typescript/util.js +1 -10
  5. package/dist/cli/codegen/languages/typescript.d.ts +22 -5
  6. package/dist/cli/codegen/languages/typescript.js +53 -17
  7. package/dist/cli/commands/install.js +33 -31
  8. package/dist/cli/storage.d.ts +10 -10
  9. package/dist/cli/storage.js +14 -1
  10. package/dist/core/errors/fetchError.d.ts +4 -4
  11. package/dist/core/getJSONSchemaDefaults.d.ts +0 -1
  12. package/dist/core/getJSONSchemaDefaults.js +0 -1
  13. package/dist/core/index.d.ts +1 -1
  14. package/dist/core/prepareAuth.d.ts +1 -1
  15. package/dist/core/prepareParams.js +8 -5
  16. package/dist/core/prepareServer.d.ts +0 -3
  17. package/dist/core/prepareServer.js +0 -3
  18. package/dist/fetcher.js +0 -1
  19. package/dist/index.js +0 -1
  20. package/dist/packageInfo.d.ts +1 -1
  21. package/dist/packageInfo.js +1 -1
  22. package/package.json +9 -22
  23. package/src/cache.ts +4 -1
  24. package/src/cli/codegen/index.ts +1 -1
  25. package/src/cli/codegen/language.ts +1 -1
  26. package/src/cli/codegen/languages/typescript/util.ts +0 -9
  27. package/src/cli/codegen/languages/typescript.ts +89 -27
  28. package/src/cli/commands/install.ts +14 -17
  29. package/src/cli/lib/prompt.ts +1 -1
  30. package/src/cli/storage.ts +27 -10
  31. package/src/core/errors/fetchError.ts +4 -4
  32. package/src/core/getJSONSchemaDefaults.ts +2 -3
  33. package/src/core/index.ts +7 -2
  34. package/src/core/prepareAuth.ts +4 -4
  35. package/src/core/prepareParams.ts +15 -10
  36. package/src/core/prepareServer.ts +0 -3
  37. package/src/fetcher.ts +2 -3
  38. package/src/index.ts +0 -1
  39. package/src/packageInfo.ts +1 -1
  40. package/tsconfig.json +1 -1
package/dist/index.js CHANGED
@@ -70,7 +70,6 @@ var Sdk = /** @class */ (function () {
70
70
  * Create dynamic accessors for every operation with a defined operation ID. If an operation
71
71
  * does not have an operation ID it can be accessed by its `.method('/path')` accessor instead.
72
72
  *
73
- * @param spec
74
73
  */
75
74
  function loadOperations(spec) {
76
75
  return Object.entries(spec.getPaths())
@@ -1,2 +1,2 @@
1
1
  export declare const PACKAGE_NAME = "api";
2
- export declare const PACKAGE_VERSION = "6.0.0";
2
+ export declare const PACKAGE_VERSION = "6.1.1";
@@ -3,4 +3,4 @@ exports.__esModule = true;
3
3
  exports.PACKAGE_VERSION = exports.PACKAGE_NAME = void 0;
4
4
  // This file is automatically updated by the build script.
5
5
  exports.PACKAGE_NAME = 'api';
6
- exports.PACKAGE_VERSION = '6.0.0';
6
+ exports.PACKAGE_VERSION = '6.1.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api",
3
- "version": "6.0.0",
3
+ "version": "6.1.1",
4
4
  "description": "Magical SDK generation from an OpenAPI definition 🪄",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -10,10 +10,11 @@
10
10
  "scripts": {
11
11
  "build": "tsc",
12
12
  "debug:bin": "node -r ts-node/register src/bin.ts",
13
+ "lint:types": "tsc --noEmit",
13
14
  "prebuild": "rm -rf dist/; npm run version",
14
15
  "prepack": "npm run build",
15
- "test": "nyc mocha $(find test -name '*.test.ts' -not -path '*/smoketest.test.ts')",
16
- "test:smoke": "npx mocha test/cli/codegen/languages/typescript/smoketest.test.ts",
16
+ "test": "vitest --coverage",
17
+ "test:smoke": "vitest --config=vitest-smoketest.config.ts ",
17
18
  "version": "node -p \"'// This file is automatically updated by the build script.\\nexport const PACKAGE_NAME = \\'' + require('./package.json').name + '\\';\\nexport const PACKAGE_VERSION = \\'' + require('./package.json').version + '\\';'\" > src/packageInfo.ts; git add src/packageInfo.ts"
18
19
  },
19
20
  "repository": {
@@ -63,7 +64,6 @@
63
64
  "node-abort-controller": "^3.1.1",
64
65
  "oas": "^20.4.0",
65
66
  "ora": "^5.4.1",
66
- "prettier": "^2.8.3",
67
67
  "prompts": "^2.4.2",
68
68
  "remove-undefined-objects": "^2.0.2",
69
69
  "semver": "^7.3.8",
@@ -74,7 +74,6 @@
74
74
  "devDependencies": {
75
75
  "@readme/oas-examples": "^5.9.0",
76
76
  "@types/caseless": "^0.12.2",
77
- "@types/chai": "^4.3.4",
78
77
  "@types/find-cache-dir": "^3.2.1",
79
78
  "@types/js-yaml": "^4.0.5",
80
79
  "@types/lodash.camelcase": "^4.3.7",
@@ -82,31 +81,19 @@
82
81
  "@types/lodash.merge": "^4.6.7",
83
82
  "@types/lodash.setwith": "^4.3.7",
84
83
  "@types/lodash.startcase": "^4.4.7",
85
- "@types/mocha": "^10.0.1",
86
84
  "@types/prettier": "^2.7.2",
87
85
  "@types/prompts": "^2.4.2",
88
86
  "@types/semver": "^7.3.13",
89
- "@types/sinon-chai": "^3.2.9",
90
87
  "@types/ssri": "^7.1.1",
91
88
  "@types/validate-npm-package-name": "^4.0.0",
92
- "chai": "^4.3.7",
89
+ "@vitest/coverage-v8": "^0.34.1",
93
90
  "fetch-mock": "^9.11.0",
94
- "mocha": "^10.1.0",
95
- "mock-require": "^3.0.3",
96
- "nyc": "^15.1.0",
97
91
  "oas-normalize": "^8.3.2",
98
- "sinon": "^15.0.0",
99
- "sinon-chai": "^3.7.0",
100
- "type-fest": "^3.5.4",
92
+ "type-fest": "^4.2.0",
101
93
  "typescript": "^4.9.5",
102
- "unique-temp-dir": "^1.0.0"
94
+ "unique-temp-dir": "^1.0.0",
95
+ "vitest": "^0.34.1"
103
96
  },
104
97
  "prettier": "@readme/eslint-config/prettier",
105
- "nyc": {
106
- "exclude": [
107
- "dist/",
108
- "test/"
109
- ]
110
- },
111
- "gitHead": "9759db4c067baf3928ad3a9d76b4965658955d78"
98
+ "gitHead": "e440a28841e753895bfb5315b4d31f679824fa39"
112
99
  }
package/src/cache.ts CHANGED
@@ -16,8 +16,11 @@ type CacheStore = Record<
16
16
  string,
17
17
  {
18
18
  hash: string;
19
- path?: string; // Deprecated in v4.5.0 in favor of `hash`.
20
19
  original: string | OASDocument;
20
+ /**
21
+ * @deprecated Deprecated in v4.5.0 in favor of `hash`.
22
+ */
23
+ path?: string;
21
24
  title?: string;
22
25
  version?: string;
23
26
  }
@@ -9,7 +9,7 @@ export default function codegen(
9
9
  language: SupportedLanguages,
10
10
  spec: Oas,
11
11
  specPath: string,
12
- identifier: string
12
+ identifier: string,
13
13
  ): CodeGeneratorLanguage {
14
14
  switch (language) {
15
15
  case 'js':
@@ -49,7 +49,7 @@ export default abstract class CodeGeneratorLanguage {
49
49
  */
50
50
  if (JSON.stringify(spec.api).includes('"$ref":"#/')) {
51
51
  throw new Error(
52
- 'Sorry, this library does not yet support generating an SDK for an OpenAPI definition that contains circular references.'
52
+ 'Sorry, this library does not yet support generating an SDK for an OpenAPI definition that contains circular references.',
53
53
  );
54
54
  }
55
55
  }
@@ -1,7 +1,6 @@
1
1
  import camelCase from 'lodash.camelcase';
2
2
  import deburr from 'lodash.deburr';
3
3
  import startCase from 'lodash.startcase';
4
- import { format as prettier } from 'prettier';
5
4
 
6
5
  /**
7
6
  * This is a mix of reserved JS words and keywords in TypeScript that might be reserved or
@@ -100,14 +99,6 @@ const RESERVED_WORDS = [
100
99
  'window',
101
100
  ];
102
101
 
103
- export function formatter(content: string) {
104
- return prettier(content, {
105
- parser: 'typescript',
106
- printWidth: 100,
107
- singleQuote: true,
108
- });
109
- }
110
-
111
102
  /**
112
103
  * @see {@link https://www.30secondsofcode.org/js/s/word-wrap}
113
104
  */
@@ -3,7 +3,13 @@ import type { InstallerOptions } from '../language';
3
3
  import type Oas from 'oas';
4
4
  import type { Operation } from 'oas';
5
5
  import type { HttpMethods, SchemaObject } from 'oas/dist/rmoas.types';
6
- import type { ClassDeclaration, JSDocStructure, OptionalKind, ParameterDeclarationStructure } from 'ts-morph';
6
+ import type {
7
+ ClassDeclaration,
8
+ JSDocStructure,
9
+ JSDocTagStructure,
10
+ OptionalKind,
11
+ ParameterDeclarationStructure,
12
+ } from 'ts-morph';
7
13
  import type { PackageJson } from 'type-fest';
8
14
 
9
15
  import fs from 'fs';
@@ -17,19 +23,25 @@ import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationK
17
23
  import logger from '../../logger';
18
24
  import CodeGeneratorLanguage from '../language';
19
25
 
20
- import { docblockEscape, formatter, generateTypeName, wordWrap } from './typescript/util';
26
+ import { docblockEscape, generateTypeName, wordWrap } from './typescript/util';
21
27
 
22
28
  export interface TSGeneratorOptions {
23
- outputJS?: boolean;
24
29
  compilerTarget?: 'cjs' | 'esm';
30
+ outputJS?: boolean;
25
31
  }
26
32
 
27
33
  interface OperationTypeHousing {
34
+ operation: Operation;
28
35
  types: {
29
36
  params?: false | Record<'body' | 'formData' | 'metadata', string>;
30
- responses?: Record<string, string>;
37
+ responses?: Record<
38
+ string | number,
39
+ {
40
+ description?: string;
41
+ type: string;
42
+ }
43
+ >;
31
44
  };
32
- operation: Operation;
33
45
  }
34
46
 
35
47
  export default class TSGenerator extends CodeGeneratorLanguage {
@@ -60,7 +72,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
60
72
  usesHTTPMethodRangeInterface = false;
61
73
 
62
74
  constructor(spec: Oas, specPath: string, identifier: string, opts: TSGeneratorOptions = {}) {
63
- const options: { outputJS: boolean; compilerTarget: 'cjs' | 'esm' } = {
75
+ const options: { compilerTarget: 'cjs' | 'esm'; outputJS: boolean } = {
64
76
  outputJS: false,
65
77
  compilerTarget: 'cjs',
66
78
  ...opts,
@@ -233,7 +245,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
233
245
  return {};
234
246
  }
235
247
 
236
- let code = formatter(sourceFile.text);
248
+ let code = sourceFile.text;
237
249
  if (file === 'index.js' && this.compilerTarget === 'cjs') {
238
250
  /**
239
251
  * There's an annoying quirk with `ts-morph` where if we're exporting a default export
@@ -259,7 +271,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
259
271
 
260
272
  return [
261
273
  ...this.project.getSourceFiles().map(sourceFile => ({
262
- [sourceFile.getBaseName()]: formatter(sourceFile.getFullText()),
274
+ [sourceFile.getBaseName()]: sourceFile.getFullText(),
263
275
  })),
264
276
 
265
277
  // Because we're returning the raw source files for TS generation we also need to separately
@@ -269,7 +281,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
269
281
  .emitToMemory({ emitOnlyDtsFiles: true })
270
282
  .getFiles()
271
283
  .map(sourceFile => ({
272
- [path.basename(sourceFile.filePath)]: formatter(sourceFile.text),
284
+ [path.basename(sourceFile.filePath)]: sourceFile.text,
273
285
  })),
274
286
  ].reduce((prev, next) => Object.assign(prev, next));
275
287
  }
@@ -328,7 +340,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
328
340
  {
329
341
  tagName: 'param',
330
342
  text: wordWrap(
331
- 'config.timeout Override the default `fetch` request timeout of 30 seconds. This number should be represented in milliseconds.'
343
+ 'config.timeout Override the default `fetch` request timeout of 30 seconds. This number should be represented in milliseconds.',
332
344
  ),
333
345
  },
334
346
  ],
@@ -358,7 +370,7 @@ sdk.auth('username', 'password');
358
370
  sdk.auth('myBearerToken');
359
371
 
360
372
  @example <caption>API Keys</caption>
361
- sdk.auth('myApiKey');`)
373
+ sdk.auth('myApiKey');`),
362
374
  ),
363
375
  tags: [
364
376
  { tagName: 'see', text: '{@link https://spec.openapis.org/oas/v3.0.3#fixed-fields-22}' },
@@ -391,7 +403,7 @@ sdk.server('https://{region}.api.example.com/{basePath}', {
391
403
  });
392
404
 
393
405
  @example <caption>Fully qualified server URL</caption>
394
- sdk.server('https://eu.api.example.com/v14');`)
406
+ sdk.server('https://eu.api.example.com/v14');`),
395
407
  ),
396
408
  tags: [
397
409
  { tagName: 'param', text: 'url Server URL' },
@@ -502,6 +514,20 @@ sdk.server('https://eu.api.example.com/v14');`)
502
514
  return sourceFile;
503
515
  }
504
516
 
517
+ /**
518
+ * Add a new JSDoc `@tag` to an existing docblock.
519
+ *
520
+ */
521
+ static addTagToDocblock(docblock: OptionalKind<JSDocStructure>, tag: OptionalKind<JSDocTagStructure>) {
522
+ const tags = docblock.tags ?? [];
523
+ tags.push(tag);
524
+
525
+ return {
526
+ ...docblock,
527
+ tags,
528
+ };
529
+ }
530
+
505
531
  /**
506
532
  * Create operation accessors on the SDK.
507
533
  *
@@ -510,9 +536,9 @@ sdk.server('https://eu.api.example.com/v14');`)
510
536
  operation: Operation,
511
537
  operationId: string,
512
538
  paramTypes?: OperationTypeHousing['types']['params'],
513
- responseTypes?: OperationTypeHousing['types']['responses']
539
+ responseTypes?: OperationTypeHousing['types']['responses'],
514
540
  ) {
515
- const docblock: OptionalKind<JSDocStructure> = {};
541
+ let docblock: OptionalKind<JSDocStructure> = {};
516
542
  const summary = operation.getSummary();
517
543
  const description = operation.getDescription();
518
544
  if (summary || description) {
@@ -531,7 +557,10 @@ sdk.server('https://eu.api.example.com/v14');`)
531
557
  };
532
558
 
533
559
  if (summary && description) {
534
- docblock.tags = [{ tagName: 'summary', text: docblockEscape(wordWrap(summary)) }];
560
+ docblock = TSGenerator.addTagToDocblock(docblock, {
561
+ tagName: 'summary',
562
+ text: docblockEscape(wordWrap(summary)),
563
+ });
535
564
  }
536
565
  }
537
566
 
@@ -568,8 +597,8 @@ sdk.server('https://eu.api.example.com/v14');`)
568
597
 
569
598
  let returnType = 'Promise<FetchResponse<number, unknown>>';
570
599
  if (responseTypes) {
571
- returnType = `Promise<${Object.entries(responseTypes)
572
- .map(([status, responseType]) => {
600
+ const returnTypes = Object.entries(responseTypes)
601
+ .map(([status, { description: responseDescription, type: responseType }]) => {
573
602
  if (status.toLowerCase() === 'default') {
574
603
  return `FetchResponse<number, ${responseType}>`;
575
604
  } else if (status.length === 3 && status.toUpperCase().endsWith('XX')) {
@@ -580,19 +609,54 @@ sdk.server('https://eu.api.example.com/v14');`)
580
609
  return `FetchResponse<number, ${responseType}>`;
581
610
  }
582
611
 
612
+ if (Number(statusPrefix) >= 4) {
613
+ docblock = TSGenerator.addTagToDocblock(docblock, {
614
+ tagName: 'throws',
615
+ text: `FetchError<${status}, ${responseType}>${
616
+ responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
617
+ }`,
618
+ });
619
+
620
+ return false;
621
+ }
622
+
583
623
  this.usesHTTPMethodRangeInterface = true;
584
624
  return `FetchResponse<HTTPMethodRange<${statusPrefix}00, ${statusPrefix}99>, ${responseType}>`;
585
625
  }
586
626
 
627
+ // 400 and 500 status code families are thrown as exceptions so adding them as a possible
628
+ // return type isn't valid.
629
+ if (Number(status) >= 400) {
630
+ docblock = TSGenerator.addTagToDocblock(docblock, {
631
+ tagName: 'throws',
632
+ text: `FetchError<${status}, ${responseType}>${
633
+ responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
634
+ }`,
635
+ });
636
+
637
+ return false;
638
+ }
639
+
587
640
  return `FetchResponse<${status}, ${responseType}>`;
588
641
  })
589
- .join(' | ')}>`;
642
+ .filter(Boolean)
643
+ .join(' | ');
644
+
645
+ // If all of our documented responses are for error status codes then all we can document for
646
+ // anything else that might happen is `unknown`.
647
+ returnType = `Promise<${returnTypes.length ? returnTypes : 'FetchResponse<number, unknown>'}>`;
590
648
  }
591
649
 
650
+ const shouldAddAltTypedOverloads = Object.keys(parameters).length === 2 && hasOptionalBody && !hasOptionalMetadata;
592
651
  const operationIdAccessor = this.sdk.addMethod({
593
652
  name: operationId,
594
653
  returnType,
595
- docs: Object.keys(docblock).length ? [docblock] : null,
654
+
655
+ // If we're going to be creating typed method overloads for optional body an metadata handling
656
+ // we should only add a docblock to the first overload we create because IDE Intellisense will
657
+ // always use that and adding a docblock to all three will bloat the SDK with unused and
658
+ // unsurfaced method documentation.
659
+ docs: shouldAddAltTypedOverloads ? null : Object.keys(docblock).length ? [docblock] : null,
596
660
  statements: writer => {
597
661
  /**
598
662
  * @example return this.core.fetch('/pet/findByStatus', 'get', body, metadata);
@@ -612,7 +676,7 @@ sdk.server('https://eu.api.example.com/v14');`)
612
676
  }
613
677
 
614
678
  fetchStmt.write(arg.name);
615
- if (totalParams > 1 && i !== totalParams) {
679
+ if (i !== totalParams - 1) {
616
680
  fetchStmt.write(', ');
617
681
  }
618
682
  });
@@ -626,10 +690,6 @@ sdk.server('https://eu.api.example.com/v14');`)
626
690
  // If we have both body and metadata parameters but only body is optional we need to create
627
691
  // a couple function overloads as Typescript doesn't let us have an optional method parameter
628
692
  // come before one that's required.
629
- //
630
- // None of these accessor overloads will receive a docblock because the original will have
631
- // that covered.
632
- const shouldAddAltTypedOverloads = Object.keys(parameters).length === 2 && hasOptionalBody && !hasOptionalMetadata;
633
693
  if (shouldAddAltTypedOverloads) {
634
694
  // Create an overload that has both `body` and `metadata` parameters as required.
635
695
  operationIdAccessor.addOverload({
@@ -645,7 +705,6 @@ sdk.server('https://eu.api.example.com/v14');`)
645
705
  operationIdAccessor.addOverload({
646
706
  parameters: [{ ...parameters.metadata }],
647
707
  returnType,
648
- docs: Object.keys(docblock).length ? [docblock] : null,
649
708
  });
650
709
 
651
710
  // Create an overload that has both `body` and `metadata` parameters as optional. Even though
@@ -807,7 +866,7 @@ sdk.server('https://eu.api.example.com/v14');`)
807
866
  .reduce((prev, next) => Object.assign(prev, next));
808
867
 
809
868
  const res = Object.entries(schemas)
810
- .map(([status, { schema }]) => {
869
+ .map(([status, { description, schema }]) => {
811
870
  let typeName;
812
871
 
813
872
  if (typeof schema === 'string' && schema.startsWith('::convert::')) {
@@ -826,7 +885,10 @@ sdk.server('https://eu.api.example.com/v14');`)
826
885
  return {
827
886
  // Types are prefixed with `types.` because that's how we're importing them from
828
887
  // `types.d.ts`.
829
- [status]: `types.${typeName}`,
888
+ [status]: {
889
+ type: `types.${typeName}`,
890
+ description,
891
+ },
830
892
  };
831
893
  })
832
894
  .reduce((prev, next) => Object.assign(prev, next), {});
@@ -4,7 +4,6 @@ import { Command, Option } from 'commander';
4
4
  import figures from 'figures';
5
5
  import Oas from 'oas';
6
6
  import ora from 'ora';
7
- import validateNPMPackageName from 'validate-npm-package-name';
8
7
 
9
8
  import Fetcher from '../../fetcher';
10
9
  import codegen from '../codegen';
@@ -18,16 +17,17 @@ cmd
18
17
  .name('install')
19
18
  .description('install an API SDK into your codebase')
20
19
  .argument('<uri>', 'an API to install')
20
+ .option('-i, --identifier <identifier>', 'API identifier (eg. `@api/petstore`)')
21
21
  .addOption(
22
22
  new Option('-l, --lang <language>', 'SDK language').choices([
23
23
  'js', // User generally wants JS, we'll prompt if they want CJS or ESM files.
24
24
  'js-cjs',
25
25
  'js-esm',
26
26
  'ts',
27
- ])
27
+ ]),
28
28
  )
29
29
  .addOption(new Option('-y, --yes', 'Automatically answer "yes" to any prompts printed'))
30
- .action(async (uri: string, options: { lang: string; yes?: boolean }) => {
30
+ .action(async (uri: string, options: { identifier?: string; lang: string; yes?: boolean }) => {
31
31
  let language: SupportedLanguages;
32
32
  if (options.lang) {
33
33
  language = options.lang as SupportedLanguages;
@@ -69,7 +69,12 @@ cmd
69
69
  }
70
70
 
71
71
  let identifier;
72
- if (Fetcher.isAPIRegistryUUID(uri)) {
72
+ if (options.identifier) {
73
+ // `Storage.isIdentifierValid` will throw an exception if an identifier is invalid.
74
+ if (Storage.isIdentifierValid(options.identifier)) {
75
+ identifier = options.identifier;
76
+ }
77
+ } else if (Fetcher.isAPIRegistryUUID(uri)) {
73
78
  identifier = Fetcher.getProjectPrefixFromRegistryUUID(uri);
74
79
  } else {
75
80
  ({ value: identifier } = await promptTerminal({
@@ -82,19 +87,11 @@ cmd
82
87
  return false;
83
88
  }
84
89
 
85
- // Is this identifier already in storage?
86
- if (Storage.isInLockFile({ identifier: value })) {
87
- return `"${value}" is already taken in your \`.api/\` directory. Please enter another identifier.`;
88
- }
89
-
90
- const isValidForNPM = validateNPMPackageName(`@api/${value}`);
91
- if (!isValidForNPM.validForNewPackages) {
92
- // `prompts` doesn't support surfacing multiple errors in a `validate` call so we can
93
- // only surface the first to the user.
94
- return isValidForNPM.errors[0];
90
+ try {
91
+ return Storage.isIdentifierValid(value, true);
92
+ } catch (err) {
93
+ return err.message;
95
94
  }
96
-
97
- return true;
98
95
  },
99
96
  }));
100
97
  }
@@ -191,7 +188,7 @@ cmd
191
188
  Examples:
192
189
  $ api install @developers/v2.0#nysezql0wwo236
193
190
  $ api install https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json
194
- $ api install ./petstore.json`
191
+ $ api install ./petstore.json`,
195
192
  );
196
193
 
197
194
  export default cmd;
@@ -9,7 +9,7 @@ import prompts from 'prompts';
9
9
  */
10
10
  export default async function promptTerminal<T extends string = string>(
11
11
  question: prompts.PromptObject<T>,
12
- options?: prompts.Options
12
+ options?: prompts.Options,
13
13
  ) {
14
14
  const enableTerminalCursor = () => {
15
15
  process.stdout.write('\x1B[?25h');
@@ -5,6 +5,7 @@ import path from 'path';
5
5
 
6
6
  import makeDir from 'make-dir';
7
7
  import ssri from 'ssri';
8
+ import validateNPMPackageName from 'validate-npm-package-name';
8
9
 
9
10
  import Fetcher from '../fetcher';
10
11
  import { PACKAGE_VERSION } from '../packageInfo';
@@ -105,6 +106,22 @@ export default class Storage {
105
106
  return Storage.lockfile;
106
107
  }
107
108
 
109
+ static isIdentifierValid(identifier: string, prefixWithAPINamespace?: boolean) {
110
+ // Is this identifier already in storage?
111
+ if (Storage.isInLockFile({ identifier })) {
112
+ throw new Error(`"${identifier}" is already taken in your \`.api/\` directory. Please try another identifier.`);
113
+ }
114
+
115
+ const isValidForNPM = validateNPMPackageName(prefixWithAPINamespace ? `@api/${identifier}` : identifier);
116
+ if (!isValidForNPM.validForNewPackages) {
117
+ // `prompts` doesn't support surfacing multiple errors in a `validate` call so we can only
118
+ // surface the first to the user.
119
+ throw new Error(`Identifier cannot be used for an NPM package: ${isValidForNPM.errors[0]}`);
120
+ }
121
+
122
+ return true;
123
+ }
124
+
108
125
  static isInLockFile(search: { identifier?: string; source?: string }) {
109
126
  // Because this method may run before we initialize a new storage object we should make sure
110
127
  // that we have a storage directory present.
@@ -207,7 +224,6 @@ export default class Storage {
207
224
  * ├── openapi.json
208
225
  * └── package.json
209
226
  *
210
- * @param spec
211
227
  */
212
228
  save(spec: OASDocument) {
213
229
  if (!this.identifier) {
@@ -254,12 +270,13 @@ export default class Storage {
254
270
  }
255
271
 
256
272
  export interface Lockfile {
273
+ apis: LockfileAPI[];
274
+
257
275
  /**
258
276
  * The `api.json` schema version. This will only ever change if we introduce breaking changes to
259
277
  * this store.
260
278
  */
261
279
  version: '1.0';
262
- apis: LockfileAPI[];
263
280
  }
264
281
 
265
282
  export interface LockfileAPI {
@@ -272,13 +289,11 @@ export interface LockfileAPI {
272
289
  identifier: string;
273
290
 
274
291
  /**
275
- * The original source that was used to generate the SDK with.
292
+ * The version of `api` that was used to install this SDK.
276
293
  *
277
- * @example https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json
278
- * @example ./petstore.json
279
- * @example @developers/v2.0#nysezql0wwo236
294
+ * @example 5.0.0
280
295
  */
281
- source: string;
296
+ installerVersion: string;
282
297
 
283
298
  /**
284
299
  * An integrity hash that will be used to determine on `npx api update` calls if the API has
@@ -289,9 +304,11 @@ export interface LockfileAPI {
289
304
  integrity: string;
290
305
 
291
306
  /**
292
- * The version of `api` that was used to install this SDK.
307
+ * The original source that was used to generate the SDK with.
293
308
  *
294
- * @example 5.0.0
309
+ * @example https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json
310
+ * @example ./petstore.json
311
+ * @example @developers/v2.0#nysezql0wwo236
295
312
  */
296
- installerVersion: string;
313
+ source: string;
297
314
  }
@@ -1,9 +1,9 @@
1
- class FetchError extends Error {
1
+ class FetchError<Status = number, Data = unknown> extends Error {
2
2
  /** HTTP Status */
3
- status: number;
3
+ status: Status;
4
4
 
5
5
  /** The content of the response. */
6
- data: unknown;
6
+ data: Data;
7
7
 
8
8
  /** The Headers of the response. */
9
9
  headers: Headers;
@@ -11,7 +11,7 @@ class FetchError extends Error {
11
11
  /** The raw `Response` object. */
12
12
  res: Response;
13
13
 
14
- constructor(status: number, data: unknown, headers: Headers, res: Response) {
14
+ constructor(status: Status, data: Data, headers: Headers, res: Response) {
15
15
  super(res.statusText);
16
16
 
17
17
  this.name = 'FetchError';
@@ -12,7 +12,6 @@ import traverse from 'json-schema-traverse';
12
12
  *
13
13
  * @todo This is a good candidate to be moved into a core `oas` library method.
14
14
  * @see {@link https://github.com/mdornseif/json-schema-default}
15
- * @param jsonSchemas
16
15
  */
17
16
  export default function getJSONSchemaDefaults(jsonSchemas: SchemaWrapper[]) {
18
17
  return jsonSchemas
@@ -27,7 +26,7 @@ export default function getJSONSchemaDefaults(jsonSchemas: SchemaWrapper[]) {
27
26
  parentPointer: string,
28
27
  parentKeyword: string,
29
28
  parentSchema: SchemaObject,
30
- indexProperty: string
29
+ indexProperty: string,
31
30
  ) => {
32
31
  if (!pointer.startsWith('/properties/')) {
33
32
  return;
@@ -59,7 +58,7 @@ export default function getJSONSchemaDefaults(jsonSchemas: SchemaWrapper[]) {
59
58
  }
60
59
  }
61
60
  }
62
- }
61
+ },
63
62
  );
64
63
 
65
64
  if (!Object.keys(defaults).length) {
package/src/core/index.ts CHANGED
@@ -26,9 +26,9 @@ export interface ConfigOptions {
26
26
 
27
27
  export interface FetchResponse<status, data> {
28
28
  data: data;
29
- status: status;
30
29
  headers: Headers;
31
30
  res: Response;
31
+ status: status;
32
32
  }
33
33
 
34
34
  // https://stackoverflow.com/a/39495173
@@ -128,7 +128,12 @@ export default class APICore {
128
128
  const parsed = await parseResponse(res);
129
129
 
130
130
  if (res.status >= 400 && res.status <= 599) {
131
- throw new FetchError(parsed.status, parsed.data, parsed.headers, parsed.res);
131
+ throw new FetchError<typeof parsed.status, typeof parsed.data>(
132
+ parsed.status,
133
+ parsed.data,
134
+ parsed.headers,
135
+ parsed.res,
136
+ );
132
137
  }
133
138
 
134
139
  return parsed;