nuxt-openapi-hyperfetch 0.2.8-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.
package/README.md CHANGED
@@ -1,4 +1,8 @@
1
- ο»Ώ# πŸš€ Nuxt OpenAPI Generator
1
+ ο»Ώ<p align="center">
2
+ <img src="./public/nuxt-openapi-hyperfetch-logo.png" alt="Nuxt OpenAPI Hyperfetch logo" width="260" />
3
+ </p>
4
+
5
+ # πŸš€ Nuxt OpenAPI Generator
2
6
 
3
7
  **Generate type-safe, SSR-compatible Nuxt composables from OpenAPI/Swagger specifications.**
4
8
 
@@ -6,7 +10,7 @@
6
10
 
7
11
  ---
8
12
 
9
- Transform your API documentation into production-ready **100% Nuxt-native** codeβ€”`useFetch` composables, `useAsyncData` composables, and Nuxt Server Routesβ€”with full TypeScript support, lifecycle callbacks, and request interception. No third-party runtime, no wrappers: just Nuxt.
13
+ Transform your API documentation into production-ready **100% Nuxt-native** codeβ€”`useFetch` composables, `useAsyncData` composables, and Nuxt Server Routesβ€”with full TypeScript support, lifecycle callbacks, and request interception. Use it either as a CLI with `nxh generate` or as a Nuxt module wired directly from `nuxt.config.ts`.
10
14
 
11
15
  ---
12
16
 
@@ -52,6 +56,8 @@ export default {
52
56
 
53
57
  ## πŸ“¦ Installation
54
58
 
59
+ ### Use as CLI
60
+
55
61
  ```bash
56
62
  npm install -g nuxt-openapi-hyperfetch
57
63
  # or
@@ -66,11 +72,49 @@ Or use directly with npx:
66
72
  npx nuxt-openapi-hyperfetch generate
67
73
  ```
68
74
 
75
+ ### Use as Nuxt module
76
+
77
+ Install it in your Nuxt project:
78
+
79
+ ```bash
80
+ npm install -D nuxt-openapi-hyperfetch
81
+ # or
82
+ pnpm add -D nuxt-openapi-hyperfetch
83
+ # or
84
+ yarn add -D nuxt-openapi-hyperfetch
85
+ ```
86
+
87
+ Then register the module in `nuxt.config.ts`:
88
+
89
+ ```ts
90
+ export default defineNuxtConfig({
91
+ modules: ['nuxt-openapi-hyperfetch'],
92
+
93
+ openApiHyperFetch: {
94
+ input: './swagger.yaml',
95
+ output: './composables/api',
96
+ generators: ['useFetch', 'useAsyncData'],
97
+ backend: 'heyapi',
98
+ enableDevBuild: true,
99
+ enableProductionBuild: true,
100
+ enableAutoGeneration: false,
101
+ enableAutoImport: true,
102
+ createUseAsyncDataConnectors: false,
103
+ },
104
+ })
105
+ ```
106
+
107
+ The module uses `openApiHyperFetch` as its Nuxt config key and runs generation during Nuxt build hooks. If you include `nuxtServer` in `generators`, you can also configure `serverRoutePath` and `enableBff` here.
108
+
69
109
  ---
70
110
 
71
111
  ## πŸš€ Quick Start
72
112
 
73
- ### 1. Run the generator
113
+ ### 1. Run the generator with the CLI
114
+
115
+ <p align="center">
116
+ <img src="./public/nuxt-openapi-hyperfetch-cli.png" alt="Nuxt OpenAPI Hyperfetch CLI" width="720" />
117
+ </p>
74
118
 
75
119
  ```bash
76
120
  nxh generate
@@ -89,7 +133,41 @@ Or pass arguments directly:
89
133
  nxh generate -i ./swagger.yaml -o ./api
90
134
  ```
91
135
 
92
- ### 2. Generated output
136
+ ### 2. Or generate through the Nuxt module
137
+
138
+ If you prefer generation to run from Nuxt itself, add the module and configure it in `nuxt.config.ts`:
139
+
140
+ ```ts
141
+ export default defineNuxtConfig({
142
+ modules: ['nuxt-openapi-hyperfetch'],
143
+
144
+ openApiHyperFetch: {
145
+ input: './swagger.yaml',
146
+ output: './composables/api',
147
+ generators: ['useFetch', 'useAsyncData', 'nuxtServer'],
148
+ backend: 'heyapi',
149
+ serverRoutePath: 'server/routes/api',
150
+ enableBff: false,
151
+ enableAutoImport: true,
152
+ enableAutoGeneration: true,
153
+ },
154
+ })
155
+ ```
156
+
157
+ Useful module options:
158
+
159
+ - `input`: OpenAPI file path relative to the Nuxt root.
160
+ - `output`: Directory where the generated SDK/composables are written.
161
+ - `generators`: Any combination of `useFetch`, `useAsyncData`, and `nuxtServer`.
162
+ - `backend`: `heyapi` or `official`.
163
+ - `enableDevBuild` / `enableProductionBuild`: Control generation before dev/build.
164
+ - `enableAutoGeneration`: Regenerate when the input spec changes in dev mode.
165
+ - `enableAutoImport`: Auto-register generated composables for Nuxt auto-imports.
166
+ - `createUseAsyncDataConnectors`: Generate headless connectors on top of `useAsyncData`.
167
+ - `serverRoutePath`: Output path for generated Nuxt server routes.
168
+ - `enableBff`: Enable the BFF transformer layer for server routes.
169
+
170
+ ### 3. Generated output
93
171
 
94
172
  ```
95
173
  api/
@@ -110,7 +188,7 @@ api/
110
188
  +-- index.ts
111
189
  ```
112
190
 
113
- ### 3. Configure the API base URL
191
+ ### 4. Configure the API base URL
114
192
 
115
193
  Add to `nuxt.config.ts`:
116
194
 
@@ -132,7 +210,7 @@ NUXT_PUBLIC_API_BASE_URL=https://api.example.com
132
210
 
133
211
  All generated `useFetch` and `useAsyncData` composables will automatically use this as `baseURL`. You can still override it per-composable via `options.baseURL`.
134
212
 
135
- ### 4. Use in your Nuxt app
213
+ ### 5. Use in your Nuxt app
136
214
 
137
215
  ```vue
138
216
  <script setup lang="ts">
@@ -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
  }
@@ -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);
package/dist/index.js CHANGED
@@ -55,7 +55,8 @@ program
55
55
  backend: options.backend === 'official' || options.backend === 'heyapi'
56
56
  ? options.backend
57
57
  : undefined,
58
- createUseAsyncDataConnectors: options.connectors,
58
+ // Only propagate if explicitly passed β€” undefined means "ask the user"
59
+ createUseAsyncDataConnectors: options.connectors === true ? true : undefined,
59
60
  });
60
61
  if (config.verbose) {
61
62
  console.log('Configuration:', config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-openapi-hyperfetch",
3
- "version": "0.2.8-alpha.1",
3
+ "version": "0.3.0-beta",
4
4
  "description": "Nuxt useFetch, useAsyncData and Nuxt server OpenAPI generator",
5
5
  "type": "module",
6
6
  "author": "",
@@ -122,20 +122,64 @@ function buildZodSchemas(resource: ResourceInfo): string {
122
122
  return lines.join('\n');
123
123
  }
124
124
 
125
+ /**
126
+ * Build a const array with the column definitions inferred from the resource.
127
+ * Returns an empty string if the resource has no columns.
128
+ */
129
+ function buildColumns(resource: ResourceInfo): string {
130
+ if (!resource.columns || resource.columns.length === 0) {
131
+ return '';
132
+ }
133
+ const camel = resource.composableName
134
+ .replace(/^use/, '')
135
+ .replace(/Connector$/, '')
136
+ .replace(/^./, (c) => c.toLowerCase());
137
+ const varName = `${camel}Columns`;
138
+ const entries = resource.columns
139
+ .map((col) => ` { key: '${col.key}', label: '${col.label}', type: '${col.type}' }`)
140
+ .join(',\n');
141
+ return `const ${varName} = [\n${entries},\n];`;
142
+ }
143
+
125
144
  /**
126
145
  * Build the body of the exported connector function.
127
146
  */
128
147
  function buildFunctionBody(resource: ResourceInfo): string {
129
148
  const pascal = pascalCase(resource.name);
149
+ const hasColumns = resource.columns && resource.columns.length > 0;
150
+ const camel = resource.composableName
151
+ .replace(/^use/, '')
152
+ .replace(/Connector$/, '')
153
+ .replace(/^./, (c) => c.toLowerCase());
154
+ const columnsVar = `${camel}Columns`;
130
155
  const subConnectors: string[] = [];
131
156
 
157
+ // Destructure options param β€” only what's relevant for this resource
158
+ const optionKeys: string[] = [];
159
+ if (resource.listEndpoint && hasColumns) {
160
+ optionKeys.push('columnLabels', 'columnLabel');
161
+ }
162
+ if (resource.createEndpoint && resource.zodSchemas.create) {
163
+ optionKeys.push('createSchema');
164
+ }
165
+ if (resource.updateEndpoint && resource.zodSchemas.update) {
166
+ optionKeys.push('updateSchema');
167
+ }
168
+
169
+ const optionsDestructure =
170
+ optionKeys.length > 0 ? ` const { ${optionKeys.join(', ')} } = options;\n` : '';
171
+
132
172
  if (resource.listEndpoint) {
133
173
  const fn = toAsyncDataName(resource.listEndpoint.operationId);
134
174
  // paginated: true tells useListConnector to expose pagination helpers
135
175
  // (goToPage, nextPage, prevPage, setPerPage, pagination ref).
136
176
  // We set it whenever the spec declares a list endpoint that has a response schema,
137
177
  // which is a reliable proxy for "this API returns structured data worth paginating".
138
- const opts = resource.listEndpoint.responseSchema ? '{ paginated: true }' : '{}';
178
+ const paginatedFlag = resource.listEndpoint.responseSchema ? 'paginated: true' : '';
179
+ const columnsArg = hasColumns ? `columns: ${columnsVar}` : '';
180
+ const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
181
+ const allArgs = [paginatedFlag, columnsArg, labelArgs].filter(Boolean).join(', ');
182
+ const opts = allArgs ? `{ ${allArgs} }` : '{}';
139
183
  subConnectors.push(` const table = useListConnector(${fn}, ${opts});`);
140
184
  }
141
185
 
@@ -146,7 +190,9 @@ function buildFunctionBody(resource: ResourceInfo): string {
146
190
 
147
191
  if (resource.createEndpoint) {
148
192
  const fn = toAsyncDataName(resource.createEndpoint.operationId);
149
- const schemaArg = resource.zodSchemas.create ? `{ schema: ${pascal}CreateSchema }` : '{}';
193
+ const schemaArg = resource.zodSchemas.create
194
+ ? `{ schema: ${pascal}CreateSchema, schemaOverride: createSchema }`
195
+ : '{}';
150
196
  subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg});`);
151
197
  }
152
198
 
@@ -163,10 +209,10 @@ function buildFunctionBody(resource: ResourceInfo): string {
163
209
  let schemaArg = '{}';
164
210
  if (resource.zodSchemas.update && hasDetail) {
165
211
  // Best case: validate AND pre-fill from detail
166
- schemaArg = `{ schema: ${pascal}UpdateSchema, loadWith: detail }`;
212
+ schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema, loadWith: detail }`;
167
213
  } else if (resource.zodSchemas.update) {
168
214
  // Validate, but no detail endpoint to pre-fill from
169
- schemaArg = `{ schema: ${pascal}UpdateSchema }`;
215
+ schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema }`;
170
216
  } else if (hasDetail) {
171
217
  // No Zod schema (no request body in spec), but still pre-fill from detail
172
218
  schemaArg = `{ loadWith: detail }`;
@@ -200,11 +246,14 @@ function buildFunctionBody(resource: ResourceInfo): string {
200
246
  const returnStatement = ` return { ${returnKeys.join(', ')} };`;
201
247
 
202
248
  return [
203
- `export function ${resource.composableName}() {`,
249
+ `export function ${resource.composableName}(options = {}) {`,
250
+ optionsDestructure.trimEnd(),
204
251
  ...subConnectors,
205
252
  returnStatement,
206
253
  `}`,
207
- ].join('\n');
254
+ ]
255
+ .filter((s) => s !== '')
256
+ .join('\n');
208
257
  }
209
258
 
210
259
  // ─── Public API ───────────────────────────────────────────────────────────────
@@ -220,15 +269,19 @@ export function generateConnectorFile(resource: ResourceInfo, composablesRelDir:
220
269
  const header = generateFileHeader();
221
270
  const imports = buildImports(resource, composablesRelDir);
222
271
  const schemas = buildZodSchemas(resource);
272
+ const columns = buildColumns(resource);
223
273
  const fn = buildFunctionBody(resource);
224
274
 
225
- // Assemble file: header + imports + (optional) Zod blocks + function body.
275
+ // Assemble file: header + imports + (optional) Zod blocks + columns const + function body.
226
276
  // Each section ends with its own trailing newline; join with \n adds one blank
227
277
  // line between sections, which matches Prettier's output for this structure.
228
278
  const parts: string[] = [header, imports];
229
279
  if (schemas.trim()) {
230
280
  parts.push(schemas);
231
281
  }
282
+ if (columns.trim()) {
283
+ parts.push(columns);
284
+ }
232
285
  parts.push(fn);
233
286
 
234
287
  return parts.join('\n') + '\n';
@@ -19,7 +19,15 @@ import { mergeZodErrors } from './zod-error-merger.js';
19
19
  * @param options { schema, fields, loadWith?, errorConfig? }
20
20
  */
21
21
  export function useFormConnector(composableFn, options = {}) {
22
- const { schema, fields = [], loadWith = null, errorConfig = {} } = options;
22
+ const { schema: baseSchema, schemaOverride, fields = [], loadWith = null, errorConfig = {} } = options;
23
+
24
+ // Resolve the active schema:
25
+ // schemaOverride(base) β€” extend or refine the generated schema
26
+ // schemaOverride β€” replace the generated schema entirely
27
+ // baseSchema β€” the generated schema unchanged (default)
28
+ const schema = schemaOverride
29
+ ? (typeof schemaOverride === 'function' ? schemaOverride(baseSchema) : schemaOverride)
30
+ : baseSchema;
23
31
 
24
32
  // ── Form state ─────────────────────────────────────────────────────────────
25
33
 
@@ -17,7 +17,7 @@ import { ref, computed, shallowRef } from 'vue';
17
17
  * @param options Configuration for the list connector
18
18
  */
19
19
  export function useListConnector(composableFn, options = {}) {
20
- const { paginated = false, columns = [] } = options;
20
+ const { paginated = false, columns = [], columnLabels = {}, columnLabel = null } = options;
21
21
 
22
22
  // ── Execute the underlying composable ──────────────────────────────────────
23
23
  const composable = composableFn({ paginated });
@@ -40,19 +40,19 @@ export function useListConnector(composableFn, options = {}) {
40
40
  const pagination = computed(() => composable.pagination?.value ?? null);
41
41
 
42
42
  function goToPage(page) {
43
- composable.goToPage?.(page);
43
+ composable.pagination?.value?.goToPage?.(page);
44
44
  }
45
45
 
46
46
  function nextPage() {
47
- composable.nextPage?.();
47
+ composable.pagination?.value?.nextPage?.();
48
48
  }
49
49
 
50
50
  function prevPage() {
51
- composable.prevPage?.();
51
+ composable.pagination?.value?.prevPage?.();
52
52
  }
53
53
 
54
54
  function setPerPage(n) {
55
- composable.setPerPage?.(n);
55
+ composable.pagination?.value?.setPerPage?.(n);
56
56
  }
57
57
 
58
58
  // ── Row selection ──────────────────────────────────────────────────────────
@@ -113,10 +113,20 @@ export function useListConnector(composableFn, options = {}) {
113
113
  _deleteTarget.value = row;
114
114
  }
115
115
 
116
+ // Apply label overrides: columnLabel function takes priority over columnLabels map
117
+ const resolvedColumns = computed(() =>
118
+ columns.map((col) => ({
119
+ ...col,
120
+ label: columnLabel
121
+ ? columnLabel(col.key)
122
+ : (columnLabels[col.key] ?? col.label),
123
+ }))
124
+ );
125
+
116
126
  return {
117
127
  // State
118
128
  rows,
119
- columns: computed(() => columns),
129
+ columns: resolvedColumns,
120
130
  loading,
121
131
  error,
122
132
 
@@ -104,6 +104,14 @@ export async function generateUseAsyncDataComposables(
104
104
  );
105
105
  const sharedHelpersDest = path.join(sharedRuntimeDir, 'apiHelpers.ts');
106
106
  await fs.copyFile(sharedHelpersSource, sharedHelpersDest);
107
+
108
+ // Copy shared pagination.ts
109
+ const sharedPaginationSource = path.resolve(
110
+ __dirname,
111
+ '../../../src/generators/shared/runtime/pagination.ts'
112
+ );
113
+ const sharedPaginationDest = path.join(sharedRuntimeDir, 'pagination.ts');
114
+ await fs.copyFile(sharedPaginationSource, sharedPaginationDest);
107
115
  mainSpinner.stop('Runtime files copied');
108
116
 
109
117
  // 5. Calculate relative import path from composables to APIs
@@ -320,10 +320,10 @@ export function useApiAsyncData<T>(
320
320
  ...paginationState.value,
321
321
  hasNextPage: hasNextPage.value,
322
322
  hasPrevPage: hasPrevPage.value,
323
+ goToPage,
324
+ nextPage,
325
+ prevPage,
326
+ setPerPage,
323
327
  })),
324
- goToPage,
325
- nextPage,
326
- prevPage,
327
- setPerPage,
328
328
  };
329
329
  }
@@ -315,10 +315,10 @@ export function useApiAsyncDataRaw<T>(
315
315
  ...paginationState.value,
316
316
  hasNextPage: hasNextPage.value,
317
317
  hasPrevPage: hasPrevPage.value,
318
+ goToPage,
319
+ nextPage,
320
+ prevPage,
321
+ setPerPage,
318
322
  })),
319
- goToPage,
320
- nextPage,
321
- prevPage,
322
- setPerPage,
323
323
  };
324
324
  }
@@ -91,6 +91,14 @@ export async function generateUseFetchComposables(
91
91
  );
92
92
  const sharedHelpersDest = path.join(sharedRuntimeDir, 'apiHelpers.ts');
93
93
  await fs.copyFile(sharedHelpersSource, sharedHelpersDest);
94
+
95
+ // Copy shared pagination.ts
96
+ const sharedPaginationSource = path.resolve(
97
+ __dirname,
98
+ '../../../src/generators/shared/runtime/pagination.ts'
99
+ );
100
+ const sharedPaginationDest = path.join(sharedRuntimeDir, 'pagination.ts');
101
+ await fs.copyFile(sharedPaginationSource, sharedPaginationDest);
94
102
  mainSpinner.stop('Runtime files copied');
95
103
 
96
104
  // 5. Calculate relative import path from composables to APIs
package/src/index.ts CHANGED
@@ -87,7 +87,8 @@ program
87
87
  options.backend === 'official' || options.backend === 'heyapi'
88
88
  ? options.backend
89
89
  : undefined,
90
- createUseAsyncDataConnectors: options.connectors,
90
+ // Only propagate if explicitly passed β€” undefined means "ask the user"
91
+ createUseAsyncDataConnectors: options.connectors === true ? true : undefined,
91
92
  });
92
93
 
93
94
  if (config.verbose) {
@@ -1,5 +0,0 @@
1
- /**
2
- * Placeholder for TanStack Query composables generator
3
- * TODO: Implement TanStack Query generation
4
- */
5
- export declare function generateTanstackQueryComposables(inputDir: string, outputDir: string): void;
@@ -1,11 +0,0 @@
1
- /**
2
- * Placeholder for TanStack Query composables generator
3
- * TODO: Implement TanStack Query generation
4
- */
5
- export function generateTanstackQueryComposables(inputDir, outputDir) {
6
- console.log('\nπŸ“¦ @tanstack/vue-query generator\n');
7
- console.log(` Input: ${inputDir}`);
8
- console.log(` Output: ${outputDir}`);
9
- console.log('\n⚠️ This generator is not yet implemented.');
10
- console.log(' Coming soon!\n');
11
- }