cus-base-ui 0.2.2 → 0.2.4
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 +145 -55
- package/dist/ui-cli.cjs +232 -0
- package/package.json +3 -3
- package/scripts/ui-cli.ts +166 -2
package/README.md
CHANGED
|
@@ -1,55 +1,145 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
Bộ
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
# cus-base-ui
|
|
2
|
+
|
|
3
|
+
Bộ component UI cho React, phân phối qua CLI — copy trực tiếp vào dự án của bạn, không cần cài như package dependency (tương tự shadcn/ui).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Yêu cầu
|
|
8
|
+
|
|
9
|
+
- Node.js 18+
|
|
10
|
+
- Dự án React + Vite + TypeScript
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Bắt đầu nhanh
|
|
15
|
+
|
|
16
|
+
### 1. Khởi tạo dự án
|
|
17
|
+
|
|
18
|
+
Đứng tại thư mục gốc dự án của bạn, chạy:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx cus-base-ui init
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Lệnh này tự động thực hiện:
|
|
25
|
+
|
|
26
|
+
| Bước | Nội dung |
|
|
27
|
+
|------|----------|
|
|
28
|
+
| Cài dev packages | `tailwindcss`, `@tailwindcss/vite`, `@vitejs/plugin-react`, `vite-plugin-babel`, `babel-plugin-react-compiler`, `@types/node` |
|
|
29
|
+
| Tạo / cập nhật `vite.config.ts` | Thêm plugin Tailwind, React, React Compiler + alias `@`, `@lib`, `@components`, `@assets`, `@pages`, `@styles` |
|
|
30
|
+
| Cập nhật `tsconfig.json` | Thêm `baseUrl` + `paths` tương ứng với alias trên |
|
|
31
|
+
| Setup Tailwind CSS | Thêm `@import "tailwindcss";` vào `src/index.css` (tạo mới nếu chưa có) |
|
|
32
|
+
| Cài core utilities | `clsx`, `tailwind-merge` + tạo `src/lib/utils/cn.ts` |
|
|
33
|
+
|
|
34
|
+
> Nếu `vite.config.ts` hoặc `tsconfig.json` đã tồn tại và đã có cấu hình, CLI sẽ bỏ qua bước đó — không ghi đè.
|
|
35
|
+
|
|
36
|
+
**Kết quả `vite.config.ts` sau init:**
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { defineConfig } from 'vite';
|
|
40
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
41
|
+
import react from '@vitejs/plugin-react';
|
|
42
|
+
import babel from 'vite-plugin-babel';
|
|
43
|
+
import { reactCompilerPreset } from 'babel-plugin-react-compiler';
|
|
44
|
+
import path from 'path';
|
|
45
|
+
|
|
46
|
+
export default defineConfig({
|
|
47
|
+
plugins: [
|
|
48
|
+
tailwindcss(),
|
|
49
|
+
react(),
|
|
50
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
51
|
+
],
|
|
52
|
+
resolve: {
|
|
53
|
+
alias: {
|
|
54
|
+
'@': path.resolve(__dirname, './src'),
|
|
55
|
+
'@lib': path.resolve(__dirname, './src/lib'),
|
|
56
|
+
'@components': path.resolve(__dirname, './src/components'),
|
|
57
|
+
'@assets': path.resolve(__dirname, './src/assets'),
|
|
58
|
+
'@pages': path.resolve(__dirname, './src/pages'),
|
|
59
|
+
'@styles': path.resolve(__dirname, './src/styles'),
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### 2. Thêm component
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx cus-base-ui add button
|
|
71
|
+
npx cus-base-ui add button input switch # nhiều component cùng lúc
|
|
72
|
+
npx cus-base-ui add button --force # ghi đè nếu đã tồn tại
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Component được copy thẳng vào `src/components/ui/<name>/`. Các component phụ thuộc lẫn nhau sẽ tự động được kéo theo.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### 3. Xóa component
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx cus-base-ui remove button
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### 4. Liệt kê tất cả component
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx cus-base-ui list
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### 5. Hướng dẫn cấu hình Tailwind theme
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx cus-base-ui tailwind
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Tùy chọn
|
|
104
|
+
|
|
105
|
+
| Flag | Mô tả |
|
|
106
|
+
|------|-------|
|
|
107
|
+
| `--local` | Dùng `registry.json` cục bộ thay vì tải từ remote |
|
|
108
|
+
| `--force` | Ghi đè file đã tồn tại khi `add` |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Dành cho maintainer
|
|
113
|
+
|
|
114
|
+
### Cập nhật registry (sau khi thêm/sửa component)
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm run registry:build
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Sau đó commit + push lên GitHub để người dùng nhận được bản mới nhất.
|
|
121
|
+
|
|
122
|
+
### Build CLI
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm run build:cli
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Output: `dist/ui-cli.js`
|
|
129
|
+
|
|
130
|
+
### Phát hành lên NPM
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm login
|
|
134
|
+
npm publish
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
> Nếu tên package bị trùng, đổi `name` trong `package.json` trước khi publish.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Cơ chế hoạt động
|
|
142
|
+
|
|
143
|
+
- **Remote mode (mặc định):** Tải `registry.json` từ `https://raw.githubusercontent.com/huy14032003/ui-component/main/registry.json`
|
|
144
|
+
- **Local mode (`--local`):** Đọc `registry.json` ngay tại thư mục hiện tại
|
|
145
|
+
- Registry chứa source code + danh sách npm dependencies của từng component — CLI đọc rồi copy/install vào dự án target
|
package/dist/ui-cli.cjs
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// scripts/ui-cli.ts
|
|
26
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
27
|
+
var import_path = __toESM(require("path"), 1);
|
|
28
|
+
var import_child_process = require("child_process");
|
|
29
|
+
var REGISTRY_LOCAL = "./registry.json";
|
|
30
|
+
var REGISTRY_REMOTE = "https://raw.githubusercontent.com/huy14032003/ui-component/main/registry.json";
|
|
31
|
+
var log = (msg) => console.log(`[CUS-BASE-UI] ${msg}`);
|
|
32
|
+
var warn = (msg) => console.warn(`[CUS-BASE-UI] WARN: ${msg}`);
|
|
33
|
+
var error = (msg) => console.error(`[CUS-BASE-UI] ERROR: ${msg}`);
|
|
34
|
+
var getTargetProjectDir = () => process.cwd();
|
|
35
|
+
var validateRegistry = (data) => {
|
|
36
|
+
if (!data || typeof data !== "object") return false;
|
|
37
|
+
const reg = data;
|
|
38
|
+
return "components" in reg && typeof reg.components === "object" && reg.components !== null;
|
|
39
|
+
};
|
|
40
|
+
var getRegistry = async (isLocal) => {
|
|
41
|
+
if (isLocal && import_fs.default.existsSync(REGISTRY_LOCAL)) {
|
|
42
|
+
log("Using local registry...");
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse(import_fs.default.readFileSync(REGISTRY_LOCAL, "utf-8"));
|
|
45
|
+
if (!validateRegistry(data)) {
|
|
46
|
+
error('Invalid local registry format \u2014 missing "components" field.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return data;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
error(`Failed to parse local registry: ${err instanceof Error ? err.message : err}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
log("Fetching registry from remote...");
|
|
56
|
+
try {
|
|
57
|
+
const response = await fetch(REGISTRY_REMOTE);
|
|
58
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
59
|
+
const data = await response.json();
|
|
60
|
+
if (!validateRegistry(data)) {
|
|
61
|
+
error('Invalid remote registry format \u2014 missing "components" field.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return data;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
67
|
+
error(`Cannot fetch registry: ${message}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var installNpmPackages = (packages, cwd) => {
|
|
72
|
+
if (packages.length === 0) return;
|
|
73
|
+
const pkgJsonPath = import_path.default.join(cwd, "package.json");
|
|
74
|
+
let toInstall = packages;
|
|
75
|
+
if (import_fs.default.existsSync(pkgJsonPath)) {
|
|
76
|
+
const pkg = JSON.parse(import_fs.default.readFileSync(pkgJsonPath, "utf-8"));
|
|
77
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
78
|
+
toInstall = packages.filter((p) => !allDeps[p]);
|
|
79
|
+
}
|
|
80
|
+
if (toInstall.length === 0) return;
|
|
81
|
+
log(`Installing: ${toInstall.join(", ")}...`);
|
|
82
|
+
try {
|
|
83
|
+
(0, import_child_process.execSync)(`npm install ${toInstall.join(" ")} --save`, { stdio: "inherit", cwd });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
error(`Failed to install packages: ${toInstall.join(", ")}. ${err instanceof Error ? err.message : ""}`);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var ensureCore = (registry, cwd) => {
|
|
89
|
+
const core = registry.core;
|
|
90
|
+
if (!core) return;
|
|
91
|
+
installNpmPackages(core.dependencies, cwd);
|
|
92
|
+
for (const file of core.files) {
|
|
93
|
+
const targetPath = import_path.default.join(cwd, file.path);
|
|
94
|
+
const targetDir = import_path.default.dirname(targetPath);
|
|
95
|
+
if (!import_fs.default.existsSync(targetDir)) {
|
|
96
|
+
import_fs.default.mkdirSync(targetDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
if (!import_fs.default.existsSync(targetPath)) {
|
|
99
|
+
import_fs.default.writeFileSync(targetPath, file.content);
|
|
100
|
+
log(`Created core file: ${file.path}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var addComponent = (name, registry, cwd, options, added = /* @__PURE__ */ new Set()) => {
|
|
105
|
+
if (added.has(name)) return;
|
|
106
|
+
added.add(name);
|
|
107
|
+
const component = registry.components[name];
|
|
108
|
+
if (!component) {
|
|
109
|
+
error(`Component "${name}" not found. Run 'list' to see available components.`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
log(`Adding: ${name}...`);
|
|
113
|
+
ensureCore(registry, cwd);
|
|
114
|
+
installNpmPackages(component.dependencies, cwd);
|
|
115
|
+
if (component.internalDependencies) {
|
|
116
|
+
for (const dep of component.internalDependencies) {
|
|
117
|
+
if (registry.components[dep]) {
|
|
118
|
+
addComponent(dep, registry, cwd, options, added);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const file of component.files) {
|
|
123
|
+
const targetPath = import_path.default.join(cwd, file.path);
|
|
124
|
+
const targetDir = import_path.default.dirname(targetPath);
|
|
125
|
+
if (!import_fs.default.existsSync(targetDir)) {
|
|
126
|
+
import_fs.default.mkdirSync(targetDir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
if (import_fs.default.existsSync(targetPath) && !options.force) {
|
|
129
|
+
warn(`Skipped (exists): ${file.path} \u2014 use --force to overwrite`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
import_fs.default.writeFileSync(targetPath, file.content);
|
|
133
|
+
log(`Created: ${file.path}`);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var removeComponent = (name, registry, cwd) => {
|
|
137
|
+
const component = registry.components[name];
|
|
138
|
+
if (!component) {
|
|
139
|
+
error(`Component "${name}" not found.`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
log(`Removing: ${name}...`);
|
|
143
|
+
for (const file of component.files) {
|
|
144
|
+
const targetPath = import_path.default.join(cwd, file.path);
|
|
145
|
+
if (import_fs.default.existsSync(targetPath)) {
|
|
146
|
+
import_fs.default.unlinkSync(targetPath);
|
|
147
|
+
log(`Deleted: ${file.path}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const file of component.files) {
|
|
151
|
+
const targetDir = import_path.default.dirname(import_path.default.join(cwd, file.path));
|
|
152
|
+
try {
|
|
153
|
+
if (import_fs.default.existsSync(targetDir) && import_fs.default.readdirSync(targetDir).length === 0) {
|
|
154
|
+
import_fs.default.rmdirSync(targetDir);
|
|
155
|
+
log(`Removed empty dir: ${import_path.default.relative(cwd, targetDir)}`);
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
warn(`Could not remove directory: ${err instanceof Error ? err.message : err}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var main = async () => {
|
|
163
|
+
const args = process.argv.slice(2);
|
|
164
|
+
const isLocal = args.includes("--local");
|
|
165
|
+
const isForce = args.includes("--force");
|
|
166
|
+
const filteredArgs = args.filter((a) => !a.startsWith("--"));
|
|
167
|
+
const command = filteredArgs[0];
|
|
168
|
+
const componentNames = filteredArgs.slice(1);
|
|
169
|
+
const cwd = getTargetProjectDir();
|
|
170
|
+
const registry = await getRegistry(isLocal);
|
|
171
|
+
switch (command) {
|
|
172
|
+
case "add": {
|
|
173
|
+
if (componentNames.length === 0) {
|
|
174
|
+
error("Usage: npx cus-base-ui add <component-name> [--force]");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const name of componentNames) {
|
|
178
|
+
addComponent(name, registry, cwd, { force: isForce });
|
|
179
|
+
}
|
|
180
|
+
log("Done!");
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case "remove": {
|
|
184
|
+
if (componentNames.length === 0) {
|
|
185
|
+
error("Usage: npx cus-base-ui remove <component-name>");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const name of componentNames) {
|
|
189
|
+
removeComponent(name, registry, cwd);
|
|
190
|
+
}
|
|
191
|
+
log("Done!");
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "list": {
|
|
195
|
+
const components = Object.keys(registry.components).sort();
|
|
196
|
+
log(`Available components (${components.length}):`);
|
|
197
|
+
for (const k of components) {
|
|
198
|
+
const deps = registry.components[k].internalDependencies;
|
|
199
|
+
const depStr = deps?.length ? ` (requires: ${deps.join(", ")})` : "";
|
|
200
|
+
console.log(` - ${k}${depStr}`);
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "init": {
|
|
205
|
+
ensureCore(registry, cwd);
|
|
206
|
+
log("Initialization complete.");
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "tailwind": {
|
|
210
|
+
console.log("\n--- Copy to tailwind.config.ts / tailwind.config.js ---\n");
|
|
211
|
+
console.log("// See README_CLI.md for full theme config");
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
default: {
|
|
215
|
+
console.log(`
|
|
216
|
+
cus-base-ui \u2014 UI Component CLI
|
|
217
|
+
|
|
218
|
+
Commands:
|
|
219
|
+
init Initialize project (install core deps + files)
|
|
220
|
+
add <name> [--force] Add component(s) to your project
|
|
221
|
+
remove <name> Remove component(s) from your project
|
|
222
|
+
list List all available components
|
|
223
|
+
tailwind Show Tailwind config instructions
|
|
224
|
+
|
|
225
|
+
Options:
|
|
226
|
+
--local Use local registry.json instead of remote
|
|
227
|
+
--force Overwrite existing files when adding
|
|
228
|
+
`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cus-base-ui",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"cus-base-ui": "./dist/ui-cli.
|
|
7
|
+
"cus-base-ui": "./dist/ui-cli.cjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"dev": "vite",
|
|
17
17
|
"build": "tsc -b && vite build",
|
|
18
|
-
"build:cli": "npx -y esbuild scripts/ui-cli.ts --bundle --platform=node --outfile=dist/ui-cli.
|
|
18
|
+
"build:cli": "npx -y esbuild scripts/ui-cli.ts --bundle --platform=node --outfile=dist/ui-cli.cjs --format=cjs --packages=external",
|
|
19
19
|
"lint": "eslint .",
|
|
20
20
|
"preview": "vite preview",
|
|
21
21
|
"test": "vitest",
|
package/scripts/ui-cli.ts
CHANGED
|
@@ -56,7 +56,7 @@ const getRegistry = async (isLocal: boolean): Promise<Registry> => {
|
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
const installNpmPackages = (packages: string[], cwd: string) => {
|
|
59
|
+
const installNpmPackages = (packages: string[], cwd: string, dev = false) => {
|
|
60
60
|
if (packages.length === 0) return;
|
|
61
61
|
|
|
62
62
|
const pkgJsonPath = path.join(cwd, 'package.json');
|
|
@@ -71,13 +71,174 @@ const installNpmPackages = (packages: string[], cwd: string) => {
|
|
|
71
71
|
if (toInstall.length === 0) return;
|
|
72
72
|
|
|
73
73
|
log(`Installing: ${toInstall.join(', ')}...`);
|
|
74
|
+
const flag = dev ? '--save-dev' : '--save';
|
|
74
75
|
try {
|
|
75
|
-
execSync(`npm install ${toInstall.join(' ')}
|
|
76
|
+
execSync(`npm install ${toInstall.join(' ')} ${flag}`, { stdio: 'inherit', cwd });
|
|
76
77
|
} catch (err) {
|
|
77
78
|
error(`Failed to install packages: ${toInstall.join(', ')}. ${err instanceof Error ? err.message : ''}`);
|
|
78
79
|
}
|
|
79
80
|
};
|
|
80
81
|
|
|
82
|
+
const VITE_DEV_PACKAGES = ['tailwindcss', '@tailwindcss/vite', '@vitejs/plugin-react', 'vite-plugin-babel', 'babel-plugin-react-compiler', '@types/node'];
|
|
83
|
+
|
|
84
|
+
const VITE_CONFIG_TEMPLATE = `import { defineConfig } from 'vite';
|
|
85
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
86
|
+
import react from '@vitejs/plugin-react';
|
|
87
|
+
import babel from 'vite-plugin-babel';
|
|
88
|
+
import { reactCompilerPreset } from 'babel-plugin-react-compiler';
|
|
89
|
+
import path from 'path';
|
|
90
|
+
|
|
91
|
+
// https://vite.dev/config/
|
|
92
|
+
export default defineConfig({
|
|
93
|
+
plugins: [
|
|
94
|
+
tailwindcss(),
|
|
95
|
+
react(),
|
|
96
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
97
|
+
],
|
|
98
|
+
resolve: {
|
|
99
|
+
alias: {
|
|
100
|
+
'@': path.resolve(__dirname, './src'),
|
|
101
|
+
'@lib': path.resolve(__dirname, './src/lib'),
|
|
102
|
+
'@components': path.resolve(__dirname, './src/components'),
|
|
103
|
+
'@assets': path.resolve(__dirname, './src/assets'),
|
|
104
|
+
'@pages': path.resolve(__dirname, './src/pages'),
|
|
105
|
+
'@styles': path.resolve(__dirname, './src/styles'),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
const TSCONFIG_PATHS = {
|
|
112
|
+
'@/*': ['./src/*'],
|
|
113
|
+
'@lib/*': ['./src/lib/*'],
|
|
114
|
+
'@components/*': ['./src/components/*'],
|
|
115
|
+
'@assets/*': ['./src/assets/*'],
|
|
116
|
+
'@pages/*': ['./src/pages/*'],
|
|
117
|
+
'@styles/*': ['./src/styles/*'],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const setupViteConfig = (cwd: string) => {
|
|
121
|
+
installNpmPackages(VITE_DEV_PACKAGES, cwd, true);
|
|
122
|
+
|
|
123
|
+
const configTs = path.join(cwd, 'vite.config.ts');
|
|
124
|
+
const configJs = path.join(cwd, 'vite.config.js');
|
|
125
|
+
|
|
126
|
+
if (!fs.existsSync(configTs) && !fs.existsSync(configJs)) {
|
|
127
|
+
fs.writeFileSync(configTs, VITE_CONFIG_TEMPLATE);
|
|
128
|
+
log('Created vite.config.ts with Tailwind + React Compiler setup.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const existingPath = fs.existsSync(configTs) ? configTs : configJs;
|
|
133
|
+
const content = fs.readFileSync(existingPath, 'utf-8');
|
|
134
|
+
|
|
135
|
+
const missingImports: string[] = [];
|
|
136
|
+
if (!content.includes('@tailwindcss/vite')) missingImports.push("import tailwindcss from '@tailwindcss/vite';");
|
|
137
|
+
if (!content.includes('@vitejs/plugin-react')) missingImports.push("import react from '@vitejs/plugin-react';");
|
|
138
|
+
if (!content.includes('vite-plugin-babel')) missingImports.push("import babel from 'vite-plugin-babel';");
|
|
139
|
+
if (!content.includes('babel-plugin-react-compiler')) missingImports.push("import { reactCompilerPreset } from 'babel-plugin-react-compiler';");
|
|
140
|
+
|
|
141
|
+
const missingPlugins: string[] = [];
|
|
142
|
+
if (!content.includes('tailwindcss()')) missingPlugins.push('tailwindcss()');
|
|
143
|
+
if (!content.includes('react()') && !content.includes('react({')) missingPlugins.push('react()');
|
|
144
|
+
if (!content.includes('reactCompilerPreset')) missingPlugins.push('babel({ presets: [reactCompilerPreset()] })');
|
|
145
|
+
|
|
146
|
+
const hasAlias = content.includes('alias:') || content.includes("'@'") || content.includes('"@"');
|
|
147
|
+
|
|
148
|
+
if (missingImports.length === 0 && missingPlugins.length === 0 && hasAlias) {
|
|
149
|
+
log('vite.config already configured — skipping.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
warn(`${path.basename(existingPath)} exists but is missing required setup. Add the following manually:`);
|
|
154
|
+
if (missingImports.length > 0) {
|
|
155
|
+
console.log('\n // Imports to add:');
|
|
156
|
+
for (const imp of missingImports) console.log(` ${imp}`);
|
|
157
|
+
}
|
|
158
|
+
if (missingPlugins.length > 0) {
|
|
159
|
+
console.log('\n // Plugins to add inside defineConfig({ plugins: [...] }):');
|
|
160
|
+
for (const plugin of missingPlugins) console.log(` ${plugin},`);
|
|
161
|
+
}
|
|
162
|
+
if (!hasAlias) {
|
|
163
|
+
console.log('\n // resolve.alias to add inside defineConfig({}):');
|
|
164
|
+
console.log(" resolve: {");
|
|
165
|
+
console.log(" alias: {");
|
|
166
|
+
console.log(" '@': path.resolve(__dirname, './src'),");
|
|
167
|
+
console.log(" '@lib': path.resolve(__dirname, './src/lib'),");
|
|
168
|
+
console.log(" '@components': path.resolve(__dirname, './src/components'),");
|
|
169
|
+
console.log(" '@assets': path.resolve(__dirname, './src/assets'),");
|
|
170
|
+
console.log(" '@pages': path.resolve(__dirname, './src/pages'),");
|
|
171
|
+
console.log(" '@styles': path.resolve(__dirname, './src/styles'),");
|
|
172
|
+
console.log(" },");
|
|
173
|
+
console.log(" },");
|
|
174
|
+
}
|
|
175
|
+
console.log('');
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const ensureTailwindCss = (cwd: string) => {
|
|
179
|
+
const candidates = ['src/index.css', 'src/App.css', 'src/main.css'];
|
|
180
|
+
for (const cssFile of candidates) {
|
|
181
|
+
const cssPath = path.join(cwd, cssFile);
|
|
182
|
+
if (fs.existsSync(cssPath)) {
|
|
183
|
+
const content = fs.readFileSync(cssPath, 'utf-8');
|
|
184
|
+
if (!content.includes('@import "tailwindcss"') && !content.includes("@import 'tailwindcss'")) {
|
|
185
|
+
fs.writeFileSync(cssPath, `@import "tailwindcss";\n\n${content}`);
|
|
186
|
+
log(`Added @import "tailwindcss" to ${cssFile}`);
|
|
187
|
+
} else {
|
|
188
|
+
log(`${cssFile} already imports Tailwind — skipping.`);
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// No CSS file found — create src/index.css
|
|
194
|
+
const srcDir = path.join(cwd, 'src');
|
|
195
|
+
if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir, { recursive: true });
|
|
196
|
+
fs.writeFileSync(path.join(srcDir, 'index.css'), '@import "tailwindcss";\n');
|
|
197
|
+
log('Created src/index.css with @import "tailwindcss"');
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const setupTsConfig = (cwd: string) => {
|
|
201
|
+
const candidates = ['tsconfig.app.json', 'tsconfig.json'];
|
|
202
|
+
|
|
203
|
+
for (const candidate of candidates) {
|
|
204
|
+
const configPath = path.join(cwd, candidate);
|
|
205
|
+
if (!fs.existsSync(configPath)) continue;
|
|
206
|
+
|
|
207
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
208
|
+
|
|
209
|
+
if (raw.includes('"@/*"') || raw.includes("'@/*'")) {
|
|
210
|
+
log(`${candidate} already has path aliases — skipping.`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Strip single-line and block comments before parsing
|
|
216
|
+
const stripped = raw.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
217
|
+
const parsed = JSON.parse(stripped) as { compilerOptions?: Record<string, unknown> };
|
|
218
|
+
if (!parsed.compilerOptions) parsed.compilerOptions = {};
|
|
219
|
+
parsed.compilerOptions.baseUrl = '.';
|
|
220
|
+
parsed.compilerOptions.paths = TSCONFIG_PATHS;
|
|
221
|
+
fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2));
|
|
222
|
+
log(`Added path aliases to ${candidate}.`);
|
|
223
|
+
} catch {
|
|
224
|
+
warn(`Could not auto-patch ${candidate}. Add these to compilerOptions manually:`);
|
|
225
|
+
console.log('\n "baseUrl": ".",');
|
|
226
|
+
console.log(' "paths": {');
|
|
227
|
+
for (const [alias, targets] of Object.entries(TSCONFIG_PATHS)) {
|
|
228
|
+
console.log(` "${alias}": ["${targets[0]}"],`);
|
|
229
|
+
}
|
|
230
|
+
console.log(' }');
|
|
231
|
+
console.log('');
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// No tsconfig found — create a minimal one
|
|
237
|
+
const newConfig = { compilerOptions: { baseUrl: '.', paths: TSCONFIG_PATHS } };
|
|
238
|
+
fs.writeFileSync(path.join(cwd, 'tsconfig.json'), JSON.stringify(newConfig, null, 2));
|
|
239
|
+
log('Created tsconfig.json with path aliases.');
|
|
240
|
+
};
|
|
241
|
+
|
|
81
242
|
const ensureCore = (registry: { core?: { dependencies: string[]; files: { path: string; content: string }[] } }, cwd: string) => {
|
|
82
243
|
const core = registry.core;
|
|
83
244
|
if (!core) return;
|
|
@@ -226,6 +387,9 @@ const main = async () => {
|
|
|
226
387
|
}
|
|
227
388
|
|
|
228
389
|
case 'init': {
|
|
390
|
+
setupViteConfig(cwd);
|
|
391
|
+
setupTsConfig(cwd);
|
|
392
|
+
ensureTailwindCss(cwd);
|
|
229
393
|
ensureCore(registry, cwd);
|
|
230
394
|
log('Initialization complete.');
|
|
231
395
|
break;
|