nuxt-openapi-hyperfetch 1.0.2 → 1.0.4
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 +106 -51
- package/dist/cli/config.d.ts +15 -49
- package/dist/cli/config.js +42 -6
- package/dist/config/connectors.d.ts +6 -0
- package/dist/config/connectors.js +26 -0
- package/dist/config/types.d.ts +62 -0
- package/dist/config/types.js +1 -0
- package/dist/generators/connectors/config-resolver.d.ts +3 -0
- package/dist/generators/connectors/config-resolver.js +207 -0
- package/dist/generators/connectors/generator.js +3 -1
- package/dist/generators/connectors/templates.js +6 -2
- package/dist/generators/connectors/types.d.ts +3 -0
- package/dist/index.js +27 -10
- package/dist/module/index.js +11 -3
- package/dist/module/types.d.ts +4 -4
- package/docs/QUICK-START.md +44 -4
- package/package.json +22 -3
- package/src/cli/config.ts +61 -57
- package/src/config/connectors.ts +48 -0
- package/src/config/types.ts +67 -0
- package/src/generators/connectors/config-resolver.ts +303 -0
- package/src/generators/connectors/generator.ts +3 -1
- package/src/generators/connectors/templates.ts +5 -2
- package/src/generators/connectors/types.ts +4 -0
- package/src/index.ts +41 -12
- package/src/module/index.ts +18 -3
- package/src/module/types.ts +4 -4
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { camelCase, pascalCase } from 'change-case';
|
|
2
|
+
import { mapColumnsFromSchema, mapFieldsFromSchema, buildZodSchema, } from '../components/schema-analyzer/index.js';
|
|
3
|
+
function toConnectorName(tag) {
|
|
4
|
+
const pascal = pascalCase(tag);
|
|
5
|
+
const plural = /(?:s|x|z|ch|sh)$/i.test(pascal) ? `${pascal}es` : `${pascal}s`;
|
|
6
|
+
return `use${plural}Connector`;
|
|
7
|
+
}
|
|
8
|
+
function cloneResource(resource) {
|
|
9
|
+
return {
|
|
10
|
+
...resource,
|
|
11
|
+
endpoints: [...resource.endpoints],
|
|
12
|
+
columns: [...resource.columns],
|
|
13
|
+
formFields: {
|
|
14
|
+
...(resource.formFields.create ? { create: [...resource.formFields.create] } : {}),
|
|
15
|
+
...(resource.formFields.update ? { update: [...resource.formFields.update] } : {}),
|
|
16
|
+
},
|
|
17
|
+
zodSchemas: { ...resource.zodSchemas },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function getRefNameFromSchema(schema) {
|
|
21
|
+
if (!schema) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const directRef = schema['x-ref-name'];
|
|
25
|
+
if (typeof directRef === 'string') {
|
|
26
|
+
return directRef;
|
|
27
|
+
}
|
|
28
|
+
const items = schema.items;
|
|
29
|
+
if (items && typeof items === 'object') {
|
|
30
|
+
const itemRef = items['x-ref-name'];
|
|
31
|
+
if (typeof itemRef === 'string') {
|
|
32
|
+
return itemRef;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
function rebuildDerived(resource) {
|
|
38
|
+
const schemaForColumns = resource.listEndpoint?.responseSchema ?? resource.detailEndpoint?.responseSchema;
|
|
39
|
+
resource.columns = schemaForColumns ? mapColumnsFromSchema(schemaForColumns) : [];
|
|
40
|
+
resource.formFields = {
|
|
41
|
+
...(resource.createEndpoint?.requestBodySchema
|
|
42
|
+
? { create: mapFieldsFromSchema(resource.createEndpoint.requestBodySchema) }
|
|
43
|
+
: {}),
|
|
44
|
+
...(resource.updateEndpoint?.requestBodySchema
|
|
45
|
+
? { update: mapFieldsFromSchema(resource.updateEndpoint.requestBodySchema) }
|
|
46
|
+
: {}),
|
|
47
|
+
};
|
|
48
|
+
resource.zodSchemas = {
|
|
49
|
+
...(resource.createEndpoint?.requestBodySchema
|
|
50
|
+
? { create: buildZodSchema(resource.createEndpoint.requestBodySchema) }
|
|
51
|
+
: {}),
|
|
52
|
+
...(resource.updateEndpoint?.requestBodySchema
|
|
53
|
+
? { update: buildZodSchema(resource.updateEndpoint.requestBodySchema) }
|
|
54
|
+
: {}),
|
|
55
|
+
};
|
|
56
|
+
resource.itemTypeName =
|
|
57
|
+
getRefNameFromSchema(resource.detailEndpoint?.responseSchema) ??
|
|
58
|
+
getRefNameFromSchema(resource.listEndpoint?.responseSchema) ??
|
|
59
|
+
undefined;
|
|
60
|
+
}
|
|
61
|
+
function expectedMethodsForOperation(operationName) {
|
|
62
|
+
switch (operationName) {
|
|
63
|
+
case 'getAll':
|
|
64
|
+
case 'get':
|
|
65
|
+
return ['GET'];
|
|
66
|
+
case 'create':
|
|
67
|
+
return ['POST'];
|
|
68
|
+
case 'update':
|
|
69
|
+
return ['PUT', 'PATCH'];
|
|
70
|
+
case 'delete':
|
|
71
|
+
return ['DELETE'];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function selectByIntent(operationName, candidates) {
|
|
75
|
+
if (operationName === 'getAll') {
|
|
76
|
+
const preferred = candidates.filter((c) => c.intent === 'list');
|
|
77
|
+
return preferred.length > 0 ? preferred : candidates;
|
|
78
|
+
}
|
|
79
|
+
if (operationName === 'get') {
|
|
80
|
+
const preferred = candidates.filter((c) => c.intent === 'detail');
|
|
81
|
+
return preferred.length > 0 ? preferred : candidates;
|
|
82
|
+
}
|
|
83
|
+
if (operationName === 'update') {
|
|
84
|
+
const put = candidates.filter((c) => c.method === 'PUT');
|
|
85
|
+
return put.length > 0 ? put : candidates;
|
|
86
|
+
}
|
|
87
|
+
return candidates;
|
|
88
|
+
}
|
|
89
|
+
function buildEndpointIndex(resourceMap) {
|
|
90
|
+
const byOperationId = new Map();
|
|
91
|
+
const byPath = new Map();
|
|
92
|
+
for (const resource of resourceMap.values()) {
|
|
93
|
+
for (const endpoint of resource.endpoints) {
|
|
94
|
+
byOperationId.set(endpoint.operationId, endpoint);
|
|
95
|
+
const list = byPath.get(endpoint.path) ?? [];
|
|
96
|
+
list.push(endpoint);
|
|
97
|
+
byPath.set(endpoint.path, list);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { byOperationId, byPath };
|
|
101
|
+
}
|
|
102
|
+
function resolveEndpoint(operationName, operationConfig, endpointIndex, resourceName) {
|
|
103
|
+
if (operationConfig.operationId && operationConfig.path) {
|
|
104
|
+
throw new Error(`[connectors] Resource "${resourceName}" operation "${operationName}" cannot define both operationId and path`);
|
|
105
|
+
}
|
|
106
|
+
if (!operationConfig.operationId && !operationConfig.path) {
|
|
107
|
+
throw new Error(`[connectors] Resource "${resourceName}" operation "${operationName}" must define operationId or path`);
|
|
108
|
+
}
|
|
109
|
+
if (operationConfig.operationId) {
|
|
110
|
+
const endpoint = endpointIndex.byOperationId.get(operationConfig.operationId);
|
|
111
|
+
if (!endpoint) {
|
|
112
|
+
throw new Error(`[connectors] Resource "${resourceName}" operation "${operationName}" references unknown operationId "${operationConfig.operationId}"`);
|
|
113
|
+
}
|
|
114
|
+
return endpoint;
|
|
115
|
+
}
|
|
116
|
+
const candidates = endpointIndex.byPath.get(operationConfig.path) ?? [];
|
|
117
|
+
if (candidates.length === 0) {
|
|
118
|
+
throw new Error(`[connectors] Resource "${resourceName}" operation "${operationName}" references unknown path "${operationConfig.path}"`);
|
|
119
|
+
}
|
|
120
|
+
const validMethods = expectedMethodsForOperation(operationName);
|
|
121
|
+
const byMethod = candidates.filter((c) => validMethods.includes(c.method));
|
|
122
|
+
if (byMethod.length === 0) {
|
|
123
|
+
throw new Error(`[connectors] Resource "${resourceName}" operation "${operationName}" path "${operationConfig.path}" has no compatible method (${validMethods.join('/')})`);
|
|
124
|
+
}
|
|
125
|
+
const prioritized = selectByIntent(operationName, byMethod);
|
|
126
|
+
return prioritized[0];
|
|
127
|
+
}
|
|
128
|
+
function setOperationEndpoint(resource, operationName, endpoint) {
|
|
129
|
+
switch (operationName) {
|
|
130
|
+
case 'getAll':
|
|
131
|
+
resource.listEndpoint = endpoint;
|
|
132
|
+
break;
|
|
133
|
+
case 'get':
|
|
134
|
+
resource.detailEndpoint = endpoint;
|
|
135
|
+
break;
|
|
136
|
+
case 'create':
|
|
137
|
+
resource.createEndpoint = endpoint;
|
|
138
|
+
break;
|
|
139
|
+
case 'update':
|
|
140
|
+
resource.updateEndpoint = endpoint;
|
|
141
|
+
break;
|
|
142
|
+
case 'delete':
|
|
143
|
+
resource.deleteEndpoint = endpoint;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
if (!resource.endpoints.some((ep) => ep.operationId === endpoint.operationId)) {
|
|
147
|
+
resource.endpoints.push(endpoint);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function createResourceSkeleton(resourceKey) {
|
|
151
|
+
return {
|
|
152
|
+
name: pascalCase(resourceKey),
|
|
153
|
+
tag: resourceKey,
|
|
154
|
+
composableName: toConnectorName(resourceKey),
|
|
155
|
+
endpoints: [],
|
|
156
|
+
columns: [],
|
|
157
|
+
formFields: {},
|
|
158
|
+
zodSchemas: {},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function applyResourceOverrides(target, resourceName, config, endpointIndex) {
|
|
162
|
+
const operations = config.operations ?? {};
|
|
163
|
+
for (const [operationName, operationConfig] of Object.entries(operations)) {
|
|
164
|
+
if (!operationConfig) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const endpoint = resolveEndpoint(operationName, operationConfig, endpointIndex, resourceName);
|
|
168
|
+
setOperationEndpoint(target, operationName, endpoint);
|
|
169
|
+
}
|
|
170
|
+
rebuildDerived(target);
|
|
171
|
+
return target;
|
|
172
|
+
}
|
|
173
|
+
function normalizeConfig(config) {
|
|
174
|
+
return {
|
|
175
|
+
strategy: config?.strategy ?? 'hybrid',
|
|
176
|
+
...config,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function resolveConnectorResourceMap(baseResourceMap, connectorsConfig) {
|
|
180
|
+
if (!connectorsConfig) {
|
|
181
|
+
return baseResourceMap;
|
|
182
|
+
}
|
|
183
|
+
const normalized = normalizeConfig(connectorsConfig);
|
|
184
|
+
const resourceConfigs = normalized.resources ?? {};
|
|
185
|
+
const endpointIndex = buildEndpointIndex(baseResourceMap);
|
|
186
|
+
if (normalized.strategy === 'manual') {
|
|
187
|
+
const manualMap = new Map();
|
|
188
|
+
for (const [resourceName, resourceConfig] of Object.entries(resourceConfigs)) {
|
|
189
|
+
const resourceKey = camelCase(resourceName);
|
|
190
|
+
const baseResource = baseResourceMap.get(resourceKey);
|
|
191
|
+
const target = baseResource
|
|
192
|
+
? cloneResource(baseResource)
|
|
193
|
+
: createResourceSkeleton(resourceName);
|
|
194
|
+
applyResourceOverrides(target, resourceName, resourceConfig, endpointIndex);
|
|
195
|
+
manualMap.set(resourceKey, target);
|
|
196
|
+
}
|
|
197
|
+
return manualMap;
|
|
198
|
+
}
|
|
199
|
+
const hybridMap = new Map([...baseResourceMap.entries()].map(([key, value]) => [key, cloneResource(value)]));
|
|
200
|
+
for (const [resourceName, resourceConfig] of Object.entries(resourceConfigs)) {
|
|
201
|
+
const resourceKey = camelCase(resourceName);
|
|
202
|
+
const target = hybridMap.get(resourceKey) ?? createResourceSkeleton(resourceName);
|
|
203
|
+
applyResourceOverrides(target, resourceName, resourceConfig, endpointIndex);
|
|
204
|
+
hybridMap.set(resourceKey, target);
|
|
205
|
+
}
|
|
206
|
+
return hybridMap;
|
|
207
|
+
}
|
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { format } from 'prettier';
|
|
5
5
|
import { analyzeSpec } from '../components/schema-analyzer/index.js';
|
|
6
6
|
import { generateConnectorFile, connectorFileName, generateConnectorIndexFile, } from './templates.js';
|
|
7
|
+
import { resolveConnectorResourceMap } from './config-resolver.js';
|
|
7
8
|
import { createClackLogger } from '../../cli/logger.js';
|
|
8
9
|
// Runtime files that must be copied to the user's project
|
|
9
10
|
const RUNTIME_FILES = [
|
|
@@ -44,7 +45,8 @@ export async function generateConnectors(options, logger = createClackLogger())
|
|
|
44
45
|
const runtimeRelDir = options.runtimeRelDir ?? '../runtime';
|
|
45
46
|
// ── 1. Analyze spec ───────────────────────────────────────────────────────
|
|
46
47
|
spinner.start('Analyzing OpenAPI spec');
|
|
47
|
-
const
|
|
48
|
+
const baseResourceMap = analyzeSpec(options.inputSpec);
|
|
49
|
+
const resourceMap = resolveConnectorResourceMap(baseResourceMap, options.connectorsConfig);
|
|
48
50
|
spinner.stop(`Found ${resourceMap.size} resource(s)`);
|
|
49
51
|
if (resourceMap.size === 0) {
|
|
50
52
|
logger.log.warn('No resources found in spec — nothing to generate');
|
|
@@ -150,6 +150,7 @@ function buildOptionsInterface(resource) {
|
|
|
150
150
|
if (resource.listEndpoint && hasColumns) {
|
|
151
151
|
fields.push(` columnLabels?: Record<string, string>;`);
|
|
152
152
|
fields.push(` columnLabel?: (key: string) => string;`);
|
|
153
|
+
fields.push(` getAllRequestOptions?: Record<string, unknown>;`);
|
|
153
154
|
}
|
|
154
155
|
if (resource.createEndpoint && resource.zodSchemas.create) {
|
|
155
156
|
const pascal = pascalCase(resource.name);
|
|
@@ -218,7 +219,10 @@ function buildFunctionBody(resource) {
|
|
|
218
219
|
// Options destructure
|
|
219
220
|
const optionKeys = [];
|
|
220
221
|
if (resource.listEndpoint && hasColumns) {
|
|
221
|
-
optionKeys.push('columnLabels', 'columnLabel');
|
|
222
|
+
optionKeys.push('columnLabels', 'columnLabel', 'getAllRequestOptions');
|
|
223
|
+
}
|
|
224
|
+
else if (resource.listEndpoint) {
|
|
225
|
+
optionKeys.push('getAllRequestOptions');
|
|
222
226
|
}
|
|
223
227
|
if (resource.createEndpoint && resource.zodSchemas.create) {
|
|
224
228
|
optionKeys.push('createSchema');
|
|
@@ -257,7 +261,7 @@ function buildFunctionBody(resource) {
|
|
|
257
261
|
const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
|
|
258
262
|
const allArgs = [columnsArg, labelArgs].filter(Boolean).join(', ');
|
|
259
263
|
const opts = allArgs ? `{ ${allArgs} }` : '{}';
|
|
260
|
-
lines.push(` const isFactory = typeof paramsOrSource === 'function';`, ` const listFactory = isFactory`, ` ? (paramsOrSource as () => unknown)`, ` : () => ${fn}((paramsOrSource ?? {}) as ${listRequestTypeName});`, ` const getAll = useGetAllConnector(listFactory, ${opts}) as unknown as GetAllConnectorReturn<${pascal}>;`);
|
|
264
|
+
lines.push(` const isFactory = typeof paramsOrSource === 'function';`, ` const listFactory = isFactory`, ` ? (paramsOrSource as () => unknown)`, ` : () => ${fn}((paramsOrSource ?? {}) as ${listRequestTypeName}, { paginated: true, ...(getAllRequestOptions ?? {}) });`, ` const getAll = useGetAllConnector(listFactory, ${opts}) as unknown as GetAllConnectorReturn<${pascal}>;`);
|
|
261
265
|
}
|
|
262
266
|
else {
|
|
263
267
|
lines.push(` const getAll = useGetAllConnector(() => ({}), {}) as unknown as GetAllConnectorReturn<${pascal}>;`);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* The Connector Generator reads the ResourceMap produced by the Schema Analyzer
|
|
5
5
|
* and writes one `use{Resource}Connector.ts` file per resource using $fetch for mutations.
|
|
6
6
|
*/
|
|
7
|
+
import type { ConnectorsConfig } from '../../config/types.js';
|
|
7
8
|
export interface ConnectorGeneratorOptions {
|
|
8
9
|
/** Absolute or relative path to the OpenAPI YAML/JSON spec */
|
|
9
10
|
inputSpec: string;
|
|
@@ -24,6 +25,8 @@ export interface ConnectorGeneratorOptions {
|
|
|
24
25
|
* useRuntimeConfig().public.apiBaseUrl at runtime.
|
|
25
26
|
*/
|
|
26
27
|
baseUrl?: string;
|
|
28
|
+
/** Advanced connectors configuration (manual/hybrid, custom resources, overloads). */
|
|
29
|
+
connectorsConfig?: ConnectorsConfig;
|
|
27
30
|
}
|
|
28
31
|
export interface ConnectorFileInfo {
|
|
29
32
|
/** PascalCase resource name. E.g. 'Pet' */
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,8 @@ import { generateConnectors } from './generators/connectors/generator.js';
|
|
|
9
9
|
import { promptInitialInputs, promptInputPath, promptComposablesSelection, promptServerRoutePath, promptBffConfig, promptGeneratorBackend, promptConnectors, } from './cli/prompts.js';
|
|
10
10
|
import { MESSAGES } from './cli/messages.js';
|
|
11
11
|
import { displayLogo } from './cli/logo.js';
|
|
12
|
-
import { loadConfig, mergeConfig, parseTags, parseGenerators } from './cli/config.js';
|
|
12
|
+
import { loadConfig, mergeConfig, parseTags, parseGenerators, normalizeGenerators, } from './cli/config.js';
|
|
13
|
+
import { hasConnectorsConfig } from './config/connectors.js';
|
|
13
14
|
const program = new Command();
|
|
14
15
|
program.name('nxh').description('Nuxt OpenAPI Hyperfetch generator').version('1.0.0');
|
|
15
16
|
program
|
|
@@ -25,7 +26,7 @@ program
|
|
|
25
26
|
.option('--dry-run', 'Preview changes without writing files', false)
|
|
26
27
|
.option('-v, --verbose', 'Enable verbose logging', false)
|
|
27
28
|
.option('--watch', 'Watch mode - regenerate on file changes', false)
|
|
28
|
-
.option('--generators <types>', 'Generators to use: useFetch,useAsyncData,nuxtServer')
|
|
29
|
+
.option('--generators <types>', 'Generators to use: useFetch,useAsyncData,nuxtServer,connectors')
|
|
29
30
|
.option('--connectors', 'Generate headless UI connectors on top of useAsyncData', false)
|
|
30
31
|
.option('--server-route-path <path>', 'Server route path (for nuxtServer mode)')
|
|
31
32
|
.option('--enable-bff', 'Enable BFF pattern (for nuxtServer mode)', false)
|
|
@@ -86,16 +87,21 @@ program
|
|
|
86
87
|
}
|
|
87
88
|
// 1. Determine composables to generate FIRST
|
|
88
89
|
let composables;
|
|
90
|
+
let generateConnectorsFlag = false;
|
|
91
|
+
const connectorsConfigured = hasConnectorsConfig(config.connectors);
|
|
89
92
|
if (config.generators) {
|
|
90
|
-
|
|
91
|
-
composables =
|
|
93
|
+
const normalized = normalizeGenerators(config.generators, config.createUseAsyncDataConnectors);
|
|
94
|
+
composables = normalized.composables;
|
|
95
|
+
generateConnectorsFlag = normalized.generateConnectors;
|
|
92
96
|
if (config.verbose) {
|
|
93
97
|
console.log(`Using generators from config: ${composables.join(', ')}`);
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
else {
|
|
97
101
|
const result = await promptComposablesSelection();
|
|
98
|
-
|
|
102
|
+
const normalized = normalizeGenerators(result.composables, config.createUseAsyncDataConnectors);
|
|
103
|
+
composables = normalized.composables;
|
|
104
|
+
generateConnectorsFlag = normalized.generateConnectors;
|
|
99
105
|
}
|
|
100
106
|
if (composables.length === 0) {
|
|
101
107
|
p.outro(MESSAGES.outro.noComposables);
|
|
@@ -117,14 +123,24 @@ program
|
|
|
117
123
|
inputPath = await promptInputPath(config.input);
|
|
118
124
|
outputPath = config.output ?? './swagger';
|
|
119
125
|
}
|
|
120
|
-
// 3. Ask whether to generate headless connectors (only if
|
|
121
|
-
let generateConnectorsFlag = false;
|
|
126
|
+
// 3. Ask whether to generate headless connectors (only if still unresolved)
|
|
122
127
|
if (composables.includes('useAsyncData')) {
|
|
123
|
-
|
|
128
|
+
const hasExplicitConnectorsInGenerators = config.generators?.includes('connectors') ?? false;
|
|
129
|
+
const shouldPromptConnectors = !generateConnectorsFlag &&
|
|
130
|
+
config.createUseAsyncDataConnectors === undefined &&
|
|
131
|
+
!hasExplicitConnectorsInGenerators &&
|
|
132
|
+
!connectorsConfigured;
|
|
133
|
+
if (shouldPromptConnectors) {
|
|
134
|
+
generateConnectorsFlag = await promptConnectors();
|
|
135
|
+
}
|
|
136
|
+
else if (connectorsConfigured) {
|
|
137
|
+
generateConnectorsFlag = true;
|
|
138
|
+
}
|
|
139
|
+
else if (config.createUseAsyncDataConnectors !== undefined) {
|
|
124
140
|
generateConnectorsFlag = config.createUseAsyncDataConnectors;
|
|
125
141
|
}
|
|
126
|
-
else {
|
|
127
|
-
generateConnectorsFlag =
|
|
142
|
+
else if (hasExplicitConnectorsInGenerators) {
|
|
143
|
+
generateConnectorsFlag = true;
|
|
128
144
|
}
|
|
129
145
|
}
|
|
130
146
|
// 4. Ask for server route path if nuxtServer is selected
|
|
@@ -203,6 +219,7 @@ program
|
|
|
203
219
|
outputDir: `${composablesOutputDir}/connectors`,
|
|
204
220
|
composablesRelDir: '../use-async-data/composables',
|
|
205
221
|
runtimeRelDir: '../../runtime',
|
|
222
|
+
connectorsConfig: config.connectors,
|
|
206
223
|
});
|
|
207
224
|
spinner.stop('✓ Generated headless UI connectors');
|
|
208
225
|
}
|
package/dist/module/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { generateUseAsyncDataComposables } from '../generators/use-async-data/ge
|
|
|
7
7
|
import { generateNuxtServerRoutes } from '../generators/nuxt-server/generator.js';
|
|
8
8
|
import { generateConnectors } from '../generators/connectors/generator.js';
|
|
9
9
|
import { createConsoleLogger } from '../cli/logger.js';
|
|
10
|
+
import { normalizeGenerators } from '../cli/config.js';
|
|
11
|
+
import { isConnectorsRequested } from '../config/connectors.js';
|
|
10
12
|
export default defineNuxtModule({
|
|
11
13
|
meta: {
|
|
12
14
|
name: 'nuxt-openapi-hyperfetch',
|
|
@@ -32,7 +34,10 @@ export default defineNuxtModule({
|
|
|
32
34
|
const resolvedInput = path.resolve(nuxt.options.rootDir, options.input);
|
|
33
35
|
const resolvedOutput = path.resolve(nuxt.options.rootDir, options.output);
|
|
34
36
|
const composablesOutputDir = path.join(resolvedOutput, 'composables');
|
|
35
|
-
const
|
|
37
|
+
const normalized = normalizeGenerators(options.generators ?? ['useFetch', 'useAsyncData'], options.createUseAsyncDataConnectors);
|
|
38
|
+
const selectedGenerators = normalized.composables;
|
|
39
|
+
const generateConnectorsFlag = normalized.generateConnectors;
|
|
40
|
+
const connectorsRequested = isConnectorsRequested(options);
|
|
36
41
|
const backend = options.backend ?? 'heyapi';
|
|
37
42
|
const logger = createConsoleLogger();
|
|
38
43
|
// --- Core generation function ---
|
|
@@ -67,13 +72,15 @@ export default defineNuxtModule({
|
|
|
67
72
|
await generateNuxtServerRoutes(resolvedOutput, serverRoutePath, { enableBff: options.enableBff, backend }, logger);
|
|
68
73
|
}
|
|
69
74
|
// 3. Generate headless connectors if requested (requires useAsyncData)
|
|
70
|
-
if (
|
|
75
|
+
if ((generateConnectorsFlag || connectorsRequested) &&
|
|
76
|
+
selectedGenerators.includes('useAsyncData')) {
|
|
71
77
|
const connectorsOutputDir = path.join(composablesOutputDir, 'connectors');
|
|
72
78
|
await generateConnectors({
|
|
73
79
|
inputSpec: resolvedInput,
|
|
74
80
|
outputDir: connectorsOutputDir,
|
|
75
81
|
composablesRelDir: '../use-async-data/composables',
|
|
76
82
|
runtimeRelDir: '../../runtime',
|
|
83
|
+
connectorsConfig: options.connectors,
|
|
77
84
|
}, logger);
|
|
78
85
|
}
|
|
79
86
|
};
|
|
@@ -100,7 +107,8 @@ export default defineNuxtModule({
|
|
|
100
107
|
if (selectedGenerators.includes('useAsyncData')) {
|
|
101
108
|
addImportsDir(path.join(composablesOutputDir, 'use-async-data', 'composables'));
|
|
102
109
|
}
|
|
103
|
-
if (
|
|
110
|
+
if ((generateConnectorsFlag || connectorsRequested) &&
|
|
111
|
+
selectedGenerators.includes('useAsyncData')) {
|
|
104
112
|
addImportsDir(path.join(composablesOutputDir, 'connectors'));
|
|
105
113
|
}
|
|
106
114
|
}
|
package/dist/module/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GeneratorConfig } from '../
|
|
1
|
+
import type { GeneratorConfig } from '../config/types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Configuration options for the nuxt-openapi-hyperfetch Nuxt module.
|
|
4
4
|
* Extends the CLI GeneratorConfig so the same fields work in both nxh.config.js and nuxt.config.ts.
|
|
@@ -25,9 +25,9 @@ export interface ModuleOptions extends GeneratorConfig {
|
|
|
25
25
|
*/
|
|
26
26
|
enableAutoImport?: boolean;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* Backward-compatible connectors flag.
|
|
29
|
+
* Prefer `generators: ['connectors']` so module and CLI behave the same.
|
|
30
|
+
* When true, connectors are generated and useAsyncData is added automatically.
|
|
31
31
|
* @default false
|
|
32
32
|
*/
|
|
33
33
|
createUseAsyncDataConnectors?: boolean;
|
package/docs/QUICK-START.md
CHANGED
|
@@ -15,10 +15,10 @@ OpenAPI/Swagger → TypeScript Client → Nuxt Composables
|
|
|
15
15
|
|
|
16
16
|
The tool supports two backends for **Stage 1** (OpenAPI → TypeScript Client). You choose at runtime:
|
|
17
17
|
|
|
18
|
-
| Backend
|
|
19
|
-
|
|
20
|
-
| **OpenAPI Generator** (official) | `@openapitools/openapi-generator-cli` | Java 11+
|
|
21
|
-
| **Hey API**
|
|
18
|
+
| Backend | Command | Requires | Output format |
|
|
19
|
+
| -------------------------------- | ------------------------------------- | ------------ | ---------------------------- |
|
|
20
|
+
| **OpenAPI Generator** (official) | `@openapitools/openapi-generator-cli` | Java 11+ | `apis/PetApi.ts`, `models/` |
|
|
21
|
+
| **Hey API** | `@hey-api/openapi-ts` | Node.js only | `sdk.gen.ts`, `types.gen.ts` |
|
|
22
22
|
|
|
23
23
|
Both produce the same final Nuxt composables. The choice affects only Stage 1 output and the parser used in Stage 2.
|
|
24
24
|
|
|
@@ -98,6 +98,16 @@ useFetchAddPet → useApiRequest → useFetch (Nuxt native)
|
|
|
98
98
|
| Plugin templates | ❌ | ✅ (once) | ✅ (safe) |
|
|
99
99
|
| CLI tool itself | ❌ | ❌ | ✅ (you edit) |
|
|
100
100
|
|
|
101
|
+
### 4. Generator Selection Semantics (CLI + Nuxt module)
|
|
102
|
+
|
|
103
|
+
Generator selection is unified between `nxh.config.*` and `nuxt.config.ts`.
|
|
104
|
+
|
|
105
|
+
- `generators: ['connectors']` runs `useAsyncData` first, then `connectors`
|
|
106
|
+
- `generators: ['useAsyncData']` runs only `useAsyncData`
|
|
107
|
+
- `generators: ['useAsyncData', 'connectors']` runs both
|
|
108
|
+
|
|
109
|
+
`createUseAsyncDataConnectors` remains available for backward compatibility, but `generators: ['connectors']` is the preferred declarative option.
|
|
110
|
+
|
|
101
111
|
## Key Files (Read These First)
|
|
102
112
|
|
|
103
113
|
### 1. **CLI Entry** - `src/index.ts`
|
|
@@ -231,6 +241,36 @@ nuxt-generator/
|
|
|
231
241
|
|
|
232
242
|
## Most Common Tasks
|
|
233
243
|
|
|
244
|
+
### Configure Connectors (manual/hybrid)
|
|
245
|
+
|
|
246
|
+
Use `connectors` in `nxh.config.ts` or `openApiHyperFetch` in `nuxt.config.ts`.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
connectors: {
|
|
250
|
+
strategy: 'hybrid',
|
|
251
|
+
resources: {
|
|
252
|
+
pets: {
|
|
253
|
+
operations: {
|
|
254
|
+
getAll: { operationId: 'findPetsByStatus' },
|
|
255
|
+
get: { path: '/pet/{petId}' }
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
featuredPets: {
|
|
259
|
+
operations: {
|
|
260
|
+
getAll: { operationId: 'findPetsByTags' }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Rules:
|
|
268
|
+
|
|
269
|
+
- Each operation accepts `operationId` or `path` (never both)
|
|
270
|
+
- Partial override is allowed (you can define only `get` and `getAll`)
|
|
271
|
+
- `manual` generates only configured resources
|
|
272
|
+
- `hybrid` merges inferred resources with configured overrides/custom resources
|
|
273
|
+
|
|
234
274
|
### Debugging a Parser Error
|
|
235
275
|
|
|
236
276
|
1. Check error message for method name
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-openapi-hyperfetch",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Nuxt useFetch, useAsyncData,
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "⚡ OpenAPI to Nuxt code generator for useFetch, useAsyncData, server routes, and headless CRUD connectors.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -86,9 +86,28 @@
|
|
|
86
86
|
"fs-extra": "^11.3.4",
|
|
87
87
|
"globby": "^16.1.1",
|
|
88
88
|
"gradient-string": "^3.0.0",
|
|
89
|
+
"jiti": "^2.6.1",
|
|
89
90
|
"js-yaml": "^4.1.1",
|
|
90
91
|
"prettier": "^3.8.1",
|
|
91
92
|
"ts-morph": "^27.0.2"
|
|
92
93
|
},
|
|
93
|
-
"keywords": [
|
|
94
|
+
"keywords": [
|
|
95
|
+
"openapi",
|
|
96
|
+
"generator",
|
|
97
|
+
"nuxt",
|
|
98
|
+
"rest",
|
|
99
|
+
"composables",
|
|
100
|
+
"swagger",
|
|
101
|
+
"fetch",
|
|
102
|
+
"$fetch",
|
|
103
|
+
"useFetch",
|
|
104
|
+
"useAsyncData",
|
|
105
|
+
"api",
|
|
106
|
+
"open",
|
|
107
|
+
"codegen",
|
|
108
|
+
"server",
|
|
109
|
+
"node",
|
|
110
|
+
"headless",
|
|
111
|
+
"sdk"
|
|
112
|
+
]
|
|
94
113
|
}
|