hyapp-admin-platform/scripts/scaffold-feature.mjs
2026-05-02 13:02:57 +08:00

148 lines
4.2 KiB
JavaScript

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 <feature-name> --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<string, string | number>) {
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 (
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
<section className={styles.root} />
</DataState>
);
}
`
],
[
`${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);
}