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.
@@ -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: string): Promise<void>;
2
- export interface IsRouteEnabledParams {
3
- content: string;
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 { readFile } from "fs/promises";
1
+ import { readdir } from "fs/promises";
2
2
  import { join } from "path";
3
- import { getSharedModuleInfo, isScriptModule, removeGeneratedFile, toKebabCase, writeGeneratedFile } from "./sharedArtifact.js";
3
+ import { getSharedModuleInfo, isScriptModule, writeGeneratedFile } from "./sharedArtifact.js";
4
4
  async function createRoute(path) {
5
- const info = getSharedModuleInfo(path);
6
- if (!isScriptModule(info.relativePath)) return;
7
- const routeDirPath = join("app", "api", "actions", info.dir, toKebabCase(info.name));
8
- const routePath = join(routeDirPath, "route.ts");
9
- const content = await readFile(join("shared", info.relativePath), "utf-8");
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: routePath,
19
- content: `import { createRoute } from "@/server/createResponseFn"
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 { ${info.name} } from "@/shared/${info.importPath}"
41
+ import { createRouteFn, OriginalResponseFn, RouteBodyType, RouteHandler } from "@/server/createResponseFn"
42
+ ${importLines ? `\n${importLines}\n` : ""}
43
+ const routeMap = new Map<string, RouteHandler>()
22
44
 
23
- export const { POST } = createRoute(${info.name})
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
- function isRouteEnabled({ content, name }) {
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));
@@ -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>;
@@ -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 { useId } from "react"
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 type ${clientInputType} = Parameters<typeof ${name}Client> extends [] ? void : Parameters<typeof ${name}Client>[0]
62
+ export const use${upName} = createUse${upName}(${name}Client)
63
+ `;
64
+ const mutationPreset = `import { useId } from "react"
46
65
 
47
- export interface Use${upName}Params<TOnMutateResult = unknown> extends Omit<
48
- UseMutationOptions<Awaited<ReturnType<typeof ${name}Client>>, Error, ${clientInputType}, TOnMutateResult>,
49
- "mutationFn"
50
- > {}
66
+ import { withUseMutationDefaults } from "soda-tanstack-query"
51
67
 
52
- export function use${upName}<TOnMutateResult = unknown>({ onMutate, onSuccess, onError, onSettled, ...rest }: Use${upName}Params<TOnMutateResult> = {}) {
68
+ import { ${name} } from "@/shared/${actionImportPath}"
69
+
70
+ export const createUse${upName} = withUseMutationDefaults<typeof ${name}>(() => {
53
71
  const key = useId()
54
72
 
55
- return useMutation({
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
- ...rest,
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
- let overwrite = true;
128
- try {
129
- const current = await readFile(hookPath, "utf-8");
130
- if (current.trim()) overwrite = false;
131
- const contentType = await getHookTypeFromContent(join(cwd(), hookPath), current);
132
- if (contentType) hookType = contentType;
133
- if (map[hookType] === current) return;
134
- } catch (error) {
135
- overwrite = true;
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
- overwrite,
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) await writeGeneratedFile({
186
- path: hookPath,
187
- content: map[answer]
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)) await writeGeneratedFile({
196
- path: hookPath,
197
- content: map[type]
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, toKebabCase } from "./sharedArtifact.js";
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 removeGeneratedFile({
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 removeGeneratedFile({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdnext",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
@@ -1,44 +1,86 @@
1
- import { readFile } from "fs/promises"
1
+ import { readdir } from "fs/promises"
2
2
  import { join } from "path"
3
3
 
4
- import { getSharedModuleInfo, isScriptModule, removeGeneratedFile, toKebabCase, writeGeneratedFile } from "./sharedArtifact"
4
+ import { getSharedModuleInfo, isScriptModule, writeGeneratedFile } from "./sharedArtifact"
5
5
 
6
- export async function createRoute(path: string) {
7
- const info = getSharedModuleInfo(path)
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
- if (!isScriptModule(info.relativePath)) return
20
+ export interface SharedRouteModule {
21
+ importPath: string
22
+ name: string
23
+ }
10
24
 
11
- const routeDirPath = join("app", "api", "actions", info.dir, toKebabCase(info.name))
12
- const routePath = join(routeDirPath, "route.ts")
25
+ async function getSharedModules(dir: string): Promise<SharedRouteModule[]> {
26
+ const entries = await readdir(dir, { withFileTypes: true })
13
27
 
14
- const content = await readFile(join("shared", info.relativePath), "utf-8")
28
+ const modules: SharedRouteModule[] = []
15
29
 
16
- if (!isRouteEnabled({ content, name: info.name })) {
17
- await removeGeneratedFile({
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
- return
23
- }
33
+ if (entry.isDirectory()) {
34
+ modules.push(...(await getSharedModules(itemPath)))
35
+ continue
36
+ }
24
37
 
25
- await writeGeneratedFile({
26
- path: routePath,
27
- content: `import { createRoute } from "@/server/createResponseFn"
38
+ if (!entry.isFile() || !isScriptModule(entry.name)) continue
28
39
 
29
- import { ${info.name} } from "@/shared/${info.importPath}"
40
+ const info = getSharedModuleInfo(itemPath)
30
41
 
31
- export const { POST } = createRoute(${info.name})
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 IsRouteEnabledParams {
37
- content: string
53
+ export interface GetRouteFileContentParamsItem {
54
+ importPath: string
38
55
  name: string
39
56
  }
40
57
 
41
- function isRouteEnabled({ content, name }: IsRouteEnabledParams) {
42
- const routeRegExp = new RegExp(`\\b${name}\\.route\\s*=\\s*(true\\b|\\{)`)
43
- return routeRegExp.test(content)
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 { useId } from "react"
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 type ${clientInputType} = Parameters<typeof ${name}Client> extends [] ? void : Parameters<typeof ${name}Client>[0]
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
- export interface Use${upName}Params<TOnMutateResult = unknown> extends Omit<
70
- UseMutationOptions<Awaited<ReturnType<typeof ${name}Client>>, Error, ${clientInputType}, TOnMutateResult>,
71
- "mutationFn"
72
- > {}
100
+ import { ${name} } from "@/shared/${actionImportPath}"
73
101
 
74
- export function use${upName}<TOnMutateResult = unknown>({ onMutate, onSuccess, onError, onSettled, ...rest }: Use${upName}Params<TOnMutateResult> = {}) {
102
+ export const createUse${upName} = withUseMutationDefaults<typeof ${name}>(() => {
75
103
  const key = useId()
76
104
 
77
- return useMutation({
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
- return onSettled?.(data, error, variables, onMutateResult, context)
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
- let overwrite = true
171
+ const hookState = await getGeneratedFileState(hookPath)
154
172
 
155
- try {
156
- const current = await readFile(hookPath, "utf-8")
157
- if (current.trim()) overwrite = false
158
- const contentType = await getHookTypeFromContent(join(cwd(), hookPath), current)
159
- if (contentType) hookType = contentType
160
- if (map[hookType] === current) return
161
- } catch (error) {
162
- overwrite = true
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
- overwrite,
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, toKebabCase } from "./sharedArtifact"
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 removeGeneratedFile({
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 removeGeneratedFile({
41
- path: join("app", "api", "actions", relativePath),
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,10 +0,0 @@
1
- import { writeVsCodeSetting } from "./writeVsCodeSetting.js";
2
- function excludeGeneratedFiles() {
3
- return writeVsCodeSetting({
4
- "files.exclude": {
5
- "actions/**": true,
6
- "app/api/actions/**": true
7
- }
8
- });
9
- }
10
- export { excludeGeneratedFiles };
@@ -1,7 +0,0 @@
1
- export interface VsCodeSetting {
2
- [key: string]: unknown;
3
- }
4
- export declare function writeVsCodeSetting(config: VsCodeSetting): Promise<VsCodeSetting>;
5
- export interface NodeError {
6
- code?: string;
7
- }
@@ -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,10 +0,0 @@
1
- import { writeVsCodeSetting } from "./writeVsCodeSetting"
2
-
3
- export function excludeGeneratedFiles() {
4
- return writeVsCodeSetting({
5
- "files.exclude": {
6
- "actions/**": true,
7
- "app/api/actions/**": true,
8
- },
9
- })
10
- }
@@ -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
- }