basuicn 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 +146 -0
- package/README_CLI.md +51 -0
- package/dist/assets/index-1YAQdTE0.css +2 -0
- package/dist/assets/index-BsQ6nn74.js +237 -0
- package/dist/favicon.svg +1 -0
- package/dist/icons.svg +24 -0
- package/dist/index.html +13 -0
- package/dist/ui-cli.js +124 -0
- package/package.json +86 -0
- package/registry.json +593 -0
- package/scripts/build-registry.ts +209 -0
- package/scripts/ui-cli.ts +485 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const COMPONENTS_DIR = './src/components/ui';
|
|
5
|
+
const OUTPUT_FILE = './registry.json';
|
|
6
|
+
|
|
7
|
+
// Directories to exclude from registry (not reusable components)
|
|
8
|
+
const EXCLUDE_DIRS = new Set(['icons', 'layout', 'vs-code', 'pretty-code', 'Showcase.tsx']);
|
|
9
|
+
|
|
10
|
+
interface RegistryFile {
|
|
11
|
+
path: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RegistryComponent {
|
|
16
|
+
name: string;
|
|
17
|
+
dependencies: string[];
|
|
18
|
+
internalDependencies: string[];
|
|
19
|
+
files: RegistryFile[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Registry {
|
|
23
|
+
core: {
|
|
24
|
+
dependencies: string[];
|
|
25
|
+
files: RegistryFile[];
|
|
26
|
+
};
|
|
27
|
+
components: Record<string, RegistryComponent>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const getFiles = (dir: string): string[] => {
|
|
31
|
+
const files: string[] = [];
|
|
32
|
+
let entries: string[];
|
|
33
|
+
try {
|
|
34
|
+
entries = fs.readdirSync(dir);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.warn(`Cannot read directory ${dir}: ${err}`);
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
for (const file of entries) {
|
|
40
|
+
const fullPath = path.join(dir, file);
|
|
41
|
+
try {
|
|
42
|
+
const stat = fs.statSync(fullPath);
|
|
43
|
+
if (stat.isDirectory()) {
|
|
44
|
+
files.push(...getFiles(fullPath));
|
|
45
|
+
} else if (file.endsWith('.tsx') || file.endsWith('.ts')) {
|
|
46
|
+
if (!file.includes('.test.') && !file.includes('.stories.')) {
|
|
47
|
+
files.push(fullPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn(`Skipping ${fullPath}: ${err}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return files;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const INTERNAL_ALIAS_PREFIXES = ['@/', '@lib/', '@components/', '@app/'];
|
|
58
|
+
|
|
59
|
+
const parseDependencies = (content: string): string[] => {
|
|
60
|
+
const dependencies = new Set<string>();
|
|
61
|
+
const importRegex = /(?:from|import)\s+['"]([^'"]+)['"]/g;
|
|
62
|
+
let match;
|
|
63
|
+
|
|
64
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
65
|
+
const pkg = match[1];
|
|
66
|
+
|
|
67
|
+
// Skip relative imports
|
|
68
|
+
if (pkg.startsWith('.')) continue;
|
|
69
|
+
// Skip internal aliases
|
|
70
|
+
if (INTERNAL_ALIAS_PREFIXES.some(prefix => pkg.startsWith(prefix))) continue;
|
|
71
|
+
|
|
72
|
+
// Extract package name
|
|
73
|
+
const parts = pkg.split('/');
|
|
74
|
+
if (pkg.startsWith('@') && parts.length >= 2) {
|
|
75
|
+
dependencies.add(`${parts[0]}/${parts[1]}`);
|
|
76
|
+
} else {
|
|
77
|
+
dependencies.add(parts[0]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Remove React (always a peer dependency)
|
|
82
|
+
dependencies.delete('react');
|
|
83
|
+
dependencies.delete('react-dom');
|
|
84
|
+
|
|
85
|
+
return [...dependencies];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const getInternalDeps = (content: string, currentDirName: string): string[] => {
|
|
89
|
+
const internalDeps = new Set<string>();
|
|
90
|
+
|
|
91
|
+
// 1. Alias imports: @/components/ui/xxx or @components/ui/xxx
|
|
92
|
+
const aliasRegex = /from\s+['"](?:@\/?components\/ui)\/([^'"]+)['"]/g;
|
|
93
|
+
let match;
|
|
94
|
+
while ((match = aliasRegex.exec(content)) !== null) {
|
|
95
|
+
const depPath = match[1].split('/')[0];
|
|
96
|
+
if (depPath !== currentDirName && depPath !== 'icons') {
|
|
97
|
+
internalDeps.add(depPath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Relative imports: ../spinner/Spinner
|
|
102
|
+
const relativeRegex = /from\s+['"]\.\.\/([^'"]+)['"]/g;
|
|
103
|
+
while ((match = relativeRegex.exec(content)) !== null) {
|
|
104
|
+
const depPath = match[1].split('/')[0];
|
|
105
|
+
if (depPath !== currentDirName) {
|
|
106
|
+
internalDeps.add(depPath);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [...internalDeps];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const buildRegistry = () => {
|
|
114
|
+
console.log('Building component registry...');
|
|
115
|
+
|
|
116
|
+
const registry: Registry = {
|
|
117
|
+
core: {
|
|
118
|
+
dependencies: [
|
|
119
|
+
'@base-ui/react',
|
|
120
|
+
'clsx',
|
|
121
|
+
'tailwind-merge',
|
|
122
|
+
'tailwind-variants',
|
|
123
|
+
'tailwindcss-animate',
|
|
124
|
+
'@tailwindcss/vite',
|
|
125
|
+
'autoprefixer',
|
|
126
|
+
'tailwindcss',
|
|
127
|
+
'postcss',
|
|
128
|
+
],
|
|
129
|
+
files: [
|
|
130
|
+
{
|
|
131
|
+
path: 'src/lib/utils/cn.ts',
|
|
132
|
+
content: fs.readFileSync('./src/lib/utils/cn.ts', 'utf-8'),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
path: 'src/styles/index.css',
|
|
136
|
+
content: fs.readFileSync('./src/styles/index.css', 'utf-8'),
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
components: {},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const componentDirs = fs.readdirSync(COMPONENTS_DIR);
|
|
144
|
+
|
|
145
|
+
for (const dirName of componentDirs) {
|
|
146
|
+
if (EXCLUDE_DIRS.has(dirName)) continue;
|
|
147
|
+
|
|
148
|
+
const dirPath = path.join(COMPONENTS_DIR, dirName);
|
|
149
|
+
if (!fs.statSync(dirPath).isDirectory()) continue;
|
|
150
|
+
|
|
151
|
+
const files = getFiles(dirPath);
|
|
152
|
+
if (files.length === 0) continue;
|
|
153
|
+
|
|
154
|
+
// Find main file: prefer <DirName>.tsx, then any .tsx
|
|
155
|
+
const mainFile =
|
|
156
|
+
files.find(
|
|
157
|
+
(f) =>
|
|
158
|
+
f.toLowerCase().includes(dirName.toLowerCase()) &&
|
|
159
|
+
f.endsWith('.tsx')
|
|
160
|
+
) || files.find((f) => f.endsWith('.tsx'));
|
|
161
|
+
|
|
162
|
+
if (!mainFile) continue;
|
|
163
|
+
|
|
164
|
+
// Parse all files in the component directory for dependencies
|
|
165
|
+
const allDependencies = new Set<string>();
|
|
166
|
+
const allInternalDeps = new Set<string>();
|
|
167
|
+
|
|
168
|
+
for (const file of files) {
|
|
169
|
+
try {
|
|
170
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
171
|
+
for (const dep of parseDependencies(content)) allDependencies.add(dep);
|
|
172
|
+
for (const dep of getInternalDeps(content, dirName)) allInternalDeps.add(dep);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.warn(`Failed to read ${file}: ${err}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
registry.components[dirName] = {
|
|
179
|
+
name: dirName,
|
|
180
|
+
dependencies: [...allDependencies],
|
|
181
|
+
internalDependencies: [...allInternalDeps],
|
|
182
|
+
files: files.map((f) => {
|
|
183
|
+
try {
|
|
184
|
+
return {
|
|
185
|
+
path: f.replace(/\\/g, '/').replace(/^\.\//, ''),
|
|
186
|
+
content: fs.readFileSync(f, 'utf-8'),
|
|
187
|
+
};
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.warn(`Failed to read ${f}: ${err}`);
|
|
190
|
+
return { path: f.replace(/\\/g, '/').replace(/^\.\//, ''), content: '' };
|
|
191
|
+
}
|
|
192
|
+
}).filter(f => f.content !== ''),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const componentCount = Object.keys(registry.components).length;
|
|
197
|
+
if (componentCount === 0) {
|
|
198
|
+
console.warn('Warning: No components found in registry');
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(registry, null, 2));
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error(`Failed to write registry: ${err}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
console.log(`Registry built: ${componentCount} components → ${OUTPUT_FILE}`);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
buildRegistry();
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
const REGISTRY_LOCAL = './registry.json';
|
|
7
|
+
const REGISTRY_REMOTE = 'https://raw.githubusercontent.com/huy14032003/ui-component/main/registry.json';
|
|
8
|
+
|
|
9
|
+
const log = (msg: string) => console.log(`[basuicn] ${msg}`);
|
|
10
|
+
const warn = (msg: string) => console.warn(`[basuicn] WARN: ${msg}`);
|
|
11
|
+
const error = (msg: string) => console.error(`[basuicn] ERROR: ${msg}`);
|
|
12
|
+
|
|
13
|
+
const getTargetProjectDir = () => process.cwd();
|
|
14
|
+
|
|
15
|
+
interface Registry {
|
|
16
|
+
core?: { dependencies: string[]; files: { path: string; content: string }[] };
|
|
17
|
+
components: Record<string, { dependencies: string[]; internalDependencies?: string[]; files: { path: string; content: string }[] }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const validateRegistry = (data: unknown): data is Registry => {
|
|
21
|
+
if (!data || typeof data !== 'object') return false;
|
|
22
|
+
const reg = data as Record<string, unknown>;
|
|
23
|
+
return 'components' in reg && typeof reg.components === 'object' && reg.components !== null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const getRegistry = async (isLocal: boolean): Promise<Registry> => {
|
|
27
|
+
if (isLocal && fs.existsSync(REGISTRY_LOCAL)) {
|
|
28
|
+
log('Using local registry...');
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(fs.readFileSync(REGISTRY_LOCAL, 'utf-8'));
|
|
31
|
+
if (!validateRegistry(data)) {
|
|
32
|
+
error('Invalid local registry format — missing "components" field.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
return data;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
error(`Failed to parse local registry: ${err instanceof Error ? err.message : err}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
log('Fetching registry from remote...');
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(REGISTRY_REMOTE);
|
|
45
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
if (!validateRegistry(data)) {
|
|
48
|
+
error('Invalid remote registry format — missing "components" field.');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
return data;
|
|
52
|
+
} catch (err: unknown) {
|
|
53
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
54
|
+
error(`Cannot fetch registry: ${message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const installNpmPackages = (packages: string[], cwd: string, dev = false) => {
|
|
60
|
+
if (packages.length === 0) return;
|
|
61
|
+
|
|
62
|
+
const pkgJsonPath = path.join(cwd, 'package.json');
|
|
63
|
+
let toInstall = packages;
|
|
64
|
+
|
|
65
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
66
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
67
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
68
|
+
toInstall = packages.filter((p) => !allDeps[p]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (toInstall.length === 0) return;
|
|
72
|
+
|
|
73
|
+
log(`Installing: ${toInstall.join(', ')}...`);
|
|
74
|
+
const flag = dev ? '--save-dev' : '--save';
|
|
75
|
+
try {
|
|
76
|
+
execSync(`npm install ${toInstall.join(' ')} ${flag}`, { stdio: 'inherit', cwd });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
error(`Failed to install packages: ${toInstall.join(', ')}. ${err instanceof Error ? err.message : ''}`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const VITE_DEV_PACKAGES = ['tailwindcss', '@tailwindcss/vite', '@vitejs/plugin-react', 'vite-plugin-babel', 'babel-plugin-react-compiler', '@types/node'];
|
|
83
|
+
|
|
84
|
+
// Runtime packages always required — installed regardless of registry content
|
|
85
|
+
const RUNTIME_PACKAGES = ['@base-ui/react', 'tailwind-variants', 'clsx', 'tailwind-merge', 'tailwindcss-animate'];
|
|
86
|
+
|
|
87
|
+
const VITE_CONFIG_TEMPLATE = `import { defineConfig } from 'vite';
|
|
88
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
89
|
+
import react from '@vitejs/plugin-react';
|
|
90
|
+
import babel from 'vite-plugin-babel';
|
|
91
|
+
import { reactCompilerPreset } from 'babel-plugin-react-compiler';
|
|
92
|
+
import path from 'path';
|
|
93
|
+
|
|
94
|
+
// https://vite.dev/config/
|
|
95
|
+
export default defineConfig({
|
|
96
|
+
plugins: [
|
|
97
|
+
tailwindcss(),
|
|
98
|
+
react(),
|
|
99
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
100
|
+
],
|
|
101
|
+
resolve: {
|
|
102
|
+
alias: {
|
|
103
|
+
'@': path.resolve(__dirname, './src'),
|
|
104
|
+
'@lib': path.resolve(__dirname, './src/lib'),
|
|
105
|
+
'@components': path.resolve(__dirname, './src/components'),
|
|
106
|
+
'@assets': path.resolve(__dirname, './src/assets'),
|
|
107
|
+
'@pages': path.resolve(__dirname, './src/pages'),
|
|
108
|
+
'@styles': path.resolve(__dirname, './src/styles'),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
const TSCONFIG_PATHS = {
|
|
115
|
+
'@/*': ['./src/*'],
|
|
116
|
+
'@lib/*': ['./src/lib/*'],
|
|
117
|
+
'@components/*': ['./src/components/*'],
|
|
118
|
+
'@assets/*': ['./src/assets/*'],
|
|
119
|
+
'@pages/*': ['./src/pages/*'],
|
|
120
|
+
'@styles/*': ['./src/styles/*'],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const setupViteConfig = (cwd: string) => {
|
|
124
|
+
installNpmPackages(VITE_DEV_PACKAGES, cwd, true);
|
|
125
|
+
|
|
126
|
+
const configTs = path.join(cwd, 'vite.config.ts');
|
|
127
|
+
const configJs = path.join(cwd, 'vite.config.js');
|
|
128
|
+
|
|
129
|
+
// No config exists — create from template
|
|
130
|
+
if (!fs.existsSync(configTs) && !fs.existsSync(configJs)) {
|
|
131
|
+
fs.writeFileSync(configTs, VITE_CONFIG_TEMPLATE);
|
|
132
|
+
log('Created vite.config.ts with Tailwind + React Compiler setup.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const existingPath = fs.existsSync(configTs) ? configTs : configJs;
|
|
137
|
+
let content = fs.readFileSync(existingPath, 'utf-8');
|
|
138
|
+
|
|
139
|
+
const missingImports: string[] = [];
|
|
140
|
+
if (!content.includes('@tailwindcss/vite')) missingImports.push("import tailwindcss from '@tailwindcss/vite';");
|
|
141
|
+
if (!content.includes('@vitejs/plugin-react')) missingImports.push("import react from '@vitejs/plugin-react';");
|
|
142
|
+
if (!content.includes('vite-plugin-babel')) missingImports.push("import babel from 'vite-plugin-babel';");
|
|
143
|
+
if (!content.includes('babel-plugin-react-compiler')) missingImports.push("import { reactCompilerPreset } from 'babel-plugin-react-compiler';");
|
|
144
|
+
if (!content.includes("from 'path'") && !content.includes('from "path"')) missingImports.push("import path from 'path';");
|
|
145
|
+
|
|
146
|
+
const missingPlugins: string[] = [];
|
|
147
|
+
if (!content.includes('tailwindcss()')) missingPlugins.push('tailwindcss()');
|
|
148
|
+
if (!content.includes('react()') && !content.includes('react({')) missingPlugins.push('react()');
|
|
149
|
+
if (!content.includes('reactCompilerPreset')) missingPlugins.push("babel({ presets: [reactCompilerPreset()] })");
|
|
150
|
+
|
|
151
|
+
const hasAlias = content.includes('alias:') || content.includes('alias(') || content.includes("'@'") || content.includes('"@"');
|
|
152
|
+
|
|
153
|
+
if (missingImports.length === 0 && missingPlugins.length === 0 && hasAlias) {
|
|
154
|
+
log('vite.config already configured — skipping.');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- Auto-patch the file ---
|
|
159
|
+
|
|
160
|
+
// 1. Insert missing imports after the last import statement
|
|
161
|
+
if (missingImports.length > 0) {
|
|
162
|
+
const importBlock = missingImports.join('\n');
|
|
163
|
+
const allImports = [...content.matchAll(/^import\s.+$/gm)];
|
|
164
|
+
if (allImports.length > 0) {
|
|
165
|
+
const last = allImports[allImports.length - 1];
|
|
166
|
+
const pos = last.index! + last[0].length;
|
|
167
|
+
content = content.slice(0, pos) + '\n' + importBlock + content.slice(pos);
|
|
168
|
+
} else {
|
|
169
|
+
content = importBlock + '\n' + content;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Insert missing plugins into plugins: [...]
|
|
174
|
+
if (missingPlugins.length > 0) {
|
|
175
|
+
const match = content.match(/plugins:\s*\[/);
|
|
176
|
+
if (match && match.index !== undefined) {
|
|
177
|
+
const pos = match.index + match[0].length;
|
|
178
|
+
const after = content.slice(pos);
|
|
179
|
+
const pluginLines = missingPlugins.map((p) => `\n ${p},`).join('');
|
|
180
|
+
// If existing content continues on the same line (e.g. `plugins: [react()],`),
|
|
181
|
+
// push it to a new line so formatting stays clean
|
|
182
|
+
const needsNewline = after.length > 0 && after[0] !== '\n' && after[0] !== '\r';
|
|
183
|
+
content = content.slice(0, pos) + pluginLines + (needsNewline ? '\n ' : '') + after;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 3. Insert resolve.alias block if missing
|
|
188
|
+
if (!hasAlias) {
|
|
189
|
+
const aliasBlock = [
|
|
190
|
+
' resolve: {',
|
|
191
|
+
' alias: {',
|
|
192
|
+
" '@': path.resolve(__dirname, './src'),",
|
|
193
|
+
" '@lib': path.resolve(__dirname, './src/lib'),",
|
|
194
|
+
" '@components': path.resolve(__dirname, './src/components'),",
|
|
195
|
+
" '@assets': path.resolve(__dirname, './src/assets'),",
|
|
196
|
+
" '@pages': path.resolve(__dirname, './src/pages'),",
|
|
197
|
+
" '@styles': path.resolve(__dirname, './src/styles'),",
|
|
198
|
+
' },',
|
|
199
|
+
' },',
|
|
200
|
+
].join('\n');
|
|
201
|
+
|
|
202
|
+
// Find end of plugins array, then insert after that line
|
|
203
|
+
const pluginsStart = content.search(/plugins:\s*\[/);
|
|
204
|
+
if (pluginsStart !== -1) {
|
|
205
|
+
let depth = 0;
|
|
206
|
+
let foundStart = false;
|
|
207
|
+
for (let i = pluginsStart; i < content.length; i++) {
|
|
208
|
+
if (content[i] === '[') { depth++; foundStart = true; }
|
|
209
|
+
if (content[i] === ']') depth--;
|
|
210
|
+
if (foundStart && depth === 0) {
|
|
211
|
+
let lineEnd = content.indexOf('\n', i);
|
|
212
|
+
if (lineEnd === -1) lineEnd = content.length;
|
|
213
|
+
content = content.slice(0, lineEnd + 1) + aliasBlock + '\n' + content.slice(lineEnd + 1);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fs.writeFileSync(existingPath, content);
|
|
221
|
+
log(`Updated ${path.basename(existingPath)} with Tailwind + React Compiler + path aliases.`);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const ensureTailwindCss = (cwd: string) => {
|
|
225
|
+
const candidates = ['src/index.css', 'src/App.css', 'src/main.css'];
|
|
226
|
+
for (const cssFile of candidates) {
|
|
227
|
+
const cssPath = path.join(cwd, cssFile);
|
|
228
|
+
if (fs.existsSync(cssPath)) {
|
|
229
|
+
const content = fs.readFileSync(cssPath, 'utf-8');
|
|
230
|
+
if (!content.includes('@import "tailwindcss"') && !content.includes("@import 'tailwindcss'")) {
|
|
231
|
+
fs.writeFileSync(cssPath, `@import "tailwindcss";\n\n${content}`);
|
|
232
|
+
log(`Added @import "tailwindcss" to ${cssFile}`);
|
|
233
|
+
} else {
|
|
234
|
+
log(`${cssFile} already imports Tailwind — skipping.`);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// No CSS file found — create src/index.css
|
|
240
|
+
const srcDir = path.join(cwd, 'src');
|
|
241
|
+
if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir, { recursive: true });
|
|
242
|
+
fs.writeFileSync(path.join(srcDir, 'index.css'), '@import "tailwindcss";\n');
|
|
243
|
+
log('Created src/index.css with @import "tailwindcss"');
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const setupTsConfig = (cwd: string) => {
|
|
247
|
+
const candidates = ['tsconfig.app.json', 'tsconfig.json'];
|
|
248
|
+
|
|
249
|
+
for (const candidate of candidates) {
|
|
250
|
+
const configPath = path.join(cwd, candidate);
|
|
251
|
+
if (!fs.existsSync(configPath)) continue;
|
|
252
|
+
|
|
253
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
254
|
+
|
|
255
|
+
if (raw.includes('"@/*"') || raw.includes("'@/*'")) {
|
|
256
|
+
log(`${candidate} already has path aliases — skipping.`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// Strip comments safely: block comments first, then single-line comments
|
|
262
|
+
// only when they appear outside of string values (preceded by whitespace or line start)
|
|
263
|
+
const stripped = raw
|
|
264
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
265
|
+
.replace(/(^|[\s,{[\]])\/\/[^\n]*/g, '$1');
|
|
266
|
+
const parsed = JSON.parse(stripped) as { compilerOptions?: Record<string, unknown> };
|
|
267
|
+
if (!parsed.compilerOptions) parsed.compilerOptions = {};
|
|
268
|
+
parsed.compilerOptions.baseUrl = '.';
|
|
269
|
+
parsed.compilerOptions.paths = TSCONFIG_PATHS;
|
|
270
|
+
fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2));
|
|
271
|
+
log(`Added path aliases to ${candidate}.`);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
warn(`Could not auto-patch ${candidate}: ${err instanceof Error ? err.message : err}`);
|
|
274
|
+
warn('Add these to compilerOptions manually:');
|
|
275
|
+
console.log('\n "baseUrl": ".",');
|
|
276
|
+
console.log(' "paths": {');
|
|
277
|
+
for (const [alias, targets] of Object.entries(TSCONFIG_PATHS)) {
|
|
278
|
+
console.log(` "${alias}": ["${targets[0]}"],`);
|
|
279
|
+
}
|
|
280
|
+
console.log(' }');
|
|
281
|
+
console.log('');
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// No tsconfig found — create a minimal one
|
|
287
|
+
const newConfig = { compilerOptions: { baseUrl: '.', paths: TSCONFIG_PATHS } };
|
|
288
|
+
fs.writeFileSync(path.join(cwd, 'tsconfig.json'), JSON.stringify(newConfig, null, 2));
|
|
289
|
+
log('Created tsconfig.json with path aliases.');
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const ensureCore = (registry: { core?: { dependencies: string[]; files: { path: string; content: string }[] } }, cwd: string) => {
|
|
293
|
+
const core = registry.core;
|
|
294
|
+
if (!core) return;
|
|
295
|
+
|
|
296
|
+
installNpmPackages(core.dependencies, cwd);
|
|
297
|
+
|
|
298
|
+
for (const file of core.files) {
|
|
299
|
+
const targetPath = path.join(cwd, file.path);
|
|
300
|
+
const targetDir = path.dirname(targetPath);
|
|
301
|
+
|
|
302
|
+
if (!fs.existsSync(targetDir)) {
|
|
303
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!fs.existsSync(targetPath)) {
|
|
307
|
+
fs.writeFileSync(targetPath, file.content);
|
|
308
|
+
log(`Created core file: ${file.path}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const addComponent = (
|
|
314
|
+
name: string,
|
|
315
|
+
registry: { core?: unknown; components: Record<string, { dependencies: string[]; internalDependencies?: string[]; files: { path: string; content: string }[] }> },
|
|
316
|
+
cwd: string,
|
|
317
|
+
options: { force: boolean },
|
|
318
|
+
added: Set<string> = new Set()
|
|
319
|
+
) => {
|
|
320
|
+
if (added.has(name)) return;
|
|
321
|
+
added.add(name);
|
|
322
|
+
|
|
323
|
+
const component = registry.components[name];
|
|
324
|
+
if (!component) {
|
|
325
|
+
error(`Component "${name}" not found. Run 'list' to see available components.`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
log(`Adding: ${name}...`);
|
|
330
|
+
|
|
331
|
+
ensureCore(registry as Parameters<typeof ensureCore>[0], cwd);
|
|
332
|
+
installNpmPackages(component.dependencies, cwd);
|
|
333
|
+
|
|
334
|
+
// Resolve internal dependencies first
|
|
335
|
+
if (component.internalDependencies) {
|
|
336
|
+
for (const dep of component.internalDependencies) {
|
|
337
|
+
if (registry.components[dep]) {
|
|
338
|
+
addComponent(dep, registry, cwd, options, added);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (const file of component.files) {
|
|
344
|
+
const targetPath = path.join(cwd, file.path);
|
|
345
|
+
const targetDir = path.dirname(targetPath);
|
|
346
|
+
|
|
347
|
+
if (!fs.existsSync(targetDir)) {
|
|
348
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (fs.existsSync(targetPath) && !options.force) {
|
|
352
|
+
warn(`Skipped (exists): ${file.path} — use --force to overwrite`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fs.writeFileSync(targetPath, file.content);
|
|
357
|
+
log(`Created: ${file.path}`);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const removeComponent = (name: string, registry: { components: Record<string, { files: { path: string }[] }> }, cwd: string) => {
|
|
362
|
+
const component = registry.components[name];
|
|
363
|
+
if (!component) {
|
|
364
|
+
error(`Component "${name}" not found.`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
log(`Removing: ${name}...`);
|
|
369
|
+
|
|
370
|
+
for (const file of component.files) {
|
|
371
|
+
const targetPath = path.join(cwd, file.path);
|
|
372
|
+
if (fs.existsSync(targetPath)) {
|
|
373
|
+
fs.unlinkSync(targetPath);
|
|
374
|
+
log(`Deleted: ${file.path}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Clean up empty directories
|
|
379
|
+
for (const file of component.files) {
|
|
380
|
+
const targetDir = path.dirname(path.join(cwd, file.path));
|
|
381
|
+
try {
|
|
382
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length === 0) {
|
|
383
|
+
fs.rmdirSync(targetDir);
|
|
384
|
+
log(`Removed empty dir: ${path.relative(cwd, targetDir)}`);
|
|
385
|
+
}
|
|
386
|
+
} catch (err) {
|
|
387
|
+
warn(`Could not remove directory: ${err instanceof Error ? err.message : err}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const main = async () => {
|
|
393
|
+
const args = process.argv.slice(2);
|
|
394
|
+
const isLocal = args.includes('--local');
|
|
395
|
+
const isForce = args.includes('--force');
|
|
396
|
+
const filteredArgs = args.filter((a) => !a.startsWith('--'));
|
|
397
|
+
const command = filteredArgs[0];
|
|
398
|
+
const componentNames = filteredArgs.slice(1);
|
|
399
|
+
|
|
400
|
+
const cwd = getTargetProjectDir();
|
|
401
|
+
const registry = await getRegistry(isLocal);
|
|
402
|
+
|
|
403
|
+
switch (command) {
|
|
404
|
+
case 'add': {
|
|
405
|
+
if (componentNames.length === 0) {
|
|
406
|
+
error('Usage: npx basuicn add <component-name> [--force]');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Auto-init if not yet initialized
|
|
411
|
+
const cnPath = path.join(cwd, 'src/lib/utils/cn.ts');
|
|
412
|
+
if (!fs.existsSync(cnPath)) {
|
|
413
|
+
log('Project not initialized — running init first...');
|
|
414
|
+
setupViteConfig(cwd);
|
|
415
|
+
setupTsConfig(cwd);
|
|
416
|
+
ensureTailwindCss(cwd);
|
|
417
|
+
installNpmPackages(RUNTIME_PACKAGES, cwd);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
for (const name of componentNames) {
|
|
421
|
+
addComponent(name, registry, cwd, { force: isForce });
|
|
422
|
+
}
|
|
423
|
+
log('Done!');
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
case 'remove': {
|
|
428
|
+
if (componentNames.length === 0) {
|
|
429
|
+
error('Usage: npx basuicn remove <component-name>');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
for (const name of componentNames) {
|
|
433
|
+
removeComponent(name, registry, cwd);
|
|
434
|
+
}
|
|
435
|
+
log('Done!');
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
case 'list': {
|
|
440
|
+
const components = Object.keys(registry.components).sort();
|
|
441
|
+
log(`Available components (${components.length}):`);
|
|
442
|
+
for (const k of components) {
|
|
443
|
+
const deps = registry.components[k].internalDependencies;
|
|
444
|
+
const depStr = deps?.length ? ` (requires: ${deps.join(', ')})` : '';
|
|
445
|
+
console.log(` - ${k}${depStr}`);
|
|
446
|
+
}
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
case 'init': {
|
|
451
|
+
setupViteConfig(cwd);
|
|
452
|
+
setupTsConfig(cwd);
|
|
453
|
+
ensureTailwindCss(cwd);
|
|
454
|
+
installNpmPackages(RUNTIME_PACKAGES, cwd);
|
|
455
|
+
ensureCore(registry, cwd);
|
|
456
|
+
log('Initialization complete.');
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
case 'tailwind': {
|
|
461
|
+
console.log('\n--- Copy to tailwind.config.ts / tailwind.config.js ---\n');
|
|
462
|
+
console.log('// See README_CLI.md for full theme config');
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
default: {
|
|
467
|
+
console.log(`
|
|
468
|
+
basuicn — UI Component CLI
|
|
469
|
+
|
|
470
|
+
Commands:
|
|
471
|
+
init Initialize project (install core deps + files)
|
|
472
|
+
add <name> [--force] Add component(s) to your project
|
|
473
|
+
remove <name> Remove component(s) from your project
|
|
474
|
+
list List all available components
|
|
475
|
+
tailwind Show Tailwind config instructions
|
|
476
|
+
|
|
477
|
+
Options:
|
|
478
|
+
--local Use local registry.json instead of remote
|
|
479
|
+
--force Overwrite existing files when adding
|
|
480
|
+
`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
main();
|