routesync 1.0.14 → 1.0.16

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
@@ -249,6 +249,20 @@ const api = defineApi({
249
249
  }, config)
250
250
  ```
251
251
 
252
+ > **Note on Laravel Auto-generation:** When using `routesync sync` or `routesync scan` with the `--zod` flag, RouteSync uses PHP Reflection to automatically generate Zod schemas based on your backend validation rules.
253
+ >
254
+ > **Important:** To ensure your schemas are detected automatically, you **must use Laravel `FormRequest` classes**. Inline `$request->validate([...])` calls inside Controller methods cannot be reliably extracted.
255
+ >
256
+ > ```php
257
+ > // ✅ DO THIS: RouteSync will generate Zod schemas automatically
258
+ > public function store(StoreProductRequest $request)
259
+ >
260
+ > // ❌ AVOID THIS: Validation rules will be ignored
261
+ > public function store(Request $request) {
262
+ > $request->validate([...]);
263
+ > }
264
+ > ```
265
+
252
266
  ---
253
267
 
254
268
  ### Auto-generate TanStack hooks from defineApi
package/dist/cli.js CHANGED
@@ -8406,6 +8406,31 @@ foreach ($routes as $route) {
8406
8406
  }
8407
8407
  }
8408
8408
  }
8409
+
8410
+ // Fallback: Try to parse $request->validate([...]) from source code
8411
+ if (empty($schema)) {
8412
+ $fileName = $reflector->getFileName();
8413
+ $startLine = $reflector->getStartLine();
8414
+ $endLine = $reflector->getEndLine();
8415
+
8416
+ if ($fileName && $startLine !== false && $endLine !== false) {
8417
+ $lines = file($fileName);
8418
+ // startLine is 1-indexed
8419
+ $methodSource = implode("", array_slice($lines, $startLine - 1, $endLine - $startLine + 1));
8420
+
8421
+ // Look for $request->validate([ ... ])
8422
+ if (preg_match('/\\\\$request->validate\\\\s*\\\\(\\\\s*\\\\[(.*?)\\\\]\\\\s*\\\\)/s', $methodSource, $matches)) {
8423
+ $rulesString = $matches[1];
8424
+ // Match 'field' => 'rules'
8425
+ preg_match_all('~[\\'"]([a-zA-Z0-9_.*]+)[\\'"]\\\\s*=>\\\\s*[\\'"](.*?)[\\'"]~', $rulesString, $ruleMatches);
8426
+ if (!empty($ruleMatches[1])) {
8427
+ foreach ($ruleMatches[1] as $index => $field) {
8428
+ $schema[$field] = $ruleMatches[2][$index];
8429
+ }
8430
+ }
8431
+ }
8432
+ }
8433
+ }
8409
8434
  } catch (\\Exception $e) {}
8410
8435
  }
8411
8436
  }
@@ -8689,11 +8714,11 @@ var SDKGenerator = class _SDKGenerator {
8689
8714
  if (r.startsWith("min:")) min = r.split(":")[1];
8690
8715
  if (r.startsWith("max:")) max = r.split(":")[1];
8691
8716
  }
8692
- let zType = "z.any()";
8717
+ let zType = "z.unknown()";
8693
8718
  if (isString) zType = "z.string()";
8694
8719
  else if (isNumeric) zType = "z.number()";
8695
8720
  else if (isBoolean) zType = "z.boolean()";
8696
- else if (isArray) zType = "z.array(z.any())";
8721
+ else if (isArray) zType = "z.array(z.unknown())";
8697
8722
  else if (isEmail) zType = "z.string()";
8698
8723
  if (isEmail && zType === "z.string()") zType += ".email()";
8699
8724
  if (min && !isNaN(Number(min))) zType += `.min(${min})`;
@@ -8742,40 +8767,59 @@ var TypeGenerator = class {
8742
8767
  lines.push(` status?: number`);
8743
8768
  lines.push(`}`);
8744
8769
  lines.push(``);
8745
- const resources = new Set(
8746
- manifest.routes.map((r) => r.path.replace(/^\//, "").split("/")[0])
8747
- );
8748
- for (const resource of resources) {
8749
- const typeName = toTypeName(resource ?? "");
8750
- lines.push(`export interface ${typeName} {`);
8751
- lines.push(` id: number`);
8752
- lines.push(` // TODO: Add ${resource} fields`);
8753
- lines.push(` created_at?: string`);
8754
- lines.push(` updated_at?: string`);
8755
- lines.push(`}`);
8756
- lines.push(``);
8770
+ if (manifest.models && manifest.models.length > 0) {
8771
+ for (const model of manifest.models) {
8772
+ lines.push(`export interface ${model.name} {`);
8773
+ for (const col of model.columns) {
8774
+ if (model.hidden && model.hidden.includes(col.name)) continue;
8775
+ const tsType = this.mapSqlTypeToTs(col.type);
8776
+ const isOptional = col.nullable ? "?" : "";
8777
+ lines.push(` ${col.name}${isOptional}: ${tsType}`);
8778
+ }
8779
+ if (model.appends && model.appends.length > 0) {
8780
+ for (const append of model.appends) {
8781
+ lines.push(` ${append}?: unknown`);
8782
+ }
8783
+ }
8784
+ lines.push(`}`);
8785
+ lines.push(``);
8786
+ }
8787
+ } else {
8788
+ const resources = new Set(
8789
+ manifest.routes.map((r) => r.path.replace(/^\//, "").split("/")[0])
8790
+ );
8791
+ for (const resource of resources) {
8792
+ const typeName = toTypeName(resource ?? "");
8793
+ lines.push(`export interface ${typeName} {`);
8794
+ lines.push(` id: number`);
8795
+ lines.push(` // TODO: Add ${resource} fields`);
8796
+ lines.push(` created_at?: string`);
8797
+ lines.push(` updated_at?: string`);
8798
+ lines.push(`}`);
8799
+ lines.push(``);
8800
+ }
8757
8801
  }
8758
8802
  if (hasSchemas) {
8759
8803
  for (const route of manifest.routes) {
8760
- if (route.schema && Object.keys(route.schema).length > 0) {
8804
+ if (route.schema && route.schema.rules && Object.keys(route.schema.rules).length > 0) {
8761
8805
  const actionName = toMethodName(route);
8762
8806
  const schemaName = actionName + "Schema";
8763
8807
  lines.push(`export const ${schemaName} = z.object({`);
8764
- for (const [field, rules] of Object.entries(route.schema)) {
8808
+ for (const [field, rules] of Object.entries(route.schema.rules)) {
8765
8809
  const ruleStr = Array.isArray(rules) ? rules.join("|") : String(rules);
8766
8810
  let zodRule = "z.string()";
8767
- if (ruleStr.includes("numeric") || ruleStr.includes("integer")) {
8811
+ if (ruleStr.includes("numeric") || ruleStr.includes("integer") || ruleStr.includes("int")) {
8768
8812
  zodRule = "z.number()";
8769
- } else if (ruleStr.includes("boolean")) {
8813
+ } else if (ruleStr.includes("boolean") || ruleStr.includes("bool")) {
8770
8814
  zodRule = "z.boolean()";
8771
8815
  } else if (ruleStr.includes("array")) {
8772
8816
  zodRule = "z.array(z.unknown())";
8773
8817
  }
8774
8818
  if (ruleStr.includes("email")) zodRule += ".email()";
8775
8819
  if (ruleStr.includes("url")) zodRule += ".url()";
8776
- const matchMin = ruleStr.match(/min:(\\d+)/);
8820
+ const matchMin = ruleStr.match(/min:(\d+)/);
8777
8821
  if (matchMin) zodRule += ".min(" + matchMin[1] + ")";
8778
- const matchMax = ruleStr.match(/max:(\\d+)/);
8822
+ const matchMax = ruleStr.match(/max:(\d+)/);
8779
8823
  if (matchMax) zodRule += ".max(" + matchMax[1] + ")";
8780
8824
  if (!ruleStr.includes("required")) {
8781
8825
  zodRule += ".optional()";
@@ -8793,6 +8837,13 @@ var TypeGenerator = class {
8793
8837
  }
8794
8838
  await import_fs_extra4.default.writeFile(import_path3.default.join(outputDir, "types.ts"), lines.join("\n"));
8795
8839
  }
8840
+ static mapSqlTypeToTs(sqlType) {
8841
+ const type = sqlType.toLowerCase();
8842
+ if (type.includes("int") || type.includes("decimal") || type.includes("float") || type.includes("double")) return "number";
8843
+ if (type.includes("bool") || type.includes("tinyint(1)")) return "boolean";
8844
+ if (type.includes("json")) return "unknown";
8845
+ return "string";
8846
+ }
8796
8847
  };
8797
8848
 
8798
8849
  // packages/cli/src/generators/HookGenerator.ts
@@ -8805,26 +8856,38 @@ var HookGenerator = class _HookGenerator {
8805
8856
  lines.push(``);
8806
8857
  lines.push(`import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'`);
8807
8858
  lines.push(`import { api } from './api'`);
8859
+ lines.push(`import * as Types from './types'`);
8808
8860
  lines.push(``);
8809
8861
  const grouped = buildGeneratedRoutes(manifest.routes);
8862
+ const modelNames = (manifest.models || []).map((m) => m.name);
8810
8863
  for (const [group, routes] of Object.entries(grouped)) {
8811
8864
  for (const route of routes) {
8812
8865
  const method = route.method.toUpperCase();
8813
8866
  const hookName = _HookGenerator.toHookName(group, route.actionName);
8814
8867
  const queryKey = `['${group}', '${route.actionName}']`;
8868
+ const possibleModel = _HookGenerator.findMatchingModel(group, modelNames);
8869
+ let responseType = "unknown";
8870
+ if (possibleModel) {
8871
+ if (route.actionName === "get" || route.actionName === "index" || !route.runtimePath.includes(":") && method === "GET") {
8872
+ responseType = `Types.${possibleModel}[]`;
8873
+ } else {
8874
+ responseType = `Types.${possibleModel}`;
8875
+ }
8876
+ }
8877
+ const returnType = possibleModel ? `Types.ApiResponse<${responseType}>` : `unknown`;
8815
8878
  if (method === "GET") {
8816
- lines.push(`export function ${hookName}(params?: Record<string, unknown>) {`);
8817
- lines.push(` return useQuery({`);
8879
+ lines.push(`export function ${hookName}<TData = ${returnType}>(params?: Record<string, unknown>) {`);
8880
+ lines.push(` return useQuery<TData>({`);
8818
8881
  lines.push(` queryKey: ${queryKey},`);
8819
- lines.push(` queryFn: () => api.${group}.${route.actionName}({ query: params })`);
8882
+ lines.push(` queryFn: () => api.${group}.${route.actionName}({ query: params }) as Promise<TData>`);
8820
8883
  lines.push(` })`);
8821
8884
  lines.push(`}`);
8822
8885
  lines.push(``);
8823
8886
  } else {
8824
- lines.push(`export function ${hookName}() {`);
8887
+ lines.push(`export function ${hookName}<TData = ${returnType}>() {`);
8825
8888
  lines.push(` const queryClient = useQueryClient()`);
8826
- lines.push(` return useMutation({`);
8827
- lines.push(` mutationFn: (data: Record<string, unknown>) => api.${group}.${route.actionName}({ body: data }),`);
8889
+ lines.push(` return useMutation<TData, Error, Record<string, unknown>>({`);
8890
+ lines.push(` mutationFn: (data: Record<string, unknown>) => api.${group}.${route.actionName}({ body: data }) as Promise<TData>,`);
8828
8891
  lines.push(` onSuccess: () => {`);
8829
8892
  lines.push(` queryClient.invalidateQueries({ queryKey: ['${group}'] })`);
8830
8893
  lines.push(` }`);
@@ -8839,6 +8902,16 @@ var HookGenerator = class _HookGenerator {
8839
8902
  static toHookName(group, actionName) {
8840
8903
  return `use${toTypeName(group)}${toTypeName(actionName)}`;
8841
8904
  }
8905
+ static findMatchingModel(group, modelNames) {
8906
+ const exact = modelNames.find((m) => m.toLowerCase() === group.toLowerCase());
8907
+ if (exact) return exact;
8908
+ const singularGroup = group.toLowerCase().replace(/ies$/, "y").replace(/s$/, "");
8909
+ const singularMatch = modelNames.find((m) => m.toLowerCase() === singularGroup);
8910
+ if (singularMatch) return singularMatch;
8911
+ const prefixMatch = modelNames.find((m) => m.toLowerCase().startsWith(group.toLowerCase()) || m.toLowerCase().startsWith(singularGroup));
8912
+ if (prefixMatch) return prefixMatch;
8913
+ return void 0;
8914
+ }
8842
8915
  };
8843
8916
 
8844
8917
  // packages/cli/src/generators/NextActionGenerator.ts
@@ -9057,7 +9130,7 @@ var ModelGenerator = class {
9057
9130
  };
9058
9131
 
9059
9132
  // packages/cli/src/commands/generate.ts
9060
- var generateCommand = new Command("generate").description("Generate typed SDK, types, and hooks from route manifest").option("-m, --manifest <path>", "Path to route manifest", "routesync.manifest.json").option("-o, --output <path>", "Output directory", "src/api").option("--no-hooks", "Skip generating React hooks").option("--next-actions", "Generate Next.js Server Actions").option("--msw", "Generate MSW Mock Handlers").option("--echo", "Generate Laravel Echo Hooks").action(async (options) => {
9133
+ var generateCommand = new Command("generate").description("Generate typed SDK, types, and hooks from route manifest").option("-m, --manifest <path>", "Path to route manifest", "routesync.manifest.json").option("-o, --output <path>", "Output directory", "src/api").option("--no-hooks", "Skip generating React hooks").option("--next-actions", "Generate Next.js Server Actions").option("--msw", "Generate MSW Mock Handlers").option("--echo", "Generate Laravel Echo Hooks").option("--zod", "Generate Zod schemas for validation").action(async (options) => {
9061
9134
  const spinner = ora("Generating SDK...").start();
9062
9135
  try {
9063
9136
  if (!import_fs_extra11.default.existsSync(options.manifest)) {
@@ -9070,7 +9143,7 @@ var generateCommand = new Command("generate").description("Generate typed SDK, t
9070
9143
  spinner.text = "Generating types...";
9071
9144
  await TypeGenerator.generate(manifest, options.output);
9072
9145
  spinner.text = "Generating SDK...";
9073
- await SDKGenerator.generate(manifest, options.output);
9146
+ await SDKGenerator.generate(manifest, options.output, options);
9074
9147
  if (options.hooks !== false) {
9075
9148
  spinner.text = "Generating hooks...";
9076
9149
  await HookGenerator.generate(manifest, options.output);
package/dist/react.d.mts CHANGED
@@ -45,7 +45,7 @@ type CallOptions = {
45
45
  headers?: Record<string, string>;
46
46
  };
47
47
  interface EndpointCallable {
48
- (options?: CallOptions): Promise<any>;
48
+ <TResponse = unknown>(options?: CallOptions): Promise<TResponse>;
49
49
  /** Original RouteDefinition — used by useApiQuery / useApiMutation */
50
50
  $def: RouteDefinition;
51
51
  /** Stable TanStack query key: [group, action] */
package/dist/react.d.ts CHANGED
@@ -45,7 +45,7 @@ type CallOptions = {
45
45
  headers?: Record<string, string>;
46
46
  };
47
47
  interface EndpointCallable {
48
- (options?: CallOptions): Promise<any>;
48
+ <TResponse = unknown>(options?: CallOptions): Promise<TResponse>;
49
49
  /** Original RouteDefinition — used by useApiQuery / useApiMutation */
50
50
  $def: RouteDefinition;
51
51
  /** Stable TanStack query key: [group, action] */
package/dist/sdk.d.mts CHANGED
@@ -106,7 +106,7 @@ type CallOptions = {
106
106
  headers?: Record<string, string>;
107
107
  };
108
108
  interface EndpointCallable {
109
- (options?: CallOptions): Promise<any>;
109
+ <TResponse = unknown>(options?: CallOptions): Promise<TResponse>;
110
110
  /** Original RouteDefinition — used by useApiQuery / useApiMutation */
111
111
  $def: RouteDefinition;
112
112
  /** Stable TanStack query key: [group, action] */
package/dist/sdk.d.ts CHANGED
@@ -106,7 +106,7 @@ type CallOptions = {
106
106
  headers?: Record<string, string>;
107
107
  };
108
108
  interface EndpointCallable {
109
- (options?: CallOptions): Promise<any>;
109
+ <TResponse = unknown>(options?: CallOptions): Promise<TResponse>;
110
110
  /** Original RouteDefinition — used by useApiQuery / useApiMutation */
111
111
  $def: RouteDefinition;
112
112
  /** Stable TanStack query key: [group, action] */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routesync",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Laravel routes to typed frontend SDKs.",
5
5
  "main": "./dist/sdk.js",
6
6
  "module": "./dist/sdk.mjs",