create-geshu 0.0.1
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.
- package/dist/index.js +186 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { promises } from "node:fs";
|
|
4
|
+
import { isBuiltin } from "node:module";
|
|
5
|
+
import node_path from "node:path";
|
|
6
|
+
import node_process from "node:process";
|
|
7
|
+
import { input as prompts_input, select as prompts_select } from "@inquirer/prompts";
|
|
8
|
+
const TemplateMap = {
|
|
9
|
+
Rsbuild: "https://github.com/1adybug/geshu-rsbuild-template",
|
|
10
|
+
"Next.js": "https://github.com/1adybug/geshu-next-template"
|
|
11
|
+
};
|
|
12
|
+
const ProjectTypeChoices = [
|
|
13
|
+
"Rsbuild",
|
|
14
|
+
"Next.js"
|
|
15
|
+
];
|
|
16
|
+
const TemplatePushBlockedUrl = "no_push://template";
|
|
17
|
+
const WindowsReservedNames = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
18
|
+
async function main() {
|
|
19
|
+
try {
|
|
20
|
+
const options = parseCliOptions(node_process.argv.slice(2));
|
|
21
|
+
const projectType = options.projectType ?? await promptProjectType();
|
|
22
|
+
const projectName = options.projectName ?? await promptProjectName();
|
|
23
|
+
const nameErrors = validateProjectName(projectName);
|
|
24
|
+
if (nameErrors.length > 0) throw new Error(`项目名称不合法:\n${nameErrors.map((item)=>`- ${item}`).join("\n")}`);
|
|
25
|
+
const targetDir = node_path.resolve(node_process.cwd(), projectName);
|
|
26
|
+
await assertTargetDirectoryAvailable(targetDir, projectName);
|
|
27
|
+
const templateCloneUrl = TemplateMap[projectType];
|
|
28
|
+
console.log(`\n开始创建项目: ${projectName} (${projectType})`);
|
|
29
|
+
await runCommand("git", [
|
|
30
|
+
"clone",
|
|
31
|
+
templateCloneUrl,
|
|
32
|
+
projectName
|
|
33
|
+
], node_process.cwd());
|
|
34
|
+
await runCommand("git", [
|
|
35
|
+
"remote",
|
|
36
|
+
"rename",
|
|
37
|
+
"origin",
|
|
38
|
+
"template"
|
|
39
|
+
], targetDir);
|
|
40
|
+
await runCommand("git", [
|
|
41
|
+
"remote",
|
|
42
|
+
"set-url",
|
|
43
|
+
"--push",
|
|
44
|
+
"template",
|
|
45
|
+
TemplatePushBlockedUrl
|
|
46
|
+
], targetDir);
|
|
47
|
+
await updatePackageName(targetDir, projectName);
|
|
48
|
+
await runCommand("git", [
|
|
49
|
+
"add",
|
|
50
|
+
"package.json"
|
|
51
|
+
], targetDir);
|
|
52
|
+
await runCommand("git", [
|
|
53
|
+
"commit",
|
|
54
|
+
"-m",
|
|
55
|
+
"✨feature: init"
|
|
56
|
+
], targetDir);
|
|
57
|
+
console.log(`\n创建完成: ${projectName}`);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : "未知错误,请重试。";
|
|
60
|
+
console.error(`\n创建失败: ${message}`);
|
|
61
|
+
node_process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function parseCliOptions(args) {
|
|
65
|
+
const options = {};
|
|
66
|
+
for(let index = 0; index < args.length; index += 1){
|
|
67
|
+
const current = args[index];
|
|
68
|
+
if ("--type" === current || "-t" === current) {
|
|
69
|
+
const value = args[index + 1];
|
|
70
|
+
if (!value) throw new Error("参数 --type 缺少值,支持 Rsbuild / Next.js。");
|
|
71
|
+
const parsed = normalizeProjectType(value);
|
|
72
|
+
if (!parsed) throw new Error(`参数 --type 无效: ${value}。支持 Rsbuild / Next.js。`);
|
|
73
|
+
options.projectType = parsed;
|
|
74
|
+
index += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if ("--name" === current || "-n" === current) {
|
|
78
|
+
const value = args[index + 1];
|
|
79
|
+
if (!value) throw new Error("参数 --name 缺少值。");
|
|
80
|
+
options.projectName = value.trim();
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return options;
|
|
86
|
+
}
|
|
87
|
+
function normalizeProjectType(input) {
|
|
88
|
+
const value = input.trim().toLowerCase();
|
|
89
|
+
if ("rsbuild" === value || "1" === value) return "Rsbuild";
|
|
90
|
+
if ("nextjs" === value || "next.js" === value || "next" === value || "2" === value) return "Next.js";
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
async function promptProjectType() {
|
|
94
|
+
return prompts_select({
|
|
95
|
+
message: "请选择项目类型:",
|
|
96
|
+
choices: ProjectTypeChoices
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function promptProjectName() {
|
|
100
|
+
const answer = await prompts_input({
|
|
101
|
+
message: "请输入项目名称:",
|
|
102
|
+
validate: (value)=>{
|
|
103
|
+
const name = value.trim();
|
|
104
|
+
if (!name) return "项目名称不能为空。";
|
|
105
|
+
const errors = validateProjectName(name);
|
|
106
|
+
if (0 === errors.length) return true;
|
|
107
|
+
return `项目名称不合法: ${errors.join(";")}`;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
return answer.trim();
|
|
111
|
+
}
|
|
112
|
+
function validateProjectName(name) {
|
|
113
|
+
const errors = [];
|
|
114
|
+
const directoryError = validateDirectoryName(name);
|
|
115
|
+
if (directoryError) errors.push(directoryError);
|
|
116
|
+
const packageErrors = validatePackageJsonName(name);
|
|
117
|
+
errors.push(...packageErrors);
|
|
118
|
+
return errors;
|
|
119
|
+
}
|
|
120
|
+
function validateDirectoryName(name) {
|
|
121
|
+
if ("." === name || ".." === name) return "目录名不能是 . 或 ..。";
|
|
122
|
+
if (name !== node_path.basename(name)) return "目录名不能包含路径分隔符。";
|
|
123
|
+
if (/[<>:"/\\|?*]/.test(name) || hasAsciiControlChars(name)) return "目录名包含非法字符。";
|
|
124
|
+
if (/[. ]$/.test(name)) return "目录名不能以空格或点号结尾。";
|
|
125
|
+
if (WindowsReservedNames.test(name)) return "目录名是 Windows 保留名称。";
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function hasAsciiControlChars(input) {
|
|
129
|
+
for (const char of input){
|
|
130
|
+
const code = char.charCodeAt(0);
|
|
131
|
+
if (code >= 0 && code <= 31) return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
function validatePackageJsonName(name) {
|
|
136
|
+
const errors = [];
|
|
137
|
+
const packageNamePattern = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
138
|
+
if (name.length > 214) errors.push("package.json 的 name 长度不能超过 214 个字符。");
|
|
139
|
+
if (name.startsWith(".") || name.startsWith("_")) errors.push("package.json 的 name 不能以 . 或 _ 开头。");
|
|
140
|
+
if (/[A-Z]/.test(name)) errors.push("package.json 的 name 不能包含大写字母。");
|
|
141
|
+
if (isBuiltin(name)) errors.push("package.json 的 name 不能与 Node.js 内置模块冲突。");
|
|
142
|
+
if (!packageNamePattern.test(name)) errors.push("package.json 的 name 必须是 URL 友好的 npm 包名。");
|
|
143
|
+
return errors;
|
|
144
|
+
}
|
|
145
|
+
async function assertTargetDirectoryAvailable(targetDir, projectName) {
|
|
146
|
+
try {
|
|
147
|
+
const stat = await promises.stat(targetDir);
|
|
148
|
+
if (!stat.isDirectory()) throw new Error(`路径 ${projectName} 已存在且不是目录。`);
|
|
149
|
+
const files = await promises.readdir(targetDir);
|
|
150
|
+
if (0 === files.length) throw new Error(`已存在同名空目录 "${projectName}",请删除后再执行命令。`);
|
|
151
|
+
throw new Error(`已存在同名非空目录 "${projectName}",无法继续创建。`);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if ("ENOENT" === error.code) return;
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function updatePackageName(projectDir, name) {
|
|
158
|
+
const packageJsonPath = node_path.join(projectDir, "package.json");
|
|
159
|
+
const content = await promises.readFile(packageJsonPath, "utf8");
|
|
160
|
+
let parsed;
|
|
161
|
+
try {
|
|
162
|
+
parsed = JSON.parse(content);
|
|
163
|
+
} catch {
|
|
164
|
+
throw new Error("模板中的 package.json 不是有效 JSON。");
|
|
165
|
+
}
|
|
166
|
+
parsed.name = name;
|
|
167
|
+
await promises.writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
|
168
|
+
}
|
|
169
|
+
async function runCommand(command, args, cwd) {
|
|
170
|
+
const display = `${command} ${args.join(" ")}`;
|
|
171
|
+
console.log(`\n> ${display}`);
|
|
172
|
+
await new Promise((resolve, reject)=>{
|
|
173
|
+
const child = spawn(command, args, {
|
|
174
|
+
cwd,
|
|
175
|
+
stdio: "inherit"
|
|
176
|
+
});
|
|
177
|
+
child.once("error", (error)=>{
|
|
178
|
+
reject(error);
|
|
179
|
+
});
|
|
180
|
+
child.once("close", (code)=>{
|
|
181
|
+
if (0 === code) return void resolve();
|
|
182
|
+
reject(new Error(`命令执行失败(退出码 ${code}): ${display}`));
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-geshu",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-geshu": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "rslib build",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"format": "prettier --write .",
|
|
16
|
+
"fg": "npm run format && git add . && git commit -m \"✨feature: format\"",
|
|
17
|
+
"prepare": "husky",
|
|
18
|
+
"lint": "eslint ."
|
|
19
|
+
},
|
|
20
|
+
"keywords": [],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@1adybug/prettier": "^0.0.27",
|
|
26
|
+
"@eslint/js": "^10.0.1",
|
|
27
|
+
"@rslib/core": "^0.19.6",
|
|
28
|
+
"@types/node": "^24.10.13",
|
|
29
|
+
"eslint": "^10.0.2",
|
|
30
|
+
"globals": "^17.3.0",
|
|
31
|
+
"husky": "^9.1.7",
|
|
32
|
+
"lint-staged": "^16.2.7",
|
|
33
|
+
"prettier": "^3.8.1",
|
|
34
|
+
"typescript": "^5.8.3",
|
|
35
|
+
"typescript-eslint": "^8.56.1"
|
|
36
|
+
},
|
|
37
|
+
"lint-staged": {
|
|
38
|
+
"**/*": "prettier --write --ignore-unknown"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@inquirer/prompts": "^8.3.0"
|
|
42
|
+
}
|
|
43
|
+
}
|