get-ui-cli 0.0.0-stage → 2.0.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.
@@ -0,0 +1,390 @@
1
+ // Resolve a registry item (and, recursively, its registryDependencies) into a flat
2
+ // list of files + npm dependencies + CSS. Pure — only uses `fetch`.
3
+ import { LIBRARIES } from '../registry-types.js';
4
+ export const SHADCN_BASE = 'https://ui.shadcn.com/r/styles/new-york-v4/';
5
+ export const SHADCN_REGISTRIES_URL = 'https://ui.shadcn.com/r/registries.json';
6
+ /**
7
+ * shadcn style used when a bare dependency is not served by the item's own library.
8
+ * ReUI is built on Base UI, so its blocks expect the Base UI flavour of shadcn components.
9
+ */
10
+ const SHADCN_FALLBACK_STYLE = {
11
+ reui: 'https://ui.shadcn.com/r/styles/base-nova/',
12
+ };
13
+ export class FetchError extends Error {
14
+ status;
15
+ url;
16
+ constructor(message, status, url) {
17
+ super(message);
18
+ this.status = status;
19
+ this.url = url;
20
+ }
21
+ }
22
+ const SKIP_DEPS = new Set(['utils', 'cn']);
23
+ /** `motion@^11` -> `motion`, `@scope/pkg@1` -> `@scope/pkg` */
24
+ export function packageName(dep) {
25
+ if (dep.startsWith('@')) {
26
+ const i = dep.indexOf('@', 1);
27
+ return i === -1 ? dep : dep.slice(0, i);
28
+ }
29
+ const i = dep.indexOf('@');
30
+ return i === -1 ? dep : dep.slice(0, i);
31
+ }
32
+ function contextFor(lib) {
33
+ if (!lib || !lib.itemBase)
34
+ return { libraryId: lib?.id ?? 'external', itemBase: SHADCN_BASE, fallbackBase: SHADCN_BASE };
35
+ return { libraryId: lib.id, itemBase: lib.itemBase, fallbackBase: SHADCN_FALLBACK_STYLE[lib.id] ?? SHADCN_BASE };
36
+ }
37
+ function libraryForUrl(registry, url) {
38
+ return Object.values(registry.libraries).find((l) => l.itemBase && url.startsWith(l.itemBase));
39
+ }
40
+ function labelForUrl(registry, url) {
41
+ const lib = libraryForUrl(registry, url);
42
+ const name = url.split('/').pop().replace(/\.json(\?.*)?$/, '');
43
+ if (lib)
44
+ return `${lib.id}/${name}`;
45
+ if (url.startsWith('https://ui.shadcn.com/r/styles/'))
46
+ return `shadcn/${name}`;
47
+ return url;
48
+ }
49
+ function isItemJson(x) {
50
+ return !!x && typeof x === 'object' && !Array.isArray(x) && (Array.isArray(x.files) || 'type' in x);
51
+ }
52
+ /** Run async tasks with a concurrency limit. */
53
+ async function pool(items, limit, fn) {
54
+ const out = new Array(items.length);
55
+ let next = 0;
56
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
57
+ while (next < items.length) {
58
+ const i = next++;
59
+ out[i] = await fn(items[i]);
60
+ }
61
+ });
62
+ await Promise.all(workers);
63
+ return out;
64
+ }
65
+ export function createFetcher(opts = {}) {
66
+ const f = opts.fetch ?? globalThis.fetch;
67
+ const cache = opts.cache ?? new Map();
68
+ const timeoutMs = opts.timeoutMs ?? 20_000;
69
+ async function raw(url) {
70
+ for (let attempt = 0;; attempt++) {
71
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
72
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
73
+ let res;
74
+ try {
75
+ res = await f(url, { headers: { accept: 'application/json, text/plain, */*', ...opts.headers?.(url) }, signal: controller?.signal });
76
+ }
77
+ catch (e) {
78
+ if (attempt < 2)
79
+ continue;
80
+ throw new FetchError(`Network error fetching ${url}: ${e.message}`, 0, url);
81
+ }
82
+ finally {
83
+ if (timer)
84
+ clearTimeout(timer);
85
+ }
86
+ // Back off on rate limits / transient server errors.
87
+ if ((res.status === 429 || res.status >= 502) && attempt < 3) {
88
+ const retryAfter = Number(res.headers.get('retry-after'));
89
+ const wait = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 10) * 1000 : 800 * 2 ** attempt;
90
+ await new Promise((r) => setTimeout(r, wait));
91
+ continue;
92
+ }
93
+ return res;
94
+ }
95
+ }
96
+ function cached(key, load) {
97
+ let p = cache.get(key);
98
+ if (!p) {
99
+ p = load();
100
+ cache.set(key, p);
101
+ p.catch(() => cache.delete(key));
102
+ }
103
+ return p;
104
+ }
105
+ return {
106
+ json(url) {
107
+ return cached(`json:${url}`, async () => {
108
+ const res = await raw(url);
109
+ if (!res.ok) {
110
+ let detail = '';
111
+ try {
112
+ const body = (await res.json());
113
+ const pickStr = (v) => (typeof v === 'string' ? v : v && typeof v === 'object' && typeof v.message === 'string' ? v.message : '');
114
+ detail = pickStr(body.message) || pickStr(body.error);
115
+ }
116
+ catch {
117
+ /* ignore */
118
+ }
119
+ throw new FetchError(`${res.status} ${res.statusText || ''} for ${url}${detail ? ` — ${detail}` : ''}`.trim(), res.status, url);
120
+ }
121
+ const text = await res.text();
122
+ try {
123
+ return JSON.parse(text);
124
+ }
125
+ catch {
126
+ throw new FetchError(`Not a JSON registry item: ${url}`, 404, url);
127
+ }
128
+ });
129
+ },
130
+ text(url) {
131
+ return cached(`text:${url}`, async () => {
132
+ const res = await raw(url);
133
+ if (!res.ok)
134
+ throw new FetchError(`${res.status} ${res.statusText || ''} for ${url}`.trim(), res.status, url);
135
+ return res.text();
136
+ });
137
+ },
138
+ };
139
+ }
140
+ const LOCAL_IMPORT_RE = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)['"](\.{1,2}\/[^'"]+)['"]/g;
141
+ const LOCAL_EXTS = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx'];
142
+ const noExt = (p) => p.replace(/^\/+/, '').replace(/\.(tsx?|jsx?)$/, '');
143
+ /** Resolve a relative import against a file's path, POSIX-style (core stays free of node:path). */
144
+ function joinRelative(file, spec) {
145
+ const out = file.replace(/^\/+/, '').split('/').slice(0, -1);
146
+ for (const seg of spec.split('/')) {
147
+ if (seg === '..')
148
+ out.pop();
149
+ else if (seg !== '.' && seg)
150
+ out.push(seg);
151
+ }
152
+ return out.join('/');
153
+ }
154
+ /**
155
+ * Some items import local files (a hook, a helper) that their registry entry
156
+ * forgot to ship. For libraries with a public source repo, fetch those files
157
+ * from the repo and place them where the relative import expects them.
158
+ */
159
+ async function fetchUnshippedLocals(fetcher, files, warnings) {
160
+ const have = new Set(files.flatMap((f) => [noExt(f.path), ...(f.target ? [noExt(f.target)] : [])]));
161
+ const tried = new Set();
162
+ for (let i = 0; i < files.length; i++) {
163
+ const f = files[i];
164
+ const sourceBase = f.library === 'external' ? undefined : LIBRARIES[f.library]?.sourceBase;
165
+ if (!sourceBase)
166
+ continue;
167
+ for (const m of f.content.matchAll(LOCAL_IMPORT_RE)) {
168
+ const spec = m[1];
169
+ const repoPath = noExt(joinRelative(f.path, spec));
170
+ const targetPath = f.target ? noExt(joinRelative(f.target, spec)) : undefined;
171
+ if (have.has(repoPath) || (targetPath && have.has(targetPath)) || tried.has(repoPath))
172
+ continue;
173
+ tried.add(repoPath);
174
+ let found;
175
+ for (const ext of LOCAL_EXTS) {
176
+ try {
177
+ found = { ext, content: await fetcher.text(`${sourceBase}${repoPath}${ext}`) };
178
+ break;
179
+ }
180
+ catch {
181
+ /* try the next extension */
182
+ }
183
+ }
184
+ if (!found)
185
+ continue; // planInstall warns about imports nothing provides.
186
+ have.add(repoPath);
187
+ if (targetPath)
188
+ have.add(targetPath);
189
+ // Appended, so this loop also visits the new file's own imports.
190
+ files.push({
191
+ path: `${repoPath}${found.ext}`,
192
+ content: found.content,
193
+ type: 'registry:file',
194
+ target: targetPath ? `${targetPath}${found.ext}` : undefined,
195
+ item: f.item,
196
+ library: f.library,
197
+ });
198
+ warnings.push(`${f.item}: fetched ${spec} from the library's repo (its registry entry doesn't include it).`);
199
+ }
200
+ }
201
+ }
202
+ let registriesDirectory;
203
+ async function lookupNamespace(fetcher, ns) {
204
+ registriesDirectory ??= fetcher
205
+ .json(SHADCN_REGISTRIES_URL)
206
+ .then((r) => (Array.isArray(r) ? r : []))
207
+ .catch(() => []);
208
+ const list = await registriesDirectory;
209
+ return list.find((r) => r.name === ns)?.url;
210
+ }
211
+ /**
212
+ * Fetch an item and all of its registryDependencies (recursively, deduped,
213
+ * cycle-safe, concurrently).
214
+ */
215
+ export async function resolveItem(registry, item, opts = {}) {
216
+ const fetcher = createFetcher(opts);
217
+ const warnings = [];
218
+ const tree = [];
219
+ const deps = new Set();
220
+ const devDeps = new Set();
221
+ const cssVars = { theme: {}, light: {}, dark: {} };
222
+ const css = {};
223
+ const files = [];
224
+ const seenFiles = new Set();
225
+ const visited = new Set();
226
+ const concurrency = opts.concurrency ?? 8;
227
+ const rootCtx = contextFor(registry.libraries[item.library]);
228
+ async function fetchNode(node, root) {
229
+ try {
230
+ const data = await fetcher.json(node.url);
231
+ if (!isItemJson(data))
232
+ throw new FetchError(`Not a registry item: ${node.url}`, 404, node.url);
233
+ return { data, node };
234
+ }
235
+ catch (e) {
236
+ if (root)
237
+ throw e;
238
+ warnings.push(`Could not fetch ${node.label}: ${e.message}`);
239
+ return null;
240
+ }
241
+ }
242
+ /** Resolve a registryDependency string to a node; may probe the library before falling back to shadcn. */
243
+ async function resolveDep(dep, ctx) {
244
+ dep = dep.trim();
245
+ if (!dep || SKIP_DEPS.has(dep))
246
+ return null;
247
+ if (/^https?:\/\//.test(dep)) {
248
+ const l = libraryForUrl(registry, dep);
249
+ const c = l ? contextFor(l) : { libraryId: 'external', itemBase: dep.slice(0, dep.lastIndexOf('/') + 1), fallbackBase: SHADCN_BASE };
250
+ return { url: dep, label: labelForUrl(registry, dep), ctx: c };
251
+ }
252
+ const nsMatch = dep.match(/^(@[^/]+)\/(.+)$/);
253
+ if (nsMatch) {
254
+ const [, ns, name] = nsMatch;
255
+ if (ns === '@shadcn')
256
+ return { url: `${SHADCN_BASE}${name}.json`, label: `shadcn/${name}`, ctx: contextFor(registry.libraries.shadcn) };
257
+ const l = Object.values(registry.libraries).find((x) => x.namespace === ns);
258
+ if (l?.itemBase)
259
+ return { url: `${l.itemBase}${name}.json`, label: `${l.id}/${name}`, ctx: contextFor(l) };
260
+ const template = await lookupNamespace(fetcher, ns);
261
+ if (!template) {
262
+ warnings.push(`Unknown registry namespace ${ns} (needed for ${dep}).`);
263
+ return null;
264
+ }
265
+ const url = template.replace('{name}', name).replace('{style}', 'new-york-v4');
266
+ return { url, label: dep, ctx: { libraryId: 'external', itemBase: url.slice(0, url.lastIndexOf('/') + 1), fallbackBase: SHADCN_BASE } };
267
+ }
268
+ // Bare name: try the library first, then shadcn.
269
+ const own = `${ctx.itemBase}${dep}.json`;
270
+ if (ctx.itemBase !== ctx.fallbackBase) {
271
+ try {
272
+ const data = await fetcher.json(own);
273
+ if (isItemJson(data))
274
+ return { url: own, label: labelForUrl(registry, own), ctx };
275
+ }
276
+ catch (e) {
277
+ if (e instanceof FetchError && (e.status === 401 || e.status === 403)) {
278
+ warnings.push(`${labelForUrl(registry, own)} requires authentication: ${e.message}`);
279
+ return null;
280
+ }
281
+ }
282
+ }
283
+ const fb = `${ctx.fallbackBase}${dep}.json`;
284
+ const shadcnCtx = { libraryId: 'shadcn', itemBase: ctx.fallbackBase, fallbackBase: ctx.fallbackBase };
285
+ return { url: fb, label: `shadcn/${dep}`, ctx: shadcnCtx };
286
+ }
287
+ function absorb(data, node) {
288
+ tree.push(node.label);
289
+ for (const d of data.dependencies ?? [])
290
+ if (!SKIP_DEPS.has(packageName(d)))
291
+ deps.add(d);
292
+ for (const d of data.devDependencies ?? [])
293
+ if (!SKIP_DEPS.has(packageName(d)))
294
+ devDeps.add(d);
295
+ for (const key of ['theme', 'light', 'dark']) {
296
+ for (const [k, v] of Object.entries(data.cssVars?.[key] ?? {}))
297
+ if (!(k in cssVars[key]))
298
+ cssVars[key][k] = v;
299
+ }
300
+ for (const [k, v] of Object.entries(data.css ?? {}))
301
+ if (!(k in css))
302
+ css[k] = v;
303
+ for (const f of data.files ?? []) {
304
+ const key = (f.target ?? f.path).replace(/^\/+/, '');
305
+ if (seenFiles.has(key))
306
+ continue;
307
+ seenFiles.add(key);
308
+ if (typeof f.content !== 'string') {
309
+ warnings.push(`${node.label}: file ${f.path} has no content — skipped.`);
310
+ continue;
311
+ }
312
+ files.push({
313
+ path: f.path,
314
+ content: f.content,
315
+ type: f.type ?? data.type ?? 'registry:component',
316
+ target: f.target || undefined,
317
+ item: node.label,
318
+ library: node.ctx.libraryId,
319
+ });
320
+ }
321
+ }
322
+ // BFS by levels, fetching each level concurrently.
323
+ let level = [
324
+ { node: { url: item.source, label: item.id, ctx: rootCtx }, root: true },
325
+ ];
326
+ visited.add(item.source);
327
+ // Imports of sibling components that an item uses but forgot to declare.
328
+ const implicit = [];
329
+ const triedImplicit = new Set();
330
+ const provided = () => new Set(files.map((f) => f.path.split('/').pop().replace(/\.(tsx?|jsx?)$/, '')));
331
+ const IMPLICIT_RE = /from\s+["']@\/(?:components\/ui\/([\w-]+)|registry\/(?:[^"'/]+\/)*?ui\/([\w-]+)|registry\/[^"'/]+\/([\w-]+))["']/g;
332
+ for (;;) {
333
+ while (level.length) {
334
+ const fetched = await pool(level, concurrency, ({ node, root }) => fetchNode(node, root));
335
+ const pending = [];
336
+ for (const r of fetched) {
337
+ if (!r)
338
+ continue;
339
+ absorb(r.data, r.node);
340
+ for (const dep of r.data.registryDependencies ?? [])
341
+ pending.push({ dep, ctx: r.node.ctx });
342
+ for (const f of r.data.files ?? [])
343
+ for (const m of (f.content ?? '').matchAll(IMPLICIT_RE))
344
+ implicit.push({ dep: m[1] ?? m[2] ?? m[3], ctx: r.node.ctx });
345
+ }
346
+ // Dedupe by dep string + context before probing.
347
+ const uniq = new Map();
348
+ for (const p of pending)
349
+ uniq.set(`${p.ctx.itemBase}|${p.dep}`, p);
350
+ const nodes = await pool([...uniq.values()], concurrency, ({ dep, ctx }) => resolveDep(dep, ctx));
351
+ level = [];
352
+ for (const n of nodes) {
353
+ if (!n || visited.has(n.url))
354
+ continue;
355
+ visited.add(n.url);
356
+ level.push({ node: n, root: false });
357
+ }
358
+ }
359
+ // Second pass: undeclared component imports (e.g. a dialog importing ui/button).
360
+ const have = provided();
361
+ const missing = implicit.filter(({ dep, ctx }) => {
362
+ const key = `${ctx.itemBase}|${dep}`;
363
+ if (have.has(dep) || triedImplicit.has(key))
364
+ return false;
365
+ triedImplicit.add(key);
366
+ return true;
367
+ });
368
+ if (!missing.length)
369
+ break;
370
+ const nodes = await pool(missing, concurrency, ({ dep, ctx }) => resolveDep(dep, ctx));
371
+ for (const n of nodes) {
372
+ if (!n || visited.has(n.url))
373
+ continue;
374
+ visited.add(n.url);
375
+ level.push({ node: n, root: false });
376
+ }
377
+ if (!level.length)
378
+ break;
379
+ }
380
+ await fetchUnshippedLocals(fetcher, files, warnings);
381
+ return {
382
+ files,
383
+ dependencies: [...deps].sort(),
384
+ devDependencies: [...devDeps].filter((d) => !deps.has(d)).sort(),
385
+ cssVars: Object.fromEntries(Object.entries(cssVars).filter(([, v]) => Object.keys(v).length)),
386
+ css,
387
+ warnings,
388
+ tree,
389
+ };
390
+ }
@@ -0,0 +1,17 @@
1
+ import type { ItemKind, LibraryId, RegistryItem } from '../registry-types.js';
2
+ export interface SearchOptions {
3
+ library?: LibraryId | string;
4
+ category?: string;
5
+ kind?: ItemKind | string;
6
+ limit?: number;
7
+ }
8
+ export interface SearchResult {
9
+ item: RegistryItem;
10
+ score: number;
11
+ }
12
+ export declare function scoreItem(item: RegistryItem, query: string): number;
13
+ export declare function searchItemsScored(items: RegistryItem[], query: string, opts?: SearchOptions): SearchResult[];
14
+ /** Ranked fuzzy search (exact id > name prefix > word match > subsequence). */
15
+ export declare function searchItems(items: RegistryItem[], query: string, opts?: SearchOptions): RegistryItem[];
16
+ /** Exact lookup by id (`magicui/marquee`) or unique bare name (`marquee`). */
17
+ export declare function findExact(items: RegistryItem[], query: string, library?: string): RegistryItem | undefined;
@@ -0,0 +1,130 @@
1
+ // Ranked fuzzy search over registry items. Pure — safe to use in the browser.
2
+ const indexCache = new WeakMap();
3
+ function norm(s) {
4
+ return (s ?? '').toLowerCase();
5
+ }
6
+ function index(item) {
7
+ let ix = indexCache.get(item);
8
+ if (!ix) {
9
+ const name = norm(item.name);
10
+ const title = norm(item.title);
11
+ ix = {
12
+ id: norm(item.id),
13
+ name,
14
+ title,
15
+ description: norm(item.description),
16
+ category: norm(item.category),
17
+ library: norm(item.library),
18
+ words: `${name} ${title}`.split(/[^a-z0-9]+/).filter(Boolean),
19
+ };
20
+ indexCache.set(item, ix);
21
+ }
22
+ return ix;
23
+ }
24
+ /** Returns a score > 0 when `needle` is a subsequence of `hay` (tighter = higher). */
25
+ function subsequence(needle, hay) {
26
+ if (!needle)
27
+ return 0;
28
+ let h = 0;
29
+ let gaps = 0;
30
+ let last = -1;
31
+ for (const ch of needle) {
32
+ const found = hay.indexOf(ch, h);
33
+ if (found === -1)
34
+ return 0;
35
+ if (last !== -1)
36
+ gaps += found - last - 1;
37
+ last = found;
38
+ h = found + 1;
39
+ }
40
+ return Math.max(1, 100 - gaps * 2 - (hay.length - needle.length) * 0.2);
41
+ }
42
+ export function scoreItem(item, query) {
43
+ const q = norm(query).trim();
44
+ if (!q)
45
+ return 1;
46
+ const ix = index(item);
47
+ if (ix.id === q)
48
+ return 10_000;
49
+ if (ix.name === q)
50
+ return 9_000;
51
+ if (ix.title === q)
52
+ return 8_500;
53
+ let score = 0;
54
+ if (ix.name.startsWith(q))
55
+ score = Math.max(score, 7_000 - ix.name.length);
56
+ if (ix.title.startsWith(q))
57
+ score = Math.max(score, 6_500 - ix.title.length);
58
+ if (ix.id.endsWith('/' + q))
59
+ score = Math.max(score, 8_000);
60
+ // Word matching: every query token must match somewhere.
61
+ const tokens = q.split(/[\s/_-]+/).filter(Boolean);
62
+ if (tokens.length) {
63
+ let sum = 0;
64
+ let all = true;
65
+ for (const t of tokens) {
66
+ let best = 0;
67
+ if (ix.words.some((w) => w === t))
68
+ best = 80;
69
+ else if (ix.words.some((w) => w.startsWith(t)))
70
+ best = 60;
71
+ else if (ix.name.includes(t) || ix.title.includes(t))
72
+ best = 40;
73
+ else if (ix.category.includes(t))
74
+ best = 30;
75
+ else if (ix.library.includes(t))
76
+ best = 25;
77
+ else if (ix.description.includes(t))
78
+ best = 10;
79
+ if (!best) {
80
+ all = false;
81
+ break;
82
+ }
83
+ sum += best;
84
+ }
85
+ if (all)
86
+ score = Math.max(score, 1_000 + (sum / tokens.length) * 10 - ix.title.length * 0.5);
87
+ }
88
+ if (!score) {
89
+ const compact = q.replace(/\s+/g, '');
90
+ const s = Math.max(subsequence(compact, ix.name), subsequence(compact, ix.title));
91
+ if (s > 0)
92
+ score = s;
93
+ }
94
+ return score;
95
+ }
96
+ function matchesFilters(item, opts) {
97
+ if (opts.library && item.library !== opts.library)
98
+ return false;
99
+ if (opts.category && norm(item.category) !== norm(opts.category))
100
+ return false;
101
+ if (opts.kind && item.kind !== opts.kind)
102
+ return false;
103
+ return true;
104
+ }
105
+ export function searchItemsScored(items, query, opts = {}) {
106
+ const limit = opts.limit ?? 20;
107
+ const out = [];
108
+ for (const item of items) {
109
+ if (!matchesFilters(item, opts))
110
+ continue;
111
+ const score = scoreItem(item, query);
112
+ if (score > 0)
113
+ out.push({ item, score });
114
+ }
115
+ out.sort((a, b) => b.score - a.score || a.item.id.localeCompare(b.item.id));
116
+ return limit > 0 ? out.slice(0, limit) : out;
117
+ }
118
+ /** Ranked fuzzy search (exact id > name prefix > word match > subsequence). */
119
+ export function searchItems(items, query, opts = {}) {
120
+ return searchItemsScored(items, query, opts).map((r) => r.item);
121
+ }
122
+ /** Exact lookup by id (`magicui/marquee`) or unique bare name (`marquee`). */
123
+ export function findExact(items, query, library) {
124
+ const q = norm(query).trim();
125
+ const byId = items.find((i) => norm(i.id) === q);
126
+ if (byId)
127
+ return byId;
128
+ const byName = items.filter((i) => norm(i.name) === q && (!library || i.library === library));
129
+ return byName.length === 1 ? byName[0] : undefined;
130
+ }
@@ -0,0 +1,8 @@
1
+ import type { CssTree, CssVars } from './resolve.js';
2
+ export declare const SHADCN_BASE_THEME: Required<CssVars>;
3
+ /** Lets `dark:` follow a `.dark` class, as shadcn's theme expects. */
4
+ export declare const SHADCN_BASE_CSS: CssTree;
5
+ /** Does this code style itself with shadcn theme tokens? */
6
+ export declare const usesShadcnTokens: (code: string) => boolean;
7
+ /** Does this stylesheet already define the shadcn tokens (or its own `--primary`)? */
8
+ export declare const hasShadcnTheme: (css: string) => boolean;
@@ -0,0 +1,52 @@
1
+ // shadcn/ui's base theme (new-york-v4, neutral). Components across the shadcn
2
+ // ecosystem style themselves with these tokens (`bg-primary`, `border-input`, …)
3
+ // but registry items don't ship them: `shadcn init` normally adds them once.
4
+ // get-ui adds them on first install when a project doesn't have them. Pure.
5
+ const LIGHT = {
6
+ radius: '0.625rem',
7
+ background: 'oklch(1 0 0)', foreground: 'oklch(0.145 0 0)',
8
+ card: 'oklch(1 0 0)', 'card-foreground': 'oklch(0.145 0 0)',
9
+ popover: 'oklch(1 0 0)', 'popover-foreground': 'oklch(0.145 0 0)',
10
+ primary: 'oklch(0.205 0 0)', 'primary-foreground': 'oklch(0.985 0 0)',
11
+ secondary: 'oklch(0.97 0 0)', 'secondary-foreground': 'oklch(0.205 0 0)',
12
+ muted: 'oklch(0.97 0 0)', 'muted-foreground': 'oklch(0.556 0 0)',
13
+ accent: 'oklch(0.97 0 0)', 'accent-foreground': 'oklch(0.205 0 0)',
14
+ destructive: 'oklch(0.577 0.245 27.325)',
15
+ border: 'oklch(0.922 0 0)', input: 'oklch(0.922 0 0)', ring: 'oklch(0.708 0 0)',
16
+ 'chart-1': 'oklch(0.646 0.222 41.116)', 'chart-2': 'oklch(0.6 0.118 184.704)', 'chart-3': 'oklch(0.398 0.07 227.392)',
17
+ 'chart-4': 'oklch(0.828 0.189 84.429)', 'chart-5': 'oklch(0.769 0.188 70.08)',
18
+ sidebar: 'oklch(0.985 0 0)', 'sidebar-foreground': 'oklch(0.145 0 0)', 'sidebar-primary': 'oklch(0.205 0 0)',
19
+ 'sidebar-primary-foreground': 'oklch(0.985 0 0)', 'sidebar-accent': 'oklch(0.97 0 0)', 'sidebar-accent-foreground': 'oklch(0.205 0 0)',
20
+ 'sidebar-border': 'oklch(0.922 0 0)', 'sidebar-ring': 'oklch(0.708 0 0)',
21
+ };
22
+ const DARK = {
23
+ background: 'oklch(0.145 0 0)', foreground: 'oklch(0.985 0 0)',
24
+ card: 'oklch(0.205 0 0)', 'card-foreground': 'oklch(0.985 0 0)',
25
+ popover: 'oklch(0.205 0 0)', 'popover-foreground': 'oklch(0.985 0 0)',
26
+ primary: 'oklch(0.922 0 0)', 'primary-foreground': 'oklch(0.205 0 0)',
27
+ secondary: 'oklch(0.269 0 0)', 'secondary-foreground': 'oklch(0.985 0 0)',
28
+ muted: 'oklch(0.269 0 0)', 'muted-foreground': 'oklch(0.708 0 0)',
29
+ accent: 'oklch(0.269 0 0)', 'accent-foreground': 'oklch(0.985 0 0)',
30
+ destructive: 'oklch(0.704 0.191 22.216)',
31
+ border: 'oklch(1 0 0 / 10%)', input: 'oklch(1 0 0 / 15%)', ring: 'oklch(0.556 0 0)',
32
+ sidebar: 'oklch(0.205 0 0)', 'sidebar-foreground': 'oklch(0.985 0 0)', 'sidebar-primary': 'oklch(0.488 0.243 264.376)',
33
+ 'sidebar-primary-foreground': 'oklch(0.985 0 0)', 'sidebar-accent': 'oklch(0.269 0 0)', 'sidebar-accent-foreground': 'oklch(0.985 0 0)',
34
+ 'sidebar-border': 'oklch(1 0 0 / 10%)', 'sidebar-ring': 'oklch(0.556 0 0)',
35
+ };
36
+ /** Tailwind v4 `@theme inline` mappings so `bg-primary` etc. resolve to the variables. */
37
+ const THEME = {
38
+ 'radius-sm': 'calc(var(--radius) - 4px)',
39
+ 'radius-md': 'calc(var(--radius) - 2px)',
40
+ 'radius-lg': 'var(--radius)',
41
+ 'radius-xl': 'calc(var(--radius) + 4px)',
42
+ ...Object.fromEntries(Object.keys(LIGHT).filter((k) => k !== 'radius').map((k) => [`color-${k}`, `var(--${k})`])),
43
+ };
44
+ export const SHADCN_BASE_THEME = { theme: THEME, light: LIGHT, dark: DARK };
45
+ /** Lets `dark:` follow a `.dark` class, as shadcn's theme expects. */
46
+ export const SHADCN_BASE_CSS = { '@custom-variant dark (&:is(.dark *))': {} };
47
+ const TOKENS = Object.keys(LIGHT).filter((k) => k !== 'radius').join('|');
48
+ const TOKEN_CLASS = new RegExp(`(?:^|[\\s"'\`:])(?:bg|text|border|ring|outline|fill|stroke|from|to|via|divide|placeholder|caret|accent|decoration|shadow)-(?:${TOKENS})(?![\\w-])`);
49
+ /** Does this code style itself with shadcn theme tokens? */
50
+ export const usesShadcnTokens = (code) => TOKEN_CLASS.test(code);
51
+ /** Does this stylesheet already define the shadcn tokens (or its own `--primary`)? */
52
+ export const hasShadcnTheme = (css) => /--primary\s*:/.test(css) || /--color-primary\s*:/.test(css);
@@ -0,0 +1,33 @@
1
+ export interface Aliases {
2
+ /** e.g. `@/components` */
3
+ components: string;
4
+ /** e.g. `@/components/ui` */
5
+ ui: string;
6
+ /** e.g. `@/lib/utils` */
7
+ utils: string;
8
+ /** e.g. `@/lib` */
9
+ lib: string;
10
+ /** e.g. `@/hooks` */
11
+ hooks: string;
12
+ }
13
+ export declare const DEFAULT_ALIASES: Aliases;
14
+ /**
15
+ * Map one module specifier from registry-internal form to the user's aliases.
16
+ * `fileMap` maps specifiers of files that are being installed (derived from their
17
+ * registry `path`/`target`) to where they will actually live.
18
+ */
19
+ export declare function mapSpecifier(spec: string, aliases: Aliases, fileMap?: Record<string, string>): string;
20
+ /** Rewrite all import/export/require specifiers in `code`. */
21
+ export declare function rewriteImports(code: string, aliases: Aliases, fileMap?: Record<string, string>): string;
22
+ /** List module specifiers imported by `code`. */
23
+ export declare function listImports(code: string): string[];
24
+ /**
25
+ * Replace `<IconPlaceholder lucide="CheckIcon" tabler="…" … />` with `<CheckIcon … />`
26
+ * and swap the placeholder import for a lucide-react import.
27
+ */
28
+ export declare function transformIconPlaceholders(code: string): string;
29
+ /**
30
+ * React 19's types dropped the global `JSX` namespace, so `JSX.Element` in older
31
+ * components fails to type-check. Import the namespace from react where it's used.
32
+ */
33
+ export declare function importJsxNamespace(code: string): string;