Skip to content

Commit 4cc94da

Browse files
committed
chore: init wetw project
1 parent 5af7e3e commit 4cc94da

File tree

15 files changed

+685
-105
lines changed

15 files changed

+685
-105
lines changed

.changeset/zengjia-wetw-cli.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wetw": minor
3+
---
4+
5+
为 wetw 增加基于 cac 的 CLI(init/list/add),支持自定义配置路径、工作目录以及强制覆盖,默认写出 wetw.config.ts 并内置 counter 示例。

benchmark/data/2025-11-26.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"uni-app-webpack-vue2": {
3+
"babel": [
4+
279.05220800000006,
5+
1204.3177080000023
6+
]
7+
},
8+
"rax": {
9+
"babel": [
10+
43.26670800000102,
11+
56.27020799999991,
12+
50.776583000000755,
13+
20.865374999999403,
14+
11.805124999998952,
15+
89.58862500000032,
16+
297.7642080000005
17+
]
18+
},
19+
"uni-app-webpack5-vue2": {
20+
"babel": [
21+
471.1814169999998
22+
]
23+
},
24+
"mpx": {
25+
"babel": [
26+
478.64458399999967,
27+
23.821249999997235,
28+
193.04095800000505,
29+
325.3952500000014,
30+
148.299291000003,
31+
26.64470799999981,
32+
552.6072500000009
33+
]
34+
},
35+
"native-webpack": {
36+
"babel": [
37+
297.7599170000003
38+
]
39+
},
40+
"taro-vue3": {
41+
"babel": [
42+
372.9925839999996
43+
]
44+
},
45+
"taro-react": {
46+
"babel": [
47+
238.2564999999995,
48+
219.2747079999972,
49+
273.29708299999766
50+
]
51+
}
52+
}

packages/wetw/README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# wetw CLI 使用说明
2+
3+
一个借鉴 shadcn-ui 思路的生成器:通过配置文件 + 组件注册表,把模板文件直接拷贝到你的项目中,方便二次定制。配置文件默认使用 `wetw.config.ts`(c12 原生支持)。
4+
5+
## 安装
6+
7+
```bash
8+
pnpm add -D wetw
9+
```
10+
11+
或在未安装的情况下临时调用:
12+
13+
```bash
14+
npx wetw --help
15+
```
16+
17+
## 快速开始
18+
19+
1. 生成配置文件:
20+
21+
```bash
22+
wetw init
23+
```
24+
25+
会在当前目录写出 `wetw.config.ts`,默认输出目录是 `wetw/`
26+
27+
2. 查看可用组件:
28+
29+
```bash
30+
wetw list
31+
```
32+
33+
3. 添加组件(示例内置了一个 `counter`):
34+
35+
```bash
36+
wetw add counter
37+
```
38+
39+
生成的文件将落在配置的 `outDir` 下,例如 `wetw/counter/*`。如需覆盖已有文件,加上 `--force`
40+
41+
## 配置示例
42+
43+
`wetw.config.ts`
44+
45+
```ts
46+
import { defineConfig } from 'wetw'
47+
48+
export default defineConfig({
49+
outDir: 'wetw', // 生成目录,默认为 wetw
50+
// registry 可以是本地/远程 JSON,或直接写数组
51+
// registry: 'https://example.com/wetw/registry.json',
52+
// templatesRoot: './templates', // 用于解析 registry 文件中的相对 src
53+
})
54+
```
55+
56+
## 命令速查
57+
58+
- `wetw init [--config wetw.config.ts]`:写入默认配置文件。
59+
- `wetw list [--json]`:输出当前 registry 中的组件清单。
60+
- `wetw add <name...> [--force]`:按名称生成组件文件。
61+
- 通用参数:`--config` 指定配置文件,`--cwd` 指定工作目录。
62+
63+
## 自定义 registry
64+
65+
一个最小的注册表示例(保存为 `registry.json`):
66+
67+
```json
68+
[
69+
{
70+
"name": "button",
71+
"description": "自定义按钮",
72+
"files": [
73+
{ "path": "button/index.ts", "content": "export const btn = true" }
74+
]
75+
}
76+
]
77+
```
78+
79+
`wetw.config.ts` 中指定:
80+
81+
```ts
82+
export default defineConfig({
83+
registry: './registry.json',
84+
})
85+
```
86+
87+
随后执行 `wetw add button` 即可写入对应文件。\*\*\*

packages/wetw/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
"main": "./dist/index.cjs",
3232
"module": "./dist/index.mjs",
3333
"types": "./dist/index.d.mts",
34+
"bin": {
35+
"wetw": "./dist/cli.mjs"
36+
},
3437
"files": [
3538
"dist"
3639
],
@@ -43,5 +46,9 @@
4346
"lint": "eslint .",
4447
"lint:fix": "eslint . --fix"
4548
},
46-
"publishConfig": {}
49+
"publishConfig": {},
50+
"dependencies": {
51+
"c12": "^3.3.2",
52+
"cac": "^6.7.14"
53+
}
4754
}

packages/wetw/src/add.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { LoadWetwConfigOptions } from './config'
2+
import type { ResolvedWetwConfig, WetwRegistryFile, WetwRegistryItem } from './types'
3+
import { constants } from 'node:fs'
4+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
5+
import { dirname, isAbsolute, resolve } from 'node:path'
6+
import { loadWetwConfig } from './config'
7+
import { resolveRegistry } from './registry'
8+
import { isHttp } from './utils'
9+
10+
export interface AddComponentsOptions extends LoadWetwConfigOptions {
11+
force?: boolean
12+
registry?: WetwRegistryItem[]
13+
}
14+
15+
async function pathExists(target: string) {
16+
try {
17+
await access(target, constants.F_OK)
18+
return true
19+
}
20+
catch (error) {
21+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
22+
return false
23+
}
24+
throw error
25+
}
26+
}
27+
28+
async function getFileContent(file: WetwRegistryFile, config: ResolvedWetwConfig) {
29+
if ('content' in file && file.content !== undefined) {
30+
return file.content
31+
}
32+
33+
if ('src' in file) {
34+
if (isHttp(file.src)) {
35+
const response = await fetch(file.src)
36+
if (!response.ok) {
37+
throw new Error(`Failed to download ${file.src}: ${response.statusText}`)
38+
}
39+
return response.text()
40+
}
41+
42+
const from = isAbsolute(file.src) ? file.src : resolve(config.templatesRoot, file.src)
43+
return readFile(from, 'utf8')
44+
}
45+
46+
throw new Error(`Invalid registry file entry for ${file.path}`)
47+
}
48+
49+
async function writeFileSafely(target: string, content: string, force: boolean) {
50+
const exists = await pathExists(target)
51+
if (exists && !force) {
52+
throw new Error(`File already exists: ${target}`)
53+
}
54+
55+
await mkdir(dirname(target), { recursive: true })
56+
await writeFile(target, content, 'utf8')
57+
}
58+
59+
export async function addComponents(names: string[], options: AddComponentsOptions = {}) {
60+
if (!names.length) {
61+
throw new Error('Please provide at least one component name')
62+
}
63+
64+
const config = await loadWetwConfig({
65+
cwd: options.cwd,
66+
configFile: options.configFile,
67+
overrides: options.overrides,
68+
})
69+
70+
const registry = options.registry ?? (await resolveRegistry(config))
71+
const registryMap = new Map(registry.map(item => [item.name, item]))
72+
73+
for (const name of names) {
74+
const item = registryMap.get(name)
75+
if (!item) {
76+
throw new Error(`Component "${name}" not found in registry`)
77+
}
78+
79+
for (const file of item.files) {
80+
const content = await getFileContent(file, config)
81+
const target = resolve(config.outDir, file.path)
82+
await writeFileSafely(target, content, options.force ?? false)
83+
}
84+
}
85+
86+
return {
87+
added: names,
88+
outDir: config.outDir,
89+
}
90+
}

packages/wetw/src/cli.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env node
2+
import process from 'node:process'
3+
import { cac } from 'cac'
4+
import pkg from '../package.json' assert { type: 'json' }
5+
import { addComponents } from './add'
6+
import { loadWetwConfig } from './config'
7+
import { writeDefaultConfig } from './init'
8+
import { resolveRegistry } from './registry'
9+
10+
const cli = cac('wetw')
11+
12+
cli
13+
.option('--config <path>', 'Path to wetw.config.(ts|js|json)')
14+
.option('--cwd <path>', 'Working directory (defaults to process.cwd())')
15+
16+
function handleError(error: unknown) {
17+
console.error((error as Error).message)
18+
process.exitCode = 1
19+
}
20+
21+
cli
22+
.command('init', 'Create a config file')
23+
.option('--force', 'Overwrite existing config file')
24+
.action(async (options) => {
25+
try {
26+
const file = await writeDefaultConfig({
27+
cwd: options.cwd,
28+
configFile: options.config,
29+
force: options.force,
30+
})
31+
console.log(`Generated config at ${file}`)
32+
}
33+
catch (error) {
34+
handleError(error)
35+
}
36+
})
37+
38+
cli
39+
.command('list', 'List registry components')
40+
.option('--json', 'Emit list as JSON')
41+
.action(async (options) => {
42+
try {
43+
const config = await loadWetwConfig({
44+
cwd: options.cwd,
45+
configFile: options.config,
46+
})
47+
48+
const registry = await resolveRegistry(config)
49+
if (options.json) {
50+
console.log(JSON.stringify(registry, null, 2))
51+
return
52+
}
53+
54+
if (!registry.length) {
55+
console.log('Registry is empty')
56+
return
57+
}
58+
59+
console.log('Available components:')
60+
for (const item of registry) {
61+
const description = item.description ? ` - ${item.description}` : ''
62+
console.log(`- ${item.name}${description}`)
63+
}
64+
}
65+
catch (error) {
66+
handleError(error)
67+
}
68+
})
69+
70+
cli
71+
.command('add <names...>', 'Add components to the project')
72+
.option('--force', 'Overwrite existing files')
73+
.action(async (names: string[], options) => {
74+
try {
75+
if (!names?.length) {
76+
throw new Error('wetw add <name...> expects at least one component')
77+
}
78+
79+
await addComponents(names, {
80+
cwd: options.cwd,
81+
configFile: options.config,
82+
force: options.force,
83+
})
84+
console.log(`Added: ${names.join(', ')}`)
85+
}
86+
catch (error) {
87+
handleError(error)
88+
}
89+
})
90+
91+
cli.help()
92+
cli.version(pkg.version ?? '0.0.0')
93+
94+
if (process.argv.length <= 2) {
95+
cli.outputHelp()
96+
}
97+
98+
cli.parse()

0 commit comments

Comments
 (0)