nuxt-openapi-hyperfetch 0.2.7-alpha.1 → 0.3.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.editorconfig +26 -26
  2. package/.prettierignore +17 -17
  3. package/CONTRIBUTING.md +291 -291
  4. package/INSTRUCTIONS.md +327 -327
  5. package/LICENSE +202 -202
  6. package/README.md +309 -231
  7. package/dist/cli/config.d.ts +9 -2
  8. package/dist/cli/config.js +1 -1
  9. package/dist/cli/logo.js +5 -5
  10. package/dist/cli/messages.d.ts +1 -0
  11. package/dist/cli/messages.js +2 -0
  12. package/dist/cli/prompts.d.ts +5 -0
  13. package/dist/cli/prompts.js +12 -0
  14. package/dist/cli/types.d.ts +1 -1
  15. package/dist/generators/components/connector-generator/templates.js +68 -19
  16. package/dist/generators/shared/runtime/useFormConnector.js +8 -1
  17. package/dist/generators/shared/runtime/useListConnector.js +13 -6
  18. package/dist/generators/use-async-data/generator.js +4 -0
  19. package/dist/generators/use-async-data/runtime/useApiAsyncData.js +4 -4
  20. package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +4 -4
  21. package/dist/generators/use-async-data/templates.js +17 -17
  22. package/dist/generators/use-fetch/generator.js +4 -0
  23. package/dist/generators/use-fetch/templates.js +14 -14
  24. package/dist/index.js +40 -27
  25. package/dist/module/index.js +19 -0
  26. package/dist/module/types.d.ts +7 -0
  27. package/docs/API-REFERENCE.md +886 -886
  28. package/docs/generated-components.md +615 -615
  29. package/docs/headless-composables-ui.md +569 -569
  30. package/eslint.config.js +85 -85
  31. package/package.json +1 -1
  32. package/src/cli/config.ts +147 -140
  33. package/src/cli/logger.ts +124 -124
  34. package/src/cli/logo.ts +25 -25
  35. package/src/cli/messages.ts +4 -0
  36. package/src/cli/prompts.ts +14 -1
  37. package/src/cli/types.ts +50 -50
  38. package/src/generators/components/connector-generator/generator.ts +138 -138
  39. package/src/generators/components/connector-generator/templates.ts +307 -254
  40. package/src/generators/components/connector-generator/types.ts +34 -34
  41. package/src/generators/components/schema-analyzer/index.ts +44 -44
  42. package/src/generators/components/schema-analyzer/intent-detector.ts +187 -187
  43. package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -96
  44. package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -166
  45. package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -268
  46. package/src/generators/components/schema-analyzer/types.ts +177 -177
  47. package/src/generators/nuxt-server/generator.ts +272 -272
  48. package/src/generators/shared/runtime/apiHelpers.ts +535 -535
  49. package/src/generators/shared/runtime/pagination.ts +323 -323
  50. package/src/generators/shared/runtime/useDeleteConnector.ts +109 -109
  51. package/src/generators/shared/runtime/useDetailConnector.ts +64 -64
  52. package/src/generators/shared/runtime/useFormConnector.ts +147 -139
  53. package/src/generators/shared/runtime/useListConnector.ts +158 -148
  54. package/src/generators/shared/runtime/zod-error-merger.ts +119 -119
  55. package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -399
  56. package/src/generators/shared/templates/api-pagination-plugin.ts +158 -158
  57. package/src/generators/use-async-data/generator.ts +213 -205
  58. package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -329
  59. package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -324
  60. package/src/generators/use-async-data/templates.ts +257 -257
  61. package/src/generators/use-fetch/generator.ts +178 -170
  62. package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -354
  63. package/src/generators/use-fetch/templates.ts +214 -214
  64. package/src/index.ts +306 -303
  65. package/src/module/index.ts +158 -133
  66. package/src/module/types.ts +39 -31
  67. package/dist/generators/tanstack-query/generator.d.ts +0 -5
  68. package/dist/generators/tanstack-query/generator.js +0 -11
@@ -1,18 +1,18 @@
1
1
  import { pascalCase, kebabCase } from 'change-case';
2
2
  // ─── File header ──────────────────────────────────────────────────────────────
3
3
  function generateFileHeader() {
4
- return `/**
5
- * ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
6
- *
7
- * This file was automatically generated by nuxt-openapi-generator.
8
- * Any manual changes will be overwritten on the next generation.
9
- *
10
- * @generated by nuxt-openapi-generator
11
- * @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
12
- */
13
-
14
- /* eslint-disable */
15
- // @ts-nocheck
4
+ return `/**
5
+ * ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
6
+ *
7
+ * This file was automatically generated by nuxt-openapi-generator.
8
+ * Any manual changes will be overwritten on the next generation.
9
+ *
10
+ * @generated by nuxt-openapi-generator
11
+ * @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
12
+ */
13
+
14
+ /* eslint-disable */
15
+ // @ts-nocheck
16
16
  `;
17
17
  }
18
18
  // ─── Naming helpers ───────────────────────────────────────────────────────────
@@ -103,19 +103,59 @@ function buildZodSchemas(resource) {
103
103
  }
104
104
  return lines.join('\n');
105
105
  }
106
+ /**
107
+ * Build a const array with the column definitions inferred from the resource.
108
+ * Returns an empty string if the resource has no columns.
109
+ */
110
+ function buildColumns(resource) {
111
+ if (!resource.columns || resource.columns.length === 0) {
112
+ return '';
113
+ }
114
+ const camel = resource.composableName
115
+ .replace(/^use/, '')
116
+ .replace(/Connector$/, '')
117
+ .replace(/^./, (c) => c.toLowerCase());
118
+ const varName = `${camel}Columns`;
119
+ const entries = resource.columns
120
+ .map((col) => ` { key: '${col.key}', label: '${col.label}', type: '${col.type}' }`)
121
+ .join(',\n');
122
+ return `const ${varName} = [\n${entries},\n];`;
123
+ }
106
124
  /**
107
125
  * Build the body of the exported connector function.
108
126
  */
109
127
  function buildFunctionBody(resource) {
110
128
  const pascal = pascalCase(resource.name);
129
+ const hasColumns = resource.columns && resource.columns.length > 0;
130
+ const camel = resource.composableName
131
+ .replace(/^use/, '')
132
+ .replace(/Connector$/, '')
133
+ .replace(/^./, (c) => c.toLowerCase());
134
+ const columnsVar = `${camel}Columns`;
111
135
  const subConnectors = [];
136
+ // Destructure options param — only what's relevant for this resource
137
+ const optionKeys = [];
138
+ if (resource.listEndpoint && hasColumns) {
139
+ optionKeys.push('columnLabels', 'columnLabel');
140
+ }
141
+ if (resource.createEndpoint && resource.zodSchemas.create) {
142
+ optionKeys.push('createSchema');
143
+ }
144
+ if (resource.updateEndpoint && resource.zodSchemas.update) {
145
+ optionKeys.push('updateSchema');
146
+ }
147
+ const optionsDestructure = optionKeys.length > 0 ? ` const { ${optionKeys.join(', ')} } = options;\n` : '';
112
148
  if (resource.listEndpoint) {
113
149
  const fn = toAsyncDataName(resource.listEndpoint.operationId);
114
150
  // paginated: true tells useListConnector to expose pagination helpers
115
151
  // (goToPage, nextPage, prevPage, setPerPage, pagination ref).
116
152
  // We set it whenever the spec declares a list endpoint that has a response schema,
117
153
  // which is a reliable proxy for "this API returns structured data worth paginating".
118
- const opts = resource.listEndpoint.responseSchema ? '{ paginated: true }' : '{}';
154
+ const paginatedFlag = resource.listEndpoint.responseSchema ? 'paginated: true' : '';
155
+ const columnsArg = hasColumns ? `columns: ${columnsVar}` : '';
156
+ const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
157
+ const allArgs = [paginatedFlag, columnsArg, labelArgs].filter(Boolean).join(', ');
158
+ const opts = allArgs ? `{ ${allArgs} }` : '{}';
119
159
  subConnectors.push(` const table = useListConnector(${fn}, ${opts});`);
120
160
  }
121
161
  if (resource.detailEndpoint) {
@@ -124,7 +164,9 @@ function buildFunctionBody(resource) {
124
164
  }
125
165
  if (resource.createEndpoint) {
126
166
  const fn = toAsyncDataName(resource.createEndpoint.operationId);
127
- const schemaArg = resource.zodSchemas.create ? `{ schema: ${pascal}CreateSchema }` : '{}';
167
+ const schemaArg = resource.zodSchemas.create
168
+ ? `{ schema: ${pascal}CreateSchema, schemaOverride: createSchema }`
169
+ : '{}';
128
170
  subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg});`);
129
171
  }
130
172
  if (resource.updateEndpoint) {
@@ -139,11 +181,11 @@ function buildFunctionBody(resource) {
139
181
  let schemaArg = '{}';
140
182
  if (resource.zodSchemas.update && hasDetail) {
141
183
  // Best case: validate AND pre-fill from detail
142
- schemaArg = `{ schema: ${pascal}UpdateSchema, loadWith: detail }`;
184
+ schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema, loadWith: detail }`;
143
185
  }
144
186
  else if (resource.zodSchemas.update) {
145
187
  // Validate, but no detail endpoint to pre-fill from
146
- schemaArg = `{ schema: ${pascal}UpdateSchema }`;
188
+ schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema }`;
147
189
  }
148
190
  else if (hasDetail) {
149
191
  // No Zod schema (no request body in spec), but still pre-fill from detail
@@ -174,11 +216,14 @@ function buildFunctionBody(resource) {
174
216
  }
175
217
  const returnStatement = ` return { ${returnKeys.join(', ')} };`;
176
218
  return [
177
- `export function ${resource.composableName}() {`,
219
+ `export function ${resource.composableName}(options = {}) {`,
220
+ optionsDestructure.trimEnd(),
178
221
  ...subConnectors,
179
222
  returnStatement,
180
223
  `}`,
181
- ].join('\n');
224
+ ]
225
+ .filter((s) => s !== '')
226
+ .join('\n');
182
227
  }
183
228
  // ─── Public API ───────────────────────────────────────────────────────────────
184
229
  /**
@@ -192,14 +237,18 @@ export function generateConnectorFile(resource, composablesRelDir) {
192
237
  const header = generateFileHeader();
193
238
  const imports = buildImports(resource, composablesRelDir);
194
239
  const schemas = buildZodSchemas(resource);
240
+ const columns = buildColumns(resource);
195
241
  const fn = buildFunctionBody(resource);
196
- // Assemble file: header + imports + (optional) Zod blocks + function body.
242
+ // Assemble file: header + imports + (optional) Zod blocks + columns const + function body.
197
243
  // Each section ends with its own trailing newline; join with \n adds one blank
198
244
  // line between sections, which matches Prettier's output for this structure.
199
245
  const parts = [header, imports];
200
246
  if (schemas.trim()) {
201
247
  parts.push(schemas);
202
248
  }
249
+ if (columns.trim()) {
250
+ parts.push(columns);
251
+ }
203
252
  parts.push(fn);
204
253
  return parts.join('\n') + '\n';
205
254
  }
@@ -18,7 +18,14 @@ import { mergeZodErrors } from './zod-error-merger.js';
18
18
  * @param options { schema, fields, loadWith?, errorConfig? }
19
19
  */
20
20
  export function useFormConnector(composableFn, options = {}) {
21
- const { schema, fields = [], loadWith = null, errorConfig = {} } = options;
21
+ const { schema: baseSchema, schemaOverride, fields = [], loadWith = null, errorConfig = {} } = options;
22
+ // Resolve the active schema:
23
+ // schemaOverride(base) — extend or refine the generated schema
24
+ // schemaOverride — replace the generated schema entirely
25
+ // baseSchema — the generated schema unchanged (default)
26
+ const schema = schemaOverride
27
+ ? (typeof schemaOverride === 'function' ? schemaOverride(baseSchema) : schemaOverride)
28
+ : baseSchema;
22
29
  // ── Form state ─────────────────────────────────────────────────────────────
23
30
  const model = ref({});
24
31
  const errors = ref({});
@@ -16,7 +16,7 @@ import { ref, computed, shallowRef } from 'vue';
16
16
  * @param options Configuration for the list connector
17
17
  */
18
18
  export function useListConnector(composableFn, options = {}) {
19
- const { paginated = false, columns = [] } = options;
19
+ const { paginated = false, columns = [], columnLabels = {}, columnLabel = null } = options;
20
20
  // ── Execute the underlying composable ──────────────────────────────────────
21
21
  const composable = composableFn({ paginated });
22
22
  // ── Derived state ──────────────────────────────────────────────────────────
@@ -36,16 +36,16 @@ export function useListConnector(composableFn, options = {}) {
36
36
  // Pagination — passthrough from the underlying composable when paginated: true
37
37
  const pagination = computed(() => composable.pagination?.value ?? null);
38
38
  function goToPage(page) {
39
- composable.goToPage?.(page);
39
+ composable.pagination?.value?.goToPage?.(page);
40
40
  }
41
41
  function nextPage() {
42
- composable.nextPage?.();
42
+ composable.pagination?.value?.nextPage?.();
43
43
  }
44
44
  function prevPage() {
45
- composable.prevPage?.();
45
+ composable.pagination?.value?.prevPage?.();
46
46
  }
47
47
  function setPerPage(n) {
48
- composable.setPerPage?.(n);
48
+ composable.pagination?.value?.setPerPage?.(n);
49
49
  }
50
50
  // ── Row selection ──────────────────────────────────────────────────────────
51
51
  const selected = ref([]);
@@ -95,10 +95,17 @@ export function useListConnector(composableFn, options = {}) {
95
95
  function remove(row) {
96
96
  _deleteTarget.value = row;
97
97
  }
98
+ // Apply label overrides: columnLabel function takes priority over columnLabels map
99
+ const resolvedColumns = computed(() => columns.map((col) => ({
100
+ ...col,
101
+ label: columnLabel
102
+ ? columnLabel(col.key)
103
+ : (columnLabels[col.key] ?? col.label),
104
+ })));
98
105
  return {
99
106
  // State
100
107
  rows,
101
- columns: computed(() => columns),
108
+ columns: resolvedColumns,
102
109
  loading,
103
110
  error,
104
111
  // Pagination
@@ -66,6 +66,10 @@ export async function generateUseAsyncDataComposables(inputDir, outputDir, optio
66
66
  const sharedHelpersSource = path.resolve(__dirname, '../../../src/generators/shared/runtime/apiHelpers.ts');
67
67
  const sharedHelpersDest = path.join(sharedRuntimeDir, 'apiHelpers.ts');
68
68
  await fs.copyFile(sharedHelpersSource, sharedHelpersDest);
69
+ // Copy shared pagination.ts
70
+ const sharedPaginationSource = path.resolve(__dirname, '../../../src/generators/shared/runtime/pagination.ts');
71
+ const sharedPaginationDest = path.join(sharedRuntimeDir, 'pagination.ts');
72
+ await fs.copyFile(sharedPaginationSource, sharedPaginationDest);
69
73
  mainSpinner.stop('Runtime files copied');
70
74
  // 5. Calculate relative import path from composables to APIs
71
75
  const relativePath = calculateRelativeImportPath(composablesDir, inputDir);
@@ -232,10 +232,10 @@ export function useApiAsyncData(key, url, options) {
232
232
  ...paginationState.value,
233
233
  hasNextPage: hasNextPage.value,
234
234
  hasPrevPage: hasPrevPage.value,
235
+ goToPage,
236
+ nextPage,
237
+ prevPage,
238
+ setPerPage,
235
239
  })),
236
- goToPage,
237
- nextPage,
238
- prevPage,
239
- setPerPage,
240
240
  };
241
241
  }
@@ -213,10 +213,10 @@ export function useApiAsyncDataRaw(key, url, options) {
213
213
  ...paginationState.value,
214
214
  hasNextPage: hasNextPage.value,
215
215
  hasPrevPage: hasPrevPage.value,
216
+ goToPage,
217
+ nextPage,
218
+ prevPage,
219
+ setPerPage,
216
220
  })),
217
- goToPage,
218
- nextPage,
219
- prevPage,
220
- setPerPage,
221
221
  };
222
222
  }
@@ -2,18 +2,18 @@
2
2
  * Generate file header with auto-generation warning
3
3
  */
4
4
  function generateFileHeader() {
5
- return `/**
6
- * ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
7
- *
8
- * This file was automatically generated by nuxt-openapi-generator.
9
- * Any manual changes will be overwritten on the next generation.
10
- *
11
- * @generated by nuxt-openapi-generator
12
- * @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
13
- */
14
-
15
- /* eslint-disable */
16
- // @ts-nocheck
5
+ return `/**
6
+ * ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
7
+ *
8
+ * This file was automatically generated by nuxt-openapi-generator.
9
+ * Any manual changes will be overwritten on the next generation.
10
+ *
11
+ * @generated by nuxt-openapi-generator
12
+ * @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
13
+ */
14
+
15
+ /* eslint-disable */
16
+ // @ts-nocheck
17
17
  `;
18
18
  }
19
19
  /**
@@ -120,11 +120,11 @@ function generateFunctionBody(method, isRaw, generateOptions) {
120
120
  const argsExtraction = hasParams
121
121
  ? ` const _hasKey = typeof args[0] === 'string'\n const params = _hasKey ? args[1] : args[0]\n const options = _hasKey ? { cacheKey: args[0], ...args[2] } : args[1]`
122
122
  : ` const _hasKey = typeof args[0] === 'string'\n const options = _hasKey ? { cacheKey: args[0], ...args[1] } : args[0]`;
123
- return `${description}export function ${composableName}(key: string, ${args})
124
- export function ${composableName}(${args})
125
- export function ${composableName}(...args: any[]) {
126
- ${argsExtraction}${pInit}
127
- return ${wrapperFunction}${responseTypeGeneric}(${key}, ${url}, ${fetchOptions})
123
+ return `${description}export function ${composableName}(key: string, ${args})
124
+ export function ${composableName}(${args})
125
+ export function ${composableName}(...args: any[]) {
126
+ ${argsExtraction}${pInit}
127
+ return ${wrapperFunction}${responseTypeGeneric}(${key}, ${url}, ${fetchOptions})
128
128
  }`;
129
129
  }
130
130
  /**
@@ -62,6 +62,10 @@ export async function generateUseFetchComposables(inputDir, outputDir, options,
62
62
  const sharedHelpersSource = path.resolve(__dirname, '../../../src/generators/shared/runtime/apiHelpers.ts');
63
63
  const sharedHelpersDest = path.join(sharedRuntimeDir, 'apiHelpers.ts');
64
64
  await fs.copyFile(sharedHelpersSource, sharedHelpersDest);
65
+ // Copy shared pagination.ts
66
+ const sharedPaginationSource = path.resolve(__dirname, '../../../src/generators/shared/runtime/pagination.ts');
67
+ const sharedPaginationDest = path.join(sharedRuntimeDir, 'pagination.ts');
68
+ await fs.copyFile(sharedPaginationSource, sharedPaginationDest);
65
69
  mainSpinner.stop('Runtime files copied');
66
70
  // 5. Calculate relative import path from composables to APIs
67
71
  const relativePath = calculateRelativeImportPath(composablesDir, inputDir);
@@ -2,18 +2,18 @@
2
2
  * Generate file header with auto-generation warning
3
3
  */
4
4
  function generateFileHeader() {
5
- return `/**
6
- * ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
7
- *
8
- * This file was automatically generated by nuxt-openapi-generator.
9
- * Any manual changes will be overwritten on the next generation.
10
- *
11
- * @generated by nuxt-openapi-generator
12
- * @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
13
- */
14
-
15
- /* eslint-disable */
16
- // @ts-nocheck
5
+ return `/**
6
+ * ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
7
+ *
8
+ * This file was automatically generated by nuxt-openapi-generator.
9
+ * Any manual changes will be overwritten on the next generation.
10
+ *
11
+ * @generated by nuxt-openapi-generator
12
+ * @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
13
+ */
14
+
15
+ /* eslint-disable */
16
+ // @ts-nocheck
17
17
  `;
18
18
  }
19
19
  /**
@@ -95,8 +95,8 @@ function generateFunctionBody(method, options) {
95
95
  const fetchOptions = generateFetchOptions(method, options);
96
96
  const description = method.description ? `/**\n * ${method.description}\n */\n` : '';
97
97
  const pInit = hasParams ? `\n const p = shallowRef(params)` : '';
98
- return `${description}export const ${method.composableName} = (${args}) => {${pInit}
99
- return useApiRequest${responseTypeGeneric}(${url}, ${fetchOptions})
98
+ return `${description}export const ${method.composableName} = (${args}) => {${pInit}
99
+ return useApiRequest${responseTypeGeneric}(${url}, ${fetchOptions})
100
100
  }`;
101
101
  }
102
102
  /**
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { generateUseFetchComposables } from './generators/use-fetch/generator.js
6
6
  import { generateUseAsyncDataComposables } from './generators/use-async-data/generator.js';
7
7
  import { generateNuxtServerRoutes } from './generators/nuxt-server/generator.js';
8
8
  import { generateConnectors } from './generators/components/connector-generator/generator.js';
9
- import { promptInitialInputs, promptInputPath, promptComposablesSelection, promptServerRoutePath, promptBffConfig, promptGeneratorBackend, } from './cli/prompts.js';
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
12
  import { loadConfig, mergeConfig, parseTags, parseGenerators } from './cli/config.js';
@@ -26,6 +26,7 @@ program
26
26
  .option('-v, --verbose', 'Enable verbose logging', false)
27
27
  .option('--watch', 'Watch mode - regenerate on file changes', false)
28
28
  .option('--generators <types>', 'Generators to use: useFetch,useAsyncData,nuxtServer')
29
+ .option('--connectors', 'Generate headless UI connectors on top of useAsyncData', false)
29
30
  .option('--server-route-path <path>', 'Server route path (for nuxtServer mode)')
30
31
  .option('--enable-bff', 'Enable BFF pattern (for nuxtServer mode)', false)
31
32
  .option('--backend <type>', 'Generator backend: official (Java) or heyapi (Node.js)')
@@ -54,6 +55,8 @@ program
54
55
  backend: options.backend === 'official' || options.backend === 'heyapi'
55
56
  ? options.backend
56
57
  : undefined,
58
+ // Only propagate if explicitly passed — undefined means "ask the user"
59
+ createUseAsyncDataConnectors: options.connectors === true ? true : undefined,
57
60
  });
58
61
  if (config.verbose) {
59
62
  console.log('Configuration:', config);
@@ -85,7 +88,8 @@ program
85
88
  // 1. Determine composables to generate FIRST
86
89
  let composables;
87
90
  if (config.generators) {
88
- composables = config.generators;
91
+ // filter out 'connectors' — handled separately below
92
+ composables = config.generators.filter((g) => g !== 'connectors');
89
93
  if (config.verbose) {
90
94
  console.log(`Using generators from config: ${composables.join(', ')}`);
91
95
  }
@@ -114,7 +118,17 @@ program
114
118
  inputPath = await promptInputPath(config.input);
115
119
  outputPath = config.output ?? './swagger';
116
120
  }
117
- // 3. Ask for server route path if nuxtServer is selected
121
+ // 3. Ask whether to generate headless connectors (only if useAsyncData selected)
122
+ let generateConnectorsFlag = false;
123
+ if (composables.includes('useAsyncData')) {
124
+ if (config.createUseAsyncDataConnectors !== undefined) {
125
+ generateConnectorsFlag = config.createUseAsyncDataConnectors;
126
+ }
127
+ else {
128
+ generateConnectorsFlag = await promptConnectors();
129
+ }
130
+ }
131
+ // 4. Ask for server route path if nuxtServer is selected
118
132
  let serverRoutePath = config.serverRoutePath || '';
119
133
  let enableBff = config.enableBff || false;
120
134
  if (needsNuxtServer && !config.serverRoutePath) {
@@ -181,6 +195,29 @@ program
181
195
  throw error;
182
196
  }
183
197
  }
198
+ // Generate headless connectors if requested (requires useAsyncData)
199
+ if (generateConnectorsFlag) {
200
+ const spinner = p.spinner();
201
+ spinner.start('Generating headless UI connectors...');
202
+ try {
203
+ if (!config.dryRun) {
204
+ await generateConnectors({
205
+ inputSpec: inputPath,
206
+ outputDir: `${composablesOutputDir}/connectors`,
207
+ composablesRelDir: '../use-async-data',
208
+ runtimeRelDir: '../../runtime',
209
+ });
210
+ spinner.stop('✓ Generated headless UI connectors');
211
+ }
212
+ else {
213
+ spinner.stop('Would generate headless UI connectors (dry-run)');
214
+ }
215
+ }
216
+ catch (error) {
217
+ spinner.stop('✗ Failed to generate connectors');
218
+ throw error;
219
+ }
220
+ }
184
221
  if (config.dryRun) {
185
222
  p.outro('🔍 Dry run complete - no files were modified');
186
223
  }
@@ -211,28 +248,4 @@ program
211
248
  process.exit(1);
212
249
  }
213
250
  });
214
- program
215
- .command('connectors')
216
- .description('Generate headless connector composables from an OpenAPI spec')
217
- .requiredOption('-i, --input <path>', 'Path to OpenAPI YAML or JSON spec')
218
- .requiredOption('-o, --output <path>', 'Output directory for connector composables')
219
- .option('--composables-dir <relPath>', 'Relative path from output dir to useAsyncData composables (default: ../use-async-data)')
220
- .option('--runtime-dir <relPath>', 'Relative path from output dir where runtime helpers are copied (default: ../runtime)')
221
- .action(async (options) => {
222
- try {
223
- displayLogo();
224
- p.intro('Generating connector composables…');
225
- await generateConnectors({
226
- inputSpec: options.input,
227
- outputDir: options.output,
228
- composablesRelDir: options.composablesDir,
229
- runtimeRelDir: options.runtimeDir,
230
- });
231
- p.outro('Done!');
232
- }
233
- catch (error) {
234
- p.log.error(`Error: ${String(error)}`);
235
- process.exit(1);
236
- }
237
- });
238
251
  program.parse();
@@ -5,6 +5,7 @@ import { checkJavaInstalled } from '../generate.js';
5
5
  import { generateUseFetchComposables } from '../generators/use-fetch/generator.js';
6
6
  import { generateUseAsyncDataComposables } from '../generators/use-async-data/generator.js';
7
7
  import { generateNuxtServerRoutes } from '../generators/nuxt-server/generator.js';
8
+ import { generateConnectors } from '../generators/components/connector-generator/generator.js';
8
9
  import { createConsoleLogger } from '../cli/logger.js';
9
10
  export default defineNuxtModule({
10
11
  meta: {
@@ -19,6 +20,7 @@ export default defineNuxtModule({
19
20
  enableProductionBuild: true,
20
21
  enableAutoGeneration: false,
21
22
  enableAutoImport: true,
23
+ createUseAsyncDataConnectors: false,
22
24
  },
23
25
  setup(options, nuxt) {
24
26
  // --- Guard: input is required ---
@@ -64,6 +66,20 @@ export default defineNuxtModule({
64
66
  const serverRoutePath = path.resolve(nuxt.options.rootDir, options.serverRoutePath ?? 'server/routes/api');
65
67
  await generateNuxtServerRoutes(resolvedOutput, serverRoutePath, { enableBff: options.enableBff, backend }, logger);
66
68
  }
69
+ // 3. Generate headless connectors if requested (requires useAsyncData)
70
+ if (options.createUseAsyncDataConnectors &&
71
+ selectedGenerators.includes('useAsyncData')) {
72
+ const connectorsOutputDir = path.join(composablesOutputDir, 'connectors');
73
+ const runtimeDir = path.join(resolvedOutput, 'runtime');
74
+ await generateConnectors({
75
+ inputSpec: resolvedInput,
76
+ outputDir: connectorsOutputDir,
77
+ composablesRelDir: '../use-async-data',
78
+ runtimeRelDir: '../../runtime',
79
+ }, logger);
80
+ // Register #nxh alias so generated connector imports resolve
81
+ nuxt.options.alias['#nxh'] = runtimeDir;
82
+ }
67
83
  };
68
84
  // --- Hooks: dev build / production build ---
69
85
  const isDev = nuxt.options.dev;
@@ -88,6 +104,9 @@ export default defineNuxtModule({
88
104
  if (selectedGenerators.includes('useAsyncData')) {
89
105
  addImportsDir(path.join(composablesOutputDir, 'use-async-data', 'composables'));
90
106
  }
107
+ if (options.createUseAsyncDataConnectors && selectedGenerators.includes('useAsyncData')) {
108
+ addImportsDir(path.join(composablesOutputDir, 'connectors'));
109
+ }
91
110
  }
92
111
  },
93
112
  });
@@ -24,4 +24,11 @@ export interface ModuleOptions extends GeneratorConfig {
24
24
  * @default true
25
25
  */
26
26
  enableAutoImport?: boolean;
27
+ /**
28
+ * Generate headless UI connector composables on top of useAsyncData.
29
+ * Connectors provide ready-made logic for tables, pagination, forms and delete actions.
30
+ * Requires useAsyncData to also be in generators.
31
+ * @default false
32
+ */
33
+ createUseAsyncDataConnectors?: boolean;
27
34
  }