@designtools/codesurface 0.1.1
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 +105 -0
- package/dist/cli.js +3360 -0
- package/dist/client/assets/index-B5g8qgFW.css +1 -0
- package/dist/client/assets/index-BB9cuIlF.js +111 -0
- package/dist/client/assets/index-BH-ji2K7.js +150 -0
- package/dist/client/assets/index-BH4MtIeE.js +150 -0
- package/dist/client/assets/index-BRJOTqhn.js +130 -0
- package/dist/client/assets/index-Bb8QmYLf.css +1 -0
- package/dist/client/assets/index-BdWkGSfZ.js +130 -0
- package/dist/client/assets/index-BnRqJ-Bb.js +150 -0
- package/dist/client/assets/index-BneJISnX.js +150 -0
- package/dist/client/assets/index-C9_QxP1g.css +1 -0
- package/dist/client/assets/index-CBjuhHfq.js +150 -0
- package/dist/client/assets/index-CEFmdeB7.css +1 -0
- package/dist/client/assets/index-CQpCDasO.js +131 -0
- package/dist/client/assets/index-CRUCeIYi.css +1 -0
- package/dist/client/assets/index-CUSGVUIj.js +150 -0
- package/dist/client/assets/index-CdshIoQY.css +1 -0
- package/dist/client/assets/index-Cne4pc0S.css +1 -0
- package/dist/client/assets/index-Cps9lJoI.css +1 -0
- package/dist/client/assets/index-Cs7mKzsY.css +1 -0
- package/dist/client/assets/index-D11nArXR.css +1 -0
- package/dist/client/assets/index-D2DhQd-K.js +51 -0
- package/dist/client/assets/index-DG2JRz0f.js +150 -0
- package/dist/client/assets/index-DO-UYUx0.js +150 -0
- package/dist/client/assets/index-DOb58Ii1.js +111 -0
- package/dist/client/assets/index-DVd7DS_W.css +1 -0
- package/dist/client/assets/index-DZNHUm4M.js +130 -0
- package/dist/client/assets/index-Di7zgczR.css +1 -0
- package/dist/client/assets/index-DlUCyHdA.css +1 -0
- package/dist/client/assets/index-FTflg87d.js +130 -0
- package/dist/client/assets/index-U5_kmGxX.js +150 -0
- package/dist/client/assets/index-_4EXPfyt.css +1 -0
- package/dist/client/assets/index-gow3pju2.css +1 -0
- package/dist/client/index.html +13 -0
- package/package.json +50 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import fs18 from "fs";
|
|
5
|
+
import path16 from "path";
|
|
6
|
+
import process2 from "process";
|
|
7
|
+
import readline from "readline";
|
|
8
|
+
import open from "open";
|
|
9
|
+
|
|
10
|
+
// src/server/lib/detect-framework.ts
|
|
11
|
+
import fs from "fs/promises";
|
|
12
|
+
import path from "path";
|
|
13
|
+
async function detectFramework(projectRoot) {
|
|
14
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
15
|
+
let pkg = {};
|
|
16
|
+
try {
|
|
17
|
+
pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
const deps = {
|
|
21
|
+
...pkg.dependencies,
|
|
22
|
+
...pkg.devDependencies
|
|
23
|
+
};
|
|
24
|
+
let name = "unknown";
|
|
25
|
+
let appDirCandidates;
|
|
26
|
+
let componentDirCandidates;
|
|
27
|
+
if (deps.next) {
|
|
28
|
+
name = "nextjs";
|
|
29
|
+
appDirCandidates = ["app", "src/app"];
|
|
30
|
+
componentDirCandidates = ["components/ui", "src/components/ui"];
|
|
31
|
+
} else if (deps["@remix-run/react"] || deps["@remix-run/node"]) {
|
|
32
|
+
name = "remix";
|
|
33
|
+
appDirCandidates = ["app/routes", "src/routes"];
|
|
34
|
+
componentDirCandidates = ["components/ui", "app/components/ui", "src/components/ui"];
|
|
35
|
+
} else if (deps.vite) {
|
|
36
|
+
name = "vite";
|
|
37
|
+
appDirCandidates = ["src/pages", "src/routes", "src", "pages"];
|
|
38
|
+
componentDirCandidates = ["components/ui", "src/components/ui"];
|
|
39
|
+
} else {
|
|
40
|
+
appDirCandidates = ["app", "src", "pages"];
|
|
41
|
+
componentDirCandidates = ["components/ui", "src/components/ui"];
|
|
42
|
+
}
|
|
43
|
+
const appResult = await findDir(projectRoot, appDirCandidates);
|
|
44
|
+
const componentResult = await findDir(projectRoot, componentDirCandidates);
|
|
45
|
+
const componentFileCount = componentResult.exists ? await countFiles(projectRoot, componentResult.dir, ".tsx") : 0;
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
appDir: appResult.dir,
|
|
49
|
+
appDirExists: appResult.exists,
|
|
50
|
+
componentDir: componentResult.dir,
|
|
51
|
+
componentDirExists: componentResult.exists,
|
|
52
|
+
componentFileCount,
|
|
53
|
+
cssFiles: await findCssFiles(projectRoot)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function findDir(root, candidates) {
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
const full = path.join(root, candidate);
|
|
59
|
+
try {
|
|
60
|
+
const stat = await fs.stat(full);
|
|
61
|
+
if (stat.isDirectory()) return { dir: candidate, exists: true };
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { dir: candidates[0], exists: false };
|
|
66
|
+
}
|
|
67
|
+
async function countFiles(root, dir, ext) {
|
|
68
|
+
const full = path.join(root, dir);
|
|
69
|
+
try {
|
|
70
|
+
const entries = await fs.readdir(full);
|
|
71
|
+
return entries.filter((e) => e.endsWith(ext)).length;
|
|
72
|
+
} catch {
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function findCssFiles(projectRoot) {
|
|
77
|
+
const candidates = [
|
|
78
|
+
"app/globals.css",
|
|
79
|
+
"src/app/globals.css",
|
|
80
|
+
"app/global.css",
|
|
81
|
+
"src/globals.css",
|
|
82
|
+
"src/index.css",
|
|
83
|
+
"src/app.css",
|
|
84
|
+
"styles/globals.css"
|
|
85
|
+
];
|
|
86
|
+
const found = [];
|
|
87
|
+
for (const candidate of candidates) {
|
|
88
|
+
try {
|
|
89
|
+
await fs.access(path.join(projectRoot, candidate));
|
|
90
|
+
found.push(candidate);
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/server/lib/detect-styling.ts
|
|
98
|
+
import fs2 from "fs/promises";
|
|
99
|
+
import path2 from "path";
|
|
100
|
+
async function detectStylingSystem(projectRoot, framework) {
|
|
101
|
+
const pkgPath = path2.join(projectRoot, "package.json");
|
|
102
|
+
let pkg = {};
|
|
103
|
+
try {
|
|
104
|
+
pkg = JSON.parse(await fs2.readFile(pkgPath, "utf-8"));
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
108
|
+
if (deps.tailwindcss) {
|
|
109
|
+
const version = deps.tailwindcss;
|
|
110
|
+
const isV4 = version.startsWith("^4") || version.startsWith("~4") || version.startsWith("4");
|
|
111
|
+
if (isV4) {
|
|
112
|
+
const hasDarkMode3 = await checkDarkMode(projectRoot, framework.cssFiles);
|
|
113
|
+
return {
|
|
114
|
+
type: "tailwind-v4",
|
|
115
|
+
cssFiles: framework.cssFiles,
|
|
116
|
+
scssFiles: [],
|
|
117
|
+
hasDarkMode: hasDarkMode3
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const configCandidates = [
|
|
121
|
+
"tailwind.config.ts",
|
|
122
|
+
"tailwind.config.js",
|
|
123
|
+
"tailwind.config.mjs",
|
|
124
|
+
"tailwind.config.cjs"
|
|
125
|
+
];
|
|
126
|
+
let configPath;
|
|
127
|
+
for (const candidate of configCandidates) {
|
|
128
|
+
try {
|
|
129
|
+
await fs2.access(path2.join(projectRoot, candidate));
|
|
130
|
+
configPath = candidate;
|
|
131
|
+
break;
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const hasDarkMode2 = await checkDarkMode(projectRoot, framework.cssFiles);
|
|
136
|
+
return {
|
|
137
|
+
type: "tailwind-v3",
|
|
138
|
+
configPath,
|
|
139
|
+
cssFiles: framework.cssFiles,
|
|
140
|
+
scssFiles: [],
|
|
141
|
+
hasDarkMode: hasDarkMode2
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (deps.bootstrap) {
|
|
145
|
+
const hasDarkMode2 = await checkDarkMode(projectRoot, framework.cssFiles);
|
|
146
|
+
const scssFiles = await findBootstrapScssFiles(projectRoot);
|
|
147
|
+
return {
|
|
148
|
+
type: "bootstrap",
|
|
149
|
+
cssFiles: framework.cssFiles,
|
|
150
|
+
scssFiles,
|
|
151
|
+
hasDarkMode: hasDarkMode2
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const hasDarkMode = await checkDarkMode(projectRoot, framework.cssFiles);
|
|
155
|
+
const hasCustomProps = await checkCustomProperties(projectRoot, framework.cssFiles);
|
|
156
|
+
if (hasCustomProps) {
|
|
157
|
+
return {
|
|
158
|
+
type: "css-variables",
|
|
159
|
+
cssFiles: framework.cssFiles,
|
|
160
|
+
scssFiles: [],
|
|
161
|
+
hasDarkMode
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
type: framework.cssFiles.length > 0 ? "plain-css" : "unknown",
|
|
166
|
+
cssFiles: framework.cssFiles,
|
|
167
|
+
scssFiles: [],
|
|
168
|
+
hasDarkMode
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async function checkDarkMode(projectRoot, cssFiles) {
|
|
172
|
+
for (const file of cssFiles) {
|
|
173
|
+
try {
|
|
174
|
+
const css = await fs2.readFile(path2.join(projectRoot, file), "utf-8");
|
|
175
|
+
if (css.includes(".dark") || css.includes('[data-theme="dark"]') || css.includes("prefers-color-scheme: dark")) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
async function checkCustomProperties(projectRoot, cssFiles) {
|
|
184
|
+
for (const file of cssFiles) {
|
|
185
|
+
try {
|
|
186
|
+
const css = await fs2.readFile(path2.join(projectRoot, file), "utf-8");
|
|
187
|
+
if (/--[\w-]+\s*:/.test(css)) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
async function findBootstrapScssFiles(projectRoot) {
|
|
196
|
+
const candidates = [
|
|
197
|
+
"src/scss/_variables.scss",
|
|
198
|
+
"src/scss/_custom.scss",
|
|
199
|
+
"src/scss/custom.scss",
|
|
200
|
+
"src/styles/_variables.scss",
|
|
201
|
+
"src/styles/variables.scss",
|
|
202
|
+
"assets/scss/_variables.scss",
|
|
203
|
+
"scss/_variables.scss",
|
|
204
|
+
"styles/_variables.scss"
|
|
205
|
+
];
|
|
206
|
+
const found = [];
|
|
207
|
+
for (const candidate of candidates) {
|
|
208
|
+
try {
|
|
209
|
+
await fs2.access(path2.join(projectRoot, candidate));
|
|
210
|
+
found.push(candidate);
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return found;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/server/index.ts
|
|
218
|
+
import express from "express";
|
|
219
|
+
import path15 from "path";
|
|
220
|
+
import fs17 from "fs";
|
|
221
|
+
import { fileURLToPath } from "url";
|
|
222
|
+
import { exec } from "child_process";
|
|
223
|
+
|
|
224
|
+
// src/server/api/write-element.ts
|
|
225
|
+
import { Router } from "express";
|
|
226
|
+
import fs3 from "fs/promises";
|
|
227
|
+
|
|
228
|
+
// src/server/lib/safe-path.ts
|
|
229
|
+
import path3 from "path";
|
|
230
|
+
function safePath(projectRoot, filePath) {
|
|
231
|
+
if (!filePath || typeof filePath !== "string") {
|
|
232
|
+
throw new Error("File path is required");
|
|
233
|
+
}
|
|
234
|
+
if (path3.isAbsolute(filePath)) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Absolute paths are not allowed: "${filePath}". Paths must be relative to the project root.`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const resolvedRoot = path3.resolve(projectRoot);
|
|
240
|
+
const resolvedPath = path3.resolve(resolvedRoot, filePath);
|
|
241
|
+
if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(resolvedRoot + path3.sep)) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Path "${filePath}" resolves outside the project directory. Refusing to write.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return resolvedPath;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/server/api/write-element.ts
|
|
250
|
+
import { builders as b2 } from "ast-types";
|
|
251
|
+
|
|
252
|
+
// src/server/lib/ast-helpers.ts
|
|
253
|
+
import recast from "recast";
|
|
254
|
+
import { namedTypes as n, builders as b } from "ast-types";
|
|
255
|
+
var _parser = null;
|
|
256
|
+
async function getParser() {
|
|
257
|
+
if (!_parser) {
|
|
258
|
+
_parser = await import("recast/parsers/babel-ts");
|
|
259
|
+
}
|
|
260
|
+
return _parser;
|
|
261
|
+
}
|
|
262
|
+
function parseSource(source, parser) {
|
|
263
|
+
return recast.parse(source, { parser: parser || getParser() });
|
|
264
|
+
}
|
|
265
|
+
function printSource(ast) {
|
|
266
|
+
return recast.print(ast).code;
|
|
267
|
+
}
|
|
268
|
+
function findAttr(openingElement, name) {
|
|
269
|
+
for (const attr of openingElement.attributes || []) {
|
|
270
|
+
if (n.JSXAttribute.check(attr) && n.JSXIdentifier.check(attr.name) && attr.name.name === name) {
|
|
271
|
+
return attr;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
function classBoundaryRegex(cls, flags = "") {
|
|
277
|
+
const escaped = cls.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
278
|
+
return new RegExp(`(?<=^|[\\s"'\`])${escaped}(?=$|[\\s"'\`])`, flags);
|
|
279
|
+
}
|
|
280
|
+
function replaceClassInAttr(openingElement, oldClass, newClass) {
|
|
281
|
+
const attr = findAttr(openingElement, "className");
|
|
282
|
+
if (!attr) return false;
|
|
283
|
+
if (n.StringLiteral.check(attr.value) || n.Literal.check(attr.value)) {
|
|
284
|
+
const val = attr.value.value;
|
|
285
|
+
const regex = classBoundaryRegex(oldClass);
|
|
286
|
+
if (regex.test(val)) {
|
|
287
|
+
attr.value = b.stringLiteral(val.replace(classBoundaryRegex(oldClass, "g"), newClass));
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
if (n.JSXExpressionContainer.check(attr.value)) {
|
|
293
|
+
return replaceClassInExpression(attr.value.expression, oldClass, newClass);
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
function replaceClassInExpression(expr, oldClass, newClass) {
|
|
298
|
+
if (n.StringLiteral.check(expr) || n.Literal.check(expr)) {
|
|
299
|
+
if (typeof expr.value === "string") {
|
|
300
|
+
const regex = classBoundaryRegex(oldClass);
|
|
301
|
+
if (regex.test(expr.value)) {
|
|
302
|
+
expr.value = expr.value.replace(classBoundaryRegex(oldClass, "g"), newClass);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (n.TemplateLiteral.check(expr)) {
|
|
309
|
+
for (const quasi of expr.quasis) {
|
|
310
|
+
const regex = classBoundaryRegex(oldClass);
|
|
311
|
+
if (regex.test(quasi.value.raw)) {
|
|
312
|
+
quasi.value = {
|
|
313
|
+
raw: quasi.value.raw.replace(classBoundaryRegex(oldClass, "g"), newClass),
|
|
314
|
+
cooked: (quasi.value.cooked || quasi.value.raw).replace(classBoundaryRegex(oldClass, "g"), newClass)
|
|
315
|
+
};
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
if (n.CallExpression.check(expr)) {
|
|
322
|
+
for (const arg of expr.arguments) {
|
|
323
|
+
if (replaceClassInExpression(arg, oldClass, newClass)) return true;
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (n.ConditionalExpression.check(expr)) {
|
|
328
|
+
if (replaceClassInExpression(expr.consequent, oldClass, newClass)) return true;
|
|
329
|
+
if (replaceClassInExpression(expr.alternate, oldClass, newClass)) return true;
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (n.LogicalExpression.check(expr)) {
|
|
333
|
+
if (replaceClassInExpression(expr.left, oldClass, newClass)) return true;
|
|
334
|
+
if (replaceClassInExpression(expr.right, oldClass, newClass)) return true;
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
if (n.ArrayExpression.check(expr)) {
|
|
338
|
+
for (const el of expr.elements) {
|
|
339
|
+
if (el && replaceClassInExpression(el, oldClass, newClass)) return true;
|
|
340
|
+
}
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
function appendClassToAttr(openingElement, newClass) {
|
|
346
|
+
const attr = findAttr(openingElement, "className");
|
|
347
|
+
if (!attr) return false;
|
|
348
|
+
if (n.StringLiteral.check(attr.value) || n.Literal.check(attr.value)) {
|
|
349
|
+
const val = attr.value.value;
|
|
350
|
+
attr.value = b.stringLiteral(val + " " + newClass);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
if (n.JSXExpressionContainer.check(attr.value)) {
|
|
354
|
+
return appendClassToExpression(attr.value.expression, newClass);
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
function appendClassToExpression(expr, newClass) {
|
|
359
|
+
if (n.StringLiteral.check(expr) || n.Literal.check(expr)) {
|
|
360
|
+
if (typeof expr.value === "string") {
|
|
361
|
+
expr.value = expr.value + " " + newClass;
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
if (n.TemplateLiteral.check(expr)) {
|
|
367
|
+
const last = expr.quasis[expr.quasis.length - 1];
|
|
368
|
+
if (last) {
|
|
369
|
+
last.value = {
|
|
370
|
+
raw: last.value.raw + " " + newClass,
|
|
371
|
+
cooked: (last.value.cooked || last.value.raw) + " " + newClass
|
|
372
|
+
};
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
if (n.CallExpression.check(expr)) {
|
|
378
|
+
for (const arg of expr.arguments) {
|
|
379
|
+
if ((n.StringLiteral.check(arg) || n.Literal.check(arg)) && typeof arg.value === "string") {
|
|
380
|
+
arg.value = arg.value + " " + newClass;
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
function addClassNameAttr(openingElement, className) {
|
|
389
|
+
openingElement.attributes.push(
|
|
390
|
+
b.jsxAttribute(
|
|
391
|
+
b.jsxIdentifier("className"),
|
|
392
|
+
b.stringLiteral(className)
|
|
393
|
+
)
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/server/lib/find-element.ts
|
|
398
|
+
import { visit } from "recast";
|
|
399
|
+
import { namedTypes as n2 } from "ast-types";
|
|
400
|
+
function findElementAtSource(ast, line, col) {
|
|
401
|
+
let found = null;
|
|
402
|
+
visit(ast, {
|
|
403
|
+
visitJSXOpeningElement(path17) {
|
|
404
|
+
const loc = path17.node.loc;
|
|
405
|
+
if (loc && loc.start.line === line && loc.start.column === col) {
|
|
406
|
+
found = path17;
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
this.traverse(path17);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
return found;
|
|
413
|
+
}
|
|
414
|
+
function findComponentAtSource(ast, componentName, line, col) {
|
|
415
|
+
let found = null;
|
|
416
|
+
visit(ast, {
|
|
417
|
+
visitJSXOpeningElement(path17) {
|
|
418
|
+
const name = path17.node.name;
|
|
419
|
+
if (n2.JSXIdentifier.check(name) && name.name === componentName) {
|
|
420
|
+
const loc = path17.node.loc;
|
|
421
|
+
if (loc && loc.start.line === line && loc.start.column === col) {
|
|
422
|
+
found = path17;
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
this.traverse(path17);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
return found;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/shared/tailwind-map.ts
|
|
433
|
+
var REVERSE_MAP = {
|
|
434
|
+
// Font size
|
|
435
|
+
"font-size": {
|
|
436
|
+
"12px": "text-xs",
|
|
437
|
+
"0.75rem": "text-xs",
|
|
438
|
+
"14px": "text-sm",
|
|
439
|
+
"0.875rem": "text-sm",
|
|
440
|
+
"16px": "text-base",
|
|
441
|
+
"1rem": "text-base",
|
|
442
|
+
"18px": "text-lg",
|
|
443
|
+
"1.125rem": "text-lg",
|
|
444
|
+
"20px": "text-xl",
|
|
445
|
+
"1.25rem": "text-xl",
|
|
446
|
+
"24px": "text-2xl",
|
|
447
|
+
"1.5rem": "text-2xl",
|
|
448
|
+
"30px": "text-3xl",
|
|
449
|
+
"1.875rem": "text-3xl",
|
|
450
|
+
"36px": "text-4xl",
|
|
451
|
+
"2.25rem": "text-4xl",
|
|
452
|
+
"48px": "text-5xl",
|
|
453
|
+
"3rem": "text-5xl",
|
|
454
|
+
"60px": "text-6xl",
|
|
455
|
+
"3.75rem": "text-6xl",
|
|
456
|
+
"72px": "text-7xl",
|
|
457
|
+
"4.5rem": "text-7xl",
|
|
458
|
+
"96px": "text-8xl",
|
|
459
|
+
"6rem": "text-8xl",
|
|
460
|
+
"128px": "text-9xl",
|
|
461
|
+
"8rem": "text-9xl"
|
|
462
|
+
},
|
|
463
|
+
// Font weight
|
|
464
|
+
"font-weight": {
|
|
465
|
+
"100": "font-thin",
|
|
466
|
+
"200": "font-extralight",
|
|
467
|
+
"300": "font-light",
|
|
468
|
+
"400": "font-normal",
|
|
469
|
+
"500": "font-medium",
|
|
470
|
+
"600": "font-semibold",
|
|
471
|
+
"700": "font-bold",
|
|
472
|
+
"800": "font-extrabold",
|
|
473
|
+
"900": "font-black"
|
|
474
|
+
},
|
|
475
|
+
// Line height
|
|
476
|
+
"line-height": {
|
|
477
|
+
"1": "leading-none",
|
|
478
|
+
"1.25": "leading-tight",
|
|
479
|
+
"1.375": "leading-snug",
|
|
480
|
+
"1.5": "leading-normal",
|
|
481
|
+
"1.625": "leading-relaxed",
|
|
482
|
+
"2": "leading-loose"
|
|
483
|
+
},
|
|
484
|
+
// Letter spacing
|
|
485
|
+
"letter-spacing": {
|
|
486
|
+
"-0.05em": "tracking-tighter",
|
|
487
|
+
"-0.025em": "tracking-tight",
|
|
488
|
+
"0em": "tracking-normal",
|
|
489
|
+
"0px": "tracking-normal",
|
|
490
|
+
"0.025em": "tracking-wide",
|
|
491
|
+
"0.05em": "tracking-wider",
|
|
492
|
+
"0.1em": "tracking-widest"
|
|
493
|
+
},
|
|
494
|
+
// Text align
|
|
495
|
+
"text-align": {
|
|
496
|
+
"left": "text-left",
|
|
497
|
+
"start": "text-left",
|
|
498
|
+
"center": "text-center",
|
|
499
|
+
"right": "text-right",
|
|
500
|
+
"end": "text-right",
|
|
501
|
+
"justify": "text-justify"
|
|
502
|
+
},
|
|
503
|
+
// Text transform
|
|
504
|
+
"text-transform": {
|
|
505
|
+
"uppercase": "uppercase",
|
|
506
|
+
"lowercase": "lowercase",
|
|
507
|
+
"capitalize": "capitalize",
|
|
508
|
+
"none": "normal-case"
|
|
509
|
+
},
|
|
510
|
+
// Display
|
|
511
|
+
"display": {
|
|
512
|
+
"block": "block",
|
|
513
|
+
"inline-block": "inline-block",
|
|
514
|
+
"inline": "inline",
|
|
515
|
+
"flex": "flex",
|
|
516
|
+
"inline-flex": "inline-flex",
|
|
517
|
+
"grid": "grid",
|
|
518
|
+
"inline-grid": "inline-grid",
|
|
519
|
+
"none": "hidden"
|
|
520
|
+
},
|
|
521
|
+
// Position
|
|
522
|
+
"position": {
|
|
523
|
+
"static": "static",
|
|
524
|
+
"relative": "relative",
|
|
525
|
+
"absolute": "absolute",
|
|
526
|
+
"fixed": "fixed",
|
|
527
|
+
"sticky": "sticky"
|
|
528
|
+
},
|
|
529
|
+
// Flex direction
|
|
530
|
+
"flex-direction": {
|
|
531
|
+
"row": "flex-row",
|
|
532
|
+
"row-reverse": "flex-row-reverse",
|
|
533
|
+
"column": "flex-col",
|
|
534
|
+
"column-reverse": "flex-col-reverse"
|
|
535
|
+
},
|
|
536
|
+
// Flex wrap
|
|
537
|
+
"flex-wrap": {
|
|
538
|
+
"wrap": "flex-wrap",
|
|
539
|
+
"nowrap": "flex-nowrap",
|
|
540
|
+
"wrap-reverse": "flex-wrap-reverse"
|
|
541
|
+
},
|
|
542
|
+
// Justify content
|
|
543
|
+
"justify-content": {
|
|
544
|
+
"flex-start": "justify-start",
|
|
545
|
+
"start": "justify-start",
|
|
546
|
+
"flex-end": "justify-end",
|
|
547
|
+
"end": "justify-end",
|
|
548
|
+
"center": "justify-center",
|
|
549
|
+
"space-between": "justify-between",
|
|
550
|
+
"space-around": "justify-around",
|
|
551
|
+
"space-evenly": "justify-evenly"
|
|
552
|
+
},
|
|
553
|
+
// Align items
|
|
554
|
+
"align-items": {
|
|
555
|
+
"flex-start": "items-start",
|
|
556
|
+
"start": "items-start",
|
|
557
|
+
"flex-end": "items-end",
|
|
558
|
+
"end": "items-end",
|
|
559
|
+
"center": "items-center",
|
|
560
|
+
"baseline": "items-baseline",
|
|
561
|
+
"stretch": "items-stretch"
|
|
562
|
+
},
|
|
563
|
+
// Align self
|
|
564
|
+
"align-self": {
|
|
565
|
+
"auto": "self-auto",
|
|
566
|
+
"flex-start": "self-start",
|
|
567
|
+
"start": "self-start",
|
|
568
|
+
"flex-end": "self-end",
|
|
569
|
+
"end": "self-end",
|
|
570
|
+
"center": "self-center",
|
|
571
|
+
"stretch": "self-stretch"
|
|
572
|
+
},
|
|
573
|
+
// Overflow
|
|
574
|
+
"overflow": {
|
|
575
|
+
"visible": "overflow-visible",
|
|
576
|
+
"hidden": "overflow-hidden",
|
|
577
|
+
"scroll": "overflow-scroll",
|
|
578
|
+
"auto": "overflow-auto"
|
|
579
|
+
},
|
|
580
|
+
// Opacity
|
|
581
|
+
"opacity": {
|
|
582
|
+
"0": "opacity-0",
|
|
583
|
+
"0.05": "opacity-5",
|
|
584
|
+
"0.1": "opacity-10",
|
|
585
|
+
"0.15": "opacity-15",
|
|
586
|
+
"0.2": "opacity-20",
|
|
587
|
+
"0.25": "opacity-25",
|
|
588
|
+
"0.3": "opacity-30",
|
|
589
|
+
"0.35": "opacity-35",
|
|
590
|
+
"0.4": "opacity-40",
|
|
591
|
+
"0.45": "opacity-45",
|
|
592
|
+
"0.5": "opacity-50",
|
|
593
|
+
"0.55": "opacity-55",
|
|
594
|
+
"0.6": "opacity-60",
|
|
595
|
+
"0.65": "opacity-65",
|
|
596
|
+
"0.7": "opacity-70",
|
|
597
|
+
"0.75": "opacity-75",
|
|
598
|
+
"0.8": "opacity-80",
|
|
599
|
+
"0.85": "opacity-85",
|
|
600
|
+
"0.9": "opacity-90",
|
|
601
|
+
"0.95": "opacity-95",
|
|
602
|
+
"1": "opacity-100"
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
var SPACING_PX_MAP = {
|
|
606
|
+
"0px": "0",
|
|
607
|
+
"1px": "px",
|
|
608
|
+
"2px": "0.5",
|
|
609
|
+
"4px": "1",
|
|
610
|
+
"6px": "1.5",
|
|
611
|
+
"8px": "2",
|
|
612
|
+
"10px": "2.5",
|
|
613
|
+
"12px": "3",
|
|
614
|
+
"14px": "3.5",
|
|
615
|
+
"16px": "4",
|
|
616
|
+
"20px": "5",
|
|
617
|
+
"24px": "6",
|
|
618
|
+
"28px": "7",
|
|
619
|
+
"32px": "8",
|
|
620
|
+
"36px": "9",
|
|
621
|
+
"40px": "10",
|
|
622
|
+
"44px": "11",
|
|
623
|
+
"48px": "12",
|
|
624
|
+
"56px": "14",
|
|
625
|
+
"64px": "16",
|
|
626
|
+
"80px": "20",
|
|
627
|
+
"96px": "24",
|
|
628
|
+
"112px": "28",
|
|
629
|
+
"128px": "32",
|
|
630
|
+
"144px": "36",
|
|
631
|
+
"160px": "40",
|
|
632
|
+
"176px": "44",
|
|
633
|
+
"192px": "48",
|
|
634
|
+
"208px": "52",
|
|
635
|
+
"224px": "56",
|
|
636
|
+
"240px": "60",
|
|
637
|
+
"256px": "64",
|
|
638
|
+
"288px": "72",
|
|
639
|
+
"320px": "80",
|
|
640
|
+
"384px": "96"
|
|
641
|
+
};
|
|
642
|
+
var RADIUS_MAP = {
|
|
643
|
+
"0px": "rounded-none",
|
|
644
|
+
"2px": "rounded-sm",
|
|
645
|
+
"0.125rem": "rounded-sm",
|
|
646
|
+
"4px": "rounded",
|
|
647
|
+
"0.25rem": "rounded",
|
|
648
|
+
"6px": "rounded-md",
|
|
649
|
+
"0.375rem": "rounded-md",
|
|
650
|
+
"8px": "rounded-lg",
|
|
651
|
+
"0.5rem": "rounded-lg",
|
|
652
|
+
"12px": "rounded-xl",
|
|
653
|
+
"0.75rem": "rounded-xl",
|
|
654
|
+
"16px": "rounded-2xl",
|
|
655
|
+
"1rem": "rounded-2xl",
|
|
656
|
+
"24px": "rounded-3xl",
|
|
657
|
+
"1.5rem": "rounded-3xl",
|
|
658
|
+
"9999px": "rounded-full"
|
|
659
|
+
};
|
|
660
|
+
var CSS_TO_TW_PREFIX = {
|
|
661
|
+
"padding-top": "pt",
|
|
662
|
+
"padding-right": "pr",
|
|
663
|
+
"padding-bottom": "pb",
|
|
664
|
+
"padding-left": "pl",
|
|
665
|
+
"margin-top": "mt",
|
|
666
|
+
"margin-right": "mr",
|
|
667
|
+
"margin-bottom": "mb",
|
|
668
|
+
"margin-left": "ml",
|
|
669
|
+
"gap": "gap",
|
|
670
|
+
"row-gap": "gap-y",
|
|
671
|
+
"column-gap": "gap-x",
|
|
672
|
+
"width": "w",
|
|
673
|
+
"height": "h",
|
|
674
|
+
"min-width": "min-w",
|
|
675
|
+
"min-height": "min-h",
|
|
676
|
+
"max-width": "max-w",
|
|
677
|
+
"max-height": "max-h",
|
|
678
|
+
"top": "top",
|
|
679
|
+
"right": "right",
|
|
680
|
+
"bottom": "bottom",
|
|
681
|
+
"left": "left",
|
|
682
|
+
"border-top-width": "border-t",
|
|
683
|
+
"border-right-width": "border-r",
|
|
684
|
+
"border-bottom-width": "border-b",
|
|
685
|
+
"border-left-width": "border-l",
|
|
686
|
+
"border-top-left-radius": "rounded-tl",
|
|
687
|
+
"border-top-right-radius": "rounded-tr",
|
|
688
|
+
"border-bottom-right-radius": "rounded-br",
|
|
689
|
+
"border-bottom-left-radius": "rounded-bl",
|
|
690
|
+
"font-size": "text",
|
|
691
|
+
"font-weight": "font",
|
|
692
|
+
"line-height": "leading",
|
|
693
|
+
"letter-spacing": "tracking",
|
|
694
|
+
"opacity": "opacity",
|
|
695
|
+
"color": "text",
|
|
696
|
+
"background-color": "bg",
|
|
697
|
+
"border-color": "border"
|
|
698
|
+
};
|
|
699
|
+
var SPACING_PROPS = /* @__PURE__ */ new Set([
|
|
700
|
+
"padding-top",
|
|
701
|
+
"padding-right",
|
|
702
|
+
"padding-bottom",
|
|
703
|
+
"padding-left",
|
|
704
|
+
"margin-top",
|
|
705
|
+
"margin-right",
|
|
706
|
+
"margin-bottom",
|
|
707
|
+
"margin-left",
|
|
708
|
+
"gap",
|
|
709
|
+
"row-gap",
|
|
710
|
+
"column-gap",
|
|
711
|
+
"top",
|
|
712
|
+
"right",
|
|
713
|
+
"bottom",
|
|
714
|
+
"left",
|
|
715
|
+
"width",
|
|
716
|
+
"height",
|
|
717
|
+
"min-width",
|
|
718
|
+
"min-height",
|
|
719
|
+
"max-width",
|
|
720
|
+
"max-height"
|
|
721
|
+
]);
|
|
722
|
+
var RADIUS_PROPS = /* @__PURE__ */ new Set([
|
|
723
|
+
"border-top-left-radius",
|
|
724
|
+
"border-top-right-radius",
|
|
725
|
+
"border-bottom-right-radius",
|
|
726
|
+
"border-bottom-left-radius"
|
|
727
|
+
]);
|
|
728
|
+
function computedToTailwindClass(cssProp, computedValue) {
|
|
729
|
+
const directMap = REVERSE_MAP[cssProp];
|
|
730
|
+
if (directMap?.[computedValue]) {
|
|
731
|
+
return { tailwindClass: directMap[computedValue], exact: true };
|
|
732
|
+
}
|
|
733
|
+
if (SPACING_PROPS.has(cssProp)) {
|
|
734
|
+
const scaleVal = SPACING_PX_MAP[computedValue];
|
|
735
|
+
const prefix2 = CSS_TO_TW_PREFIX[cssProp];
|
|
736
|
+
if (scaleVal && prefix2) {
|
|
737
|
+
return { tailwindClass: `${prefix2}-${scaleVal}`, exact: true };
|
|
738
|
+
}
|
|
739
|
+
if (prefix2 && computedValue !== "auto" && computedValue !== "none") {
|
|
740
|
+
return { tailwindClass: `${prefix2}-[${computedValue}]`, exact: false };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (RADIUS_PROPS.has(cssProp)) {
|
|
744
|
+
const radiusClass = RADIUS_MAP[computedValue];
|
|
745
|
+
if (radiusClass) {
|
|
746
|
+
const prefix3 = CSS_TO_TW_PREFIX[cssProp];
|
|
747
|
+
if (prefix3) {
|
|
748
|
+
const suffix = radiusClass.replace("rounded", "");
|
|
749
|
+
return { tailwindClass: `${prefix3}${suffix || ""}`, exact: true };
|
|
750
|
+
}
|
|
751
|
+
return { tailwindClass: radiusClass, exact: true };
|
|
752
|
+
}
|
|
753
|
+
const prefix2 = CSS_TO_TW_PREFIX[cssProp];
|
|
754
|
+
if (prefix2 && computedValue !== "0px") {
|
|
755
|
+
return { tailwindClass: `${prefix2}-[${computedValue}]`, exact: false };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const prefix = CSS_TO_TW_PREFIX[cssProp];
|
|
759
|
+
if (prefix) {
|
|
760
|
+
return { tailwindClass: `${prefix}-[${computedValue}]`, exact: false };
|
|
761
|
+
}
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/shared/tailwind-parser.ts
|
|
766
|
+
var CLASS_PATTERNS = [
|
|
767
|
+
// Colors
|
|
768
|
+
{ regex: /^bg-([\w-]+(?:\/\d+)?)$/, category: "color", property: "backgroundColor", label: "Background", extractValue: (m) => m[1] },
|
|
769
|
+
{ regex: /^text-([\w-]+(?:\/\d+)?)$/, category: "color", property: "textColor", label: "Text", extractValue: (m) => m[1] },
|
|
770
|
+
{ regex: /^border-([\w-]+(?:\/\d+)?)$/, category: "color", property: "borderColor", label: "Border", extractValue: (m) => m[1] },
|
|
771
|
+
{ regex: /^ring-([\w-]+(?:\/\d+)?)$/, category: "color", property: "ringColor", label: "Ring", extractValue: (m) => m[1] },
|
|
772
|
+
{ regex: /^outline-([\w-]+(?:\/\d+)?)$/, category: "color", property: "outlineColor", label: "Outline", extractValue: (m) => m[1] },
|
|
773
|
+
// Spacing
|
|
774
|
+
{ regex: /^p-([\d.]+|px)$/, category: "spacing", property: "padding", label: "Padding", extractValue: (m) => m[1] },
|
|
775
|
+
{ regex: /^px-([\d.]+|px)$/, category: "spacing", property: "paddingX", label: "Padding X", extractValue: (m) => m[1] },
|
|
776
|
+
{ regex: /^py-([\d.]+|px)$/, category: "spacing", property: "paddingY", label: "Padding Y", extractValue: (m) => m[1] },
|
|
777
|
+
{ regex: /^pt-([\d.]+|px)$/, category: "spacing", property: "paddingTop", label: "Padding Top", extractValue: (m) => m[1] },
|
|
778
|
+
{ regex: /^pr-([\d.]+|px)$/, category: "spacing", property: "paddingRight", label: "Padding Right", extractValue: (m) => m[1] },
|
|
779
|
+
{ regex: /^pb-([\d.]+|px)$/, category: "spacing", property: "paddingBottom", label: "Padding Bottom", extractValue: (m) => m[1] },
|
|
780
|
+
{ regex: /^pl-([\d.]+|px)$/, category: "spacing", property: "paddingLeft", label: "Padding Left", extractValue: (m) => m[1] },
|
|
781
|
+
{ regex: /^m-([\d.]+|px|auto)$/, category: "spacing", property: "margin", label: "Margin", extractValue: (m) => m[1] },
|
|
782
|
+
{ regex: /^mx-([\d.]+|px|auto)$/, category: "spacing", property: "marginX", label: "Margin X", extractValue: (m) => m[1] },
|
|
783
|
+
{ regex: /^my-([\d.]+|px|auto)$/, category: "spacing", property: "marginY", label: "Margin Y", extractValue: (m) => m[1] },
|
|
784
|
+
{ regex: /^mt-([\d.]+|px|auto)$/, category: "spacing", property: "marginTop", label: "Margin Top", extractValue: (m) => m[1] },
|
|
785
|
+
{ regex: /^mr-([\d.]+|px|auto)$/, category: "spacing", property: "marginRight", label: "Margin Right", extractValue: (m) => m[1] },
|
|
786
|
+
{ regex: /^mb-([\d.]+|px|auto)$/, category: "spacing", property: "marginBottom", label: "Margin Bottom", extractValue: (m) => m[1] },
|
|
787
|
+
{ regex: /^ml-([\d.]+|px|auto)$/, category: "spacing", property: "marginLeft", label: "Margin Left", extractValue: (m) => m[1] },
|
|
788
|
+
{ regex: /^gap-([\d.]+|px)$/, category: "spacing", property: "gap", label: "Gap", extractValue: (m) => m[1] },
|
|
789
|
+
{ regex: /^gap-x-([\d.]+|px)$/, category: "spacing", property: "gapX", label: "Gap X", extractValue: (m) => m[1] },
|
|
790
|
+
{ regex: /^gap-y-([\d.]+|px)$/, category: "spacing", property: "gapY", label: "Gap Y", extractValue: (m) => m[1] },
|
|
791
|
+
{ regex: /^space-x-([\d.]+)$/, category: "spacing", property: "spaceX", label: "Space X", extractValue: (m) => m[1] },
|
|
792
|
+
{ regex: /^space-y-([\d.]+)$/, category: "spacing", property: "spaceY", label: "Space Y", extractValue: (m) => m[1] },
|
|
793
|
+
// Shape
|
|
794
|
+
{ regex: /^rounded$/, category: "shape", property: "borderRadius", label: "Radius", extractValue: () => "DEFAULT" },
|
|
795
|
+
{ regex: /^rounded-(none|sm|md|lg|xl|2xl|3xl|full)$/, category: "shape", property: "borderRadius", label: "Radius", extractValue: (m) => m[1] },
|
|
796
|
+
{ regex: /^border$/, category: "shape", property: "borderWidth", label: "Border Width", extractValue: () => "1" },
|
|
797
|
+
{ regex: /^border-(0|2|4|8)$/, category: "shape", property: "borderWidth", label: "Border Width", extractValue: (m) => m[1] },
|
|
798
|
+
// Typography
|
|
799
|
+
{ regex: /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/, category: "typography", property: "fontSize", label: "Font Size", extractValue: (m) => m[1] },
|
|
800
|
+
{ regex: /^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/, category: "typography", property: "fontWeight", label: "Font Weight", extractValue: (m) => m[1] },
|
|
801
|
+
{ regex: /^leading-(none|tight|snug|normal|relaxed|loose)$/, category: "typography", property: "lineHeight", label: "Line Height", extractValue: (m) => m[1] },
|
|
802
|
+
{ regex: /^tracking-(tighter|tight|normal|wide|wider|widest)$/, category: "typography", property: "letterSpacing", label: "Letter Spacing", extractValue: (m) => m[1] },
|
|
803
|
+
{ regex: /^font-(sans|serif|mono)$/, category: "typography", property: "fontFamily", label: "Font Family", extractValue: (m) => m[1] },
|
|
804
|
+
{ regex: /^text-(left|center|right|justify)$/, category: "typography", property: "textAlign", label: "Text Align", extractValue: (m) => m[1] },
|
|
805
|
+
{ regex: /^(uppercase|lowercase|capitalize|normal-case)$/, category: "typography", property: "textTransform", label: "Text Transform", extractValue: (m) => m[1] },
|
|
806
|
+
{ regex: /^(underline|overline|line-through|no-underline)$/, category: "typography", property: "textDecoration", label: "Text Decoration", extractValue: (m) => m[1] },
|
|
807
|
+
{ regex: /^(truncate|whitespace-nowrap|whitespace-normal)$/, category: "typography", property: "overflow", label: "Overflow", extractValue: (m) => m[1] },
|
|
808
|
+
// Layout
|
|
809
|
+
{ regex: /^(flex|inline-flex|grid|inline-grid|block|inline-block|inline|hidden)$/, category: "layout", property: "display", label: "Display", extractValue: (m) => m[1] },
|
|
810
|
+
{ regex: /^(flex-row|flex-col|flex-row-reverse|flex-col-reverse)$/, category: "layout", property: "flexDirection", label: "Direction", extractValue: (m) => m[1] },
|
|
811
|
+
{ regex: /^(flex-wrap|flex-nowrap|flex-wrap-reverse)$/, category: "layout", property: "flexWrap", label: "Wrap", extractValue: (m) => m[1] },
|
|
812
|
+
{ regex: /^items-(start|end|center|baseline|stretch)$/, category: "layout", property: "alignItems", label: "Align Items", extractValue: (m) => m[1] },
|
|
813
|
+
{ regex: /^justify-(start|end|center|between|around|evenly)$/, category: "layout", property: "justifyContent", label: "Justify", extractValue: (m) => m[1] },
|
|
814
|
+
{ regex: /^(self-auto|self-start|self-end|self-center|self-stretch)$/, category: "layout", property: "alignSelf", label: "Align Self", extractValue: (m) => m[1] },
|
|
815
|
+
{ regex: /^grid-cols-(\d+|none)$/, category: "layout", property: "gridCols", label: "Grid Columns", extractValue: (m) => m[1] },
|
|
816
|
+
{ regex: /^grid-rows-(\d+|none)$/, category: "layout", property: "gridRows", label: "Grid Rows", extractValue: (m) => m[1] },
|
|
817
|
+
{ regex: /^col-span-(\d+|full)$/, category: "layout", property: "colSpan", label: "Column Span", extractValue: (m) => m[1] },
|
|
818
|
+
{ regex: /^row-span-(\d+)$/, category: "layout", property: "rowSpan", label: "Row Span", extractValue: (m) => m[1] },
|
|
819
|
+
{ regex: /^(relative|absolute|fixed|sticky)$/, category: "layout", property: "position", label: "Position", extractValue: (m) => m[1] },
|
|
820
|
+
{ regex: /^(overflow-hidden|overflow-auto|overflow-scroll|overflow-visible)$/, category: "layout", property: "overflow", label: "Overflow", extractValue: (m) => m[1] },
|
|
821
|
+
// Size
|
|
822
|
+
{ regex: /^w-([\d.]+|full|screen|auto|min|max|fit|px)$/, category: "size", property: "width", label: "Width", extractValue: (m) => m[1] },
|
|
823
|
+
{ regex: /^h-([\d.]+|full|screen|auto|min|max|fit|px)$/, category: "size", property: "height", label: "Height", extractValue: (m) => m[1] },
|
|
824
|
+
{ regex: /^min-w-([\d.]+|full|min|max|fit|0)$/, category: "size", property: "minWidth", label: "Min Width", extractValue: (m) => m[1] },
|
|
825
|
+
{ regex: /^min-h-([\d.]+|full|screen|min|max|fit|0)$/, category: "size", property: "minHeight", label: "Min Height", extractValue: (m) => m[1] },
|
|
826
|
+
{ regex: /^max-w-([\w.]+)$/, category: "size", property: "maxWidth", label: "Max Width", extractValue: (m) => m[1] },
|
|
827
|
+
{ regex: /^max-h-([\w.]+)$/, category: "size", property: "maxHeight", label: "Max Height", extractValue: (m) => m[1] },
|
|
828
|
+
{ regex: /^size-([\d.]+|full|auto|px)$/, category: "size", property: "size", label: "Size", extractValue: (m) => m[1] },
|
|
829
|
+
{ regex: /^(flex-1|flex-auto|flex-initial|flex-none)$/, category: "size", property: "flex", label: "Flex", extractValue: (m) => m[1] },
|
|
830
|
+
{ regex: /^(grow|grow-0|shrink|shrink-0)$/, category: "size", property: "flexGrowShrink", label: "Grow/Shrink", extractValue: (m) => m[1] },
|
|
831
|
+
// Arbitrary values
|
|
832
|
+
{ regex: /^text-\[(-?[\w.%]+)\]$/, category: "typography", property: "fontSize", label: "Font Size", extractValue: (m) => `[${m[1]}]` },
|
|
833
|
+
{ regex: /^leading-\[(-?[\w.%]+)\]$/, category: "typography", property: "lineHeight", label: "Line Height", extractValue: (m) => `[${m[1]}]` },
|
|
834
|
+
{ regex: /^tracking-\[(-?[\w.%]+)\]$/, category: "typography", property: "letterSpacing", label: "Letter Spacing", extractValue: (m) => `[${m[1]}]` },
|
|
835
|
+
{ regex: /^font-\[(-?[\w.%]+)\]$/, category: "typography", property: "fontWeight", label: "Font Weight", extractValue: (m) => `[${m[1]}]` },
|
|
836
|
+
{ regex: /^p-\[(-?[\w.%]+)\]$/, category: "spacing", property: "padding", label: "Padding", extractValue: (m) => `[${m[1]}]` },
|
|
837
|
+
{ regex: /^px-\[(-?[\w.%]+)\]$/, category: "spacing", property: "paddingX", label: "Padding X", extractValue: (m) => `[${m[1]}]` },
|
|
838
|
+
{ regex: /^py-\[(-?[\w.%]+)\]$/, category: "spacing", property: "paddingY", label: "Padding Y", extractValue: (m) => `[${m[1]}]` },
|
|
839
|
+
{ regex: /^pt-\[(-?[\w.%]+)\]$/, category: "spacing", property: "paddingTop", label: "Padding Top", extractValue: (m) => `[${m[1]}]` },
|
|
840
|
+
{ regex: /^pr-\[(-?[\w.%]+)\]$/, category: "spacing", property: "paddingRight", label: "Padding Right", extractValue: (m) => `[${m[1]}]` },
|
|
841
|
+
{ regex: /^pb-\[(-?[\w.%]+)\]$/, category: "spacing", property: "paddingBottom", label: "Padding Bottom", extractValue: (m) => `[${m[1]}]` },
|
|
842
|
+
{ regex: /^pl-\[(-?[\w.%]+)\]$/, category: "spacing", property: "paddingLeft", label: "Padding Left", extractValue: (m) => `[${m[1]}]` },
|
|
843
|
+
{ regex: /^m-\[(-?[\w.%]+)\]$/, category: "spacing", property: "margin", label: "Margin", extractValue: (m) => `[${m[1]}]` },
|
|
844
|
+
{ regex: /^mx-\[(-?[\w.%]+)\]$/, category: "spacing", property: "marginX", label: "Margin X", extractValue: (m) => `[${m[1]}]` },
|
|
845
|
+
{ regex: /^my-\[(-?[\w.%]+)\]$/, category: "spacing", property: "marginY", label: "Margin Y", extractValue: (m) => `[${m[1]}]` },
|
|
846
|
+
{ regex: /^mt-\[(-?[\w.%]+)\]$/, category: "spacing", property: "marginTop", label: "Margin Top", extractValue: (m) => `[${m[1]}]` },
|
|
847
|
+
{ regex: /^mr-\[(-?[\w.%]+)\]$/, category: "spacing", property: "marginRight", label: "Margin Right", extractValue: (m) => `[${m[1]}]` },
|
|
848
|
+
{ regex: /^mb-\[(-?[\w.%]+)\]$/, category: "spacing", property: "marginBottom", label: "Margin Bottom", extractValue: (m) => `[${m[1]}]` },
|
|
849
|
+
{ regex: /^ml-\[(-?[\w.%]+)\]$/, category: "spacing", property: "marginLeft", label: "Margin Left", extractValue: (m) => `[${m[1]}]` },
|
|
850
|
+
{ regex: /^gap-\[(-?[\w.%]+)\]$/, category: "spacing", property: "gap", label: "Gap", extractValue: (m) => `[${m[1]}]` },
|
|
851
|
+
{ regex: /^gap-x-\[(-?[\w.%]+)\]$/, category: "spacing", property: "gapX", label: "Gap X", extractValue: (m) => `[${m[1]}]` },
|
|
852
|
+
{ regex: /^gap-y-\[(-?[\w.%]+)\]$/, category: "spacing", property: "gapY", label: "Gap Y", extractValue: (m) => `[${m[1]}]` },
|
|
853
|
+
{ regex: /^w-\[(-?[\w.%]+)\]$/, category: "size", property: "width", label: "Width", extractValue: (m) => `[${m[1]}]` },
|
|
854
|
+
{ regex: /^h-\[(-?[\w.%]+)\]$/, category: "size", property: "height", label: "Height", extractValue: (m) => `[${m[1]}]` },
|
|
855
|
+
{ regex: /^min-w-\[(-?[\w.%]+)\]$/, category: "size", property: "minWidth", label: "Min Width", extractValue: (m) => `[${m[1]}]` },
|
|
856
|
+
{ regex: /^min-h-\[(-?[\w.%]+)\]$/, category: "size", property: "minHeight", label: "Min Height", extractValue: (m) => `[${m[1]}]` },
|
|
857
|
+
{ regex: /^max-w-\[(-?[\w.%]+)\]$/, category: "size", property: "maxWidth", label: "Max Width", extractValue: (m) => `[${m[1]}]` },
|
|
858
|
+
{ regex: /^max-h-\[(-?[\w.%]+)\]$/, category: "size", property: "maxHeight", label: "Max Height", extractValue: (m) => `[${m[1]}]` },
|
|
859
|
+
{ regex: /^rounded-\[(-?[\w.%]+)\]$/, category: "shape", property: "borderRadius", label: "Radius", extractValue: (m) => `[${m[1]}]` }
|
|
860
|
+
];
|
|
861
|
+
function stripPrefix(cls) {
|
|
862
|
+
const parts = cls.split(":");
|
|
863
|
+
if (parts.length === 1) return { prefix: void 0, core: cls };
|
|
864
|
+
const core = parts.pop();
|
|
865
|
+
const prefix = parts.join(":") + ":";
|
|
866
|
+
return { prefix, core };
|
|
867
|
+
}
|
|
868
|
+
function parseClasses(classString) {
|
|
869
|
+
const result = {
|
|
870
|
+
color: [],
|
|
871
|
+
spacing: [],
|
|
872
|
+
shape: [],
|
|
873
|
+
typography: [],
|
|
874
|
+
layout: [],
|
|
875
|
+
size: [],
|
|
876
|
+
other: []
|
|
877
|
+
};
|
|
878
|
+
const classes = classString.split(/\s+/).filter(Boolean);
|
|
879
|
+
for (const cls of classes) {
|
|
880
|
+
const { prefix, core } = stripPrefix(cls);
|
|
881
|
+
let matched = false;
|
|
882
|
+
for (const pattern of CLASS_PATTERNS) {
|
|
883
|
+
const match = core.match(pattern.regex);
|
|
884
|
+
if (match) {
|
|
885
|
+
if (pattern.property === "textColor") {
|
|
886
|
+
const sizeValues = ["xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl", "9xl"];
|
|
887
|
+
if (sizeValues.includes(match[1])) continue;
|
|
888
|
+
const alignValues = ["left", "center", "right", "justify"];
|
|
889
|
+
if (alignValues.includes(match[1])) continue;
|
|
890
|
+
}
|
|
891
|
+
if (pattern.property === "borderColor") {
|
|
892
|
+
const widthValues = ["0", "2", "4", "8"];
|
|
893
|
+
if (widthValues.includes(match[1])) continue;
|
|
894
|
+
}
|
|
895
|
+
result[pattern.category].push({
|
|
896
|
+
category: pattern.category,
|
|
897
|
+
property: pattern.property,
|
|
898
|
+
label: pattern.label,
|
|
899
|
+
value: pattern.extractValue(match),
|
|
900
|
+
fullClass: cls,
|
|
901
|
+
prefix
|
|
902
|
+
});
|
|
903
|
+
matched = true;
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (!matched) {
|
|
908
|
+
result.other.push({
|
|
909
|
+
category: "other",
|
|
910
|
+
property: "unknown",
|
|
911
|
+
label: cls,
|
|
912
|
+
value: cls,
|
|
913
|
+
fullClass: cls,
|
|
914
|
+
prefix
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return result;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/server/api/write-element.ts
|
|
922
|
+
var CSS_TO_PARSER_PROP = {
|
|
923
|
+
"padding-top": "paddingTop",
|
|
924
|
+
"padding-right": "paddingRight",
|
|
925
|
+
"padding-bottom": "paddingBottom",
|
|
926
|
+
"padding-left": "paddingLeft",
|
|
927
|
+
"margin-top": "marginTop",
|
|
928
|
+
"margin-right": "marginRight",
|
|
929
|
+
"margin-bottom": "marginBottom",
|
|
930
|
+
"margin-left": "marginLeft",
|
|
931
|
+
"gap": "gap",
|
|
932
|
+
"row-gap": "gapY",
|
|
933
|
+
"column-gap": "gapX",
|
|
934
|
+
"width": "width",
|
|
935
|
+
"height": "height",
|
|
936
|
+
"min-width": "minWidth",
|
|
937
|
+
"min-height": "minHeight",
|
|
938
|
+
"max-width": "maxWidth",
|
|
939
|
+
"max-height": "maxHeight",
|
|
940
|
+
"font-size": "fontSize",
|
|
941
|
+
"font-weight": "fontWeight",
|
|
942
|
+
"line-height": "lineHeight",
|
|
943
|
+
"letter-spacing": "letterSpacing",
|
|
944
|
+
"text-align": "textAlign",
|
|
945
|
+
"text-transform": "textTransform",
|
|
946
|
+
"display": "display",
|
|
947
|
+
"position": "position",
|
|
948
|
+
"flex-direction": "flexDirection",
|
|
949
|
+
"flex-wrap": "flexWrap",
|
|
950
|
+
"align-items": "alignItems",
|
|
951
|
+
"justify-content": "justifyContent",
|
|
952
|
+
"color": "textColor",
|
|
953
|
+
"background-color": "backgroundColor",
|
|
954
|
+
"border-color": "borderColor",
|
|
955
|
+
"border-top-left-radius": "borderRadius",
|
|
956
|
+
"border-top-right-radius": "borderRadius",
|
|
957
|
+
"border-bottom-right-radius": "borderRadius",
|
|
958
|
+
"border-bottom-left-radius": "borderRadius"
|
|
959
|
+
};
|
|
960
|
+
function createWriteElementRouter(config) {
|
|
961
|
+
const router = Router();
|
|
962
|
+
router.get("/instance-props", async (req, res) => {
|
|
963
|
+
try {
|
|
964
|
+
const file = req.query.file;
|
|
965
|
+
const line = parseInt(req.query.line, 10);
|
|
966
|
+
const col = req.query.col ? parseInt(req.query.col, 10) : void 0;
|
|
967
|
+
const componentName = req.query.componentName;
|
|
968
|
+
if (!file || !line || !componentName) {
|
|
969
|
+
res.status(400).json({ error: "Missing file, line, or componentName" });
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (col == null) {
|
|
973
|
+
res.status(400).json({ error: "Missing col \u2014 rebuild next-plugin" });
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const fullPath = safePath(config.projectRoot, file);
|
|
977
|
+
const parser = await getParser();
|
|
978
|
+
const source = await fs3.readFile(fullPath, "utf-8");
|
|
979
|
+
const ast = parseSource(source, parser);
|
|
980
|
+
const elementPath = findComponentAtSource(ast, componentName, line, col);
|
|
981
|
+
if (!elementPath) {
|
|
982
|
+
res.status(404).json({
|
|
983
|
+
error: `Component <${componentName}> not found at ${file}:${line}:${col}`
|
|
984
|
+
});
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const props = {};
|
|
988
|
+
for (const attr of elementPath.node.attributes) {
|
|
989
|
+
if (attr.type === "JSXAttribute" && attr.name?.type === "JSXIdentifier") {
|
|
990
|
+
const name = attr.name.name;
|
|
991
|
+
if (attr.value?.type === "StringLiteral" || attr.value?.type === "Literal") {
|
|
992
|
+
props[name] = String(attr.value.value);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
res.json({ props });
|
|
997
|
+
} catch (err) {
|
|
998
|
+
console.error("[instance-props]", err.message);
|
|
999
|
+
res.status(500).json({ error: err.message });
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
router.post("/", async (req, res) => {
|
|
1003
|
+
try {
|
|
1004
|
+
const body = req.body;
|
|
1005
|
+
if (!body.source) {
|
|
1006
|
+
res.status(400).json({ error: "Missing source" });
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (body.type === "instanceOverride") {
|
|
1010
|
+
if (!body.componentName || !body.newClass) {
|
|
1011
|
+
res.status(400).json({ error: "Missing componentName or newClass" });
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const fullPath2 = safePath(config.projectRoot, body.source.file);
|
|
1015
|
+
const parser2 = await getParser();
|
|
1016
|
+
const source2 = await fs3.readFile(fullPath2, "utf-8");
|
|
1017
|
+
const ast2 = parseSource(source2, parser2);
|
|
1018
|
+
if (body.source.col == null) {
|
|
1019
|
+
res.status(400).json({ error: "Missing col \u2014 rebuild next-plugin" });
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const elementPath2 = findComponentAtSource(ast2, body.componentName, body.source.line, body.source.col);
|
|
1023
|
+
if (!elementPath2) {
|
|
1024
|
+
res.status(404).json({
|
|
1025
|
+
error: `Component <${body.componentName}> not found at ${body.source.file}:${body.source.line}:${body.source.col}`
|
|
1026
|
+
});
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const openingElement2 = elementPath2.node;
|
|
1030
|
+
const classAttr2 = findAttr(openingElement2, "className");
|
|
1031
|
+
if (body.oldClass && classAttr2) {
|
|
1032
|
+
const replaced = replaceClassInAttr(openingElement2, body.oldClass, body.newClass);
|
|
1033
|
+
if (!replaced) {
|
|
1034
|
+
appendClassToAttr(openingElement2, body.newClass);
|
|
1035
|
+
}
|
|
1036
|
+
} else if (classAttr2) {
|
|
1037
|
+
appendClassToAttr(openingElement2, body.newClass);
|
|
1038
|
+
} else {
|
|
1039
|
+
addClassNameAttr(openingElement2, body.newClass);
|
|
1040
|
+
}
|
|
1041
|
+
const output2 = printSource(ast2);
|
|
1042
|
+
await fs3.writeFile(fullPath2, output2, "utf-8");
|
|
1043
|
+
res.json({ ok: true });
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (body.type === "resetInstanceClassName") {
|
|
1047
|
+
if (!body.componentName) {
|
|
1048
|
+
res.status(400).json({ error: "Missing componentName" });
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const fullPath2 = safePath(config.projectRoot, body.source.file);
|
|
1052
|
+
const parser2 = await getParser();
|
|
1053
|
+
const source2 = await fs3.readFile(fullPath2, "utf-8");
|
|
1054
|
+
const ast2 = parseSource(source2, parser2);
|
|
1055
|
+
if (body.source.col == null) {
|
|
1056
|
+
res.status(400).json({ error: "Missing col \u2014 rebuild next-plugin" });
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const elementPath2 = findComponentAtSource(ast2, body.componentName, body.source.line, body.source.col);
|
|
1060
|
+
if (!elementPath2) {
|
|
1061
|
+
res.status(404).json({
|
|
1062
|
+
error: `Component <${body.componentName}> not found at ${body.source.file}:${body.source.line}:${body.source.col}`
|
|
1063
|
+
});
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const openingElement2 = elementPath2.node;
|
|
1067
|
+
openingElement2.attributes = openingElement2.attributes.filter((attr) => {
|
|
1068
|
+
if (attr.type === "JSXAttribute" && attr.name?.type === "JSXIdentifier") {
|
|
1069
|
+
return attr.name.name !== "className";
|
|
1070
|
+
}
|
|
1071
|
+
return true;
|
|
1072
|
+
});
|
|
1073
|
+
const output2 = printSource(ast2);
|
|
1074
|
+
await fs3.writeFile(fullPath2, output2, "utf-8");
|
|
1075
|
+
res.json({ ok: true });
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (body.type === "prop") {
|
|
1079
|
+
if (!body.componentName || !body.propName || body.propValue === void 0) {
|
|
1080
|
+
res.status(400).json({ error: "Missing componentName, propName, or propValue" });
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const fullPath2 = safePath(config.projectRoot, body.source.file);
|
|
1084
|
+
const parser2 = await getParser();
|
|
1085
|
+
const source2 = await fs3.readFile(fullPath2, "utf-8");
|
|
1086
|
+
const ast2 = parseSource(source2, parser2);
|
|
1087
|
+
if (body.source.col == null) {
|
|
1088
|
+
res.status(400).json({ error: "Missing col \u2014 rebuild next-plugin" });
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const elementPath2 = findComponentAtSource(ast2, body.componentName, body.source.line, body.source.col);
|
|
1092
|
+
if (!elementPath2) {
|
|
1093
|
+
res.status(404).json({
|
|
1094
|
+
error: `Component <${body.componentName}> not found at ${body.source.file}:${body.source.line}:${body.source.col}`
|
|
1095
|
+
});
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const openingElement2 = elementPath2.node;
|
|
1099
|
+
const existingProp = findAttr(openingElement2, body.propName);
|
|
1100
|
+
if (existingProp) {
|
|
1101
|
+
existingProp.value = b2.stringLiteral(body.propValue);
|
|
1102
|
+
} else {
|
|
1103
|
+
openingElement2.attributes.push(
|
|
1104
|
+
b2.jsxAttribute(
|
|
1105
|
+
b2.jsxIdentifier(body.propName),
|
|
1106
|
+
b2.stringLiteral(body.propValue)
|
|
1107
|
+
)
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
const output2 = printSource(ast2);
|
|
1111
|
+
await fs3.writeFile(fullPath2, output2, "utf-8");
|
|
1112
|
+
res.json({ ok: true });
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (body.type === "replaceClass" || body.type === "addClass") {
|
|
1116
|
+
const fullPath2 = safePath(config.projectRoot, body.source.file);
|
|
1117
|
+
const parser2 = await getParser();
|
|
1118
|
+
const source2 = await fs3.readFile(fullPath2, "utf-8");
|
|
1119
|
+
const ast2 = parseSource(source2, parser2);
|
|
1120
|
+
const elementPath2 = findElementAtSource(ast2, body.source.line, body.source.col);
|
|
1121
|
+
if (!elementPath2) {
|
|
1122
|
+
res.status(404).json({
|
|
1123
|
+
error: `Element not found at ${body.source.file}:${body.source.line}:${body.source.col}`
|
|
1124
|
+
});
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const openingElement2 = elementPath2.node;
|
|
1128
|
+
const classAttr2 = findAttr(openingElement2, "className");
|
|
1129
|
+
if (body.type === "replaceClass" && body.oldClass && body.newClass) {
|
|
1130
|
+
if (classAttr2) {
|
|
1131
|
+
replaceClassInAttr(openingElement2, body.oldClass, body.newClass);
|
|
1132
|
+
}
|
|
1133
|
+
} else if (body.type === "addClass" && body.newClass) {
|
|
1134
|
+
if (classAttr2) {
|
|
1135
|
+
appendClassToAttr(openingElement2, body.newClass);
|
|
1136
|
+
} else {
|
|
1137
|
+
addClassNameAttr(openingElement2, body.newClass);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const output2 = printSource(ast2);
|
|
1141
|
+
await fs3.writeFile(fullPath2, output2, "utf-8");
|
|
1142
|
+
res.json({ ok: true });
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (!body.changes || body.changes.length === 0) {
|
|
1146
|
+
res.status(400).json({ error: "Missing changes" });
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const fullPath = safePath(config.projectRoot, body.source.file);
|
|
1150
|
+
const parser = await getParser();
|
|
1151
|
+
const source = await fs3.readFile(fullPath, "utf-8");
|
|
1152
|
+
const ast = parseSource(source, parser);
|
|
1153
|
+
const elementPath = findElementAtSource(ast, body.source.line, body.source.col);
|
|
1154
|
+
if (!elementPath) {
|
|
1155
|
+
res.status(404).json({
|
|
1156
|
+
error: `Element not found at ${body.source.file}:${body.source.line}:${body.source.col}`
|
|
1157
|
+
});
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
const openingElement = elementPath.node;
|
|
1161
|
+
const classAttr = findAttr(openingElement, "className");
|
|
1162
|
+
const currentClassName = classAttr ? classAttr.value?.value || "" : "";
|
|
1163
|
+
for (const change of body.changes) {
|
|
1164
|
+
let newClass;
|
|
1165
|
+
if (change.hint?.tailwindClass) {
|
|
1166
|
+
newClass = change.hint.tailwindClass;
|
|
1167
|
+
} else {
|
|
1168
|
+
const match = computedToTailwindClass(change.property, change.value);
|
|
1169
|
+
if (!match) continue;
|
|
1170
|
+
newClass = match.tailwindClass;
|
|
1171
|
+
}
|
|
1172
|
+
const parserProp = CSS_TO_PARSER_PROP[change.property];
|
|
1173
|
+
let existingClass = null;
|
|
1174
|
+
if (parserProp && currentClassName) {
|
|
1175
|
+
const parsed = parseClasses(currentClassName);
|
|
1176
|
+
const allParsed = [
|
|
1177
|
+
...parsed.color,
|
|
1178
|
+
...parsed.spacing,
|
|
1179
|
+
...parsed.shape,
|
|
1180
|
+
...parsed.typography,
|
|
1181
|
+
...parsed.layout,
|
|
1182
|
+
...parsed.size
|
|
1183
|
+
];
|
|
1184
|
+
const match = allParsed.find((p) => p.property === parserProp);
|
|
1185
|
+
if (match) {
|
|
1186
|
+
existingClass = match.fullClass;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (existingClass) {
|
|
1190
|
+
replaceClassInAttr(openingElement, existingClass, newClass);
|
|
1191
|
+
} else if (classAttr) {
|
|
1192
|
+
appendClassToAttr(openingElement, newClass);
|
|
1193
|
+
} else {
|
|
1194
|
+
addClassNameAttr(openingElement, newClass);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const output = printSource(ast);
|
|
1198
|
+
await fs3.writeFile(fullPath, output, "utf-8");
|
|
1199
|
+
res.json({ ok: true });
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
console.error("[write-element]", err.message);
|
|
1202
|
+
res.status(500).json({ error: err.message });
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
return router;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/server/api/write-tokens.ts
|
|
1209
|
+
import { Router as Router3 } from "express";
|
|
1210
|
+
import fs13 from "fs/promises";
|
|
1211
|
+
|
|
1212
|
+
// src/server/lib/scanner.ts
|
|
1213
|
+
import { Router as Router2 } from "express";
|
|
1214
|
+
import fs12 from "fs/promises";
|
|
1215
|
+
import path12 from "path";
|
|
1216
|
+
|
|
1217
|
+
// src/server/lib/scan-tokens.ts
|
|
1218
|
+
import fs4 from "fs/promises";
|
|
1219
|
+
import path4 from "path";
|
|
1220
|
+
async function scanTokens(projectRoot, framework) {
|
|
1221
|
+
if (framework.cssFiles.length === 0) {
|
|
1222
|
+
return { tokens: [], cssFilePath: "", groups: {} };
|
|
1223
|
+
}
|
|
1224
|
+
const cssFilePath = framework.cssFiles[0];
|
|
1225
|
+
const fullPath = path4.join(projectRoot, cssFilePath);
|
|
1226
|
+
const css = await fs4.readFile(fullPath, "utf-8");
|
|
1227
|
+
const rootTokens = parseBlock(css, ":root");
|
|
1228
|
+
const darkTokens = parseBlock(css, ".dark");
|
|
1229
|
+
const tokenMap = /* @__PURE__ */ new Map();
|
|
1230
|
+
for (const [name, value] of rootTokens) {
|
|
1231
|
+
const def = {
|
|
1232
|
+
name,
|
|
1233
|
+
category: categorizeToken(name, value),
|
|
1234
|
+
group: getTokenGroup(name),
|
|
1235
|
+
lightValue: value,
|
|
1236
|
+
darkValue: darkTokens.get(name) || "",
|
|
1237
|
+
colorFormat: detectColorFormat(value)
|
|
1238
|
+
};
|
|
1239
|
+
tokenMap.set(name, def);
|
|
1240
|
+
}
|
|
1241
|
+
for (const [name, value] of darkTokens) {
|
|
1242
|
+
if (!tokenMap.has(name)) {
|
|
1243
|
+
tokenMap.set(name, {
|
|
1244
|
+
name,
|
|
1245
|
+
category: categorizeToken(name, value),
|
|
1246
|
+
group: getTokenGroup(name),
|
|
1247
|
+
lightValue: "",
|
|
1248
|
+
darkValue: value,
|
|
1249
|
+
colorFormat: detectColorFormat(value)
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const tokens = Array.from(tokenMap.values());
|
|
1254
|
+
const groups = {};
|
|
1255
|
+
for (const token of tokens) {
|
|
1256
|
+
if (!groups[token.group]) groups[token.group] = [];
|
|
1257
|
+
groups[token.group].push(token);
|
|
1258
|
+
}
|
|
1259
|
+
return { tokens, cssFilePath, groups };
|
|
1260
|
+
}
|
|
1261
|
+
function parseBlock(css, selector) {
|
|
1262
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
1263
|
+
const selectorEscaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1264
|
+
const blockStart = css.search(new RegExp(`${selectorEscaped}\\s*\\{`));
|
|
1265
|
+
if (blockStart === -1) return tokens;
|
|
1266
|
+
const openBrace = css.indexOf("{", blockStart);
|
|
1267
|
+
let depth = 1;
|
|
1268
|
+
let pos = openBrace + 1;
|
|
1269
|
+
while (depth > 0 && pos < css.length) {
|
|
1270
|
+
if (css[pos] === "{") depth++;
|
|
1271
|
+
if (css[pos] === "}") depth--;
|
|
1272
|
+
pos++;
|
|
1273
|
+
}
|
|
1274
|
+
const block = css.slice(openBrace + 1, pos - 1);
|
|
1275
|
+
const propRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
|
|
1276
|
+
let match;
|
|
1277
|
+
while ((match = propRegex.exec(block)) !== null) {
|
|
1278
|
+
tokens.set(match[1], match[2].trim());
|
|
1279
|
+
}
|
|
1280
|
+
return tokens;
|
|
1281
|
+
}
|
|
1282
|
+
function categorizeToken(name, value) {
|
|
1283
|
+
if (value.includes("oklch") || value.includes("hsl") || value.includes("rgb") || value.startsWith("#")) {
|
|
1284
|
+
return "color";
|
|
1285
|
+
}
|
|
1286
|
+
if (name.includes("radius")) return "radius";
|
|
1287
|
+
if (name.includes("shadow")) return "shadow";
|
|
1288
|
+
if (name.includes("spacing")) return "spacing";
|
|
1289
|
+
if (name.includes("border") && !value.includes("oklch") && !value.includes("hsl") && !value.includes("rgb") && !value.startsWith("#")) return "border";
|
|
1290
|
+
if (name.includes("font") || name.includes("text") || name.includes("tracking") || name.includes("leading")) {
|
|
1291
|
+
return "typography";
|
|
1292
|
+
}
|
|
1293
|
+
if (value.endsWith("rem") || value.endsWith("px") || value.endsWith("em")) {
|
|
1294
|
+
if (name.includes("radius")) return "radius";
|
|
1295
|
+
return "spacing";
|
|
1296
|
+
}
|
|
1297
|
+
return "other";
|
|
1298
|
+
}
|
|
1299
|
+
function getTokenGroup(name) {
|
|
1300
|
+
const n3 = name.replace(/^--/, "");
|
|
1301
|
+
const scaleMatch = n3.match(/^([\w]+)-\d+$/);
|
|
1302
|
+
if (scaleMatch) return scaleMatch[1];
|
|
1303
|
+
const semanticPrefixes = [
|
|
1304
|
+
"primary",
|
|
1305
|
+
"secondary",
|
|
1306
|
+
"neutral",
|
|
1307
|
+
"success",
|
|
1308
|
+
"destructive",
|
|
1309
|
+
"warning"
|
|
1310
|
+
];
|
|
1311
|
+
for (const prefix of semanticPrefixes) {
|
|
1312
|
+
if (n3 === prefix || n3.startsWith(`${prefix}-`)) return prefix;
|
|
1313
|
+
}
|
|
1314
|
+
if (["background", "foreground", "card", "card-foreground", "popover", "popover-foreground"].includes(n3)) {
|
|
1315
|
+
return "surface";
|
|
1316
|
+
}
|
|
1317
|
+
if (["border", "input", "ring", "muted", "muted-foreground", "accent", "accent-foreground"].includes(n3)) {
|
|
1318
|
+
return "utility";
|
|
1319
|
+
}
|
|
1320
|
+
if (n3.startsWith("chart")) return "chart";
|
|
1321
|
+
if (n3.startsWith("sidebar")) return "sidebar";
|
|
1322
|
+
if (n3.startsWith("radius")) return "radius";
|
|
1323
|
+
if (n3.startsWith("shadow")) return "shadow";
|
|
1324
|
+
if (n3.startsWith("border-width")) return "border";
|
|
1325
|
+
return "other";
|
|
1326
|
+
}
|
|
1327
|
+
function detectColorFormat(value) {
|
|
1328
|
+
if (value.includes("oklch")) return "oklch";
|
|
1329
|
+
if (value.includes("hsl")) return "hsl";
|
|
1330
|
+
if (value.includes("rgb")) return "rgb";
|
|
1331
|
+
if (value.startsWith("#")) return "hex";
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/server/lib/scan-components.ts
|
|
1336
|
+
import fs5 from "fs/promises";
|
|
1337
|
+
import path5 from "path";
|
|
1338
|
+
async function scanComponents(projectRoot) {
|
|
1339
|
+
const componentDirs = [
|
|
1340
|
+
"components/ui",
|
|
1341
|
+
"src/components/ui"
|
|
1342
|
+
];
|
|
1343
|
+
let componentDir = "";
|
|
1344
|
+
for (const dir of componentDirs) {
|
|
1345
|
+
try {
|
|
1346
|
+
await fs5.access(path5.join(projectRoot, dir));
|
|
1347
|
+
componentDir = dir;
|
|
1348
|
+
break;
|
|
1349
|
+
} catch {
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (!componentDir) {
|
|
1353
|
+
return { components: [] };
|
|
1354
|
+
}
|
|
1355
|
+
const fullDir = path5.join(projectRoot, componentDir);
|
|
1356
|
+
const files = await fs5.readdir(fullDir);
|
|
1357
|
+
const tsxFiles = files.filter((f) => f.endsWith(".tsx"));
|
|
1358
|
+
const components = [];
|
|
1359
|
+
for (const file of tsxFiles) {
|
|
1360
|
+
const filePath = path5.join(componentDir, file);
|
|
1361
|
+
const source = await fs5.readFile(path5.join(projectRoot, filePath), "utf-8");
|
|
1362
|
+
const entry = parseComponent(source, filePath);
|
|
1363
|
+
if (entry) {
|
|
1364
|
+
components.push(entry);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return { components };
|
|
1368
|
+
}
|
|
1369
|
+
function parseComponent(source, filePath) {
|
|
1370
|
+
const cvaMatch = source.match(
|
|
1371
|
+
/const\s+(\w+)\s*=\s*cva\(\s*(["'`])([\s\S]*?)\2\s*,\s*\{/
|
|
1372
|
+
);
|
|
1373
|
+
const slotMatch = source.match(/data-slot=["'](\w+)["']/);
|
|
1374
|
+
if (!slotMatch) return null;
|
|
1375
|
+
const dataSlot = slotMatch[1];
|
|
1376
|
+
const name = dataSlot.charAt(0).toUpperCase() + dataSlot.slice(1);
|
|
1377
|
+
if (!cvaMatch) {
|
|
1378
|
+
return {
|
|
1379
|
+
name,
|
|
1380
|
+
filePath,
|
|
1381
|
+
dataSlot,
|
|
1382
|
+
baseClasses: "",
|
|
1383
|
+
variants: [],
|
|
1384
|
+
tokenReferences: extractTokenReferences(source)
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
const baseClasses = cvaMatch[3].trim();
|
|
1388
|
+
const variants = parseVariants(source);
|
|
1389
|
+
const tokenReferences = extractTokenReferences(source);
|
|
1390
|
+
return {
|
|
1391
|
+
name,
|
|
1392
|
+
filePath,
|
|
1393
|
+
dataSlot,
|
|
1394
|
+
baseClasses,
|
|
1395
|
+
variants,
|
|
1396
|
+
tokenReferences
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function parseVariants(source) {
|
|
1400
|
+
const dimensions = [];
|
|
1401
|
+
const variantsBlock = source.match(/variants\s*:\s*\{([\s\S]*?)\}\s*,?\s*defaultVariants/);
|
|
1402
|
+
if (!variantsBlock) return dimensions;
|
|
1403
|
+
const block = variantsBlock[1];
|
|
1404
|
+
const dimRegex = /(\w+)\s*:\s*\{([^}]+)\}/g;
|
|
1405
|
+
let dimMatch;
|
|
1406
|
+
while ((dimMatch = dimRegex.exec(block)) !== null) {
|
|
1407
|
+
const dimName = dimMatch[1];
|
|
1408
|
+
const dimBody = dimMatch[2];
|
|
1409
|
+
const options = [];
|
|
1410
|
+
const classes = {};
|
|
1411
|
+
const optRegex = /["']?([\w-]+)["']?\s*:\s*\n?\s*["'`]([^"'`]*)["'`]/g;
|
|
1412
|
+
let optMatch;
|
|
1413
|
+
while ((optMatch = optRegex.exec(dimBody)) !== null) {
|
|
1414
|
+
options.push(optMatch[1]);
|
|
1415
|
+
classes[optMatch[1]] = optMatch[2].trim();
|
|
1416
|
+
}
|
|
1417
|
+
const defaultVariantsSection = source.match(
|
|
1418
|
+
/defaultVariants\s*:\s*\{([^}]+)\}/
|
|
1419
|
+
);
|
|
1420
|
+
let defaultVal = options[0] || "";
|
|
1421
|
+
if (defaultVariantsSection) {
|
|
1422
|
+
const defMatch = defaultVariantsSection[1].match(
|
|
1423
|
+
new RegExp(`${dimName}\\s*:\\s*["'](\\w+)["']`)
|
|
1424
|
+
);
|
|
1425
|
+
if (defMatch) defaultVal = defMatch[1];
|
|
1426
|
+
}
|
|
1427
|
+
if (options.length > 0) {
|
|
1428
|
+
dimensions.push({
|
|
1429
|
+
name: dimName,
|
|
1430
|
+
options,
|
|
1431
|
+
default: defaultVal,
|
|
1432
|
+
classes
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return dimensions;
|
|
1437
|
+
}
|
|
1438
|
+
function extractTokenReferences(source) {
|
|
1439
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
1440
|
+
const classStrings = source.match(/["'`][^"'`]*["'`]/g) || [];
|
|
1441
|
+
for (const str of classStrings) {
|
|
1442
|
+
const tokenPattern = /(?:bg|text|border|ring|shadow|outline|fill|stroke)-([a-z][\w-]*(?:\/\d+)?)/g;
|
|
1443
|
+
let match;
|
|
1444
|
+
while ((match = tokenPattern.exec(str)) !== null) {
|
|
1445
|
+
const val = match[1];
|
|
1446
|
+
if (!val.match(/^\d/) && // not a number
|
|
1447
|
+
!["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "full", "none"].includes(val) && !val.startsWith("[")) {
|
|
1448
|
+
tokens.add(val.split("/")[0]);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return Array.from(tokens);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/server/lib/scan-shadows.ts
|
|
1456
|
+
import fs8 from "fs/promises";
|
|
1457
|
+
import path8 from "path";
|
|
1458
|
+
|
|
1459
|
+
// src/server/lib/presets/tailwind.ts
|
|
1460
|
+
var TAILWIND_SHADOW_PRESETS = [
|
|
1461
|
+
{
|
|
1462
|
+
name: "shadow-2xs",
|
|
1463
|
+
value: "0 1px rgb(0 0 0 / 0.05)"
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
name: "shadow-xs",
|
|
1467
|
+
value: "0 1px 2px 0 rgb(0 0 0 / 0.05)"
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
name: "shadow-sm",
|
|
1471
|
+
value: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
name: "shadow",
|
|
1475
|
+
value: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
name: "shadow-md",
|
|
1479
|
+
value: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
name: "shadow-lg",
|
|
1483
|
+
value: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
name: "shadow-xl",
|
|
1487
|
+
value: "0 25px 50px -12px rgb(0 0 0 / 0.25)"
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
name: "shadow-2xl",
|
|
1491
|
+
value: "0 50px 100px -20px rgb(0 0 0 / 0.25)"
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
name: "shadow-inner",
|
|
1495
|
+
value: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)"
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
name: "shadow-none",
|
|
1499
|
+
value: "none"
|
|
1500
|
+
}
|
|
1501
|
+
];
|
|
1502
|
+
var TAILWIND_RADIUS_PRESETS = [
|
|
1503
|
+
{ name: "radius-xs", value: "0.125rem", kind: "radius" },
|
|
1504
|
+
{ name: "radius-sm", value: "0.25rem", kind: "radius" },
|
|
1505
|
+
{ name: "radius-md", value: "0.375rem", kind: "radius" },
|
|
1506
|
+
{ name: "radius-lg", value: "0.5rem", kind: "radius" },
|
|
1507
|
+
{ name: "radius-xl", value: "0.75rem", kind: "radius" },
|
|
1508
|
+
{ name: "radius-2xl", value: "1rem", kind: "radius" },
|
|
1509
|
+
{ name: "radius-3xl", value: "1.5rem", kind: "radius" },
|
|
1510
|
+
{ name: "radius-full", value: "9999px", kind: "radius" }
|
|
1511
|
+
];
|
|
1512
|
+
var TAILWIND_BORDER_WIDTH_PRESETS = [
|
|
1513
|
+
{ name: "border-width-0", value: "0px", kind: "width" },
|
|
1514
|
+
{ name: "border-width-1", value: "1px", kind: "width" },
|
|
1515
|
+
{ name: "border-width-2", value: "2px", kind: "width" },
|
|
1516
|
+
{ name: "border-width-4", value: "4px", kind: "width" },
|
|
1517
|
+
{ name: "border-width-8", value: "8px", kind: "width" }
|
|
1518
|
+
];
|
|
1519
|
+
|
|
1520
|
+
// src/server/lib/presets/bootstrap.ts
|
|
1521
|
+
import fs6 from "fs/promises";
|
|
1522
|
+
import path6 from "path";
|
|
1523
|
+
var BOOTSTRAP_SHADOW_PRESETS = [
|
|
1524
|
+
{
|
|
1525
|
+
name: "box-shadow-sm",
|
|
1526
|
+
value: "0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)"
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
name: "box-shadow",
|
|
1530
|
+
value: "0 0.5rem 1rem rgba(0, 0, 0, 0.15)"
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
name: "box-shadow-lg",
|
|
1534
|
+
value: "0 1rem 3rem rgba(0, 0, 0, 0.175)"
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
name: "box-shadow-inset",
|
|
1538
|
+
value: "inset 0 1px 2px rgba(0, 0, 0, 0.075)"
|
|
1539
|
+
}
|
|
1540
|
+
];
|
|
1541
|
+
async function scanBootstrapScssOverrides(projectRoot, scssFiles) {
|
|
1542
|
+
const overrides = [];
|
|
1543
|
+
for (const file of scssFiles) {
|
|
1544
|
+
try {
|
|
1545
|
+
const content = await fs6.readFile(path6.join(projectRoot, file), "utf-8");
|
|
1546
|
+
const lines = content.split("\n");
|
|
1547
|
+
for (const line of lines) {
|
|
1548
|
+
const match = line.match(
|
|
1549
|
+
/\$(box-shadow(?:-sm|-lg|-inset)?)\s*:\s*(.+?)(?:\s*!default)?\s*;/
|
|
1550
|
+
);
|
|
1551
|
+
if (match) {
|
|
1552
|
+
const sassName = match[1];
|
|
1553
|
+
let value = match[2].trim();
|
|
1554
|
+
value = resolveBootstrapSassColors(value);
|
|
1555
|
+
overrides.push({
|
|
1556
|
+
name: sassName,
|
|
1557
|
+
value,
|
|
1558
|
+
sassVariable: `$${sassName}`,
|
|
1559
|
+
cssVariable: `--bs-${sassName}`,
|
|
1560
|
+
filePath: file
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
} catch {
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return overrides;
|
|
1568
|
+
}
|
|
1569
|
+
async function scanBootstrapCssOverrides(projectRoot, cssFiles) {
|
|
1570
|
+
const overrides = [];
|
|
1571
|
+
for (const file of cssFiles) {
|
|
1572
|
+
try {
|
|
1573
|
+
const content = await fs6.readFile(path6.join(projectRoot, file), "utf-8");
|
|
1574
|
+
const propRegex = /(--bs-box-shadow(?:-sm|-lg|-inset)?)\s*:\s*([^;]+);/g;
|
|
1575
|
+
let match;
|
|
1576
|
+
while ((match = propRegex.exec(content)) !== null) {
|
|
1577
|
+
const cssVar = match[1];
|
|
1578
|
+
const value = match[2].trim();
|
|
1579
|
+
const name = cssVar.replace(/^--bs-/, "");
|
|
1580
|
+
overrides.push({
|
|
1581
|
+
name,
|
|
1582
|
+
value,
|
|
1583
|
+
sassVariable: `$${name}`,
|
|
1584
|
+
cssVariable: cssVar,
|
|
1585
|
+
filePath: file
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return overrides;
|
|
1592
|
+
}
|
|
1593
|
+
var BOOTSTRAP_RADIUS_PRESETS = [
|
|
1594
|
+
{ name: "border-radius", value: "0.375rem", kind: "radius" },
|
|
1595
|
+
{ name: "border-radius-sm", value: "0.25rem", kind: "radius" },
|
|
1596
|
+
{ name: "border-radius-lg", value: "0.5rem", kind: "radius" },
|
|
1597
|
+
{ name: "border-radius-xl", value: "1rem", kind: "radius" },
|
|
1598
|
+
{ name: "border-radius-xxl", value: "2rem", kind: "radius" },
|
|
1599
|
+
{ name: "border-radius-pill", value: "50rem", kind: "radius" }
|
|
1600
|
+
];
|
|
1601
|
+
var BOOTSTRAP_BORDER_WIDTH_PRESETS = [
|
|
1602
|
+
{ name: "border-width", value: "1px", kind: "width" }
|
|
1603
|
+
];
|
|
1604
|
+
function resolveBootstrapSassColors(value) {
|
|
1605
|
+
return value.replace(/rgba\(\$black,\s*([\d.]+)\)/g, "rgba(0, 0, 0, $1)").replace(/rgba\(\$white,\s*([\d.]+)\)/g, "rgba(255, 255, 255, $1)").replace(/\$black/g, "#000").replace(/\$white/g, "#fff");
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/server/lib/presets/w3c-design-tokens.ts
|
|
1609
|
+
import fs7 from "fs/promises";
|
|
1610
|
+
import path7 from "path";
|
|
1611
|
+
async function findDesignTokenFiles(projectRoot) {
|
|
1612
|
+
const candidates = [
|
|
1613
|
+
"tokens",
|
|
1614
|
+
"design-tokens",
|
|
1615
|
+
"src/tokens",
|
|
1616
|
+
"src/design-tokens",
|
|
1617
|
+
"styles/tokens",
|
|
1618
|
+
"."
|
|
1619
|
+
];
|
|
1620
|
+
const found = [];
|
|
1621
|
+
for (const dir of candidates) {
|
|
1622
|
+
try {
|
|
1623
|
+
const fullDir = path7.join(projectRoot, dir);
|
|
1624
|
+
const entries = await fs7.readdir(fullDir);
|
|
1625
|
+
for (const entry of entries) {
|
|
1626
|
+
if (entry.endsWith(".tokens.json") || entry.endsWith(".tokens")) {
|
|
1627
|
+
found.push(path7.join(dir, entry));
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
} catch {
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return found;
|
|
1634
|
+
}
|
|
1635
|
+
async function scanDesignTokenShadows(projectRoot, tokenFiles) {
|
|
1636
|
+
const files = tokenFiles || await findDesignTokenFiles(projectRoot);
|
|
1637
|
+
const tokens = [];
|
|
1638
|
+
for (const file of files) {
|
|
1639
|
+
try {
|
|
1640
|
+
const content = await fs7.readFile(path7.join(projectRoot, file), "utf-8");
|
|
1641
|
+
const parsed = JSON.parse(content);
|
|
1642
|
+
extractShadowTokens(parsed, [], file, tokens);
|
|
1643
|
+
} catch {
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return tokens;
|
|
1647
|
+
}
|
|
1648
|
+
function extractShadowTokens(obj, pathParts, filePath, results) {
|
|
1649
|
+
if (!obj || typeof obj !== "object") return;
|
|
1650
|
+
if (obj.$type === "shadow" && obj.$value !== void 0) {
|
|
1651
|
+
const tokenPath = pathParts.join(".");
|
|
1652
|
+
const name = pathParts[pathParts.length - 1] || tokenPath;
|
|
1653
|
+
results.push({
|
|
1654
|
+
name,
|
|
1655
|
+
value: obj.$value,
|
|
1656
|
+
cssValue: w3cShadowToCss(obj.$value),
|
|
1657
|
+
description: obj.$description,
|
|
1658
|
+
filePath,
|
|
1659
|
+
tokenPath
|
|
1660
|
+
});
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const groupType = obj.$type;
|
|
1664
|
+
for (const [key, child] of Object.entries(obj)) {
|
|
1665
|
+
if (key.startsWith("$")) continue;
|
|
1666
|
+
if (child && typeof child === "object") {
|
|
1667
|
+
const childObj = child;
|
|
1668
|
+
if (groupType === "shadow" && childObj.$value !== void 0 && !childObj.$type) {
|
|
1669
|
+
const tokenPath = [...pathParts, key].join(".");
|
|
1670
|
+
results.push({
|
|
1671
|
+
name: key,
|
|
1672
|
+
value: childObj.$value,
|
|
1673
|
+
cssValue: w3cShadowToCss(childObj.$value),
|
|
1674
|
+
description: childObj.$description,
|
|
1675
|
+
filePath,
|
|
1676
|
+
tokenPath
|
|
1677
|
+
});
|
|
1678
|
+
} else {
|
|
1679
|
+
extractShadowTokens(childObj, [...pathParts, key], filePath, results);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
function w3cShadowToCss(value) {
|
|
1685
|
+
if (Array.isArray(value)) {
|
|
1686
|
+
return value.map(singleW3cToCss).join(", ");
|
|
1687
|
+
}
|
|
1688
|
+
return singleW3cToCss(value);
|
|
1689
|
+
}
|
|
1690
|
+
function singleW3cToCss(v) {
|
|
1691
|
+
const parts = [
|
|
1692
|
+
v.offsetX || "0px",
|
|
1693
|
+
v.offsetY || "0px",
|
|
1694
|
+
v.blur || "0px",
|
|
1695
|
+
v.spread || "0px",
|
|
1696
|
+
v.color || "rgb(0, 0, 0, 0.1)"
|
|
1697
|
+
];
|
|
1698
|
+
return parts.join(" ");
|
|
1699
|
+
}
|
|
1700
|
+
function cssToW3cShadow(cssValue) {
|
|
1701
|
+
if (!cssValue || cssValue === "none") {
|
|
1702
|
+
return { offsetX: "0px", offsetY: "0px", blur: "0px", spread: "0px", color: "transparent" };
|
|
1703
|
+
}
|
|
1704
|
+
const parts = [];
|
|
1705
|
+
let depth = 0;
|
|
1706
|
+
let current = "";
|
|
1707
|
+
for (const char of cssValue) {
|
|
1708
|
+
if (char === "(") depth++;
|
|
1709
|
+
if (char === ")") depth--;
|
|
1710
|
+
if (char === "," && depth === 0) {
|
|
1711
|
+
parts.push(current.trim());
|
|
1712
|
+
current = "";
|
|
1713
|
+
} else {
|
|
1714
|
+
current += char;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (current.trim()) parts.push(current.trim());
|
|
1718
|
+
const shadows = parts.map(parseSingleCssToW3c).filter((s) => s !== null);
|
|
1719
|
+
if (shadows.length === 1) return shadows[0];
|
|
1720
|
+
return shadows;
|
|
1721
|
+
}
|
|
1722
|
+
function parseSingleCssToW3c(shadow) {
|
|
1723
|
+
const trimmed = shadow.trim();
|
|
1724
|
+
if (!trimmed) return null;
|
|
1725
|
+
const withoutInset = trimmed.replace(/^inset\s+/, "");
|
|
1726
|
+
let color = "rgb(0, 0, 0, 0.1)";
|
|
1727
|
+
let measurements = withoutInset;
|
|
1728
|
+
const colorPatterns = [
|
|
1729
|
+
/\s+((?:rgb|rgba|oklch|hsl|hsla)\([^)]+\))$/,
|
|
1730
|
+
/\s+(#[\da-fA-F]{3,8})$/,
|
|
1731
|
+
/\s+((?:black|white|transparent|currentColor))$/i
|
|
1732
|
+
];
|
|
1733
|
+
for (const pattern of colorPatterns) {
|
|
1734
|
+
const match = measurements.match(pattern);
|
|
1735
|
+
if (match) {
|
|
1736
|
+
color = match[1];
|
|
1737
|
+
measurements = measurements.slice(0, match.index).trim();
|
|
1738
|
+
break;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
const dims = measurements.split(/\s+/);
|
|
1742
|
+
if (dims.length < 2) return null;
|
|
1743
|
+
return {
|
|
1744
|
+
offsetX: dims[0] || "0px",
|
|
1745
|
+
offsetY: dims[1] || "0px",
|
|
1746
|
+
blur: dims[2] || "0px",
|
|
1747
|
+
spread: dims[3] || "0px",
|
|
1748
|
+
color
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
function buildDesignTokensJson(shadows) {
|
|
1752
|
+
const tokens = {};
|
|
1753
|
+
for (const shadow of shadows) {
|
|
1754
|
+
tokens[shadow.name] = {
|
|
1755
|
+
$type: "shadow",
|
|
1756
|
+
$value: cssToW3cShadow(shadow.value),
|
|
1757
|
+
...shadow.description ? { $description: shadow.description } : {}
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
return tokens;
|
|
1761
|
+
}
|
|
1762
|
+
async function writeDesignTokensFile(filePath, tokens) {
|
|
1763
|
+
const content = JSON.stringify(tokens, null, 2) + "\n";
|
|
1764
|
+
await fs7.writeFile(filePath, content, "utf-8");
|
|
1765
|
+
}
|
|
1766
|
+
async function updateDesignTokenShadow(filePath, tokenPath, newCssValue) {
|
|
1767
|
+
const content = await fs7.readFile(filePath, "utf-8");
|
|
1768
|
+
const tokens = JSON.parse(content);
|
|
1769
|
+
const pathParts = tokenPath.split(".");
|
|
1770
|
+
let current = tokens;
|
|
1771
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
1772
|
+
current = current[pathParts[i]];
|
|
1773
|
+
if (!current) throw new Error(`Token path "${tokenPath}" not found`);
|
|
1774
|
+
}
|
|
1775
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
1776
|
+
if (!current[lastKey]) {
|
|
1777
|
+
throw new Error(`Token "${tokenPath}" not found`);
|
|
1778
|
+
}
|
|
1779
|
+
current[lastKey].$value = cssToW3cShadow(newCssValue);
|
|
1780
|
+
await fs7.writeFile(filePath, JSON.stringify(tokens, null, 2) + "\n", "utf-8");
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/server/lib/scan-shadows.ts
|
|
1784
|
+
async function scanShadows(projectRoot, framework, styling) {
|
|
1785
|
+
const shadows = [];
|
|
1786
|
+
const allCssFiles = framework.cssFiles.length > 0 ? framework.cssFiles : styling.cssFiles;
|
|
1787
|
+
const cssFilePath = allCssFiles[0] || styling.scssFiles?.[0] || "";
|
|
1788
|
+
const customShadows = await scanCustomShadows(projectRoot, allCssFiles);
|
|
1789
|
+
const overriddenNames = new Set(customShadows.map((s) => s.name));
|
|
1790
|
+
if (styling.type === "tailwind-v4" || styling.type === "tailwind-v3") {
|
|
1791
|
+
addPresets(shadows, TAILWIND_SHADOW_PRESETS, overriddenNames);
|
|
1792
|
+
} else if (styling.type === "bootstrap") {
|
|
1793
|
+
await addBootstrapShadows(shadows, projectRoot, styling, overriddenNames);
|
|
1794
|
+
}
|
|
1795
|
+
const designTokenFiles = await findDesignTokenFiles(projectRoot);
|
|
1796
|
+
if (designTokenFiles.length > 0) {
|
|
1797
|
+
const tokenShadows = await scanDesignTokenShadows(projectRoot, designTokenFiles);
|
|
1798
|
+
for (const token of tokenShadows) {
|
|
1799
|
+
if (!overriddenNames.has(token.name)) {
|
|
1800
|
+
shadows.push({
|
|
1801
|
+
name: token.name,
|
|
1802
|
+
value: token.cssValue,
|
|
1803
|
+
source: "design-token",
|
|
1804
|
+
isOverridden: false,
|
|
1805
|
+
layers: parseShadowValue(token.cssValue),
|
|
1806
|
+
tokenPath: token.tokenPath,
|
|
1807
|
+
tokenFilePath: token.filePath
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
for (const custom of customShadows) {
|
|
1813
|
+
shadows.push({
|
|
1814
|
+
...custom,
|
|
1815
|
+
isOverridden: true
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
const sizeOrder = {
|
|
1819
|
+
"2xs": 0,
|
|
1820
|
+
"xs": 1,
|
|
1821
|
+
"sm": 2,
|
|
1822
|
+
"": 3,
|
|
1823
|
+
"md": 4,
|
|
1824
|
+
"lg": 5,
|
|
1825
|
+
"xl": 6,
|
|
1826
|
+
"2xl": 7
|
|
1827
|
+
};
|
|
1828
|
+
const naturalCollator = new Intl.Collator(void 0, { numeric: true, sensitivity: "base" });
|
|
1829
|
+
shadows.sort((a, b3) => {
|
|
1830
|
+
const order = { custom: 0, "design-token": 1, "framework-preset": 2 };
|
|
1831
|
+
const aOrder = order[a.source] ?? 3;
|
|
1832
|
+
const bOrder = order[b3.source] ?? 3;
|
|
1833
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
1834
|
+
const extractSize = (name) => {
|
|
1835
|
+
const match = name.match(/^[\w-]+-(\d*x[sl]|sm|md|lg)$/);
|
|
1836
|
+
if (match) return match[1];
|
|
1837
|
+
if (/^[a-z]+(-[a-z]+)*$/.test(name) && !name.includes("-inner") && !name.includes("-none")) {
|
|
1838
|
+
const parts = name.split("-");
|
|
1839
|
+
const last = parts[parts.length - 1];
|
|
1840
|
+
if (last in sizeOrder) return last;
|
|
1841
|
+
if (parts.length === 1 || !Object.keys(sizeOrder).includes(last)) return "";
|
|
1842
|
+
}
|
|
1843
|
+
return null;
|
|
1844
|
+
};
|
|
1845
|
+
const aSize = extractSize(a.name);
|
|
1846
|
+
const bSize = extractSize(b3.name);
|
|
1847
|
+
if (aSize !== null && bSize !== null) {
|
|
1848
|
+
const aIdx = sizeOrder[aSize] ?? 99;
|
|
1849
|
+
const bIdx = sizeOrder[bSize] ?? 99;
|
|
1850
|
+
if (aIdx !== bIdx) return aIdx - bIdx;
|
|
1851
|
+
}
|
|
1852
|
+
return naturalCollator.compare(a.name, b3.name);
|
|
1853
|
+
});
|
|
1854
|
+
return { shadows, cssFilePath, stylingType: styling.type, designTokenFiles };
|
|
1855
|
+
}
|
|
1856
|
+
function addPresets(shadows, presets, overriddenNames) {
|
|
1857
|
+
for (const preset of presets) {
|
|
1858
|
+
if (!overriddenNames.has(preset.name)) {
|
|
1859
|
+
shadows.push({
|
|
1860
|
+
name: preset.name,
|
|
1861
|
+
value: preset.value,
|
|
1862
|
+
source: "framework-preset",
|
|
1863
|
+
isOverridden: false,
|
|
1864
|
+
layers: parseShadowValue(preset.value),
|
|
1865
|
+
cssVariable: `--${preset.name}`
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
async function addBootstrapShadows(shadows, projectRoot, styling, overriddenNames) {
|
|
1871
|
+
const scssOverrides = await scanBootstrapScssOverrides(projectRoot, styling.scssFiles);
|
|
1872
|
+
const scssOverrideMap = new Map(scssOverrides.map((o) => [o.name, o]));
|
|
1873
|
+
const cssOverrides = await scanBootstrapCssOverrides(projectRoot, styling.cssFiles);
|
|
1874
|
+
const cssOverrideMap = new Map(cssOverrides.map((o) => [o.name, o]));
|
|
1875
|
+
for (const preset of BOOTSTRAP_SHADOW_PRESETS) {
|
|
1876
|
+
if (overriddenNames.has(preset.name)) continue;
|
|
1877
|
+
const scssOverride = scssOverrideMap.get(preset.name);
|
|
1878
|
+
const cssOverride = cssOverrideMap.get(preset.name);
|
|
1879
|
+
const override = cssOverride || scssOverride;
|
|
1880
|
+
if (override) {
|
|
1881
|
+
shadows.push({
|
|
1882
|
+
name: preset.name,
|
|
1883
|
+
value: override.value,
|
|
1884
|
+
source: "framework-preset",
|
|
1885
|
+
isOverridden: true,
|
|
1886
|
+
layers: parseShadowValue(override.value),
|
|
1887
|
+
cssVariable: override.cssVariable,
|
|
1888
|
+
sassVariable: override.sassVariable
|
|
1889
|
+
});
|
|
1890
|
+
} else {
|
|
1891
|
+
shadows.push({
|
|
1892
|
+
name: preset.name,
|
|
1893
|
+
value: preset.value,
|
|
1894
|
+
source: "framework-preset",
|
|
1895
|
+
isOverridden: false,
|
|
1896
|
+
layers: parseShadowValue(preset.value),
|
|
1897
|
+
cssVariable: `--bs-${preset.name}`,
|
|
1898
|
+
sassVariable: `$${preset.name}`
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
async function scanCustomShadows(projectRoot, cssFiles) {
|
|
1904
|
+
const shadows = [];
|
|
1905
|
+
for (const file of cssFiles) {
|
|
1906
|
+
try {
|
|
1907
|
+
const css = await fs8.readFile(path8.join(projectRoot, file), "utf-8");
|
|
1908
|
+
const rootTokens = parseBlock(css, ":root");
|
|
1909
|
+
for (const [name, value] of rootTokens) {
|
|
1910
|
+
if (name.includes("shadow") || isShadowValue(value)) {
|
|
1911
|
+
shadows.push({
|
|
1912
|
+
name: name.replace(/^--/, ""),
|
|
1913
|
+
value,
|
|
1914
|
+
source: "custom",
|
|
1915
|
+
isOverridden: true,
|
|
1916
|
+
layers: parseShadowValue(value),
|
|
1917
|
+
cssVariable: name
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
const themeMatch = css.match(/@theme\s*(?:inline\s*)?\{([\s\S]*?)\}/);
|
|
1922
|
+
if (themeMatch) {
|
|
1923
|
+
const themeBlock = themeMatch[1];
|
|
1924
|
+
const propRegex = /(--shadow[\w-]*)\s*:\s*([^;]+);/g;
|
|
1925
|
+
let match;
|
|
1926
|
+
while ((match = propRegex.exec(themeBlock)) !== null) {
|
|
1927
|
+
const name = match[1].replace(/^--/, "");
|
|
1928
|
+
if (!shadows.find((s) => s.name === name)) {
|
|
1929
|
+
shadows.push({
|
|
1930
|
+
name,
|
|
1931
|
+
value: match[2].trim(),
|
|
1932
|
+
source: "custom",
|
|
1933
|
+
isOverridden: true,
|
|
1934
|
+
layers: parseShadowValue(match[2].trim()),
|
|
1935
|
+
cssVariable: match[1]
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
} catch {
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return shadows;
|
|
1944
|
+
}
|
|
1945
|
+
function isShadowValue(value) {
|
|
1946
|
+
return /\d+px\s+\d+px/.test(value) || value.includes("inset");
|
|
1947
|
+
}
|
|
1948
|
+
function parseShadowValue(value) {
|
|
1949
|
+
if (!value || value === "none") return [];
|
|
1950
|
+
const parts = [];
|
|
1951
|
+
let depth = 0;
|
|
1952
|
+
let current = "";
|
|
1953
|
+
for (const char of value) {
|
|
1954
|
+
if (char === "(") depth++;
|
|
1955
|
+
if (char === ")") depth--;
|
|
1956
|
+
if (char === "," && depth === 0) {
|
|
1957
|
+
parts.push(current.trim());
|
|
1958
|
+
current = "";
|
|
1959
|
+
} else {
|
|
1960
|
+
current += char;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (current.trim()) parts.push(current.trim());
|
|
1964
|
+
return parts.map(parseSingleShadow).filter((s) => s !== null);
|
|
1965
|
+
}
|
|
1966
|
+
function parseSingleShadow(shadow) {
|
|
1967
|
+
const trimmed = shadow.trim();
|
|
1968
|
+
if (!trimmed) return null;
|
|
1969
|
+
const inset = trimmed.startsWith("inset");
|
|
1970
|
+
const withoutInset = inset ? trimmed.replace(/^inset\s*/, "") : trimmed;
|
|
1971
|
+
let color = "rgb(0 0 0 / 0.1)";
|
|
1972
|
+
let measurements = withoutInset;
|
|
1973
|
+
const colorPatterns = [
|
|
1974
|
+
/\s+((?:rgb|rgba|oklch|hsl|hsla)\([^)]+\))$/,
|
|
1975
|
+
/\s+(#[\da-fA-F]{3,8})$/,
|
|
1976
|
+
/\s+((?:black|white|transparent|currentColor))$/i
|
|
1977
|
+
];
|
|
1978
|
+
for (const pattern of colorPatterns) {
|
|
1979
|
+
const match = measurements.match(pattern);
|
|
1980
|
+
if (match) {
|
|
1981
|
+
color = match[1];
|
|
1982
|
+
measurements = measurements.slice(0, match.index).trim();
|
|
1983
|
+
break;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
const parts = measurements.split(/\s+/);
|
|
1987
|
+
if (parts.length < 2) return null;
|
|
1988
|
+
return {
|
|
1989
|
+
offsetX: parts[0] || "0",
|
|
1990
|
+
offsetY: parts[1] || "0",
|
|
1991
|
+
blur: parts[2] || "0",
|
|
1992
|
+
spread: parts[3] || "0",
|
|
1993
|
+
color,
|
|
1994
|
+
inset
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/server/lib/scan-borders.ts
|
|
1999
|
+
import fs9 from "fs/promises";
|
|
2000
|
+
import path9 from "path";
|
|
2001
|
+
async function scanBorders(projectRoot, framework, styling) {
|
|
2002
|
+
const borders = [];
|
|
2003
|
+
const allCssFiles = framework.cssFiles.length > 0 ? framework.cssFiles : styling.cssFiles;
|
|
2004
|
+
const cssFilePath = allCssFiles[0] || "";
|
|
2005
|
+
const customBorders = await scanCustomBorders(projectRoot, allCssFiles);
|
|
2006
|
+
const overriddenNames = new Set(customBorders.map((b3) => b3.name));
|
|
2007
|
+
if (styling.type === "tailwind-v4" || styling.type === "tailwind-v3") {
|
|
2008
|
+
addPresets2(borders, TAILWIND_RADIUS_PRESETS, overriddenNames);
|
|
2009
|
+
addPresets2(borders, TAILWIND_BORDER_WIDTH_PRESETS, overriddenNames);
|
|
2010
|
+
} else if (styling.type === "bootstrap") {
|
|
2011
|
+
addPresets2(borders, BOOTSTRAP_RADIUS_PRESETS, overriddenNames);
|
|
2012
|
+
addPresets2(borders, BOOTSTRAP_BORDER_WIDTH_PRESETS, overriddenNames);
|
|
2013
|
+
}
|
|
2014
|
+
for (const custom of customBorders) {
|
|
2015
|
+
borders.push({ ...custom, isOverridden: true });
|
|
2016
|
+
}
|
|
2017
|
+
const sizeOrder = {
|
|
2018
|
+
"xs": 0,
|
|
2019
|
+
"sm": 1,
|
|
2020
|
+
"": 2,
|
|
2021
|
+
"md": 3,
|
|
2022
|
+
"lg": 4,
|
|
2023
|
+
"xl": 5,
|
|
2024
|
+
"2xl": 6,
|
|
2025
|
+
"3xl": 7,
|
|
2026
|
+
"full": 8,
|
|
2027
|
+
"pill": 9,
|
|
2028
|
+
"xxl": 10
|
|
2029
|
+
};
|
|
2030
|
+
borders.sort((a, b3) => {
|
|
2031
|
+
if (a.kind !== b3.kind) return a.kind === "radius" ? -1 : 1;
|
|
2032
|
+
const order = { custom: 0, "framework-preset": 1 };
|
|
2033
|
+
const aOrder = order[a.source];
|
|
2034
|
+
const bOrder = order[b3.source];
|
|
2035
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
2036
|
+
const extractSize = (name) => {
|
|
2037
|
+
const match = name.match(/-(xs|sm|md|lg|xl|2xl|3xl|full|pill|xxl)$/);
|
|
2038
|
+
return match ? match[1] : "";
|
|
2039
|
+
};
|
|
2040
|
+
const aSize = sizeOrder[extractSize(a.name)] ?? 50;
|
|
2041
|
+
const bSize = sizeOrder[extractSize(b3.name)] ?? 50;
|
|
2042
|
+
if (aSize !== bSize) return aSize - bSize;
|
|
2043
|
+
return a.name.localeCompare(b3.name);
|
|
2044
|
+
});
|
|
2045
|
+
return { borders, cssFilePath, stylingType: styling.type };
|
|
2046
|
+
}
|
|
2047
|
+
function addPresets2(borders, presets, overriddenNames) {
|
|
2048
|
+
for (const preset of presets) {
|
|
2049
|
+
if (!overriddenNames.has(preset.name)) {
|
|
2050
|
+
borders.push({
|
|
2051
|
+
name: preset.name,
|
|
2052
|
+
value: preset.value,
|
|
2053
|
+
kind: preset.kind,
|
|
2054
|
+
source: "framework-preset",
|
|
2055
|
+
isOverridden: false,
|
|
2056
|
+
cssVariable: `--${preset.name}`
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
async function scanCustomBorders(projectRoot, cssFiles) {
|
|
2062
|
+
const borders = [];
|
|
2063
|
+
for (const file of cssFiles) {
|
|
2064
|
+
try {
|
|
2065
|
+
const css = await fs9.readFile(path9.join(projectRoot, file), "utf-8");
|
|
2066
|
+
const rootTokens = parseBlock(css, ":root");
|
|
2067
|
+
for (const [name, value] of rootTokens) {
|
|
2068
|
+
const border = classifyBorderToken(name, value);
|
|
2069
|
+
if (border) borders.push(border);
|
|
2070
|
+
}
|
|
2071
|
+
const themeMatch = css.match(/@theme\s*(?:inline\s*)?\{([\s\S]*?)\}/);
|
|
2072
|
+
if (themeMatch) {
|
|
2073
|
+
const themeBlock = themeMatch[1];
|
|
2074
|
+
const propRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
|
|
2075
|
+
let match;
|
|
2076
|
+
while ((match = propRegex.exec(themeBlock)) !== null) {
|
|
2077
|
+
const name = match[1];
|
|
2078
|
+
const value = match[2].trim();
|
|
2079
|
+
if (!borders.find((b3) => b3.name === name.replace(/^--/, ""))) {
|
|
2080
|
+
const border = classifyBorderToken(name, value);
|
|
2081
|
+
if (border) borders.push(border);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
} catch {
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return borders;
|
|
2089
|
+
}
|
|
2090
|
+
function classifyBorderToken(name, value) {
|
|
2091
|
+
const cleanName = name.replace(/^--/, "");
|
|
2092
|
+
if (value.includes("oklch") || value.includes("hsl") || value.includes("rgb") || value.startsWith("#")) {
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
if (cleanName.includes("radius")) {
|
|
2096
|
+
return {
|
|
2097
|
+
name: cleanName,
|
|
2098
|
+
value,
|
|
2099
|
+
kind: "radius",
|
|
2100
|
+
source: "custom",
|
|
2101
|
+
isOverridden: false,
|
|
2102
|
+
cssVariable: name.startsWith("--") ? name : `--${name}`
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
if (cleanName.includes("border-width") || cleanName.includes("border") && /^\d/.test(value)) {
|
|
2106
|
+
return {
|
|
2107
|
+
name: cleanName,
|
|
2108
|
+
value,
|
|
2109
|
+
kind: "width",
|
|
2110
|
+
source: "custom",
|
|
2111
|
+
isOverridden: false,
|
|
2112
|
+
cssVariable: name.startsWith("--") ? name : `--${name}`
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// src/server/lib/scan-gradients.ts
|
|
2119
|
+
import fs10 from "fs/promises";
|
|
2120
|
+
import path10 from "path";
|
|
2121
|
+
async function scanGradients(projectRoot, framework, styling) {
|
|
2122
|
+
const gradients = [];
|
|
2123
|
+
const allCssFiles = framework.cssFiles.length > 0 ? framework.cssFiles : styling.cssFiles;
|
|
2124
|
+
const cssFilePath = allCssFiles[0] || "";
|
|
2125
|
+
for (const file of allCssFiles) {
|
|
2126
|
+
try {
|
|
2127
|
+
const css = await fs10.readFile(path10.join(projectRoot, file), "utf-8");
|
|
2128
|
+
const rootTokens = parseBlock(css, ":root");
|
|
2129
|
+
for (const [name, value] of rootTokens) {
|
|
2130
|
+
if (isGradientValue(name, value)) {
|
|
2131
|
+
gradients.push({
|
|
2132
|
+
name: name.replace(/^--/, ""),
|
|
2133
|
+
value,
|
|
2134
|
+
cssVariable: name
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const themeMatch = css.match(/@theme\s*(?:inline\s*)?\{([\s\S]*?)\}/);
|
|
2139
|
+
if (themeMatch) {
|
|
2140
|
+
const themeBlock = themeMatch[1];
|
|
2141
|
+
const propRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
|
|
2142
|
+
let match;
|
|
2143
|
+
while ((match = propRegex.exec(themeBlock)) !== null) {
|
|
2144
|
+
const name = match[1];
|
|
2145
|
+
const value = match[2].trim();
|
|
2146
|
+
if (isGradientValue(name, value) && !gradients.find((g) => g.cssVariable === name)) {
|
|
2147
|
+
gradients.push({
|
|
2148
|
+
name: name.replace(/^--/, ""),
|
|
2149
|
+
value,
|
|
2150
|
+
cssVariable: name
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
} catch {
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return { gradients, cssFilePath, stylingType: styling.type };
|
|
2159
|
+
}
|
|
2160
|
+
function isGradientValue(name, value) {
|
|
2161
|
+
if (name.includes("gradient")) return true;
|
|
2162
|
+
if (value.includes("linear-gradient") || value.includes("radial-gradient") || value.includes("conic-gradient")) {
|
|
2163
|
+
return true;
|
|
2164
|
+
}
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// src/server/lib/scan-usages.ts
|
|
2169
|
+
import fs11 from "fs/promises";
|
|
2170
|
+
import path11 from "path";
|
|
2171
|
+
var PAGE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
|
|
2172
|
+
var LAYOUT_FILES = ["layout", "page"];
|
|
2173
|
+
async function scanUsages(projectRoot, framework) {
|
|
2174
|
+
if (!framework.appDirExists) {
|
|
2175
|
+
return { usages: {} };
|
|
2176
|
+
}
|
|
2177
|
+
const absAppDir = path11.join(projectRoot, framework.appDir);
|
|
2178
|
+
const usages = {};
|
|
2179
|
+
const routeFiles = [];
|
|
2180
|
+
await walkAppDir(absAppDir, absAppDir, "", routeFiles);
|
|
2181
|
+
for (const { file, route } of routeFiles) {
|
|
2182
|
+
const absFile = path11.join(absAppDir, file);
|
|
2183
|
+
let source;
|
|
2184
|
+
try {
|
|
2185
|
+
source = await fs11.readFile(absFile, "utf-8");
|
|
2186
|
+
} catch {
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
const importedComponents = extractImportedComponents(source, framework.componentDir);
|
|
2190
|
+
for (const comp of importedComponents) {
|
|
2191
|
+
if (!usages[comp]) usages[comp] = /* @__PURE__ */ new Set();
|
|
2192
|
+
usages[comp].add(route);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
const result = {};
|
|
2196
|
+
for (const [comp, routes] of Object.entries(usages)) {
|
|
2197
|
+
result[comp] = Array.from(routes).sort().map((route) => {
|
|
2198
|
+
const rf = routeFiles.find((r) => r.route === route);
|
|
2199
|
+
return {
|
|
2200
|
+
route,
|
|
2201
|
+
file: rf ? path11.join(framework.appDir, rf.file) : ""
|
|
2202
|
+
};
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
return { usages: result };
|
|
2206
|
+
}
|
|
2207
|
+
async function walkAppDir(baseDir, currentDir, routePrefix, results) {
|
|
2208
|
+
let entries;
|
|
2209
|
+
try {
|
|
2210
|
+
entries = await fs11.readdir(currentDir, { withFileTypes: true });
|
|
2211
|
+
} catch {
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
for (const base of LAYOUT_FILES) {
|
|
2215
|
+
for (const ext of PAGE_EXTENSIONS) {
|
|
2216
|
+
const candidate = `${base}${ext}`;
|
|
2217
|
+
if (entries.some((e) => e.isFile() && e.name === candidate)) {
|
|
2218
|
+
const relFile = path11.relative(baseDir, path11.join(currentDir, candidate));
|
|
2219
|
+
const route = routePrefix || "/";
|
|
2220
|
+
results.push({ file: relFile, route });
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
for (const entry of entries) {
|
|
2225
|
+
if (!entry.isDirectory()) continue;
|
|
2226
|
+
const dirName = entry.name;
|
|
2227
|
+
if (dirName.startsWith("_") || dirName.startsWith(".")) continue;
|
|
2228
|
+
if (dirName === "api") continue;
|
|
2229
|
+
const subDir = path11.join(currentDir, dirName);
|
|
2230
|
+
if (dirName.startsWith("(") && dirName.endsWith(")")) {
|
|
2231
|
+
await walkAppDir(baseDir, subDir, routePrefix, results);
|
|
2232
|
+
} else if (dirName.startsWith("@")) {
|
|
2233
|
+
await walkAppDir(baseDir, subDir, routePrefix, results);
|
|
2234
|
+
} else {
|
|
2235
|
+
let segment = dirName;
|
|
2236
|
+
const routePart = `${routePrefix}/${segment}`;
|
|
2237
|
+
await walkAppDir(baseDir, subDir, routePart, results);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function extractImportedComponents(source, componentDir) {
|
|
2242
|
+
const components = [];
|
|
2243
|
+
const importRegex = /import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+["']([^"']+)["']/g;
|
|
2244
|
+
let match;
|
|
2245
|
+
const compDirParts = componentDir.split("/");
|
|
2246
|
+
const compDirName = compDirParts[compDirParts.length - 1];
|
|
2247
|
+
const compDirParent = compDirParts.length > 1 ? compDirParts[compDirParts.length - 2] : "";
|
|
2248
|
+
while ((match = importRegex.exec(source)) !== null) {
|
|
2249
|
+
const namedImports = match[1];
|
|
2250
|
+
const defaultImport = match[2];
|
|
2251
|
+
const fromPath = match[3];
|
|
2252
|
+
const isComponentImport = isFromComponentDir(fromPath, componentDir, compDirParent, compDirName);
|
|
2253
|
+
if (!isComponentImport) continue;
|
|
2254
|
+
if (namedImports) {
|
|
2255
|
+
const names = namedImports.split(",").map((s) => s.trim());
|
|
2256
|
+
for (const name of names) {
|
|
2257
|
+
if (name.startsWith("type ")) continue;
|
|
2258
|
+
const actualName = name.split(/\s+as\s+/)[0].trim();
|
|
2259
|
+
if (actualName && /^[A-Z]/.test(actualName)) {
|
|
2260
|
+
components.push(actualName);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
} else if (defaultImport && /^[A-Z]/.test(defaultImport)) {
|
|
2264
|
+
components.push(defaultImport);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
return components;
|
|
2268
|
+
}
|
|
2269
|
+
function isFromComponentDir(importPath, componentDir, _parentDir, leafDir) {
|
|
2270
|
+
if (importPath.startsWith("@/")) {
|
|
2271
|
+
const resolved = importPath.slice(2);
|
|
2272
|
+
return resolved.startsWith(componentDir + "/") || resolved === componentDir;
|
|
2273
|
+
}
|
|
2274
|
+
if (importPath.startsWith("~/")) {
|
|
2275
|
+
const resolved = importPath.slice(2);
|
|
2276
|
+
return resolved.startsWith(componentDir + "/") || resolved === componentDir;
|
|
2277
|
+
}
|
|
2278
|
+
if (importPath.startsWith(".")) {
|
|
2279
|
+
return importPath.includes(`/${componentDir}/`) || importPath.endsWith(`/${componentDir}`);
|
|
2280
|
+
}
|
|
2281
|
+
if (importPath.includes(`/${leafDir}/`)) {
|
|
2282
|
+
return true;
|
|
2283
|
+
}
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// src/server/lib/scanner.ts
|
|
2288
|
+
var cachedScan = null;
|
|
2289
|
+
async function runScan(projectRoot) {
|
|
2290
|
+
const framework = await detectFramework(projectRoot);
|
|
2291
|
+
const styling = await detectStylingSystem(projectRoot, framework);
|
|
2292
|
+
const [tokens, components, shadows, borders, gradients, usages] = await Promise.all([
|
|
2293
|
+
scanTokens(projectRoot, framework),
|
|
2294
|
+
scanComponents(projectRoot),
|
|
2295
|
+
scanShadows(projectRoot, framework, styling),
|
|
2296
|
+
scanBorders(projectRoot, framework, styling),
|
|
2297
|
+
scanGradients(projectRoot, framework, styling),
|
|
2298
|
+
scanUsages(projectRoot, framework)
|
|
2299
|
+
]);
|
|
2300
|
+
cachedScan = { framework, tokens, components, shadows, borders, gradients, styling, usages };
|
|
2301
|
+
return cachedScan;
|
|
2302
|
+
}
|
|
2303
|
+
function patchCachedScan(key, value) {
|
|
2304
|
+
if (cachedScan) {
|
|
2305
|
+
cachedScan = { ...cachedScan, [key]: value };
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
async function rescanTokens(projectRoot) {
|
|
2309
|
+
const scan = cachedScan || await runScan(projectRoot);
|
|
2310
|
+
const tokens = await scanTokens(projectRoot, scan.framework);
|
|
2311
|
+
patchCachedScan("tokens", tokens);
|
|
2312
|
+
return tokens;
|
|
2313
|
+
}
|
|
2314
|
+
async function rescanShadows(projectRoot) {
|
|
2315
|
+
const scan = cachedScan || await runScan(projectRoot);
|
|
2316
|
+
const shadows = await scanShadows(projectRoot, scan.framework, scan.styling);
|
|
2317
|
+
patchCachedScan("shadows", shadows);
|
|
2318
|
+
return shadows;
|
|
2319
|
+
}
|
|
2320
|
+
async function rescanBorders(projectRoot) {
|
|
2321
|
+
const scan = cachedScan || await runScan(projectRoot);
|
|
2322
|
+
const borders = await scanBorders(projectRoot, scan.framework, scan.styling);
|
|
2323
|
+
patchCachedScan("borders", borders);
|
|
2324
|
+
return borders;
|
|
2325
|
+
}
|
|
2326
|
+
async function rescanGradients(projectRoot) {
|
|
2327
|
+
const scan = cachedScan || await runScan(projectRoot);
|
|
2328
|
+
const gradients = await scanGradients(projectRoot, scan.framework, scan.styling);
|
|
2329
|
+
patchCachedScan("gradients", gradients);
|
|
2330
|
+
return gradients;
|
|
2331
|
+
}
|
|
2332
|
+
function createScanRouter(projectRoot) {
|
|
2333
|
+
const router = Router2();
|
|
2334
|
+
runScan(projectRoot).then(() => {
|
|
2335
|
+
console.log(" Project scanned successfully");
|
|
2336
|
+
}).catch((err) => {
|
|
2337
|
+
console.error(" Scan error:", err.message);
|
|
2338
|
+
});
|
|
2339
|
+
router.get("/all", async (_req, res) => {
|
|
2340
|
+
try {
|
|
2341
|
+
const result = cachedScan || await runScan(projectRoot);
|
|
2342
|
+
res.json(result);
|
|
2343
|
+
} catch (err) {
|
|
2344
|
+
res.status(500).json({ error: err.message });
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
router.get("/tokens", async (_req, res) => {
|
|
2348
|
+
try {
|
|
2349
|
+
const result = cachedScan || await runScan(projectRoot);
|
|
2350
|
+
res.json(result.tokens);
|
|
2351
|
+
} catch (err) {
|
|
2352
|
+
res.status(500).json({ error: err.message });
|
|
2353
|
+
}
|
|
2354
|
+
});
|
|
2355
|
+
router.get("/components", async (_req, res) => {
|
|
2356
|
+
try {
|
|
2357
|
+
const result = cachedScan || await runScan(projectRoot);
|
|
2358
|
+
res.json(result.components);
|
|
2359
|
+
} catch (err) {
|
|
2360
|
+
res.status(500).json({ error: err.message });
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
router.get("/gradients", async (_req, res) => {
|
|
2364
|
+
try {
|
|
2365
|
+
const result = cachedScan || await runScan(projectRoot);
|
|
2366
|
+
res.json(result.gradients);
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
res.status(500).json({ error: err.message });
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
2371
|
+
router.get("/borders", async (_req, res) => {
|
|
2372
|
+
try {
|
|
2373
|
+
const result = cachedScan || await runScan(projectRoot);
|
|
2374
|
+
res.json(result.borders);
|
|
2375
|
+
} catch (err) {
|
|
2376
|
+
res.status(500).json({ error: err.message });
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
router.get("/shadows", async (_req, res) => {
|
|
2380
|
+
try {
|
|
2381
|
+
const result = cachedScan || await runScan(projectRoot);
|
|
2382
|
+
res.json(result.shadows);
|
|
2383
|
+
} catch (err) {
|
|
2384
|
+
res.status(500).json({ error: err.message });
|
|
2385
|
+
}
|
|
2386
|
+
});
|
|
2387
|
+
router.get("/usages", async (_req, res) => {
|
|
2388
|
+
try {
|
|
2389
|
+
const result = cachedScan || await runScan(projectRoot);
|
|
2390
|
+
res.json(result.usages);
|
|
2391
|
+
} catch (err) {
|
|
2392
|
+
res.status(500).json({ error: err.message });
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2395
|
+
router.post("/rescan", async (_req, res) => {
|
|
2396
|
+
try {
|
|
2397
|
+
const result = await runScan(projectRoot);
|
|
2398
|
+
res.json(result);
|
|
2399
|
+
} catch (err) {
|
|
2400
|
+
res.status(500).json({ error: err.message });
|
|
2401
|
+
}
|
|
2402
|
+
});
|
|
2403
|
+
router.post("/rescan/tokens", async (_req, res) => {
|
|
2404
|
+
try {
|
|
2405
|
+
const tokens = await rescanTokens(projectRoot);
|
|
2406
|
+
res.json(tokens);
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
res.status(500).json({ error: err.message });
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
router.post("/rescan/shadows", async (_req, res) => {
|
|
2412
|
+
try {
|
|
2413
|
+
const shadows = await rescanShadows(projectRoot);
|
|
2414
|
+
res.json(shadows);
|
|
2415
|
+
} catch (err) {
|
|
2416
|
+
res.status(500).json({ error: err.message });
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
router.post("/rescan/borders", async (_req, res) => {
|
|
2420
|
+
try {
|
|
2421
|
+
const borders = await rescanBorders(projectRoot);
|
|
2422
|
+
res.json(borders);
|
|
2423
|
+
} catch (err) {
|
|
2424
|
+
res.status(500).json({ error: err.message });
|
|
2425
|
+
}
|
|
2426
|
+
});
|
|
2427
|
+
router.post("/rescan/gradients", async (_req, res) => {
|
|
2428
|
+
try {
|
|
2429
|
+
const gradients = await rescanGradients(projectRoot);
|
|
2430
|
+
res.json(gradients);
|
|
2431
|
+
} catch (err) {
|
|
2432
|
+
res.status(500).json({ error: err.message });
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
router.get("/resolve-route", async (req, res) => {
|
|
2436
|
+
try {
|
|
2437
|
+
const routePath = req.query.path || "/";
|
|
2438
|
+
const scan = cachedScan || await runScan(projectRoot);
|
|
2439
|
+
const appDir = scan.framework.appDir;
|
|
2440
|
+
const result = await resolveRouteToFile(projectRoot, appDir, routePath);
|
|
2441
|
+
res.json({ filePath: result });
|
|
2442
|
+
} catch (err) {
|
|
2443
|
+
res.status(500).json({ error: err.message });
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2446
|
+
return router;
|
|
2447
|
+
}
|
|
2448
|
+
var PAGE_EXTENSIONS2 = [".tsx", ".jsx", ".ts", ".js"];
|
|
2449
|
+
async function resolveRouteToFile(projectRoot, appDir, routePath) {
|
|
2450
|
+
const segments = routePath === "/" ? [] : routePath.replace(/^\//, "").replace(/\/$/, "").split("/");
|
|
2451
|
+
const absAppDir = path12.join(projectRoot, appDir);
|
|
2452
|
+
const result = await matchSegments(absAppDir, segments, 0);
|
|
2453
|
+
if (result) {
|
|
2454
|
+
return path12.relative(projectRoot, result);
|
|
2455
|
+
}
|
|
2456
|
+
return null;
|
|
2457
|
+
}
|
|
2458
|
+
async function findPageFile(dir) {
|
|
2459
|
+
for (const ext of PAGE_EXTENSIONS2) {
|
|
2460
|
+
const candidate = path12.join(dir, `page${ext}`);
|
|
2461
|
+
try {
|
|
2462
|
+
await fs12.access(candidate);
|
|
2463
|
+
return candidate;
|
|
2464
|
+
} catch {
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
for (const ext of PAGE_EXTENSIONS2) {
|
|
2468
|
+
const candidate = path12.join(dir, `index${ext}`);
|
|
2469
|
+
try {
|
|
2470
|
+
await fs12.access(candidate);
|
|
2471
|
+
return candidate;
|
|
2472
|
+
} catch {
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return null;
|
|
2476
|
+
}
|
|
2477
|
+
async function listDirs(dir) {
|
|
2478
|
+
try {
|
|
2479
|
+
const entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
2480
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2481
|
+
} catch {
|
|
2482
|
+
return [];
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
async function matchSegments(currentDir, segments, index) {
|
|
2486
|
+
if (index >= segments.length) {
|
|
2487
|
+
const page = await findPageFile(currentDir);
|
|
2488
|
+
if (page) return page;
|
|
2489
|
+
const dirs2 = await listDirs(currentDir);
|
|
2490
|
+
for (const d of dirs2) {
|
|
2491
|
+
if (d.startsWith("(") && d.endsWith(")")) {
|
|
2492
|
+
const page2 = await findPageFile(path12.join(currentDir, d));
|
|
2493
|
+
if (page2) return page2;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
return null;
|
|
2497
|
+
}
|
|
2498
|
+
const segment = segments[index];
|
|
2499
|
+
const dirs = await listDirs(currentDir);
|
|
2500
|
+
if (dirs.includes(segment)) {
|
|
2501
|
+
const result = await matchSegments(path12.join(currentDir, segment), segments, index + 1);
|
|
2502
|
+
if (result) return result;
|
|
2503
|
+
}
|
|
2504
|
+
for (const d of dirs) {
|
|
2505
|
+
if (d.startsWith("(") && d.endsWith(")")) {
|
|
2506
|
+
const result = await matchSegments(path12.join(currentDir, d), segments, index);
|
|
2507
|
+
if (result) return result;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
for (const d of dirs) {
|
|
2511
|
+
if (d.startsWith("[") && d.endsWith("]") && !d.startsWith("[...") && !d.startsWith("[[")) {
|
|
2512
|
+
const result = await matchSegments(path12.join(currentDir, d), segments, index + 1);
|
|
2513
|
+
if (result) return result;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
for (const d of dirs) {
|
|
2517
|
+
if (d.startsWith("[...") && d.endsWith("]")) {
|
|
2518
|
+
const page = await findPageFile(path12.join(currentDir, d));
|
|
2519
|
+
if (page) return page;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
for (const d of dirs) {
|
|
2523
|
+
if (d.startsWith("[[...") && d.endsWith("]]")) {
|
|
2524
|
+
const page = await findPageFile(path12.join(currentDir, d));
|
|
2525
|
+
if (page) return page;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
return null;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// src/server/api/write-tokens.ts
|
|
2532
|
+
function createTokensRouter(projectRoot) {
|
|
2533
|
+
const router = Router3();
|
|
2534
|
+
router.post("/", async (req, res) => {
|
|
2535
|
+
try {
|
|
2536
|
+
const { filePath, token, value, selector } = req.body;
|
|
2537
|
+
const fullPath = safePath(projectRoot, filePath);
|
|
2538
|
+
let css = await fs13.readFile(fullPath, "utf-8");
|
|
2539
|
+
css = replaceTokenInBlock(css, selector, token, value);
|
|
2540
|
+
await fs13.writeFile(fullPath, css, "utf-8");
|
|
2541
|
+
const tokens = await rescanTokens(projectRoot);
|
|
2542
|
+
res.json({ ok: true, filePath, token, value, tokens });
|
|
2543
|
+
} catch (err) {
|
|
2544
|
+
console.error("Token write error:", err);
|
|
2545
|
+
res.status(500).json({ error: err.message });
|
|
2546
|
+
}
|
|
2547
|
+
});
|
|
2548
|
+
return router;
|
|
2549
|
+
}
|
|
2550
|
+
function replaceTokenInBlock(css, selector, token, newValue) {
|
|
2551
|
+
const selectorEscaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2552
|
+
const blockStart = css.search(new RegExp(`${selectorEscaped}\\s*\\{`));
|
|
2553
|
+
if (blockStart === -1) {
|
|
2554
|
+
throw new Error(`Selector "${selector}" not found in CSS file`);
|
|
2555
|
+
}
|
|
2556
|
+
const openBrace = css.indexOf("{", blockStart);
|
|
2557
|
+
let depth = 1;
|
|
2558
|
+
let pos = openBrace + 1;
|
|
2559
|
+
while (depth > 0 && pos < css.length) {
|
|
2560
|
+
if (css[pos] === "{") depth++;
|
|
2561
|
+
if (css[pos] === "}") depth--;
|
|
2562
|
+
pos++;
|
|
2563
|
+
}
|
|
2564
|
+
const blockEnd = pos;
|
|
2565
|
+
let block = css.slice(openBrace + 1, blockEnd - 1);
|
|
2566
|
+
const tokenEscaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2567
|
+
const tokenRegex = new RegExp(
|
|
2568
|
+
`(${tokenEscaped}\\s*:\\s*)([^;]+)(;)`,
|
|
2569
|
+
"g"
|
|
2570
|
+
);
|
|
2571
|
+
if (!tokenRegex.test(block)) {
|
|
2572
|
+
throw new Error(`Token "${token}" not found in "${selector}" block`);
|
|
2573
|
+
}
|
|
2574
|
+
block = block.replace(tokenRegex, `$1${newValue}$3`);
|
|
2575
|
+
return css.slice(0, openBrace + 1) + block + css.slice(blockEnd - 1);
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// src/server/api/write-component.ts
|
|
2579
|
+
import { Router as Router4 } from "express";
|
|
2580
|
+
import fs14 from "fs/promises";
|
|
2581
|
+
function createComponentRouter(projectRoot) {
|
|
2582
|
+
const router = Router4();
|
|
2583
|
+
router.post("/", async (req, res) => {
|
|
2584
|
+
try {
|
|
2585
|
+
const { filePath, oldClass, newClass, variantContext } = req.body;
|
|
2586
|
+
const fullPath = safePath(projectRoot, filePath);
|
|
2587
|
+
let source = await fs14.readFile(fullPath, "utf-8");
|
|
2588
|
+
source = replaceClassInComponent(source, oldClass, newClass, variantContext);
|
|
2589
|
+
await fs14.writeFile(fullPath, source, "utf-8");
|
|
2590
|
+
res.json({ ok: true, filePath, oldClass, newClass });
|
|
2591
|
+
} catch (err) {
|
|
2592
|
+
console.error("Component write error:", err);
|
|
2593
|
+
res.status(500).json({ error: err.message });
|
|
2594
|
+
}
|
|
2595
|
+
});
|
|
2596
|
+
return router;
|
|
2597
|
+
}
|
|
2598
|
+
function replaceClassInComponent(source, oldClass, newClass, variantContext) {
|
|
2599
|
+
if (variantContext) {
|
|
2600
|
+
const variantIndex = source.indexOf(`${variantContext}:`);
|
|
2601
|
+
if (variantIndex === -1) {
|
|
2602
|
+
const quotedIndex = source.indexOf(`"${variantContext}":`);
|
|
2603
|
+
if (quotedIndex === -1) {
|
|
2604
|
+
throw new Error(`Variant context "${variantContext}" not found`);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
const oldClassEscaped = oldClass.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2609
|
+
const classRegex = new RegExp(
|
|
2610
|
+
`(["'\`][^"'\`]*?)\\b${oldClassEscaped}\\b([^"'\`]*?["'\`])`,
|
|
2611
|
+
"g"
|
|
2612
|
+
);
|
|
2613
|
+
let replaced = false;
|
|
2614
|
+
if (variantContext) {
|
|
2615
|
+
const variantPattern = new RegExp(
|
|
2616
|
+
`(${variantContext.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"\\s]*:[\\s]*)(["'\`])([^"'\`]*?)\\b${oldClassEscaped}\\b([^"'\`]*?)(\\2)`,
|
|
2617
|
+
"g"
|
|
2618
|
+
);
|
|
2619
|
+
if (variantPattern.test(source)) {
|
|
2620
|
+
source = source.replace(
|
|
2621
|
+
variantPattern,
|
|
2622
|
+
`$1$2$3${newClass}$4$5`
|
|
2623
|
+
);
|
|
2624
|
+
replaced = true;
|
|
2625
|
+
}
|
|
2626
|
+
if (!replaced) {
|
|
2627
|
+
const quotedVariantPattern = new RegExp(
|
|
2628
|
+
`(["']${variantContext.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*:\\s*)(["'\`])([^"'\`]*?)\\b${oldClassEscaped}\\b([^"'\`]*?)(\\2)`,
|
|
2629
|
+
"g"
|
|
2630
|
+
);
|
|
2631
|
+
if (quotedVariantPattern.test(source)) {
|
|
2632
|
+
source = source.replace(
|
|
2633
|
+
quotedVariantPattern,
|
|
2634
|
+
`$1$2$3${newClass}$4$5`
|
|
2635
|
+
);
|
|
2636
|
+
replaced = true;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (!replaced) {
|
|
2641
|
+
const count = (source.match(classRegex) || []).length;
|
|
2642
|
+
if (count === 0) {
|
|
2643
|
+
throw new Error(
|
|
2644
|
+
`Class "${oldClass}" not found in component file`
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
if (count > 1 && !variantContext) {
|
|
2648
|
+
const cvaResult = replaceInCvaBase(source, oldClass, newClass);
|
|
2649
|
+
if (cvaResult !== false) {
|
|
2650
|
+
return cvaResult;
|
|
2651
|
+
}
|
|
2652
|
+
source = source.replace(classRegex, `$1${newClass}$2`);
|
|
2653
|
+
} else {
|
|
2654
|
+
source = source.replace(classRegex, `$1${newClass}$2`);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
return source;
|
|
2658
|
+
}
|
|
2659
|
+
function replaceInCvaBase(source, oldClass, newClass) {
|
|
2660
|
+
const cvaIndex = source.indexOf("cva(");
|
|
2661
|
+
if (cvaIndex === -1) return false;
|
|
2662
|
+
const afterCva = source.substring(cvaIndex + 4);
|
|
2663
|
+
const quoteMatch = afterCva.match(/^\s*(["'`])/);
|
|
2664
|
+
if (!quoteMatch) return false;
|
|
2665
|
+
const quote = quoteMatch[1];
|
|
2666
|
+
const quoteStart = cvaIndex + 4 + quoteMatch[0].length - 1;
|
|
2667
|
+
const afterQuote = source.substring(quoteStart + 1);
|
|
2668
|
+
const closeIndex = afterQuote.indexOf(quote);
|
|
2669
|
+
if (closeIndex === -1) return false;
|
|
2670
|
+
const baseString = source.substring(quoteStart + 1, quoteStart + 1 + closeIndex);
|
|
2671
|
+
const oldClassEscaped = oldClass.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2672
|
+
const regex = new RegExp(`(?<=^|\\s)${oldClassEscaped}(?=$|\\s)`, "g");
|
|
2673
|
+
if (!regex.test(baseString)) return false;
|
|
2674
|
+
const newBaseString = baseString.replace(
|
|
2675
|
+
new RegExp(`(?<=^|\\s)${oldClassEscaped}(?=$|\\s)`, "g"),
|
|
2676
|
+
newClass
|
|
2677
|
+
);
|
|
2678
|
+
return source.substring(0, quoteStart + 1) + newBaseString + source.substring(quoteStart + 1 + closeIndex);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// src/server/api/write-shadows.ts
|
|
2682
|
+
import { Router as Router5 } from "express";
|
|
2683
|
+
import fs15 from "fs/promises";
|
|
2684
|
+
import path13 from "path";
|
|
2685
|
+
function safePath2(projectRoot, filePath) {
|
|
2686
|
+
const resolved = path13.resolve(projectRoot, filePath);
|
|
2687
|
+
if (!resolved.startsWith(projectRoot)) {
|
|
2688
|
+
throw new Error(`Path "${filePath}" escapes project root`);
|
|
2689
|
+
}
|
|
2690
|
+
return resolved;
|
|
2691
|
+
}
|
|
2692
|
+
function createShadowsRouter(projectRoot) {
|
|
2693
|
+
const router = Router5();
|
|
2694
|
+
router.post("/", async (req, res) => {
|
|
2695
|
+
try {
|
|
2696
|
+
const { filePath, variableName, value, selector } = req.body;
|
|
2697
|
+
const fullPath = safePath2(projectRoot, filePath);
|
|
2698
|
+
if (selector === "scss") {
|
|
2699
|
+
let scss = await fs15.readFile(fullPath, "utf-8");
|
|
2700
|
+
scss = writeShadowToScss(scss, variableName, value);
|
|
2701
|
+
await fs15.writeFile(fullPath, scss, "utf-8");
|
|
2702
|
+
} else if (selector === "@theme") {
|
|
2703
|
+
let css = await fs15.readFile(fullPath, "utf-8");
|
|
2704
|
+
css = writeShadowToTheme(css, variableName, value);
|
|
2705
|
+
await fs15.writeFile(fullPath, css, "utf-8");
|
|
2706
|
+
} else {
|
|
2707
|
+
let css = await fs15.readFile(fullPath, "utf-8");
|
|
2708
|
+
css = writeShadowToSelector(css, selector, variableName, value);
|
|
2709
|
+
await fs15.writeFile(fullPath, css, "utf-8");
|
|
2710
|
+
}
|
|
2711
|
+
const shadows = await rescanShadows(projectRoot);
|
|
2712
|
+
res.json({ ok: true, filePath, variableName, value, shadows });
|
|
2713
|
+
} catch (err) {
|
|
2714
|
+
console.error("Shadow write error:", err);
|
|
2715
|
+
res.status(500).json({ error: err.message });
|
|
2716
|
+
}
|
|
2717
|
+
});
|
|
2718
|
+
router.post("/create", async (req, res) => {
|
|
2719
|
+
try {
|
|
2720
|
+
const { filePath, variableName, value, selector } = req.body;
|
|
2721
|
+
const fullPath = safePath2(projectRoot, filePath);
|
|
2722
|
+
if (selector === "scss") {
|
|
2723
|
+
let scss;
|
|
2724
|
+
try {
|
|
2725
|
+
scss = await fs15.readFile(fullPath, "utf-8");
|
|
2726
|
+
} catch {
|
|
2727
|
+
scss = "";
|
|
2728
|
+
}
|
|
2729
|
+
scss = addShadowToScss(scss, variableName, value);
|
|
2730
|
+
await fs15.writeFile(fullPath, scss, "utf-8");
|
|
2731
|
+
} else if (selector === "@theme") {
|
|
2732
|
+
let css = await fs15.readFile(fullPath, "utf-8");
|
|
2733
|
+
css = addShadowToTheme(css, variableName, value);
|
|
2734
|
+
await fs15.writeFile(fullPath, css, "utf-8");
|
|
2735
|
+
} else {
|
|
2736
|
+
let css = await fs15.readFile(fullPath, "utf-8");
|
|
2737
|
+
css = addShadowToSelector(css, selector, variableName, value);
|
|
2738
|
+
await fs15.writeFile(fullPath, css, "utf-8");
|
|
2739
|
+
}
|
|
2740
|
+
const shadows = await rescanShadows(projectRoot);
|
|
2741
|
+
res.json({ ok: true, filePath, variableName, value, shadows });
|
|
2742
|
+
} catch (err) {
|
|
2743
|
+
console.error("Shadow create error:", err);
|
|
2744
|
+
res.status(500).json({ error: err.message });
|
|
2745
|
+
}
|
|
2746
|
+
});
|
|
2747
|
+
router.post("/design-token", async (req, res) => {
|
|
2748
|
+
try {
|
|
2749
|
+
const { filePath, tokenPath, value } = req.body;
|
|
2750
|
+
const fullPath = safePath2(projectRoot, filePath);
|
|
2751
|
+
await updateDesignTokenShadow(fullPath, tokenPath, value);
|
|
2752
|
+
const shadows = await rescanShadows(projectRoot);
|
|
2753
|
+
res.json({ ok: true, filePath, tokenPath, value, shadows });
|
|
2754
|
+
} catch (err) {
|
|
2755
|
+
console.error("Design token write error:", err);
|
|
2756
|
+
res.status(500).json({ error: err.message });
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
router.post("/export-tokens", async (req, res) => {
|
|
2760
|
+
try {
|
|
2761
|
+
const { filePath, shadows } = req.body;
|
|
2762
|
+
const fullPath = safePath2(projectRoot, filePath);
|
|
2763
|
+
const tokens = buildDesignTokensJson(shadows);
|
|
2764
|
+
await fs15.mkdir(path13.dirname(fullPath), { recursive: true });
|
|
2765
|
+
await writeDesignTokensFile(fullPath, tokens);
|
|
2766
|
+
res.json({ ok: true, filePath, tokenCount: shadows.length });
|
|
2767
|
+
} catch (err) {
|
|
2768
|
+
console.error("Token export error:", err);
|
|
2769
|
+
res.status(500).json({ error: err.message });
|
|
2770
|
+
}
|
|
2771
|
+
});
|
|
2772
|
+
return router;
|
|
2773
|
+
}
|
|
2774
|
+
function writeShadowToSelector(css, selector, variableName, newValue) {
|
|
2775
|
+
const selectorEscaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2776
|
+
const blockStart = css.search(new RegExp(`${selectorEscaped}\\s*\\{`));
|
|
2777
|
+
if (blockStart === -1) {
|
|
2778
|
+
throw new Error(`Selector "${selector}" not found in CSS file`);
|
|
2779
|
+
}
|
|
2780
|
+
const openBrace = css.indexOf("{", blockStart);
|
|
2781
|
+
let depth = 1;
|
|
2782
|
+
let pos = openBrace + 1;
|
|
2783
|
+
while (depth > 0 && pos < css.length) {
|
|
2784
|
+
if (css[pos] === "{") depth++;
|
|
2785
|
+
if (css[pos] === "}") depth--;
|
|
2786
|
+
pos++;
|
|
2787
|
+
}
|
|
2788
|
+
const blockEnd = pos;
|
|
2789
|
+
let block = css.slice(openBrace + 1, blockEnd - 1);
|
|
2790
|
+
const varEscaped = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2791
|
+
const tokenRegex = new RegExp(`(${varEscaped}\\s*:\\s*)([^;]+)(;)`, "g");
|
|
2792
|
+
const original = block;
|
|
2793
|
+
block = block.replace(tokenRegex, `$1${newValue}$3`);
|
|
2794
|
+
if (block === original) {
|
|
2795
|
+
throw new Error(`Variable "${variableName}" not found in "${selector}" block`);
|
|
2796
|
+
}
|
|
2797
|
+
return css.slice(0, openBrace + 1) + block + css.slice(blockEnd - 1);
|
|
2798
|
+
}
|
|
2799
|
+
function writeShadowToTheme(css, variableName, newValue) {
|
|
2800
|
+
const themeMatch = css.match(/@theme\s*(?:inline\s*)?\{/);
|
|
2801
|
+
if (!themeMatch) {
|
|
2802
|
+
throw new Error("No @theme block found in CSS file");
|
|
2803
|
+
}
|
|
2804
|
+
const blockStart = themeMatch.index;
|
|
2805
|
+
const openBrace = css.indexOf("{", blockStart);
|
|
2806
|
+
let depth = 1;
|
|
2807
|
+
let pos = openBrace + 1;
|
|
2808
|
+
while (depth > 0 && pos < css.length) {
|
|
2809
|
+
if (css[pos] === "{") depth++;
|
|
2810
|
+
if (css[pos] === "}") depth--;
|
|
2811
|
+
pos++;
|
|
2812
|
+
}
|
|
2813
|
+
const blockEnd = pos;
|
|
2814
|
+
let block = css.slice(openBrace + 1, blockEnd - 1);
|
|
2815
|
+
const varEscaped = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2816
|
+
const tokenRegex = new RegExp(`(${varEscaped}\\s*:\\s*)([^;]+)(;)`, "g");
|
|
2817
|
+
const original = block;
|
|
2818
|
+
block = block.replace(tokenRegex, `$1${newValue}$3`);
|
|
2819
|
+
if (block === original) {
|
|
2820
|
+
block = block.trimEnd() + `
|
|
2821
|
+
${variableName}: ${newValue};
|
|
2822
|
+
`;
|
|
2823
|
+
}
|
|
2824
|
+
return css.slice(0, openBrace + 1) + block + css.slice(blockEnd - 1);
|
|
2825
|
+
}
|
|
2826
|
+
function addShadowToSelector(css, selector, variableName, value) {
|
|
2827
|
+
const selectorEscaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2828
|
+
const blockStart = css.search(new RegExp(`${selectorEscaped}\\s*\\{`));
|
|
2829
|
+
if (blockStart === -1) {
|
|
2830
|
+
return css + `
|
|
2831
|
+
${selector} {
|
|
2832
|
+
${variableName}: ${value};
|
|
2833
|
+
}
|
|
2834
|
+
`;
|
|
2835
|
+
}
|
|
2836
|
+
const openBrace = css.indexOf("{", blockStart);
|
|
2837
|
+
let depth = 1;
|
|
2838
|
+
let pos = openBrace + 1;
|
|
2839
|
+
while (depth > 0 && pos < css.length) {
|
|
2840
|
+
if (css[pos] === "{") depth++;
|
|
2841
|
+
if (css[pos] === "}") depth--;
|
|
2842
|
+
pos++;
|
|
2843
|
+
}
|
|
2844
|
+
const blockEnd = pos;
|
|
2845
|
+
const block = css.slice(openBrace + 1, blockEnd - 1);
|
|
2846
|
+
const newBlock = block.trimEnd() + `
|
|
2847
|
+
${variableName}: ${value};
|
|
2848
|
+
`;
|
|
2849
|
+
return css.slice(0, openBrace + 1) + newBlock + css.slice(blockEnd - 1);
|
|
2850
|
+
}
|
|
2851
|
+
function addShadowToTheme(css, variableName, value) {
|
|
2852
|
+
const themeMatch = css.match(/@theme\s*(?:inline\s*)?\{/);
|
|
2853
|
+
if (!themeMatch) {
|
|
2854
|
+
return css + `
|
|
2855
|
+
@theme {
|
|
2856
|
+
${variableName}: ${value};
|
|
2857
|
+
}
|
|
2858
|
+
`;
|
|
2859
|
+
}
|
|
2860
|
+
return writeShadowToTheme(css, variableName, value);
|
|
2861
|
+
}
|
|
2862
|
+
function writeShadowToScss(scss, variableName, newValue) {
|
|
2863
|
+
const varEscaped = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2864
|
+
const regex = new RegExp(
|
|
2865
|
+
`(${varEscaped}\\s*:\\s*)(.+?)(\\s*(?:!default)?\\s*;)`,
|
|
2866
|
+
"g"
|
|
2867
|
+
);
|
|
2868
|
+
const result = scss.replace(regex, `$1${newValue}$3`);
|
|
2869
|
+
if (result === scss) {
|
|
2870
|
+
throw new Error(`Sass variable "${variableName}" not found in SCSS file`);
|
|
2871
|
+
}
|
|
2872
|
+
return result;
|
|
2873
|
+
}
|
|
2874
|
+
function addShadowToScss(scss, variableName, value) {
|
|
2875
|
+
const line = `${variableName}: ${value};
|
|
2876
|
+
`;
|
|
2877
|
+
return scss.endsWith("\n") ? scss + line : scss + "\n" + line;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
// src/server/api/write-gradients.ts
|
|
2881
|
+
import { Router as Router6 } from "express";
|
|
2882
|
+
import fs16 from "fs/promises";
|
|
2883
|
+
import path14 from "path";
|
|
2884
|
+
function safePath3(projectRoot, filePath) {
|
|
2885
|
+
const resolved = path14.resolve(projectRoot, filePath);
|
|
2886
|
+
if (!resolved.startsWith(projectRoot)) {
|
|
2887
|
+
throw new Error(`Path "${filePath}" escapes project root`);
|
|
2888
|
+
}
|
|
2889
|
+
return resolved;
|
|
2890
|
+
}
|
|
2891
|
+
function createGradientsRouter(projectRoot) {
|
|
2892
|
+
const router = Router6();
|
|
2893
|
+
router.post("/", async (req, res) => {
|
|
2894
|
+
try {
|
|
2895
|
+
const { filePath, variableName, value, selector } = req.body;
|
|
2896
|
+
const fullPath = safePath3(projectRoot, filePath);
|
|
2897
|
+
let css = await fs16.readFile(fullPath, "utf-8");
|
|
2898
|
+
if (selector === "@theme") {
|
|
2899
|
+
css = writeToTheme(css, variableName, value);
|
|
2900
|
+
} else {
|
|
2901
|
+
css = writeToSelector(css, selector, variableName, value);
|
|
2902
|
+
}
|
|
2903
|
+
await fs16.writeFile(fullPath, css, "utf-8");
|
|
2904
|
+
const [gradients, borders] = await Promise.all([
|
|
2905
|
+
rescanGradients(projectRoot),
|
|
2906
|
+
rescanBorders(projectRoot)
|
|
2907
|
+
]);
|
|
2908
|
+
res.json({ ok: true, filePath, variableName, value, gradients, borders });
|
|
2909
|
+
} catch (err) {
|
|
2910
|
+
console.error("Gradient write error:", err);
|
|
2911
|
+
res.status(500).json({ error: err.message });
|
|
2912
|
+
}
|
|
2913
|
+
});
|
|
2914
|
+
router.post("/create", async (req, res) => {
|
|
2915
|
+
try {
|
|
2916
|
+
const { filePath, variableName, value, selector } = req.body;
|
|
2917
|
+
const fullPath = safePath3(projectRoot, filePath);
|
|
2918
|
+
let css = await fs16.readFile(fullPath, "utf-8");
|
|
2919
|
+
if (selector === "@theme") {
|
|
2920
|
+
css = addToTheme(css, variableName, value);
|
|
2921
|
+
} else {
|
|
2922
|
+
css = addToSelector(css, selector, variableName, value);
|
|
2923
|
+
}
|
|
2924
|
+
await fs16.writeFile(fullPath, css, "utf-8");
|
|
2925
|
+
const [gradients, borders] = await Promise.all([
|
|
2926
|
+
rescanGradients(projectRoot),
|
|
2927
|
+
rescanBorders(projectRoot)
|
|
2928
|
+
]);
|
|
2929
|
+
res.json({ ok: true, filePath, variableName, value, gradients, borders });
|
|
2930
|
+
} catch (err) {
|
|
2931
|
+
console.error("Gradient create error:", err);
|
|
2932
|
+
res.status(500).json({ error: err.message });
|
|
2933
|
+
}
|
|
2934
|
+
});
|
|
2935
|
+
router.post("/delete", async (req, res) => {
|
|
2936
|
+
try {
|
|
2937
|
+
const { filePath, variableName, selector } = req.body;
|
|
2938
|
+
const fullPath = safePath3(projectRoot, filePath);
|
|
2939
|
+
let css = await fs16.readFile(fullPath, "utf-8");
|
|
2940
|
+
css = deleteFromBlock(css, selector, variableName);
|
|
2941
|
+
await fs16.writeFile(fullPath, css, "utf-8");
|
|
2942
|
+
const [gradients, borders] = await Promise.all([
|
|
2943
|
+
rescanGradients(projectRoot),
|
|
2944
|
+
rescanBorders(projectRoot)
|
|
2945
|
+
]);
|
|
2946
|
+
res.json({ ok: true, filePath, variableName, gradients, borders });
|
|
2947
|
+
} catch (err) {
|
|
2948
|
+
console.error("Gradient delete error:", err);
|
|
2949
|
+
res.status(500).json({ error: err.message });
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
return router;
|
|
2953
|
+
}
|
|
2954
|
+
function writeToSelector(css, selector, variableName, newValue) {
|
|
2955
|
+
const selectorEscaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2956
|
+
const blockStart = css.search(new RegExp(`${selectorEscaped}\\s*\\{`));
|
|
2957
|
+
if (blockStart === -1) {
|
|
2958
|
+
return css + `
|
|
2959
|
+
${selector} {
|
|
2960
|
+
${variableName}: ${newValue};
|
|
2961
|
+
}
|
|
2962
|
+
`;
|
|
2963
|
+
}
|
|
2964
|
+
const openBrace = css.indexOf("{", blockStart);
|
|
2965
|
+
let depth = 1;
|
|
2966
|
+
let pos = openBrace + 1;
|
|
2967
|
+
while (depth > 0 && pos < css.length) {
|
|
2968
|
+
if (css[pos] === "{") depth++;
|
|
2969
|
+
if (css[pos] === "}") depth--;
|
|
2970
|
+
pos++;
|
|
2971
|
+
}
|
|
2972
|
+
const blockEnd = pos;
|
|
2973
|
+
let block = css.slice(openBrace + 1, blockEnd - 1);
|
|
2974
|
+
const varEscaped = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2975
|
+
const tokenRegex = new RegExp(`(${varEscaped}\\s*:\\s*)([^;]+)(;)`, "g");
|
|
2976
|
+
const original = block;
|
|
2977
|
+
block = block.replace(tokenRegex, `$1${newValue}$3`);
|
|
2978
|
+
if (block === original) {
|
|
2979
|
+
block = block.trimEnd() + `
|
|
2980
|
+
${variableName}: ${newValue};
|
|
2981
|
+
`;
|
|
2982
|
+
}
|
|
2983
|
+
return css.slice(0, openBrace + 1) + block + css.slice(blockEnd - 1);
|
|
2984
|
+
}
|
|
2985
|
+
function writeToTheme(css, variableName, newValue) {
|
|
2986
|
+
const themeMatch = css.match(/@theme\s*(?:inline\s*)?\{/);
|
|
2987
|
+
if (!themeMatch) {
|
|
2988
|
+
return css + `
|
|
2989
|
+
@theme {
|
|
2990
|
+
${variableName}: ${newValue};
|
|
2991
|
+
}
|
|
2992
|
+
`;
|
|
2993
|
+
}
|
|
2994
|
+
const blockStart = themeMatch.index;
|
|
2995
|
+
const openBrace = css.indexOf("{", blockStart);
|
|
2996
|
+
let depth = 1;
|
|
2997
|
+
let pos = openBrace + 1;
|
|
2998
|
+
while (depth > 0 && pos < css.length) {
|
|
2999
|
+
if (css[pos] === "{") depth++;
|
|
3000
|
+
if (css[pos] === "}") depth--;
|
|
3001
|
+
pos++;
|
|
3002
|
+
}
|
|
3003
|
+
const blockEnd = pos;
|
|
3004
|
+
let block = css.slice(openBrace + 1, blockEnd - 1);
|
|
3005
|
+
const varEscaped = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3006
|
+
const tokenRegex = new RegExp(`(${varEscaped}\\s*:\\s*)([^;]+)(;)`, "g");
|
|
3007
|
+
const original = block;
|
|
3008
|
+
block = block.replace(tokenRegex, `$1${newValue}$3`);
|
|
3009
|
+
if (block === original) {
|
|
3010
|
+
block = block.trimEnd() + `
|
|
3011
|
+
${variableName}: ${newValue};
|
|
3012
|
+
`;
|
|
3013
|
+
}
|
|
3014
|
+
return css.slice(0, openBrace + 1) + block + css.slice(blockEnd - 1);
|
|
3015
|
+
}
|
|
3016
|
+
function addToTheme(css, variableName, value) {
|
|
3017
|
+
return writeToTheme(css, variableName, value);
|
|
3018
|
+
}
|
|
3019
|
+
function addToSelector(css, selector, variableName, value) {
|
|
3020
|
+
return writeToSelector(css, selector, variableName, value);
|
|
3021
|
+
}
|
|
3022
|
+
function deleteFromBlock(css, selector, variableName) {
|
|
3023
|
+
let blockRegex;
|
|
3024
|
+
if (selector === "@theme") {
|
|
3025
|
+
blockRegex = /@theme\s*(?:inline\s*)?\{/;
|
|
3026
|
+
} else {
|
|
3027
|
+
const selectorEscaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3028
|
+
blockRegex = new RegExp(`${selectorEscaped}\\s*\\{`);
|
|
3029
|
+
}
|
|
3030
|
+
const match = css.match(blockRegex);
|
|
3031
|
+
if (!match) return css;
|
|
3032
|
+
const blockStart = match.index;
|
|
3033
|
+
const openBrace = css.indexOf("{", blockStart);
|
|
3034
|
+
let depth = 1;
|
|
3035
|
+
let pos = openBrace + 1;
|
|
3036
|
+
while (depth > 0 && pos < css.length) {
|
|
3037
|
+
if (css[pos] === "{") depth++;
|
|
3038
|
+
if (css[pos] === "}") depth--;
|
|
3039
|
+
pos++;
|
|
3040
|
+
}
|
|
3041
|
+
const blockEnd = pos;
|
|
3042
|
+
let block = css.slice(openBrace + 1, blockEnd - 1);
|
|
3043
|
+
const varEscaped = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3044
|
+
block = block.replace(new RegExp(`\\n?\\s*${varEscaped}\\s*:[^;]+;`, "g"), "");
|
|
3045
|
+
return css.slice(0, openBrace + 1) + block + css.slice(blockEnd - 1);
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
// src/server/index.ts
|
|
3049
|
+
async function createServer(config) {
|
|
3050
|
+
const app = express();
|
|
3051
|
+
app.use("/api", express.json());
|
|
3052
|
+
app.use("/scan", express.json());
|
|
3053
|
+
app.get("/api/config", (_req, res) => {
|
|
3054
|
+
res.json({
|
|
3055
|
+
targetUrl: `http://localhost:${config.targetPort}`,
|
|
3056
|
+
stylingType: config.stylingType,
|
|
3057
|
+
projectRoot: config.projectRoot
|
|
3058
|
+
});
|
|
3059
|
+
});
|
|
3060
|
+
app.use(
|
|
3061
|
+
"/api/write-element",
|
|
3062
|
+
createWriteElementRouter({
|
|
3063
|
+
projectRoot: config.projectRoot,
|
|
3064
|
+
stylingType: config.stylingType
|
|
3065
|
+
})
|
|
3066
|
+
);
|
|
3067
|
+
app.use("/api/tokens", createTokensRouter(config.projectRoot));
|
|
3068
|
+
app.use("/api/component", createComponentRouter(config.projectRoot));
|
|
3069
|
+
app.use("/api/shadows", createShadowsRouter(config.projectRoot));
|
|
3070
|
+
app.use("/api/gradients", createGradientsRouter(config.projectRoot));
|
|
3071
|
+
app.get("/api/open-file", (req, res) => {
|
|
3072
|
+
const file = req.query.file;
|
|
3073
|
+
const line = req.query.line;
|
|
3074
|
+
const col = req.query.col;
|
|
3075
|
+
if (!file) {
|
|
3076
|
+
res.status(400).json({ error: "Missing file" });
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
const absPath = path15.isAbsolute(file) ? file : path15.join(config.projectRoot, file);
|
|
3080
|
+
const lineCol = line ? `:${line}${col ? `:${col}` : ""}` : "";
|
|
3081
|
+
const platform = process.platform;
|
|
3082
|
+
const tryOpen = () => {
|
|
3083
|
+
const editors = [
|
|
3084
|
+
{ cmd: "code", arg: `--goto "${absPath}${lineCol}"` },
|
|
3085
|
+
{ cmd: "cursor", arg: `--goto "${absPath}${lineCol}"` }
|
|
3086
|
+
];
|
|
3087
|
+
let idx = 0;
|
|
3088
|
+
const attempt = () => {
|
|
3089
|
+
if (idx >= editors.length) {
|
|
3090
|
+
const openCmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
3091
|
+
exec(`${openCmd} "${absPath}"`);
|
|
3092
|
+
res.json({ ok: true, editor: "system" });
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
const { cmd, arg } = editors[idx];
|
|
3096
|
+
exec(`which ${cmd}`, (err) => {
|
|
3097
|
+
if (!err) {
|
|
3098
|
+
exec(`${cmd} ${arg}`);
|
|
3099
|
+
res.json({ ok: true, editor: cmd });
|
|
3100
|
+
} else {
|
|
3101
|
+
idx++;
|
|
3102
|
+
attempt();
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
};
|
|
3106
|
+
attempt();
|
|
3107
|
+
};
|
|
3108
|
+
tryOpen();
|
|
3109
|
+
});
|
|
3110
|
+
app.use("/scan", createScanRouter(config.projectRoot));
|
|
3111
|
+
const __dirname = path15.dirname(fileURLToPath(import.meta.url));
|
|
3112
|
+
const clientDistPath = path15.join(__dirname, "client");
|
|
3113
|
+
const isDev = !fs17.existsSync(path15.join(clientDistPath, "index.html"));
|
|
3114
|
+
let viteDevServer = null;
|
|
3115
|
+
if (isDev) {
|
|
3116
|
+
const { createServer: createViteServer } = await import("vite");
|
|
3117
|
+
const viteRoot = path15.resolve(__dirname, "../client");
|
|
3118
|
+
viteDevServer = await createViteServer({
|
|
3119
|
+
configFile: path15.resolve(__dirname, "../../vite.config.ts"),
|
|
3120
|
+
server: {
|
|
3121
|
+
middlewareMode: true,
|
|
3122
|
+
hmr: { port: 24679 }
|
|
3123
|
+
},
|
|
3124
|
+
appType: "custom"
|
|
3125
|
+
});
|
|
3126
|
+
app.use(viteDevServer.middlewares);
|
|
3127
|
+
app.use(async (req, res, next) => {
|
|
3128
|
+
try {
|
|
3129
|
+
const url = req.originalUrl || "/";
|
|
3130
|
+
const htmlPath = path15.join(viteRoot, "index.html");
|
|
3131
|
+
let html = fs17.readFileSync(htmlPath, "utf-8");
|
|
3132
|
+
html = await viteDevServer.transformIndexHtml(url, html);
|
|
3133
|
+
res.status(200).set({ "Content-Type": "text/html" }).end(html);
|
|
3134
|
+
} catch (err) {
|
|
3135
|
+
viteDevServer.ssrFixStacktrace(err);
|
|
3136
|
+
next(err);
|
|
3137
|
+
}
|
|
3138
|
+
});
|
|
3139
|
+
} else {
|
|
3140
|
+
app.use(express.static(clientDistPath));
|
|
3141
|
+
app.use((_req, res) => {
|
|
3142
|
+
res.sendFile(path15.join(clientDistPath, "index.html"));
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
return { app, viteDevServer };
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// src/cli.ts
|
|
3149
|
+
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
3150
|
+
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
3151
|
+
var red = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
3152
|
+
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
3153
|
+
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
3154
|
+
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
3155
|
+
async function promptPort(message, options) {
|
|
3156
|
+
const rl = readline.createInterface({ input: process2.stdin, output: process2.stdout });
|
|
3157
|
+
console.log("");
|
|
3158
|
+
console.log(` ${yellow("?")} ${message}`);
|
|
3159
|
+
for (let i = 0; i < options.length; i++) {
|
|
3160
|
+
console.log(` ${cyan(String(i + 1))}. http://localhost:${options[i]}`);
|
|
3161
|
+
}
|
|
3162
|
+
console.log(` ${cyan(String(options.length + 1))}. Enter a different port`);
|
|
3163
|
+
console.log("");
|
|
3164
|
+
return new Promise((resolve) => {
|
|
3165
|
+
rl.question(` ${dim("Choice [1]:")} `, (answer) => {
|
|
3166
|
+
rl.close();
|
|
3167
|
+
const trimmed = answer.trim();
|
|
3168
|
+
if (!trimmed || trimmed === "1") {
|
|
3169
|
+
resolve(options[0]);
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
const idx = parseInt(trimmed, 10);
|
|
3173
|
+
if (idx >= 1 && idx <= options.length) {
|
|
3174
|
+
resolve(options[idx - 1]);
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
if (idx === options.length + 1) {
|
|
3178
|
+
const rl2 = readline.createInterface({ input: process2.stdin, output: process2.stdout });
|
|
3179
|
+
rl2.question(` ${dim("Port:")} `, (portAnswer) => {
|
|
3180
|
+
rl2.close();
|
|
3181
|
+
const port = parseInt(portAnswer.trim(), 10);
|
|
3182
|
+
resolve(port || options[0]);
|
|
3183
|
+
});
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
const directPort = parseInt(trimmed, 10);
|
|
3187
|
+
if (directPort > 1e3) {
|
|
3188
|
+
resolve(directPort);
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
resolve(options[0]);
|
|
3192
|
+
});
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
async function main() {
|
|
3196
|
+
const args = process2.argv.slice(2);
|
|
3197
|
+
let targetPort = 3e3;
|
|
3198
|
+
let toolPort = 4400;
|
|
3199
|
+
for (let i = 0; i < args.length; i++) {
|
|
3200
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
3201
|
+
targetPort = parseInt(args[i + 1], 10);
|
|
3202
|
+
i++;
|
|
3203
|
+
}
|
|
3204
|
+
if (args[i] === "--tool-port" && args[i + 1]) {
|
|
3205
|
+
toolPort = parseInt(args[i + 1], 10);
|
|
3206
|
+
i++;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
const projectRoot = process2.cwd();
|
|
3210
|
+
console.log("");
|
|
3211
|
+
console.log(` ${bold("@designtools/codesurface")}`);
|
|
3212
|
+
console.log(` ${dim(projectRoot)}`);
|
|
3213
|
+
console.log("");
|
|
3214
|
+
const pkgPath = path16.join(projectRoot, "package.json");
|
|
3215
|
+
if (!fs18.existsSync(pkgPath)) {
|
|
3216
|
+
console.log(` ${red("\u2717")} No package.json found in ${projectRoot}`);
|
|
3217
|
+
console.log(` ${dim("Run this command from the root of the app you want to edit.")}`);
|
|
3218
|
+
console.log(` ${dim("All file reads and writes are scoped to this directory.")}`);
|
|
3219
|
+
console.log("");
|
|
3220
|
+
process2.exit(1);
|
|
3221
|
+
}
|
|
3222
|
+
const framework = await detectFramework(projectRoot);
|
|
3223
|
+
const frameworkLabel = framework.name === "nextjs" ? "Next.js" : framework.name === "remix" ? "Remix" : framework.name === "vite" ? "Vite" : "Unknown";
|
|
3224
|
+
console.log(` ${green("\u2713")} Framework ${frameworkLabel}`);
|
|
3225
|
+
if (framework.appDirExists) {
|
|
3226
|
+
console.log(` ${green("\u2713")} App dir ${framework.appDir}/`);
|
|
3227
|
+
} else {
|
|
3228
|
+
console.log(` ${yellow("\u26A0")} App dir ${dim("not found \u2014 route detection won't be available")}`);
|
|
3229
|
+
}
|
|
3230
|
+
if (framework.componentDirExists) {
|
|
3231
|
+
console.log(
|
|
3232
|
+
` ${green("\u2713")} Components ${framework.componentDir}/ ${dim(`(${framework.componentFileCount} files)`)}`
|
|
3233
|
+
);
|
|
3234
|
+
} else {
|
|
3235
|
+
console.log(` ${yellow("\u26A0")} Components ${dim("not found \u2014 component editing won't be available")}`);
|
|
3236
|
+
}
|
|
3237
|
+
if (framework.cssFiles.length > 0) {
|
|
3238
|
+
console.log(` ${green("\u2713")} CSS files ${framework.cssFiles[0]}`);
|
|
3239
|
+
} else {
|
|
3240
|
+
console.log(` ${yellow("\u26A0")} CSS files ${dim("no CSS files found")}`);
|
|
3241
|
+
}
|
|
3242
|
+
const styling = await detectStylingSystem(projectRoot, framework);
|
|
3243
|
+
const stylingLabels = {
|
|
3244
|
+
"tailwind-v4": "Tailwind CSS v4",
|
|
3245
|
+
"tailwind-v3": "Tailwind CSS v3",
|
|
3246
|
+
"bootstrap": "Bootstrap",
|
|
3247
|
+
"css-variables": "CSS Custom Properties",
|
|
3248
|
+
"plain-css": "Plain CSS",
|
|
3249
|
+
"unknown": "Unknown"
|
|
3250
|
+
};
|
|
3251
|
+
const stylingLabel = stylingLabels[styling.type];
|
|
3252
|
+
if (styling.type !== "unknown") {
|
|
3253
|
+
console.log(` ${green("\u2713")} Styling ${stylingLabel}`);
|
|
3254
|
+
} else {
|
|
3255
|
+
console.log(` ${yellow("\u26A0")} Styling ${dim("no styling system detected")}`);
|
|
3256
|
+
}
|
|
3257
|
+
console.log("");
|
|
3258
|
+
const scanPorts = [targetPort, targetPort + 1, targetPort + 2];
|
|
3259
|
+
let targetReachable = false;
|
|
3260
|
+
let waited = false;
|
|
3261
|
+
async function findReachablePorts() {
|
|
3262
|
+
const reachable = [];
|
|
3263
|
+
for (const port of scanPorts) {
|
|
3264
|
+
try {
|
|
3265
|
+
await fetch(`http://localhost:${port}`, { signal: AbortSignal.timeout(1e3) });
|
|
3266
|
+
reachable.push(port);
|
|
3267
|
+
} catch {
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
return reachable;
|
|
3271
|
+
}
|
|
3272
|
+
let reachablePorts = [];
|
|
3273
|
+
for (let attempt = 0; attempt < 15; attempt++) {
|
|
3274
|
+
reachablePorts = await findReachablePorts();
|
|
3275
|
+
if (reachablePorts.length > 0) {
|
|
3276
|
+
targetReachable = true;
|
|
3277
|
+
break;
|
|
3278
|
+
}
|
|
3279
|
+
if (attempt === 0) {
|
|
3280
|
+
process2.stdout.write(` ${dim("Waiting for dev server on port " + scanPorts.join("/") + "...")}`);
|
|
3281
|
+
waited = true;
|
|
3282
|
+
}
|
|
3283
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
3284
|
+
}
|
|
3285
|
+
if (waited) process2.stdout.write("\r\x1B[K");
|
|
3286
|
+
if (!targetReachable) {
|
|
3287
|
+
console.log("");
|
|
3288
|
+
console.log(` ${red("\u2717")} No dev server on port ${scanPorts.join(", ")}`);
|
|
3289
|
+
console.log(` ${dim("Start your dev server first, then run this command.")}`);
|
|
3290
|
+
console.log(` ${dim(`Use --port to specify a different port.`)}`);
|
|
3291
|
+
console.log("");
|
|
3292
|
+
process2.exit(1);
|
|
3293
|
+
}
|
|
3294
|
+
if (reachablePorts.length === 1 && reachablePorts[0] === targetPort) {
|
|
3295
|
+
console.log(` ${green("\u2713")} Target http://localhost:${targetPort}`);
|
|
3296
|
+
} else if (reachablePorts.length === 1) {
|
|
3297
|
+
const found = reachablePorts[0];
|
|
3298
|
+
console.log(` ${yellow("\u26A0")} Target http://localhost:${found} ${dim(`(port ${targetPort} not reachable, found server on ${found})`)}`);
|
|
3299
|
+
targetPort = found;
|
|
3300
|
+
} else {
|
|
3301
|
+
targetPort = await promptPort(
|
|
3302
|
+
`Multiple servers found. Which is your dev app?`,
|
|
3303
|
+
reachablePorts
|
|
3304
|
+
);
|
|
3305
|
+
console.log(` ${green("\u2713")} Target http://localhost:${targetPort}`);
|
|
3306
|
+
}
|
|
3307
|
+
const { app, viteDevServer } = await createServer({
|
|
3308
|
+
targetPort,
|
|
3309
|
+
toolPort,
|
|
3310
|
+
projectRoot,
|
|
3311
|
+
stylingType: styling.type
|
|
3312
|
+
});
|
|
3313
|
+
const httpServer = await new Promise((resolve, reject) => {
|
|
3314
|
+
let attempts = 0;
|
|
3315
|
+
const maxAttempts = 5;
|
|
3316
|
+
function tryListen(port) {
|
|
3317
|
+
const server = app.listen(port);
|
|
3318
|
+
server.on("listening", () => {
|
|
3319
|
+
if (port !== toolPort) {
|
|
3320
|
+
console.log(` ${yellow("\u26A0")} Tool http://localhost:${port} ${dim(`(port ${toolPort} was busy)`)}`);
|
|
3321
|
+
} else {
|
|
3322
|
+
console.log(` ${green("\u2713")} Tool http://localhost:${port}`);
|
|
3323
|
+
}
|
|
3324
|
+
toolPort = port;
|
|
3325
|
+
resolve(server);
|
|
3326
|
+
});
|
|
3327
|
+
server.on("error", (err) => {
|
|
3328
|
+
if (err.code === "EADDRINUSE" && attempts < maxAttempts) {
|
|
3329
|
+
attempts++;
|
|
3330
|
+
tryListen(port + 1);
|
|
3331
|
+
} else {
|
|
3332
|
+
reject(err);
|
|
3333
|
+
}
|
|
3334
|
+
});
|
|
3335
|
+
}
|
|
3336
|
+
tryListen(toolPort);
|
|
3337
|
+
});
|
|
3338
|
+
console.log("");
|
|
3339
|
+
console.log(` ${dim("All file writes are scoped to:")} ${bold(projectRoot)}`);
|
|
3340
|
+
console.log("");
|
|
3341
|
+
open(`http://localhost:${toolPort}`);
|
|
3342
|
+
const shutdown = () => {
|
|
3343
|
+
console.log(`
|
|
3344
|
+
${dim("Shutting down...")}`);
|
|
3345
|
+
if (viteDevServer) {
|
|
3346
|
+
viteDevServer.close().catch(() => {
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
httpServer.close(() => {
|
|
3350
|
+
process2.exit(0);
|
|
3351
|
+
});
|
|
3352
|
+
setTimeout(() => process2.exit(0), 3e3);
|
|
3353
|
+
};
|
|
3354
|
+
process2.on("SIGINT", shutdown);
|
|
3355
|
+
process2.on("SIGTERM", shutdown);
|
|
3356
|
+
}
|
|
3357
|
+
main().catch((err) => {
|
|
3358
|
+
console.error(err);
|
|
3359
|
+
process2.exit(1);
|
|
3360
|
+
});
|