swaggie 1.9.0-dev.1 → 2.0.0
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/README.md +117 -31
- package/dist/browser.js +24 -5
- package/dist/cli.js +54 -7
- package/dist/gen/genMocks.js +453 -0
- package/dist/gen/genOperations.js +112 -20
- package/dist/gen/genTypes.js +6 -5
- package/dist/gen/header.js +0 -1
- package/dist/gen/index.js +14 -1
- package/dist/generated/bundledTemplates.js +17 -19
- package/dist/index.js +17 -1
- package/dist/swagger/operations.js +16 -7
- package/dist/swagger/typesExtractor.js +12 -11
- package/dist/types.d.ts +55 -5
- package/dist/utils/documentLoader.js +1 -3
- package/dist/utils/fileUtils.js +22 -0
- package/dist/utils/refResolver.js +9 -21
- package/dist/utils/templateEngine.js +18 -0
- package/dist/utils/templateManager.js +123 -14
- package/dist/utils/templateValidator.js +127 -0
- package/dist/utils/utils.js +19 -13
- package/package.json +5 -4
- package/templates/axios/operation.ejs +25 -21
- package/templates/fetch/operation.ejs +4 -0
- package/templates/ng1/operation.ejs +11 -3
- package/templates/ng2/baseClient.ejs +9 -49
- package/templates/ng2/client.ejs +3 -7
- package/templates/ng2/operation.ejs +34 -17
- package/templates/swr/baseClient.ejs +7 -0
- package/templates/swr/client.ejs +63 -0
- package/templates/swr/swrMutationOperation.ejs +32 -0
- package/templates/swr/swrOperation.ejs +18 -0
- package/templates/tsq/baseClient.ejs +1 -0
- package/templates/tsq/client.ejs +67 -0
- package/templates/tsq/mutationOperation.ejs +31 -0
- package/templates/tsq/queryOperation.ejs +19 -0
- package/templates/xior/operation.ejs +25 -21
- package/templates/swr-axios/barrel.ejs +0 -58
- package/templates/swr-axios/baseClient.ejs +0 -20
- package/templates/swr-axios/client.ejs +0 -21
- package/templates/swr-axios/operation.ejs +0 -40
- package/templates/swr-axios/swrOperation.ejs +0 -50
- package/templates/tsq-xior/barrel.ejs +0 -0
- package/templates/tsq-xior/baseClient.ejs +0 -14
- package/templates/tsq-xior/client.ejs +0 -22
- package/templates/tsq-xior/operation.ejs +0 -40
- package/templates/tsq-xior/queryOperation.ejs +0 -30
package/dist/utils/fileUtils.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});var _nodefs = require('node:fs');
|
|
2
2
|
var _nodepath = require('node:path');
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Computes the relative import path from `fromFile` to `toFile`, stripping the
|
|
6
|
+
* file extension and ensuring the result always starts with `./` or `../`.
|
|
7
|
+
*
|
|
8
|
+
* @param fromFile - The file that will contain the import statement (e.g. the mock file)
|
|
9
|
+
* @param toFile - The file being imported (e.g. the generated API client file)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* deriveRelativeImport('src/__mocks__/api.ts', 'src/generated/api.ts')
|
|
13
|
+
* // → '../generated/api'
|
|
14
|
+
*
|
|
15
|
+
* deriveRelativeImport('src/api.mock.ts', 'src/api.ts')
|
|
16
|
+
* // → './api'
|
|
17
|
+
*/
|
|
18
|
+
function deriveRelativeImport(fromFile, toFile) {
|
|
19
|
+
const rel = _nodepath.relative.call(void 0, _nodepath.dirname.call(void 0, fromFile), toFile);
|
|
20
|
+
const withoutExt = rel.replace(/\.[jt]sx?$/i, '');
|
|
21
|
+
// Normalise backslashes on Windows and ensure a leading ./
|
|
22
|
+
const normalised = withoutExt.replace(/\\/g, '/');
|
|
23
|
+
return normalised.startsWith('.') ? normalised : './' + normalised;
|
|
24
|
+
} exports.deriveRelativeImport = deriveRelativeImport;
|
|
25
|
+
|
|
4
26
|
function saveFile(filePath, contents) {
|
|
5
27
|
return new Promise((resolve, reject) => {
|
|
6
28
|
_nodefs.mkdir.call(void 0, _nodepath.dirname.call(void 0, filePath), { recursive: true }, (err) => {
|
|
@@ -5,10 +5,6 @@ var _yaml = require('yaml');
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
8
|
const SUPPORTED_COMPONENT_SECTIONS = new Set([
|
|
13
9
|
'schemas',
|
|
14
10
|
'parameters',
|
|
@@ -26,8 +22,6 @@ const SUPPORTED_COMPONENT_SECTIONS = new Set([
|
|
|
26
22
|
|
|
27
23
|
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
25
|
/**
|
|
32
26
|
* Resolves external file refs into local component refs.
|
|
33
27
|
* For now we only support local file refs (no http/https) and component targets.
|
|
@@ -109,9 +103,7 @@ async function resolveRef(
|
|
|
109
103
|
}
|
|
110
104
|
|
|
111
105
|
if (!fragment) {
|
|
112
|
-
throw new Error(
|
|
113
|
-
`External refs must include a JSON pointer fragment: '${rawRef}'`
|
|
114
|
-
);
|
|
106
|
+
throw new Error(`External refs must include a JSON pointer fragment: '${rawRef}'`);
|
|
115
107
|
}
|
|
116
108
|
|
|
117
109
|
if (!fragment.startsWith('/')) {
|
|
@@ -136,9 +128,7 @@ async function importRefFromFile(
|
|
|
136
128
|
|
|
137
129
|
if (!targetInfo) {
|
|
138
130
|
if (context.resolvingRefs.has(importKey)) {
|
|
139
|
-
throw new Error(
|
|
140
|
-
`Circular non-component external ref is not supported: '${rawRef}'`
|
|
141
|
-
);
|
|
131
|
+
throw new Error(`Circular non-component external ref is not supported: '${rawRef}'`);
|
|
142
132
|
}
|
|
143
133
|
|
|
144
134
|
const targetCopy = structuredClone(target);
|
|
@@ -172,10 +162,7 @@ async function importRefFromFile(
|
|
|
172
162
|
}
|
|
173
163
|
|
|
174
164
|
function parseImportTarget(pointer) {
|
|
175
|
-
const parts = pointer
|
|
176
|
-
.replace(/^#\//, '')
|
|
177
|
-
.split('/')
|
|
178
|
-
.map(unescapePointerSegment);
|
|
165
|
+
const parts = pointer.replace(/^#\//, '').split('/').map(unescapePointerSegment);
|
|
179
166
|
|
|
180
167
|
if (parts.length === 2) {
|
|
181
168
|
const [legacySection, name] = parts;
|
|
@@ -250,12 +237,13 @@ function getOrCreateComponentAlias(
|
|
|
250
237
|
return candidate;
|
|
251
238
|
}
|
|
252
239
|
|
|
253
|
-
async function getValueByPointer(
|
|
240
|
+
async function getValueByPointer(
|
|
241
|
+
filePath,
|
|
242
|
+
pointer,
|
|
243
|
+
context
|
|
244
|
+
) {
|
|
254
245
|
const doc = await loadDocument(filePath, context);
|
|
255
|
-
const parts = pointer
|
|
256
|
-
.replace(/^#\//, '')
|
|
257
|
-
.split('/')
|
|
258
|
-
.map(unescapePointerSegment);
|
|
246
|
+
const parts = pointer.replace(/^#\//, '').split('/').map(unescapePointerSegment);
|
|
259
247
|
|
|
260
248
|
let current = doc;
|
|
261
249
|
for (const part of parts) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _eta = require('eta');
|
|
2
2
|
|
|
3
3
|
let engine;
|
|
4
|
+
let loadedFiles = null;
|
|
4
5
|
|
|
5
6
|
function initTemplateEngineFromDirectory(templatesDir) {
|
|
6
7
|
engine = new (0, _eta.Eta)({ views: templatesDir });
|
|
8
|
+
loadedFiles = null;
|
|
7
9
|
} exports.initTemplateEngineFromDirectory = initTemplateEngineFromDirectory;
|
|
8
10
|
|
|
9
11
|
function initTemplateEngineFromBundled(templateFiles) {
|
|
@@ -18,8 +20,24 @@ let engine;
|
|
|
18
20
|
const baseName = _nullishCoalesce(_optionalChain([templatePath, 'access', _ => _.split, 'call', _2 => _2('/'), 'access', _3 => _3.pop, 'call', _4 => _4(), 'optionalAccess', _5 => _5.split, 'call', _6 => _6('\\'), 'access', _7 => _7.pop, 'call', _8 => _8()]), () => ( templatePath));
|
|
19
21
|
return templateFiles[baseName];
|
|
20
22
|
};
|
|
23
|
+
loadedFiles = templateFiles;
|
|
21
24
|
} exports.initTemplateEngineFromBundled = initTemplateEngineFromBundled;
|
|
22
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Returns true if the given template file is available in the currently loaded
|
|
28
|
+
* bundled template store. Always returns false when using a directory-based
|
|
29
|
+
* engine (filesystem templates are checked via renderFile directly).
|
|
30
|
+
*/
|
|
31
|
+
function hasTemplateFile(templateFile) {
|
|
32
|
+
if (!loadedFiles) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return (
|
|
36
|
+
templateFile in loadedFiles ||
|
|
37
|
+
Object.keys(loadedFiles).some((k) => _optionalChain([k, 'access', _9 => _9.split, 'call', _10 => _10('/'), 'access', _11 => _11.pop, 'call', _12 => _12(), 'optionalAccess', _13 => _13.split, 'call', _14 => _14('\\'), 'access', _15 => _15.pop, 'call', _16 => _16()]) === templateFile)
|
|
38
|
+
);
|
|
39
|
+
} exports.hasTemplateFile = hasTemplateFile;
|
|
40
|
+
|
|
23
41
|
/**
|
|
24
42
|
* Get's a template file and renders it with the provided data.
|
|
25
43
|
*/
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _nodefs = require('node:fs'); var _nodefs2 = _interopRequireDefault(_nodefs);
|
|
2
2
|
var _nodepath = require('node:path'); var _nodepath2 = _interopRequireDefault(_nodepath);
|
|
3
3
|
var _templateEngine = require('./templateEngine');
|
|
4
|
+
|
|
5
|
+
|
|
4
6
|
let bundledTemplates = null;
|
|
5
7
|
|
|
6
8
|
|
|
@@ -9,34 +11,141 @@ let bundledTemplates = null;
|
|
|
9
11
|
bundledTemplates = templates;
|
|
10
12
|
} exports.setBundledTemplates = setBundledTemplates;
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Loads template files for the given template spec and initializes the
|
|
16
|
+
* template engine.
|
|
17
|
+
*
|
|
18
|
+
* Accepts:
|
|
19
|
+
* - A single template name or filesystem path (L1 or custom).
|
|
20
|
+
* - A [L2, L1] tuple: the two template sets are merged, with L2 winning on
|
|
21
|
+
* filename conflicts except for `baseClient.ejs` which is stored as
|
|
22
|
+
* `baseClientL2.ejs` so that both L1 and L2 base clients can be rendered.
|
|
23
|
+
*/
|
|
24
|
+
function loadAllTemplateFiles(template) {
|
|
25
|
+
if (!template) {
|
|
14
26
|
throw new Error('No template name was provided');
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
if (
|
|
29
|
+
if (Array.isArray(template)) {
|
|
30
|
+
const [l2, l1] = template;
|
|
31
|
+
const l1Files = resolveTemplateFiles(l1);
|
|
32
|
+
const l2Files = resolveTemplateFiles(l2);
|
|
33
|
+
|
|
34
|
+
if (!l1Files || !l2Files) {
|
|
35
|
+
// At least one side was a filesystem path — use directory overlay
|
|
36
|
+
loadCompositeFromFilesystem(l2, l1);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Both sides are bundled: merge in memory
|
|
41
|
+
const merged = mergeTemplateFiles(l1Files, l2Files);
|
|
42
|
+
_templateEngine.initTemplateEngineFromBundled.call(void 0, merged);
|
|
18
43
|
return;
|
|
19
44
|
}
|
|
20
45
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!_nodefs2.default.existsSync(templatesDir)) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`Could not find directory with the template (we tried ${templatesDir}). Is the template name correct?`
|
|
28
|
-
);
|
|
46
|
+
// Single template
|
|
47
|
+
if (!loadFromBundledTemplates(template)) {
|
|
48
|
+
loadFromFilesystem(template);
|
|
29
49
|
}
|
|
30
|
-
_templateEngine.initTemplateEngineFromDirectory.call(void 0, templatesDir);
|
|
31
50
|
} exports.loadAllTemplateFiles = loadAllTemplateFiles;
|
|
32
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Merges L1 and L2 file sets. L2 wins on filename conflicts, EXCEPT that
|
|
54
|
+
* `baseClient.ejs` from L2 is stored under the key `baseClientL2.ejs` so
|
|
55
|
+
* both L1 and L2 base clients can be rendered independently.
|
|
56
|
+
*/
|
|
57
|
+
function mergeTemplateFiles(
|
|
58
|
+
l1Files,
|
|
59
|
+
l2Files
|
|
60
|
+
) {
|
|
61
|
+
const merged = { ...l1Files };
|
|
62
|
+
|
|
63
|
+
for (const [name, content] of Object.entries(l2Files)) {
|
|
64
|
+
if (name === 'baseClient.ejs') {
|
|
65
|
+
merged['baseClientL2.ejs'] = content;
|
|
66
|
+
} else {
|
|
67
|
+
merged[name] = content;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return merged;
|
|
72
|
+
} exports.mergeTemplateFiles = mergeTemplateFiles;
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Private helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns the bundled file map for a named template, or null if the name
|
|
80
|
+
* refers to a filesystem path (i.e. the bundled store does not have it).
|
|
81
|
+
*/
|
|
82
|
+
function resolveTemplateFiles(templateName) {
|
|
83
|
+
const bundled = _optionalChain([bundledTemplates, 'optionalAccess', _ => _[templateName]]);
|
|
84
|
+
if (bundled) return bundled;
|
|
85
|
+
|
|
86
|
+
// Could be a bundled name that just isn't in the store yet, or a filesystem
|
|
87
|
+
// path. Return null to signal "use filesystem".
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
33
91
|
function loadFromBundledTemplates(templateName) {
|
|
34
|
-
const templateFiles = _optionalChain([bundledTemplates, 'optionalAccess',
|
|
92
|
+
const templateFiles = _optionalChain([bundledTemplates, 'optionalAccess', _2 => _2[templateName]]);
|
|
35
93
|
if (!templateFiles) {
|
|
36
94
|
return false;
|
|
37
95
|
}
|
|
38
96
|
|
|
39
97
|
_templateEngine.initTemplateEngineFromBundled.call(void 0, templateFiles);
|
|
40
|
-
|
|
41
98
|
return true;
|
|
42
99
|
}
|
|
100
|
+
|
|
101
|
+
function loadFromFilesystem(templateName) {
|
|
102
|
+
const templatesDir = resolveDir(templateName);
|
|
103
|
+
_templateEngine.initTemplateEngineFromDirectory.call(void 0, templatesDir);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* For composite [L2, L1] pairs where at least one side lives on the
|
|
108
|
+
* filesystem: read both directories into memory, merge, and initialize a
|
|
109
|
+
* bundled-style engine so the merge logic is uniform.
|
|
110
|
+
*/
|
|
111
|
+
function loadCompositeFromFilesystem(l2, l1) {
|
|
112
|
+
const l1Files = getFilesFromSource(l1);
|
|
113
|
+
const l2Files = getFilesFromSource(l2);
|
|
114
|
+
const merged = mergeTemplateFiles(l1Files, l2Files);
|
|
115
|
+
_templateEngine.initTemplateEngineFromBundled.call(void 0, merged);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Reads all `.ejs` files from a template source (bundled store or filesystem
|
|
120
|
+
* directory) into a plain `Record<filename, content>` map.
|
|
121
|
+
*/
|
|
122
|
+
function getFilesFromSource(templateName) {
|
|
123
|
+
// Prefer bundled store
|
|
124
|
+
const bundled = _optionalChain([bundledTemplates, 'optionalAccess', _3 => _3[templateName]]);
|
|
125
|
+
if (bundled) return bundled;
|
|
126
|
+
|
|
127
|
+
const dir = resolveDir(templateName);
|
|
128
|
+
return readEjsFilesFromDir(dir);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveDir(templateName) {
|
|
132
|
+
const resolved = _nodefs2.default.existsSync(templateName)
|
|
133
|
+
? templateName
|
|
134
|
+
: _nodepath2.default.join(__dirname, '..', '..', 'templates', templateName);
|
|
135
|
+
|
|
136
|
+
if (!_nodefs2.default.existsSync(resolved)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Could not find directory with the template (we tried ${resolved}). Is the template name correct?`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return resolved;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readEjsFilesFromDir(dir) {
|
|
145
|
+
const files = _nodefs2.default.readdirSync(dir).filter((f) => f.endsWith('.ejs'));
|
|
146
|
+
const result = {};
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
result[file] = _nodefs2.default.readFileSync(_nodepath2.default.join(dir, file), 'utf8');
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
const L1_TEMPLATES = ['axios', 'fetch', 'xior', 'ng1', 'ng2'];
|
|
4
|
+
const L2_TEMPLATES = ['swr', 'tsq'];
|
|
5
|
+
|
|
6
|
+
/** L1 templates that are incompatible with any L2 (framework-specific clients) */
|
|
7
|
+
const L2_INCOMPATIBLE_L1 = ['ng1', 'ng2'];
|
|
8
|
+
|
|
9
|
+
/** Legacy composite template names and their replacements */
|
|
10
|
+
const LEGACY_TEMPLATES = {
|
|
11
|
+
'swr-axios': ['swr', 'axios'],
|
|
12
|
+
'tsq-xior': ['tsq', 'xior'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Default L1 to use when only an L2 template is specified */
|
|
16
|
+
const DEFAULT_L1_FOR_L2 = 'fetch';
|
|
17
|
+
|
|
18
|
+
function isL1Template(name) {
|
|
19
|
+
return (L1_TEMPLATES ).includes(name);
|
|
20
|
+
} exports.isL1Template = isL1Template;
|
|
21
|
+
|
|
22
|
+
function isL2Template(name) {
|
|
23
|
+
return (L2_TEMPLATES ).includes(name);
|
|
24
|
+
} exports.isL2Template = isL2Template;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validates that a raw template input is acceptable and throws a descriptive
|
|
28
|
+
* error if not. Does NOT normalize — call `normalizeTemplate` for that.
|
|
29
|
+
*
|
|
30
|
+
* Rules:
|
|
31
|
+
* - Legacy composite names ('swr-axios', 'tsq-xior') → migration error
|
|
32
|
+
* - Single L2 name → allowed (will be normalized to [L2, default-L1] later)
|
|
33
|
+
* - Single L1 name or custom path → allowed
|
|
34
|
+
* - Array with wrong element count → error
|
|
35
|
+
* - Array where first element is not an L2 → error
|
|
36
|
+
* - Array where second element is an incompatible L1 (ng1/ng2) → error
|
|
37
|
+
*/
|
|
38
|
+
function validateTemplate(template) {
|
|
39
|
+
if (Array.isArray(template)) {
|
|
40
|
+
const arr = template ;
|
|
41
|
+
if (arr.length !== 2) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Invalid template: array must have exactly 2 elements [L2, L1], e.g. ["swr", "axios"]. Got ${arr.length} element(s).`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [l2, l1] = template;
|
|
48
|
+
|
|
49
|
+
// First element must be an L2 template (or a custom path — we can't validate
|
|
50
|
+
// custom paths statically, so we only reject known-bad L1 names here).
|
|
51
|
+
if (isL1Template(l2)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Invalid template pair: "${l2}" is an L1 (HTTP client) template and cannot be used as the first element. ` +
|
|
54
|
+
`The first element must be an L2 template (${L2_TEMPLATES.join(', ')}) or a custom path. ` +
|
|
55
|
+
`To use "${l2}" alone, pass it as a string: template: "${l2}".`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isL2Template(l2) && isL1Template(l1) && (L2_INCOMPATIBLE_L1 ).includes(l1)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Invalid template pair: "${l1}" is a framework-specific client and is not compatible with L2 template "${l2}". ` +
|
|
62
|
+
`Compatible L1 templates for L2 are: ${L1_TEMPLATES.filter((t) => !L2_INCOMPATIBLE_L1.includes(t)).join(', ')}.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Single string
|
|
70
|
+
if (typeof template === 'string') {
|
|
71
|
+
const legacy = LEGACY_TEMPLATES[template];
|
|
72
|
+
if (legacy) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`"${template}" is no longer a valid template name. ` +
|
|
75
|
+
`It has been split into separate L1 and L2 templates. ` +
|
|
76
|
+
`Use template: ["${legacy[0]}", "${legacy[1]}"] instead.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
// Single L2, single L1, or custom path are all fine — L2 alone gets a
|
|
80
|
+
// default L1 applied during normalization.
|
|
81
|
+
}
|
|
82
|
+
} exports.validateTemplate = validateTemplate;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalizes a validated template input into a `ResolvedTemplate`:
|
|
86
|
+
* - Single L2 name → [L2, DEFAULT_L1_FOR_L2]
|
|
87
|
+
* - Everything else passes through unchanged.
|
|
88
|
+
*
|
|
89
|
+
* Call `validateTemplate` before this function.
|
|
90
|
+
*/
|
|
91
|
+
function normalizeTemplate(template) {
|
|
92
|
+
if (typeof template === 'string' && isL2Template(template)) {
|
|
93
|
+
return [template, DEFAULT_L1_FOR_L2];
|
|
94
|
+
}
|
|
95
|
+
return template ;
|
|
96
|
+
} exports.normalizeTemplate = normalizeTemplate;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns the effective L1 template name from a resolved template.
|
|
100
|
+
* For arrays, returns the second element; for single strings, returns the string.
|
|
101
|
+
*/
|
|
102
|
+
function getL1Template(template) {
|
|
103
|
+
return Array.isArray(template) ? template[1] : template;
|
|
104
|
+
} exports.getL1Template = getL1Template;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns the TypeScript type name for the `$httpConfig` parameter that the
|
|
108
|
+
* L1 template exposes on each operation method.
|
|
109
|
+
*
|
|
110
|
+
* Used by L2 operation partials so the `$httpConfig` parameter they forward
|
|
111
|
+
* to the underlying client method has the correct type regardless of which L1
|
|
112
|
+
* is paired with the L2.
|
|
113
|
+
*/
|
|
114
|
+
function getHttpConfigType(template) {
|
|
115
|
+
const l1 = getL1Template(template);
|
|
116
|
+
switch (l1) {
|
|
117
|
+
case 'axios':
|
|
118
|
+
return 'AxiosRequestConfig';
|
|
119
|
+
case 'xior':
|
|
120
|
+
return 'XiorRequestConfig';
|
|
121
|
+
case 'fetch':
|
|
122
|
+
return 'RequestInit';
|
|
123
|
+
default:
|
|
124
|
+
// Custom path or unknown — fall back to a permissive type
|
|
125
|
+
return 'Record<string, unknown>';
|
|
126
|
+
}
|
|
127
|
+
} exports.getHttpConfigType = getHttpConfigType;
|
package/dist/utils/utils.js
CHANGED
|
@@ -74,7 +74,7 @@ const reservedKeywords = new Set([
|
|
|
74
74
|
*/
|
|
75
75
|
function escapeIdentifier(name) {
|
|
76
76
|
if (!name) {
|
|
77
|
-
return name;
|
|
77
|
+
return _nullishCoalesce(name, () => ( ''));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
if (reservedKeywords.has(name) || /^[0-9]/.test(name)) {
|
|
@@ -139,10 +139,11 @@ const reservedKeywords = new Set([
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
return operations.reduce((groups, op) => {
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
const groupKey = _nullishCoalesce(op.group, () => ( 'default'));
|
|
143
|
+
if (!groups[groupKey]) {
|
|
144
|
+
groups[groupKey] = [];
|
|
144
145
|
}
|
|
145
|
-
groups[
|
|
146
|
+
groups[groupKey].push(op);
|
|
146
147
|
return groups;
|
|
147
148
|
}, {});
|
|
148
149
|
} exports.groupOperationsByGroupName = groupOperationsByGroupName;
|
|
@@ -219,11 +220,13 @@ function resolveResponseRef(
|
|
|
219
220
|
return [];
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
return arr.concat().sort(
|
|
223
|
+
return arr.concat().sort((a, b) => {
|
|
224
|
+
const aVal = (a )[key] ;
|
|
225
|
+
const bVal = (b )[key] ;
|
|
226
|
+
return aVal != null && bVal != null ? (aVal > bVal ? 1 : bVal > aVal ? -1 : 0) : 0;
|
|
227
|
+
});
|
|
223
228
|
} exports.orderBy = orderBy;
|
|
224
229
|
|
|
225
|
-
const sortByKey = (key) => (a, b) => (a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0);
|
|
226
|
-
|
|
227
230
|
const orderedContentTypes = [
|
|
228
231
|
'text/plain',
|
|
229
232
|
'application/x-www-form-urlencoded',
|
|
@@ -233,32 +236,35 @@ const preferredJsonContentTypes = ['application/json', 'text/json'];
|
|
|
233
236
|
function getBestContentType(
|
|
234
237
|
reqBody
|
|
235
238
|
) {
|
|
236
|
-
const
|
|
239
|
+
const content = _nullishCoalesce(reqBody.content, () => ( {}));
|
|
240
|
+
const contentTypes = Object.keys(content);
|
|
237
241
|
if (contentTypes.length === 0) {
|
|
238
242
|
return [null, null];
|
|
239
243
|
}
|
|
240
244
|
|
|
241
|
-
const preferredJsonContentType = preferredJsonContentTypes.find((ct) =>
|
|
245
|
+
const preferredJsonContentType = preferredJsonContentTypes.find((ct) =>
|
|
246
|
+
contentTypes.includes(ct)
|
|
247
|
+
);
|
|
242
248
|
if (preferredJsonContentType) {
|
|
243
|
-
const typeObject =
|
|
249
|
+
const typeObject = content[preferredJsonContentType];
|
|
244
250
|
const type = getContentType(preferredJsonContentType);
|
|
245
251
|
return [typeObject, type];
|
|
246
252
|
}
|
|
247
253
|
|
|
248
254
|
const jsonLikeContentType = contentTypes.find(isJsonLikeContentType);
|
|
249
255
|
if (jsonLikeContentType) {
|
|
250
|
-
const typeObject =
|
|
256
|
+
const typeObject = content[jsonLikeContentType];
|
|
251
257
|
return [typeObject, 'json'];
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
const firstContentType = orderedContentTypes.find((ct) => contentTypes.includes(ct));
|
|
255
261
|
if (firstContentType) {
|
|
256
|
-
const typeObject =
|
|
262
|
+
const typeObject = content[firstContentType];
|
|
257
263
|
const type = getContentType(firstContentType);
|
|
258
264
|
return [typeObject, type];
|
|
259
265
|
}
|
|
260
266
|
|
|
261
|
-
const typeObject =
|
|
267
|
+
const typeObject = content[contentTypes[0]];
|
|
262
268
|
const type = getContentType(contentTypes[0]);
|
|
263
269
|
return [typeObject, type];
|
|
264
270
|
} exports.getBestContentType = getBestContentType;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swaggie",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Generate a fully typed TypeScript API client from your OpenAPI 3 spec",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Piotr Dabrowski",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"build": "bun run bundle-templates && sucrase ./src -d ./dist --transforms typescript,imports && bun run rm-tests && bun run types",
|
|
41
41
|
"bundle-templates": "bun scripts/bundle-templates.ts",
|
|
42
42
|
"rm-tests": "find dist/ \\( -name '*.spec.js' -o -name 'types.js' \\) -type f -delete",
|
|
43
|
-
"types": "tsc
|
|
43
|
+
"types": "tsc --project tsconfig.types.json && cp test/index.d.ts ./dist/",
|
|
44
44
|
"docs:build": "vitepress build docs",
|
|
45
45
|
"docs:dev": "vitepress dev docs",
|
|
46
46
|
"docs:preview": "vitepress preview docs"
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"service",
|
|
67
67
|
"typescript",
|
|
68
68
|
"codegen",
|
|
69
|
-
"TanStack Query"
|
|
69
|
+
"TanStack Query",
|
|
70
|
+
"mocks"
|
|
70
71
|
],
|
|
71
72
|
"dependencies": {
|
|
72
73
|
"case": "^1.6.3",
|
|
@@ -79,7 +80,7 @@
|
|
|
79
80
|
"bun-types": "1.3.11",
|
|
80
81
|
"openapi-types": "^12.1.3",
|
|
81
82
|
"sucrase": "3.35.1",
|
|
82
|
-
"typescript": "
|
|
83
|
+
"typescript": "6.0.2",
|
|
83
84
|
"vitepress": "^2.0.0-alpha.17",
|
|
84
85
|
"vitepress-plugin-tabs": "0.8.0"
|
|
85
86
|
}
|
|
@@ -10,31 +10,35 @@ $config?: AxiosRequestConfig
|
|
|
10
10
|
return axios.request<<%~ it.returnType %>>({
|
|
11
11
|
url: url,
|
|
12
12
|
method: '<%= it.method %>',
|
|
13
|
-
<% if(it.body) {
|
|
14
|
-
<% if(it.body.contentType === 'urlencoded') {
|
|
13
|
+
<% if(it.body) { -%>
|
|
14
|
+
<% if(it.body.contentType === 'urlencoded') { -%>
|
|
15
15
|
data: new URLSearchParams(<%= it.body.name %> as any),
|
|
16
|
-
<% } else {
|
|
16
|
+
<% } else { -%>
|
|
17
17
|
data: <%= it.body.name %>,
|
|
18
|
-
<% }
|
|
19
|
-
<% }
|
|
20
|
-
<% if(it.query && it.query.length > 0) {
|
|
18
|
+
<% } -%>
|
|
19
|
+
<% } -%>
|
|
20
|
+
<% if(it.query && it.query.length > 0) { -%>
|
|
21
21
|
params: {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<%
|
|
22
|
+
<% it.query.forEach((parameter) => { -%>
|
|
23
|
+
<% if (it.queryParamObject) { -%>
|
|
24
|
+
'<%= parameter.originalName %>': <%= it.queryParamObject.name %>?.<%= parameter.name %>,
|
|
25
|
+
<% } else { -%>
|
|
26
|
+
'<%= parameter.originalName %>': <%= parameter.name %>,
|
|
27
|
+
<% } -%>
|
|
28
|
+
<% }); -%>
|
|
29
|
+
},
|
|
30
|
+
<% } -%>
|
|
31
|
+
<% if(it.headers && it.headers.length > 0) { -%>
|
|
28
32
|
headers: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
<% }
|
|
33
|
+
<% it.headers.forEach((parameter) => { -%>
|
|
34
|
+
<% if (parameter.value) { -%>
|
|
35
|
+
'<%= parameter.originalName %>': '<%= parameter.value %>',
|
|
36
|
+
<% } else { -%>
|
|
37
|
+
'<%= parameter.originalName %>': <%= parameter.name %>,
|
|
38
|
+
<% } -%>
|
|
39
|
+
<% }); -%>
|
|
40
|
+
},
|
|
41
|
+
<% } -%>
|
|
38
42
|
...$config,
|
|
39
43
|
});
|
|
40
44
|
},
|
|
@@ -8,7 +8,11 @@ $config?: RequestInit
|
|
|
8
8
|
const url = `${defaults.baseUrl}<%= it.url %>?<%
|
|
9
9
|
if(it.query && it.query.length > 0) { %>${defaults.paramsSerializer({<%
|
|
10
10
|
it.query.forEach((parameter) => { %>
|
|
11
|
+
<% if (it.queryParamObject) { %>
|
|
12
|
+
'<%= parameter.originalName %>': <%= it.queryParamObject.name %>?.<%= parameter.name %>,
|
|
13
|
+
<% } else { %>
|
|
11
14
|
'<%= parameter.originalName %>': <%= parameter.name %>,
|
|
15
|
+
<% } %>
|
|
12
16
|
<% }); %>})}<% } %>`;
|
|
13
17
|
|
|
14
18
|
<% if(it.headers && it.headers.length > 0) { %>
|
|
@@ -8,16 +8,24 @@
|
|
|
8
8
|
let url = `<%= it.url %>?`;
|
|
9
9
|
<% if(it.query && it.query.length > 0) { %>
|
|
10
10
|
<% it.query.forEach((parameter) => { %>
|
|
11
|
-
if (<%= parameter.name %> !== undefined) {
|
|
11
|
+
if (<%= it.queryParamObject ? `${it.queryParamObject.name}?.${parameter.name}` : parameter.name %> !== undefined) {
|
|
12
12
|
<% if(!!parameter.original && parameter.original.type === 'array') { %>
|
|
13
|
-
<%= parameter.name %>.forEach(item => { url += serializeQueryParam(item, '<%= parameter.originalName %>') + "&"; });
|
|
13
|
+
<%= it.queryParamObject ? `${it.queryParamObject.name}.${parameter.name}` : parameter.name %>.forEach(item => { url += serializeQueryParam(item, '<%= parameter.originalName %>') + "&"; });
|
|
14
14
|
<% } else {%>
|
|
15
|
-
url += serializeQueryParam(<%= parameter.name %>, '<%= parameter.originalName %>') + "&";
|
|
15
|
+
url += serializeQueryParam(<%= it.queryParamObject ? `${it.queryParamObject.name}.${parameter.name}` : parameter.name %>, '<%= parameter.originalName %>') + "&";
|
|
16
16
|
<% } %>
|
|
17
17
|
}
|
|
18
18
|
<% }); %>
|
|
19
19
|
<% } %>
|
|
20
20
|
|
|
21
|
+
<% if(it.headers && it.headers.length > 0) { it.headers.forEach((parameter) => { %>
|
|
22
|
+
<% if (parameter.value) { %>
|
|
23
|
+
config = { ...config, headers: { ...config?.headers, '<%= parameter.originalName %>': '<%= parameter.value %>' } };
|
|
24
|
+
<% } else { %>
|
|
25
|
+
if (<%= parameter.name %>) {
|
|
26
|
+
config = { ...config, headers: { ...config?.headers, '<%= parameter.originalName %>': <%= parameter.name %> } };
|
|
27
|
+
}
|
|
28
|
+
<% } %><% }); } %>
|
|
21
29
|
return this.$<%= it.method.toLowerCase() %>(
|
|
22
30
|
url,
|
|
23
31
|
<% if(['POST', 'PUT', 'PATCH'].includes(it.method)) { %>
|