@stayicon/drift-guard 0.1.0 → 0.2.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 +31 -1
- package/dist/{chunk-27T45SVD.js → chunk-HI6H6PCS.js} +526 -16
- package/dist/chunk-HI6H6PCS.js.map +1 -0
- package/dist/cli/index.js +438 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +118 -1
- package/dist/index.js +17 -3
- package/package.json +1 -1
- package/dist/chunk-27T45SVD.js.map +0 -1
|
@@ -1,3 +1,113 @@
|
|
|
1
|
+
// src/parsers/structure-parser.ts
|
|
2
|
+
import * as cheerio from "cheerio";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
var SEMANTIC_TAGS = [
|
|
5
|
+
"header",
|
|
6
|
+
"nav",
|
|
7
|
+
"main",
|
|
8
|
+
"section",
|
|
9
|
+
"article",
|
|
10
|
+
"aside",
|
|
11
|
+
"footer",
|
|
12
|
+
"form",
|
|
13
|
+
"table",
|
|
14
|
+
"dialog"
|
|
15
|
+
];
|
|
16
|
+
function shortHash(input) {
|
|
17
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
18
|
+
}
|
|
19
|
+
function computeMaxDepth($, el, depth) {
|
|
20
|
+
let maxDepth = depth;
|
|
21
|
+
el.children().each((_, child) => {
|
|
22
|
+
if ($(child).prop("nodeType") === 1) {
|
|
23
|
+
const childDepth = computeMaxDepth($, $(child), depth + 1);
|
|
24
|
+
if (childDepth > maxDepth) {
|
|
25
|
+
maxDepth = childDepth;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return maxDepth;
|
|
30
|
+
}
|
|
31
|
+
function computeStructureFingerprint(htmlContent) {
|
|
32
|
+
const $ = cheerio.load(htmlContent);
|
|
33
|
+
const semanticTags = {};
|
|
34
|
+
for (const tag of SEMANTIC_TAGS) {
|
|
35
|
+
const count = $(tag).length;
|
|
36
|
+
if (count > 0) {
|
|
37
|
+
semanticTags[tag] = count;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const body = $("body");
|
|
41
|
+
const maxDepth = body.length > 0 ? computeMaxDepth($, body, 0) : 0;
|
|
42
|
+
const layoutElements = [];
|
|
43
|
+
$("[style]").each((_, el) => {
|
|
44
|
+
const style = $(el).attr("style") ?? "";
|
|
45
|
+
if (/display\s*:\s*(flex|grid|inline-flex|inline-grid)/i.test(style)) {
|
|
46
|
+
const tag = ($(el).prop("tagName") ?? "div").toLowerCase();
|
|
47
|
+
const cls = $(el).attr("class") ?? "";
|
|
48
|
+
layoutElements.push(`${tag}.${cls}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
$('[class*="flex"], [class*="grid"]').each((_, el) => {
|
|
52
|
+
const tag = ($(el).prop("tagName") ?? "div").toLowerCase();
|
|
53
|
+
const cls = $(el).attr("class") ?? "";
|
|
54
|
+
if (/\b(flex|grid|inline-flex|inline-grid)\b/.test(cls)) {
|
|
55
|
+
const key = `${tag}.${cls}`;
|
|
56
|
+
if (!layoutElements.includes(key)) {
|
|
57
|
+
layoutElements.push(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
layoutElements.sort();
|
|
62
|
+
const layoutHash = layoutElements.length > 0 ? shortHash(layoutElements.join("|")) : "empty";
|
|
63
|
+
const childTags = [];
|
|
64
|
+
body.children().each((_, child) => {
|
|
65
|
+
if ($(child).prop("nodeType") === 1) {
|
|
66
|
+
const tag = ($(child).prop("tagName") ?? "").toLowerCase();
|
|
67
|
+
if (tag && tag !== "script" && tag !== "link") {
|
|
68
|
+
childTags.push(tag);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const childSequenceHash = childTags.length > 0 ? shortHash(childTags.join(",")) : "empty";
|
|
73
|
+
return {
|
|
74
|
+
semanticTags,
|
|
75
|
+
maxDepth,
|
|
76
|
+
layoutHash,
|
|
77
|
+
childSequenceHash
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function compareStructure(original, current) {
|
|
81
|
+
const details = [];
|
|
82
|
+
if (original.maxDepth !== current.maxDepth) {
|
|
83
|
+
details.push(`maxDepth: ${original.maxDepth} \u2192 ${current.maxDepth}`);
|
|
84
|
+
}
|
|
85
|
+
const allTags = /* @__PURE__ */ new Set([
|
|
86
|
+
...Object.keys(original.semanticTags),
|
|
87
|
+
...Object.keys(current.semanticTags)
|
|
88
|
+
]);
|
|
89
|
+
for (const tag of allTags) {
|
|
90
|
+
const origCount = original.semanticTags[tag] ?? 0;
|
|
91
|
+
const currCount = current.semanticTags[tag] ?? 0;
|
|
92
|
+
if (origCount !== currCount) {
|
|
93
|
+
if (origCount === 0) {
|
|
94
|
+
details.push(`<${tag}> added (${currCount})`);
|
|
95
|
+
} else if (currCount === 0) {
|
|
96
|
+
details.push(`<${tag}> removed (was ${origCount})`);
|
|
97
|
+
} else {
|
|
98
|
+
details.push(`<${tag}> count: ${origCount} \u2192 ${currCount}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (original.layoutHash !== current.layoutHash) {
|
|
103
|
+
details.push(`layout elements changed (hash: ${original.layoutHash} \u2192 ${current.layoutHash})`);
|
|
104
|
+
}
|
|
105
|
+
if (original.childSequenceHash !== current.childSequenceHash) {
|
|
106
|
+
details.push(`body child sequence changed (hash: ${original.childSequenceHash} \u2192 ${current.childSequenceHash})`);
|
|
107
|
+
}
|
|
108
|
+
return details;
|
|
109
|
+
}
|
|
110
|
+
|
|
1
111
|
// src/types/index.ts
|
|
2
112
|
var DEFAULT_CONFIG = {
|
|
3
113
|
cssFiles: [
|
|
@@ -77,19 +187,62 @@ var CATEGORY_MAP = {
|
|
|
77
187
|
"align-items": "layout",
|
|
78
188
|
"grid-template-columns": "layout",
|
|
79
189
|
"grid-template-rows": "layout",
|
|
80
|
-
"position": "layout"
|
|
190
|
+
"position": "layout",
|
|
191
|
+
// Visual effects
|
|
192
|
+
"backdrop-filter": "other",
|
|
193
|
+
"filter": "other",
|
|
194
|
+
"animation": "other",
|
|
195
|
+
"transition": "other"
|
|
81
196
|
};
|
|
82
|
-
function getCategory(property) {
|
|
197
|
+
function getCategory(property, value) {
|
|
83
198
|
if (CATEGORY_MAP[property]) {
|
|
84
199
|
return CATEGORY_MAP[property];
|
|
85
200
|
}
|
|
86
201
|
if (property.startsWith("--")) {
|
|
87
202
|
const lower = property.toLowerCase();
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
203
|
+
const colorKeywords = [
|
|
204
|
+
"color",
|
|
205
|
+
"bg",
|
|
206
|
+
"text",
|
|
207
|
+
"foreground",
|
|
208
|
+
"background",
|
|
209
|
+
// Semantic color tokens (Shadcn UI / Tailwind)
|
|
210
|
+
"primary",
|
|
211
|
+
"secondary",
|
|
212
|
+
"accent",
|
|
213
|
+
"muted",
|
|
214
|
+
"destructive",
|
|
215
|
+
"success",
|
|
216
|
+
"warning",
|
|
217
|
+
"danger",
|
|
218
|
+
"error",
|
|
219
|
+
"info",
|
|
220
|
+
// UI component colors
|
|
221
|
+
"card",
|
|
222
|
+
"popover",
|
|
223
|
+
"border",
|
|
224
|
+
"input",
|
|
225
|
+
"ring",
|
|
226
|
+
"sidebar",
|
|
227
|
+
"chart",
|
|
228
|
+
"glow",
|
|
229
|
+
// State colors
|
|
230
|
+
"hover",
|
|
231
|
+
"active",
|
|
232
|
+
"focus",
|
|
233
|
+
"disabled"
|
|
234
|
+
];
|
|
235
|
+
if (colorKeywords.some((kw) => lower.includes(kw))) return "color";
|
|
236
|
+
if (lower.includes("font") || lower.includes("size") || lower.includes("weight") || lower.includes("line-height") || lower.includes("letter")) return "font";
|
|
237
|
+
if (lower.includes("spacing") || lower.includes("margin") || lower.includes("padding") || lower.includes("gap") || lower.includes("inset")) return "spacing";
|
|
91
238
|
if (lower.includes("shadow")) return "shadow";
|
|
92
239
|
if (lower.includes("radius") || lower.includes("rounded")) return "radius";
|
|
240
|
+
if (lower.includes("width") || lower.includes("height") || lower.includes("sidebar-width")) return "layout";
|
|
241
|
+
if (value) {
|
|
242
|
+
const trimmed = value.trim();
|
|
243
|
+
if (/^\d{1,3}\s+\d{1,3}%\s+\d{1,3}%$/.test(trimmed)) return "color";
|
|
244
|
+
if (/^(#|rgb|hsl|oklch|lch|lab|color\()/.test(trimmed)) return "color";
|
|
245
|
+
}
|
|
93
246
|
return "other";
|
|
94
247
|
}
|
|
95
248
|
return null;
|
|
@@ -105,9 +258,9 @@ function parseCss(cssContent, filePath) {
|
|
|
105
258
|
visit: "Declaration",
|
|
106
259
|
enter(node) {
|
|
107
260
|
const property = node.property;
|
|
108
|
-
const category = getCategory(property);
|
|
109
|
-
if (!category) return;
|
|
110
261
|
const value = csstree.generate(node.value);
|
|
262
|
+
const category = getCategory(property, value);
|
|
263
|
+
if (!category) return;
|
|
111
264
|
if (!value || ["inherit", "initial", "unset", "revert"].includes(value)) return;
|
|
112
265
|
let selector = ":root";
|
|
113
266
|
let parent = this.atrule ?? this.rule;
|
|
@@ -136,7 +289,7 @@ function extractCssVariables(cssContent, filePath) {
|
|
|
136
289
|
while ((match = varRegex.exec(cssContent)) !== null) {
|
|
137
290
|
const property = `--${match[1]}`;
|
|
138
291
|
const value = match[2].trim();
|
|
139
|
-
const category = getCategory(property) ?? "other";
|
|
292
|
+
const category = getCategory(property, value) ?? "other";
|
|
140
293
|
tokens.push({
|
|
141
294
|
category,
|
|
142
295
|
property,
|
|
@@ -150,7 +303,7 @@ function extractCssVariables(cssContent, filePath) {
|
|
|
150
303
|
}
|
|
151
304
|
|
|
152
305
|
// src/parsers/html-parser.ts
|
|
153
|
-
import * as
|
|
306
|
+
import * as cheerio2 from "cheerio";
|
|
154
307
|
var TRACKED_PROPERTIES = {
|
|
155
308
|
"color": "color",
|
|
156
309
|
"background-color": "color",
|
|
@@ -177,7 +330,12 @@ var TRACKED_PROPERTIES = {
|
|
|
177
330
|
"display": "layout",
|
|
178
331
|
"flex-direction": "layout",
|
|
179
332
|
"justify-content": "layout",
|
|
180
|
-
"align-items": "layout"
|
|
333
|
+
"align-items": "layout",
|
|
334
|
+
// Visual effects
|
|
335
|
+
"backdrop-filter": "other",
|
|
336
|
+
"filter": "other",
|
|
337
|
+
"animation": "other",
|
|
338
|
+
"transition": "other"
|
|
181
339
|
};
|
|
182
340
|
function parseInlineStyle(styleStr) {
|
|
183
341
|
const result = {};
|
|
@@ -195,7 +353,7 @@ function parseInlineStyle(styleStr) {
|
|
|
195
353
|
}
|
|
196
354
|
function parseHtml(htmlContent, filePath) {
|
|
197
355
|
const tokens = [];
|
|
198
|
-
const $ =
|
|
356
|
+
const $ = cheerio2.load(htmlContent);
|
|
199
357
|
const styleBlocks = [];
|
|
200
358
|
$("style").each((_, el) => {
|
|
201
359
|
const text = $(el).text();
|
|
@@ -224,7 +382,7 @@ function parseHtml(htmlContent, filePath) {
|
|
|
224
382
|
return tokens;
|
|
225
383
|
}
|
|
226
384
|
function extractStyleBlocks(htmlContent) {
|
|
227
|
-
const $ =
|
|
385
|
+
const $ = cheerio2.load(htmlContent);
|
|
228
386
|
const blocks = [];
|
|
229
387
|
$("style").each((_, el) => {
|
|
230
388
|
const text = $(el).text();
|
|
@@ -234,6 +392,64 @@ function extractStyleBlocks(htmlContent) {
|
|
|
234
392
|
});
|
|
235
393
|
return blocks;
|
|
236
394
|
}
|
|
395
|
+
function extractTailwindConfig(htmlContent, filePath) {
|
|
396
|
+
const tokens = [];
|
|
397
|
+
const $ = cheerio2.load(htmlContent);
|
|
398
|
+
$("script").each((_, el) => {
|
|
399
|
+
const scriptId = $(el).attr("id") ?? "";
|
|
400
|
+
const text = $(el).text();
|
|
401
|
+
if (!text) return;
|
|
402
|
+
const isTailwindConfig = scriptId.toLowerCase().includes("tailwind") || text.includes("tailwind.config");
|
|
403
|
+
if (!isTailwindConfig) return;
|
|
404
|
+
const colorsMatch = text.match(/colors\s*:\s*\{([^}]+)\}/);
|
|
405
|
+
if (colorsMatch) {
|
|
406
|
+
const colorsBlock = colorsMatch[1];
|
|
407
|
+
const colorRegex = /["']([^"']+)["']\s*:\s*["']([^"']+)["']/g;
|
|
408
|
+
let match;
|
|
409
|
+
while ((match = colorRegex.exec(colorsBlock)) !== null) {
|
|
410
|
+
tokens.push({
|
|
411
|
+
category: "color",
|
|
412
|
+
property: `--tw-${match[1]}`,
|
|
413
|
+
value: match[2],
|
|
414
|
+
selector: "[tailwind.config]",
|
|
415
|
+
file: filePath
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const radiusMatch = text.match(/borderRadius\s*:\s*\{([^}]+)\}/);
|
|
420
|
+
if (radiusMatch) {
|
|
421
|
+
const radiusBlock = radiusMatch[1];
|
|
422
|
+
const radiusRegex = /["']([^"']+)["']\s*:\s*["']([^"']+)["']/g;
|
|
423
|
+
let match;
|
|
424
|
+
while ((match = radiusRegex.exec(radiusBlock)) !== null) {
|
|
425
|
+
tokens.push({
|
|
426
|
+
category: "radius",
|
|
427
|
+
property: `--tw-radius-${match[1]}`,
|
|
428
|
+
value: match[2],
|
|
429
|
+
selector: "[tailwind.config]",
|
|
430
|
+
file: filePath
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const fontMatch = text.match(/fontFamily\s*:\s*\{([^}]+)\}/);
|
|
435
|
+
if (fontMatch) {
|
|
436
|
+
const fontBlock = fontMatch[1];
|
|
437
|
+
const fontRegex = /["']([^"']+)["']\s*:\s*\[([^\]]+)\]/g;
|
|
438
|
+
let match;
|
|
439
|
+
while ((match = fontRegex.exec(fontBlock)) !== null) {
|
|
440
|
+
const familyValues = match[2].split(",").map((v) => v.trim().replace(/["']/g, "")).join(", ");
|
|
441
|
+
tokens.push({
|
|
442
|
+
category: "font",
|
|
443
|
+
property: `--tw-font-${match[1]}`,
|
|
444
|
+
value: familyValues,
|
|
445
|
+
selector: "[tailwind.config]",
|
|
446
|
+
file: filePath
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
return tokens;
|
|
452
|
+
}
|
|
237
453
|
function buildSelectorPath($, element) {
|
|
238
454
|
const parts = [];
|
|
239
455
|
const tagName = element.prop("tagName")?.toLowerCase() ?? "div";
|
|
@@ -292,6 +508,8 @@ async function scanProject(projectRoot, config, stitchHtmlPath) {
|
|
|
292
508
|
allTokens.push(...vars);
|
|
293
509
|
}
|
|
294
510
|
scannedFiles.push(stitchHtmlPath);
|
|
511
|
+
const twTokens = extractTailwindConfig(htmlContent, stitchHtmlPath);
|
|
512
|
+
allTokens.push(...twTokens);
|
|
295
513
|
}
|
|
296
514
|
}
|
|
297
515
|
const cssFiles = await fg(config.cssFiles, {
|
|
@@ -324,6 +542,8 @@ async function scanProject(projectRoot, config, stitchHtmlPath) {
|
|
|
324
542
|
const cssTokens = parseCss(block, file);
|
|
325
543
|
allTokens.push(...cssTokens);
|
|
326
544
|
}
|
|
545
|
+
const twTokens = extractTailwindConfig(content, file);
|
|
546
|
+
allTokens.push(...twTokens);
|
|
327
547
|
scannedFiles.push(file);
|
|
328
548
|
}
|
|
329
549
|
const filtered = allTokens.filter(
|
|
@@ -356,13 +576,38 @@ function buildSummary(tokens) {
|
|
|
356
576
|
async function createSnapshot(projectRoot, stitchHtmlPath) {
|
|
357
577
|
const config = loadConfig(projectRoot);
|
|
358
578
|
const { tokens, files } = await scanProject(projectRoot, config, stitchHtmlPath);
|
|
579
|
+
let structure;
|
|
580
|
+
try {
|
|
581
|
+
let htmlForStructure = null;
|
|
582
|
+
if (stitchHtmlPath) {
|
|
583
|
+
const absPath = path.resolve(projectRoot, stitchHtmlPath);
|
|
584
|
+
if (fs.existsSync(absPath)) {
|
|
585
|
+
htmlForStructure = fs.readFileSync(absPath, "utf-8");
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
for (const file of files) {
|
|
589
|
+
if (file.endsWith(".html")) {
|
|
590
|
+
const absPath = path.join(projectRoot, file);
|
|
591
|
+
if (fs.existsSync(absPath)) {
|
|
592
|
+
htmlForStructure = fs.readFileSync(absPath, "utf-8");
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (htmlForStructure) {
|
|
599
|
+
structure = computeStructureFingerprint(htmlForStructure);
|
|
600
|
+
}
|
|
601
|
+
} catch {
|
|
602
|
+
}
|
|
359
603
|
const snapshot = {
|
|
360
604
|
version: "1.0.0",
|
|
361
605
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
362
606
|
projectRoot,
|
|
363
607
|
sourceFiles: files,
|
|
364
608
|
tokens,
|
|
365
|
-
summary: buildSummary(tokens)
|
|
609
|
+
summary: buildSummary(tokens),
|
|
610
|
+
structure
|
|
366
611
|
};
|
|
367
612
|
return snapshot;
|
|
368
613
|
}
|
|
@@ -430,6 +675,37 @@ async function detectDrift(projectRoot, snapshot, threshold = 10) {
|
|
|
430
675
|
driftPercent: total > 0 ? Math.round(changed / total * 100 * 100) / 100 : 0
|
|
431
676
|
};
|
|
432
677
|
}
|
|
678
|
+
let structureDrift;
|
|
679
|
+
if (snapshot.structure) {
|
|
680
|
+
try {
|
|
681
|
+
const config2 = loadConfig(projectRoot);
|
|
682
|
+
const fg2 = (await import("fast-glob")).default;
|
|
683
|
+
const htmlFiles = await fg2(config2.htmlFiles, {
|
|
684
|
+
cwd: projectRoot,
|
|
685
|
+
ignore: config2.ignore,
|
|
686
|
+
absolute: false
|
|
687
|
+
});
|
|
688
|
+
let htmlContent = null;
|
|
689
|
+
const fs3 = await import("fs");
|
|
690
|
+
const path3 = await import("path");
|
|
691
|
+
for (const file of htmlFiles) {
|
|
692
|
+
const absPath = path3.join(projectRoot, file);
|
|
693
|
+
if (fs3.existsSync(absPath)) {
|
|
694
|
+
htmlContent = fs3.readFileSync(absPath, "utf-8");
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (htmlContent) {
|
|
699
|
+
const currentStructure = computeStructureFingerprint(htmlContent);
|
|
700
|
+
const details = compareStructure(snapshot.structure, currentStructure);
|
|
701
|
+
structureDrift = {
|
|
702
|
+
changed: details.length > 0,
|
|
703
|
+
details
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
}
|
|
433
709
|
return {
|
|
434
710
|
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
435
711
|
snapshotCreatedAt: snapshot.createdAt,
|
|
@@ -439,7 +715,8 @@ async function detectDrift(projectRoot, snapshot, threshold = 10) {
|
|
|
439
715
|
threshold,
|
|
440
716
|
passed: driftScore <= threshold,
|
|
441
717
|
items: driftItems,
|
|
442
|
-
categorySummary
|
|
718
|
+
categorySummary,
|
|
719
|
+
structureDrift
|
|
443
720
|
};
|
|
444
721
|
}
|
|
445
722
|
|
|
@@ -612,7 +889,235 @@ ${buildTokenList(snapshot)}
|
|
|
612
889
|
`;
|
|
613
890
|
}
|
|
614
891
|
|
|
892
|
+
// src/core/sync.ts
|
|
893
|
+
var CATEGORY_LABELS = {
|
|
894
|
+
color: "color",
|
|
895
|
+
font: "font",
|
|
896
|
+
spacing: "spacing",
|
|
897
|
+
shadow: "shadow",
|
|
898
|
+
radius: "border-radius",
|
|
899
|
+
layout: "layout",
|
|
900
|
+
other: "style"
|
|
901
|
+
};
|
|
902
|
+
function isDesignTokenProperty(property) {
|
|
903
|
+
if (property.startsWith("--")) return true;
|
|
904
|
+
if (property === "font-family") return true;
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
function normalizeTokenKey(property) {
|
|
908
|
+
return property.replace(/^--tw-/, "--").replace(/^--tw-font-/, "--font-").toLowerCase();
|
|
909
|
+
}
|
|
910
|
+
function driftItemsToSyncChanges(items) {
|
|
911
|
+
return items.map((item) => {
|
|
912
|
+
if (item.changeType === "deleted") {
|
|
913
|
+
return {
|
|
914
|
+
category: item.original.category,
|
|
915
|
+
property: item.original.property,
|
|
916
|
+
fromValue: item.original.value,
|
|
917
|
+
toValue: "",
|
|
918
|
+
action: "remove"
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
if (item.changeType === "added") {
|
|
922
|
+
return {
|
|
923
|
+
category: (item.current ?? item.original).category,
|
|
924
|
+
property: (item.current ?? item.original).property,
|
|
925
|
+
fromValue: "",
|
|
926
|
+
toValue: (item.current ?? item.original).value,
|
|
927
|
+
action: "add"
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
category: item.original.category,
|
|
932
|
+
property: item.original.property,
|
|
933
|
+
fromValue: item.original.value,
|
|
934
|
+
toValue: item.current?.value ?? "",
|
|
935
|
+
action: "update"
|
|
936
|
+
};
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
function generateSyncPrompt(changes) {
|
|
940
|
+
if (changes.length === 0) {
|
|
941
|
+
return "";
|
|
942
|
+
}
|
|
943
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
944
|
+
for (const change of changes) {
|
|
945
|
+
const existing = grouped.get(change.category) ?? [];
|
|
946
|
+
existing.push(change);
|
|
947
|
+
grouped.set(change.category, existing);
|
|
948
|
+
}
|
|
949
|
+
const lines = [
|
|
950
|
+
"Update the following design tokens to match the latest code changes:",
|
|
951
|
+
""
|
|
952
|
+
];
|
|
953
|
+
for (const [category, categoryChanges] of grouped) {
|
|
954
|
+
const label = CATEGORY_LABELS[category];
|
|
955
|
+
for (const change of categoryChanges) {
|
|
956
|
+
const propName = cleanPropertyName(change.property);
|
|
957
|
+
if (change.action === "update") {
|
|
958
|
+
lines.push(
|
|
959
|
+
`- Change ${label} '${propName}' from ${change.fromValue} to ${change.toValue}`
|
|
960
|
+
);
|
|
961
|
+
} else if (change.action === "add") {
|
|
962
|
+
lines.push(
|
|
963
|
+
`- Add new ${label} '${propName}' with value ${change.toValue}`
|
|
964
|
+
);
|
|
965
|
+
} else if (change.action === "remove") {
|
|
966
|
+
lines.push(
|
|
967
|
+
`- Remove ${label} '${propName}' (was ${change.fromValue})`
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
lines.push("");
|
|
973
|
+
lines.push(
|
|
974
|
+
"Keep all other design elements unchanged. Only modify the specified tokens."
|
|
975
|
+
);
|
|
976
|
+
return lines.join("\n");
|
|
977
|
+
}
|
|
978
|
+
function cleanPropertyName(property) {
|
|
979
|
+
return property.replace(/^--tw-/, "").replace(/^--tw-radius-/, "").replace(/^--tw-font-/, "").replace(/^--/, "");
|
|
980
|
+
}
|
|
981
|
+
function syncToStitch(driftItems) {
|
|
982
|
+
const changes = driftItemsToSyncChanges(driftItems);
|
|
983
|
+
const prompt = generateSyncPrompt(changes);
|
|
984
|
+
return {
|
|
985
|
+
direction: "to-stitch",
|
|
986
|
+
changes,
|
|
987
|
+
prompt: prompt || void 0,
|
|
988
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
function syncFromStitch(stitchTokens, snapshotTokens) {
|
|
992
|
+
const changes = [];
|
|
993
|
+
const stitchDesignTokens = stitchTokens.filter((t) => isDesignTokenProperty(t.property));
|
|
994
|
+
const snapshotDesignTokens = snapshotTokens.filter((t) => isDesignTokenProperty(t.property));
|
|
995
|
+
const snapshotMap = /* @__PURE__ */ new Map();
|
|
996
|
+
for (const token of snapshotDesignTokens) {
|
|
997
|
+
const key = normalizeTokenKey(token.property);
|
|
998
|
+
if (!snapshotMap.has(key)) {
|
|
999
|
+
snapshotMap.set(key, token);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const stitchMap = /* @__PURE__ */ new Map();
|
|
1003
|
+
for (const token of stitchDesignTokens) {
|
|
1004
|
+
const key = normalizeTokenKey(token.property);
|
|
1005
|
+
if (!stitchMap.has(key)) {
|
|
1006
|
+
stitchMap.set(key, token);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
for (const [key, stitchToken] of stitchMap) {
|
|
1010
|
+
const snapshotToken = snapshotMap.get(key);
|
|
1011
|
+
if (!snapshotToken) {
|
|
1012
|
+
changes.push({
|
|
1013
|
+
category: stitchToken.category,
|
|
1014
|
+
property: stitchToken.property,
|
|
1015
|
+
fromValue: "",
|
|
1016
|
+
toValue: stitchToken.value,
|
|
1017
|
+
action: "add"
|
|
1018
|
+
});
|
|
1019
|
+
} else if (stitchToken.value !== snapshotToken.value) {
|
|
1020
|
+
changes.push({
|
|
1021
|
+
category: stitchToken.category,
|
|
1022
|
+
property: snapshotToken.property,
|
|
1023
|
+
fromValue: snapshotToken.value,
|
|
1024
|
+
toValue: stitchToken.value,
|
|
1025
|
+
action: "update"
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (stitchDesignTokens.length > 0) {
|
|
1030
|
+
for (const [key, snapshotToken] of snapshotMap) {
|
|
1031
|
+
const isStitchOrigin = snapshotToken.property.startsWith("--tw-") || snapshotToken.selector === "[tailwind.config]";
|
|
1032
|
+
if (isStitchOrigin && !stitchMap.has(key)) {
|
|
1033
|
+
changes.push({
|
|
1034
|
+
category: snapshotToken.category,
|
|
1035
|
+
property: snapshotToken.property,
|
|
1036
|
+
fromValue: snapshotToken.value,
|
|
1037
|
+
toValue: "",
|
|
1038
|
+
action: "remove"
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const patchFile = generateCssPatch(changes);
|
|
1044
|
+
return {
|
|
1045
|
+
direction: "to-code",
|
|
1046
|
+
changes,
|
|
1047
|
+
patchFile: patchFile || void 0,
|
|
1048
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function applySyncChanges(changes, cssFiles) {
|
|
1052
|
+
const modifiedFiles = /* @__PURE__ */ new Map();
|
|
1053
|
+
let appliedCount = 0;
|
|
1054
|
+
const updateChanges = changes.filter((c) => c.action === "update");
|
|
1055
|
+
for (const [filename, content] of cssFiles) {
|
|
1056
|
+
let modified = content;
|
|
1057
|
+
let fileChanged = false;
|
|
1058
|
+
for (const change of updateChanges) {
|
|
1059
|
+
const propName = change.property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1060
|
+
const escapedFrom = change.fromValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1061
|
+
const cssRegex = new RegExp(
|
|
1062
|
+
`(${propName}\\s*:\\s*)${escapedFrom}(\\s*[;\\n])`,
|
|
1063
|
+
"g"
|
|
1064
|
+
);
|
|
1065
|
+
let newContent = modified.replace(cssRegex, `$1${change.toValue}$2`);
|
|
1066
|
+
if (newContent === modified) {
|
|
1067
|
+
const twResult = applySyncToHtml(modified, change);
|
|
1068
|
+
if (twResult !== modified) {
|
|
1069
|
+
newContent = twResult;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (newContent !== modified) {
|
|
1073
|
+
modified = newContent;
|
|
1074
|
+
fileChanged = true;
|
|
1075
|
+
appliedCount++;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (fileChanged) {
|
|
1079
|
+
modifiedFiles.set(filename, modified);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return { modifiedFiles, appliedCount };
|
|
1083
|
+
}
|
|
1084
|
+
function applySyncToHtml(html, change) {
|
|
1085
|
+
const tokenName = change.property.replace(/^--tw-/, "").replace(/^--/, "");
|
|
1086
|
+
const escapedFrom = change.fromValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1087
|
+
const regex = new RegExp(
|
|
1088
|
+
`("${tokenName}"\\s*:\\s*)["']${escapedFrom}["']`,
|
|
1089
|
+
"g"
|
|
1090
|
+
);
|
|
1091
|
+
return html.replace(regex, `$1"${change.toValue}"`);
|
|
1092
|
+
}
|
|
1093
|
+
function generateCssPatch(changes) {
|
|
1094
|
+
if (changes.length === 0) return "";
|
|
1095
|
+
const lines = [
|
|
1096
|
+
"/* drift-guard sync patch \u2014 apply these changes to your CSS */",
|
|
1097
|
+
"/* Generated by: drift-guard sync --direction to-code */",
|
|
1098
|
+
"",
|
|
1099
|
+
":root {"
|
|
1100
|
+
];
|
|
1101
|
+
for (const change of changes) {
|
|
1102
|
+
if (change.action === "remove") {
|
|
1103
|
+
lines.push(` /* REMOVED: ${change.property}: ${change.fromValue}; */`);
|
|
1104
|
+
} else {
|
|
1105
|
+
const comment = change.action === "update" ? ` /* was: ${change.fromValue} */` : " /* NEW */";
|
|
1106
|
+
lines.push(` ${change.property}: ${change.toValue};${comment}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
lines.push("}");
|
|
1110
|
+
return lines.join("\n");
|
|
1111
|
+
}
|
|
1112
|
+
|
|
615
1113
|
export {
|
|
1114
|
+
parseCss,
|
|
1115
|
+
extractCssVariables,
|
|
1116
|
+
parseHtml,
|
|
1117
|
+
extractStyleBlocks,
|
|
1118
|
+
extractTailwindConfig,
|
|
1119
|
+
computeStructureFingerprint,
|
|
1120
|
+
compareStructure,
|
|
616
1121
|
DEFAULT_CONFIG,
|
|
617
1122
|
loadConfig,
|
|
618
1123
|
saveConfig,
|
|
@@ -622,6 +1127,11 @@ export {
|
|
|
622
1127
|
loadSnapshot,
|
|
623
1128
|
detectDrift,
|
|
624
1129
|
generateRules,
|
|
625
|
-
saveRules
|
|
1130
|
+
saveRules,
|
|
1131
|
+
driftItemsToSyncChanges,
|
|
1132
|
+
generateSyncPrompt,
|
|
1133
|
+
syncToStitch,
|
|
1134
|
+
syncFromStitch,
|
|
1135
|
+
applySyncChanges
|
|
626
1136
|
};
|
|
627
|
-
//# sourceMappingURL=chunk-
|
|
1137
|
+
//# sourceMappingURL=chunk-HI6H6PCS.js.map
|