@appweaver/client 1.2.1 → 1.3.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.
@@ -41,7 +41,9 @@ function generateCommand(program) {
41
41
  const schemaContent = await (0, utils_1.readSchemaContent)(schemaPath);
42
42
  const schemaObject = await (0, utils_1.toSchemaObject)(schemaContent);
43
43
  if (!clientOnly && !noTypes) {
44
- const typesContent = await (0, generators_1.generateTypes)(schemaObject);
44
+ const typesContent = await (0, generators_1.generateTypes)(schemaObject, {
45
+ declaration: typesPath.endsWith('.d.ts')
46
+ });
45
47
  await formatAndWriteFile(typesPath, typesContent, typesOnly || typesPath !== clientPath);
46
48
  console.log(`Generated types to ${node_path_1.default.relative(cwd, typesPath)}`);
47
49
  }
@@ -8,6 +8,15 @@ export declare const CONFIG_RESOURCE_FIELD = "x-appweaver-resource";
8
8
  * The key is the enum values joined by a `|`; the value is the name of the shared type generated
9
9
  * for them. Enums that are not listed here are named after the definitions declaring them. */
10
10
  export declare const SHARED_ENUM_NAMES: Record<string, string>;
11
+ /** Shapes an Appweaver schema repeats inline across its definitions, and the name of the shared
12
+ * definition each of them is hoisted into. A `$ref` names the title of the definition it points
13
+ * to, since the keys the definitions are generated under vary per document. The shapes are
14
+ * matched in the order they are declared, so an outer shape is hoisted before the shapes nested
15
+ * inside it (i.e. `QueryFilterValue` before the `QueryFilterScalar` it is built from). */
16
+ export declare const SHARED_SCHEMA_SHAPES: {
17
+ name: string;
18
+ schema: unknown;
19
+ }[];
11
20
  /** Suffix used when generating the TypeScript module type name for a resource. */
12
21
  export declare const RESOURCE_MODULE_TYPE = "ResourceModuleType";
13
22
  /** Maps CRUD operation names to HTTP methods used for matching OpenAPI paths to `ResourceClient` methods. */
package/cjs/constants.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // They are typically only modified when changes occur in other Appweaver packages
4
4
  // (such as core and common) to reflect new route paths or methods.
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.FILE_OPERATIONS = exports.HEALTH_TYPES = exports.HEALTH_OPERATIONS = exports.HEALTH_MODULE_TYPE = exports.ACCOUNT_TYPES = exports.ACCOUNT_OPERATIONS = exports.ACCOUNT_MODULE_TYPE = exports.AUTH_TYPES = exports.AUTH_OPERATIONS = exports.AUTH_MODULE_TYPE = exports.RESOURCE_TYPES = exports.RESOURCE_OPERATIONS = exports.RESOURCE_MODULE_TYPE = exports.SHARED_ENUM_NAMES = exports.CONFIG_RESOURCE_FIELD = exports.CONFIG_FIELD = exports.FRAMEWORKS = void 0;
6
+ exports.FILE_OPERATIONS = exports.HEALTH_TYPES = exports.HEALTH_OPERATIONS = exports.HEALTH_MODULE_TYPE = exports.ACCOUNT_TYPES = exports.ACCOUNT_OPERATIONS = exports.ACCOUNT_MODULE_TYPE = exports.AUTH_TYPES = exports.AUTH_OPERATIONS = exports.AUTH_MODULE_TYPE = exports.RESOURCE_TYPES = exports.RESOURCE_OPERATIONS = exports.RESOURCE_MODULE_TYPE = exports.SHARED_SCHEMA_SHAPES = exports.SHARED_ENUM_NAMES = exports.CONFIG_RESOURCE_FIELD = exports.CONFIG_FIELD = exports.FRAMEWORKS = void 0;
7
7
  /** Supported frameworks for generating the client class. */
8
8
  exports.FRAMEWORKS = ['fetch', 'angular'];
9
9
  /** Custom OpenAPI extension key used for extracting route prefixes and base paths to their resources. */
@@ -16,6 +16,33 @@ exports.CONFIG_RESOURCE_FIELD = 'x-appweaver-resource';
16
16
  exports.SHARED_ENUM_NAMES = {
17
17
  'asc|desc': 'SortDirection'
18
18
  };
19
+ /** The primitive types a plain filter value takes, as an Appweaver schema declares them. */
20
+ const FILTER_SCALAR = {
21
+ anyOf: [
22
+ { type: 'string' },
23
+ { type: 'number' },
24
+ { type: 'boolean' },
25
+ { type: 'null' }
26
+ ]
27
+ };
28
+ /** Shapes an Appweaver schema repeats inline across its definitions, and the name of the shared
29
+ * definition each of them is hoisted into. A `$ref` names the title of the definition it points
30
+ * to, since the keys the definitions are generated under vary per document. The shapes are
31
+ * matched in the order they are declared, so an outer shape is hoisted before the shapes nested
32
+ * inside it (i.e. `QueryFilterValue` before the `QueryFilterScalar` it is built from). */
33
+ exports.SHARED_SCHEMA_SHAPES = [
34
+ {
35
+ name: 'QueryFilterValue',
36
+ schema: {
37
+ anyOf: [
38
+ FILTER_SCALAR,
39
+ { type: 'array', items: FILTER_SCALAR },
40
+ { $ref: 'QueryCondition' }
41
+ ]
42
+ }
43
+ },
44
+ { name: 'QueryFilterScalar', schema: FILTER_SCALAR }
45
+ ];
19
46
  /** Suffix used when generating the TypeScript module type name for a resource. */
20
47
  exports.RESOURCE_MODULE_TYPE = 'ResourceModuleType';
21
48
  /** Maps CRUD operation names to HTTP methods used for matching OpenAPI paths to `ResourceClient` methods. */
@@ -1,9 +1,11 @@
1
1
  import { OpenAPI3 } from 'openapi-typescript';
2
+ import { GenerateTypesOptions } from '../types';
2
3
  /**
3
4
  * Generates TypeScript types based on an OpenAPI V3 schema content.
4
5
  *
5
6
  * @param {string | OpenAPI3} schema - The OpenAPI V3 schema to generate types from. The value can be a string
6
7
  * representing JSON or YAML format, or an already parsed OpenAPI3 object.
8
+ * @param {GenerateTypesOptions} [options] - Options controlling how the types are emitted.
7
9
  * @return {Promise<string>} A promise that resolves to a string containing the generated TypeScript types.
8
10
  */
9
- export declare function generateTypes(schema: string | OpenAPI3): Promise<string>;
11
+ export declare function generateTypes(schema: string | OpenAPI3, options?: GenerateTypesOptions): Promise<string>;
@@ -46,15 +46,16 @@ const constants_1 = require("../constants");
46
46
  *
47
47
  * @param {string | OpenAPI3} schema - The OpenAPI V3 schema to generate types from. The value can be a string
48
48
  * representing JSON or YAML format, or an already parsed OpenAPI3 object.
49
+ * @param {GenerateTypesOptions} [options] - Options controlling how the types are emitted.
49
50
  * @return {Promise<string>} A promise that resolves to a string containing the generated TypeScript types.
50
51
  */
51
- async function generateTypes(schema) {
52
- // The schema is copied before the shared enums are hoisted into it, so the
53
- // caller keeps the schema it passed in unchanged
52
+ async function generateTypes(schema, options = {}) {
53
+ // The schema is copied before the shared definitions are hoisted into it, so
54
+ // the caller keeps the schema it passed in unchanged
54
55
  const schemaObject = typeof schema === 'string'
55
56
  ? await (0, utils_1.toSchemaObject)(schema)
56
57
  : structuredClone(schema);
57
- const sharedEnums = (0, utils_1.hoistSharedEnums)(schemaObject);
58
+ const sharedTypes = (0, utils_1.hoistSharedTypes)(schemaObject);
58
59
  const ast = await (0, openapi_typescript_1.default)(schemaObject, {
59
60
  exportType: true,
60
61
  emptyObjectsUnknown: true,
@@ -91,10 +92,11 @@ async function generateTypes(schema) {
91
92
  }
92
93
  });
93
94
  let typesContent = (0, openapi_typescript_1.astToString)(deduplicateUnionConstituents(ast));
94
- typesContent = extractSchemaTypes(typesContent, sharedEnums);
95
+ typesContent = extractSchemaTypes(typesContent, sharedTypes);
95
96
  typesContent = combineModuleTypes(typesContent, schemaObject);
96
97
  typesContent = deduplicateExportedTypes(typesContent);
97
- return replaceFileUploadTypes(typesContent);
98
+ typesContent = replaceFileUploadTypes(typesContent);
99
+ return (0, utils_1.rewriteEnumsAsObjects)(typesContent, options.declaration);
98
100
  }
99
101
  /**
100
102
  * Extracts inline schema types from the generated `schemas` block.
@@ -108,12 +110,16 @@ async function generateTypes(schema) {
108
110
  *
109
111
  * And replaces the inline body in `schemas` with a reference to the new type.
110
112
  *
113
+ * The definitions holding the schemas shared between the other definitions are lifted the
114
+ * same way, whether they hold an object or, as the hoisted enums and filter values do, a
115
+ * union of their own.
116
+ *
111
117
  * @param {string} typeContent - The generated TypeScript type content as a string.
112
- * @param {string[]} sharedEnums - The names of the definitions holding the enums shared
118
+ * @param {string[]} sharedTypes - The names of the definitions holding the schemas shared
113
119
  * between the other definitions, whose references are replaced by the name alone.
114
120
  * @return {string} The transformed types content with extracted schema types.
115
121
  */
116
- function extractSchemaTypes(typeContent, sharedEnums = []) {
122
+ function extractSchemaTypes(typeContent, sharedTypes = []) {
117
123
  const extractedTypes = [];
118
124
  const typeNames = new Set();
119
125
  const entries = [];
@@ -179,8 +185,11 @@ function extractSchemaTypes(typeContent, sharedEnums = []) {
179
185
  }
180
186
  }
181
187
  updatedBaseContent += normalizedTypes.slice(cursor);
188
+ // Lift the shared definitions holding a union rather than an object, which the
189
+ // entries above leave in place (i.e. `QueryFilterValue: QueryFilterScalar | ...;`)
190
+ updatedBaseContent = extractSharedTypes(updatedBaseContent, sharedTypes, extractedTypes, typeNames);
182
191
  // Replace all cross-references like components["schemas"]["def-89"] with the type name
183
- const references = new Map(sharedEnums.map((name) => [`"${name}"`, name]));
192
+ const references = new Map(sharedTypes.map((name) => [`"${name}"`, name]));
184
193
  for (const [defKey, typeName] of defToTypeName) {
185
194
  references.set(defKey, typeName);
186
195
  }
@@ -215,6 +224,87 @@ function extractSchemaTypes(typeContent, sharedEnums = []) {
215
224
  const mergedExtractedTypes = stripFormat(joined.replace(doubleCommentsRegex, '$1*'));
216
225
  return updatedBaseContent + '\n' + mergedExtractedTypes;
217
226
  }
227
+ /**
228
+ * Lifts the shared definitions holding a type of their own out of the generated `schemas`
229
+ * block, so the type they hold is declared once and referenced by name everywhere else.
230
+ *
231
+ * Finds entries like:
232
+ * QueryFilterValue: QueryFilterScalar | QueryFilterScalar[] | QueryCondition;
233
+ *
234
+ * Lifts each one into:
235
+ * export type QueryFilterValue = QueryFilterScalar | QueryFilterScalar[] | QueryCondition;
236
+ *
237
+ * The definitions holding nothing but a reference to a type declared elsewhere, as the hoisted
238
+ * enums do, are left alone.
239
+ *
240
+ * @param {string} content - The generated types, with the object definitions already extracted.
241
+ * @param {string[]} names - The names of the shared definitions to lift.
242
+ * @param {string[]} extractedTypes - The extracted types, appended to for every lifted entry.
243
+ * @param {Set<string>} typeNames - The names already extracted, added to for every lifted entry.
244
+ * @return {string} The types with the body of every lifted entry replaced by its name.
245
+ */
246
+ function extractSharedTypes(content, names, extractedTypes, typeNames) {
247
+ // The definitions live in the components block, so a property named after one of them
248
+ // elsewhere in the types is never mistaken for its declaration
249
+ const componentsStart = content.indexOf('export type components');
250
+ if (componentsStart < 0) {
251
+ return content;
252
+ }
253
+ for (const name of names) {
254
+ if (typeNames.has(name)) {
255
+ continue;
256
+ }
257
+ // The entry starts on a line of its own, optionally preceded by the JSDoc header
258
+ // holding the name of the definition
259
+ const entry = new RegExp(String.raw `\n[ \t]*(?:\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*)?${name}: `).exec(content.slice(componentsStart));
260
+ if (!entry) {
261
+ continue;
262
+ }
263
+ const start = componentsStart + entry.index + entry[0].length;
264
+ const end = findTypeEnd(content, start);
265
+ const body = content.slice(start, end).trim();
266
+ // A definition referencing a type declared elsewhere holds nothing to lift
267
+ if (!body || body === name) {
268
+ continue;
269
+ }
270
+ typeNames.add(name);
271
+ extractedTypes.push(`export type ${name} = ${body};\n`);
272
+ content = content.slice(0, start) + name + content.slice(end);
273
+ }
274
+ return content;
275
+ }
276
+ /**
277
+ * Finds the end of the type starting at the given index, which is the first semicolon that is
278
+ * not nested inside braces, brackets, parentheses or a string literal.
279
+ *
280
+ * @param {string} content - The content holding the type.
281
+ * @param {number} start - The index the type starts at.
282
+ * @return {number} The index of the semicolon terminating the type.
283
+ */
284
+ function findTypeEnd(content, start) {
285
+ const closing = { '{': '}', '[': ']', '(': ')' };
286
+ const stack = [];
287
+ for (let i = start; i < content.length; i++) {
288
+ const character = content[i];
289
+ if (character === '"' || character === "'") {
290
+ i = content.indexOf(character, i + 1);
291
+ if (i < 0) {
292
+ return content.length;
293
+ }
294
+ continue;
295
+ }
296
+ if (closing[character]) {
297
+ stack.push(closing[character]);
298
+ }
299
+ else if (character === stack[stack.length - 1]) {
300
+ stack.pop();
301
+ }
302
+ else if (character === ';' && stack.length === 0) {
303
+ return i;
304
+ }
305
+ }
306
+ return content.length;
307
+ }
218
308
  /**
219
309
  * Deduplicates and simplifies union type constituents in the provided TypeScript Abstract Syntax Tree (AST).
220
310
  * Ensures that duplicate constituents are removed and shared types across parenthesized union branches are hoisted.
@@ -0,0 +1,6 @@
1
+ /** Options controlling how the TypeScript types are generated from an OpenAPI schema. */
2
+ export type GenerateTypesOptions = {
3
+ /** Whether the types are written into a declaration (`.d.ts`) file, which holds no runtime
4
+ * values and so declares the enum constants instead of initializing them. (default: false) */
5
+ declaration?: boolean;
6
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1 +1,2 @@
1
+ export * from './generator';
1
2
  export * from './routes';
@@ -14,4 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./generator"), exports);
17
18
  __exportStar(require("./routes"), exports);
@@ -1,22 +1,22 @@
1
- import { OpenAPI3 } from 'openapi-typescript';
2
1
  /**
3
- * Hoists the inline enums a schema repeats across its definitions into shared definitions
4
- * of their own, replacing every occurrence with a reference to the hoisted definition.
2
+ * Rewrites the TypeScript enums of the generated types into a constant object holding the
3
+ * members and a type alias of their values, so both forms are accepted wherever the enum is
4
+ * used (i.e. `SortDirection.asc` and the plain `'asc'` literal alike).
5
5
  *
6
- * An Appweaver schema declares the same enum inline over and over, once per property that
7
- * accepts it, and each of them would otherwise become an enum of its own in the generated
8
- * types (i.e. a separate `asc | desc` enum for every sortable field of every resource).
6
+ * A TypeScript `enum` declares a nominal type, which rejects the very literals it is built
7
+ * from, forcing users of the generated client to import the enum for a value the API
8
+ * documents as a string. The rewritten declaration keeps the member access working while
9
+ * typing the property as the union of its values:
9
10
  *
10
- * Only enums that are byte for byte identical are hoisted, so no property loses its
11
- * description, example or nullability along the way. The hoisted definition is named after
12
- * {@link SHARED_ENUM_NAMES} when its values are well known, and otherwise after the part
13
- * the declaring definitions have in common followed by the property name (i.e. `PostCreate`
14
- * and `PostSingle` declaring `status` give `PostStatus`). Enums whose name cannot be
15
- * resolved, or whose name is already taken, are left inline.
11
+ * ```ts
12
+ * export const SortDirection = { asc: 'asc', desc: 'desc' } as const;
13
+ * export type SortDirection = (typeof SortDirection)[keyof typeof SortDirection];
14
+ * ```
16
15
  *
17
- * The given schema is mutated in place.
18
- *
19
- * @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared enums of.
20
- * @return {string[]} The names of the hoisted definitions, in the order they were created.
16
+ * @param {string} content The generated TypeScript types to rewrite the enums of.
17
+ * @param {boolean} [declaration=false] Whether the types are emitted into a declaration
18
+ * (`.d.ts`) file, which holds no runtime values and so declares the constant instead of
19
+ * initializing it.
20
+ * @return {string} The types with every enum declaration rewritten.
21
21
  */
22
- export declare function hoistSharedEnums(schema: OpenAPI3): string[];
22
+ export declare function rewriteEnumsAsObjects(content: string, declaration?: boolean): string;
@@ -1,179 +1,59 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hoistSharedEnums = hoistSharedEnums;
4
- const constants_1 = require("../constants");
3
+ exports.rewriteEnumsAsObjects = rewriteEnumsAsObjects;
5
4
  /**
6
- * Hoists the inline enums a schema repeats across its definitions into shared definitions
7
- * of their own, replacing every occurrence with a reference to the hoisted definition.
8
- *
9
- * An Appweaver schema declares the same enum inline over and over, once per property that
10
- * accepts it, and each of them would otherwise become an enum of its own in the generated
11
- * types (i.e. a separate `asc | desc` enum for every sortable field of every resource).
12
- *
13
- * Only enums that are byte for byte identical are hoisted, so no property loses its
14
- * description, example or nullability along the way. The hoisted definition is named after
15
- * {@link SHARED_ENUM_NAMES} when its values are well known, and otherwise after the part
16
- * the declaring definitions have in common followed by the property name (i.e. `PostCreate`
17
- * and `PostSingle` declaring `status` give `PostStatus`). Enums whose name cannot be
18
- * resolved, or whose name is already taken, are left inline.
19
- *
20
- * The given schema is mutated in place.
21
- *
22
- * @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared enums of.
23
- * @return {string[]} The names of the hoisted definitions, in the order they were created.
24
- */
25
- function hoistSharedEnums(schema) {
26
- const definitions = schema.components?.schemas;
27
- if (!definitions) {
28
- return [];
29
- }
30
- // Enums declared by identical schema content, keyed by that content
31
- const groups = new Map();
32
- for (const [key, definition] of Object.entries(definitions)) {
33
- if (definition && typeof definition === 'object') {
34
- const owner = definition['title'];
35
- collectEnums(definition, typeof owner === 'string' ? owner : key, '', groups);
36
- }
37
- }
38
- const takenNames = new Set(Object.keys(definitions));
39
- for (const definition of Object.values(definitions)) {
40
- const title = definition?.['title'];
41
- if (typeof title === 'string') {
42
- takenNames.add(title);
43
- }
44
- }
45
- const hoistedNames = [];
46
- for (const [content, occurrences] of groups) {
47
- if (occurrences.length < 2) {
48
- continue;
49
- }
50
- const name = resolveEnumName(JSON.parse(content), occurrences);
51
- if (!name || takenNames.has(name)) {
52
- continue;
53
- }
54
- takenNames.add(name);
55
- hoistedNames.push(name);
56
- definitions[name] = { title: name, ...JSON.parse(content) };
57
- for (const { container, key } of occurrences) {
58
- container[key] = {
59
- $ref: `#/components/schemas/${name}`
60
- };
61
- }
62
- }
63
- return hoistedNames;
64
- }
65
- /**
66
- * Collects every inline enum declared below the given schema node into the groups map,
67
- * keyed by the stable serialization of the enum schema, so identical enums group together.
68
- *
69
- * @param {SchemaContainer} container The schema node to collect the enums of.
70
- * @param {string} owner The name of the definition the node belongs to.
71
- * @param {string} property The name of the property the node is declared under, if any.
72
- * @param {Map<string, EnumOccurrence[]>} groups The collected occurrences, grouped by content.
73
- */
74
- function collectEnums(container, owner, property, groups) {
75
- const entries = Array.isArray(container)
76
- ? container.map((value, index) => [index, value])
77
- : Object.entries(container);
78
- for (const [key, value] of entries) {
79
- if (!value || typeof value !== 'object') {
80
- continue;
81
- }
82
- // The keys of a `properties` object name the schemas below them, everything
83
- // else keeps the property name of the node it was reached through
84
- if (key === 'properties' && !Array.isArray(value)) {
85
- const properties = value;
86
- for (const name of Object.keys(properties)) {
87
- visitSchema(properties, name, owner, name, groups);
88
- }
89
- continue;
90
- }
91
- visitSchema(container, key, owner, property, groups);
92
- }
93
- }
94
- /**
95
- * Records the schema held by the container under the given key as an enum occurrence, or
96
- * descends into it when it declares no enum of its own.
97
- *
98
- * @param {SchemaContainer} container The container holding the schema node.
99
- * @param {string | number} key The key the schema node is held under.
100
- * @param {string} owner The name of the definition the node belongs to.
101
- * @param {string} property The name of the property the node is declared under, if any.
102
- * @param {Map<string, EnumOccurrence[]>} groups The collected occurrences, grouped by content.
103
- */
104
- function visitSchema(container, key, owner, property, groups) {
105
- const node = container[key];
106
- if (Array.isArray(node['enum'])) {
107
- const content = stableStringify(node);
108
- const occurrences = groups.get(content) ?? [];
109
- occurrences.push({ container, key, owner, property });
110
- groups.set(content, occurrences);
111
- return;
112
- }
113
- collectEnums(node, owner, property, groups);
114
- }
115
- /**
116
- * Resolves the name of the definition an enum is hoisted into, preferring the name given to
117
- * its values by {@link SHARED_ENUM_NAMES} and falling back to the common part of the names of
118
- * the definitions declaring it, followed by the property name they all declare it under.
119
- *
120
- * @param {Record<string, unknown>} node The enum schema to resolve the name of.
121
- * @param {EnumOccurrence[]} occurrences The places the enum is declared in.
122
- * @return {string | undefined} The resolved name, or undefined when the enum has no name to
123
- * be hoisted under.
124
- */
125
- function resolveEnumName(node, occurrences) {
126
- const values = node['enum'];
127
- const knownName = constants_1.SHARED_ENUM_NAMES[values.join('|')];
128
- if (knownName) {
129
- return knownName;
130
- }
131
- const { property } = occurrences[0];
132
- if (!property || occurrences.some((o) => o.property !== property)) {
133
- return undefined;
134
- }
135
- const prefix = commonNamePrefix(occurrences.map((o) => o.owner));
136
- if (!prefix) {
137
- return undefined;
138
- }
139
- return prefix + property.charAt(0).toUpperCase() + property.slice(1);
140
- }
141
- /**
142
- * Resolves the longest prefix the given names share, cut at a word boundary so the result
143
- * stays a readable name (i.e. `HealthCheckResponse` and `HealthCheckResult` give
144
- * `HealthCheck` rather than `HealthCheckRes`).
145
- *
146
- * @param {string[]} names The names to find the common prefix of.
147
- * @return {string} The common prefix, empty when the names start with different words.
5
+ * Rewrites the TypeScript enums of the generated types into a constant object holding the
6
+ * members and a type alias of their values, so both forms are accepted wherever the enum is
7
+ * used (i.e. `SortDirection.asc` and the plain `'asc'` literal alike).
8
+ *
9
+ * A TypeScript `enum` declares a nominal type, which rejects the very literals it is built
10
+ * from, forcing users of the generated client to import the enum for a value the API
11
+ * documents as a string. The rewritten declaration keeps the member access working while
12
+ * typing the property as the union of its values:
13
+ *
14
+ * ```ts
15
+ * export const SortDirection = { asc: 'asc', desc: 'desc' } as const;
16
+ * export type SortDirection = (typeof SortDirection)[keyof typeof SortDirection];
17
+ * ```
18
+ *
19
+ * @param {string} content The generated TypeScript types to rewrite the enums of.
20
+ * @param {boolean} [declaration=false] Whether the types are emitted into a declaration
21
+ * (`.d.ts`) file, which holds no runtime values and so declares the constant instead of
22
+ * initializing it.
23
+ * @return {string} The types with every enum declaration rewritten.
148
24
  */
149
- function commonNamePrefix(names) {
150
- const wordsPerName = names.map((name) => name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+|[A-Z]/g) ?? []);
151
- const words = [];
152
- for (let i = 0; i < wordsPerName[0].length; i++) {
153
- const word = wordsPerName[0][i];
154
- if (wordsPerName.some((other) => other[i] !== word)) {
155
- break;
156
- }
157
- words.push(word);
158
- }
159
- return words.join('');
25
+ function rewriteEnumsAsObjects(content, declaration = false) {
26
+ return content.replace(/export enum (\w+) \{([^}]*)}/g, (match, name, body) => {
27
+ const members = parseEnumMembers(body);
28
+ if (members.length === 0) {
29
+ return match;
30
+ }
31
+ const values = members
32
+ .map(([member, value]) => declaration
33
+ ? ` readonly ${member}: ${value};`
34
+ : ` ${member}: ${value},`)
35
+ .join('\n');
36
+ const constant = declaration
37
+ ? `export declare const ${name}: {\n${values}\n};`
38
+ : `export const ${name} = {\n${values}\n} as const;`;
39
+ return `${constant}\nexport type ${name} = (typeof ${name})[keyof typeof ${name}];`;
40
+ });
160
41
  }
161
42
  /**
162
- * Serializes a value with its object keys sorted, so two schemas holding the same content
163
- * in a different key order serialize alike.
43
+ * Parses the members of an enum declaration body into their name and value pairs, keeping
44
+ * the value verbatim so a quoted member name or a numeric value survives the rewrite.
164
45
  *
165
- * @param {unknown} value The value to serialize.
166
- * @return {string} The stable JSON serialization of the value.
46
+ * @param {string} body The body of the enum declaration, without the enclosing braces.
47
+ * @return {[string, string][]} The name and value of every member, in declaration order.
167
48
  */
168
- function stableStringify(value) {
169
- if (Array.isArray(value)) {
170
- return `[${value.map(stableStringify).join(',')}]`;
171
- }
172
- if (value && typeof value === 'object') {
173
- const entries = Object.keys(value)
174
- .sort()
175
- .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
176
- return `{${entries.join(',')}}`;
177
- }
178
- return JSON.stringify(value) ?? 'null';
49
+ function parseEnumMembers(body) {
50
+ const members = [];
51
+ // A member name, quoted or not, followed by its value, which is a quoted string
52
+ // (holding a comma of its own or not) or a number
53
+ const pattern = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[\w$]+)\s*=\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|-?[\d.]+)/g;
54
+ let member;
55
+ while ((member = pattern.exec(body)) !== null) {
56
+ members.push([member[1], member[2]]);
57
+ }
58
+ return members;
179
59
  }
@@ -0,0 +1,29 @@
1
+ import { OpenAPI3 } from 'openapi-typescript';
2
+ /**
3
+ * Hoists the schemas an OpenAPI document repeats inline across its definitions into shared
4
+ * definitions of their own, replacing every occurrence with a reference to the hoisted
5
+ * definition.
6
+ *
7
+ * An Appweaver schema declares the same shapes over and over, once per property that accepts
8
+ * them, and each of them would otherwise be spelled out again in the generated types (i.e. the
9
+ * `asc | desc` enum of every sortable field, or the accepted value of every filterable field of
10
+ * every resource). Two kinds of schema are hoisted:
11
+ *
12
+ * - The well known shapes of {@link SHARED_SCHEMA_SHAPES}, matched in the order they are
13
+ * declared so an outer shape is hoisted before the shapes nested inside it.
14
+ * - The enums the definitions repeat, named after {@link SHARED_ENUM_NAMES} when their values
15
+ * are well known, and otherwise after the part the declaring definitions have in common
16
+ * followed by the property name (i.e. `PostCreate` and `PostSingle` declaring `status` give
17
+ * `PostStatus`). Enums whose name cannot be resolved are left inline.
18
+ *
19
+ * Only the structure of a schema decides whether it is hoisted, and every occurrence keeps the
20
+ * documentation it carries, so no property loses its description or example along the way.
21
+ *
22
+ * A schema declared only once, or whose name is already taken, is left inline as well.
23
+ *
24
+ * The given schema is mutated in place.
25
+ *
26
+ * @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared definitions of.
27
+ * @return {string[]} The names of the hoisted definitions, in the order they were created.
28
+ */
29
+ export declare function hoistSharedTypes(schema: OpenAPI3): string[];