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.
Files changed (3) hide show
  1. package/bin/uix-ui.mjs +200 -0
  2. package/package.json +11 -4
  3. 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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.