sdnext 0.0.23 → 0.0.25

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.
Files changed (37) hide show
  1. package/README.md +111 -1
  2. package/dist/index.js +145 -14
  3. package/dist/utils/build.js +7 -8
  4. package/dist/utils/createAction.js +10 -17
  5. package/dist/utils/createRoute.d.ts +5 -0
  6. package/dist/utils/createRoute.js +31 -0
  7. package/dist/utils/dev.js +24 -9
  8. package/dist/utils/excludeGeneratedFiles.d.ts +1 -0
  9. package/dist/utils/{excludeActions.js → excludeGeneratedFiles.js} +4 -3
  10. package/dist/utils/hook.d.ts +5 -1
  11. package/dist/utils/hook.js +42 -35
  12. package/dist/utils/readSdNextSetting.d.ts +3 -0
  13. package/dist/utils/readSdNextSetting.js +8 -3
  14. package/dist/utils/runCommand.d.ts +7 -0
  15. package/dist/utils/runCommand.js +25 -0
  16. package/dist/utils/sharedArtifact.d.ts +23 -0
  17. package/dist/utils/sharedArtifact.js +67 -0
  18. package/dist/utils/syncSharedArtifacts.d.ts +3 -0
  19. package/dist/utils/syncSharedArtifacts.js +34 -0
  20. package/dist/utils/watch.js +5 -21
  21. package/dist/utils/writeVsCodeSetting.d.ts +7 -1
  22. package/dist/utils/writeVsCodeSetting.js +4 -0
  23. package/package.json +2 -2
  24. package/src/index.ts +5 -4
  25. package/src/utils/build.ts +6 -9
  26. package/src/utils/createAction.ts +12 -19
  27. package/src/utils/createRoute.ts +44 -0
  28. package/src/utils/dev.ts +26 -9
  29. package/src/utils/{excludeActions.ts → excludeGeneratedFiles.ts} +2 -1
  30. package/src/utils/hook.ts +49 -44
  31. package/src/utils/readSdNextSetting.ts +14 -4
  32. package/src/utils/runCommand.ts +35 -0
  33. package/src/utils/sharedArtifact.ts +90 -0
  34. package/src/utils/syncSharedArtifacts.ts +44 -0
  35. package/src/utils/watch.ts +5 -16
  36. package/src/utils/writeVsCodeSetting.ts +20 -4
  37. package/dist/utils/excludeActions.d.ts +0 -1
@@ -0,0 +1,7 @@
1
+ import { ChildProcess } from "child_process";
2
+ export interface SpawnCommandParams {
3
+ args: string[];
4
+ }
5
+ export declare function spawnCommand({ args }: SpawnCommandParams): ChildProcess;
6
+ export declare function runCommand({ args }: SpawnCommandParams): Promise<number>;
7
+ export declare function waitForChild(child: ChildProcess): Promise<number>;
@@ -0,0 +1,25 @@
1
+ import { spawn } from "child_process";
2
+ function spawnCommand({ args }) {
3
+ const [command, ...commandArgs] = args;
4
+ return spawn(command, commandArgs, {
5
+ shell: true,
6
+ stdio: "inherit"
7
+ });
8
+ }
9
+ async function runCommand({ args }) {
10
+ if (0 === args.length) return 0;
11
+ const child = spawnCommand({
12
+ args
13
+ });
14
+ return waitForChild(child);
15
+ }
16
+ async function waitForChild(child) {
17
+ return await new Promise((resolve, reject)=>{
18
+ child.once("error", reject);
19
+ child.once("close", (code, signal)=>{
20
+ if ("number" == typeof code) return void resolve(code);
21
+ resolve(signal ? 1 : 0);
22
+ });
23
+ });
24
+ }
25
+ export { runCommand, spawnCommand, waitForChild };
@@ -0,0 +1,23 @@
1
+ export interface SharedModuleInfo {
2
+ dir: string;
3
+ ext: string;
4
+ importPath: string;
5
+ name: string;
6
+ relativePath: string;
7
+ }
8
+ export interface GeneratedFileParams {
9
+ content: string;
10
+ path: string;
11
+ }
12
+ export interface RemoveGeneratedFileParams {
13
+ path: string;
14
+ stopPath?: string;
15
+ }
16
+ export declare const scriptModuleExtensions: string[];
17
+ export declare function normalizePathSeparator(path: string): string;
18
+ export declare function isScriptModule(path: string): boolean;
19
+ export declare function normalizeSharedPath(path: string): string;
20
+ export declare function getSharedModuleInfo(path: string): SharedModuleInfo;
21
+ export declare function toKebabCase(name: string): string;
22
+ export declare function writeGeneratedFile({ content, path }: GeneratedFileParams): Promise<void>;
23
+ export declare function removeGeneratedFile({ path, stopPath }: RemoveGeneratedFileParams): Promise<void>;
@@ -0,0 +1,67 @@
1
+ import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
2
+ import { dirname, join, parse, relative } from "path";
3
+ const scriptModuleExtensions = [
4
+ ".ts",
5
+ ".tsx",
6
+ ".js",
7
+ ".jsx"
8
+ ];
9
+ function normalizePathSeparator(path) {
10
+ return path.replace(/\\/g, "/");
11
+ }
12
+ function isScriptModule(path) {
13
+ const { ext } = parse(path);
14
+ return scriptModuleExtensions.includes(ext);
15
+ }
16
+ function normalizeSharedPath(path) {
17
+ return normalizePathSeparator(relative("shared", path));
18
+ }
19
+ function getSharedModuleInfo(path) {
20
+ const relativePath = normalizeSharedPath(path);
21
+ const { dir, name, ext } = parse(relativePath);
22
+ return {
23
+ dir,
24
+ ext,
25
+ importPath: normalizePathSeparator(join(dir, name)),
26
+ name,
27
+ relativePath
28
+ };
29
+ }
30
+ function toKebabCase(name) {
31
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
32
+ }
33
+ async function writeGeneratedFile({ content, path }) {
34
+ try {
35
+ const current = await readFile(path, "utf-8");
36
+ if (current === content) return;
37
+ } catch (error) {}
38
+ await mkdir(dirname(path), {
39
+ recursive: true
40
+ });
41
+ await writeFile(path, content);
42
+ }
43
+ async function removeGeneratedFile({ path, stopPath }) {
44
+ await rm(path, {
45
+ force: true,
46
+ recursive: true
47
+ });
48
+ if (!stopPath) return;
49
+ await removeEmptyDirectories(dirname(path), stopPath);
50
+ }
51
+ async function removeEmptyDirectories(path, stopPath) {
52
+ let currentPath = path;
53
+ while(currentPath !== stopPath && "." !== currentPath && currentPath !== dirname(currentPath)){
54
+ try {
55
+ const content = await readdir(currentPath);
56
+ if (content.length > 0) return;
57
+ } catch (error) {
58
+ return;
59
+ }
60
+ await rm(currentPath, {
61
+ force: true,
62
+ recursive: true
63
+ });
64
+ currentPath = dirname(currentPath);
65
+ }
66
+ }
67
+ export { getSharedModuleInfo, isScriptModule, normalizePathSeparator, normalizeSharedPath, removeGeneratedFile, scriptModuleExtensions, toKebabCase, writeGeneratedFile };
@@ -0,0 +1,3 @@
1
+ export declare function syncSharedArtifacts(path: string): Promise<void>;
2
+ export declare function removeSharedArtifacts(path: string): Promise<void>;
3
+ export declare function removeSharedArtifactDirectory(path: string): Promise<void>;
@@ -0,0 +1,34 @@
1
+ import { join } from "path";
2
+ import { createAction } from "./createAction.js";
3
+ import { createRoute } from "./createRoute.js";
4
+ import { getSharedModuleInfo, isScriptModule, normalizeSharedPath, removeGeneratedFile, toKebabCase } from "./sharedArtifact.js";
5
+ async function syncSharedArtifacts(path) {
6
+ const info = getSharedModuleInfo(path);
7
+ if (!isScriptModule(info.relativePath)) return;
8
+ await createAction(path);
9
+ await createRoute(path);
10
+ }
11
+ async function removeSharedArtifacts(path) {
12
+ const info = getSharedModuleInfo(path);
13
+ if (!isScriptModule(info.relativePath)) return;
14
+ await removeGeneratedFile({
15
+ path: join("actions", info.relativePath),
16
+ stopPath: "actions"
17
+ });
18
+ await removeGeneratedFile({
19
+ path: join("app", "api", "actions", info.dir, toKebabCase(info.name)),
20
+ stopPath: join("app", "api", "actions")
21
+ });
22
+ }
23
+ async function removeSharedArtifactDirectory(path) {
24
+ const relativePath = normalizeSharedPath(path);
25
+ await removeGeneratedFile({
26
+ path: join("actions", relativePath),
27
+ stopPath: "actions"
28
+ });
29
+ await removeGeneratedFile({
30
+ path: join("app", "api", "actions", relativePath),
31
+ stopPath: join("app", "api", "actions")
32
+ });
33
+ }
34
+ export { removeSharedArtifactDirectory, removeSharedArtifacts, syncSharedArtifacts };
@@ -1,26 +1,10 @@
1
- import { rm } from "fs/promises";
2
- import { join, relative } from "path";
3
1
  import { watch } from "chokidar";
4
- import { createAction } from "./createAction.js";
2
+ import { removeSharedArtifactDirectory, removeSharedArtifacts, syncSharedArtifacts } from "./syncSharedArtifacts.js";
5
3
  const watcher = watch("shared", {
6
4
  awaitWriteFinish: true,
7
5
  persistent: true
8
6
  });
9
- watcher.on("add", createAction);
10
- watcher.on("change", createAction);
11
- watcher.on("unlink", async (path)=>{
12
- path = relative("shared", path).replace(/\\/g, "/");
13
- const actionPath = join("actions", path);
14
- await rm(actionPath, {
15
- recursive: true,
16
- force: true
17
- });
18
- });
19
- watcher.on("unlinkDir", async (path)=>{
20
- path = relative("shared", path).replace(/\\/g, "/");
21
- const actionPath = join("actions", path);
22
- await rm(actionPath, {
23
- recursive: true,
24
- force: true
25
- });
26
- });
7
+ watcher.on("add", syncSharedArtifacts);
8
+ watcher.on("change", syncSharedArtifacts);
9
+ watcher.on("unlink", removeSharedArtifacts);
10
+ watcher.on("unlinkDir", removeSharedArtifactDirectory);
@@ -1 +1,7 @@
1
- export declare function writeVsCodeSetting(config: Record<string, any>): Promise<Record<string, any>>;
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
+ }
@@ -9,10 +9,14 @@ async function writeVsCodeSetting(config) {
9
9
  const json = await readFile(".vscode/settings.json", "utf-8");
10
10
  data = JSON.parse(json);
11
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.");
12
13
  data = {};
13
14
  }
14
15
  data = merge(data, config);
15
16
  await writeFile(".vscode/settings.json", JSON.stringify(data, null, 4));
16
17
  return data;
17
18
  }
19
+ function isNodeError(error) {
20
+ return "object" == typeof error && null !== error;
21
+ }
18
22
  export { writeVsCodeSetting };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdnext",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "exports": {
@@ -41,7 +41,7 @@
41
41
  "@inquirer/prompts": "^8.1.0",
42
42
  "chokidar": "^5.0.0",
43
43
  "commander": "^14.0.2",
44
- "deepsea-tools": "5.47.7"
44
+ "deepsea-tools": "5.47.8"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^24.10.14",
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync } from "fs"
3
+ import { readFile } from "fs/promises"
4
4
  import { join } from "path"
5
+ import { fileURLToPath } from "url"
5
6
 
6
7
  import { Command } from "commander"
7
8
 
@@ -11,11 +12,11 @@ import { hook } from "./utils/hook"
11
12
 
12
13
  const program = new Command()
13
14
 
14
- const path = process.platform === "win32" ? import.meta.resolve("../").replace(/^file:\/\/\//, "") : import.meta.resolve("../").replace(/^file:\/\//, "")
15
+ const path = fileURLToPath(new URL("../", import.meta.url))
15
16
 
16
- const packgeJson = JSON.parse(readFileSync(join(path, "package.json"), "utf-8"))
17
+ const packageJson = JSON.parse(await readFile(join(path, "package.json"), "utf-8"))
17
18
 
18
- program.name("soda next").version(packgeJson.version)
19
+ program.name("soda next").version(packageJson.version)
19
20
 
20
21
  program.command("build").allowUnknownOption(true).allowExcessArguments(true).action(build)
21
22
 
@@ -1,11 +1,11 @@
1
- import { spawn } from "child_process"
2
1
  import { readdir, stat } from "fs/promises"
3
2
  import { join } from "path"
4
3
 
5
4
  import { Command } from "commander"
6
5
 
7
- import { createAction } from "./createAction"
8
- import { excludeActions } from "./excludeActions"
6
+ import { excludeGeneratedFiles } from "./excludeGeneratedFiles"
7
+ import { runCommand } from "./runCommand"
8
+ import { syncSharedArtifacts } from "./syncSharedArtifacts"
9
9
 
10
10
  export async function buildFolder(dir: string) {
11
11
  const content = await readdir(dir)
@@ -15,19 +15,16 @@ export async function buildFolder(dir: string) {
15
15
  const stats = await stat(path)
16
16
 
17
17
  if (stats.isDirectory()) await buildFolder(path)
18
- else await createAction(path)
18
+ else await syncSharedArtifacts(path)
19
19
  }
20
20
  }
21
21
 
22
22
  export async function build(options: Record<string, string>, { args }: Command) {
23
- await excludeActions()
23
+ await excludeGeneratedFiles()
24
24
 
25
25
  await buildFolder("shared")
26
26
 
27
27
  if (args.length === 0) return
28
28
 
29
- spawn(args.join(" "), {
30
- stdio: "inherit",
31
- shell: true,
32
- })
29
+ process.exitCode = await runCommand({ args })
33
30
  }
@@ -1,28 +1,21 @@
1
- import { mkdir, readFile, writeFile } from "fs/promises"
2
- import { join, parse, relative } from "path"
1
+ import { join } from "path"
2
+
3
+ import { getSharedModuleInfo, isScriptModule, writeGeneratedFile } from "./sharedArtifact"
3
4
 
4
5
  export async function createAction(path: string) {
5
- path = relative("shared", path).replace(/\\/g, "/")
6
- const { dir, name, ext } = parse(path)
7
- if (ext !== ".ts" && ext !== ".tsx" && ext !== ".js" && ext !== ".jsx") return
6
+ const info = getSharedModuleInfo(path)
7
+
8
+ if (!isScriptModule(info.relativePath)) return
8
9
 
9
- const content = `"use server"
10
+ await writeGeneratedFile({
11
+ path: join("actions", info.relativePath),
12
+ content: `"use server"
10
13
 
11
14
  import { createResponseFn } from "@/server/createResponseFn"
12
15
 
13
- import { ${name} } from "@/shared/${join(dir, name)}"
16
+ import { ${info.name} } from "@/shared/${info.importPath}"
14
17
 
15
- export const ${name}Action = createResponseFn(${name})
18
+ export const ${info.name}Action = createResponseFn(${info.name})
16
19
  `
17
-
18
- const actionPath = join("actions", path)
19
-
20
- try {
21
- const current = await readFile(actionPath, "utf-8")
22
- if (current === content) return
23
- } catch (error) {}
24
-
25
- await mkdir(join("actions", dir), { recursive: true })
26
-
27
- await writeFile(actionPath, content)
20
+ })
28
21
  }
@@ -0,0 +1,44 @@
1
+ import { readFile } from "fs/promises"
2
+ import { join } from "path"
3
+
4
+ import { getSharedModuleInfo, isScriptModule, removeGeneratedFile, toKebabCase, writeGeneratedFile } from "./sharedArtifact"
5
+
6
+ export async function createRoute(path: string) {
7
+ const info = getSharedModuleInfo(path)
8
+
9
+ if (!isScriptModule(info.relativePath)) return
10
+
11
+ const routeDirPath = join("app", "api", "actions", info.dir, toKebabCase(info.name))
12
+ const routePath = join(routeDirPath, "route.ts")
13
+
14
+ const content = await readFile(join("shared", info.relativePath), "utf-8")
15
+
16
+ if (!isRouteEnabled({ content, name: info.name })) {
17
+ await removeGeneratedFile({
18
+ path: routeDirPath,
19
+ stopPath: join("app", "api", "actions"),
20
+ })
21
+
22
+ return
23
+ }
24
+
25
+ await writeGeneratedFile({
26
+ path: routePath,
27
+ content: `import { createRoute } from "@/server/createResponseFn"
28
+
29
+ import { ${info.name} } from "@/shared/${info.importPath}"
30
+
31
+ export const { POST } = createRoute(${info.name})
32
+ `,
33
+ })
34
+ }
35
+
36
+ export interface IsRouteEnabledParams {
37
+ content: string
38
+ name: string
39
+ }
40
+
41
+ function isRouteEnabled({ content, name }: IsRouteEnabledParams) {
42
+ const routeRegExp = new RegExp(`\\b${name}\\.route\\s*=\\s*(true\\b|\\{)`)
43
+ return routeRegExp.test(content)
44
+ }
package/src/utils/dev.ts CHANGED
@@ -1,27 +1,44 @@
1
1
  import { spawn } from "child_process"
2
+ import { fileURLToPath } from "url"
2
3
 
3
4
  import { Command } from "commander"
4
5
 
5
6
  import { buildFolder } from "./build"
6
- import { excludeActions } from "./excludeActions"
7
+ import { excludeGeneratedFiles } from "./excludeGeneratedFiles"
8
+ import { spawnCommand } from "./runCommand"
7
9
 
8
10
  export async function dev(options: Record<string, string>, { args }: Command) {
9
- await excludeActions()
11
+ await excludeGeneratedFiles()
10
12
 
11
13
  await buildFolder("shared")
12
14
 
13
15
  if (args.length === 0) return
14
16
 
15
- const watchPath = import.meta.resolve("./watch.js").replace(process.platform === "win32" ? /^file:\/\/\// : /^file:\/\//, "")
17
+ const watchPath = fileURLToPath(new URL("./watch.js", import.meta.url))
16
18
 
17
- const child = spawn(process.execPath, [watchPath])
18
-
19
- const child2 = spawn(args.join(" "), {
19
+ const child = spawn(process.execPath, [watchPath], {
20
20
  stdio: "inherit",
21
- shell: true,
22
21
  })
23
22
 
24
- child.on("close", () => child2.kill())
23
+ const child2 = spawnCommand({
24
+ args,
25
+ })
26
+
27
+ process.exitCode = await new Promise<number>((resolve, reject) => {
28
+ let settled = false
25
29
 
26
- child2.on("close", () => child.kill())
30
+ function onClose(code: number) {
31
+ if (settled) return
32
+
33
+ settled = true
34
+ child.kill()
35
+ child2.kill()
36
+ resolve(code)
37
+ }
38
+
39
+ child.once("error", reject)
40
+ child2.once("error", reject)
41
+ child.once("close", (code, signal) => onClose(typeof code === "number" ? code : signal ? 1 : 0))
42
+ child2.once("close", (code, signal) => onClose(typeof code === "number" ? code : signal ? 1 : 0))
43
+ })
27
44
  }
@@ -1,9 +1,10 @@
1
1
  import { writeVsCodeSetting } from "./writeVsCodeSetting"
2
2
 
3
- export function excludeActions() {
3
+ export function excludeGeneratedFiles() {
4
4
  return writeVsCodeSetting({
5
5
  "files.exclude": {
6
6
  "actions/**": true,
7
+ "app/api/actions/**": true,
7
8
  },
8
9
  })
9
10
  }
package/src/utils/hook.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readdir, readFile, stat, writeFile } from "fs/promises"
1
+ import { readdir, readFile, stat } from "fs/promises"
2
2
  import { join, parse, relative } from "path"
3
3
  import { cwd } from "process"
4
4
 
@@ -6,6 +6,7 @@ import { checkbox, select } from "@inquirer/prompts"
6
6
  import { Command } from "commander"
7
7
 
8
8
  import { readSdNextSetting, SdNextSetting } from "./readSdNextSetting"
9
+ import { isScriptModule, normalizePathSeparator, writeGeneratedFile } from "./sharedArtifact"
9
10
  import { writeSdNextSetting } from "./writeSdNextSetting"
10
11
 
11
12
  export type HookType = "get" | "query" | "mutation"
@@ -32,37 +33,41 @@ async function getHookTypeFromContent(path: string, content: string): Promise<Ho
32
33
  const type = setting.hook?.[path]
33
34
  if (type !== undefined && type !== "skip") return type
34
35
  if (content.includes("useMutation")) return "mutation"
35
- if (content.includes("IdOrParams")) return "get"
36
+ if (content.includes("ClientOptional")) return "get"
36
37
  if (content.includes("useQuery")) return "query"
37
38
  return undefined
38
39
  }
39
40
 
40
41
  export interface HookData extends HookContentMap {
42
+ hookPath: string
41
43
  overwrite: boolean
42
44
  type: HookType
43
45
  }
44
46
 
45
47
  export async function createHook(path: string, hookMap: Record<string, HookData>) {
46
48
  path = relative("actions", path).replace(/\\/g, "/")
47
- const { dir, name, ext, base } = parse(path)
48
- if (ext !== ".ts" && ext !== ".tsx" && ext !== ".js" && ext !== ".jsx") return
49
- const serverContent = await readFile(join("shared", path), "utf-8")
50
- const match = serverContent.match(new RegExp(`export async function ${name}\\(.+?: (.+?)Params\\)`, "s"))
51
- const hasSchema = !!match
49
+ const { dir, name, base } = parse(path)
50
+ if (!isScriptModule(path)) return
52
51
  const upName = name.replace(/^./, char => char.toUpperCase())
53
52
  const key = name.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`)
53
+ const actionImportPath = normalizePathSeparator(join(dir, name))
54
+ const hookName = base.replace(/^./, char => `use${char.toUpperCase()}`)
55
+ const hookPath = join("hooks", dir, hookName)
56
+ const clientInputType = `${upName}ClientInput`
54
57
 
55
58
  const mutationHook = `import { useId } from "react"
56
59
 
57
60
  import { useMutation, UseMutationOptions } from "@tanstack/react-query"
58
61
  import { createRequestFn } from "deepsea-tools"
59
62
 
60
- import { ${name}Action } from "@/actions/${join(dir, name)}"
63
+ import { ${name}Action } from "@/actions/${actionImportPath}"
61
64
 
62
65
  export const ${name}Client = createRequestFn(${name}Action)
63
66
 
67
+ export type ${clientInputType} = Parameters<typeof ${name}Client> extends [] ? void : Parameters<typeof ${name}Client>[0]
68
+
64
69
  export interface Use${upName}Params<TOnMutateResult = unknown> extends Omit<
65
- UseMutationOptions<Awaited<ReturnType<typeof ${name}Client>>, Error, Parameters<typeof ${name}Client>[0], TOnMutateResult>,
70
+ UseMutationOptions<Awaited<ReturnType<typeof ${name}Client>>, Error, ${clientInputType}, TOnMutateResult>,
66
71
  "mutationFn"
67
72
  > {}
68
73
 
@@ -109,11 +114,13 @@ export function use${upName}<TOnMutateResult = unknown>({ onMutate, onSuccess, o
109
114
  const getHook = `import { createRequestFn, isNonNullable } from "deepsea-tools"
110
115
  import { createUseQuery } from "soda-tanstack-query"
111
116
 
112
- import { ${name}Action } from "@/actions/${join(dir, name)}"
117
+ import { ${name}Action } from "@/actions/${actionImportPath}"
113
118
 
114
119
  export const ${name}Client = createRequestFn(${name}Action)
115
120
 
116
- export function ${name}ClientOptional(id?: ${hasSchema ? `${match[1].replace(/Schema$/, "Params").replace(/^./, char => char.toUpperCase())} | ` : ""}undefined | null) {
121
+ export type ${clientInputType} = Parameters<typeof ${name}Client> extends [] ? undefined : Parameters<typeof ${name}Client>[0]
122
+
123
+ export function ${name}ClientOptional(id?: ${clientInputType} | null) {
117
124
  return isNonNullable(id) ? ${name}Client(id) : null
118
125
  }
119
126
 
@@ -126,7 +133,7 @@ export const use${upName} = createUseQuery({
126
133
  const queryHook = `import { createRequestFn } from "deepsea-tools"
127
134
  import { createUseQuery } from "soda-tanstack-query"
128
135
 
129
- import { ${name}Action } from "@/actions/${join(dir, name)}"
136
+ import { ${name}Action } from "@/actions/${actionImportPath}"
130
137
 
131
138
  export const ${name}Client = createRequestFn(${name}Action)
132
139
 
@@ -142,10 +149,6 @@ export const use${upName} = createUseQuery({
142
149
  mutation: mutationHook,
143
150
  }
144
151
 
145
- const hookName = base.replace(/^./, char => `use${char.toUpperCase()}`)
146
-
147
- const hookPath = join("hooks", dir, hookName)
148
-
149
152
  let hookType = getHookTypeFromName(name)
150
153
  let overwrite = true
151
154
 
@@ -160,34 +163,40 @@ export const use${upName} = createUseQuery({
160
163
  }
161
164
 
162
165
  hookMap[path] = {
166
+ hookPath,
163
167
  overwrite,
164
168
  type: hookType,
165
169
  ...map,
166
170
  }
167
171
  }
168
172
 
169
- export async function createActionFromFolder() {
173
+ export async function createHookFromFolder() {
170
174
  const map: Record<string, HookData> = {}
171
175
 
172
- async function _createActionFromFolder(dir: string) {
176
+ async function _createHookFromFolder(dir: string) {
173
177
  const content = await readdir(dir)
174
178
 
175
179
  for (const item of content) {
176
180
  const path = join(dir, item)
177
181
  const stats = await stat(path)
178
182
 
179
- if (stats.isDirectory()) await _createActionFromFolder(path)
183
+ if (stats.isDirectory()) await _createHookFromFolder(path)
180
184
  if (stats.isFile()) await createHook(path, map)
181
185
  }
182
186
  }
183
187
 
184
- await _createActionFromFolder("actions")
188
+ try {
189
+ await _createHookFromFolder("actions")
190
+ } catch (error) {
191
+ if (isNodeError(error) && error.code === "ENOENT") return map
192
+ throw error
193
+ }
185
194
 
186
195
  return map
187
196
  }
188
197
 
189
198
  export async function hook(options: Record<string, string>, { args }: Command) {
190
- const map = await createActionFromFolder()
199
+ const map = await createHookFromFolder()
191
200
 
192
201
  const entires = Object.entries(map)
193
202
 
@@ -204,8 +213,8 @@ export async function hook(options: Record<string, string>, { args }: Command) {
204
213
 
205
214
  const setting = await getSetting()
206
215
 
207
- for await (const [path, { overwrite, type, ...map }] of newEntires) {
208
- const resolved = join(root, "hooks", path)
216
+ for (const [path, { hookPath, overwrite, type, ...map }] of newEntires) {
217
+ const resolved = join(root, hookPath)
209
218
 
210
219
  const answer = await select<OperationType>({
211
220
  message: path,
@@ -218,16 +227,10 @@ export async function hook(options: Record<string, string>, { args }: Command) {
218
227
 
219
228
  if (answer === "skip") continue
220
229
 
221
- const { dir, base } = parse(path)
222
- await mkdir(join("hooks", dir), { recursive: true })
223
- await writeFile(
224
- join(
225
- "hooks",
226
- dir,
227
- base.replace(/^./, char => `use${char.toUpperCase()}`),
228
- ),
229
- map[answer],
230
- )
230
+ await writeGeneratedFile({
231
+ path: hookPath,
232
+ content: map[answer],
233
+ })
231
234
  }
232
235
 
233
236
  await writeSdNextSetting(setting)
@@ -237,18 +240,20 @@ export async function hook(options: Record<string, string>, { args }: Command) {
237
240
  choices: oldEntires.map(([key]) => key),
238
241
  })
239
242
 
240
- for (const [path, { overwrite, type, ...map }] of oldEntires) {
243
+ for (const [path, { hookPath, overwrite, type, ...map }] of oldEntires) {
241
244
  if (!overwrites.includes(path)) continue
242
245
 
243
- const { dir, base } = parse(path)
244
- await mkdir(join("hooks", dir), { recursive: true })
245
- await writeFile(
246
- join(
247
- "hooks",
248
- dir,
249
- base.replace(/^./, char => `use${char.toUpperCase()}`),
250
- ),
251
- map[type],
252
- )
246
+ await writeGeneratedFile({
247
+ path: hookPath,
248
+ content: map[type],
249
+ })
253
250
  }
254
251
  }
252
+
253
+ export interface NodeError {
254
+ code?: string
255
+ }
256
+
257
+ function isNodeError(error: unknown): error is NodeError {
258
+ return typeof error === "object" && error !== null
259
+ }