@bitovi/vybit 0.4.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.
@@ -0,0 +1,281 @@
1
+ // In-memory patch queue with draft + commits model
2
+
3
+ import { EventEmitter } from 'node:events';
4
+ import type { Patch, PatchStatus, PatchSummary, Commit, CommitStatus, CommitSummary } from '../shared/types.js';
5
+
6
+ const emitter = new EventEmitter();
7
+
8
+ function toSummary(p: Patch): PatchSummary {
9
+ return {
10
+ id: p.id,
11
+ kind: p.kind,
12
+ elementKey: p.elementKey,
13
+ status: p.status,
14
+ originalClass: p.originalClass,
15
+ newClass: p.newClass,
16
+ property: p.property,
17
+ timestamp: p.timestamp,
18
+ component: p.component,
19
+ errorMessage: p.errorMessage,
20
+ message: p.message,
21
+ image: p.image,
22
+ };
23
+ }
24
+
25
+ function toCommitSummary(c: Commit): CommitSummary {
26
+ return {
27
+ id: c.id,
28
+ status: c.status,
29
+ timestamp: c.timestamp,
30
+ patches: c.patches.map(toSummary),
31
+ };
32
+ }
33
+
34
+ // Mutable draft: accumulates patches as the user stages them (preserves insertion order)
35
+ const draftPatches: Patch[] = [];
36
+
37
+ // Finalized commits
38
+ const commits: Commit[] = [];
39
+
40
+ export function addPatch(patch: Patch): Patch {
41
+ // Dedup by ID first — if an identical PATCH_STAGED arrives twice (e.g. from
42
+ // two overlays connected to the same server) just ignore the duplicate.
43
+ if (draftPatches.some(p => p.id === patch.id)) {
44
+ return patch;
45
+ }
46
+
47
+ if (patch.kind === 'class-change') {
48
+ // Dedup: if a staged patch exists for the same elementKey+property, replace it
49
+ const existingIdx = draftPatches.findIndex(
50
+ p => p.kind === 'class-change' && p.elementKey === patch.elementKey && p.property === patch.property && p.status === 'staged'
51
+ );
52
+ if (existingIdx !== -1) {
53
+ draftPatches.splice(existingIdx, 1);
54
+ }
55
+ }
56
+ // Message patches are always appended (no dedup)
57
+ draftPatches.push(patch);
58
+ return patch;
59
+ }
60
+
61
+ export function commitDraft(ids: string[]): Commit {
62
+ const idSet = new Set(ids);
63
+ const commitPatches: Patch[] = [];
64
+
65
+ // Extract matching patches from draft, preserving order
66
+ for (let i = draftPatches.length - 1; i >= 0; i--) {
67
+ if (idSet.has(draftPatches[i].id) && draftPatches[i].status === 'staged') {
68
+ draftPatches[i].status = 'committed';
69
+ commitPatches.unshift(draftPatches[i]);
70
+ draftPatches.splice(i, 1);
71
+ }
72
+ }
73
+
74
+ const commit: Commit = {
75
+ id: crypto.randomUUID(),
76
+ patches: commitPatches,
77
+ status: 'committed',
78
+ timestamp: new Date().toISOString(),
79
+ };
80
+
81
+ // Set commitId on each patch
82
+ for (const p of commit.patches) {
83
+ p.commitId = commit.id;
84
+ }
85
+
86
+ commits.push(commit);
87
+ if (commitPatches.length > 0) emitter.emit('committed');
88
+ return commit;
89
+ }
90
+
91
+ /** @deprecated Use commitDraft instead. Backward compat shim. */
92
+ export function commitPatches(ids: string[]): number {
93
+ const commit = commitDraft(ids);
94
+ return commit.patches.length;
95
+ }
96
+
97
+ /** Returns the oldest Commit with status 'committed', or null. */
98
+ export function getNextCommitted(): Commit | null {
99
+ return commits.find(c => c.status === 'committed') ?? null;
100
+ }
101
+
102
+ export function markCommitImplementing(commitId: string): void {
103
+ const commit = commits.find(c => c.id === commitId);
104
+ if (!commit) return;
105
+ commit.status = 'implementing';
106
+ for (const p of commit.patches) {
107
+ if (p.status === 'committed') p.status = 'implementing';
108
+ }
109
+ }
110
+
111
+ export interface PatchResult {
112
+ patchId: string;
113
+ success: boolean;
114
+ error?: string;
115
+ }
116
+
117
+ export function markCommitImplemented(commitId: string, results: PatchResult[]): void {
118
+ const commit = commits.find(c => c.id === commitId);
119
+ if (!commit) return;
120
+
121
+ // Apply results to class-change patches
122
+ for (const result of results) {
123
+ const patch = commit.patches.find(p => p.id === result.patchId);
124
+ if (!patch) continue;
125
+ patch.status = result.success ? 'implemented' : 'error';
126
+ if (result.error) patch.errorMessage = result.error;
127
+ }
128
+
129
+ // Message patches are always "implemented" (informational, no action needed)
130
+ for (const patch of commit.patches) {
131
+ if (patch.kind === 'message') patch.status = 'implemented';
132
+ }
133
+
134
+ const classChanges = commit.patches.filter(p => p.kind === 'class-change');
135
+ const allSucceeded = classChanges.every(p => p.status === 'implemented');
136
+ const allFailed = classChanges.every(p => p.status === 'error');
137
+
138
+ commit.status = classChanges.length === 0 ? 'implemented' // message-only commit
139
+ : allSucceeded ? 'implemented'
140
+ : allFailed ? 'error'
141
+ : 'partial';
142
+ }
143
+
144
+ /** Legacy: mark individual patch IDs as implementing (backward compat for old MCP tools). */
145
+ export function markImplementing(ids: string[]): number {
146
+ const idSet = new Set(ids);
147
+ let moved = 0;
148
+ for (const commit of commits) {
149
+ for (const p of commit.patches) {
150
+ if (idSet.has(p.id) && p.status === 'committed') {
151
+ p.status = 'implementing';
152
+ moved++;
153
+ }
154
+ }
155
+ // If all patches in commit are implementing, update commit status
156
+ if (commit.status === 'committed' && commit.patches.every(p => p.status !== 'committed')) {
157
+ commit.status = 'implementing';
158
+ }
159
+ }
160
+ return moved;
161
+ }
162
+
163
+ /** Legacy: mark individual patch IDs as implemented (backward compat). */
164
+ export function markImplemented(ids: string[]): number {
165
+ const idSet = new Set(ids);
166
+ let moved = 0;
167
+ for (const commit of commits) {
168
+ for (const p of commit.patches) {
169
+ if (idSet.has(p.id) && (p.status === 'committed' || p.status === 'implementing')) {
170
+ p.status = 'implemented';
171
+ moved++;
172
+ }
173
+ }
174
+ // Auto-succeed message patches if all class-changes done
175
+ const classChanges = commit.patches.filter(p => p.kind === 'class-change');
176
+ if (classChanges.length > 0 && classChanges.every(p => p.status === 'implemented')) {
177
+ for (const p of commit.patches) {
178
+ if (p.kind === 'message' && p.status !== 'implemented') p.status = 'implemented';
179
+ }
180
+ commit.status = 'implemented';
181
+ }
182
+ }
183
+ return moved;
184
+ }
185
+
186
+ export function getByStatus(status: PatchStatus): Patch[] {
187
+ const result: Patch[] = [];
188
+ // Draft patches
189
+ for (const p of draftPatches) {
190
+ if (p.status === status) result.push(p);
191
+ }
192
+ // Commit patches
193
+ for (const commit of commits) {
194
+ for (const p of commit.patches) {
195
+ if (p.status === status) result.push(p);
196
+ }
197
+ }
198
+ return result;
199
+ }
200
+
201
+ export function getCounts(): { staged: number; committed: number; implementing: number; implemented: number } {
202
+ const counts = { staged: 0, committed: 0, implementing: 0, implemented: 0 };
203
+ for (const p of draftPatches) {
204
+ if (p.status in counts) counts[p.status as keyof typeof counts]++;
205
+ }
206
+ for (const commit of commits) {
207
+ for (const p of commit.patches) {
208
+ if (p.status in counts) counts[p.status as keyof typeof counts]++;
209
+ }
210
+ }
211
+ return counts;
212
+ }
213
+
214
+ /** Build the full QUEUE_UPDATE payload */
215
+ export function getQueueUpdate() {
216
+ // Count commits by status
217
+ let committedCount = 0;
218
+ let implementingCount = 0;
219
+ let implementedCount = 0;
220
+ let partialCount = 0;
221
+ let errorCount = 0;
222
+ for (const c of commits) {
223
+ switch (c.status) {
224
+ case 'committed': committedCount++; break;
225
+ case 'implementing': implementingCount++; break;
226
+ case 'implemented': implementedCount++; break;
227
+ case 'partial': partialCount++; break;
228
+ case 'error': errorCount++; break;
229
+ }
230
+ }
231
+
232
+ return {
233
+ draftCount: draftPatches.length,
234
+ committedCount,
235
+ implementingCount,
236
+ implementedCount,
237
+ partialCount,
238
+ errorCount,
239
+ draft: draftPatches.map(toSummary),
240
+ commits: commits.map(toCommitSummary),
241
+ };
242
+ }
243
+
244
+ /** @deprecated Use getQueueUpdate instead. Backward compat shim. */
245
+ export function getPatchUpdate() {
246
+ const allPatches: Patch[] = [...draftPatches];
247
+ for (const c of commits) allPatches.push(...c.patches);
248
+ const counts = getCounts();
249
+ return {
250
+ ...counts,
251
+ patches: {
252
+ staged: allPatches.filter(p => p.status === 'staged').map(toSummary),
253
+ committed: allPatches.filter(p => p.status === 'committed').map(toSummary),
254
+ implementing: allPatches.filter(p => p.status === 'implementing').map(toSummary),
255
+ implemented: allPatches.filter(p => p.status === 'implemented').map(toSummary),
256
+ },
257
+ };
258
+ }
259
+
260
+ export function discardDraftPatch(id: string): boolean {
261
+ // Remove ALL patches with this ID (guards against any duplicates that
262
+ // slipped through addPatch before the ID-dedup was in place).
263
+ const before = draftPatches.length;
264
+ const remaining = draftPatches.filter(p => p.id !== id);
265
+ draftPatches.length = 0;
266
+ draftPatches.push(...remaining);
267
+ return remaining.length < before;
268
+ }
269
+
270
+ export function clearAll(): { staged: number; committed: number; implementing: number; implemented: number } {
271
+ const counts = getCounts();
272
+ draftPatches.length = 0;
273
+ commits.length = 0;
274
+ return counts;
275
+ }
276
+
277
+ /** Subscribe to commit events. Returns an unsubscribe function. */
278
+ export function onCommitted(listener: () => void): () => void {
279
+ emitter.on('committed', listener);
280
+ return () => { emitter.off('committed', listener); };
281
+ }
@@ -0,0 +1,17 @@
1
+ // Shared interface for Tailwind version adapters.
2
+ // Both v3 and v4 adapters implement this contract so the rest of
3
+ // the server code is version-agnostic.
4
+
5
+ export interface TailwindThemeSubset {
6
+ spacing: Record<string, string>;
7
+ colors: Record<string, unknown>;
8
+ fontSize: Record<string, unknown>;
9
+ fontWeight: Record<string, unknown>;
10
+ borderRadius: Record<string, string>;
11
+ }
12
+
13
+ export interface TailwindAdapter {
14
+ readonly version: 3 | 4;
15
+ resolveTailwindConfig(): Promise<TailwindThemeSubset>;
16
+ generateCssForClasses(classes: string[]): Promise<string>;
17
+ }
@@ -0,0 +1,159 @@
1
+ // Tailwind v3 adapter — uses resolveConfig + PostCSS from the target project's tailwindcss v3.
2
+
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { resolve } from "path";
5
+ import { createRequire } from "module";
6
+ import type { TailwindAdapter, TailwindThemeSubset } from "./tailwind-adapter.js";
7
+
8
+ let cached: TailwindThemeSubset | null = null;
9
+
10
+ const defaultTheme: TailwindThemeSubset = {
11
+ spacing: {
12
+ "0": "0px", px: "1px",
13
+ "0.5": "0.125rem", "1": "0.25rem", "1.5": "0.375rem", "2": "0.5rem",
14
+ "2.5": "0.625rem", "3": "0.75rem", "3.5": "0.875rem", "4": "1rem",
15
+ "5": "1.25rem", "6": "1.5rem", "7": "1.75rem", "8": "2rem",
16
+ "9": "2.25rem", "10": "2.5rem", "11": "2.75rem", "12": "3rem",
17
+ "14": "3.5rem", "16": "4rem", "20": "5rem", "24": "6rem",
18
+ "28": "7rem", "32": "8rem", "36": "9rem", "40": "10rem",
19
+ "44": "11rem", "48": "12rem", "52": "13rem", "56": "14rem",
20
+ "60": "15rem", "64": "16rem", "72": "18rem", "80": "20rem", "96": "24rem",
21
+ },
22
+ colors: {
23
+ inherit: "inherit", current: "currentColor", transparent: "transparent",
24
+ black: "#000", white: "#fff",
25
+ slate: { "50": "#f8fafc", "100": "#f1f5f9", "200": "#e2e8f0", "300": "#cbd5e1", "400": "#94a3b8", "500": "#64748b", "600": "#475569", "700": "#334155", "800": "#1e293b", "900": "#0f172a", "950": "#020617" },
26
+ gray: { "50": "#f9fafb", "100": "#f3f4f6", "200": "#e5e7eb", "300": "#d1d5db", "400": "#9ca3af", "500": "#6b7280", "600": "#4b5563", "700": "#374151", "800": "#1f2937", "900": "#111827", "950": "#030712" },
27
+ zinc: { "50": "#fafafa", "100": "#f4f4f5", "200": "#e4e4e7", "300": "#d4d4d8", "400": "#a1a1aa", "500": "#71717a", "600": "#52525b", "700": "#3f3f46", "800": "#27272a", "900": "#18181b", "950": "#09090b" },
28
+ neutral: { "50": "#fafafa", "100": "#f5f5f5", "200": "#e5e5e5", "300": "#d4d4d4", "400": "#a3a3a3", "500": "#737373", "600": "#525252", "700": "#404040", "800": "#262626", "900": "#171717", "950": "#0a0a0a" },
29
+ stone: { "50": "#fafaf9", "100": "#f5f5f4", "200": "#e7e5e4", "300": "#d6d3d1", "400": "#a8a29e", "500": "#78716c", "600": "#57534e", "700": "#44403c", "800": "#292524", "900": "#1c1917", "950": "#0c0a09" },
30
+ red: { "50": "#fef2f2", "100": "#fee2e2", "200": "#fecaca", "300": "#fca5a5", "400": "#f87171", "500": "#ef4444", "600": "#dc2626", "700": "#b91c1c", "800": "#991b1b", "900": "#7f1d1d", "950": "#450a0a" },
31
+ orange: { "50": "#fff7ed", "100": "#ffedd5", "200": "#fed7aa", "300": "#fdba74", "400": "#fb923c", "500": "#f97316", "600": "#ea580c", "700": "#c2410c", "800": "#9a3412", "900": "#7c2d12", "950": "#431407" },
32
+ amber: { "50": "#fffbeb", "100": "#fef3c7", "200": "#fde68a", "300": "#fcd34d", "400": "#fbbf24", "500": "#f59e0b", "600": "#d97706", "700": "#b45309", "800": "#92400e", "900": "#78350f", "950": "#451a03" },
33
+ yellow: { "50": "#fefce8", "100": "#fef9c3", "200": "#fef08a", "300": "#fde047", "400": "#facc15", "500": "#eab308", "600": "#ca8a04", "700": "#a16207", "800": "#854d0e", "900": "#713f12", "950": "#422006" },
34
+ lime: { "50": "#f7fee7", "100": "#ecfccb", "200": "#d9f99d", "300": "#bef264", "400": "#a3e635", "500": "#84cc16", "600": "#65a30d", "700": "#4d7c0f", "800": "#3f6212", "900": "#365314", "950": "#1a2e05" },
35
+ green: { "50": "#f0fdf4", "100": "#dcfce7", "200": "#bbf7d0", "300": "#86efac", "400": "#4ade80", "500": "#22c55e", "600": "#16a34a", "700": "#15803d", "800": "#166534", "900": "#14532d", "950": "#052e16" },
36
+ emerald: { "50": "#ecfdf5", "100": "#d1fae5", "200": "#a7f3d0", "300": "#6ee7b7", "400": "#34d399", "500": "#10b981", "600": "#059669", "700": "#047857", "800": "#065f46", "900": "#064e3b", "950": "#022c22" },
37
+ teal: { "50": "#f0fdfa", "100": "#ccfbf1", "200": "#99f6e4", "300": "#5eead4", "400": "#2dd4bf", "500": "#14b8a6", "600": "#0d9488", "700": "#0f766e", "800": "#115e59", "900": "#134e4a", "950": "#042f2e" },
38
+ cyan: { "50": "#ecfeff", "100": "#cffafe", "200": "#a5f3fc", "300": "#67e8f9", "400": "#22d3ee", "500": "#06b6d4", "600": "#0891b2", "700": "#0e7490", "800": "#155e75", "900": "#164e63", "950": "#083344" },
39
+ sky: { "50": "#f0f9ff", "100": "#e0f2fe", "200": "#bae6fd", "300": "#7dd3fc", "400": "#38bdf8", "500": "#0ea5e9", "600": "#0284c7", "700": "#0369a1", "800": "#075985", "900": "#0c4a6e", "950": "#082f49" },
40
+ blue: { "50": "#eff6ff", "100": "#dbeafe", "200": "#bfdbfe", "300": "#93c5fd", "400": "#60a5fa", "500": "#3b82f6", "600": "#2563eb", "700": "#1d4ed8", "800": "#1e40af", "900": "#1e3a8a", "950": "#172554" },
41
+ indigo: { "50": "#eef2ff", "100": "#e0e7ff", "200": "#c7d2fe", "300": "#a5b4fc", "400": "#818cf8", "500": "#6366f1", "600": "#4f46e5", "700": "#4338ca", "800": "#3730a3", "900": "#312e81", "950": "#1e1b4b" },
42
+ violet: { "50": "#f5f3ff", "100": "#ede9fe", "200": "#ddd6fe", "300": "#c4b5fd", "400": "#a78bfa", "500": "#8b5cf6", "600": "#7c3aed", "700": "#6d28d9", "800": "#5b21b6", "900": "#4c1d95", "950": "#2e1065" },
43
+ purple: { "50": "#faf5ff", "100": "#f3e8ff", "200": "#e9d5ff", "300": "#d8b4fe", "400": "#c084fc", "500": "#a855f7", "600": "#9333ea", "700": "#7e22ce", "800": "#6b21a8", "900": "#581c87", "950": "#3b0764" },
44
+ fuchsia: { "50": "#fdf4ff", "100": "#fae8ff", "200": "#f5d0fe", "300": "#f0abfc", "400": "#e879f9", "500": "#d946ef", "600": "#c026d3", "700": "#a21caf", "800": "#86198f", "900": "#701a75", "950": "#4a044e" },
45
+ pink: { "50": "#fdf2f8", "100": "#fce7f3", "200": "#fbcfe8", "300": "#f9a8d4", "400": "#f472b6", "500": "#ec4899", "600": "#db2777", "700": "#be185d", "800": "#9d174d", "900": "#831843", "950": "#500724" },
46
+ rose: { "50": "#fff1f2", "100": "#ffe4e6", "200": "#fecdd3", "300": "#fda4af", "400": "#fb7185", "500": "#f43f5e", "600": "#e11d48", "700": "#be123c", "800": "#9f1239", "900": "#881337", "950": "#4c0519" },
47
+ },
48
+ fontSize: {
49
+ xs: ["0.75rem", { lineHeight: "1rem" }],
50
+ sm: ["0.875rem", { lineHeight: "1.25rem" }],
51
+ base: ["1rem", { lineHeight: "1.5rem" }],
52
+ lg: ["1.125rem", { lineHeight: "1.75rem" }],
53
+ xl: ["1.25rem", { lineHeight: "1.75rem" }],
54
+ "2xl": ["1.5rem", { lineHeight: "2rem" }],
55
+ "3xl": ["1.875rem", { lineHeight: "2.25rem" }],
56
+ "4xl": ["2.25rem", { lineHeight: "2.5rem" }],
57
+ "5xl": ["3rem", { lineHeight: "1" }],
58
+ "6xl": ["3.75rem", { lineHeight: "1" }],
59
+ "7xl": ["4.5rem", { lineHeight: "1" }],
60
+ "8xl": ["6rem", { lineHeight: "1" }],
61
+ "9xl": ["8rem", { lineHeight: "1" }],
62
+ },
63
+ fontWeight: {
64
+ thin: "100", extralight: "200", light: "300", normal: "400",
65
+ medium: "500", semibold: "600", bold: "700", extrabold: "800", black: "900",
66
+ },
67
+ borderRadius: {
68
+ none: "0px", sm: "0.125rem", DEFAULT: "0.25rem", md: "0.375rem",
69
+ lg: "0.5rem", xl: "0.75rem", "2xl": "1rem", "3xl": "1.5rem", full: "9999px",
70
+ },
71
+ };
72
+
73
+ export class TailwindV3Adapter implements TailwindAdapter {
74
+ readonly version = 3 as const;
75
+
76
+ async resolveTailwindConfig(): Promise<TailwindThemeSubset> {
77
+ if (cached) return cached;
78
+
79
+ const cwd = process.cwd();
80
+ const jsPath = resolve(cwd, "tailwind.config.js");
81
+ const tsPath = resolve(cwd, "tailwind.config.ts");
82
+ const cjsPath = resolve(cwd, "tailwind.config.cjs");
83
+ const configPath = existsSync(jsPath)
84
+ ? jsPath
85
+ : existsSync(tsPath)
86
+ ? tsPath
87
+ : existsSync(cjsPath)
88
+ ? cjsPath
89
+ : null;
90
+
91
+ if (configPath) {
92
+ try {
93
+ const req = createRequire(resolve(cwd, "package.json"));
94
+ // @ts-expect-error — tailwindcss resolveConfig lacks NodeNext-compatible exports
95
+ const resolveConfig = (await import(req.resolve("tailwindcss/resolveConfig"))).default;
96
+ const userConfig = (await import(configPath)).default;
97
+ const full = resolveConfig(userConfig);
98
+ const theme = full.theme ?? {};
99
+
100
+ cached = {
101
+ spacing: (theme.spacing as Record<string, string>) ?? defaultTheme.spacing,
102
+ colors: (theme.colors as Record<string, unknown>) ?? defaultTheme.colors,
103
+ fontSize: (theme.fontSize as Record<string, unknown>) ?? defaultTheme.fontSize,
104
+ fontWeight: (theme.fontWeight as Record<string, unknown>) ?? defaultTheme.fontWeight,
105
+ borderRadius: (theme.borderRadius as Record<string, string>) ?? defaultTheme.borderRadius,
106
+ };
107
+
108
+ console.error(`[tailwind] v3 resolved config from ${configPath}`);
109
+ return cached;
110
+ } catch (err) {
111
+ console.error("[tailwind] v3 failed to resolve config, using defaults:", err);
112
+ }
113
+ } else {
114
+ console.error("[tailwind] v3 no tailwind.config found — using defaults.");
115
+ }
116
+
117
+ cached = defaultTheme;
118
+ return cached;
119
+ }
120
+
121
+ async generateCssForClasses(classes: string[]): Promise<string> {
122
+ const cwd = process.cwd();
123
+ const req = createRequire(resolve(cwd, "package.json"));
124
+
125
+ const jsPath = resolve(cwd, "tailwind.config.js");
126
+ const tsPath = resolve(cwd, "tailwind.config.ts");
127
+ const cjsPath = resolve(cwd, "tailwind.config.cjs");
128
+ const configPath = existsSync(jsPath)
129
+ ? jsPath
130
+ : existsSync(tsPath)
131
+ ? tsPath
132
+ : existsSync(cjsPath)
133
+ ? cjsPath
134
+ : null;
135
+
136
+ let userConfig: object = {};
137
+ if (configPath) {
138
+ try {
139
+ userConfig = (await import(configPath)).default;
140
+ } catch {
141
+ // fall through to empty config
142
+ }
143
+ }
144
+
145
+ // Load postcss and tailwindcss as a PostCSS plugin from the target project
146
+ const postcss = (await import(req.resolve("postcss"))).default;
147
+ const tailwindPlugin = (await import(req.resolve("tailwindcss"))).default;
148
+
149
+ const result = await postcss([
150
+ tailwindPlugin({
151
+ ...userConfig,
152
+ content: [], // skip file scanning
153
+ safelist: classes, // generate only the requested classes
154
+ }),
155
+ ]).process("@tailwind utilities;", { from: undefined });
156
+
157
+ return result.css;
158
+ }
159
+ }
@@ -0,0 +1,160 @@
1
+ // Tailwind v4 adapter — uses compile() / build() from the target project's tailwindcss v4.
2
+
3
+ import { readFileSync } from "fs";
4
+ import { resolve, dirname } from "path";
5
+ import { createRequire } from "module";
6
+ import type { TailwindAdapter, TailwindThemeSubset } from "./tailwind-adapter.js";
7
+
8
+ // Cached compiler instance (from target project's tailwindcss)
9
+ let compilerCache: { build: (classes: string[]) => string } | null = null;
10
+
11
+ /**
12
+ * Get a Tailwind v4 compile() function from the target project's node_modules.
13
+ */
14
+ async function getCompile(): Promise<(css: string, opts: any) => Promise<{ build: (classes: string[]) => string }>> {
15
+ const cwd = process.cwd();
16
+ const req = createRequire(resolve(cwd, "package.json"));
17
+ const tw = await import(req.resolve("tailwindcss"));
18
+ // Handle CJS/ESM interop: compile may be on tw directly or on tw.default
19
+ const mod = tw.default ?? tw;
20
+ const compile = mod.compile ?? mod.default?.compile;
21
+ if (typeof compile !== "function") {
22
+ throw new Error("Could not find compile() in target project's tailwindcss");
23
+ }
24
+ return compile;
25
+ }
26
+
27
+ /**
28
+ * loadStylesheet callback for Tailwind v4 compile().
29
+ * Resolves @import "tailwindcss" and other stylesheet imports from the target project.
30
+ */
31
+ function makeLoadStylesheet(cwd: string) {
32
+ const req = createRequire(resolve(cwd, "package.json"));
33
+ return async (id: string, base: string) => {
34
+ let resolved: string;
35
+ if (id === "tailwindcss") {
36
+ resolved = req.resolve("tailwindcss/index.css");
37
+ } else {
38
+ resolved = req.resolve(id, { paths: [base || cwd] });
39
+ }
40
+ return {
41
+ content: readFileSync(resolved, "utf8"),
42
+ base: dirname(resolved),
43
+ };
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Initialize the Tailwind v4 compiler for the target project.
49
+ */
50
+ async function getCompiler(): Promise<{ build: (classes: string[]) => string }> {
51
+ if (compilerCache) return compilerCache;
52
+
53
+ const cwd = process.cwd();
54
+ const compile = await getCompile();
55
+
56
+ const result = await compile('@import "tailwindcss";', {
57
+ loadStylesheet: makeLoadStylesheet(cwd),
58
+ });
59
+
60
+ compilerCache = result;
61
+ console.error("[tailwind] Initialized Tailwind v4 compiler from target project");
62
+ return result;
63
+ }
64
+
65
+ // Classes we probe to extract theme values
66
+ const HUES = [
67
+ "slate", "gray", "zinc", "neutral", "stone",
68
+ "red", "orange", "amber", "yellow", "lime",
69
+ "green", "emerald", "teal", "cyan", "sky",
70
+ "blue", "indigo", "violet", "purple", "fuchsia",
71
+ "pink", "rose",
72
+ ];
73
+ const SHADES = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"];
74
+ const SPECIAL_COLORS = ["black", "white", "transparent"];
75
+
76
+ const SPACING_KEYS = [
77
+ "0", "px", "0.5", "1", "1.5", "2", "2.5", "3", "3.5", "4", "5", "6", "7",
78
+ "8", "9", "10", "11", "12", "14", "16", "20", "24", "28", "32", "36", "40",
79
+ "44", "48", "52", "56", "60", "64", "72", "80", "96",
80
+ ];
81
+
82
+ const FONT_SIZE_KEYS = ["xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl", "9xl"];
83
+ const FONT_WEIGHT_KEYS = ["thin", "extralight", "light", "normal", "medium", "semibold", "bold", "extrabold", "black"];
84
+ const BORDER_RADIUS_KEYS = ["none", "sm", "", "md", "lg", "xl", "2xl", "3xl", "full"];
85
+
86
+ /**
87
+ * Extract --var definitions from compiled CSS output.
88
+ */
89
+ function extractVars(css: string, prefix: string): Map<string, string> {
90
+ const vars = new Map<string, string>();
91
+ const regex = new RegExp(`^\\s*--${prefix}-([\\w.-]+):\\s*([^;]+);`, "gm");
92
+ let match;
93
+ while ((match = regex.exec(css)) !== null) {
94
+ vars.set(match[1], match[2].trim());
95
+ }
96
+ return vars;
97
+ }
98
+
99
+ export class TailwindV4Adapter implements TailwindAdapter {
100
+ readonly version = 4 as const;
101
+
102
+ async resolveTailwindConfig(): Promise<TailwindThemeSubset> {
103
+ const compiler = await getCompiler();
104
+
105
+ // Probe color classes to extract theme variable definitions
106
+ const probeClasses: string[] = [];
107
+ for (const h of HUES) {
108
+ for (const s of SHADES) probeClasses.push(`bg-${h}-${s}`);
109
+ }
110
+ for (const s of SPECIAL_COLORS) probeClasses.push(`bg-${s}`);
111
+
112
+ const css = compiler.build(probeClasses);
113
+
114
+ // --- Colors (extracted from CSS custom properties) ---
115
+ const colorVars = extractVars(css, "color");
116
+ const colors: Record<string, unknown> = {};
117
+ for (const [name, value] of colorVars) {
118
+ const dashIdx = name.lastIndexOf("-");
119
+ if (dashIdx > 0) {
120
+ const hue = name.substring(0, dashIdx);
121
+ const shade = name.substring(dashIdx + 1);
122
+ if (/^\d+$/.test(shade)) {
123
+ if (!colors[hue]) colors[hue] = {};
124
+ (colors[hue] as Record<string, string>)[shade] = value;
125
+ continue;
126
+ }
127
+ }
128
+ colors[name] = value;
129
+ }
130
+ if (!colors["transparent"]) colors["transparent"] = "transparent";
131
+
132
+ // --- Spacing (v4 uses calc(var(--spacing) * N)) ---
133
+ const spacing: Record<string, string> = {};
134
+ for (const k of SPACING_KEYS) {
135
+ spacing[k] = k === "px" ? "1px" : k === "0" ? "0px" : `calc(var(--spacing) * ${k})`;
136
+ }
137
+
138
+ // --- Font size, weight, border radius (static scales in v4) ---
139
+ const fontSize: Record<string, unknown> = {};
140
+ for (const k of FONT_SIZE_KEYS) fontSize[k] = k;
141
+
142
+ const fontWeight: Record<string, unknown> = {};
143
+ for (const k of FONT_WEIGHT_KEYS) fontWeight[k] = k;
144
+
145
+ const borderRadius: Record<string, string> = {};
146
+ for (const k of BORDER_RADIUS_KEYS) borderRadius[k || "DEFAULT"] = k || "DEFAULT";
147
+
148
+ const result: TailwindThemeSubset = { spacing, colors, fontSize, fontWeight, borderRadius };
149
+ console.error("[tailwind] v4 resolved theme:", {
150
+ colors: Object.keys(colors).length + " entries",
151
+ spacing: Object.keys(spacing).length + " entries",
152
+ });
153
+ return result;
154
+ }
155
+
156
+ async generateCssForClasses(classes: string[]): Promise<string> {
157
+ const compiler = await getCompiler();
158
+ return compiler.build(classes);
159
+ }
160
+ }
@@ -0,0 +1,50 @@
1
+ // Tailwind adapter factory — auto-detects v3 vs v4 from the target project's
2
+ // installed tailwindcss version and delegates to the appropriate adapter.
3
+
4
+ import { readFileSync } from "fs";
5
+ import { resolve } from "path";
6
+ import { createRequire } from "module";
7
+ import type { TailwindAdapter, TailwindThemeSubset } from "./tailwind-adapter.js";
8
+
9
+ export type { TailwindThemeSubset };
10
+
11
+ let adapterCache: TailwindAdapter | null = null;
12
+
13
+ /**
14
+ * Detect which major version of tailwindcss is installed in the target project.
15
+ */
16
+ function detectTailwindVersion(): 3 | 4 {
17
+ const cwd = process.cwd();
18
+ const req = createRequire(resolve(cwd, "package.json"));
19
+ const pkgPath = req.resolve("tailwindcss/package.json");
20
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
21
+ const major = parseInt(pkg.version.split(".")[0], 10);
22
+ return major >= 4 ? 4 : 3;
23
+ }
24
+
25
+ async function getAdapter(): Promise<TailwindAdapter> {
26
+ if (adapterCache) return adapterCache;
27
+ const version = detectTailwindVersion();
28
+ if (version === 3) {
29
+ const { TailwindV3Adapter } = await import("./tailwind-v3.js");
30
+ adapterCache = new TailwindV3Adapter();
31
+ } else {
32
+ const { TailwindV4Adapter } = await import("./tailwind-v4.js");
33
+ adapterCache = new TailwindV4Adapter();
34
+ }
35
+ console.error(`[tailwind] Using Tailwind v${version} adapter`);
36
+ return adapterCache;
37
+ }
38
+
39
+ /** Expose the detected version for the /api/info endpoint. */
40
+ export async function getTailwindVersion(): Promise<3 | 4> {
41
+ return (await getAdapter()).version;
42
+ }
43
+
44
+ export async function resolveTailwindConfig(): Promise<TailwindThemeSubset> {
45
+ return (await getAdapter()).resolveTailwindConfig();
46
+ }
47
+
48
+ export async function generateCssForClasses(classes: string[]): Promise<string> {
49
+ return (await getAdapter()).generateCssForClasses(classes);
50
+ }