httpsnippet-client-api 7.0.0-alpha.6 → 7.0.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,20 +1,42 @@
1
1
  import type { ReducedHelperObject } from '@readme/httpsnippet/helpers/reducer';
2
- import type { Client } from '@readme/httpsnippet/targets';
3
- import type Operation from 'oas/operation';
4
- import type { HttpMethods, OASDocument } from 'oas/rmoas.types';
2
+ import type { Client, ClientPlugin } from '@readme/httpsnippet/targets';
3
+ import type { Operation } from 'oas/operation';
4
+ import type { HttpMethods, OASDocument } from 'oas/types';
5
5
 
6
6
  import { CodeBuilder } from '@readme/httpsnippet/helpers/code-builder';
7
+ import camelCase from 'camelcase'; // eslint-disable-line import/no-extraneous-dependencies
7
8
  import contentType from 'content-type';
8
9
  import Oas from 'oas';
9
10
  import { matchesMimeType } from 'oas/utils';
10
- import stringifyObject from 'stringify-object';
11
+ import { isReservedOrBuiltinsLC } from 'reserved2';
12
+ import stringifyObject from 'stringify-object'; // eslint-disable-line import/no-extraneous-dependencies
13
+
14
+ /**
15
+ * @note This regex also exists in `api/fetcher`.
16
+ *
17
+ * @example @petstore/v1.0#n6kvf10vakpemvplx
18
+ * @example @petstore#n6kvf10vakpemvplx
19
+ */
20
+ const registryUUIDRegex = /^@(?<project>[a-zA-Z0-9-_]+)(\/?(?<version>.+))?#(?<uuid>[a-z0-9]+)$/;
21
+
22
+ /**
23
+ * @note This function also exists in `api/fetcher`.
24
+ */
25
+ function getProjectPrefixFromRegistryUUID(uri: string) {
26
+ const matches = uri.match(registryUUIDRegex);
27
+ if (!matches) {
28
+ return undefined;
29
+ }
30
+
31
+ return matches.groups?.project;
32
+ }
11
33
 
12
34
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
35
  function stringify(obj: any, opts = {}) {
14
36
  return stringifyObject(obj, { indent: ' ', ...opts });
15
37
  }
16
38
 
17
- function buildAuthSnippet(authKey: string | string[]) {
39
+ function buildAuthSnippet(sdkVariable: string, authKey: string[] | string) {
18
40
  // Auth key will be an array for Basic auth cases.
19
41
  if (Array.isArray(authKey)) {
20
42
  const auth: string[] = [];
@@ -27,10 +49,10 @@ function buildAuthSnippet(authKey: string | string[]) {
27
49
  auth.push(`'${token.replace(/'/g, "\\'")}'`);
28
50
  });
29
51
 
30
- return `sdk.auth(${auth.join(', ')});`;
52
+ return `${sdkVariable}.auth(${auth.join(', ')});`;
31
53
  }
32
54
 
33
- return `sdk.auth('${authKey.replace(/'/g, "\\'")}');`;
55
+ return `${sdkVariable}.auth('${authKey.replace(/'/g, "\\'")}');`;
34
56
  }
35
57
 
36
58
  function getAuthSources(operation: Operation) {
@@ -75,8 +97,24 @@ function getAuthSources(operation: Operation) {
75
97
  }
76
98
 
77
99
  interface APIOptions {
78
- apiDefinition: OASDocument;
79
- apiDefinitionUri: string;
100
+ api?: {
101
+ definition: OASDocument;
102
+
103
+ /**
104
+ * The string to identify this SDK as. This is used in the `import sdk from '<identifier>'`
105
+ * sample as well as the the variable name we attach the SDK to.
106
+ *
107
+ * @example `@api/developers`
108
+ */
109
+ identifier?: string;
110
+
111
+ /**
112
+ * The URI that is used to download this API definition from `npx api install`.
113
+ *
114
+ * @example `@developers/v2.0#17273l2glm9fq4l5`
115
+ */
116
+ registryURI: string;
117
+ };
80
118
  escapeBrackets?: boolean;
81
119
  indent?: string | false;
82
120
  }
@@ -88,28 +126,47 @@ const client: Client<APIOptions> = {
88
126
  link: 'https://npm.im/api',
89
127
  description: 'Automatic SDK generation from an OpenAPI definition.',
90
128
  extname: '.js',
129
+ installation: 'npx api install "{packageName}"',
91
130
  },
92
131
  convert: ({ cookiesObj, headersObj, postData, queryObj, url, ...source }, options) => {
93
132
  const opts = {
94
133
  ...options,
95
- } as APIOptions;
96
-
97
- if (!('apiDefinitionUri' in opts)) {
98
- throw new Error('This HTTP Snippet client must have an `apiDefinitionUri` option supplied to it.');
99
- } else if (!('apiDefinition' in opts)) {
100
- throw new Error('This HTTP Snippet client must have an `apiDefinition` option supplied to it.');
134
+ };
135
+
136
+ if (!opts?.api) {
137
+ throw new Error('This HTTPSnippet client must have an `api` config supplied to it.');
138
+ } else if (!opts?.api?.definition) {
139
+ throw new Error('This HTTPSnippet client must have an `api.definition` option supplied to it.');
140
+ } else if (!opts?.api?.registryURI) {
141
+ throw new Error('This HTTPSnippet client must have an `api.registryURI` option supplied to it.');
101
142
  }
102
143
 
103
144
  const method = source.method.toLowerCase() as HttpMethods;
104
- const oas = new Oas(opts.apiDefinition);
145
+ const oas = new Oas(opts.api.definition);
105
146
  const apiDefinition = oas.getDefinition();
106
147
  const foundOperation = oas.findOperation(url, method);
107
148
  if (!foundOperation) {
108
149
  throw new Error(
109
- `Unable to locate a matching operation in the supplied \`apiDefinition\` for: ${source.method} ${url}`,
150
+ `Unable to locate a matching operation in the supplied \`api.definition\` for: ${source.method} ${url}`,
110
151
  );
111
152
  }
112
153
 
154
+ let sdkPackageName: string | undefined;
155
+ let sdkVariable: string;
156
+ if (opts.api.identifier) {
157
+ sdkPackageName = opts.api.identifier;
158
+
159
+ sdkVariable = camelCase(opts.api.identifier);
160
+ if (isReservedOrBuiltinsLC(sdkVariable)) {
161
+ // If this identifier is a reserved JS word then we should prefix it with an underscore so
162
+ // this snippet can be valid code.
163
+ sdkVariable = `_${sdkVariable}`;
164
+ }
165
+ } else {
166
+ sdkPackageName = getProjectPrefixFromRegistryUUID(opts.api.registryURI);
167
+ sdkVariable = camelCase(sdkPackageName || 'sdk');
168
+ }
169
+
113
170
  const operationSlugs = foundOperation.url.slugs;
114
171
  const operation = oas.operation(foundOperation.url.nonNormalizedPath, method);
115
172
  const operationPathParameters = operation.getParameters().filter(param => param.in === 'path');
@@ -119,7 +176,7 @@ const client: Client<APIOptions> = {
119
176
 
120
177
  const { blank, push, join } = new CodeBuilder({ indent: opts.indent || ' ' });
121
178
 
122
- push(`const sdk = require('api')('${opts.apiDefinitionUri}');`);
179
+ push(`import ${sdkVariable} from '@api/${sdkPackageName}';`);
123
180
  blank();
124
181
 
125
182
  // If we have multiple servers configured and our source URL differs from the stock URL that we
@@ -134,14 +191,14 @@ const client: Client<APIOptions> = {
134
191
  const serverVars = oas.splitVariables(baseUrl);
135
192
  const serverUrl = serverVars ? oas.url(serverVars.selected, serverVars.variables) : baseUrl;
136
193
 
137
- configData.push(`sdk.server('${serverUrl}');`);
194
+ configData.push(`${sdkVariable}.server('${serverUrl}');`);
138
195
  }
139
196
  }
140
197
 
141
- let metadata: Record<string, string | string[]> = {};
198
+ let metadata: Record<string, string[] | string> = {};
142
199
  Object.keys(queryObj).forEach(param => {
143
200
  if (authSources.query.includes(param)) {
144
- authData.push(buildAuthSnippet(queryObj[param]));
201
+ authData.push(buildAuthSnippet(sdkVariable, queryObj[param]));
145
202
 
146
203
  // If this query param is part of an auth source then we don't want it doubled up in the
147
204
  // snippet.
@@ -153,7 +210,7 @@ const client: Client<APIOptions> = {
153
210
 
154
211
  Object.keys(cookiesObj).forEach(cookie => {
155
212
  if (authSources.cookie.includes(cookie)) {
156
- authData.push(buildAuthSnippet(cookiesObj[cookie]));
213
+ authData.push(buildAuthSnippet(sdkVariable, cookiesObj[cookie]));
157
214
 
158
215
  // If this cookie is part of an auth source then we don't want it doubled up.
159
216
  return;
@@ -199,7 +256,7 @@ const client: Client<APIOptions> = {
199
256
  // into our auth data so we can build up an `.auth()` snippet for the SDK.
200
257
  const authScheme = authSources.header[headerLower];
201
258
  if (authScheme === '*') {
202
- authData.push(buildAuthSnippet(headers[header]));
259
+ authData.push(buildAuthSnippet(sdkVariable, headers[header]));
203
260
  } else {
204
261
  // @ts-expect-error `headers[header]` is typed improperly in HTTPSnippet.
205
262
  let authKey = headers[header].replace(`${authSources.header[headerLower]} `, '');
@@ -208,7 +265,7 @@ const client: Client<APIOptions> = {
208
265
  authKey = authKey.split(':');
209
266
  }
210
267
 
211
- authData.push(buildAuthSnippet(authKey));
268
+ authData.push(buildAuthSnippet(sdkVariable, authKey));
212
269
  }
213
270
 
214
271
  delete headers[header];
@@ -232,9 +289,14 @@ const client: Client<APIOptions> = {
232
289
  }
233
290
 
234
291
  // If we haven't used our header anywhere else, or we've deleted it from the payload
235
- // because it'll be handled internally by `api` then we should add the lowercased version
236
- // of our header into the generated code snippet.
237
- requestHeaders[headerLower] = headers[header];
292
+ // because it'll be handled internally by `api` then we should add it into our code snippet.
293
+ if (['accept', 'content-type'].includes(headerLower)) {
294
+ requestHeaders[headerLower] = headers[header];
295
+ } else {
296
+ // Non-reserved headers retain their casing because we want to generate a snippet that
297
+ // matches the TS types that are created during codegeneration.
298
+ requestHeaders[header] = headers[header];
299
+ }
238
300
  });
239
301
 
240
302
  if (Object.keys(requestHeaders).length > 0) {
@@ -306,7 +368,7 @@ const client: Client<APIOptions> = {
306
368
  push(configData.join('\n'));
307
369
  }
308
370
 
309
- push(`sdk.${accessor}(${args.join(', ')})`);
371
+ push(`${sdkVariable}.${accessor}(${args.join(', ')})`);
310
372
  push('.then(({ data }) => console.log(data))', 1);
311
373
  push('.catch(err => console.error(err));', 1);
312
374
 
@@ -314,4 +376,9 @@ const client: Client<APIOptions> = {
314
376
  },
315
377
  };
316
378
 
317
- export default client;
379
+ const plugin: ClientPlugin<APIOptions> = {
380
+ target: 'node',
381
+ client,
382
+ };
383
+
384
+ export default plugin;
package/tsup.config.ts CHANGED
@@ -1,6 +1,6 @@
1
+ /* eslint-disable import/no-extraneous-dependencies, node/no-extraneous-import */
1
2
  import type { Options } from 'tsup';
2
3
 
3
- // eslint-disable-next-line import/no-extraneous-dependencies
4
4
  import { defineConfig } from 'tsup';
5
5
 
6
6
  // eslint-disable-next-line import/no-relative-packages
@@ -11,5 +11,15 @@ export default defineConfig((options: Options) => ({
11
11
  ...config,
12
12
 
13
13
  entry: ['src/index.ts'],
14
+
15
+ noExternal: [
16
+ // These dependencies are ESM-only but because we're building for ESM **and** CJS we can't
17
+ // treat them as external dependencies as CJS libraries can't load ESM code that uses `export`.
18
+ // `noExternal` will instead treeshake these dependencies down and include them in our compiled
19
+ // dists.
20
+ 'camelcase',
21
+ 'stringify-object',
22
+ ],
23
+
14
24
  silent: !options.watch,
15
25
  }));