uix-ui 0.0.0-stage → 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/bin/uix-ui.mjs +200 -0
- package/package.json +11 -4
- package/README.md +0 -3
package/bin/uix-ui.mjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// uix-ui: add UIX components and themes to a React + Tailwind v4 app. No dependencies.
|
|
3
|
+
// Reads the same registry as the shadcn CLI (components.json → registries), so `npx shadcn add @uix/button` works too.
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_REGISTRY = 'https://uixdesign.pagestro.com/ui/r';
|
|
9
|
+
const HELP = `uix-ui: UIX components and themes for React + Tailwind v4
|
|
10
|
+
|
|
11
|
+
npx uix-ui init [--preset <id>] [--registry <url|folder>] set up the theme, utils and components.json
|
|
12
|
+
npx uix-ui add <component...> copy components into your app
|
|
13
|
+
npx uix-ui theme <preset> switch to another UIX preset (uix = default)
|
|
14
|
+
npx uix-ui list components and themes in the registry
|
|
15
|
+
|
|
16
|
+
--overwrite replace files that already exist (add)
|
|
17
|
+
--no-install don't run the package manager, just print the command
|
|
18
|
+
Pro items: set UIX_LICENSE=<your key> and add @uix-pro/<name>.`;
|
|
19
|
+
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
const flag = (n, bool = false) => {
|
|
23
|
+
const i = argv.indexOf(`--${n}`);
|
|
24
|
+
if (i < 0) return undefined;
|
|
25
|
+
const v = argv[i + 1], takes = !bool && v && !v.startsWith('--');
|
|
26
|
+
argv.splice(i, takes ? 2 : 1);
|
|
27
|
+
return takes ? v : true;
|
|
28
|
+
};
|
|
29
|
+
const opts = { preset: flag('preset'), registry: flag('registry'), overwrite: flag('overwrite', true), noInstall: flag('no-install', true) };
|
|
30
|
+
const [cmd, ...names] = argv;
|
|
31
|
+
|
|
32
|
+
const say = (s) => console.log(s);
|
|
33
|
+
const die = (s) => { console.error(`uix-ui: ${s}`); process.exit(1); };
|
|
34
|
+
const readJson = (f) => JSON.parse(readFileSync(join(cwd, f), 'utf8'));
|
|
35
|
+
const write = (file, content) => { mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, content); };
|
|
36
|
+
const rel = (from, to) => { const r = relative(dirname(from), to).split(sep).join('/'); return r.startsWith('.') ? r : `./${r}`; };
|
|
37
|
+
|
|
38
|
+
// `@` is the src folder when there is one (Vite, Next with src/), else the project root, as in shadcn's defaults.
|
|
39
|
+
const srcRoot = existsSync(join(cwd, 'src')) ? join(cwd, 'src') : cwd;
|
|
40
|
+
const aliasPath = (a) => join(srcRoot, a.replace(/^@\//, ''));
|
|
41
|
+
|
|
42
|
+
function config() {
|
|
43
|
+
if (!existsSync(join(cwd, 'components.json'))) die('no components.json here. Run `npx uix-ui init` first.');
|
|
44
|
+
return readJson('components.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `@uix/button` → its registry entry (string or { url, headers } with ${ENV} values, the shadcn format). */
|
|
48
|
+
function source(cfg, name) {
|
|
49
|
+
const [ns, item] = name.startsWith('@') ? [name.slice(0, name.indexOf('/')), name.slice(name.indexOf('/') + 1)] : ['@uix', name];
|
|
50
|
+
const reg = cfg.registries?.[ns];
|
|
51
|
+
if (!reg) die(`no registry "${ns}" in components.json.`);
|
|
52
|
+
const { url, headers = {} } = typeof reg === 'string' ? { url: reg } : reg;
|
|
53
|
+
const env = (s) => s.replace(/\$\{(\w+)\}/g, (_, k) => process.env[k] ?? '');
|
|
54
|
+
return { ns, item, url: url.replace('{name}', item), headers: Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, env(v)])) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function load(src) {
|
|
58
|
+
if (!/^https?:/.test(src.url)) {
|
|
59
|
+
if (!existsSync(src.url)) die(`"${src.item}" is not in the registry (${src.url}).`);
|
|
60
|
+
return JSON.parse(readFileSync(src.url, 'utf8'));
|
|
61
|
+
}
|
|
62
|
+
const res = await fetch(src.url, { headers: src.headers });
|
|
63
|
+
if (res.status === 401 || res.status === 403) die(`"${src.item}" is a UIX Pro item. Set UIX_LICENSE to your key (https://uixdesign.pagestro.com/pricing).`);
|
|
64
|
+
if (!res.ok) die(`"${src.item}" could not be loaded (${res.status} from ${src.url}).`);
|
|
65
|
+
return res.json();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Loads the items and everything they depend on, dependencies first. */
|
|
69
|
+
async function resolveAll(cfg, list) {
|
|
70
|
+
const seen = new Map();
|
|
71
|
+
const visit = async (name) => {
|
|
72
|
+
const src = source(cfg, name);
|
|
73
|
+
const key = `${src.ns}/${src.item}`;
|
|
74
|
+
if (seen.has(key)) return;
|
|
75
|
+
seen.set(key, null);
|
|
76
|
+
const it = await load(src);
|
|
77
|
+
for (const d of it.registryDependencies ?? []) await visit(d.startsWith('@') ? d : `${src.ns}/${d}`);
|
|
78
|
+
seen.delete(key); seen.set(key, it);
|
|
79
|
+
};
|
|
80
|
+
for (const n of list) await visit(n);
|
|
81
|
+
return [...seen.values()];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function target(cfg, f) {
|
|
85
|
+
const base = f.path.split('/').pop();
|
|
86
|
+
if (f.type === 'registry:ui') return join(aliasPath(cfg.aliases.ui), base);
|
|
87
|
+
if (f.type === 'registry:lib') return join(aliasPath(cfg.aliases.lib), base);
|
|
88
|
+
if (f.type === 'registry:hook') return join(aliasPath(cfg.aliases.hooks ?? '@/hooks'), base);
|
|
89
|
+
return join(srcRoot, f.path); // registry:file, e.g. styles/uix.css
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Writes the files; `@/…` imports become relative paths, so no path alias is needed. */
|
|
93
|
+
function place(cfg, items, overwrite) {
|
|
94
|
+
for (const it of items) for (const f of it.files) {
|
|
95
|
+
const file = target(cfg, f);
|
|
96
|
+
const code = f.content.replace(/(["'])@\/([^"']+)\1/g, (_, q, p) => {
|
|
97
|
+
const to = p.startsWith('lib/') ? join(aliasPath(cfg.aliases.lib), p.slice(4)) : p.startsWith('components/ui/') ? join(aliasPath(cfg.aliases.ui), p.slice(14)) : join(srcRoot, p);
|
|
98
|
+
return q + rel(file, to) + q;
|
|
99
|
+
});
|
|
100
|
+
const shown = relative(cwd, file);
|
|
101
|
+
if (existsSync(file) && !overwrite) { if (readFileSync(file, 'utf8') !== code) say(` skipped ${shown} (exists, use --overwrite)`); continue; }
|
|
102
|
+
write(file, code);
|
|
103
|
+
say(` wrote ${shown}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function install(items) {
|
|
108
|
+
const deps = [...new Set(items.flatMap(i => i.dependencies ?? []))];
|
|
109
|
+
const pkg = existsSync(join(cwd, 'package.json')) ? readJson('package.json') : {};
|
|
110
|
+
const have = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
111
|
+
const need = deps.filter(d => !have[d]);
|
|
112
|
+
if (!need.length) return;
|
|
113
|
+
const pm = existsSync(join(cwd, 'pnpm-lock.yaml')) ? 'pnpm' : existsSync(join(cwd, 'yarn.lock')) ? 'yarn' : existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock')) ? 'bun' : 'npm';
|
|
114
|
+
const args = [pm === 'npm' ? 'install' : 'add', ...need];
|
|
115
|
+
if (opts.noInstall) return say(`\nInstall: ${pm} ${args.join(' ')}`);
|
|
116
|
+
say(`\n${pm} ${args.join(' ')}`);
|
|
117
|
+
if (spawnSync(pm, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' }).status) die('install failed.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Google Fonts links for the theme's fonts, between markers in index.html (Vite). Other setups get the links printed. */
|
|
121
|
+
function fonts(list = []) {
|
|
122
|
+
const links = list.map(f => ` <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=${encodeURIComponent(f).replace(/%20/g, '+')}:wght@400;500;600;700&display=swap" />`);
|
|
123
|
+
const html = join(cwd, 'index.html');
|
|
124
|
+
if (!existsSync(html)) return list.length && say(`\nLoad the theme's fonts (${list.join(', ')}), e.g.:\n${links.join('\n')}`);
|
|
125
|
+
const block = `<!-- uix-fonts -->\n <link rel="preconnect" href="https://fonts.googleapis.com" />\n <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />\n${links.join('\n')}\n <!-- /uix-fonts -->`;
|
|
126
|
+
let src = readFileSync(html, 'utf8');
|
|
127
|
+
src = /<!-- uix-fonts -->[\s\S]*?<!-- \/uix-fonts -->/.test(src) ? src.replace(/<!-- uix-fonts -->[\s\S]*?<!-- \/uix-fonts -->/, block) : src.replace(/\s*<\/head>/, `\n ${block}\n </head>`);
|
|
128
|
+
writeFileSync(html, src);
|
|
129
|
+
say(` fonts ${list.join(', ')} → index.html`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const themeName = (p) => (!p || p === true || p === 'uix' ? 'theme' : `theme-${p}`);
|
|
133
|
+
|
|
134
|
+
async function applyTheme(cfg, preset) {
|
|
135
|
+
const [it] = await resolveAll(cfg, [themeName(preset)]);
|
|
136
|
+
place(cfg, [it], true);
|
|
137
|
+
fonts(it.meta?.fonts);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function init() {
|
|
141
|
+
if (!existsSync(join(cwd, 'package.json'))) die('run this in your app folder (no package.json here).');
|
|
142
|
+
const pkg = readJson('package.json');
|
|
143
|
+
const tw = { ...pkg.dependencies, ...pkg.devDependencies }.tailwindcss;
|
|
144
|
+
if (!tw) say('Note: Tailwind CSS v4 is not installed yet (https://tailwindcss.com/docs/installation).');
|
|
145
|
+
else if (/^[\^~]?3/.test(tw)) die('UIX themes need Tailwind CSS v4 (found v3).');
|
|
146
|
+
|
|
147
|
+
const css = ['src/index.css', 'src/app/globals.css', 'app/globals.css', 'src/styles/globals.css', 'src/main.css'].find(f => existsSync(join(cwd, f))) ?? 'src/index.css';
|
|
148
|
+
const local = opts.registry && !/^https?:/.test(opts.registry);
|
|
149
|
+
const registry = opts.registry ? (local ? resolve(cwd, opts.registry) : opts.registry).replace(/\/$/, '') : DEFAULT_REGISTRY;
|
|
150
|
+
const cfg = existsSync(join(cwd, 'components.json')) ? readJson('components.json') : {
|
|
151
|
+
$schema: 'https://ui.shadcn.com/schema.json', style: 'new-york', rsc: existsSync(join(cwd, 'next.config.js')) || existsSync(join(cwd, 'next.config.mjs')) || existsSync(join(cwd, 'next.config.ts')), tsx: true,
|
|
152
|
+
tailwind: { config: '', css, baseColor: 'neutral', cssVariables: true, prefix: '' },
|
|
153
|
+
aliases: { components: '@/components', utils: '@/lib/utils', ui: '@/components/ui', lib: '@/lib', hooks: '@/hooks' },
|
|
154
|
+
iconLibrary: 'lucide',
|
|
155
|
+
};
|
|
156
|
+
cfg.registries = {
|
|
157
|
+
...cfg.registries,
|
|
158
|
+
'@uix': `${registry}/{name}.json`,
|
|
159
|
+
'@uix-pro': { url: `${registry.replace(/\/r$/, '/r/pro')}/{name}.json`, headers: { Authorization: 'Bearer ${UIX_LICENSE}' } },
|
|
160
|
+
};
|
|
161
|
+
write(join(cwd, 'components.json'), JSON.stringify(cfg, null, 2) + '\n');
|
|
162
|
+
say(' wrote components.json');
|
|
163
|
+
|
|
164
|
+
await applyTheme(cfg, opts.preset);
|
|
165
|
+
const cssFile = join(cwd, cfg.tailwind.css);
|
|
166
|
+
let src = existsSync(cssFile) ? readFileSync(cssFile, 'utf8') : '@import "tailwindcss";\n';
|
|
167
|
+
const imp = `@import "${rel(cssFile, join(srcRoot, 'styles/uix.css'))}";`;
|
|
168
|
+
if (!src.includes(imp)) {
|
|
169
|
+
src = /@import ["']tailwindcss["'];?/.test(src) ? src.replace(/(@import ["']tailwindcss["'];?)/, `$1\n${imp}`) : `@import "tailwindcss";\n${imp}\n${src}`;
|
|
170
|
+
write(cssFile, src);
|
|
171
|
+
say(` updated ${cfg.tailwind.css}`);
|
|
172
|
+
}
|
|
173
|
+
const items = await resolveAll(cfg, ['utils']);
|
|
174
|
+
place(cfg, items, false);
|
|
175
|
+
install(items);
|
|
176
|
+
say(`\nDone. Next: npx uix-ui add button card input\nDark mode: <html data-theme="dark">. Other presets: npx uix-ui theme <preset>.`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function main() {
|
|
180
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') return say(HELP);
|
|
181
|
+
if (cmd === 'init') return init();
|
|
182
|
+
const cfg = config();
|
|
183
|
+
if (cmd === 'add') {
|
|
184
|
+
if (!names.length) die('name at least one component, e.g. `npx uix-ui add button`.');
|
|
185
|
+
const items = await resolveAll(cfg, names);
|
|
186
|
+
place(cfg, items, !!opts.overwrite);
|
|
187
|
+
return install(items);
|
|
188
|
+
}
|
|
189
|
+
if (cmd === 'theme') return applyTheme(cfg, names[0] ?? opts.preset);
|
|
190
|
+
if (cmd === 'list') {
|
|
191
|
+
const src = source(cfg, 'x');
|
|
192
|
+
const index = await load({ ...src, item: 'registry.json', url: src.url.replace(/\/r\/x\.json$/, '/registry.json') });
|
|
193
|
+
const comps = index.items.filter(i => i.type === 'registry:ui' && i.name !== 'icons').map(i => i.name);
|
|
194
|
+
const themes = index.items.filter(i => i.name.startsWith('theme')).map(i => i.name === 'theme' ? 'uix' : i.name.slice(6));
|
|
195
|
+
return say(`Components (${comps.length}): ${comps.join(', ')}\n\nThemes (${themes.length}): ${themes.join(', ')}`);
|
|
196
|
+
}
|
|
197
|
+
die(`unknown command "${cmd}".\n\n${HELP}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
main().catch(e => die(e.message));
|
package/package.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uix-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Add UIX components and themes to your React + Tailwind app: npx uix-ui init.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "uix-ui": "bin/uix-ui.mjs" },
|
|
8
|
+
"files": ["bin"],
|
|
9
|
+
"engines": { "node": ">=18" },
|
|
10
|
+
"homepage": "https://uixdesign.pagestro.com/ui",
|
|
11
|
+
"repository": { "type": "git", "url": "git+https://github.com/mdrakibshekh/uix-ui.git", "directory": "packages/cli" },
|
|
12
|
+
"keywords": ["shadcn", "tailwindcss", "react", "components", "design-system", "cli"]
|
|
13
|
+
}
|
package/README.md
DELETED