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 +14 -0
- package/dist/cli.js +101 -28
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/sdk.d.mts +1 -1
- package/dist/sdk.d.ts +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
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:(
|
|
8820
|
+
const matchMin = ruleStr.match(/min:(\d+)/);
|
|
8777
8821
|
if (matchMin) zodRule += ".min(" + matchMin[1] + ")";
|
|
8778
|
-
const matchMax = ruleStr.match(/max:(
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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] */
|