create-wucaishi-cpn 0.1.0

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/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # create-wucaishi-cpn
2
+
3
+ 一个用来生成 `Vite + React + TypeScript` 组件库模板项目的 npm CLI。
4
+
5
+ ## 使用方式
6
+
7
+ 本地调试:
8
+
9
+ ```bash
10
+ node ./bin/create-wucaishi-cpn.js my-lib
11
+ ```
12
+
13
+ 发布到 npm 后:
14
+
15
+ ```bash
16
+ npm create wucaishi-cpn@latest my-lib
17
+ ```
18
+
19
+ 或者:
20
+
21
+ ```bash
22
+ npx create-wucaishi-cpn@latest my-lib
23
+ ```
24
+
25
+ ## 生成内容
26
+
27
+ 执行后会生成一个可直接开发组件库的模板项目,内置:
28
+
29
+ - `Vite` 库模式构建
30
+ - `React + TypeScript`
31
+ - `vite-plugin-dts` 类型声明输出
32
+ - 一个示例 `Button` 组件
33
+ - 一个本地预览页面,方便 `npm run dev`
34
+
35
+ ## 本地验证
36
+
37
+ ```bash
38
+ npm run smoke
39
+ npm run pack:check
40
+ ```
41
+
42
+ ## 后续建议
43
+
44
+ 如果你想继续,我下一步还可以帮你补:
45
+
46
+ - 交互式提问(比如选择包管理器、是否安装依赖)
47
+ - `tsup` / `rollup` 版本的模板
48
+ - `Storybook` 模板
49
+ - 自动初始化 git 仓库和首个提交
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from "../src/index.js";
4
+
5
+ run(process.argv.slice(2)).catch((error) => {
6
+ console.error(`\n[create-wucaishi-cpn] ${error.message}`);
7
+ process.exitCode = 1;
8
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "create-wucaishi-cpn",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a Vite + React component library project.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-wucaishi-cpn": "./bin/create-wucaishi-cpn.js"
8
+ },
9
+ "exports": {
10
+ ".": "./src/index.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "templates/vite-react-lib/_gitignore",
16
+ "templates/vite-react-lib/index.html",
17
+ "templates/vite-react-lib/package.json",
18
+ "templates/vite-react-lib/script.js",
19
+ "templates/vite-react-lib/src",
20
+ "templates/vite-react-lib/tsconfig.json",
21
+ "templates/vite-react-lib/vite.config.ts",
22
+ "README.md"
23
+ ],
24
+ "scripts": {
25
+ "pack:check": "npm pack --dry-run"
26
+ },
27
+ "keywords": [
28
+ "vite",
29
+ "react",
30
+ "component-library",
31
+ "scaffold",
32
+ "cli",
33
+ "template"
34
+ ],
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "license": "MIT"
39
+ }
package/src/index.js ADDED
@@ -0,0 +1,347 @@
1
+ import { spawn } from "node:child_process";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { stdin as input, stdout as output } from "node:process";
6
+ import { fileURLToPath } from "node:url";
7
+ import readline from "node:readline/promises";
8
+
9
+ const CLI_VERSION = "0.1.0";
10
+ const TEMPLATE_NAME = "vite-react-lib";
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const TEMPLATE_DIR = path.resolve(__dirname, "../templates", TEMPLATE_NAME);
14
+ const DOTFILE_ALIASES = new Map([["_gitignore", ".gitignore"]]);
15
+ const SKIPPED_TEMPLATE_ENTRIES = new Set([
16
+ ".DS_Store",
17
+ ".git",
18
+ ".turbo",
19
+ ".vite",
20
+ "bun.lock",
21
+ "bun.lockb",
22
+ "dist",
23
+ "node_modules",
24
+ "package-lock.json",
25
+ "pnpm-lock.yaml",
26
+ "yarn.lock",
27
+ ]);
28
+
29
+ export async function run(rawArgs = []) {
30
+ const options = parseArgs(rawArgs);
31
+
32
+ if (options.help) {
33
+ printHelp();
34
+ return;
35
+ }
36
+
37
+ if (options.version) {
38
+ console.log(CLI_VERSION);
39
+ return;
40
+ }
41
+
42
+ const requestedTarget = options.targetDir ?? (await promptProjectName());
43
+
44
+ if (!requestedTarget) {
45
+ throw new Error("Project name is required.");
46
+ }
47
+
48
+ const cwd = process.cwd();
49
+ const targetDir = path.resolve(cwd, requestedTarget);
50
+ const projectDirName = path.basename(targetDir);
51
+ const packageName = toValidPackageName(projectDirName);
52
+
53
+ if (!packageName || !isValidPackageName(packageName)) {
54
+ throw new Error(`Unable to derive a valid package name from "${projectDirName}".`);
55
+ }
56
+
57
+ const directoryState = await getDirectoryState(targetDir);
58
+ if (directoryState.exists && !directoryState.empty) {
59
+ if (!options.force) {
60
+ const shouldOverwrite = await confirmOverwrite(requestedTarget);
61
+ if (!shouldOverwrite) {
62
+ console.log("Canceled.");
63
+ return;
64
+ }
65
+ }
66
+
67
+ await fs.rm(targetDir, { recursive: true, force: true });
68
+ }
69
+
70
+ await fs.mkdir(targetDir, { recursive: true });
71
+
72
+ const replacements = {
73
+ "__PROJECT_NAME__": projectDirName,
74
+ "__PACKAGE_NAME__": packageName,
75
+ "__LIBRARY_NAME__": toPascalCase(projectDirName),
76
+ };
77
+
78
+ await copyTemplate(TEMPLATE_DIR, targetDir, replacements);
79
+
80
+ console.log(`\nScaffolded "${projectDirName}" in ${path.relative(cwd, targetDir) || "."}.`);
81
+
82
+ if (packageName !== projectDirName) {
83
+ console.log(`Package name normalized to "${packageName}".`);
84
+ }
85
+
86
+ if (options.install) {
87
+ await installDependencies(targetDir, options.packageManager);
88
+ printNextSteps(requestedTarget, options.packageManager, true);
89
+ return;
90
+ }
91
+
92
+ printNextSteps(requestedTarget, options.packageManager, false);
93
+ }
94
+
95
+ function parseArgs(rawArgs) {
96
+ const options = {
97
+ force: false,
98
+ help: false,
99
+ install: false,
100
+ packageManager: detectPackageManager(),
101
+ targetDir: undefined,
102
+ version: false,
103
+ };
104
+
105
+ for (let index = 0; index < rawArgs.length; index += 1) {
106
+ const arg = rawArgs[index];
107
+
108
+ if (arg === "--force" || arg === "-f") {
109
+ options.force = true;
110
+ continue;
111
+ }
112
+
113
+ if (arg === "--install") {
114
+ options.install = true;
115
+ continue;
116
+ }
117
+
118
+ if (arg === "--help" || arg === "-h") {
119
+ options.help = true;
120
+ continue;
121
+ }
122
+
123
+ if (arg === "--version" || arg === "-v") {
124
+ options.version = true;
125
+ continue;
126
+ }
127
+
128
+ if (arg === "--package-manager" || arg === "--pm") {
129
+ const packageManager = rawArgs[index + 1];
130
+
131
+ if (!packageManager) {
132
+ throw new Error("Missing value for --package-manager.");
133
+ }
134
+
135
+ options.packageManager = packageManager;
136
+ index += 1;
137
+ continue;
138
+ }
139
+
140
+ if (arg.startsWith("-")) {
141
+ throw new Error(`Unknown option: ${arg}`);
142
+ }
143
+
144
+ if (!options.targetDir) {
145
+ options.targetDir = arg;
146
+ continue;
147
+ }
148
+
149
+ throw new Error(`Unexpected argument: ${arg}`);
150
+ }
151
+
152
+ return options;
153
+ }
154
+
155
+ function printHelp() {
156
+ console.log(
157
+ [
158
+ "create-wucaishi-cpn",
159
+ "",
160
+ "Usage:",
161
+ " create-wucaishi-cpn <project-name> [options]",
162
+ "",
163
+ "Options:",
164
+ " -f, --force Overwrite the target directory if it exists",
165
+ " --install Install dependencies after scaffolding",
166
+ " --pm, --package-manager Choose npm, pnpm, yarn or bun",
167
+ " -h, --help Show help",
168
+ " -v, --version Show CLI version",
169
+ ].join("\n"),
170
+ );
171
+ }
172
+
173
+ async function promptProjectName() {
174
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
175
+ return undefined;
176
+ }
177
+
178
+ const rl = readline.createInterface({ input, output });
179
+
180
+ try {
181
+ const answer = await rl.question("Project name: ");
182
+ return answer.trim() || undefined;
183
+ } finally {
184
+ rl.close();
185
+ }
186
+ }
187
+
188
+ async function getDirectoryState(targetDir) {
189
+ try {
190
+ const stat = await fs.stat(targetDir);
191
+
192
+ if (!stat.isDirectory()) {
193
+ throw new Error(`Target path "${targetDir}" exists and is not a directory.`);
194
+ }
195
+
196
+ const entries = await fs.readdir(targetDir);
197
+ return { empty: entries.length === 0, exists: true };
198
+ } catch (error) {
199
+ if (error.code === "ENOENT") {
200
+ return { empty: true, exists: false };
201
+ }
202
+
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ async function confirmOverwrite(targetDir) {
208
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
209
+ throw new Error(
210
+ `Target directory "${targetDir}" is not empty. Re-run with --force to overwrite it.`,
211
+ );
212
+ }
213
+
214
+ const rl = readline.createInterface({ input, output });
215
+
216
+ try {
217
+ const answer = await rl.question(`"${targetDir}" is not empty. Overwrite it? (y/N) `);
218
+ return /^y(es)?$/i.test(answer.trim());
219
+ } finally {
220
+ rl.close();
221
+ }
222
+ }
223
+
224
+ async function copyTemplate(sourceDir, targetDir, replacements) {
225
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
226
+
227
+ for (const entry of entries) {
228
+ if (shouldSkipTemplateEntry(entry)) {
229
+ continue;
230
+ }
231
+
232
+ const sourcePath = path.join(sourceDir, entry.name);
233
+ const outputName = DOTFILE_ALIASES.get(entry.name) ?? entry.name;
234
+ const targetPath = path.join(targetDir, outputName);
235
+
236
+ if (entry.isDirectory()) {
237
+ await fs.mkdir(targetPath, { recursive: true });
238
+ await copyTemplate(sourcePath, targetPath, replacements);
239
+ continue;
240
+ }
241
+
242
+ const fileContents = await fs.readFile(sourcePath, "utf8");
243
+ const renderedContents = renderTemplate(fileContents, replacements);
244
+ await fs.writeFile(targetPath, renderedContents, "utf8");
245
+ }
246
+ }
247
+
248
+ function shouldSkipTemplateEntry(entry) {
249
+ if (SKIPPED_TEMPLATE_ENTRIES.has(entry.name)) {
250
+ return true;
251
+ }
252
+
253
+ return entry.isSymbolicLink();
254
+ }
255
+
256
+ function renderTemplate(template, replacements) {
257
+ return Object.entries(replacements).reduce((output, [token, value]) => {
258
+ return output.split(token).join(value);
259
+ }, template);
260
+ }
261
+
262
+ async function installDependencies(targetDir, packageManager) {
263
+ console.log(`\nInstalling dependencies with ${packageManager}...`);
264
+
265
+ const args = packageManager === "yarn" ? [] : ["install"];
266
+
267
+ await new Promise((resolve, reject) => {
268
+ const child = spawn(packageManager, args, {
269
+ cwd: targetDir,
270
+ shell: process.platform === "win32",
271
+ stdio: "inherit",
272
+ });
273
+
274
+ child.on("close", (code) => {
275
+ if (code === 0) {
276
+ resolve();
277
+ return;
278
+ }
279
+
280
+ reject(new Error(`${packageManager} exited with code ${code}.`));
281
+ });
282
+
283
+ child.on("error", reject);
284
+ });
285
+ }
286
+
287
+ function detectPackageManager() {
288
+ const userAgent = process.env.npm_config_user_agent ?? "";
289
+
290
+ if (userAgent.startsWith("pnpm")) {
291
+ return "pnpm";
292
+ }
293
+
294
+ if (userAgent.startsWith("yarn")) {
295
+ return "yarn";
296
+ }
297
+
298
+ if (userAgent.startsWith("bun")) {
299
+ return "bun";
300
+ }
301
+
302
+ return "npm";
303
+ }
304
+
305
+ function printNextSteps(targetDir, packageManager, alreadyInstalled) {
306
+ const steps = [];
307
+
308
+ if (targetDir !== ".") {
309
+ steps.push(`cd ${targetDir}`);
310
+ }
311
+
312
+ if (!alreadyInstalled) {
313
+ steps.push(packageManager === "yarn" ? "yarn" : `${packageManager} install`);
314
+ }
315
+
316
+ steps.push(`${packageManager} run dev`);
317
+ steps.push(`${packageManager} run build`);
318
+
319
+ console.log("\nNext steps:");
320
+ for (const step of steps) {
321
+ console.log(` ${step}`);
322
+ }
323
+ }
324
+
325
+ function isValidPackageName(value) {
326
+ return /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(value);
327
+ }
328
+
329
+ function toValidPackageName(projectName) {
330
+ return projectName
331
+ .trim()
332
+ .toLowerCase()
333
+ .replace(/\s+/g, "-")
334
+ .replace(/^[._]+/, "")
335
+ .replace(/[^a-z0-9-._~]+/g, "-")
336
+ .replace(/-+/g, "-")
337
+ .replace(/^-|-$/g, "");
338
+ }
339
+
340
+ function toPascalCase(value) {
341
+ return value
342
+ .replace(/[^a-zA-Z0-9]+/g, " ")
343
+ .split(" ")
344
+ .filter(Boolean)
345
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
346
+ .join("");
347
+ }
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ .DS_Store
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>test demo</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/demo/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "dev": "vite",
18
+ "typecheck": "tsc --noEmit",
19
+ "build": "npm run typecheck && vite build",
20
+ "preview": "vite preview"
21
+ },
22
+ "peerDependencies": {
23
+ "react": "^18.0.0 || ^19.0.0",
24
+ "react-dom": "^18.0.0 || ^19.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/react": "^18.3.0",
28
+ "@types/react-dom": "^18.3.0",
29
+ "@vitejs/plugin-react": "^4.3.0",
30
+ "react": "^18.3.1",
31
+ "react-dom": "^18.3.1",
32
+ "terser": "^5.31.0",
33
+ "typescript": "^5.5.4",
34
+ "vite": "^5.4.10"
35
+ },
36
+ "dependencies": {
37
+ "styled-components": "^6.4.1"
38
+ }
39
+ }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+
4
+ // demo
5
+ function Demo() {
6
+ return <div></div>;
7
+ }
8
+
9
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
10
+ <React.StrictMode>
11
+ <Demo />
12
+ </React.StrictMode>,
13
+ );
File without changes
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Bundler",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src"]
20
+ }
@@ -0,0 +1,28 @@
1
+ import { resolve, dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import react from "@vitejs/plugin-react";
5
+ import { defineConfig } from "vite";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ export default defineConfig({
11
+ plugins: [react()],
12
+ build: {
13
+ emptyOutDir: true,
14
+ minify: "terser",
15
+ lib: {
16
+ entry: resolve(__dirname, "src/index.ts"),
17
+ fileName: () => "index.js",
18
+ formats: ["es"],
19
+ name: "__LIBRARY_NAME__",
20
+ },
21
+ rollupOptions: {
22
+ external: ["react", "react/jsx-runtime", "react-dom"],
23
+ output: {
24
+ inlineDynamicImports: true,
25
+ },
26
+ },
27
+ },
28
+ });