sdnext 0.0.25 → 0.0.26
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/dist/utils/build.js +0 -2
- package/dist/utils/createRoute.d.ts +7 -3
- package/dist/utils/createRoute.js +56 -23
- package/dist/utils/dev.js +0 -2
- package/dist/utils/hook.d.ts +6 -0
- package/dist/utils/hook.js +72 -43
- package/dist/utils/syncSharedArtifacts.js +3 -9
- package/package.json +1 -1
- package/src/utils/build.ts +0 -3
- package/src/utils/createRoute.ts +69 -27
- package/src/utils/dev.ts +0 -3
- package/src/utils/hook.ts +82 -36
- package/src/utils/syncSharedArtifacts.ts +4 -10
- package/dist/utils/excludeGeneratedFiles.d.ts +0 -1
- package/dist/utils/excludeGeneratedFiles.js +0 -10
- package/dist/utils/writeVsCodeSetting.d.ts +0 -7
- package/dist/utils/writeVsCodeSetting.js +0 -22
- package/src/utils/excludeGeneratedFiles.ts +0 -10
- package/src/utils/writeVsCodeSetting.ts +0 -38
package/dist/utils/build.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readdir, stat } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import { excludeGeneratedFiles } from "./excludeGeneratedFiles.js";
|
|
4
3
|
import { runCommand } from "./runCommand.js";
|
|
5
4
|
import { syncSharedArtifacts } from "./syncSharedArtifacts.js";
|
|
6
5
|
async function buildFolder(dir) {
|
|
@@ -13,7 +12,6 @@ async function buildFolder(dir) {
|
|
|
13
12
|
}
|
|
14
13
|
}
|
|
15
14
|
async function build(options, { args }) {
|
|
16
|
-
await excludeGeneratedFiles();
|
|
17
15
|
await buildFolder("shared");
|
|
18
16
|
if (0 === args.length) return;
|
|
19
17
|
process.exitCode = await runCommand({
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export declare function createRoute(path
|
|
2
|
-
export interface
|
|
3
|
-
|
|
1
|
+
export declare function createRoute(path?: string): Promise<void>;
|
|
2
|
+
export interface SharedRouteModule {
|
|
3
|
+
importPath: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GetRouteFileContentParamsItem {
|
|
7
|
+
importPath: string;
|
|
4
8
|
name: string;
|
|
5
9
|
}
|
|
@@ -1,31 +1,64 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdir } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import { getSharedModuleInfo, isScriptModule,
|
|
3
|
+
import { getSharedModuleInfo, isScriptModule, writeGeneratedFile } from "./sharedArtifact.js";
|
|
4
4
|
async function createRoute(path) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
if (!isRouteEnabled({
|
|
11
|
-
content,
|
|
12
|
-
name: info.name
|
|
13
|
-
})) return void await removeGeneratedFile({
|
|
14
|
-
path: routeDirPath,
|
|
15
|
-
stopPath: join("app", "api", "actions")
|
|
16
|
-
});
|
|
5
|
+
if (path) {
|
|
6
|
+
const info = getSharedModuleInfo(path);
|
|
7
|
+
if (!isScriptModule(info.relativePath)) return;
|
|
8
|
+
}
|
|
9
|
+
const modules = await getSharedModules("shared");
|
|
17
10
|
await writeGeneratedFile({
|
|
18
|
-
path:
|
|
19
|
-
content:
|
|
11
|
+
path: join("app", "api", "action", "[...action]", "route.ts"),
|
|
12
|
+
content: getRouteFileContent(modules)
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async function getSharedModules(dir) {
|
|
16
|
+
const entries = await readdir(dir, {
|
|
17
|
+
withFileTypes: true
|
|
18
|
+
});
|
|
19
|
+
const modules = [];
|
|
20
|
+
for (const entry of entries){
|
|
21
|
+
const itemPath = join(dir, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
modules.push(...await getSharedModules(itemPath));
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!entry.isFile() || !isScriptModule(entry.name)) continue;
|
|
27
|
+
const info = getSharedModuleInfo(itemPath);
|
|
28
|
+
modules.push({
|
|
29
|
+
importPath: info.importPath,
|
|
30
|
+
name: info.name
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
modules.sort((a, b)=>a.importPath.localeCompare(b.importPath));
|
|
34
|
+
return modules;
|
|
35
|
+
}
|
|
36
|
+
function getRouteFileContent(items) {
|
|
37
|
+
const importLines = items.map((item)=>`import { ${item.name} } from "@/shared/${item.importPath}"`).join("\n");
|
|
38
|
+
const registerLines = items.map((item)=>`registerRoute(${item.name})`).join("\n");
|
|
39
|
+
return `import { NextRequest, NextResponse } from "next/server"
|
|
20
40
|
|
|
21
|
-
import {
|
|
41
|
+
import { createRouteFn, OriginalResponseFn, RouteBodyType, RouteHandler } from "@/server/createResponseFn"
|
|
42
|
+
${importLines ? `\n${importLines}\n` : ""}
|
|
43
|
+
const routeMap = new Map<string, RouteHandler>()
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
function registerRoute<TParams extends [arg?: unknown], TData, TPathname extends string, TRouteBodyType extends RouteBodyType = "json">(
|
|
46
|
+
fn: OriginalResponseFn<TParams, TData, TPathname, TRouteBodyType>,
|
|
47
|
+
) {
|
|
48
|
+
if (!fn.route) return
|
|
49
|
+
const pathname = fn.route.pathname.replace(/(^\\/|\\/$)/g, "")
|
|
50
|
+
if (routeMap.has(pathname)) throw new Error(\`pathname \${pathname} is duplicate\`)
|
|
51
|
+
routeMap.set(pathname, createRouteFn(fn))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
${registerLines ? `${registerLines}\n\n` : ""}export function POST(request: NextRequest) {
|
|
55
|
+
const { pathname } = new URL(request.url)
|
|
56
|
+
const routeHandler = routeMap.get(pathname.replace(/(^\\/api\\/action\\/|\\/$)/g, ""))
|
|
57
|
+
|
|
58
|
+
if (!routeHandler) return NextResponse.json({ success: false, data: undefined, message: "Not Found", code: 404 }, { status: 404 })
|
|
59
|
+
|
|
60
|
+
return routeHandler(request)
|
|
26
61
|
}
|
|
27
|
-
|
|
28
|
-
const routeRegExp = new RegExp(`\\b${name}\\.route\\s*=\\s*(true\\b|\\{)`);
|
|
29
|
-
return routeRegExp.test(content);
|
|
62
|
+
`;
|
|
30
63
|
}
|
|
31
64
|
export { createRoute };
|
package/dist/utils/dev.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
3
|
import { buildFolder } from "./build.js";
|
|
4
|
-
import { excludeGeneratedFiles } from "./excludeGeneratedFiles.js";
|
|
5
4
|
import { spawnCommand } from "./runCommand.js";
|
|
6
5
|
async function dev(options, { args }) {
|
|
7
|
-
await excludeGeneratedFiles();
|
|
8
6
|
await buildFolder("shared");
|
|
9
7
|
if (0 === args.length) return;
|
|
10
8
|
const watchPath = fileURLToPath(new URL("./watch.js", import.meta.url));
|
package/dist/utils/hook.d.ts
CHANGED
|
@@ -4,9 +4,15 @@ export type OperationType = HookType | "skip";
|
|
|
4
4
|
export type HookContentMap = Record<HookType, string>;
|
|
5
5
|
export interface HookData extends HookContentMap {
|
|
6
6
|
hookPath: string;
|
|
7
|
+
mutationPreset: string;
|
|
8
|
+
mutationPresetPath: string;
|
|
7
9
|
overwrite: boolean;
|
|
8
10
|
type: HookType;
|
|
9
11
|
}
|
|
12
|
+
export interface GeneratedFileState {
|
|
13
|
+
content: string;
|
|
14
|
+
overwrite: boolean;
|
|
15
|
+
}
|
|
10
16
|
export declare function createHook(path: string, hookMap: Record<string, HookData>): Promise<void>;
|
|
11
17
|
export declare function createHookFromFolder(): Promise<Record<string, HookData>>;
|
|
12
18
|
export declare function hook(options: Record<string, string>, { args }: Command): Promise<void>;
|
package/dist/utils/hook.js
CHANGED
|
@@ -20,9 +20,24 @@ async function getHookTypeFromContent(path, content) {
|
|
|
20
20
|
const type = setting.hook?.[path];
|
|
21
21
|
if (void 0 !== type && "skip" !== type) return type;
|
|
22
22
|
if (content.includes("useMutation")) return "mutation";
|
|
23
|
+
if (content.includes("createUse") && content.includes("@/presets/")) return "mutation";
|
|
23
24
|
if (content.includes("ClientOptional")) return "get";
|
|
24
25
|
if (content.includes("useQuery")) return "query";
|
|
25
26
|
}
|
|
27
|
+
async function getGeneratedFileState(path) {
|
|
28
|
+
try {
|
|
29
|
+
const content = await readFile(path, "utf-8");
|
|
30
|
+
return {
|
|
31
|
+
content,
|
|
32
|
+
overwrite: !content.trim()
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return {
|
|
36
|
+
content: "",
|
|
37
|
+
overwrite: true
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
26
41
|
async function createHook(path, hookMap) {
|
|
27
42
|
path = relative("actions", path).replace(/\\/g, "/");
|
|
28
43
|
const { dir, name, base } = parse(path);
|
|
@@ -32,28 +47,30 @@ async function createHook(path, hookMap) {
|
|
|
32
47
|
const actionImportPath = normalizePathSeparator(join(dir, name));
|
|
33
48
|
const hookName = base.replace(/^./, (char)=>`use${char.toUpperCase()}`);
|
|
34
49
|
const hookPath = join("hooks", dir, hookName);
|
|
50
|
+
const mutationPresetName = `createUse${upName}.ts`;
|
|
51
|
+
const mutationPresetPath = join("presets", dir, mutationPresetName);
|
|
52
|
+
const mutationPresetImportPath = normalizePathSeparator(join(dir, `createUse${upName}`));
|
|
35
53
|
const clientInputType = `${upName}ClientInput`;
|
|
36
|
-
const mutationHook = `import {
|
|
37
|
-
|
|
38
|
-
import { useMutation, UseMutationOptions } from "@tanstack/react-query"
|
|
39
|
-
import { createRequestFn } from "deepsea-tools"
|
|
54
|
+
const mutationHook = `import { createRequestFn } from "deepsea-tools"
|
|
40
55
|
|
|
41
56
|
import { ${name}Action } from "@/actions/${actionImportPath}"
|
|
42
57
|
|
|
58
|
+
import { createUse${upName} } from "@/presets/${mutationPresetImportPath}"
|
|
59
|
+
|
|
43
60
|
export const ${name}Client = createRequestFn(${name}Action)
|
|
44
61
|
|
|
45
|
-
export
|
|
62
|
+
export const use${upName} = createUse${upName}(${name}Client)
|
|
63
|
+
`;
|
|
64
|
+
const mutationPreset = `import { useId } from "react"
|
|
46
65
|
|
|
47
|
-
|
|
48
|
-
UseMutationOptions<Awaited<ReturnType<typeof ${name}Client>>, Error, ${clientInputType}, TOnMutateResult>,
|
|
49
|
-
"mutationFn"
|
|
50
|
-
> {}
|
|
66
|
+
import { withUseMutationDefaults } from "soda-tanstack-query"
|
|
51
67
|
|
|
52
|
-
|
|
68
|
+
import { ${name} } from "@/shared/${actionImportPath}"
|
|
69
|
+
|
|
70
|
+
export const createUse${upName} = withUseMutationDefaults<typeof ${name}>(() => {
|
|
53
71
|
const key = useId()
|
|
54
72
|
|
|
55
|
-
return
|
|
56
|
-
mutationFn: ${name}Client,
|
|
73
|
+
return {
|
|
57
74
|
onMutate(variables, context) {
|
|
58
75
|
message.open({
|
|
59
76
|
key,
|
|
@@ -61,8 +78,6 @@ export function use${upName}<TOnMutateResult = unknown>({ onMutate, onSuccess, o
|
|
|
61
78
|
content: "中...",
|
|
62
79
|
duration: 0,
|
|
63
80
|
})
|
|
64
|
-
|
|
65
|
-
return onMutate?.(variables, context) as TOnMutateResult | Promise<TOnMutateResult>
|
|
66
81
|
},
|
|
67
82
|
onSuccess(data, variables, onMutateResult, context) {
|
|
68
83
|
context.client.invalidateQueries({ queryKey: ["query-${key.replace(/^.+?-/, "")}"] })
|
|
@@ -73,20 +88,13 @@ export function use${upName}<TOnMutateResult = unknown>({ onMutate, onSuccess, o
|
|
|
73
88
|
type: "success",
|
|
74
89
|
content: "成功",
|
|
75
90
|
})
|
|
76
|
-
|
|
77
|
-
return onSuccess?.(data, variables, onMutateResult, context)
|
|
78
91
|
},
|
|
79
92
|
onError(error, variables, onMutateResult, context) {
|
|
80
93
|
message.destroy(key)
|
|
81
|
-
|
|
82
|
-
return onError?.(error, variables, onMutateResult, context)
|
|
83
|
-
},
|
|
84
|
-
onSettled(data, error, variables, onMutateResult, context) {
|
|
85
|
-
return onSettled?.(data, error, variables, onMutateResult, context)
|
|
86
94
|
},
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
}
|
|
95
|
+
onSettled(data, error, variables, onMutateResult, context) {},
|
|
96
|
+
}
|
|
97
|
+
})
|
|
90
98
|
`;
|
|
91
99
|
const getHook = `import { createRequestFn, isNonNullable } from "deepsea-tools"
|
|
92
100
|
import { createUseQuery } from "soda-tanstack-query"
|
|
@@ -124,19 +132,28 @@ export const use${upName} = createUseQuery({
|
|
|
124
132
|
mutation: mutationHook
|
|
125
133
|
};
|
|
126
134
|
let hookType = getHookTypeFromName(name);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
const hookState = await getGeneratedFileState(hookPath);
|
|
136
|
+
const contentType = await getHookTypeFromContent(join(cwd(), hookPath), hookState.content);
|
|
137
|
+
if (contentType) hookType = contentType;
|
|
138
|
+
if ("mutation" === hookType) {
|
|
139
|
+
const mutationPresetState = await getGeneratedFileState(mutationPresetPath);
|
|
140
|
+
if (map[hookType] === hookState.content && mutationPreset === mutationPresetState.content) return;
|
|
141
|
+
hookMap[path] = {
|
|
142
|
+
hookPath,
|
|
143
|
+
mutationPreset,
|
|
144
|
+
mutationPresetPath,
|
|
145
|
+
overwrite: hookState.overwrite && mutationPresetState.overwrite,
|
|
146
|
+
type: hookType,
|
|
147
|
+
...map
|
|
148
|
+
};
|
|
149
|
+
return;
|
|
136
150
|
}
|
|
151
|
+
if (map[hookType] === hookState.content) return;
|
|
137
152
|
hookMap[path] = {
|
|
138
153
|
hookPath,
|
|
139
|
-
|
|
154
|
+
mutationPreset,
|
|
155
|
+
mutationPresetPath,
|
|
156
|
+
overwrite: hookState.overwrite,
|
|
140
157
|
type: hookType,
|
|
141
158
|
...map
|
|
142
159
|
};
|
|
@@ -168,7 +185,7 @@ async function hook(options, { args }) {
|
|
|
168
185
|
const oldEntires = entires.filter(([path, { overwrite }])=>!overwrite);
|
|
169
186
|
const root = cwd();
|
|
170
187
|
const setting = await getSetting();
|
|
171
|
-
for (const [path, { hookPath, overwrite, type, ...map }] of newEntires){
|
|
188
|
+
for (const [path, { hookPath, mutationPresetPath, mutationPreset, overwrite, type, ...map }] of newEntires){
|
|
172
189
|
const resolved = join(root, hookPath);
|
|
173
190
|
const answer = await prompts_select({
|
|
174
191
|
message: path,
|
|
@@ -182,20 +199,32 @@ async function hook(options, { args }) {
|
|
|
182
199
|
});
|
|
183
200
|
setting.hook ??= {};
|
|
184
201
|
setting.hook[resolved] = answer;
|
|
185
|
-
if ("skip" !== answer)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
202
|
+
if ("skip" !== answer) {
|
|
203
|
+
await writeGeneratedFile({
|
|
204
|
+
path: hookPath,
|
|
205
|
+
content: map[answer]
|
|
206
|
+
});
|
|
207
|
+
if ("mutation" === answer) await writeGeneratedFile({
|
|
208
|
+
path: mutationPresetPath,
|
|
209
|
+
content: mutationPreset
|
|
210
|
+
});
|
|
211
|
+
}
|
|
189
212
|
}
|
|
190
213
|
await writeSdNextSetting(setting);
|
|
191
214
|
const overwrites = await prompts_checkbox({
|
|
192
215
|
message: "Please check the hooks you want to overwrite",
|
|
193
216
|
choices: oldEntires.map(([key])=>key)
|
|
194
217
|
});
|
|
195
|
-
for (const [path, { hookPath, overwrite, type, ...map }] of oldEntires)if (overwrites.includes(path))
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
for (const [path, { hookPath, mutationPresetPath, mutationPreset, overwrite, type, ...map }] of oldEntires)if (overwrites.includes(path)) {
|
|
219
|
+
await writeGeneratedFile({
|
|
220
|
+
path: hookPath,
|
|
221
|
+
content: map[type]
|
|
222
|
+
});
|
|
223
|
+
if ("mutation" === type) await writeGeneratedFile({
|
|
224
|
+
path: mutationPresetPath,
|
|
225
|
+
content: mutationPreset
|
|
226
|
+
});
|
|
227
|
+
}
|
|
199
228
|
}
|
|
200
229
|
function isNodeError(error) {
|
|
201
230
|
return "object" == typeof error && null !== error;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "path";
|
|
2
2
|
import { createAction } from "./createAction.js";
|
|
3
3
|
import { createRoute } from "./createRoute.js";
|
|
4
|
-
import { getSharedModuleInfo, isScriptModule, normalizeSharedPath, removeGeneratedFile
|
|
4
|
+
import { getSharedModuleInfo, isScriptModule, normalizeSharedPath, removeGeneratedFile } from "./sharedArtifact.js";
|
|
5
5
|
async function syncSharedArtifacts(path) {
|
|
6
6
|
const info = getSharedModuleInfo(path);
|
|
7
7
|
if (!isScriptModule(info.relativePath)) return;
|
|
@@ -15,10 +15,7 @@ async function removeSharedArtifacts(path) {
|
|
|
15
15
|
path: join("actions", info.relativePath),
|
|
16
16
|
stopPath: "actions"
|
|
17
17
|
});
|
|
18
|
-
await
|
|
19
|
-
path: join("app", "api", "actions", info.dir, toKebabCase(info.name)),
|
|
20
|
-
stopPath: join("app", "api", "actions")
|
|
21
|
-
});
|
|
18
|
+
await createRoute();
|
|
22
19
|
}
|
|
23
20
|
async function removeSharedArtifactDirectory(path) {
|
|
24
21
|
const relativePath = normalizeSharedPath(path);
|
|
@@ -26,9 +23,6 @@ async function removeSharedArtifactDirectory(path) {
|
|
|
26
23
|
path: join("actions", relativePath),
|
|
27
24
|
stopPath: "actions"
|
|
28
25
|
});
|
|
29
|
-
await
|
|
30
|
-
path: join("app", "api", "actions", relativePath),
|
|
31
|
-
stopPath: join("app", "api", "actions")
|
|
32
|
-
});
|
|
26
|
+
await createRoute();
|
|
33
27
|
}
|
|
34
28
|
export { removeSharedArtifactDirectory, removeSharedArtifacts, syncSharedArtifacts };
|
package/package.json
CHANGED
package/src/utils/build.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { join } from "path"
|
|
|
3
3
|
|
|
4
4
|
import { Command } from "commander"
|
|
5
5
|
|
|
6
|
-
import { excludeGeneratedFiles } from "./excludeGeneratedFiles"
|
|
7
6
|
import { runCommand } from "./runCommand"
|
|
8
7
|
import { syncSharedArtifacts } from "./syncSharedArtifacts"
|
|
9
8
|
|
|
@@ -20,8 +19,6 @@ export async function buildFolder(dir: string) {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export async function build(options: Record<string, string>, { args }: Command) {
|
|
23
|
-
await excludeGeneratedFiles()
|
|
24
|
-
|
|
25
22
|
await buildFolder("shared")
|
|
26
23
|
|
|
27
24
|
if (args.length === 0) return
|
package/src/utils/createRoute.ts
CHANGED
|
@@ -1,44 +1,86 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdir } from "fs/promises"
|
|
2
2
|
import { join } from "path"
|
|
3
3
|
|
|
4
|
-
import { getSharedModuleInfo, isScriptModule,
|
|
4
|
+
import { getSharedModuleInfo, isScriptModule, writeGeneratedFile } from "./sharedArtifact"
|
|
5
5
|
|
|
6
|
-
export async function createRoute(path
|
|
7
|
-
|
|
6
|
+
export async function createRoute(path?: string) {
|
|
7
|
+
if (path) {
|
|
8
|
+
const info = getSharedModuleInfo(path)
|
|
9
|
+
if (!isScriptModule(info.relativePath)) return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const modules = await getSharedModules("shared")
|
|
13
|
+
|
|
14
|
+
await writeGeneratedFile({
|
|
15
|
+
path: join("app", "api", "action", "[...action]", "route.ts"),
|
|
16
|
+
content: getRouteFileContent(modules),
|
|
17
|
+
})
|
|
18
|
+
}
|
|
8
19
|
|
|
9
|
-
|
|
20
|
+
export interface SharedRouteModule {
|
|
21
|
+
importPath: string
|
|
22
|
+
name: string
|
|
23
|
+
}
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
const
|
|
25
|
+
async function getSharedModules(dir: string): Promise<SharedRouteModule[]> {
|
|
26
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
13
27
|
|
|
14
|
-
const
|
|
28
|
+
const modules: SharedRouteModule[] = []
|
|
15
29
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
path: routeDirPath,
|
|
19
|
-
stopPath: join("app", "api", "actions"),
|
|
20
|
-
})
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const itemPath = join(dir, entry.name)
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
modules.push(...(await getSharedModules(itemPath)))
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
path: routePath,
|
|
27
|
-
content: `import { createRoute } from "@/server/createResponseFn"
|
|
38
|
+
if (!entry.isFile() || !isScriptModule(entry.name)) continue
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
const info = getSharedModuleInfo(itemPath)
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
modules.push({
|
|
43
|
+
importPath: info.importPath,
|
|
44
|
+
name: info.name,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
modules.sort((a, b) => a.importPath.localeCompare(b.importPath))
|
|
49
|
+
|
|
50
|
+
return modules
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
export interface
|
|
37
|
-
|
|
53
|
+
export interface GetRouteFileContentParamsItem {
|
|
54
|
+
importPath: string
|
|
38
55
|
name: string
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
function
|
|
42
|
-
const
|
|
43
|
-
|
|
58
|
+
function getRouteFileContent(items: GetRouteFileContentParamsItem[]) {
|
|
59
|
+
const importLines = items.map(item => `import { ${item.name} } from "@/shared/${item.importPath}"`).join("\n")
|
|
60
|
+
const registerLines = items.map(item => `registerRoute(${item.name})`).join("\n")
|
|
61
|
+
|
|
62
|
+
return `import { NextRequest, NextResponse } from "next/server"
|
|
63
|
+
|
|
64
|
+
import { createRouteFn, OriginalResponseFn, RouteBodyType, RouteHandler } from "@/server/createResponseFn"
|
|
65
|
+
${importLines ? `\n${importLines}\n` : ""}
|
|
66
|
+
const routeMap = new Map<string, RouteHandler>()
|
|
67
|
+
|
|
68
|
+
function registerRoute<TParams extends [arg?: unknown], TData, TPathname extends string, TRouteBodyType extends RouteBodyType = "json">(
|
|
69
|
+
fn: OriginalResponseFn<TParams, TData, TPathname, TRouteBodyType>,
|
|
70
|
+
) {
|
|
71
|
+
if (!fn.route) return
|
|
72
|
+
const pathname = fn.route.pathname.replace(/(^\\/|\\/$)/g, "")
|
|
73
|
+
if (routeMap.has(pathname)) throw new Error(\`pathname \${pathname} is duplicate\`)
|
|
74
|
+
routeMap.set(pathname, createRouteFn(fn))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
${registerLines ? `${registerLines}\n\n` : ""}export function POST(request: NextRequest) {
|
|
78
|
+
const { pathname } = new URL(request.url)
|
|
79
|
+
const routeHandler = routeMap.get(pathname.replace(/(^\\/api\\/action\\/|\\/$)/g, ""))
|
|
80
|
+
|
|
81
|
+
if (!routeHandler) return NextResponse.json({ success: false, data: undefined, message: "Not Found", code: 404 }, { status: 404 })
|
|
82
|
+
|
|
83
|
+
return routeHandler(request)
|
|
44
84
|
}
|
|
85
|
+
`
|
|
86
|
+
}
|
package/src/utils/dev.ts
CHANGED
|
@@ -4,12 +4,9 @@ import { fileURLToPath } from "url"
|
|
|
4
4
|
import { Command } from "commander"
|
|
5
5
|
|
|
6
6
|
import { buildFolder } from "./build"
|
|
7
|
-
import { excludeGeneratedFiles } from "./excludeGeneratedFiles"
|
|
8
7
|
import { spawnCommand } from "./runCommand"
|
|
9
8
|
|
|
10
9
|
export async function dev(options: Record<string, string>, { args }: Command) {
|
|
11
|
-
await excludeGeneratedFiles()
|
|
12
|
-
|
|
13
10
|
await buildFolder("shared")
|
|
14
11
|
|
|
15
12
|
if (args.length === 0) return
|
package/src/utils/hook.ts
CHANGED
|
@@ -33,6 +33,7 @@ async function getHookTypeFromContent(path: string, content: string): Promise<Ho
|
|
|
33
33
|
const type = setting.hook?.[path]
|
|
34
34
|
if (type !== undefined && type !== "skip") return type
|
|
35
35
|
if (content.includes("useMutation")) return "mutation"
|
|
36
|
+
if (content.includes("createUse") && content.includes("@/presets/")) return "mutation"
|
|
36
37
|
if (content.includes("ClientOptional")) return "get"
|
|
37
38
|
if (content.includes("useQuery")) return "query"
|
|
38
39
|
return undefined
|
|
@@ -40,10 +41,33 @@ async function getHookTypeFromContent(path: string, content: string): Promise<Ho
|
|
|
40
41
|
|
|
41
42
|
export interface HookData extends HookContentMap {
|
|
42
43
|
hookPath: string
|
|
44
|
+
mutationPreset: string
|
|
45
|
+
mutationPresetPath: string
|
|
43
46
|
overwrite: boolean
|
|
44
47
|
type: HookType
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
export interface GeneratedFileState {
|
|
51
|
+
content: string
|
|
52
|
+
overwrite: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getGeneratedFileState(path: string): Promise<GeneratedFileState> {
|
|
56
|
+
try {
|
|
57
|
+
const content = await readFile(path, "utf-8")
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
content,
|
|
61
|
+
overwrite: !content.trim(),
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
content: "",
|
|
66
|
+
overwrite: true,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
export async function createHook(path: string, hookMap: Record<string, HookData>) {
|
|
48
72
|
path = relative("actions", path).replace(/\\/g, "/")
|
|
49
73
|
const { dir, name, base } = parse(path)
|
|
@@ -53,29 +77,32 @@ export async function createHook(path: string, hookMap: Record<string, HookData>
|
|
|
53
77
|
const actionImportPath = normalizePathSeparator(join(dir, name))
|
|
54
78
|
const hookName = base.replace(/^./, char => `use${char.toUpperCase()}`)
|
|
55
79
|
const hookPath = join("hooks", dir, hookName)
|
|
80
|
+
const mutationPresetName = `createUse${upName}.ts`
|
|
81
|
+
const mutationPresetPath = join("presets", dir, mutationPresetName)
|
|
82
|
+
const mutationPresetImportPath = normalizePathSeparator(join(dir, `createUse${upName}`))
|
|
56
83
|
const clientInputType = `${upName}ClientInput`
|
|
57
84
|
|
|
58
|
-
const mutationHook = `import {
|
|
59
|
-
|
|
60
|
-
import { useMutation, UseMutationOptions } from "@tanstack/react-query"
|
|
61
|
-
import { createRequestFn } from "deepsea-tools"
|
|
85
|
+
const mutationHook = `import { createRequestFn } from "deepsea-tools"
|
|
62
86
|
|
|
63
87
|
import { ${name}Action } from "@/actions/${actionImportPath}"
|
|
64
88
|
|
|
89
|
+
import { createUse${upName} } from "@/presets/${mutationPresetImportPath}"
|
|
90
|
+
|
|
65
91
|
export const ${name}Client = createRequestFn(${name}Action)
|
|
66
92
|
|
|
67
|
-
export
|
|
93
|
+
export const use${upName} = createUse${upName}(${name}Client)
|
|
94
|
+
`
|
|
95
|
+
|
|
96
|
+
const mutationPreset = `import { useId } from "react"
|
|
97
|
+
|
|
98
|
+
import { withUseMutationDefaults } from "soda-tanstack-query"
|
|
68
99
|
|
|
69
|
-
|
|
70
|
-
UseMutationOptions<Awaited<ReturnType<typeof ${name}Client>>, Error, ${clientInputType}, TOnMutateResult>,
|
|
71
|
-
"mutationFn"
|
|
72
|
-
> {}
|
|
100
|
+
import { ${name} } from "@/shared/${actionImportPath}"
|
|
73
101
|
|
|
74
|
-
export
|
|
102
|
+
export const createUse${upName} = withUseMutationDefaults<typeof ${name}>(() => {
|
|
75
103
|
const key = useId()
|
|
76
104
|
|
|
77
|
-
return
|
|
78
|
-
mutationFn: ${name}Client,
|
|
105
|
+
return {
|
|
79
106
|
onMutate(variables, context) {
|
|
80
107
|
message.open({
|
|
81
108
|
key,
|
|
@@ -83,8 +110,6 @@ export function use${upName}<TOnMutateResult = unknown>({ onMutate, onSuccess, o
|
|
|
83
110
|
content: "中...",
|
|
84
111
|
duration: 0,
|
|
85
112
|
})
|
|
86
|
-
|
|
87
|
-
return onMutate?.(variables, context) as TOnMutateResult | Promise<TOnMutateResult>
|
|
88
113
|
},
|
|
89
114
|
onSuccess(data, variables, onMutateResult, context) {
|
|
90
115
|
context.client.invalidateQueries({ queryKey: ["query-${key.replace(/^.+?-/, "")}"] })
|
|
@@ -95,20 +120,13 @@ export function use${upName}<TOnMutateResult = unknown>({ onMutate, onSuccess, o
|
|
|
95
120
|
type: "success",
|
|
96
121
|
content: "成功",
|
|
97
122
|
})
|
|
98
|
-
|
|
99
|
-
return onSuccess?.(data, variables, onMutateResult, context)
|
|
100
123
|
},
|
|
101
124
|
onError(error, variables, onMutateResult, context) {
|
|
102
125
|
message.destroy(key)
|
|
103
|
-
|
|
104
|
-
return onError?.(error, variables, onMutateResult, context)
|
|
105
126
|
},
|
|
106
|
-
onSettled(data, error, variables, onMutateResult, context) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
...rest,
|
|
110
|
-
})
|
|
111
|
-
}
|
|
127
|
+
onSettled(data, error, variables, onMutateResult, context) {},
|
|
128
|
+
}
|
|
129
|
+
})
|
|
112
130
|
`
|
|
113
131
|
|
|
114
132
|
const getHook = `import { createRequestFn, isNonNullable } from "deepsea-tools"
|
|
@@ -150,21 +168,35 @@ export const use${upName} = createUseQuery({
|
|
|
150
168
|
}
|
|
151
169
|
|
|
152
170
|
let hookType = getHookTypeFromName(name)
|
|
153
|
-
|
|
171
|
+
const hookState = await getGeneratedFileState(hookPath)
|
|
154
172
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
const contentType = await getHookTypeFromContent(join(cwd(), hookPath), hookState.content)
|
|
174
|
+
if (contentType) hookType = contentType
|
|
175
|
+
|
|
176
|
+
if (hookType === "mutation") {
|
|
177
|
+
const mutationPresetState = await getGeneratedFileState(mutationPresetPath)
|
|
178
|
+
|
|
179
|
+
if (map[hookType] === hookState.content && mutationPreset === mutationPresetState.content) return
|
|
180
|
+
|
|
181
|
+
hookMap[path] = {
|
|
182
|
+
hookPath,
|
|
183
|
+
mutationPreset,
|
|
184
|
+
mutationPresetPath,
|
|
185
|
+
overwrite: hookState.overwrite && mutationPresetState.overwrite,
|
|
186
|
+
type: hookType,
|
|
187
|
+
...map,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return
|
|
163
191
|
}
|
|
164
192
|
|
|
193
|
+
if (map[hookType] === hookState.content) return
|
|
194
|
+
|
|
165
195
|
hookMap[path] = {
|
|
166
196
|
hookPath,
|
|
167
|
-
|
|
197
|
+
mutationPreset,
|
|
198
|
+
mutationPresetPath,
|
|
199
|
+
overwrite: hookState.overwrite,
|
|
168
200
|
type: hookType,
|
|
169
201
|
...map,
|
|
170
202
|
}
|
|
@@ -213,7 +245,7 @@ export async function hook(options: Record<string, string>, { args }: Command) {
|
|
|
213
245
|
|
|
214
246
|
const setting = await getSetting()
|
|
215
247
|
|
|
216
|
-
for (const [path, { hookPath, overwrite, type, ...map }] of newEntires) {
|
|
248
|
+
for (const [path, { hookPath, mutationPresetPath, mutationPreset, overwrite, type, ...map }] of newEntires) {
|
|
217
249
|
const resolved = join(root, hookPath)
|
|
218
250
|
|
|
219
251
|
const answer = await select<OperationType>({
|
|
@@ -231,6 +263,13 @@ export async function hook(options: Record<string, string>, { args }: Command) {
|
|
|
231
263
|
path: hookPath,
|
|
232
264
|
content: map[answer],
|
|
233
265
|
})
|
|
266
|
+
|
|
267
|
+
if (answer !== "mutation") continue
|
|
268
|
+
|
|
269
|
+
await writeGeneratedFile({
|
|
270
|
+
path: mutationPresetPath,
|
|
271
|
+
content: mutationPreset,
|
|
272
|
+
})
|
|
234
273
|
}
|
|
235
274
|
|
|
236
275
|
await writeSdNextSetting(setting)
|
|
@@ -240,13 +279,20 @@ export async function hook(options: Record<string, string>, { args }: Command) {
|
|
|
240
279
|
choices: oldEntires.map(([key]) => key),
|
|
241
280
|
})
|
|
242
281
|
|
|
243
|
-
for (const [path, { hookPath, overwrite, type, ...map }] of oldEntires) {
|
|
282
|
+
for (const [path, { hookPath, mutationPresetPath, mutationPreset, overwrite, type, ...map }] of oldEntires) {
|
|
244
283
|
if (!overwrites.includes(path)) continue
|
|
245
284
|
|
|
246
285
|
await writeGeneratedFile({
|
|
247
286
|
path: hookPath,
|
|
248
287
|
content: map[type],
|
|
249
288
|
})
|
|
289
|
+
|
|
290
|
+
if (type !== "mutation") continue
|
|
291
|
+
|
|
292
|
+
await writeGeneratedFile({
|
|
293
|
+
path: mutationPresetPath,
|
|
294
|
+
content: mutationPreset,
|
|
295
|
+
})
|
|
250
296
|
}
|
|
251
297
|
}
|
|
252
298
|
|
|
@@ -2,7 +2,7 @@ import { join } from "path"
|
|
|
2
2
|
|
|
3
3
|
import { createAction } from "./createAction"
|
|
4
4
|
import { createRoute } from "./createRoute"
|
|
5
|
-
import { getSharedModuleInfo, isScriptModule, normalizeSharedPath, removeGeneratedFile
|
|
5
|
+
import { getSharedModuleInfo, isScriptModule, normalizeSharedPath, removeGeneratedFile } from "./sharedArtifact"
|
|
6
6
|
|
|
7
7
|
export async function syncSharedArtifacts(path: string) {
|
|
8
8
|
const info = getSharedModuleInfo(path)
|
|
@@ -23,10 +23,7 @@ export async function removeSharedArtifacts(path: string) {
|
|
|
23
23
|
stopPath: "actions",
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
await
|
|
27
|
-
path: join("app", "api", "actions", info.dir, toKebabCase(info.name)),
|
|
28
|
-
stopPath: join("app", "api", "actions"),
|
|
29
|
-
})
|
|
26
|
+
await createRoute()
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
export async function removeSharedArtifactDirectory(path: string) {
|
|
@@ -37,8 +34,5 @@ export async function removeSharedArtifactDirectory(path: string) {
|
|
|
37
34
|
stopPath: "actions",
|
|
38
35
|
})
|
|
39
36
|
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
stopPath: join("app", "api", "actions"),
|
|
43
|
-
})
|
|
44
|
-
}
|
|
37
|
+
await createRoute()
|
|
38
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function excludeGeneratedFiles(): Promise<import("./writeVsCodeSetting").VsCodeSetting>;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2
|
-
import { merge } from "deepsea-tools";
|
|
3
|
-
async function writeVsCodeSetting(config) {
|
|
4
|
-
await mkdir(".vscode", {
|
|
5
|
-
recursive: true
|
|
6
|
-
});
|
|
7
|
-
let data;
|
|
8
|
-
try {
|
|
9
|
-
const json = await readFile(".vscode/settings.json", "utf-8");
|
|
10
|
-
data = JSON.parse(json);
|
|
11
|
-
} catch (error) {
|
|
12
|
-
if (!isNodeError(error) || "ENOENT" !== error.code) throw new Error("Failed to read .vscode/settings.json. Please ensure it is a valid JSON file.");
|
|
13
|
-
data = {};
|
|
14
|
-
}
|
|
15
|
-
data = merge(data, config);
|
|
16
|
-
await writeFile(".vscode/settings.json", JSON.stringify(data, null, 4));
|
|
17
|
-
return data;
|
|
18
|
-
}
|
|
19
|
-
function isNodeError(error) {
|
|
20
|
-
return "object" == typeof error && null !== error;
|
|
21
|
-
}
|
|
22
|
-
export { writeVsCodeSetting };
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "fs/promises"
|
|
2
|
-
|
|
3
|
-
import { merge } from "deepsea-tools"
|
|
4
|
-
|
|
5
|
-
export interface VsCodeSetting {
|
|
6
|
-
[key: string]: unknown
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export async function writeVsCodeSetting(config: VsCodeSetting) {
|
|
10
|
-
await mkdir(".vscode", { recursive: true })
|
|
11
|
-
|
|
12
|
-
let data: VsCodeSetting
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const json = await readFile(".vscode/settings.json", "utf-8")
|
|
16
|
-
data = JSON.parse(json) as VsCodeSetting
|
|
17
|
-
} catch (error) {
|
|
18
|
-
if (!isNodeError(error) || error.code !== "ENOENT") {
|
|
19
|
-
throw new Error("Failed to read .vscode/settings.json. Please ensure it is a valid JSON file.")
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
data = {}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
data = merge(data, config) as VsCodeSetting
|
|
26
|
-
|
|
27
|
-
await writeFile(".vscode/settings.json", JSON.stringify(data, null, 4))
|
|
28
|
-
|
|
29
|
-
return data
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface NodeError {
|
|
33
|
-
code?: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isNodeError(error: unknown): error is NodeError {
|
|
37
|
-
return typeof error === "object" && error !== null
|
|
38
|
-
}
|