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
package/README.md CHANGED
@@ -1,3 +1,113 @@
1
1
  # sdnext
2
2
 
3
- to do.
3
+ `sdnext` 是一个面向 `Next.js` 项目的轻量生成工具,用来基于 `shared/**` 自动生成 `actions/**`、`app/api/actions/**/route.ts`,以及基于 `actions/**` 生成 `hooks/**`。
4
+
5
+ ## 约定
6
+
7
+ ### shared -> action
8
+
9
+ `shared/addUser.ts`
10
+
11
+ ```ts
12
+ export async function addUser() {}
13
+ ```
14
+
15
+ 会生成:
16
+
17
+ `actions/addUser.ts`
18
+
19
+ ```ts
20
+ "use server"
21
+
22
+ import { createResponseFn } from "@/server/createResponseFn"
23
+
24
+ import { addUser } from "@/shared/addUser"
25
+
26
+ export const addUserAction = createResponseFn(addUser)
27
+ ```
28
+
29
+ ### shared -> route
30
+
31
+ 如果源函数显式声明了 `route` 元数据:
32
+
33
+ ```ts
34
+ export async function addUser() {}
35
+
36
+ addUser.route = true
37
+ ```
38
+
39
+ 或:
40
+
41
+ ```ts
42
+ export async function addUser() {}
43
+
44
+ addUser.route = {}
45
+ ```
46
+
47
+ 会额外生成:
48
+
49
+ `app/api/actions/add-user/route.ts`
50
+
51
+ ```ts
52
+ import { createRoute } from "@/server/createResponseFn"
53
+
54
+ import { addUser } from "@/shared/addUser"
55
+
56
+ export const { POST } = createRoute(addUser)
57
+ ```
58
+
59
+ 默认情况下,`fn.route` 视为 `false`,不会生成 route。
60
+
61
+ ### actions -> hook
62
+
63
+ 执行 `sdnext hook` 后,会根据 `actions/**` 生成 `hooks/**`。
64
+
65
+ 命名规则:
66
+
67
+ - `getUser` 默认识别为 `get`
68
+ - `queryUser` 默认识别为 `query`
69
+ - 其他函数默认识别为 `mutation`
70
+
71
+ ## 命令
72
+
73
+ ### build
74
+
75
+ ```bash
76
+ sdnext build next build
77
+ ```
78
+
79
+ 执行顺序:
80
+
81
+ 1. 同步生成 `actions/**`
82
+ 2. 根据 `fn.route` 同步生成 `app/api/actions/**`
83
+ 3. 再执行后续命令
84
+
85
+ ### dev
86
+
87
+ ```bash
88
+ sdnext dev next dev
89
+ ```
90
+
91
+ 行为与 `build` 类似,但会额外监听 `shared/**` 的新增、修改、删除,并实时同步生成物。
92
+
93
+ ### hook
94
+
95
+ ```bash
96
+ sdnext hook
97
+ ```
98
+
99
+ 扫描 `actions/**` 并交互式生成或覆盖 `hooks/**`。
100
+
101
+ ## 路径映射
102
+
103
+ - `shared/addUser.ts` -> `actions/addUser.ts`
104
+ - `shared/addUser.ts` -> `app/api/actions/add-user/route.ts`
105
+ - `shared/admin/addUser.ts` -> `actions/admin/addUser.ts`
106
+ - `shared/admin/addUser.ts` -> `app/api/actions/admin/add-user/route.ts`
107
+
108
+ ## 说明
109
+
110
+ - 只处理 `.ts`、`.tsx`、`.js`、`.jsx`
111
+ - 生成文件会做幂等比较,内容未变化时不会重复写入
112
+ - 删除 `shared` 文件或移除 `fn.route` 后,对应生成物会自动清理
113
+ - `sdnext build` 和 `sdnext dev` 会自动把 `actions/**` 与 `app/api/actions/**` 写入 `.vscode/settings.json` 的 `files.exclude`
package/dist/index.js CHANGED
@@ -1,15 +1,146 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync } from "fs";
3
- import { join } from "path";
4
- import { Command } from "commander";
5
- import { build } from "./utils/build.js";
6
- import { dev } from "./utils/dev.js";
7
- import { hook } from "./utils/hook.js";
8
- const program = new Command();
9
- const path = "win32" === process.platform ? import.meta.resolve("../").replace(/^file:\/\/\//, "") : import.meta.resolve("../").replace(/^file:\/\//, "");
10
- const packgeJson = JSON.parse(readFileSync(join(path, "package.json"), "utf-8"));
11
- program.name("soda next").version(packgeJson.version);
12
- program.command("build").allowUnknownOption(true).allowExcessArguments(true).action(build);
13
- program.command("dev").allowUnknownOption(true).allowExcessArguments(true).action(dev);
14
- program.command("hook").action(hook);
15
- program.parse();
2
+ import * as __WEBPACK_EXTERNAL_MODULE__utils_build_js_f9ba07bf__ from "./utils/build.js";
3
+ import * as __WEBPACK_EXTERNAL_MODULE__utils_dev_js_df994271__ from "./utils/dev.js";
4
+ import * as __WEBPACK_EXTERNAL_MODULE__utils_hook_js_8cbfab27__ from "./utils/hook.js";
5
+ import * as __WEBPACK_EXTERNAL_MODULE_commander__ from "commander";
6
+ import * as __WEBPACK_EXTERNAL_MODULE_fs_promises_400951f8__ from "fs/promises";
7
+ import * as __WEBPACK_EXTERNAL_MODULE_path__ from "path";
8
+ import * as __WEBPACK_EXTERNAL_MODULE_url__ from "url";
9
+ var __webpack_modules__ = {
10
+ "./src/index.ts": function(module, __webpack_exports__, __webpack_require__) {
11
+ __webpack_require__.a(module, async function(__webpack_handle_async_dependencies__, __webpack_async_result__) {
12
+ try {
13
+ __webpack_require__.r(__webpack_exports__);
14
+ var fs_promises__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("fs/promises");
15
+ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("path");
16
+ var url__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("url");
17
+ var commander__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__("commander");
18
+ var _utils_build__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("./utils/build");
19
+ var _utils_dev__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__("./utils/dev");
20
+ var _utils_hook__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__("./utils/hook");
21
+ const program = new commander__WEBPACK_IMPORTED_MODULE_3__.Command();
22
+ const path = (0, url__WEBPACK_IMPORTED_MODULE_2__.fileURLToPath)(new URL("../", import.meta.url));
23
+ const packageJson = JSON.parse(await (0, fs_promises__WEBPACK_IMPORTED_MODULE_0__.readFile)((0, path__WEBPACK_IMPORTED_MODULE_1__.join)(path, "package.json"), "utf-8"));
24
+ program.name("soda next").version(packageJson.version);
25
+ program.command("build").allowUnknownOption(true).allowExcessArguments(true).action(_utils_build__WEBPACK_IMPORTED_MODULE_4__.build);
26
+ program.command("dev").allowUnknownOption(true).allowExcessArguments(true).action(_utils_dev__WEBPACK_IMPORTED_MODULE_5__.dev);
27
+ program.command("hook").action(_utils_hook__WEBPACK_IMPORTED_MODULE_6__.hook);
28
+ program.parse();
29
+ __webpack_async_result__();
30
+ } catch (e) {
31
+ __webpack_async_result__(e);
32
+ }
33
+ }, 1);
34
+ },
35
+ "./utils/build": function(module) {
36
+ module.exports = __WEBPACK_EXTERNAL_MODULE__utils_build_js_f9ba07bf__;
37
+ },
38
+ "./utils/dev": function(module) {
39
+ module.exports = __WEBPACK_EXTERNAL_MODULE__utils_dev_js_df994271__;
40
+ },
41
+ "./utils/hook": function(module) {
42
+ module.exports = __WEBPACK_EXTERNAL_MODULE__utils_hook_js_8cbfab27__;
43
+ },
44
+ commander: function(module) {
45
+ module.exports = __WEBPACK_EXTERNAL_MODULE_commander__;
46
+ },
47
+ "fs/promises": function(module) {
48
+ module.exports = __WEBPACK_EXTERNAL_MODULE_fs_promises_400951f8__;
49
+ },
50
+ path: function(module) {
51
+ module.exports = __WEBPACK_EXTERNAL_MODULE_path__;
52
+ },
53
+ url: function(module) {
54
+ module.exports = __WEBPACK_EXTERNAL_MODULE_url__;
55
+ }
56
+ };
57
+ var __webpack_module_cache__ = {};
58
+ function __webpack_require__(moduleId) {
59
+ var cachedModule = __webpack_module_cache__[moduleId];
60
+ if (void 0 !== cachedModule) return cachedModule.exports;
61
+ var module = __webpack_module_cache__[moduleId] = {
62
+ exports: {}
63
+ };
64
+ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
65
+ return module.exports;
66
+ }
67
+ (()=>{
68
+ var webpackQueues = "function" == typeof Symbol ? Symbol("webpack queues") : "__webpack_queues__";
69
+ var webpackExports = "function" == typeof Symbol ? Symbol("webpack exports") : "__webpack_exports__";
70
+ var webpackError = "function" == typeof Symbol ? Symbol("webpack error") : "__webpack_error__";
71
+ var resolveQueue = (queue)=>{
72
+ if (queue && queue.d < 1) {
73
+ queue.d = 1;
74
+ queue.forEach((fn)=>fn.r--);
75
+ queue.forEach((fn)=>fn.r-- ? fn.r++ : fn());
76
+ }
77
+ };
78
+ var wrapDeps = (deps)=>deps.map((dep)=>{
79
+ if (null !== dep && "object" == typeof dep) {
80
+ if (dep[webpackQueues]) return dep;
81
+ if (dep.then) {
82
+ var queue = [];
83
+ queue.d = 0;
84
+ dep.then((r)=>{
85
+ obj[webpackExports] = r;
86
+ resolveQueue(queue);
87
+ }, (e)=>{
88
+ obj[webpackError] = e;
89
+ resolveQueue(queue);
90
+ });
91
+ var obj = {};
92
+ obj[webpackQueues] = (fn)=>fn(queue);
93
+ return obj;
94
+ }
95
+ }
96
+ var ret = {};
97
+ ret[webpackQueues] = function() {};
98
+ ret[webpackExports] = dep;
99
+ return ret;
100
+ });
101
+ __webpack_require__.a = (module, body, hasAwait)=>{
102
+ var queue;
103
+ hasAwait && ((queue = []).d = -1);
104
+ var depQueues = new Set();
105
+ var exports = module.exports;
106
+ var currentDeps;
107
+ var outerResolve;
108
+ var reject;
109
+ var promise = new Promise((resolve, rej)=>{
110
+ reject = rej;
111
+ outerResolve = resolve;
112
+ });
113
+ promise[webpackExports] = exports;
114
+ promise[webpackQueues] = (fn)=>{
115
+ queue && fn(queue), depQueues.forEach(fn), promise["catch"](function() {});
116
+ };
117
+ module.exports = promise;
118
+ body((deps)=>{
119
+ currentDeps = wrapDeps(deps);
120
+ var fn;
121
+ var getResult = ()=>currentDeps.map((d)=>{
122
+ if (d[webpackError]) throw d[webpackError];
123
+ return d[webpackExports];
124
+ });
125
+ var promise = new Promise((resolve)=>{
126
+ fn = ()=>resolve(getResult);
127
+ fn.r = 0;
128
+ var fnQueue = (q)=>q !== queue && !depQueues.has(q) && (depQueues.add(q), q && !q.d && (fn.r++, q.push(fn)));
129
+ currentDeps.map((dep)=>dep[webpackQueues](fnQueue));
130
+ });
131
+ return fn.r ? promise : getResult();
132
+ }, (err)=>(err ? reject(promise[webpackError] = err) : outerResolve(exports), resolveQueue(queue)));
133
+ queue && queue.d < 0 && (queue.d = 0);
134
+ };
135
+ })();
136
+ (()=>{
137
+ __webpack_require__.r = (exports)=>{
138
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports, Symbol.toStringTag, {
139
+ value: 'Module'
140
+ });
141
+ Object.defineProperty(exports, '__esModule', {
142
+ value: true
143
+ });
144
+ };
145
+ })();
146
+ __webpack_require__("./src/index.ts");
@@ -1,24 +1,23 @@
1
- import { spawn } from "child_process";
2
1
  import { readdir, stat } from "fs/promises";
3
2
  import { join } from "path";
4
- import { createAction } from "./createAction.js";
5
- import { excludeActions } from "./excludeActions.js";
3
+ import { excludeGeneratedFiles } from "./excludeGeneratedFiles.js";
4
+ import { runCommand } from "./runCommand.js";
5
+ import { syncSharedArtifacts } from "./syncSharedArtifacts.js";
6
6
  async function buildFolder(dir) {
7
7
  const content = await readdir(dir);
8
8
  for (const item of content){
9
9
  const path = join(dir, item);
10
10
  const stats = await stat(path);
11
11
  if (stats.isDirectory()) await buildFolder(path);
12
- else await createAction(path);
12
+ else await syncSharedArtifacts(path);
13
13
  }
14
14
  }
15
15
  async function build(options, { args }) {
16
- await excludeActions();
16
+ await excludeGeneratedFiles();
17
17
  await buildFolder("shared");
18
18
  if (0 === args.length) return;
19
- spawn(args.join(" "), {
20
- stdio: "inherit",
21
- shell: true
19
+ process.exitCode = await runCommand({
20
+ args
22
21
  });
23
22
  }
24
23
  export { build, buildFolder };
@@ -1,25 +1,18 @@
1
- import { mkdir, readFile, writeFile } from "fs/promises";
2
- import { join, parse, relative } from "path";
1
+ import { join } from "path";
2
+ import { getSharedModuleInfo, isScriptModule, writeGeneratedFile } from "./sharedArtifact.js";
3
3
  async function createAction(path) {
4
- path = relative("shared", path).replace(/\\/g, "/");
5
- const { dir, name, ext } = parse(path);
6
- if (".ts" !== ext && ".tsx" !== ext && ".js" !== ext && ".jsx" !== ext) return;
7
- const content = `"use server"
4
+ const info = getSharedModuleInfo(path);
5
+ if (!isScriptModule(info.relativePath)) return;
6
+ await writeGeneratedFile({
7
+ path: join("actions", info.relativePath),
8
+ content: `"use server"
8
9
 
9
10
  import { createResponseFn } from "@/server/createResponseFn"
10
11
 
11
- import { ${name} } from "@/shared/${join(dir, name)}"
12
+ import { ${info.name} } from "@/shared/${info.importPath}"
12
13
 
13
- export const ${name}Action = createResponseFn(${name})
14
- `;
15
- const actionPath = join("actions", path);
16
- try {
17
- const current = await readFile(actionPath, "utf-8");
18
- if (current === content) return;
19
- } catch (error) {}
20
- await mkdir(join("actions", dir), {
21
- recursive: true
14
+ export const ${info.name}Action = createResponseFn(${info.name})
15
+ `
22
16
  });
23
- await writeFile(actionPath, content);
24
17
  }
25
18
  export { createAction };
@@ -0,0 +1,5 @@
1
+ export declare function createRoute(path: string): Promise<void>;
2
+ export interface IsRouteEnabledParams {
3
+ content: string;
4
+ name: string;
5
+ }
@@ -0,0 +1,31 @@
1
+ import { readFile } from "fs/promises";
2
+ import { join } from "path";
3
+ import { getSharedModuleInfo, isScriptModule, removeGeneratedFile, toKebabCase, writeGeneratedFile } from "./sharedArtifact.js";
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
+ });
17
+ await writeGeneratedFile({
18
+ path: routePath,
19
+ content: `import { createRoute } from "@/server/createResponseFn"
20
+
21
+ import { ${info.name} } from "@/shared/${info.importPath}"
22
+
23
+ export const { POST } = createRoute(${info.name})
24
+ `
25
+ });
26
+ }
27
+ function isRouteEnabled({ content, name }) {
28
+ const routeRegExp = new RegExp(`\\b${name}\\.route\\s*=\\s*(true\\b|\\{)`);
29
+ return routeRegExp.test(content);
30
+ }
31
+ export { createRoute };
package/dist/utils/dev.js CHANGED
@@ -1,19 +1,34 @@
1
1
  import { spawn } from "child_process";
2
+ import { fileURLToPath } from "url";
2
3
  import { buildFolder } from "./build.js";
3
- import { excludeActions } from "./excludeActions.js";
4
+ import { excludeGeneratedFiles } from "./excludeGeneratedFiles.js";
5
+ import { spawnCommand } from "./runCommand.js";
4
6
  async function dev(options, { args }) {
5
- await excludeActions();
7
+ await excludeGeneratedFiles();
6
8
  await buildFolder("shared");
7
9
  if (0 === args.length) return;
8
- const watchPath = import.meta.resolve("./watch.js").replace("win32" === process.platform ? /^file:\/\/\// : /^file:\/\//, "");
10
+ const watchPath = fileURLToPath(new URL("./watch.js", import.meta.url));
9
11
  const child = spawn(process.execPath, [
10
12
  watchPath
11
- ]);
12
- const child2 = spawn(args.join(" "), {
13
- stdio: "inherit",
14
- shell: true
13
+ ], {
14
+ stdio: "inherit"
15
+ });
16
+ const child2 = spawnCommand({
17
+ args
18
+ });
19
+ process.exitCode = await new Promise((resolve, reject)=>{
20
+ let settled = false;
21
+ function onClose(code) {
22
+ if (settled) return;
23
+ settled = true;
24
+ child.kill();
25
+ child2.kill();
26
+ resolve(code);
27
+ }
28
+ child.once("error", reject);
29
+ child2.once("error", reject);
30
+ child.once("close", (code, signal)=>onClose("number" == typeof code ? code : signal ? 1 : 0));
31
+ child2.once("close", (code, signal)=>onClose("number" == typeof code ? code : signal ? 1 : 0));
15
32
  });
16
- child.on("close", ()=>child2.kill());
17
- child2.on("close", ()=>child.kill());
18
33
  }
19
34
  export { dev };
@@ -0,0 +1 @@
1
+ export declare function excludeGeneratedFiles(): Promise<import("./writeVsCodeSetting").VsCodeSetting>;
@@ -1,9 +1,10 @@
1
1
  import { writeVsCodeSetting } from "./writeVsCodeSetting.js";
2
- function excludeActions() {
2
+ function excludeGeneratedFiles() {
3
3
  return writeVsCodeSetting({
4
4
  "files.exclude": {
5
- "actions/**": true
5
+ "actions/**": true,
6
+ "app/api/actions/**": true
6
7
  }
7
8
  });
8
9
  }
9
- export { excludeActions };
10
+ export { excludeGeneratedFiles };
@@ -3,9 +3,13 @@ export type HookType = "get" | "query" | "mutation";
3
3
  export type OperationType = HookType | "skip";
4
4
  export type HookContentMap = Record<HookType, string>;
5
5
  export interface HookData extends HookContentMap {
6
+ hookPath: string;
6
7
  overwrite: boolean;
7
8
  type: HookType;
8
9
  }
9
10
  export declare function createHook(path: string, hookMap: Record<string, HookData>): Promise<void>;
10
- export declare function createActionFromFolder(): Promise<Record<string, HookData>>;
11
+ export declare function createHookFromFolder(): Promise<Record<string, HookData>>;
11
12
  export declare function hook(options: Record<string, string>, { args }: Command): Promise<void>;
13
+ export interface NodeError {
14
+ code?: string;
15
+ }
@@ -1,8 +1,9 @@
1
- import { mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
1
+ import { readFile, readdir, stat } from "fs/promises";
2
2
  import { join, parse, relative } from "path";
3
3
  import { cwd } from "process";
4
4
  import { checkbox as prompts_checkbox, select as prompts_select } from "@inquirer/prompts";
5
5
  import { readSdNextSetting } from "./readSdNextSetting.js";
6
+ import { isScriptModule, normalizePathSeparator, writeGeneratedFile } from "./sharedArtifact.js";
6
7
  import { writeSdNextSetting } from "./writeSdNextSetting.js";
7
8
  function getHookTypeFromName(name) {
8
9
  if (/^get[^a-z]/.test(name)) return "get";
@@ -19,29 +20,32 @@ async function getHookTypeFromContent(path, content) {
19
20
  const type = setting.hook?.[path];
20
21
  if (void 0 !== type && "skip" !== type) return type;
21
22
  if (content.includes("useMutation")) return "mutation";
22
- if (content.includes("IdOrParams")) return "get";
23
+ if (content.includes("ClientOptional")) return "get";
23
24
  if (content.includes("useQuery")) return "query";
24
25
  }
25
26
  async function createHook(path, hookMap) {
26
27
  path = relative("actions", path).replace(/\\/g, "/");
27
- const { dir, name, ext, base } = parse(path);
28
- if (".ts" !== ext && ".tsx" !== ext && ".js" !== ext && ".jsx" !== ext) return;
29
- const serverContent = await readFile(join("shared", path), "utf-8");
30
- const match = serverContent.match(new RegExp(`export async function ${name}\\(.+?: (.+?)Params\\)`, "s"));
31
- const hasSchema = !!match;
28
+ const { dir, name, base } = parse(path);
29
+ if (!isScriptModule(path)) return;
32
30
  const upName = name.replace(/^./, (char)=>char.toUpperCase());
33
31
  const key = name.replace(/[A-Z]/g, (char)=>`-${char.toLowerCase()}`);
32
+ const actionImportPath = normalizePathSeparator(join(dir, name));
33
+ const hookName = base.replace(/^./, (char)=>`use${char.toUpperCase()}`);
34
+ const hookPath = join("hooks", dir, hookName);
35
+ const clientInputType = `${upName}ClientInput`;
34
36
  const mutationHook = `import { useId } from "react"
35
37
 
36
38
  import { useMutation, UseMutationOptions } from "@tanstack/react-query"
37
39
  import { createRequestFn } from "deepsea-tools"
38
40
 
39
- import { ${name}Action } from "@/actions/${join(dir, name)}"
41
+ import { ${name}Action } from "@/actions/${actionImportPath}"
40
42
 
41
43
  export const ${name}Client = createRequestFn(${name}Action)
42
44
 
45
+ export type ${clientInputType} = Parameters<typeof ${name}Client> extends [] ? void : Parameters<typeof ${name}Client>[0]
46
+
43
47
  export interface Use${upName}Params<TOnMutateResult = unknown> extends Omit<
44
- UseMutationOptions<Awaited<ReturnType<typeof ${name}Client>>, Error, Parameters<typeof ${name}Client>[0], TOnMutateResult>,
48
+ UseMutationOptions<Awaited<ReturnType<typeof ${name}Client>>, Error, ${clientInputType}, TOnMutateResult>,
45
49
  "mutationFn"
46
50
  > {}
47
51
 
@@ -87,11 +91,13 @@ export function use${upName}<TOnMutateResult = unknown>({ onMutate, onSuccess, o
87
91
  const getHook = `import { createRequestFn, isNonNullable } from "deepsea-tools"
88
92
  import { createUseQuery } from "soda-tanstack-query"
89
93
 
90
- import { ${name}Action } from "@/actions/${join(dir, name)}"
94
+ import { ${name}Action } from "@/actions/${actionImportPath}"
91
95
 
92
96
  export const ${name}Client = createRequestFn(${name}Action)
93
97
 
94
- export function ${name}ClientOptional(id?: ${hasSchema ? `${match[1].replace(/Schema$/, "Params").replace(/^./, (char)=>char.toUpperCase())} | ` : ""}undefined | null) {
98
+ export type ${clientInputType} = Parameters<typeof ${name}Client> extends [] ? undefined : Parameters<typeof ${name}Client>[0]
99
+
100
+ export function ${name}ClientOptional(id?: ${clientInputType} | null) {
95
101
  return isNonNullable(id) ? ${name}Client(id) : null
96
102
  }
97
103
 
@@ -103,7 +109,7 @@ export const use${upName} = createUseQuery({
103
109
  const queryHook = `import { createRequestFn } from "deepsea-tools"
104
110
  import { createUseQuery } from "soda-tanstack-query"
105
111
 
106
- import { ${name}Action } from "@/actions/${join(dir, name)}"
112
+ import { ${name}Action } from "@/actions/${actionImportPath}"
107
113
 
108
114
  export const ${name}Client = createRequestFn(${name}Action)
109
115
 
@@ -117,8 +123,6 @@ export const use${upName} = createUseQuery({
117
123
  query: queryHook,
118
124
  mutation: mutationHook
119
125
  };
120
- const hookName = base.replace(/^./, (char)=>`use${char.toUpperCase()}`);
121
- const hookPath = join("hooks", dir, hookName);
122
126
  let hookType = getHookTypeFromName(name);
123
127
  let overwrite = true;
124
128
  try {
@@ -131,35 +135,41 @@ export const use${upName} = createUseQuery({
131
135
  overwrite = true;
132
136
  }
133
137
  hookMap[path] = {
138
+ hookPath,
134
139
  overwrite,
135
140
  type: hookType,
136
141
  ...map
137
142
  };
138
143
  }
139
- async function createActionFromFolder() {
144
+ async function createHookFromFolder() {
140
145
  const map = {};
141
- async function _createActionFromFolder(dir) {
146
+ async function _createHookFromFolder(dir) {
142
147
  const content = await readdir(dir);
143
148
  for (const item of content){
144
149
  const path = join(dir, item);
145
150
  const stats = await stat(path);
146
- if (stats.isDirectory()) await _createActionFromFolder(path);
151
+ if (stats.isDirectory()) await _createHookFromFolder(path);
147
152
  if (stats.isFile()) await createHook(path, map);
148
153
  }
149
154
  }
150
- await _createActionFromFolder("actions");
155
+ try {
156
+ await _createHookFromFolder("actions");
157
+ } catch (error) {
158
+ if (isNodeError(error) && "ENOENT" === error.code) return map;
159
+ throw error;
160
+ }
151
161
  return map;
152
162
  }
153
163
  async function hook(options, { args }) {
154
- const map = await createActionFromFolder();
164
+ const map = await createHookFromFolder();
155
165
  const entires = Object.entries(map);
156
166
  if (0 === entires.length) return void console.log("All hooks are the latest.");
157
167
  const newEntires = entires.filter(([path, { overwrite }])=>overwrite);
158
168
  const oldEntires = entires.filter(([path, { overwrite }])=>!overwrite);
159
169
  const root = cwd();
160
170
  const setting = await getSetting();
161
- for await (const [path, { overwrite, type, ...map }] of newEntires){
162
- const resolved = join(root, "hooks", path);
171
+ for (const [path, { hookPath, overwrite, type, ...map }] of newEntires){
172
+ const resolved = join(root, hookPath);
163
173
  const answer = await prompts_select({
164
174
  message: path,
165
175
  choices: [
@@ -172,25 +182,22 @@ async function hook(options, { args }) {
172
182
  });
173
183
  setting.hook ??= {};
174
184
  setting.hook[resolved] = answer;
175
- if ("skip" === answer) continue;
176
- const { dir, base } = parse(path);
177
- await mkdir(join("hooks", dir), {
178
- recursive: true
185
+ if ("skip" !== answer) await writeGeneratedFile({
186
+ path: hookPath,
187
+ content: map[answer]
179
188
  });
180
- await writeFile(join("hooks", dir, base.replace(/^./, (char)=>`use${char.toUpperCase()}`)), map[answer]);
181
189
  }
182
190
  await writeSdNextSetting(setting);
183
191
  const overwrites = await prompts_checkbox({
184
192
  message: "Please check the hooks you want to overwrite",
185
193
  choices: oldEntires.map(([key])=>key)
186
194
  });
187
- for (const [path, { overwrite, type, ...map }] of oldEntires){
188
- if (!overwrites.includes(path)) continue;
189
- const { dir, base } = parse(path);
190
- await mkdir(join("hooks", dir), {
191
- recursive: true
192
- });
193
- await writeFile(join("hooks", dir, base.replace(/^./, (char)=>`use${char.toUpperCase()}`)), map[type]);
194
- }
195
+ for (const [path, { hookPath, overwrite, type, ...map }] of oldEntires)if (overwrites.includes(path)) await writeGeneratedFile({
196
+ path: hookPath,
197
+ content: map[type]
198
+ });
199
+ }
200
+ function isNodeError(error) {
201
+ return "object" == typeof error && null !== error;
195
202
  }
196
- export { createActionFromFolder, createHook, hook };
203
+ export { createHook, createHookFromFolder, hook };
@@ -3,3 +3,6 @@ export interface SdNextSetting {
3
3
  hook?: Record<string, OperationType>;
4
4
  }
5
5
  export declare function readSdNextSetting(): Promise<SdNextSetting>;
6
+ export interface NodeError {
7
+ code?: string;
8
+ }
@@ -1,14 +1,19 @@
1
- import { existsSync } from "node:fs";
2
1
  import { readFile } from "node:fs/promises";
3
2
  import { homedir } from "node:os";
4
3
  import { join } from "node:path";
5
4
  async function readSdNextSetting() {
6
5
  const userDir = homedir();
7
6
  const settingPath = join(userDir, ".sdnext.json");
8
- if (existsSync(settingPath)) {
7
+ try {
9
8
  const setting = JSON.parse(await readFile(settingPath, "utf-8"));
10
9
  return setting;
10
+ } catch (error) {
11
+ if (isNodeError(error) && "ENOENT" === error.code) return {};
12
+ console.warn(`Failed to read ${settingPath}, fallback to default setting.`);
13
+ return {};
11
14
  }
12
- return {};
15
+ }
16
+ function isNodeError(error) {
17
+ return "object" == typeof error && null !== error;
13
18
  }
14
19
  export { readSdNextSetting };