import { mkdir, writeFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const args = process.argv.slice(2); const featureName = args.find((arg) => !arg.startsWith("--")); if (!featureName) { console.error("Usage: pnpm scaffold:feature --label=模块名称 --path=/path --menu-code=menu-code --permission=module:view"); process.exit(1); } const options = Object.fromEntries( args .filter((arg) => arg.startsWith("--")) .map((arg) => { const [key, ...valueParts] = arg.slice(2).split("="); return [key, valueParts.join("=")]; }) ); const kebabName = toKebabCase(featureName); const camelName = toCamelCase(kebabName); const pascalName = toPascalCase(kebabName); const routePath = options.path || `/${kebabName}`; const label = options.label || pascalName; const menuCode = options["menu-code"] || kebabName; const permission = options.permission || `${kebabName}:view`; const featureDir = path.join(rootDir, "src/features", kebabName); const files = new Map([ [ "api.ts", `import { apiRequest } from "@/shared/api/request"; export function list${pascalName}(query?: Record) { return apiRequest("${routePath}", { query }); } ` ], [ "routes.js", `export const ${camelName}Routes = [ { label: ${JSON.stringify(label)}, loader: () => import("./pages/${pascalName}Page.jsx").then((module) => module.${pascalName}Page), menuCode: ${JSON.stringify(menuCode)}, pageKey: ${JSON.stringify(kebabName)}, path: ${JSON.stringify(routePath)}, permission: ${JSON.stringify(permission)} } ]; ` ], [ "permissions.js", `import { useAuth } from "@/app/auth/AuthProvider.jsx"; export function use${pascalName}Abilities() { const { can } = useAuth(); return { canView: can(${JSON.stringify(permission)}) }; } ` ], [ `hooks/use${pascalName}Page.js`, `import { useCallback } from "react"; import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js"; import { list${pascalName} } from "@/features/${kebabName}/api"; import { use${pascalName}Abilities } from "@/features/${kebabName}/permissions.js"; const emptyData = { items: [], total: 0, page: 1, pageSize: 10 }; export function use${pascalName}Page() { const abilities = use${pascalName}Abilities(); const queryFn = useCallback(() => list${pascalName}({ page: 1, page_size: 10 }), []); const { data, error, loading, reload } = useAdminQuery(queryFn, { errorMessage: "加载失败", initialData: emptyData, queryKey: [${JSON.stringify(kebabName)}] }); return { abilities, data, error, loading, reload }; } ` ], [ `pages/${pascalName}Page.jsx`, `import { DataState } from "@/shared/ui/DataState.jsx"; import { use${pascalName}Page } from "@/features/${kebabName}/hooks/use${pascalName}Page.js"; import styles from "@/features/${kebabName}/${kebabName}.module.css"; export function ${pascalName}Page() { const page = use${pascalName}Page(); return (
); } ` ], [ `${kebabName}.module.css`, `.root { min-height: 100%; } ` ], ["components/.gitkeep", ""] ]); await mkdir(featureDir, { recursive: true }); for (const [relativePath, content] of files) { const absolutePath = path.join(featureDir, relativePath); await mkdir(path.dirname(absolutePath), { recursive: true }); if (existsSync(absolutePath)) { console.log(`Skipped existing ${path.relative(rootDir, absolutePath)}`); continue; } await writeFile(absolutePath, content); console.log(`Created ${path.relative(rootDir, absolutePath)}`); } function toKebabCase(value) { return value .replace(/([a-z0-9])([A-Z])/g, "$1-$2") .replace(/[^a-zA-Z0-9]+/g, "-") .replace(/^-|-$/g, "") .toLowerCase(); } function toCamelCase(value) { return value.replace(/-([a-z0-9])/g, (_, letter) => letter.toUpperCase()); } function toPascalCase(value) { const camel = toCamelCase(value); return camel.charAt(0).toUpperCase() + camel.slice(1); }