uilint 0.2.8 → 0.2.9
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/dist/chunk-2RNDQVEK.js +176 -0
- package/dist/chunk-2RNDQVEK.js.map +1 -0
- package/dist/chunk-RHTG6DUD.js +89 -0
- package/dist/chunk-RHTG6DUD.js.map +1 -0
- package/dist/index.js +273 -3724
- package/dist/index.js.map +1 -1
- package/dist/install-ui-OEFHX4FG.js +3331 -0
- package/dist/install-ui-OEFHX4FG.js.map +1 -0
- package/dist/plan-PX7FFJ25.js +337 -0
- package/dist/plan-PX7FFJ25.js.map +1 -0
- package/package.json +7 -4
|
@@ -0,0 +1,3331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
detectNextAppRouter,
|
|
4
|
+
findNextAppRouterProjects
|
|
5
|
+
} from "./chunk-RHTG6DUD.js";
|
|
6
|
+
import {
|
|
7
|
+
GENSTYLEGUIDE_COMMAND_MD,
|
|
8
|
+
loadSkill
|
|
9
|
+
} from "./chunk-2RNDQVEK.js";
|
|
10
|
+
|
|
11
|
+
// src/commands/install-ui.tsx
|
|
12
|
+
import { render } from "ink";
|
|
13
|
+
|
|
14
|
+
// src/commands/install/components/InstallApp.tsx
|
|
15
|
+
import { useState as useState3, useEffect as useEffect2 } from "react";
|
|
16
|
+
import { Box as Box3, Text as Text4, useApp as useApp2 } from "ink";
|
|
17
|
+
|
|
18
|
+
// src/commands/install/components/Spinner.tsx
|
|
19
|
+
import { useState, useEffect } from "react";
|
|
20
|
+
import { Text } from "ink";
|
|
21
|
+
import { jsx } from "react/jsx-runtime";
|
|
22
|
+
var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
23
|
+
function Spinner() {
|
|
24
|
+
const [frame, setFrame] = useState(0);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const timer = setInterval(() => {
|
|
27
|
+
setFrame((prevFrame) => (prevFrame + 1) % frames.length);
|
|
28
|
+
}, 80);
|
|
29
|
+
return () => {
|
|
30
|
+
clearInterval(timer);
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
return /* @__PURE__ */ jsx(Text, { color: "cyan", children: frames[frame] });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/commands/install/components/MultiSelect.tsx
|
|
37
|
+
import { useState as useState2, useCallback } from "react";
|
|
38
|
+
import { Box, Text as Text2, useInput, useApp } from "ink";
|
|
39
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
40
|
+
function StatusIndicator({ status, isSelected }) {
|
|
41
|
+
if (status === "installed") {
|
|
42
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "green", children: "\u2713" });
|
|
43
|
+
}
|
|
44
|
+
if (isSelected || status === "selected") {
|
|
45
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u25C9" });
|
|
46
|
+
}
|
|
47
|
+
if (status === "partial") {
|
|
48
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "\u25D0" });
|
|
49
|
+
}
|
|
50
|
+
return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u25CB" });
|
|
51
|
+
}
|
|
52
|
+
function StatusLabel({ status }) {
|
|
53
|
+
if (status === "installed") {
|
|
54
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "green", dimColor: true, children: "installed" });
|
|
55
|
+
}
|
|
56
|
+
if (status === "partial") {
|
|
57
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "yellow", dimColor: true, children: "partial" });
|
|
58
|
+
}
|
|
59
|
+
return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "-" });
|
|
60
|
+
}
|
|
61
|
+
function ConfigSelector({
|
|
62
|
+
items,
|
|
63
|
+
onSubmit,
|
|
64
|
+
onCancel
|
|
65
|
+
}) {
|
|
66
|
+
const { exit } = useApp();
|
|
67
|
+
const [cursor, setCursor] = useState2(0);
|
|
68
|
+
const [selected, setSelected] = useState2(() => {
|
|
69
|
+
return new Set(
|
|
70
|
+
items.filter((item) => item.status !== "installed" && !item.disabled).map((item) => item.id)
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
const categories = Array.from(new Set(items.map((item) => item.category)));
|
|
74
|
+
const itemsByCategory = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const cat of categories) {
|
|
76
|
+
itemsByCategory.set(cat, items.filter((item) => item.category === cat));
|
|
77
|
+
}
|
|
78
|
+
const flatItems = items;
|
|
79
|
+
const handleToggle = useCallback(() => {
|
|
80
|
+
const item = flatItems[cursor];
|
|
81
|
+
if (!item || item.disabled || item.status === "installed") return;
|
|
82
|
+
setSelected((prev) => {
|
|
83
|
+
const next = new Set(prev);
|
|
84
|
+
if (next.has(item.id)) {
|
|
85
|
+
next.delete(item.id);
|
|
86
|
+
} else {
|
|
87
|
+
next.add(item.id);
|
|
88
|
+
}
|
|
89
|
+
return next;
|
|
90
|
+
});
|
|
91
|
+
}, [cursor, flatItems]);
|
|
92
|
+
useInput((input, key) => {
|
|
93
|
+
if (key.upArrow) {
|
|
94
|
+
setCursor((prev) => prev > 0 ? prev - 1 : flatItems.length - 1);
|
|
95
|
+
} else if (key.downArrow) {
|
|
96
|
+
setCursor((prev) => prev < flatItems.length - 1 ? prev + 1 : 0);
|
|
97
|
+
} else if (input === " ") {
|
|
98
|
+
handleToggle();
|
|
99
|
+
} else if (key.return) {
|
|
100
|
+
onSubmit(Array.from(selected));
|
|
101
|
+
} else if (input === "q" || key.escape) {
|
|
102
|
+
onCancel?.();
|
|
103
|
+
exit();
|
|
104
|
+
} else if (input === "a") {
|
|
105
|
+
setSelected(
|
|
106
|
+
new Set(
|
|
107
|
+
items.filter((item) => item.status !== "installed" && !item.disabled).map((item) => item.id)
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
} else if (input === "n") {
|
|
111
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
let globalIndex = 0;
|
|
115
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
116
|
+
categories.map((category) => {
|
|
117
|
+
const categoryItems = itemsByCategory.get(category) || [];
|
|
118
|
+
const categoryIcon = categoryItems[0]?.categoryIcon || "\u2022";
|
|
119
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
120
|
+
/* @__PURE__ */ jsx2(Box, { children: /* @__PURE__ */ jsxs(Text2, { bold: true, color: "white", children: [
|
|
121
|
+
categoryIcon,
|
|
122
|
+
" ",
|
|
123
|
+
category
|
|
124
|
+
] }) }),
|
|
125
|
+
categoryItems.map((item) => {
|
|
126
|
+
const itemIndex = globalIndex++;
|
|
127
|
+
const isCursor = itemIndex === cursor;
|
|
128
|
+
const isItemSelected = selected.has(item.id);
|
|
129
|
+
const isDisabled = item.disabled || item.status === "installed";
|
|
130
|
+
return /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, children: [
|
|
131
|
+
/* @__PURE__ */ jsx2(Text2, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u203A " : " " }),
|
|
132
|
+
/* @__PURE__ */ jsx2(Box, { width: 2, children: /* @__PURE__ */ jsx2(StatusIndicator, { status: item.status, isSelected: isItemSelected }) }),
|
|
133
|
+
/* @__PURE__ */ jsx2(Box, { width: 28, children: /* @__PURE__ */ jsx2(
|
|
134
|
+
Text2,
|
|
135
|
+
{
|
|
136
|
+
color: isDisabled ? void 0 : isCursor ? "cyan" : void 0,
|
|
137
|
+
dimColor: isDisabled,
|
|
138
|
+
children: item.label
|
|
139
|
+
}
|
|
140
|
+
) }),
|
|
141
|
+
/* @__PURE__ */ jsx2(Box, { width: 20, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint || "" }) }),
|
|
142
|
+
/* @__PURE__ */ jsx2(StatusLabel, { status: item.status })
|
|
143
|
+
] }, item.id);
|
|
144
|
+
})
|
|
145
|
+
] }, category);
|
|
146
|
+
}),
|
|
147
|
+
/* @__PURE__ */ jsx2(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs(Text2, { dimColor: true, children: [
|
|
148
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2191\u2193" }),
|
|
149
|
+
" navigate",
|
|
150
|
+
" ",
|
|
151
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "space" }),
|
|
152
|
+
" toggle",
|
|
153
|
+
" ",
|
|
154
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "a" }),
|
|
155
|
+
" all",
|
|
156
|
+
" ",
|
|
157
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "n" }),
|
|
158
|
+
" none",
|
|
159
|
+
" ",
|
|
160
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "enter" }),
|
|
161
|
+
" apply",
|
|
162
|
+
" ",
|
|
163
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "q" }),
|
|
164
|
+
" quit"
|
|
165
|
+
] }) }),
|
|
166
|
+
/* @__PURE__ */ jsx2(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text2, { children: [
|
|
167
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: selected.size }),
|
|
168
|
+
/* @__PURE__ */ jsxs(Text2, { dimColor: true, children: [
|
|
169
|
+
" item",
|
|
170
|
+
selected.size !== 1 ? "s" : "",
|
|
171
|
+
" selected"
|
|
172
|
+
] })
|
|
173
|
+
] }) })
|
|
174
|
+
] });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/commands/install/components/ProgressList.tsx
|
|
178
|
+
import { Box as Box2, Text as Text3, Static } from "ink";
|
|
179
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
180
|
+
function CompletedTask({ task }) {
|
|
181
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
182
|
+
/* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2713 " }),
|
|
183
|
+
/* @__PURE__ */ jsx3(Text3, { children: task.message })
|
|
184
|
+
] });
|
|
185
|
+
}
|
|
186
|
+
function RunningTask({ task }) {
|
|
187
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
188
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
189
|
+
/* @__PURE__ */ jsx3(Spinner, {}),
|
|
190
|
+
/* @__PURE__ */ jsxs2(Text3, { children: [
|
|
191
|
+
" ",
|
|
192
|
+
task.message
|
|
193
|
+
] })
|
|
194
|
+
] }),
|
|
195
|
+
task.detail && /* @__PURE__ */ jsx3(Box2, { paddingLeft: 2, children: /* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
|
|
196
|
+
"\u2514\u2500 ",
|
|
197
|
+
task.detail
|
|
198
|
+
] }) })
|
|
199
|
+
] });
|
|
200
|
+
}
|
|
201
|
+
function ErrorTask({ task }) {
|
|
202
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
203
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
204
|
+
/* @__PURE__ */ jsx3(Text3, { color: "red", children: "\u2717 " }),
|
|
205
|
+
/* @__PURE__ */ jsx3(Text3, { color: "red", children: task.message })
|
|
206
|
+
] }),
|
|
207
|
+
task.error && /* @__PURE__ */ jsx3(Box2, { paddingLeft: 2, children: /* @__PURE__ */ jsxs2(Text3, { color: "red", dimColor: true, children: [
|
|
208
|
+
"\u2514\u2500 ",
|
|
209
|
+
task.error
|
|
210
|
+
] }) })
|
|
211
|
+
] });
|
|
212
|
+
}
|
|
213
|
+
function PendingTask({ task }) {
|
|
214
|
+
return /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
|
|
215
|
+
"\u25CB ",
|
|
216
|
+
task.message
|
|
217
|
+
] }) });
|
|
218
|
+
}
|
|
219
|
+
function ProgressList({ tasks }) {
|
|
220
|
+
const completedTasks = tasks.filter((t) => t.status === "complete");
|
|
221
|
+
const runningTasks = tasks.filter((t) => t.status === "running");
|
|
222
|
+
const errorTasks = tasks.filter((t) => t.status === "error");
|
|
223
|
+
const pendingTasks = tasks.filter((t) => t.status === "pending");
|
|
224
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
225
|
+
completedTasks.length > 0 && /* @__PURE__ */ jsx3(Static, { items: completedTasks, children: (task) => /* @__PURE__ */ jsx3(CompletedTask, { task }, task.id) }),
|
|
226
|
+
runningTasks.map((task) => /* @__PURE__ */ jsx3(RunningTask, { task }, task.id)),
|
|
227
|
+
errorTasks.map((task) => /* @__PURE__ */ jsx3(ErrorTask, { task }, task.id)),
|
|
228
|
+
pendingTasks.map((task) => /* @__PURE__ */ jsx3(PendingTask, { task }, task.id))
|
|
229
|
+
] });
|
|
230
|
+
}
|
|
231
|
+
function progressEventsToTasks(events) {
|
|
232
|
+
const tasks = [];
|
|
233
|
+
let taskIdCounter = 0;
|
|
234
|
+
for (const event of events) {
|
|
235
|
+
if (event.type === "start") {
|
|
236
|
+
tasks.push({
|
|
237
|
+
id: `task-${taskIdCounter++}`,
|
|
238
|
+
status: "running",
|
|
239
|
+
message: event.message,
|
|
240
|
+
detail: event.detail
|
|
241
|
+
});
|
|
242
|
+
} else if (event.type === "progress") {
|
|
243
|
+
const lastRunning = tasks.reverse().find((t) => t.status === "running");
|
|
244
|
+
tasks.reverse();
|
|
245
|
+
if (lastRunning) {
|
|
246
|
+
lastRunning.message = event.message;
|
|
247
|
+
lastRunning.detail = event.detail;
|
|
248
|
+
} else {
|
|
249
|
+
tasks.push({
|
|
250
|
+
id: `task-${taskIdCounter++}`,
|
|
251
|
+
status: "running",
|
|
252
|
+
message: event.message,
|
|
253
|
+
detail: event.detail
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
} else if (event.type === "complete") {
|
|
257
|
+
const lastRunning = tasks.reverse().find((t) => t.status === "running");
|
|
258
|
+
tasks.reverse();
|
|
259
|
+
if (lastRunning) {
|
|
260
|
+
lastRunning.status = "complete";
|
|
261
|
+
lastRunning.detail = void 0;
|
|
262
|
+
}
|
|
263
|
+
} else if (event.type === "error") {
|
|
264
|
+
const lastRunning = tasks.reverse().find((t) => t.status === "running");
|
|
265
|
+
tasks.reverse();
|
|
266
|
+
if (lastRunning) {
|
|
267
|
+
lastRunning.status = "error";
|
|
268
|
+
lastRunning.error = event.error;
|
|
269
|
+
} else {
|
|
270
|
+
tasks.push({
|
|
271
|
+
id: `task-${taskIdCounter++}`,
|
|
272
|
+
status: "error",
|
|
273
|
+
message: event.message,
|
|
274
|
+
error: event.error
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return tasks;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/commands/install/installers/registry.ts
|
|
283
|
+
var installers = [];
|
|
284
|
+
function registerInstaller(installer) {
|
|
285
|
+
if (installers.some((i) => i.id === installer.id)) {
|
|
286
|
+
console.warn(`Installer "${installer.id}" is already registered`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
installers.push(installer);
|
|
290
|
+
}
|
|
291
|
+
function getAllInstallers() {
|
|
292
|
+
return installers;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/commands/install/components/InstallApp.tsx
|
|
296
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
297
|
+
function buildConfigItems(project, selections) {
|
|
298
|
+
const items = [];
|
|
299
|
+
for (const selection of selections) {
|
|
300
|
+
const { installer, targets } = selection;
|
|
301
|
+
const categoryMap = {
|
|
302
|
+
genstyleguide: { name: "Commands", icon: "\u{1F4DD}" },
|
|
303
|
+
skill: { name: "Agent Skills", icon: "\u26A1" },
|
|
304
|
+
next: { name: "UI Overlay", icon: "\u{1F537}" },
|
|
305
|
+
vite: { name: "UI Overlay", icon: "\u26A1" },
|
|
306
|
+
eslint: { name: "ESLint Plugin", icon: "\u{1F50D}" }
|
|
307
|
+
};
|
|
308
|
+
const category = categoryMap[installer.id] || { name: "Other", icon: "\u2022" };
|
|
309
|
+
for (const target of targets) {
|
|
310
|
+
items.push({
|
|
311
|
+
id: `${installer.id}:${target.id}`,
|
|
312
|
+
label: target.label,
|
|
313
|
+
hint: target.hint,
|
|
314
|
+
status: target.isInstalled ? "installed" : "not_installed",
|
|
315
|
+
category: category.name,
|
|
316
|
+
categoryIcon: category.icon
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return items;
|
|
321
|
+
}
|
|
322
|
+
function Header({ subtitle }) {
|
|
323
|
+
return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
324
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "\u25C6 UILint" }),
|
|
325
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " v0.5.0" }),
|
|
326
|
+
subtitle && /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
327
|
+
" \xB7 ",
|
|
328
|
+
subtitle
|
|
329
|
+
] })
|
|
330
|
+
] }) });
|
|
331
|
+
}
|
|
332
|
+
function ProjectContext({ project }) {
|
|
333
|
+
const parts = [];
|
|
334
|
+
parts.push(project.packageManager);
|
|
335
|
+
if (project.nextApps.length > 0) {
|
|
336
|
+
parts.push(`${project.nextApps.length} Next.js app${project.nextApps.length > 1 ? "s" : ""}`);
|
|
337
|
+
}
|
|
338
|
+
if (project.viteApps.length > 0) {
|
|
339
|
+
parts.push(`${project.viteApps.length} Vite app${project.viteApps.length > 1 ? "s" : ""}`);
|
|
340
|
+
}
|
|
341
|
+
const eslintCount = project.packages.filter((p) => p.eslintConfigPath).length;
|
|
342
|
+
if (eslintCount > 0) {
|
|
343
|
+
parts.push(`${eslintCount} ESLint config${eslintCount > 1 ? "s" : ""}`);
|
|
344
|
+
}
|
|
345
|
+
return /* @__PURE__ */ jsx4(Box3, { marginBottom: 1, children: /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
346
|
+
"Detected: ",
|
|
347
|
+
parts.join(" \xB7 ")
|
|
348
|
+
] }) });
|
|
349
|
+
}
|
|
350
|
+
function InstallApp({
|
|
351
|
+
projectPromise,
|
|
352
|
+
onComplete,
|
|
353
|
+
onError
|
|
354
|
+
}) {
|
|
355
|
+
const { exit } = useApp2();
|
|
356
|
+
const [state, setState] = useState3("scanning");
|
|
357
|
+
const [project, setProject] = useState3(null);
|
|
358
|
+
const [selections, setSelections] = useState3([]);
|
|
359
|
+
const [configItems, setConfigItems] = useState3([]);
|
|
360
|
+
const [progressEvents] = useState3([]);
|
|
361
|
+
const [error, setError] = useState3(null);
|
|
362
|
+
useEffect2(() => {
|
|
363
|
+
if (state !== "scanning") return;
|
|
364
|
+
projectPromise.then((proj) => {
|
|
365
|
+
setProject(proj);
|
|
366
|
+
const installers2 = getAllInstallers();
|
|
367
|
+
const initialSelections = installers2.filter((installer) => installer.isApplicable(proj)).map((installer) => {
|
|
368
|
+
const targets = installer.getTargets(proj);
|
|
369
|
+
const nonInstalledTargets = targets.filter((t) => !t.isInstalled);
|
|
370
|
+
return {
|
|
371
|
+
installer,
|
|
372
|
+
targets,
|
|
373
|
+
selected: nonInstalledTargets.length > 0
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
setSelections(initialSelections);
|
|
377
|
+
const items = buildConfigItems(proj, initialSelections);
|
|
378
|
+
setConfigItems(items);
|
|
379
|
+
setState("configuring");
|
|
380
|
+
}).catch((err) => {
|
|
381
|
+
setError(err);
|
|
382
|
+
setState("error");
|
|
383
|
+
onError?.(err);
|
|
384
|
+
});
|
|
385
|
+
}, [state, projectPromise, onError]);
|
|
386
|
+
const handleConfigSubmit = (selectedIds) => {
|
|
387
|
+
const selectedSet = new Set(selectedIds);
|
|
388
|
+
const updatedSelections = selections.map((sel) => {
|
|
389
|
+
const selectedTargets = sel.targets.filter(
|
|
390
|
+
(t) => selectedSet.has(`${sel.installer.id}:${t.id}`)
|
|
391
|
+
);
|
|
392
|
+
return {
|
|
393
|
+
...sel,
|
|
394
|
+
targets: selectedTargets,
|
|
395
|
+
selected: selectedTargets.length > 0
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
setSelections(updatedSelections);
|
|
399
|
+
onComplete(updatedSelections);
|
|
400
|
+
};
|
|
401
|
+
const handleCancel = () => {
|
|
402
|
+
exit();
|
|
403
|
+
};
|
|
404
|
+
if (state === "scanning") {
|
|
405
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
406
|
+
/* @__PURE__ */ jsx4(Header, { subtitle: "Install" }),
|
|
407
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
408
|
+
/* @__PURE__ */ jsx4(Spinner, {}),
|
|
409
|
+
/* @__PURE__ */ jsx4(Text4, { children: " Scanning project..." })
|
|
410
|
+
] })
|
|
411
|
+
] });
|
|
412
|
+
}
|
|
413
|
+
if (state === "error") {
|
|
414
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
415
|
+
/* @__PURE__ */ jsx4(Header, {}),
|
|
416
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
417
|
+
/* @__PURE__ */ jsx4(Text4, { color: "red", children: "\u2717 " }),
|
|
418
|
+
/* @__PURE__ */ jsx4(Text4, { color: "red", children: error?.message || "An unknown error occurred" })
|
|
419
|
+
] })
|
|
420
|
+
] });
|
|
421
|
+
}
|
|
422
|
+
if (state === "configuring" && project && configItems.length > 0) {
|
|
423
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
424
|
+
/* @__PURE__ */ jsx4(Header, { subtitle: "Configuration" }),
|
|
425
|
+
/* @__PURE__ */ jsx4(ProjectContext, { project }),
|
|
426
|
+
/* @__PURE__ */ jsx4(
|
|
427
|
+
ConfigSelector,
|
|
428
|
+
{
|
|
429
|
+
items: configItems,
|
|
430
|
+
onSubmit: handleConfigSubmit,
|
|
431
|
+
onCancel: handleCancel
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
] });
|
|
435
|
+
}
|
|
436
|
+
if (state === "executing") {
|
|
437
|
+
const tasks = progressEventsToTasks(progressEvents);
|
|
438
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
439
|
+
/* @__PURE__ */ jsx4(Header, { subtitle: "Installing" }),
|
|
440
|
+
/* @__PURE__ */ jsx4(ProgressList, { tasks })
|
|
441
|
+
] });
|
|
442
|
+
}
|
|
443
|
+
if (state === "complete") {
|
|
444
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
445
|
+
/* @__PURE__ */ jsx4(Header, {}),
|
|
446
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
447
|
+
/* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2713 " }),
|
|
448
|
+
/* @__PURE__ */ jsx4(Text4, { children: "Configuration applied successfully!" })
|
|
449
|
+
] })
|
|
450
|
+
] });
|
|
451
|
+
}
|
|
452
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
453
|
+
/* @__PURE__ */ jsx4(Header, {}),
|
|
454
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." })
|
|
455
|
+
] });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/commands/install/analyze.ts
|
|
459
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
460
|
+
import { join as join5 } from "path";
|
|
461
|
+
import { findWorkspaceRoot as findWorkspaceRoot2 } from "uilint-core/node";
|
|
462
|
+
|
|
463
|
+
// src/utils/vite-detect.ts
|
|
464
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
465
|
+
import { join } from "path";
|
|
466
|
+
var VITE_CONFIG_EXTS = [".ts", ".mjs", ".js", ".cjs"];
|
|
467
|
+
function findViteConfigFile(projectPath) {
|
|
468
|
+
for (const ext of VITE_CONFIG_EXTS) {
|
|
469
|
+
const rel = `vite.config${ext}`;
|
|
470
|
+
if (existsSync(join(projectPath, rel))) return rel;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
function looksLikeReactPackage(projectPath) {
|
|
475
|
+
try {
|
|
476
|
+
const pkgPath = join(projectPath, "package.json");
|
|
477
|
+
if (!existsSync(pkgPath)) return false;
|
|
478
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
479
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
480
|
+
return "react" in deps || "react-dom" in deps;
|
|
481
|
+
} catch {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function fileExists(projectPath, relPath) {
|
|
486
|
+
return existsSync(join(projectPath, relPath));
|
|
487
|
+
}
|
|
488
|
+
function detectViteReact(projectPath) {
|
|
489
|
+
const configFile = findViteConfigFile(projectPath);
|
|
490
|
+
if (!configFile) return null;
|
|
491
|
+
if (!looksLikeReactPackage(projectPath)) return null;
|
|
492
|
+
const entryRoot = "src";
|
|
493
|
+
const candidates = [];
|
|
494
|
+
const entryCandidates = [
|
|
495
|
+
join(entryRoot, "main.tsx"),
|
|
496
|
+
join(entryRoot, "main.jsx"),
|
|
497
|
+
join(entryRoot, "main.ts"),
|
|
498
|
+
join(entryRoot, "main.js")
|
|
499
|
+
];
|
|
500
|
+
for (const rel of entryCandidates) {
|
|
501
|
+
if (fileExists(projectPath, rel)) candidates.push(rel);
|
|
502
|
+
}
|
|
503
|
+
const fallbackCandidates = [
|
|
504
|
+
join(entryRoot, "App.tsx"),
|
|
505
|
+
join(entryRoot, "App.jsx")
|
|
506
|
+
];
|
|
507
|
+
for (const rel of fallbackCandidates) {
|
|
508
|
+
if (!candidates.includes(rel) && fileExists(projectPath, rel)) {
|
|
509
|
+
candidates.push(rel);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
configFile,
|
|
514
|
+
configFileAbs: join(projectPath, configFile),
|
|
515
|
+
entryRoot,
|
|
516
|
+
candidates
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
520
|
+
"node_modules",
|
|
521
|
+
".git",
|
|
522
|
+
".next",
|
|
523
|
+
"dist",
|
|
524
|
+
"build",
|
|
525
|
+
"out",
|
|
526
|
+
".turbo",
|
|
527
|
+
".vercel",
|
|
528
|
+
".cursor",
|
|
529
|
+
"coverage",
|
|
530
|
+
".uilint"
|
|
531
|
+
]);
|
|
532
|
+
function findViteReactProjects(rootDir, options) {
|
|
533
|
+
const maxDepth = options?.maxDepth ?? 4;
|
|
534
|
+
const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS;
|
|
535
|
+
const results = [];
|
|
536
|
+
const visited = /* @__PURE__ */ new Set();
|
|
537
|
+
function walk(dir, depth) {
|
|
538
|
+
if (depth > maxDepth) return;
|
|
539
|
+
if (visited.has(dir)) return;
|
|
540
|
+
visited.add(dir);
|
|
541
|
+
const detection = detectViteReact(dir);
|
|
542
|
+
if (detection) {
|
|
543
|
+
results.push({ projectPath: dir, detection });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
let entries = [];
|
|
547
|
+
try {
|
|
548
|
+
entries = readdirSync(dir, { withFileTypes: true }).map((d) => ({
|
|
549
|
+
name: d.name,
|
|
550
|
+
isDirectory: d.isDirectory()
|
|
551
|
+
}));
|
|
552
|
+
} catch {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
for (const ent of entries) {
|
|
556
|
+
if (!ent.isDirectory) continue;
|
|
557
|
+
if (ignoreDirs.has(ent.name)) continue;
|
|
558
|
+
if (ent.name.startsWith(".") && ent.name !== ".") continue;
|
|
559
|
+
walk(join(dir, ent.name), depth + 1);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
walk(rootDir, 0);
|
|
563
|
+
return results;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/utils/package-detect.ts
|
|
567
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
568
|
+
import { join as join2, relative } from "path";
|
|
569
|
+
var DEFAULT_IGNORE_DIRS2 = /* @__PURE__ */ new Set([
|
|
570
|
+
"node_modules",
|
|
571
|
+
".git",
|
|
572
|
+
".next",
|
|
573
|
+
"dist",
|
|
574
|
+
"build",
|
|
575
|
+
"out",
|
|
576
|
+
".turbo",
|
|
577
|
+
".vercel",
|
|
578
|
+
".cursor",
|
|
579
|
+
"coverage",
|
|
580
|
+
".uilint",
|
|
581
|
+
".pnpm"
|
|
582
|
+
]);
|
|
583
|
+
var ESLINT_CONFIG_FILES = [
|
|
584
|
+
"eslint.config.js",
|
|
585
|
+
"eslint.config.ts",
|
|
586
|
+
"eslint.config.mjs",
|
|
587
|
+
"eslint.config.cjs",
|
|
588
|
+
".eslintrc.js",
|
|
589
|
+
".eslintrc.cjs",
|
|
590
|
+
".eslintrc.json",
|
|
591
|
+
".eslintrc.yml",
|
|
592
|
+
".eslintrc.yaml",
|
|
593
|
+
".eslintrc"
|
|
594
|
+
];
|
|
595
|
+
var FRONTEND_INDICATORS = [
|
|
596
|
+
"react",
|
|
597
|
+
"react-dom",
|
|
598
|
+
"next",
|
|
599
|
+
"vue",
|
|
600
|
+
"svelte",
|
|
601
|
+
"@angular/core",
|
|
602
|
+
"solid-js",
|
|
603
|
+
"preact"
|
|
604
|
+
];
|
|
605
|
+
function isFrontendPackage(pkgJson) {
|
|
606
|
+
const deps = {
|
|
607
|
+
...pkgJson.dependencies,
|
|
608
|
+
...pkgJson.devDependencies
|
|
609
|
+
};
|
|
610
|
+
return FRONTEND_INDICATORS.some((pkg) => pkg in deps);
|
|
611
|
+
}
|
|
612
|
+
function isTypeScriptPackage(dir, pkgJson) {
|
|
613
|
+
if (existsSync2(join2(dir, "tsconfig.json"))) {
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
const deps = {
|
|
617
|
+
...pkgJson.dependencies,
|
|
618
|
+
...pkgJson.devDependencies
|
|
619
|
+
};
|
|
620
|
+
if ("typescript" in deps) {
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
for (const configFile of ESLINT_CONFIG_FILES) {
|
|
624
|
+
if (configFile.endsWith(".ts") && existsSync2(join2(dir, configFile))) {
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
function hasEslintConfig(dir) {
|
|
631
|
+
for (const file of ESLINT_CONFIG_FILES) {
|
|
632
|
+
if (existsSync2(join2(dir, file))) {
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
const pkgPath = join2(dir, "package.json");
|
|
638
|
+
if (existsSync2(pkgPath)) {
|
|
639
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
640
|
+
if (pkg.eslintConfig) return true;
|
|
641
|
+
}
|
|
642
|
+
} catch {
|
|
643
|
+
}
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
function findPackages(rootDir, options) {
|
|
647
|
+
const maxDepth = options?.maxDepth ?? 5;
|
|
648
|
+
const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS2;
|
|
649
|
+
const results = [];
|
|
650
|
+
const visited = /* @__PURE__ */ new Set();
|
|
651
|
+
function processPackage(dir, isRoot) {
|
|
652
|
+
const pkgPath = join2(dir, "package.json");
|
|
653
|
+
if (!existsSync2(pkgPath)) return null;
|
|
654
|
+
try {
|
|
655
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
656
|
+
const name = pkg.name || relative(rootDir, dir) || ".";
|
|
657
|
+
return {
|
|
658
|
+
path: dir,
|
|
659
|
+
displayPath: relative(rootDir, dir) || ".",
|
|
660
|
+
name,
|
|
661
|
+
hasEslintConfig: hasEslintConfig(dir),
|
|
662
|
+
isFrontend: isFrontendPackage(pkg),
|
|
663
|
+
isRoot,
|
|
664
|
+
isTypeScript: isTypeScriptPackage(dir, pkg)
|
|
665
|
+
};
|
|
666
|
+
} catch {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function walk(dir, depth) {
|
|
671
|
+
if (depth > maxDepth) return;
|
|
672
|
+
if (visited.has(dir)) return;
|
|
673
|
+
visited.add(dir);
|
|
674
|
+
const pkg = processPackage(dir, depth === 0);
|
|
675
|
+
if (pkg) {
|
|
676
|
+
results.push(pkg);
|
|
677
|
+
}
|
|
678
|
+
let entries = [];
|
|
679
|
+
try {
|
|
680
|
+
entries = readdirSync2(dir, { withFileTypes: true }).map((d) => ({
|
|
681
|
+
name: d.name,
|
|
682
|
+
isDirectory: d.isDirectory()
|
|
683
|
+
}));
|
|
684
|
+
} catch {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
for (const ent of entries) {
|
|
688
|
+
if (!ent.isDirectory) continue;
|
|
689
|
+
if (ignoreDirs.has(ent.name)) continue;
|
|
690
|
+
if (ent.name.startsWith(".")) continue;
|
|
691
|
+
walk(join2(dir, ent.name), depth + 1);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
walk(rootDir, 0);
|
|
695
|
+
return results.sort((a, b) => {
|
|
696
|
+
if (a.isRoot && !b.isRoot) return -1;
|
|
697
|
+
if (!a.isRoot && b.isRoot) return 1;
|
|
698
|
+
if (a.isFrontend && !b.isFrontend) return -1;
|
|
699
|
+
if (!a.isFrontend && b.isFrontend) return 1;
|
|
700
|
+
return a.displayPath.localeCompare(b.displayPath);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/utils/package-manager.ts
|
|
705
|
+
import { existsSync as existsSync3 } from "fs";
|
|
706
|
+
import { spawn } from "child_process";
|
|
707
|
+
import { dirname, join as join3 } from "path";
|
|
708
|
+
function detectPackageManager(projectPath) {
|
|
709
|
+
let dir = projectPath;
|
|
710
|
+
for (; ; ) {
|
|
711
|
+
if (existsSync3(join3(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
712
|
+
if (existsSync3(join3(dir, "pnpm-workspace.yaml"))) return "pnpm";
|
|
713
|
+
if (existsSync3(join3(dir, "yarn.lock"))) return "yarn";
|
|
714
|
+
if (existsSync3(join3(dir, "bun.lockb"))) return "bun";
|
|
715
|
+
if (existsSync3(join3(dir, "bun.lock"))) return "bun";
|
|
716
|
+
if (existsSync3(join3(dir, "package-lock.json"))) return "npm";
|
|
717
|
+
const parent = dirname(dir);
|
|
718
|
+
if (parent === dir) break;
|
|
719
|
+
dir = parent;
|
|
720
|
+
}
|
|
721
|
+
return "npm";
|
|
722
|
+
}
|
|
723
|
+
function spawnAsync(command, args, cwd) {
|
|
724
|
+
return new Promise((resolve, reject) => {
|
|
725
|
+
const child = spawn(command, args, {
|
|
726
|
+
cwd,
|
|
727
|
+
stdio: "inherit",
|
|
728
|
+
shell: process.platform === "win32"
|
|
729
|
+
});
|
|
730
|
+
child.on("error", reject);
|
|
731
|
+
child.on("close", (code) => {
|
|
732
|
+
if (code === 0) resolve();
|
|
733
|
+
else
|
|
734
|
+
reject(new Error(`${command} ${args.join(" ")} exited with ${code}`));
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
async function installDependencies(pm, projectPath, packages) {
|
|
739
|
+
if (!packages.length) return;
|
|
740
|
+
switch (pm) {
|
|
741
|
+
case "pnpm":
|
|
742
|
+
await spawnAsync("pnpm", ["add", ...packages], projectPath);
|
|
743
|
+
return;
|
|
744
|
+
case "yarn":
|
|
745
|
+
await spawnAsync("yarn", ["add", ...packages], projectPath);
|
|
746
|
+
return;
|
|
747
|
+
case "bun":
|
|
748
|
+
await spawnAsync("bun", ["add", ...packages], projectPath);
|
|
749
|
+
return;
|
|
750
|
+
case "npm":
|
|
751
|
+
default:
|
|
752
|
+
await spawnAsync("npm", ["install", "--save", ...packages], projectPath);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/utils/eslint-config-inject.ts
|
|
758
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
759
|
+
import { join as join4, relative as relative2, dirname as dirname2 } from "path";
|
|
760
|
+
import { parseExpression, parseModule, generateCode } from "magicast";
|
|
761
|
+
import { findWorkspaceRoot } from "uilint-core/node";
|
|
762
|
+
var CONFIG_EXTENSIONS = [".ts", ".mjs", ".js", ".cjs"];
|
|
763
|
+
function findEslintConfigFile(projectPath) {
|
|
764
|
+
for (const ext of CONFIG_EXTENSIONS) {
|
|
765
|
+
const configPath = join4(projectPath, `eslint.config${ext}`);
|
|
766
|
+
if (existsSync4(configPath)) {
|
|
767
|
+
return configPath;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
function getEslintConfigFilename(configPath) {
|
|
773
|
+
const parts = configPath.split("/");
|
|
774
|
+
return parts[parts.length - 1] || "eslint.config.mjs";
|
|
775
|
+
}
|
|
776
|
+
function isIdentifier(node, name) {
|
|
777
|
+
return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
|
|
778
|
+
}
|
|
779
|
+
function isStringLiteral(node) {
|
|
780
|
+
return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
|
|
781
|
+
}
|
|
782
|
+
function getObjectPropertyValue(obj, keyName) {
|
|
783
|
+
if (!obj || obj.type !== "ObjectExpression") return null;
|
|
784
|
+
for (const prop of obj.properties ?? []) {
|
|
785
|
+
if (!prop) continue;
|
|
786
|
+
if (prop.type === "ObjectProperty" || prop.type === "Property") {
|
|
787
|
+
const key = prop.key;
|
|
788
|
+
const keyMatch = key?.type === "Identifier" && key.name === keyName || isStringLiteral(key) && key.value === keyName;
|
|
789
|
+
if (keyMatch) return prop.value;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
function hasSpreadProperties(obj) {
|
|
795
|
+
if (!obj || obj.type !== "ObjectExpression") return false;
|
|
796
|
+
return (obj.properties ?? []).some(
|
|
797
|
+
(p) => p && (p.type === "SpreadElement" || p.type === "SpreadProperty")
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
var IGNORED_AST_KEYS = /* @__PURE__ */ new Set([
|
|
801
|
+
"loc",
|
|
802
|
+
"start",
|
|
803
|
+
"end",
|
|
804
|
+
"extra",
|
|
805
|
+
"leadingComments",
|
|
806
|
+
"trailingComments",
|
|
807
|
+
"innerComments"
|
|
808
|
+
]);
|
|
809
|
+
function normalizeAstForCompare(node) {
|
|
810
|
+
if (node === null) return null;
|
|
811
|
+
if (node === void 0) return void 0;
|
|
812
|
+
if (typeof node !== "object") return node;
|
|
813
|
+
if (Array.isArray(node)) return node.map(normalizeAstForCompare);
|
|
814
|
+
const out = {};
|
|
815
|
+
const keys = Object.keys(node).filter((k) => !IGNORED_AST_KEYS.has(k)).sort();
|
|
816
|
+
for (const k of keys) {
|
|
817
|
+
if (k.startsWith("$")) continue;
|
|
818
|
+
out[k] = normalizeAstForCompare(node[k]);
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
function astEquivalent(a, b) {
|
|
823
|
+
try {
|
|
824
|
+
return JSON.stringify(normalizeAstForCompare(a)) === JSON.stringify(normalizeAstForCompare(b));
|
|
825
|
+
} catch {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function collectUilintRuleIdsFromRulesObject(rulesObj) {
|
|
830
|
+
const ids = /* @__PURE__ */ new Set();
|
|
831
|
+
if (!rulesObj || rulesObj.type !== "ObjectExpression") return ids;
|
|
832
|
+
for (const prop of rulesObj.properties ?? []) {
|
|
833
|
+
if (!prop) continue;
|
|
834
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
|
|
835
|
+
const key = prop.key;
|
|
836
|
+
if (!isStringLiteral(key)) continue;
|
|
837
|
+
const val = key.value;
|
|
838
|
+
if (typeof val !== "string") continue;
|
|
839
|
+
if (val.startsWith("uilint/")) {
|
|
840
|
+
ids.add(val.slice("uilint/".length));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return ids;
|
|
844
|
+
}
|
|
845
|
+
function findExportedConfigArrayExpression(mod) {
|
|
846
|
+
function unwrapExpression2(expr) {
|
|
847
|
+
let e = expr;
|
|
848
|
+
while (e) {
|
|
849
|
+
if (e.type === "TSAsExpression" || e.type === "TSNonNullExpression") {
|
|
850
|
+
e = e.expression;
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
if (e.type === "TSSatisfiesExpression") {
|
|
854
|
+
e = e.expression;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (e.type === "ParenthesizedExpression") {
|
|
858
|
+
e = e.expression;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
return e;
|
|
864
|
+
}
|
|
865
|
+
function resolveTopLevelIdentifierToArrayExpr(program2, name) {
|
|
866
|
+
if (!program2 || program2.type !== "Program") return null;
|
|
867
|
+
for (const stmt of program2.body ?? []) {
|
|
868
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
869
|
+
for (const decl of stmt.declarations ?? []) {
|
|
870
|
+
const id = decl?.id;
|
|
871
|
+
if (!isIdentifier(id, name)) continue;
|
|
872
|
+
const init = unwrapExpression2(decl?.init);
|
|
873
|
+
if (!init) return null;
|
|
874
|
+
if (init.type === "ArrayExpression") return init;
|
|
875
|
+
if (init.type === "CallExpression" && isIdentifier(init.callee, "defineConfig") && unwrapExpression2(init.arguments?.[0])?.type === "ArrayExpression") {
|
|
876
|
+
return unwrapExpression2(init.arguments?.[0]);
|
|
877
|
+
}
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
const program = mod?.$ast;
|
|
884
|
+
if (program && program.type === "Program") {
|
|
885
|
+
for (const stmt of program.body ?? []) {
|
|
886
|
+
if (!stmt || stmt.type !== "ExportDefaultDeclaration") continue;
|
|
887
|
+
const decl = unwrapExpression2(stmt.declaration);
|
|
888
|
+
if (!decl) break;
|
|
889
|
+
if (decl.type === "ArrayExpression") {
|
|
890
|
+
return { kind: "esm", arrayExpr: decl, program };
|
|
891
|
+
}
|
|
892
|
+
if (decl.type === "CallExpression" && isIdentifier(decl.callee, "defineConfig") && unwrapExpression2(decl.arguments?.[0])?.type === "ArrayExpression") {
|
|
893
|
+
return {
|
|
894
|
+
kind: "esm",
|
|
895
|
+
arrayExpr: unwrapExpression2(decl.arguments?.[0]),
|
|
896
|
+
program
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
if (decl.type === "Identifier" && typeof decl.name === "string") {
|
|
900
|
+
const resolved = resolveTopLevelIdentifierToArrayExpr(
|
|
901
|
+
program,
|
|
902
|
+
decl.name
|
|
903
|
+
);
|
|
904
|
+
if (resolved) return { kind: "esm", arrayExpr: resolved, program };
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (!program || program.type !== "Program") return null;
|
|
910
|
+
for (const stmt of program.body ?? []) {
|
|
911
|
+
if (!stmt || stmt.type !== "ExpressionStatement") continue;
|
|
912
|
+
const expr = stmt.expression;
|
|
913
|
+
if (!expr || expr.type !== "AssignmentExpression") continue;
|
|
914
|
+
const left = expr.left;
|
|
915
|
+
const right = expr.right;
|
|
916
|
+
const isModuleExports = left?.type === "MemberExpression" && isIdentifier(left.object, "module") && isIdentifier(left.property, "exports");
|
|
917
|
+
if (!isModuleExports) continue;
|
|
918
|
+
if (right?.type === "ArrayExpression") {
|
|
919
|
+
return { kind: "cjs", arrayExpr: right, program };
|
|
920
|
+
}
|
|
921
|
+
if (right?.type === "CallExpression" && isIdentifier(right.callee, "defineConfig") && right.arguments?.[0]?.type === "ArrayExpression") {
|
|
922
|
+
return { kind: "cjs", arrayExpr: right.arguments[0], program };
|
|
923
|
+
}
|
|
924
|
+
if (right?.type === "Identifier" && typeof right.name === "string") {
|
|
925
|
+
const resolved = resolveTopLevelIdentifierToArrayExpr(
|
|
926
|
+
program,
|
|
927
|
+
right.name
|
|
928
|
+
);
|
|
929
|
+
if (resolved) return { kind: "cjs", arrayExpr: resolved, program };
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
function collectConfiguredUilintRuleIdsFromConfigArray(arrayExpr) {
|
|
935
|
+
const ids = /* @__PURE__ */ new Set();
|
|
936
|
+
if (!arrayExpr || arrayExpr.type !== "ArrayExpression") return ids;
|
|
937
|
+
for (const el of arrayExpr.elements ?? []) {
|
|
938
|
+
if (!el || el.type !== "ObjectExpression") continue;
|
|
939
|
+
const rules = getObjectPropertyValue(el, "rules");
|
|
940
|
+
for (const id of collectUilintRuleIdsFromRulesObject(rules)) ids.add(id);
|
|
941
|
+
}
|
|
942
|
+
return ids;
|
|
943
|
+
}
|
|
944
|
+
function findExistingUilintRulesObject(arrayExpr) {
|
|
945
|
+
if (!arrayExpr || arrayExpr.type !== "ArrayExpression") {
|
|
946
|
+
return { configObj: null, rulesObj: null, safeToMutate: false };
|
|
947
|
+
}
|
|
948
|
+
for (const el of arrayExpr.elements ?? []) {
|
|
949
|
+
if (!el || el.type !== "ObjectExpression") continue;
|
|
950
|
+
const plugins = getObjectPropertyValue(el, "plugins");
|
|
951
|
+
const rules = getObjectPropertyValue(el, "rules");
|
|
952
|
+
const hasUilintPlugin = plugins?.type === "ObjectExpression" && getObjectPropertyValue(plugins, "uilint") !== null;
|
|
953
|
+
const uilintIds = collectUilintRuleIdsFromRulesObject(rules);
|
|
954
|
+
const hasUilintRules = uilintIds.size > 0;
|
|
955
|
+
if (!hasUilintPlugin && !hasUilintRules) continue;
|
|
956
|
+
const safe = rules?.type === "ObjectExpression" && !hasSpreadProperties(rules);
|
|
957
|
+
return { configObj: el, rulesObj: rules, safeToMutate: safe };
|
|
958
|
+
}
|
|
959
|
+
return { configObj: null, rulesObj: null, safeToMutate: false };
|
|
960
|
+
}
|
|
961
|
+
function collectTopLevelBindings(program) {
|
|
962
|
+
const names = /* @__PURE__ */ new Set();
|
|
963
|
+
if (!program || program.type !== "Program") return names;
|
|
964
|
+
for (const stmt of program.body ?? []) {
|
|
965
|
+
if (stmt?.type === "VariableDeclaration") {
|
|
966
|
+
for (const decl of stmt.declarations ?? []) {
|
|
967
|
+
const id = decl?.id;
|
|
968
|
+
if (id?.type === "Identifier" && typeof id.name === "string") {
|
|
969
|
+
names.add(id.name);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
} else if (stmt?.type === "FunctionDeclaration") {
|
|
973
|
+
if (stmt.id?.type === "Identifier" && typeof stmt.id.name === "string") {
|
|
974
|
+
names.add(stmt.id.name);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return names;
|
|
979
|
+
}
|
|
980
|
+
function chooseUniqueIdentifier(base, used) {
|
|
981
|
+
if (!used.has(base)) return base;
|
|
982
|
+
let i = 2;
|
|
983
|
+
while (used.has(`${base}${i}`)) i++;
|
|
984
|
+
return `${base}${i}`;
|
|
985
|
+
}
|
|
986
|
+
function addLocalRuleImportsAst(mod, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
|
|
987
|
+
const importNames = /* @__PURE__ */ new Map();
|
|
988
|
+
let changed = false;
|
|
989
|
+
const configDir = dirname2(configPath);
|
|
990
|
+
const rulesDir = join4(rulesRoot, ".uilint", "rules");
|
|
991
|
+
const relativeRulesPath = relative2(configDir, rulesDir).replace(/\\/g, "/");
|
|
992
|
+
const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
|
|
993
|
+
const used = collectTopLevelBindings(mod.$ast);
|
|
994
|
+
for (const rule of selectedRules) {
|
|
995
|
+
const importName = chooseUniqueIdentifier(
|
|
996
|
+
`${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
|
|
997
|
+
used
|
|
998
|
+
);
|
|
999
|
+
importNames.set(rule.id, importName);
|
|
1000
|
+
used.add(importName);
|
|
1001
|
+
const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
|
|
1002
|
+
mod.imports.$add({
|
|
1003
|
+
imported: "default",
|
|
1004
|
+
local: importName,
|
|
1005
|
+
from: rulePath
|
|
1006
|
+
});
|
|
1007
|
+
changed = true;
|
|
1008
|
+
}
|
|
1009
|
+
return { importNames, changed };
|
|
1010
|
+
}
|
|
1011
|
+
function addLocalRuleRequiresAst(program, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
|
|
1012
|
+
const importNames = /* @__PURE__ */ new Map();
|
|
1013
|
+
let changed = false;
|
|
1014
|
+
if (!program || program.type !== "Program") {
|
|
1015
|
+
return { importNames, changed };
|
|
1016
|
+
}
|
|
1017
|
+
const configDir = dirname2(configPath);
|
|
1018
|
+
const rulesDir = join4(rulesRoot, ".uilint", "rules");
|
|
1019
|
+
const relativeRulesPath = relative2(configDir, rulesDir).replace(/\\/g, "/");
|
|
1020
|
+
const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
|
|
1021
|
+
const used = collectTopLevelBindings(program);
|
|
1022
|
+
for (const rule of selectedRules) {
|
|
1023
|
+
const importName = chooseUniqueIdentifier(
|
|
1024
|
+
`${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
|
|
1025
|
+
used
|
|
1026
|
+
);
|
|
1027
|
+
importNames.set(rule.id, importName);
|
|
1028
|
+
used.add(importName);
|
|
1029
|
+
const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
|
|
1030
|
+
const stmtMod = parseModule(
|
|
1031
|
+
`const ${importName} = require("${rulePath}");`
|
|
1032
|
+
);
|
|
1033
|
+
const stmt = stmtMod.$ast.body?.[0];
|
|
1034
|
+
if (stmt) {
|
|
1035
|
+
let insertAt = 0;
|
|
1036
|
+
const first = program.body?.[0];
|
|
1037
|
+
if (first?.type === "ExpressionStatement" && first.expression?.type === "StringLiteral" && first.expression.value === "use strict") {
|
|
1038
|
+
insertAt = 1;
|
|
1039
|
+
}
|
|
1040
|
+
program.body.splice(insertAt, 0, stmt);
|
|
1041
|
+
changed = true;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return { importNames, changed };
|
|
1045
|
+
}
|
|
1046
|
+
function appendUilintConfigBlockToArray(arrayExpr, selectedRules, ruleImportNames) {
|
|
1047
|
+
const pluginRulesCode = Array.from(ruleImportNames.entries()).map(([ruleId, importName]) => ` "${ruleId}": ${importName},`).join("\n");
|
|
1048
|
+
const rulesPropsCode = selectedRules.map((r) => {
|
|
1049
|
+
const ruleKey = `uilint/${r.id}`;
|
|
1050
|
+
const valueCode = r.defaultOptions && r.defaultOptions.length > 0 ? `["${r.defaultSeverity}", ...${JSON.stringify(
|
|
1051
|
+
r.defaultOptions,
|
|
1052
|
+
null,
|
|
1053
|
+
2
|
|
1054
|
+
)}]` : `"${r.defaultSeverity}"`;
|
|
1055
|
+
return ` "${ruleKey}": ${valueCode},`;
|
|
1056
|
+
}).join("\n");
|
|
1057
|
+
const blockCode = `{
|
|
1058
|
+
files: [
|
|
1059
|
+
"src/**/*.{js,jsx,ts,tsx}",
|
|
1060
|
+
"app/**/*.{js,jsx,ts,tsx}",
|
|
1061
|
+
"pages/**/*.{js,jsx,ts,tsx}",
|
|
1062
|
+
],
|
|
1063
|
+
plugins: {
|
|
1064
|
+
uilint: {
|
|
1065
|
+
rules: {
|
|
1066
|
+
${pluginRulesCode}
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
1070
|
+
rules: {
|
|
1071
|
+
${rulesPropsCode}
|
|
1072
|
+
},
|
|
1073
|
+
}`;
|
|
1074
|
+
const objExpr = parseExpression(blockCode).$ast;
|
|
1075
|
+
arrayExpr.elements.push(objExpr);
|
|
1076
|
+
}
|
|
1077
|
+
function getUilintEslintConfigInfoFromSourceAst(source) {
|
|
1078
|
+
try {
|
|
1079
|
+
const mod = parseModule(source);
|
|
1080
|
+
const found = findExportedConfigArrayExpression(mod);
|
|
1081
|
+
if (!found) {
|
|
1082
|
+
return {
|
|
1083
|
+
error: "Could not locate an exported ESLint flat config array (expected `export default [...]`, `export default defineConfig([...])`, `module.exports = [...]`, or `module.exports = defineConfig([...])`)."
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
const configuredRuleIds = collectConfiguredUilintRuleIdsFromConfigArray(
|
|
1087
|
+
found.arrayExpr
|
|
1088
|
+
);
|
|
1089
|
+
const existingUilint = findExistingUilintRulesObject(found.arrayExpr);
|
|
1090
|
+
const configured = configuredRuleIds.size > 0 || existingUilint.configObj !== null;
|
|
1091
|
+
return {
|
|
1092
|
+
info: { configuredRuleIds, configured },
|
|
1093
|
+
mod,
|
|
1094
|
+
arrayExpr: found.arrayExpr,
|
|
1095
|
+
kind: found.kind
|
|
1096
|
+
};
|
|
1097
|
+
} catch {
|
|
1098
|
+
return {
|
|
1099
|
+
error: "Unable to parse ESLint config as JavaScript. Please update it manually or simplify the config so it can be safely auto-modified."
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function getUilintEslintConfigInfoFromSource(source) {
|
|
1104
|
+
const ast = getUilintEslintConfigInfoFromSourceAst(source);
|
|
1105
|
+
if ("error" in ast) {
|
|
1106
|
+
const configuredRuleIds = extractConfiguredUilintRuleIds(source);
|
|
1107
|
+
return {
|
|
1108
|
+
configuredRuleIds,
|
|
1109
|
+
configured: configuredRuleIds.size > 0
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
return ast.info;
|
|
1113
|
+
}
|
|
1114
|
+
function extractConfiguredUilintRuleIds(source) {
|
|
1115
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1116
|
+
const re = /["']uilint\/([^"']+)["']\s*:/g;
|
|
1117
|
+
for (const m of source.matchAll(re)) {
|
|
1118
|
+
if (m[1]) ids.add(m[1]);
|
|
1119
|
+
}
|
|
1120
|
+
return ids;
|
|
1121
|
+
}
|
|
1122
|
+
function getMissingSelectedRules(selectedRules, configuredIds) {
|
|
1123
|
+
return selectedRules.filter((r) => !configuredIds.has(r.id));
|
|
1124
|
+
}
|
|
1125
|
+
function buildDesiredRuleValueExpression(rule) {
|
|
1126
|
+
if (rule.defaultOptions && rule.defaultOptions.length > 0) {
|
|
1127
|
+
return `["${rule.defaultSeverity}", ...${JSON.stringify(
|
|
1128
|
+
rule.defaultOptions,
|
|
1129
|
+
null,
|
|
1130
|
+
2
|
|
1131
|
+
)}]`;
|
|
1132
|
+
}
|
|
1133
|
+
return `"${rule.defaultSeverity}"`;
|
|
1134
|
+
}
|
|
1135
|
+
function collectUilintRuleValueNodesFromConfigArray(arrayExpr) {
|
|
1136
|
+
const out = /* @__PURE__ */ new Map();
|
|
1137
|
+
if (!arrayExpr || arrayExpr.type !== "ArrayExpression") return out;
|
|
1138
|
+
for (const el of arrayExpr.elements ?? []) {
|
|
1139
|
+
if (!el || el.type !== "ObjectExpression") continue;
|
|
1140
|
+
const rules = getObjectPropertyValue(el, "rules");
|
|
1141
|
+
if (!rules || rules.type !== "ObjectExpression") continue;
|
|
1142
|
+
for (const prop of rules.properties ?? []) {
|
|
1143
|
+
if (!prop) continue;
|
|
1144
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
|
|
1145
|
+
const key = prop.key;
|
|
1146
|
+
if (!isStringLiteral(key)) continue;
|
|
1147
|
+
const k = key.value;
|
|
1148
|
+
if (typeof k !== "string" || !k.startsWith("uilint/")) continue;
|
|
1149
|
+
const id = k.slice("uilint/".length);
|
|
1150
|
+
if (!out.has(id)) out.set(id, prop.value);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return out;
|
|
1154
|
+
}
|
|
1155
|
+
function getRulesNeedingUpdate(selectedRules, configuredIds, arrayExpr) {
|
|
1156
|
+
const existingVals = collectUilintRuleValueNodesFromConfigArray(arrayExpr);
|
|
1157
|
+
return selectedRules.filter((r) => {
|
|
1158
|
+
if (!configuredIds.has(r.id)) return false;
|
|
1159
|
+
const existing = existingVals.get(r.id);
|
|
1160
|
+
if (!existing) return true;
|
|
1161
|
+
const desiredExpr = buildDesiredRuleValueExpression(r);
|
|
1162
|
+
const desiredAst = parseExpression(desiredExpr).$ast;
|
|
1163
|
+
return !astEquivalent(existing, desiredAst);
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
async function installEslintPlugin(opts) {
|
|
1167
|
+
const configPath = findEslintConfigFile(opts.projectPath);
|
|
1168
|
+
if (!configPath) {
|
|
1169
|
+
return {
|
|
1170
|
+
configFile: null,
|
|
1171
|
+
modified: false,
|
|
1172
|
+
missingRuleIds: [],
|
|
1173
|
+
configured: false
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
const configFilename = getEslintConfigFilename(configPath);
|
|
1177
|
+
const original = readFileSync3(configPath, "utf-8");
|
|
1178
|
+
const isCommonJS = configPath.endsWith(".cjs");
|
|
1179
|
+
const ast = getUilintEslintConfigInfoFromSourceAst(original);
|
|
1180
|
+
if ("error" in ast) {
|
|
1181
|
+
return {
|
|
1182
|
+
configFile: configFilename,
|
|
1183
|
+
modified: false,
|
|
1184
|
+
missingRuleIds: [],
|
|
1185
|
+
configured: false,
|
|
1186
|
+
error: ast.error
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
const { info, mod, arrayExpr, kind } = ast;
|
|
1190
|
+
const configuredIds = info.configuredRuleIds;
|
|
1191
|
+
const missingRules = getMissingSelectedRules(
|
|
1192
|
+
opts.selectedRules,
|
|
1193
|
+
configuredIds
|
|
1194
|
+
);
|
|
1195
|
+
const rulesToUpdate = getRulesNeedingUpdate(
|
|
1196
|
+
opts.selectedRules,
|
|
1197
|
+
configuredIds,
|
|
1198
|
+
arrayExpr
|
|
1199
|
+
);
|
|
1200
|
+
let rulesToApply = [];
|
|
1201
|
+
if (!info.configured) {
|
|
1202
|
+
rulesToApply = opts.selectedRules;
|
|
1203
|
+
} else {
|
|
1204
|
+
rulesToApply = [...missingRules, ...rulesToUpdate];
|
|
1205
|
+
if (missingRules.length > 0 && !opts.force) {
|
|
1206
|
+
const ok = await opts.confirmAddMissingRules?.(
|
|
1207
|
+
configFilename,
|
|
1208
|
+
missingRules
|
|
1209
|
+
);
|
|
1210
|
+
if (!ok) {
|
|
1211
|
+
return {
|
|
1212
|
+
configFile: configFilename,
|
|
1213
|
+
modified: false,
|
|
1214
|
+
missingRuleIds: missingRules.map((r) => r.id),
|
|
1215
|
+
configured: true
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (rulesToApply.length === 0) {
|
|
1221
|
+
return {
|
|
1222
|
+
configFile: configFilename,
|
|
1223
|
+
modified: false,
|
|
1224
|
+
missingRuleIds: missingRules.map((r) => r.id),
|
|
1225
|
+
configured: info.configured
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
let modifiedAst = false;
|
|
1229
|
+
const localRulesDir = join4(opts.projectPath, ".uilint", "rules");
|
|
1230
|
+
const workspaceRoot = findWorkspaceRoot(opts.projectPath);
|
|
1231
|
+
const workspaceRulesDir = join4(workspaceRoot, ".uilint", "rules");
|
|
1232
|
+
const rulesRoot = existsSync4(localRulesDir) ? opts.projectPath : workspaceRoot;
|
|
1233
|
+
let fileExtension = ".js";
|
|
1234
|
+
if (rulesToApply.length > 0) {
|
|
1235
|
+
const firstRulePath = join4(
|
|
1236
|
+
rulesRoot,
|
|
1237
|
+
".uilint",
|
|
1238
|
+
"rules",
|
|
1239
|
+
`${rulesToApply[0].id}.ts`
|
|
1240
|
+
);
|
|
1241
|
+
if (existsSync4(firstRulePath)) {
|
|
1242
|
+
fileExtension = ".ts";
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
let ruleImportNames;
|
|
1246
|
+
if (kind === "esm") {
|
|
1247
|
+
const result = addLocalRuleImportsAst(
|
|
1248
|
+
mod,
|
|
1249
|
+
rulesToApply,
|
|
1250
|
+
configPath,
|
|
1251
|
+
rulesRoot,
|
|
1252
|
+
fileExtension
|
|
1253
|
+
);
|
|
1254
|
+
ruleImportNames = result.importNames;
|
|
1255
|
+
if (result.changed) modifiedAst = true;
|
|
1256
|
+
} else {
|
|
1257
|
+
const result = addLocalRuleRequiresAst(
|
|
1258
|
+
mod.$ast,
|
|
1259
|
+
rulesToApply,
|
|
1260
|
+
configPath,
|
|
1261
|
+
rulesRoot,
|
|
1262
|
+
fileExtension
|
|
1263
|
+
);
|
|
1264
|
+
ruleImportNames = result.importNames;
|
|
1265
|
+
if (result.changed) modifiedAst = true;
|
|
1266
|
+
}
|
|
1267
|
+
if (ruleImportNames && ruleImportNames.size > 0) {
|
|
1268
|
+
appendUilintConfigBlockToArray(arrayExpr, rulesToApply, ruleImportNames);
|
|
1269
|
+
modifiedAst = true;
|
|
1270
|
+
}
|
|
1271
|
+
if (!info.configured) {
|
|
1272
|
+
if (kind === "esm") {
|
|
1273
|
+
mod.imports.$add({
|
|
1274
|
+
imported: "createRule",
|
|
1275
|
+
local: "createRule",
|
|
1276
|
+
from: "uilint-eslint"
|
|
1277
|
+
});
|
|
1278
|
+
modifiedAst = true;
|
|
1279
|
+
} else {
|
|
1280
|
+
const stmtMod = parseModule(
|
|
1281
|
+
`const { createRule } = require("uilint-eslint");`
|
|
1282
|
+
);
|
|
1283
|
+
const stmt = stmtMod.$ast.body?.[0];
|
|
1284
|
+
if (stmt) {
|
|
1285
|
+
let insertAt = 0;
|
|
1286
|
+
const first = mod.$ast.body?.[0];
|
|
1287
|
+
if (first?.type === "ExpressionStatement" && first.expression?.type === "StringLiteral" && first.expression.value === "use strict") {
|
|
1288
|
+
insertAt = 1;
|
|
1289
|
+
}
|
|
1290
|
+
mod.$ast.body.splice(insertAt, 0, stmt);
|
|
1291
|
+
modifiedAst = true;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
const updated = modifiedAst ? generateCode(mod).code : original;
|
|
1296
|
+
if (updated !== original) {
|
|
1297
|
+
writeFileSync(configPath, updated, "utf-8");
|
|
1298
|
+
return {
|
|
1299
|
+
configFile: configFilename,
|
|
1300
|
+
modified: true,
|
|
1301
|
+
missingRuleIds: missingRules.map((r) => r.id),
|
|
1302
|
+
configured: getUilintEslintConfigInfoFromSource(updated).configured
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
configFile: configFilename,
|
|
1307
|
+
modified: false,
|
|
1308
|
+
missingRuleIds: missingRules.map((r) => r.id),
|
|
1309
|
+
configured: getUilintEslintConfigInfoFromSource(updated).configured
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/commands/install/analyze.ts
|
|
1314
|
+
async function analyze(projectPath = process.cwd()) {
|
|
1315
|
+
const workspaceRoot = findWorkspaceRoot2(projectPath);
|
|
1316
|
+
const packageManager = detectPackageManager(projectPath);
|
|
1317
|
+
const cursorDir = join5(projectPath, ".cursor");
|
|
1318
|
+
const cursorDirExists = existsSync5(cursorDir);
|
|
1319
|
+
const styleguidePath = join5(projectPath, ".uilint", "styleguide.md");
|
|
1320
|
+
const styleguideExists = existsSync5(styleguidePath);
|
|
1321
|
+
const commandsDir = join5(cursorDir, "commands");
|
|
1322
|
+
const genstyleguideExists = existsSync5(join5(commandsDir, "genstyleguide.md"));
|
|
1323
|
+
const nextApps = [];
|
|
1324
|
+
const directDetection = detectNextAppRouter(projectPath);
|
|
1325
|
+
if (directDetection) {
|
|
1326
|
+
nextApps.push({ projectPath, detection: directDetection });
|
|
1327
|
+
} else {
|
|
1328
|
+
const matches = findNextAppRouterProjects(workspaceRoot, { maxDepth: 5 });
|
|
1329
|
+
for (const match of matches) {
|
|
1330
|
+
nextApps.push({
|
|
1331
|
+
projectPath: match.projectPath,
|
|
1332
|
+
detection: match.detection
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const viteApps = [];
|
|
1337
|
+
const directVite = detectViteReact(projectPath);
|
|
1338
|
+
if (directVite) {
|
|
1339
|
+
viteApps.push({ projectPath, detection: directVite });
|
|
1340
|
+
} else {
|
|
1341
|
+
const matches = findViteReactProjects(workspaceRoot, { maxDepth: 5 });
|
|
1342
|
+
for (const match of matches) {
|
|
1343
|
+
viteApps.push({
|
|
1344
|
+
projectPath: match.projectPath,
|
|
1345
|
+
detection: match.detection
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
const rawPackages = findPackages(workspaceRoot);
|
|
1350
|
+
const packages = rawPackages.map((pkg) => {
|
|
1351
|
+
const eslintConfigPath = findEslintConfigFile(pkg.path);
|
|
1352
|
+
let eslintConfigFilename = null;
|
|
1353
|
+
let hasRules = false;
|
|
1354
|
+
let configuredRuleIds = [];
|
|
1355
|
+
if (eslintConfigPath) {
|
|
1356
|
+
eslintConfigFilename = getEslintConfigFilename(eslintConfigPath);
|
|
1357
|
+
try {
|
|
1358
|
+
const source = readFileSync4(eslintConfigPath, "utf-8");
|
|
1359
|
+
const info = getUilintEslintConfigInfoFromSource(source);
|
|
1360
|
+
hasRules = info.configuredRuleIds.size > 0;
|
|
1361
|
+
configuredRuleIds = Array.from(info.configuredRuleIds);
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return {
|
|
1366
|
+
...pkg,
|
|
1367
|
+
eslintConfigPath,
|
|
1368
|
+
eslintConfigFilename,
|
|
1369
|
+
hasUilintRules: hasRules,
|
|
1370
|
+
configuredRuleIds
|
|
1371
|
+
};
|
|
1372
|
+
});
|
|
1373
|
+
return {
|
|
1374
|
+
projectPath,
|
|
1375
|
+
workspaceRoot,
|
|
1376
|
+
packageManager,
|
|
1377
|
+
cursorDir: {
|
|
1378
|
+
exists: cursorDirExists,
|
|
1379
|
+
path: cursorDir
|
|
1380
|
+
},
|
|
1381
|
+
styleguide: {
|
|
1382
|
+
exists: styleguideExists,
|
|
1383
|
+
path: styleguidePath
|
|
1384
|
+
},
|
|
1385
|
+
commands: {
|
|
1386
|
+
genstyleguide: genstyleguideExists
|
|
1387
|
+
},
|
|
1388
|
+
nextApps,
|
|
1389
|
+
viteApps,
|
|
1390
|
+
packages
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/commands/install/execute.ts
|
|
1395
|
+
import {
|
|
1396
|
+
existsSync as existsSync10,
|
|
1397
|
+
mkdirSync,
|
|
1398
|
+
writeFileSync as writeFileSync5,
|
|
1399
|
+
readFileSync as readFileSync8,
|
|
1400
|
+
unlinkSync,
|
|
1401
|
+
chmodSync
|
|
1402
|
+
} from "fs";
|
|
1403
|
+
import { dirname as dirname3 } from "path";
|
|
1404
|
+
|
|
1405
|
+
// src/utils/react-inject.ts
|
|
1406
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1407
|
+
import { join as join6 } from "path";
|
|
1408
|
+
import { parseModule as parseModule2, generateCode as generateCode2 } from "magicast";
|
|
1409
|
+
function getDefaultCandidates(projectPath, appRoot) {
|
|
1410
|
+
const viteMainCandidates = [
|
|
1411
|
+
join6(appRoot, "main.tsx"),
|
|
1412
|
+
join6(appRoot, "main.jsx"),
|
|
1413
|
+
join6(appRoot, "main.ts"),
|
|
1414
|
+
join6(appRoot, "main.js")
|
|
1415
|
+
];
|
|
1416
|
+
const existingViteMain = viteMainCandidates.filter(
|
|
1417
|
+
(rel) => existsSync6(join6(projectPath, rel))
|
|
1418
|
+
);
|
|
1419
|
+
if (existingViteMain.length > 0) return existingViteMain;
|
|
1420
|
+
const viteAppCandidates = [join6(appRoot, "App.tsx"), join6(appRoot, "App.jsx")];
|
|
1421
|
+
const existingViteApp = viteAppCandidates.filter(
|
|
1422
|
+
(rel) => existsSync6(join6(projectPath, rel))
|
|
1423
|
+
);
|
|
1424
|
+
if (existingViteApp.length > 0) return existingViteApp;
|
|
1425
|
+
const layoutCandidates = [
|
|
1426
|
+
join6(appRoot, "layout.tsx"),
|
|
1427
|
+
join6(appRoot, "layout.jsx"),
|
|
1428
|
+
join6(appRoot, "layout.ts"),
|
|
1429
|
+
join6(appRoot, "layout.js")
|
|
1430
|
+
];
|
|
1431
|
+
const existingLayouts = layoutCandidates.filter(
|
|
1432
|
+
(rel) => existsSync6(join6(projectPath, rel))
|
|
1433
|
+
);
|
|
1434
|
+
if (existingLayouts.length > 0) {
|
|
1435
|
+
return existingLayouts;
|
|
1436
|
+
}
|
|
1437
|
+
const pageCandidates = [join6(appRoot, "page.tsx"), join6(appRoot, "page.jsx")];
|
|
1438
|
+
return pageCandidates.filter((rel) => existsSync6(join6(projectPath, rel)));
|
|
1439
|
+
}
|
|
1440
|
+
function isUseClientDirective(stmt) {
|
|
1441
|
+
return stmt?.type === "ExpressionStatement" && stmt.expression?.type === "StringLiteral" && stmt.expression.value === "use client";
|
|
1442
|
+
}
|
|
1443
|
+
function findImportDeclaration(program, from) {
|
|
1444
|
+
if (!program || program.type !== "Program") return null;
|
|
1445
|
+
for (const stmt of program.body ?? []) {
|
|
1446
|
+
if (stmt?.type !== "ImportDeclaration") continue;
|
|
1447
|
+
if (stmt.source?.value === from) return stmt;
|
|
1448
|
+
}
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
function walkAst(node, visit) {
|
|
1452
|
+
if (!node || typeof node !== "object") return;
|
|
1453
|
+
if (node.type) visit(node);
|
|
1454
|
+
for (const key of Object.keys(node)) {
|
|
1455
|
+
const v = node[key];
|
|
1456
|
+
if (!v) continue;
|
|
1457
|
+
if (Array.isArray(v)) {
|
|
1458
|
+
for (const item of v) walkAst(item, visit);
|
|
1459
|
+
} else if (typeof v === "object" && v.type) {
|
|
1460
|
+
walkAst(v, visit);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
function hasUILintDevtoolsJsx(program) {
|
|
1465
|
+
let found = false;
|
|
1466
|
+
walkAst(program, (node) => {
|
|
1467
|
+
if (found) return;
|
|
1468
|
+
if (node.type !== "JSXElement") return;
|
|
1469
|
+
const name = node.openingElement?.name;
|
|
1470
|
+
if (name?.type === "JSXIdentifier") {
|
|
1471
|
+
if (name.name === "UILintProvider" || name.name === "uilint-devtools") {
|
|
1472
|
+
found = true;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
return found;
|
|
1477
|
+
}
|
|
1478
|
+
function addDevtoolsElementNextJs(program) {
|
|
1479
|
+
if (!program || program.type !== "Program") return { changed: false };
|
|
1480
|
+
if (hasUILintDevtoolsJsx(program)) return { changed: false };
|
|
1481
|
+
const devtoolsMod = parseModule2(
|
|
1482
|
+
"const __uilint_devtools = (<uilint-devtools />);"
|
|
1483
|
+
);
|
|
1484
|
+
const devtoolsJsx = devtoolsMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
|
|
1485
|
+
if (!devtoolsJsx || devtoolsJsx.type !== "JSXElement")
|
|
1486
|
+
return { changed: false };
|
|
1487
|
+
let added = false;
|
|
1488
|
+
walkAst(program, (node) => {
|
|
1489
|
+
if (added) return;
|
|
1490
|
+
if (node.type !== "JSXElement" && node.type !== "JSXFragment") return;
|
|
1491
|
+
const children = node.children ?? [];
|
|
1492
|
+
const childrenIndex = children.findIndex(
|
|
1493
|
+
(child) => child?.type === "JSXExpressionContainer" && child.expression?.type === "Identifier" && child.expression.name === "children"
|
|
1494
|
+
);
|
|
1495
|
+
if (childrenIndex === -1) return;
|
|
1496
|
+
children.splice(childrenIndex + 1, 0, devtoolsJsx);
|
|
1497
|
+
added = true;
|
|
1498
|
+
});
|
|
1499
|
+
if (!added) {
|
|
1500
|
+
throw new Error("Could not find `{children}` in target file to add devtools.");
|
|
1501
|
+
}
|
|
1502
|
+
return { changed: true };
|
|
1503
|
+
}
|
|
1504
|
+
function addDevtoolsElementVite(program) {
|
|
1505
|
+
if (!program || program.type !== "Program") return { changed: false };
|
|
1506
|
+
if (hasUILintDevtoolsJsx(program)) return { changed: false };
|
|
1507
|
+
let added = false;
|
|
1508
|
+
walkAst(program, (node) => {
|
|
1509
|
+
if (added) return;
|
|
1510
|
+
if (node.type !== "CallExpression") return;
|
|
1511
|
+
const callee = node.callee;
|
|
1512
|
+
if (callee?.type !== "MemberExpression") return;
|
|
1513
|
+
const prop = callee.property;
|
|
1514
|
+
const isRender = prop?.type === "Identifier" && prop.name === "render" || prop?.type === "StringLiteral" && prop.value === "render" || prop?.type === "Literal" && prop.value === "render";
|
|
1515
|
+
if (!isRender) return;
|
|
1516
|
+
const arg0 = node.arguments?.[0];
|
|
1517
|
+
if (!arg0) return;
|
|
1518
|
+
if (arg0.type !== "JSXElement" && arg0.type !== "JSXFragment") return;
|
|
1519
|
+
const devtoolsMod = parseModule2(
|
|
1520
|
+
"const __uilint_devtools = (<uilint-devtools />);"
|
|
1521
|
+
);
|
|
1522
|
+
const devtoolsJsx = devtoolsMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
|
|
1523
|
+
if (!devtoolsJsx) return;
|
|
1524
|
+
const fragmentMod = parseModule2(
|
|
1525
|
+
"const __fragment = (<></>);"
|
|
1526
|
+
);
|
|
1527
|
+
const fragmentJsx = fragmentMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
|
|
1528
|
+
if (!fragmentJsx) return;
|
|
1529
|
+
fragmentJsx.children = [arg0, devtoolsJsx];
|
|
1530
|
+
node.arguments[0] = fragmentJsx;
|
|
1531
|
+
added = true;
|
|
1532
|
+
});
|
|
1533
|
+
if (!added) {
|
|
1534
|
+
throw new Error(
|
|
1535
|
+
"Could not find a `.render(<...>)` call to add devtools. Expected a React entry like `createRoot(...).render(<App />)`."
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
return { changed: true };
|
|
1539
|
+
}
|
|
1540
|
+
function ensureSideEffectImport(program, from) {
|
|
1541
|
+
if (!program || program.type !== "Program") return { changed: false };
|
|
1542
|
+
const existing = findImportDeclaration(program, from);
|
|
1543
|
+
if (existing) return { changed: false };
|
|
1544
|
+
const importDecl = parseModule2(`import "${from}";`).$ast.body?.[0];
|
|
1545
|
+
if (!importDecl) return { changed: false };
|
|
1546
|
+
const body = program.body ?? [];
|
|
1547
|
+
let insertAt = 0;
|
|
1548
|
+
while (insertAt < body.length && isUseClientDirective(body[insertAt])) {
|
|
1549
|
+
insertAt++;
|
|
1550
|
+
}
|
|
1551
|
+
while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
|
|
1552
|
+
insertAt++;
|
|
1553
|
+
}
|
|
1554
|
+
program.body.splice(insertAt, 0, importDecl);
|
|
1555
|
+
return { changed: true };
|
|
1556
|
+
}
|
|
1557
|
+
async function installReactUILintOverlay(opts) {
|
|
1558
|
+
const candidates = getDefaultCandidates(opts.projectPath, opts.appRoot);
|
|
1559
|
+
if (!candidates.length) {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`No suitable entry files found under ${opts.appRoot} (expected Next.js layout/page or Vite main/App).`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
let chosen;
|
|
1565
|
+
if (candidates.length > 1 && opts.confirmFileChoice) {
|
|
1566
|
+
chosen = await opts.confirmFileChoice(candidates);
|
|
1567
|
+
} else {
|
|
1568
|
+
chosen = candidates[0];
|
|
1569
|
+
}
|
|
1570
|
+
const absTarget = join6(opts.projectPath, chosen);
|
|
1571
|
+
const original = readFileSync5(absTarget, "utf-8");
|
|
1572
|
+
let mod;
|
|
1573
|
+
try {
|
|
1574
|
+
mod = parseModule2(original);
|
|
1575
|
+
} catch {
|
|
1576
|
+
throw new Error(
|
|
1577
|
+
`Unable to parse ${chosen} as JavaScript/TypeScript. Please update it manually.`
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
const program = mod.$ast;
|
|
1581
|
+
const hasDevtoolsImport = !!findImportDeclaration(program, "uilint-react/devtools");
|
|
1582
|
+
const hasOldImport = !!findImportDeclaration(program, "uilint-react");
|
|
1583
|
+
const alreadyConfigured = (hasDevtoolsImport || hasOldImport) && hasUILintDevtoolsJsx(program);
|
|
1584
|
+
let changed = false;
|
|
1585
|
+
const importRes = ensureSideEffectImport(program, "uilint-react/devtools");
|
|
1586
|
+
if (importRes.changed) changed = true;
|
|
1587
|
+
const mode = opts.mode ?? "next";
|
|
1588
|
+
const addRes = mode === "vite" ? addDevtoolsElementVite(program) : addDevtoolsElementNextJs(program);
|
|
1589
|
+
if (addRes.changed) changed = true;
|
|
1590
|
+
const updated = changed ? generateCode2(mod).code : original;
|
|
1591
|
+
const modified = updated !== original;
|
|
1592
|
+
if (modified) {
|
|
1593
|
+
writeFileSync2(absTarget, updated, "utf-8");
|
|
1594
|
+
}
|
|
1595
|
+
return {
|
|
1596
|
+
targetFile: chosen,
|
|
1597
|
+
modified,
|
|
1598
|
+
alreadyConfigured: alreadyConfigured && !modified
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// src/utils/next-config-inject.ts
|
|
1603
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
1604
|
+
import { join as join7 } from "path";
|
|
1605
|
+
import { parseModule as parseModule3, generateCode as generateCode3 } from "magicast";
|
|
1606
|
+
var CONFIG_EXTENSIONS2 = [".ts", ".mjs", ".js", ".cjs"];
|
|
1607
|
+
function findNextConfigFile(projectPath) {
|
|
1608
|
+
for (const ext of CONFIG_EXTENSIONS2) {
|
|
1609
|
+
const configPath = join7(projectPath, `next.config${ext}`);
|
|
1610
|
+
if (existsSync7(configPath)) {
|
|
1611
|
+
return configPath;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
function getNextConfigFilename(configPath) {
|
|
1617
|
+
const parts = configPath.split("/");
|
|
1618
|
+
return parts[parts.length - 1] || "next.config.ts";
|
|
1619
|
+
}
|
|
1620
|
+
function isIdentifier2(node, name) {
|
|
1621
|
+
return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
|
|
1622
|
+
}
|
|
1623
|
+
function isStringLiteral2(node) {
|
|
1624
|
+
return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
|
|
1625
|
+
}
|
|
1626
|
+
function ensureEsmWithJsxLocImport(program) {
|
|
1627
|
+
if (!program || program.type !== "Program") return { changed: false };
|
|
1628
|
+
const existing = (program.body ?? []).find(
|
|
1629
|
+
(s) => s?.type === "ImportDeclaration" && s.source?.value === "jsx-loc-plugin"
|
|
1630
|
+
);
|
|
1631
|
+
if (existing) {
|
|
1632
|
+
const has = (existing.specifiers ?? []).some(
|
|
1633
|
+
(sp) => sp?.type === "ImportSpecifier" && (sp.imported?.name === "withJsxLoc" || sp.imported?.value === "withJsxLoc")
|
|
1634
|
+
);
|
|
1635
|
+
if (has) return { changed: false };
|
|
1636
|
+
const spec = parseModule3('import { withJsxLoc } from "jsx-loc-plugin";').$ast.body?.[0]?.specifiers?.[0];
|
|
1637
|
+
if (!spec) return { changed: false };
|
|
1638
|
+
existing.specifiers = [...existing.specifiers ?? [], spec];
|
|
1639
|
+
return { changed: true };
|
|
1640
|
+
}
|
|
1641
|
+
const importDecl = parseModule3('import { withJsxLoc } from "jsx-loc-plugin";').$ast.body?.[0];
|
|
1642
|
+
if (!importDecl) return { changed: false };
|
|
1643
|
+
const body = program.body ?? [];
|
|
1644
|
+
let insertAt = 0;
|
|
1645
|
+
while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
|
|
1646
|
+
insertAt++;
|
|
1647
|
+
}
|
|
1648
|
+
program.body.splice(insertAt, 0, importDecl);
|
|
1649
|
+
return { changed: true };
|
|
1650
|
+
}
|
|
1651
|
+
function ensureCjsWithJsxLocRequire(program) {
|
|
1652
|
+
if (!program || program.type !== "Program") return { changed: false };
|
|
1653
|
+
for (const stmt of program.body ?? []) {
|
|
1654
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
1655
|
+
for (const decl of stmt.declarations ?? []) {
|
|
1656
|
+
const init = decl?.init;
|
|
1657
|
+
if (init?.type === "CallExpression" && isIdentifier2(init.callee, "require") && isStringLiteral2(init.arguments?.[0]) && init.arguments[0].value === "jsx-loc-plugin") {
|
|
1658
|
+
if (decl.id?.type === "ObjectPattern") {
|
|
1659
|
+
const has = (decl.id.properties ?? []).some((p) => {
|
|
1660
|
+
if (p?.type !== "ObjectProperty" && p?.type !== "Property") return false;
|
|
1661
|
+
return isIdentifier2(p.key, "withJsxLoc");
|
|
1662
|
+
});
|
|
1663
|
+
if (has) return { changed: false };
|
|
1664
|
+
const prop = parseModule3('const { withJsxLoc } = require("jsx-loc-plugin");').$ast.body?.[0]?.declarations?.[0]?.id?.properties?.[0];
|
|
1665
|
+
if (!prop) return { changed: false };
|
|
1666
|
+
decl.id.properties = [...decl.id.properties ?? [], prop];
|
|
1667
|
+
return { changed: true };
|
|
1668
|
+
}
|
|
1669
|
+
return { changed: false };
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
const reqDecl = parseModule3('const { withJsxLoc } = require("jsx-loc-plugin");').$ast.body?.[0];
|
|
1674
|
+
if (!reqDecl) return { changed: false };
|
|
1675
|
+
program.body.unshift(reqDecl);
|
|
1676
|
+
return { changed: true };
|
|
1677
|
+
}
|
|
1678
|
+
function wrapEsmExportDefault(program) {
|
|
1679
|
+
if (!program || program.type !== "Program") return { changed: false };
|
|
1680
|
+
const exportDecl = (program.body ?? []).find(
|
|
1681
|
+
(s) => s?.type === "ExportDefaultDeclaration"
|
|
1682
|
+
);
|
|
1683
|
+
if (!exportDecl) return { changed: false };
|
|
1684
|
+
const decl = exportDecl.declaration;
|
|
1685
|
+
if (decl?.type === "CallExpression" && isIdentifier2(decl.callee, "withJsxLoc")) {
|
|
1686
|
+
return { changed: false };
|
|
1687
|
+
}
|
|
1688
|
+
exportDecl.declaration = {
|
|
1689
|
+
type: "CallExpression",
|
|
1690
|
+
callee: { type: "Identifier", name: "withJsxLoc" },
|
|
1691
|
+
arguments: [decl]
|
|
1692
|
+
};
|
|
1693
|
+
return { changed: true };
|
|
1694
|
+
}
|
|
1695
|
+
function wrapCjsModuleExports(program) {
|
|
1696
|
+
if (!program || program.type !== "Program") return { changed: false };
|
|
1697
|
+
for (const stmt of program.body ?? []) {
|
|
1698
|
+
if (!stmt || stmt.type !== "ExpressionStatement") continue;
|
|
1699
|
+
const expr = stmt.expression;
|
|
1700
|
+
if (!expr || expr.type !== "AssignmentExpression") continue;
|
|
1701
|
+
const left = expr.left;
|
|
1702
|
+
const right = expr.right;
|
|
1703
|
+
const isModuleExports = left?.type === "MemberExpression" && isIdentifier2(left.object, "module") && isIdentifier2(left.property, "exports");
|
|
1704
|
+
if (!isModuleExports) continue;
|
|
1705
|
+
if (right?.type === "CallExpression" && isIdentifier2(right.callee, "withJsxLoc")) {
|
|
1706
|
+
return { changed: false };
|
|
1707
|
+
}
|
|
1708
|
+
expr.right = {
|
|
1709
|
+
type: "CallExpression",
|
|
1710
|
+
callee: { type: "Identifier", name: "withJsxLoc" },
|
|
1711
|
+
arguments: [right]
|
|
1712
|
+
};
|
|
1713
|
+
return { changed: true };
|
|
1714
|
+
}
|
|
1715
|
+
return { changed: false };
|
|
1716
|
+
}
|
|
1717
|
+
async function installJsxLocPlugin(opts) {
|
|
1718
|
+
const configPath = findNextConfigFile(opts.projectPath);
|
|
1719
|
+
if (!configPath) {
|
|
1720
|
+
return { configFile: null, modified: false };
|
|
1721
|
+
}
|
|
1722
|
+
const configFilename = getNextConfigFilename(configPath);
|
|
1723
|
+
const original = readFileSync6(configPath, "utf-8");
|
|
1724
|
+
let mod;
|
|
1725
|
+
try {
|
|
1726
|
+
mod = parseModule3(original);
|
|
1727
|
+
} catch {
|
|
1728
|
+
return { configFile: configFilename, modified: false };
|
|
1729
|
+
}
|
|
1730
|
+
const program = mod.$ast;
|
|
1731
|
+
const isCjs = configPath.endsWith(".cjs");
|
|
1732
|
+
let changed = false;
|
|
1733
|
+
if (isCjs) {
|
|
1734
|
+
const reqRes = ensureCjsWithJsxLocRequire(program);
|
|
1735
|
+
if (reqRes.changed) changed = true;
|
|
1736
|
+
const wrapRes = wrapCjsModuleExports(program);
|
|
1737
|
+
if (wrapRes.changed) changed = true;
|
|
1738
|
+
} else {
|
|
1739
|
+
const impRes = ensureEsmWithJsxLocImport(program);
|
|
1740
|
+
if (impRes.changed) changed = true;
|
|
1741
|
+
const wrapRes = wrapEsmExportDefault(program);
|
|
1742
|
+
if (wrapRes.changed) changed = true;
|
|
1743
|
+
}
|
|
1744
|
+
const updated = changed ? generateCode3(mod).code : original;
|
|
1745
|
+
if (updated !== original) {
|
|
1746
|
+
writeFileSync3(configPath, updated, "utf-8");
|
|
1747
|
+
return { configFile: configFilename, modified: true };
|
|
1748
|
+
}
|
|
1749
|
+
return { configFile: configFilename, modified: false };
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// src/utils/vite-config-inject.ts
|
|
1753
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
1754
|
+
import { join as join8 } from "path";
|
|
1755
|
+
import { parseModule as parseModule4, generateCode as generateCode4 } from "magicast";
|
|
1756
|
+
var CONFIG_EXTENSIONS3 = [".ts", ".mjs", ".js", ".cjs"];
|
|
1757
|
+
function findViteConfigFile2(projectPath) {
|
|
1758
|
+
for (const ext of CONFIG_EXTENSIONS3) {
|
|
1759
|
+
const configPath = join8(projectPath, `vite.config${ext}`);
|
|
1760
|
+
if (existsSync8(configPath)) return configPath;
|
|
1761
|
+
}
|
|
1762
|
+
return null;
|
|
1763
|
+
}
|
|
1764
|
+
function getViteConfigFilename(configPath) {
|
|
1765
|
+
const parts = configPath.split("/");
|
|
1766
|
+
return parts[parts.length - 1] || "vite.config.ts";
|
|
1767
|
+
}
|
|
1768
|
+
function isIdentifier3(node, name) {
|
|
1769
|
+
return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
|
|
1770
|
+
}
|
|
1771
|
+
function isStringLiteral3(node) {
|
|
1772
|
+
return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
|
|
1773
|
+
}
|
|
1774
|
+
function unwrapExpression(expr) {
|
|
1775
|
+
let e = expr;
|
|
1776
|
+
while (e) {
|
|
1777
|
+
if (e.type === "TSAsExpression" || e.type === "TSNonNullExpression") {
|
|
1778
|
+
e = e.expression;
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if (e.type === "TSSatisfiesExpression") {
|
|
1782
|
+
e = e.expression;
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
if (e.type === "ParenthesizedExpression") {
|
|
1786
|
+
e = e.expression;
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
return e;
|
|
1792
|
+
}
|
|
1793
|
+
function findExportedConfigObjectExpression(mod) {
|
|
1794
|
+
const program = mod?.$ast;
|
|
1795
|
+
if (!program || program.type !== "Program") return null;
|
|
1796
|
+
for (const stmt of program.body ?? []) {
|
|
1797
|
+
if (!stmt || stmt.type !== "ExportDefaultDeclaration") continue;
|
|
1798
|
+
const decl = unwrapExpression(stmt.declaration);
|
|
1799
|
+
if (!decl) break;
|
|
1800
|
+
if (decl.type === "ObjectExpression") {
|
|
1801
|
+
return { kind: "esm", objExpr: decl, program };
|
|
1802
|
+
}
|
|
1803
|
+
if (decl.type === "CallExpression" && isIdentifier3(decl.callee, "defineConfig") && unwrapExpression(decl.arguments?.[0])?.type === "ObjectExpression") {
|
|
1804
|
+
return {
|
|
1805
|
+
kind: "esm",
|
|
1806
|
+
objExpr: unwrapExpression(decl.arguments?.[0]),
|
|
1807
|
+
program
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
break;
|
|
1811
|
+
}
|
|
1812
|
+
for (const stmt of program.body ?? []) {
|
|
1813
|
+
if (!stmt || stmt.type !== "ExpressionStatement") continue;
|
|
1814
|
+
const expr = stmt.expression;
|
|
1815
|
+
if (!expr || expr.type !== "AssignmentExpression") continue;
|
|
1816
|
+
const left = expr.left;
|
|
1817
|
+
const right = unwrapExpression(expr.right);
|
|
1818
|
+
const isModuleExports = left?.type === "MemberExpression" && isIdentifier3(left.object, "module") && isIdentifier3(left.property, "exports");
|
|
1819
|
+
if (!isModuleExports) continue;
|
|
1820
|
+
if (right?.type === "ObjectExpression") {
|
|
1821
|
+
return { kind: "cjs", objExpr: right, program };
|
|
1822
|
+
}
|
|
1823
|
+
if (right?.type === "CallExpression" && isIdentifier3(right.callee, "defineConfig") && unwrapExpression(right.arguments?.[0])?.type === "ObjectExpression") {
|
|
1824
|
+
return {
|
|
1825
|
+
kind: "cjs",
|
|
1826
|
+
objExpr: unwrapExpression(right.arguments?.[0]),
|
|
1827
|
+
program
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
function getObjectProperty(obj, keyName) {
|
|
1834
|
+
if (!obj || obj.type !== "ObjectExpression") return null;
|
|
1835
|
+
for (const prop of obj.properties ?? []) {
|
|
1836
|
+
if (!prop) continue;
|
|
1837
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
|
|
1838
|
+
const key = prop.key;
|
|
1839
|
+
const keyMatch = key?.type === "Identifier" && key.name === keyName || isStringLiteral3(key) && key.value === keyName;
|
|
1840
|
+
if (keyMatch) return prop;
|
|
1841
|
+
}
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
function ensureEsmJsxLocImport(program) {
|
|
1845
|
+
if (!program || program.type !== "Program") return { changed: false };
|
|
1846
|
+
const existing = (program.body ?? []).find(
|
|
1847
|
+
(s) => s?.type === "ImportDeclaration" && s.source?.value === "jsx-loc-plugin/vite"
|
|
1848
|
+
);
|
|
1849
|
+
if (existing) {
|
|
1850
|
+
const has = (existing.specifiers ?? []).some(
|
|
1851
|
+
(sp) => sp?.type === "ImportSpecifier" && (sp.imported?.name === "jsxLoc" || sp.imported?.value === "jsxLoc")
|
|
1852
|
+
);
|
|
1853
|
+
if (has) return { changed: false };
|
|
1854
|
+
const spec = parseModule4('import { jsxLoc } from "jsx-loc-plugin/vite";').$ast.body?.[0]?.specifiers?.[0];
|
|
1855
|
+
if (!spec) return { changed: false };
|
|
1856
|
+
existing.specifiers = [...existing.specifiers ?? [], spec];
|
|
1857
|
+
return { changed: true };
|
|
1858
|
+
}
|
|
1859
|
+
const importDecl = parseModule4('import { jsxLoc } from "jsx-loc-plugin/vite";').$ast.body?.[0];
|
|
1860
|
+
if (!importDecl) return { changed: false };
|
|
1861
|
+
const body = program.body ?? [];
|
|
1862
|
+
let insertAt = 0;
|
|
1863
|
+
while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
|
|
1864
|
+
insertAt++;
|
|
1865
|
+
}
|
|
1866
|
+
program.body.splice(insertAt, 0, importDecl);
|
|
1867
|
+
return { changed: true };
|
|
1868
|
+
}
|
|
1869
|
+
function ensureCjsJsxLocRequire(program) {
|
|
1870
|
+
if (!program || program.type !== "Program") return { changed: false };
|
|
1871
|
+
for (const stmt of program.body ?? []) {
|
|
1872
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
1873
|
+
for (const decl of stmt.declarations ?? []) {
|
|
1874
|
+
const init = decl?.init;
|
|
1875
|
+
if (init?.type === "CallExpression" && isIdentifier3(init.callee, "require") && isStringLiteral3(init.arguments?.[0]) && init.arguments[0].value === "jsx-loc-plugin/vite") {
|
|
1876
|
+
if (decl.id?.type === "ObjectPattern") {
|
|
1877
|
+
const has = (decl.id.properties ?? []).some((p) => {
|
|
1878
|
+
if (p?.type !== "ObjectProperty" && p?.type !== "Property") return false;
|
|
1879
|
+
return isIdentifier3(p.key, "jsxLoc");
|
|
1880
|
+
});
|
|
1881
|
+
if (has) return { changed: false };
|
|
1882
|
+
const prop = parseModule4('const { jsxLoc } = require("jsx-loc-plugin/vite");').$ast.body?.[0]?.declarations?.[0]?.id?.properties?.[0];
|
|
1883
|
+
if (!prop) return { changed: false };
|
|
1884
|
+
decl.id.properties = [...decl.id.properties ?? [], prop];
|
|
1885
|
+
return { changed: true };
|
|
1886
|
+
}
|
|
1887
|
+
return { changed: false };
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
const reqDecl = parseModule4('const { jsxLoc } = require("jsx-loc-plugin/vite");').$ast.body?.[0];
|
|
1892
|
+
if (!reqDecl) return { changed: false };
|
|
1893
|
+
program.body.unshift(reqDecl);
|
|
1894
|
+
return { changed: true };
|
|
1895
|
+
}
|
|
1896
|
+
function pluginsHasJsxLoc(arr) {
|
|
1897
|
+
if (!arr || arr.type !== "ArrayExpression") return false;
|
|
1898
|
+
for (const el of arr.elements ?? []) {
|
|
1899
|
+
const e = unwrapExpression(el);
|
|
1900
|
+
if (!e) continue;
|
|
1901
|
+
if (e.type === "CallExpression" && isIdentifier3(e.callee, "jsxLoc")) return true;
|
|
1902
|
+
}
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
function ensurePluginsContainsJsxLoc(configObj) {
|
|
1906
|
+
const pluginsProp = getObjectProperty(configObj, "plugins");
|
|
1907
|
+
if (!pluginsProp) {
|
|
1908
|
+
const prop = parseModule4("export default { plugins: [jsxLoc()] };").$ast.body?.find((s) => s.type === "ExportDefaultDeclaration")?.declaration?.properties?.find((p) => {
|
|
1909
|
+
const k = p?.key;
|
|
1910
|
+
return k?.type === "Identifier" && k.name === "plugins" || isStringLiteral3(k) && k.value === "plugins";
|
|
1911
|
+
});
|
|
1912
|
+
if (!prop) return { changed: false };
|
|
1913
|
+
configObj.properties = [...configObj.properties ?? [], prop];
|
|
1914
|
+
return { changed: true };
|
|
1915
|
+
}
|
|
1916
|
+
const value = unwrapExpression(pluginsProp.value);
|
|
1917
|
+
if (!value) return { changed: false };
|
|
1918
|
+
if (value.type === "ArrayExpression") {
|
|
1919
|
+
if (pluginsHasJsxLoc(value)) return { changed: false };
|
|
1920
|
+
const jsxLocCall2 = parseModule4("const __x = jsxLoc();").$ast.body?.[0]?.declarations?.[0]?.init;
|
|
1921
|
+
if (!jsxLocCall2) return { changed: false };
|
|
1922
|
+
value.elements.push(jsxLocCall2);
|
|
1923
|
+
return { changed: true };
|
|
1924
|
+
}
|
|
1925
|
+
const jsxLocCall = parseModule4("const __x = jsxLoc();").$ast.body?.[0]?.declarations?.[0]?.init;
|
|
1926
|
+
if (!jsxLocCall) return { changed: false };
|
|
1927
|
+
const spread = { type: "SpreadElement", argument: value };
|
|
1928
|
+
pluginsProp.value = { type: "ArrayExpression", elements: [spread, jsxLocCall] };
|
|
1929
|
+
return { changed: true };
|
|
1930
|
+
}
|
|
1931
|
+
async function installViteJsxLocPlugin(opts) {
|
|
1932
|
+
const configPath = findViteConfigFile2(opts.projectPath);
|
|
1933
|
+
if (!configPath) return { configFile: null, modified: false };
|
|
1934
|
+
const configFilename = getViteConfigFilename(configPath);
|
|
1935
|
+
const original = readFileSync7(configPath, "utf-8");
|
|
1936
|
+
const isCjs = configPath.endsWith(".cjs");
|
|
1937
|
+
let mod;
|
|
1938
|
+
try {
|
|
1939
|
+
mod = parseModule4(original);
|
|
1940
|
+
} catch {
|
|
1941
|
+
return { configFile: configFilename, modified: false };
|
|
1942
|
+
}
|
|
1943
|
+
const found = findExportedConfigObjectExpression(mod);
|
|
1944
|
+
if (!found) return { configFile: configFilename, modified: false };
|
|
1945
|
+
let changed = false;
|
|
1946
|
+
if (isCjs) {
|
|
1947
|
+
const reqRes = ensureCjsJsxLocRequire(found.program);
|
|
1948
|
+
if (reqRes.changed) changed = true;
|
|
1949
|
+
} else {
|
|
1950
|
+
const impRes = ensureEsmJsxLocImport(found.program);
|
|
1951
|
+
if (impRes.changed) changed = true;
|
|
1952
|
+
}
|
|
1953
|
+
const pluginsRes = ensurePluginsContainsJsxLoc(found.objExpr);
|
|
1954
|
+
if (pluginsRes.changed) changed = true;
|
|
1955
|
+
const updated = changed ? generateCode4(mod).code : original;
|
|
1956
|
+
if (updated !== original) {
|
|
1957
|
+
writeFileSync4(configPath, updated, "utf-8");
|
|
1958
|
+
return { configFile: configFilename, modified: true };
|
|
1959
|
+
}
|
|
1960
|
+
return { configFile: configFilename, modified: false };
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// src/utils/next-routes.ts
|
|
1964
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1965
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
1966
|
+
import { join as join9 } from "path";
|
|
1967
|
+
var DEV_SOURCE_ROUTE_TS = `/**
|
|
1968
|
+
* Dev-only API route for fetching source files
|
|
1969
|
+
*
|
|
1970
|
+
* This route allows the UILint overlay to fetch and display source code
|
|
1971
|
+
* for components rendered on the page.
|
|
1972
|
+
*
|
|
1973
|
+
* Security:
|
|
1974
|
+
* - Only available in development mode
|
|
1975
|
+
* - Validates file path is within project root
|
|
1976
|
+
* - Only allows specific file extensions
|
|
1977
|
+
*/
|
|
1978
|
+
|
|
1979
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
1980
|
+
import { readFileSync, existsSync } from "fs";
|
|
1981
|
+
import { resolve, relative, dirname, extname, sep } from "path";
|
|
1982
|
+
import { fileURLToPath } from "url";
|
|
1983
|
+
|
|
1984
|
+
export const runtime = "nodejs";
|
|
1985
|
+
|
|
1986
|
+
// Allowed file extensions
|
|
1987
|
+
const ALLOWED_EXTENSIONS = new Set([".tsx", ".ts", ".jsx", ".js", ".css"]);
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Best-effort: resolve the Next.js project root (the dir that owns .next/) even in monorepos.
|
|
1991
|
+
*
|
|
1992
|
+
* Why: In monorepos, process.cwd() might be the workspace root (it also has package.json),
|
|
1993
|
+
* which would incorrectly store/read files under the wrong directory.
|
|
1994
|
+
*/
|
|
1995
|
+
function findNextProjectRoot(): string {
|
|
1996
|
+
// Prefer discovering via this route module's on-disk path.
|
|
1997
|
+
// In Next, route code is executed from within ".next/server/...".
|
|
1998
|
+
try {
|
|
1999
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
2000
|
+
const marker = sep + ".next" + sep;
|
|
2001
|
+
const idx = selfPath.lastIndexOf(marker);
|
|
2002
|
+
if (idx !== -1) {
|
|
2003
|
+
return selfPath.slice(0, idx);
|
|
2004
|
+
}
|
|
2005
|
+
} catch {
|
|
2006
|
+
// ignore
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// Fallback: walk up from cwd looking for .next/
|
|
2010
|
+
let dir = process.cwd();
|
|
2011
|
+
for (let i = 0; i < 20; i++) {
|
|
2012
|
+
if (existsSync(resolve(dir, ".next"))) return dir;
|
|
2013
|
+
const parent = dirname(dir);
|
|
2014
|
+
if (parent === dir) break;
|
|
2015
|
+
dir = parent;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// Final fallback: cwd
|
|
2019
|
+
return process.cwd();
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/**
|
|
2023
|
+
* Validate that a path is within the allowed directory
|
|
2024
|
+
*/
|
|
2025
|
+
function isPathWithinRoot(filePath: string, root: string): boolean {
|
|
2026
|
+
const resolved = resolve(filePath);
|
|
2027
|
+
const resolvedRoot = resolve(root);
|
|
2028
|
+
return resolved.startsWith(resolvedRoot + "/") || resolved === resolvedRoot;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* Find workspace root by walking up looking for pnpm-workspace.yaml or .git
|
|
2033
|
+
*/
|
|
2034
|
+
function findWorkspaceRoot(startDir: string): string {
|
|
2035
|
+
let dir = startDir;
|
|
2036
|
+
for (let i = 0; i < 10; i++) {
|
|
2037
|
+
if (
|
|
2038
|
+
existsSync(resolve(dir, "pnpm-workspace.yaml")) ||
|
|
2039
|
+
existsSync(resolve(dir, ".git"))
|
|
2040
|
+
) {
|
|
2041
|
+
return dir;
|
|
2042
|
+
}
|
|
2043
|
+
const parent = dirname(dir);
|
|
2044
|
+
if (parent === dir) break;
|
|
2045
|
+
dir = parent;
|
|
2046
|
+
}
|
|
2047
|
+
return startDir;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
export async function GET(request: NextRequest) {
|
|
2051
|
+
// Block in production
|
|
2052
|
+
if (process.env.NODE_ENV === "production") {
|
|
2053
|
+
return NextResponse.json(
|
|
2054
|
+
{ error: "Not available in production" },
|
|
2055
|
+
{ status: 404 }
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
const { searchParams } = new URL(request.url);
|
|
2060
|
+
const filePath = searchParams.get("path");
|
|
2061
|
+
|
|
2062
|
+
if (!filePath) {
|
|
2063
|
+
return NextResponse.json(
|
|
2064
|
+
{ error: "Missing 'path' query parameter" },
|
|
2065
|
+
{ status: 400 }
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// Validate extension
|
|
2070
|
+
const ext = extname(filePath).toLowerCase();
|
|
2071
|
+
if (!ALLOWED_EXTENSIONS.has(ext)) {
|
|
2072
|
+
return NextResponse.json(
|
|
2073
|
+
{ error: \`File extension '\${ext}' not allowed\` },
|
|
2074
|
+
{ status: 403 }
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// Find project root (prefer Next project root over workspace root)
|
|
2079
|
+
const projectRoot = findNextProjectRoot();
|
|
2080
|
+
|
|
2081
|
+
// Resolve the file path
|
|
2082
|
+
const resolvedPath = resolve(filePath);
|
|
2083
|
+
|
|
2084
|
+
// Security check: ensure path is within project root or workspace root
|
|
2085
|
+
const workspaceRoot = findWorkspaceRoot(projectRoot);
|
|
2086
|
+
const isWithinApp = isPathWithinRoot(resolvedPath, projectRoot);
|
|
2087
|
+
const isWithinWorkspace = isPathWithinRoot(resolvedPath, workspaceRoot);
|
|
2088
|
+
|
|
2089
|
+
if (!isWithinApp && !isWithinWorkspace) {
|
|
2090
|
+
return NextResponse.json(
|
|
2091
|
+
{ error: "Path outside project directory" },
|
|
2092
|
+
{ status: 403 }
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// Check file exists
|
|
2097
|
+
if (!existsSync(resolvedPath)) {
|
|
2098
|
+
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
try {
|
|
2102
|
+
const content = readFileSync(resolvedPath, "utf-8");
|
|
2103
|
+
const relativePath = relative(workspaceRoot, resolvedPath);
|
|
2104
|
+
|
|
2105
|
+
return NextResponse.json({
|
|
2106
|
+
content,
|
|
2107
|
+
relativePath,
|
|
2108
|
+
projectRoot,
|
|
2109
|
+
workspaceRoot,
|
|
2110
|
+
});
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
console.error("[Dev Source API] Error reading file:", error);
|
|
2113
|
+
return NextResponse.json({ error: "Failed to read file" }, { status: 500 });
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
`;
|
|
2117
|
+
var SCREENSHOT_ROUTE_TS = `/**
|
|
2118
|
+
* Dev-only API route for saving and retrieving vision analysis screenshots
|
|
2119
|
+
*
|
|
2120
|
+
* This route allows the UILint overlay to:
|
|
2121
|
+
* - POST: Save screenshots and element manifests for vision analysis
|
|
2122
|
+
* - GET: Retrieve screenshots or list available screenshots
|
|
2123
|
+
*
|
|
2124
|
+
* Security:
|
|
2125
|
+
* - Only available in development mode
|
|
2126
|
+
* - Saves to .uilint/screenshots/ directory within project
|
|
2127
|
+
*/
|
|
2128
|
+
|
|
2129
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2130
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
2131
|
+
import { resolve, join, dirname, basename, sep } from "path";
|
|
2132
|
+
import { fileURLToPath } from "url";
|
|
2133
|
+
|
|
2134
|
+
export const runtime = "nodejs";
|
|
2135
|
+
|
|
2136
|
+
// Maximum screenshot size (10MB)
|
|
2137
|
+
const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
|
|
2138
|
+
|
|
2139
|
+
/**
|
|
2140
|
+
* Best-effort: resolve the Next.js project root (the dir that owns .next/) even in monorepos.
|
|
2141
|
+
*/
|
|
2142
|
+
function findNextProjectRoot(): string {
|
|
2143
|
+
try {
|
|
2144
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
2145
|
+
const marker = sep + ".next" + sep;
|
|
2146
|
+
const idx = selfPath.lastIndexOf(marker);
|
|
2147
|
+
if (idx !== -1) {
|
|
2148
|
+
return selfPath.slice(0, idx);
|
|
2149
|
+
}
|
|
2150
|
+
} catch {
|
|
2151
|
+
// ignore
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
let dir = process.cwd();
|
|
2155
|
+
for (let i = 0; i < 20; i++) {
|
|
2156
|
+
if (existsSync(resolve(dir, ".next"))) return dir;
|
|
2157
|
+
const parent = dirname(dir);
|
|
2158
|
+
if (parent === dir) break;
|
|
2159
|
+
dir = parent;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
return process.cwd();
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
/**
|
|
2166
|
+
* Get the screenshots directory path, creating it if needed
|
|
2167
|
+
*/
|
|
2168
|
+
function getScreenshotsDir(projectRoot: string): string {
|
|
2169
|
+
const screenshotsDir = join(projectRoot, ".uilint", "screenshots");
|
|
2170
|
+
if (!existsSync(screenshotsDir)) {
|
|
2171
|
+
mkdirSync(screenshotsDir, { recursive: true });
|
|
2172
|
+
}
|
|
2173
|
+
return screenshotsDir;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
/**
|
|
2177
|
+
* Validate filename to prevent path traversal
|
|
2178
|
+
*/
|
|
2179
|
+
function isValidFilename(filename: string): boolean {
|
|
2180
|
+
// Only allow alphanumeric, hyphens, underscores, and dots
|
|
2181
|
+
// Must end with .png, .jpeg, .jpg, or .json
|
|
2182
|
+
const validPattern = /^[a-zA-Z0-9_-]+\\.(png|jpeg|jpg|json)$/;
|
|
2183
|
+
return validPattern.test(filename) && !filename.includes("..");
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
/**
|
|
2187
|
+
* POST: Save a screenshot and optionally its manifest
|
|
2188
|
+
*/
|
|
2189
|
+
export async function POST(request: NextRequest) {
|
|
2190
|
+
// Block in production
|
|
2191
|
+
if (process.env.NODE_ENV === "production") {
|
|
2192
|
+
return NextResponse.json(
|
|
2193
|
+
{ error: "Not available in production" },
|
|
2194
|
+
{ status: 404 }
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
try {
|
|
2199
|
+
const body = await request.json();
|
|
2200
|
+
const { filename, imageData, manifest, analysisResult } = body;
|
|
2201
|
+
|
|
2202
|
+
if (!filename) {
|
|
2203
|
+
return NextResponse.json({ error: "Missing 'filename'" }, { status: 400 });
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// Validate filename
|
|
2207
|
+
if (!isValidFilename(filename)) {
|
|
2208
|
+
return NextResponse.json(
|
|
2209
|
+
{ error: "Invalid filename format" },
|
|
2210
|
+
{ status: 400 }
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// Allow "sidecar-only" updates (manifest/analysisResult) without re-sending image bytes.
|
|
2215
|
+
const hasImageData = typeof imageData === "string" && imageData.length > 0;
|
|
2216
|
+
const hasSidecar =
|
|
2217
|
+
typeof manifest !== "undefined" || typeof analysisResult !== "undefined";
|
|
2218
|
+
|
|
2219
|
+
if (!hasImageData && !hasSidecar) {
|
|
2220
|
+
return NextResponse.json(
|
|
2221
|
+
{ error: "Nothing to save (provide imageData and/or manifest/analysisResult)" },
|
|
2222
|
+
{ status: 400 }
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// Check size (image only)
|
|
2227
|
+
if (hasImageData && imageData.length > MAX_SCREENSHOT_SIZE) {
|
|
2228
|
+
return NextResponse.json(
|
|
2229
|
+
{ error: "Screenshot too large (max 10MB)" },
|
|
2230
|
+
{ status: 413 }
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
const projectRoot = findNextProjectRoot();
|
|
2235
|
+
const screenshotsDir = getScreenshotsDir(projectRoot);
|
|
2236
|
+
|
|
2237
|
+
const imagePath = join(screenshotsDir, filename);
|
|
2238
|
+
|
|
2239
|
+
// Save the image (base64 data URL) if provided
|
|
2240
|
+
if (hasImageData) {
|
|
2241
|
+
const base64Data = imageData.includes(",")
|
|
2242
|
+
? imageData.split(",")[1]
|
|
2243
|
+
: imageData;
|
|
2244
|
+
writeFileSync(imagePath, Buffer.from(base64Data, "base64"));
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// Save manifest and analysis result as JSON sidecar
|
|
2248
|
+
if (hasSidecar) {
|
|
2249
|
+
const jsonFilename = filename.replace(/\\.(png|jpeg|jpg)$/, ".json");
|
|
2250
|
+
const jsonPath = join(screenshotsDir, jsonFilename);
|
|
2251
|
+
|
|
2252
|
+
// If a sidecar already exists, merge updates (lets us POST analysisResult later without re-sending image).
|
|
2253
|
+
let existing: any = null;
|
|
2254
|
+
if (existsSync(jsonPath)) {
|
|
2255
|
+
try {
|
|
2256
|
+
existing = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
2257
|
+
} catch {
|
|
2258
|
+
existing = null;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
const routeFromAnalysis =
|
|
2263
|
+
analysisResult && typeof analysisResult === "object"
|
|
2264
|
+
? (analysisResult as any).route
|
|
2265
|
+
: undefined;
|
|
2266
|
+
const issuesFromAnalysis =
|
|
2267
|
+
analysisResult && typeof analysisResult === "object"
|
|
2268
|
+
? (analysisResult as any).issues
|
|
2269
|
+
: undefined;
|
|
2270
|
+
|
|
2271
|
+
const jsonData = {
|
|
2272
|
+
...(existing && typeof existing === "object" ? existing : {}),
|
|
2273
|
+
timestamp: Date.now(),
|
|
2274
|
+
filename,
|
|
2275
|
+
screenshotFile: filename,
|
|
2276
|
+
route:
|
|
2277
|
+
typeof routeFromAnalysis === "string"
|
|
2278
|
+
? routeFromAnalysis
|
|
2279
|
+
: (existing as any)?.route ?? null,
|
|
2280
|
+
issues:
|
|
2281
|
+
Array.isArray(issuesFromAnalysis)
|
|
2282
|
+
? issuesFromAnalysis
|
|
2283
|
+
: (existing as any)?.issues ?? null,
|
|
2284
|
+
manifest: typeof manifest === "undefined" ? existing?.manifest ?? null : manifest,
|
|
2285
|
+
analysisResult:
|
|
2286
|
+
typeof analysisResult === "undefined"
|
|
2287
|
+
? existing?.analysisResult ?? null
|
|
2288
|
+
: analysisResult,
|
|
2289
|
+
};
|
|
2290
|
+
writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2));
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
return NextResponse.json({
|
|
2294
|
+
success: true,
|
|
2295
|
+
path: imagePath,
|
|
2296
|
+
projectRoot,
|
|
2297
|
+
screenshotsDir,
|
|
2298
|
+
});
|
|
2299
|
+
} catch (error) {
|
|
2300
|
+
console.error("[Screenshot API] Error saving screenshot:", error);
|
|
2301
|
+
return NextResponse.json(
|
|
2302
|
+
{ error: "Failed to save screenshot" },
|
|
2303
|
+
{ status: 500 }
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
/**
|
|
2309
|
+
* GET: Retrieve a screenshot or list available screenshots
|
|
2310
|
+
*/
|
|
2311
|
+
export async function GET(request: NextRequest) {
|
|
2312
|
+
// Block in production
|
|
2313
|
+
if (process.env.NODE_ENV === "production") {
|
|
2314
|
+
return NextResponse.json(
|
|
2315
|
+
{ error: "Not available in production" },
|
|
2316
|
+
{ status: 404 }
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
const { searchParams } = new URL(request.url);
|
|
2321
|
+
const filename = searchParams.get("filename");
|
|
2322
|
+
const list = searchParams.get("list");
|
|
2323
|
+
|
|
2324
|
+
const projectRoot = findNextProjectRoot();
|
|
2325
|
+
const screenshotsDir = getScreenshotsDir(projectRoot);
|
|
2326
|
+
|
|
2327
|
+
// List mode: return all screenshots
|
|
2328
|
+
if (list === "true") {
|
|
2329
|
+
try {
|
|
2330
|
+
const files = readdirSync(screenshotsDir);
|
|
2331
|
+
const screenshots = files
|
|
2332
|
+
.filter((f) => /\\.(png|jpeg|jpg)$/.test(f))
|
|
2333
|
+
.map((f) => {
|
|
2334
|
+
const jsonFile = f.replace(/\\.(png|jpeg|jpg)$/, ".json");
|
|
2335
|
+
const jsonPath = join(screenshotsDir, jsonFile);
|
|
2336
|
+
let metadata = null;
|
|
2337
|
+
if (existsSync(jsonPath)) {
|
|
2338
|
+
try {
|
|
2339
|
+
metadata = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
2340
|
+
} catch {
|
|
2341
|
+
// Ignore parse errors
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return {
|
|
2345
|
+
filename: f,
|
|
2346
|
+
metadata,
|
|
2347
|
+
};
|
|
2348
|
+
})
|
|
2349
|
+
.sort((a, b) => {
|
|
2350
|
+
// Sort by timestamp descending (newest first)
|
|
2351
|
+
const aTime = a.metadata?.timestamp || 0;
|
|
2352
|
+
const bTime = b.metadata?.timestamp || 0;
|
|
2353
|
+
return bTime - aTime;
|
|
2354
|
+
});
|
|
2355
|
+
|
|
2356
|
+
return NextResponse.json({ screenshots, projectRoot, screenshotsDir });
|
|
2357
|
+
} catch (error) {
|
|
2358
|
+
console.error("[Screenshot API] Error listing screenshots:", error);
|
|
2359
|
+
return NextResponse.json(
|
|
2360
|
+
{ error: "Failed to list screenshots" },
|
|
2361
|
+
{ status: 500 }
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// Retrieve mode: get specific screenshot
|
|
2367
|
+
if (!filename) {
|
|
2368
|
+
return NextResponse.json(
|
|
2369
|
+
{ error: "Missing 'filename' parameter" },
|
|
2370
|
+
{ status: 400 }
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
if (!isValidFilename(filename)) {
|
|
2375
|
+
return NextResponse.json(
|
|
2376
|
+
{ error: "Invalid filename format" },
|
|
2377
|
+
{ status: 400 }
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const filePath = join(screenshotsDir, filename);
|
|
2382
|
+
|
|
2383
|
+
if (!existsSync(filePath)) {
|
|
2384
|
+
return NextResponse.json(
|
|
2385
|
+
{ error: "Screenshot not found" },
|
|
2386
|
+
{ status: 404 }
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
try {
|
|
2391
|
+
const content = readFileSync(filePath);
|
|
2392
|
+
|
|
2393
|
+
// Determine content type
|
|
2394
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
2395
|
+
const contentType =
|
|
2396
|
+
ext === "json"
|
|
2397
|
+
? "application/json"
|
|
2398
|
+
: ext === "png"
|
|
2399
|
+
? "image/png"
|
|
2400
|
+
: "image/jpeg";
|
|
2401
|
+
|
|
2402
|
+
if (ext === "json") {
|
|
2403
|
+
return NextResponse.json(JSON.parse(content.toString()));
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
return new NextResponse(content, {
|
|
2407
|
+
headers: {
|
|
2408
|
+
"Content-Type": contentType,
|
|
2409
|
+
"Cache-Control": "no-cache",
|
|
2410
|
+
},
|
|
2411
|
+
});
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
console.error("[Screenshot API] Error reading screenshot:", error);
|
|
2414
|
+
return NextResponse.json(
|
|
2415
|
+
{ error: "Failed to read screenshot" },
|
|
2416
|
+
{ status: 500 }
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
`;
|
|
2421
|
+
async function writeRouteFile(absPath, relPath, content, opts) {
|
|
2422
|
+
if (existsSync9(absPath) && !opts.force) return;
|
|
2423
|
+
await writeFile(absPath, content, "utf-8");
|
|
2424
|
+
}
|
|
2425
|
+
async function installNextUILintRoutes(opts) {
|
|
2426
|
+
const baseRel = join9(opts.appRoot, "api", ".uilint");
|
|
2427
|
+
const baseAbs = join9(opts.projectPath, baseRel);
|
|
2428
|
+
await mkdir(join9(baseAbs, "source"), { recursive: true });
|
|
2429
|
+
await writeRouteFile(
|
|
2430
|
+
join9(baseAbs, "source", "route.ts"),
|
|
2431
|
+
join9(baseRel, "source", "route.ts"),
|
|
2432
|
+
DEV_SOURCE_ROUTE_TS,
|
|
2433
|
+
opts
|
|
2434
|
+
);
|
|
2435
|
+
await mkdir(join9(baseAbs, "screenshots"), { recursive: true });
|
|
2436
|
+
await writeRouteFile(
|
|
2437
|
+
join9(baseAbs, "screenshots", "route.ts"),
|
|
2438
|
+
join9(baseRel, "screenshots", "route.ts"),
|
|
2439
|
+
SCREENSHOT_ROUTE_TS,
|
|
2440
|
+
opts
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// src/commands/install/execute.ts
|
|
2445
|
+
async function executeAction(action, options) {
|
|
2446
|
+
const { dryRun = false } = options;
|
|
2447
|
+
try {
|
|
2448
|
+
switch (action.type) {
|
|
2449
|
+
case "create_directory": {
|
|
2450
|
+
if (dryRun) {
|
|
2451
|
+
return {
|
|
2452
|
+
action,
|
|
2453
|
+
success: true,
|
|
2454
|
+
wouldDo: `Create directory: ${action.path}`
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
if (!existsSync10(action.path)) {
|
|
2458
|
+
mkdirSync(action.path, { recursive: true });
|
|
2459
|
+
}
|
|
2460
|
+
return { action, success: true };
|
|
2461
|
+
}
|
|
2462
|
+
case "create_file": {
|
|
2463
|
+
if (dryRun) {
|
|
2464
|
+
return {
|
|
2465
|
+
action,
|
|
2466
|
+
success: true,
|
|
2467
|
+
wouldDo: `Create file: ${action.path}${action.permissions ? ` (mode: ${action.permissions.toString(8)})` : ""}`
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
const dir = dirname3(action.path);
|
|
2471
|
+
if (!existsSync10(dir)) {
|
|
2472
|
+
mkdirSync(dir, { recursive: true });
|
|
2473
|
+
}
|
|
2474
|
+
writeFileSync5(action.path, action.content, "utf-8");
|
|
2475
|
+
if (action.permissions) {
|
|
2476
|
+
chmodSync(action.path, action.permissions);
|
|
2477
|
+
}
|
|
2478
|
+
return { action, success: true };
|
|
2479
|
+
}
|
|
2480
|
+
case "merge_json": {
|
|
2481
|
+
if (dryRun) {
|
|
2482
|
+
return {
|
|
2483
|
+
action,
|
|
2484
|
+
success: true,
|
|
2485
|
+
wouldDo: `Merge JSON into: ${action.path}`
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
let existing = {};
|
|
2489
|
+
if (existsSync10(action.path)) {
|
|
2490
|
+
try {
|
|
2491
|
+
existing = JSON.parse(readFileSync8(action.path, "utf-8"));
|
|
2492
|
+
} catch {
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
const merged = deepMerge(existing, action.merge);
|
|
2496
|
+
const dir = dirname3(action.path);
|
|
2497
|
+
if (!existsSync10(dir)) {
|
|
2498
|
+
mkdirSync(dir, { recursive: true });
|
|
2499
|
+
}
|
|
2500
|
+
writeFileSync5(action.path, JSON.stringify(merged, null, 2), "utf-8");
|
|
2501
|
+
return { action, success: true };
|
|
2502
|
+
}
|
|
2503
|
+
case "delete_file": {
|
|
2504
|
+
if (dryRun) {
|
|
2505
|
+
return {
|
|
2506
|
+
action,
|
|
2507
|
+
success: true,
|
|
2508
|
+
wouldDo: `Delete file: ${action.path}`
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
if (existsSync10(action.path)) {
|
|
2512
|
+
unlinkSync(action.path);
|
|
2513
|
+
}
|
|
2514
|
+
return { action, success: true };
|
|
2515
|
+
}
|
|
2516
|
+
case "append_to_file": {
|
|
2517
|
+
if (dryRun) {
|
|
2518
|
+
return {
|
|
2519
|
+
action,
|
|
2520
|
+
success: true,
|
|
2521
|
+
wouldDo: `Append to file: ${action.path}`
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
if (existsSync10(action.path)) {
|
|
2525
|
+
const content = readFileSync8(action.path, "utf-8");
|
|
2526
|
+
if (action.ifNotContains && content.includes(action.ifNotContains)) {
|
|
2527
|
+
return { action, success: true };
|
|
2528
|
+
}
|
|
2529
|
+
writeFileSync5(action.path, content + action.content, "utf-8");
|
|
2530
|
+
}
|
|
2531
|
+
return { action, success: true };
|
|
2532
|
+
}
|
|
2533
|
+
case "inject_eslint": {
|
|
2534
|
+
return await executeInjectEslint(action, options);
|
|
2535
|
+
}
|
|
2536
|
+
case "inject_react": {
|
|
2537
|
+
return await executeInjectReact(action, options);
|
|
2538
|
+
}
|
|
2539
|
+
case "inject_next_config": {
|
|
2540
|
+
return await executeInjectNextConfig(action, options);
|
|
2541
|
+
}
|
|
2542
|
+
case "inject_vite_config": {
|
|
2543
|
+
return await executeInjectViteConfig(action, options);
|
|
2544
|
+
}
|
|
2545
|
+
case "install_next_routes": {
|
|
2546
|
+
return await executeInstallNextRoutes(action, options);
|
|
2547
|
+
}
|
|
2548
|
+
default: {
|
|
2549
|
+
const _exhaustive = action;
|
|
2550
|
+
return {
|
|
2551
|
+
action: _exhaustive,
|
|
2552
|
+
success: false,
|
|
2553
|
+
error: `Unknown action type`
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
return {
|
|
2559
|
+
action,
|
|
2560
|
+
success: false,
|
|
2561
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
async function executeInjectEslint(action, options) {
|
|
2566
|
+
const { dryRun = false } = options;
|
|
2567
|
+
if (dryRun) {
|
|
2568
|
+
return {
|
|
2569
|
+
action,
|
|
2570
|
+
success: true,
|
|
2571
|
+
wouldDo: `Inject ESLint rules into: ${action.configPath}`
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
const result = await installEslintPlugin({
|
|
2575
|
+
projectPath: action.packagePath,
|
|
2576
|
+
selectedRules: action.rules,
|
|
2577
|
+
force: !action.hasExistingRules,
|
|
2578
|
+
// Don't force if already has rules
|
|
2579
|
+
// Auto-confirm for execute phase (choices were made during planning)
|
|
2580
|
+
confirmAddMissingRules: async () => true
|
|
2581
|
+
});
|
|
2582
|
+
return {
|
|
2583
|
+
action,
|
|
2584
|
+
success: result.configFile !== null && result.configured,
|
|
2585
|
+
error: result.configFile === null ? "No ESLint config found" : result.configured ? void 0 : result.error ?? "Failed to configure uilint in ESLint config"
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
async function executeInjectReact(action, options) {
|
|
2589
|
+
const { dryRun = false } = options;
|
|
2590
|
+
if (dryRun) {
|
|
2591
|
+
return {
|
|
2592
|
+
action,
|
|
2593
|
+
success: true,
|
|
2594
|
+
wouldDo: `Inject <uilint-devtools /> into React app: ${action.projectPath}`
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
const result = await installReactUILintOverlay({
|
|
2598
|
+
projectPath: action.projectPath,
|
|
2599
|
+
appRoot: action.appRoot,
|
|
2600
|
+
mode: action.mode,
|
|
2601
|
+
force: false,
|
|
2602
|
+
// Auto-select first choice for execute phase
|
|
2603
|
+
confirmFileChoice: async (choices) => choices[0]
|
|
2604
|
+
});
|
|
2605
|
+
const success = result.modified || result.alreadyConfigured === true;
|
|
2606
|
+
return {
|
|
2607
|
+
action,
|
|
2608
|
+
success,
|
|
2609
|
+
error: success ? void 0 : "Failed to configure React overlay"
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
async function executeInjectViteConfig(action, options) {
|
|
2613
|
+
const { dryRun = false } = options;
|
|
2614
|
+
if (dryRun) {
|
|
2615
|
+
return {
|
|
2616
|
+
action,
|
|
2617
|
+
success: true,
|
|
2618
|
+
wouldDo: `Inject jsx-loc-plugin into vite.config: ${action.projectPath}`
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
const result = await installViteJsxLocPlugin({
|
|
2622
|
+
projectPath: action.projectPath,
|
|
2623
|
+
force: false
|
|
2624
|
+
});
|
|
2625
|
+
return {
|
|
2626
|
+
action,
|
|
2627
|
+
success: result.modified || result.configFile !== null,
|
|
2628
|
+
error: result.configFile === null ? "No vite.config found" : void 0
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
async function executeInjectNextConfig(action, options) {
|
|
2632
|
+
const { dryRun = false } = options;
|
|
2633
|
+
if (dryRun) {
|
|
2634
|
+
return {
|
|
2635
|
+
action,
|
|
2636
|
+
success: true,
|
|
2637
|
+
wouldDo: `Inject jsx-loc-plugin into next.config: ${action.projectPath}`
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
const result = await installJsxLocPlugin({
|
|
2641
|
+
projectPath: action.projectPath,
|
|
2642
|
+
force: false
|
|
2643
|
+
});
|
|
2644
|
+
return {
|
|
2645
|
+
action,
|
|
2646
|
+
success: result.modified || result.configFile !== null,
|
|
2647
|
+
error: result.configFile === null ? "No next.config found" : void 0
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
async function executeInstallNextRoutes(action, options) {
|
|
2651
|
+
const { dryRun = false } = options;
|
|
2652
|
+
if (dryRun) {
|
|
2653
|
+
return {
|
|
2654
|
+
action,
|
|
2655
|
+
success: true,
|
|
2656
|
+
wouldDo: `Install Next.js API routes: ${action.projectPath}`
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
await installNextUILintRoutes({
|
|
2660
|
+
projectPath: action.projectPath,
|
|
2661
|
+
appRoot: action.appRoot,
|
|
2662
|
+
force: false
|
|
2663
|
+
});
|
|
2664
|
+
return { action, success: true };
|
|
2665
|
+
}
|
|
2666
|
+
function deepMerge(target, source) {
|
|
2667
|
+
const result = { ...target };
|
|
2668
|
+
for (const key of Object.keys(source)) {
|
|
2669
|
+
const sourceVal = source[key];
|
|
2670
|
+
const targetVal = target[key];
|
|
2671
|
+
if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) {
|
|
2672
|
+
result[key] = deepMerge(
|
|
2673
|
+
targetVal,
|
|
2674
|
+
sourceVal
|
|
2675
|
+
);
|
|
2676
|
+
} else {
|
|
2677
|
+
result[key] = sourceVal;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return result;
|
|
2681
|
+
}
|
|
2682
|
+
function buildSummary(actionsPerformed, dependencyResults, items) {
|
|
2683
|
+
const filesCreated = [];
|
|
2684
|
+
const filesModified = [];
|
|
2685
|
+
const filesDeleted = [];
|
|
2686
|
+
const eslintTargets = [];
|
|
2687
|
+
let nextApp;
|
|
2688
|
+
let viteApp;
|
|
2689
|
+
for (const result of actionsPerformed) {
|
|
2690
|
+
if (!result.success) continue;
|
|
2691
|
+
const { action } = result;
|
|
2692
|
+
switch (action.type) {
|
|
2693
|
+
case "create_file":
|
|
2694
|
+
filesCreated.push(action.path);
|
|
2695
|
+
break;
|
|
2696
|
+
case "merge_json":
|
|
2697
|
+
case "append_to_file":
|
|
2698
|
+
filesModified.push(action.path);
|
|
2699
|
+
break;
|
|
2700
|
+
case "delete_file":
|
|
2701
|
+
filesDeleted.push(action.path);
|
|
2702
|
+
break;
|
|
2703
|
+
case "inject_eslint":
|
|
2704
|
+
filesModified.push(action.configPath);
|
|
2705
|
+
eslintTargets.push({
|
|
2706
|
+
displayName: action.packagePath,
|
|
2707
|
+
configFile: action.configPath
|
|
2708
|
+
});
|
|
2709
|
+
break;
|
|
2710
|
+
case "inject_react":
|
|
2711
|
+
if (action.mode === "vite") {
|
|
2712
|
+
viteApp = { entryRoot: action.appRoot };
|
|
2713
|
+
} else {
|
|
2714
|
+
nextApp = { appRoot: action.appRoot };
|
|
2715
|
+
}
|
|
2716
|
+
break;
|
|
2717
|
+
case "install_next_routes":
|
|
2718
|
+
nextApp = { appRoot: action.appRoot };
|
|
2719
|
+
break;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
const dependenciesInstalled = [];
|
|
2723
|
+
for (const result of dependencyResults) {
|
|
2724
|
+
if (result.success && !result.skipped) {
|
|
2725
|
+
dependenciesInstalled.push({
|
|
2726
|
+
packagePath: result.install.packagePath,
|
|
2727
|
+
packages: result.install.packages
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
return {
|
|
2732
|
+
installedItems: items,
|
|
2733
|
+
filesCreated,
|
|
2734
|
+
filesModified,
|
|
2735
|
+
filesDeleted,
|
|
2736
|
+
dependenciesInstalled,
|
|
2737
|
+
eslintTargets,
|
|
2738
|
+
nextApp,
|
|
2739
|
+
viteApp
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
async function execute(plan, options = {}) {
|
|
2743
|
+
const { dryRun = false, installDependencies: installDependencies2 = installDependencies } = options;
|
|
2744
|
+
const actionsPerformed = [];
|
|
2745
|
+
const dependencyResults = [];
|
|
2746
|
+
for (const action of plan.actions) {
|
|
2747
|
+
const result = await executeAction(action, options);
|
|
2748
|
+
actionsPerformed.push(result);
|
|
2749
|
+
}
|
|
2750
|
+
for (const dep of plan.dependencies) {
|
|
2751
|
+
if (dryRun) {
|
|
2752
|
+
dependencyResults.push({
|
|
2753
|
+
install: dep,
|
|
2754
|
+
success: true,
|
|
2755
|
+
skipped: true
|
|
2756
|
+
});
|
|
2757
|
+
continue;
|
|
2758
|
+
}
|
|
2759
|
+
try {
|
|
2760
|
+
await installDependencies2(
|
|
2761
|
+
dep.packageManager,
|
|
2762
|
+
dep.packagePath,
|
|
2763
|
+
dep.packages
|
|
2764
|
+
);
|
|
2765
|
+
dependencyResults.push({
|
|
2766
|
+
install: dep,
|
|
2767
|
+
success: true
|
|
2768
|
+
});
|
|
2769
|
+
} catch (error) {
|
|
2770
|
+
dependencyResults.push({
|
|
2771
|
+
install: dep,
|
|
2772
|
+
success: false,
|
|
2773
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
const actionsFailed = actionsPerformed.filter((r) => !r.success);
|
|
2778
|
+
const depsFailed = dependencyResults.filter((r) => !r.success);
|
|
2779
|
+
const success = actionsFailed.length === 0 && depsFailed.length === 0;
|
|
2780
|
+
const items = [];
|
|
2781
|
+
for (const result of actionsPerformed) {
|
|
2782
|
+
if (!result.success) continue;
|
|
2783
|
+
const { action } = result;
|
|
2784
|
+
if (action.type === "create_file") {
|
|
2785
|
+
if (action.path.includes("genstyleguide.md")) items.push("genstyleguide");
|
|
2786
|
+
if (action.path.includes("/skills/") && action.path.includes("SKILL.md")) items.push("skill");
|
|
2787
|
+
}
|
|
2788
|
+
if (action.type === "inject_eslint") items.push("eslint");
|
|
2789
|
+
if (action.type === "install_next_routes") items.push("next");
|
|
2790
|
+
if (action.type === "inject_react") {
|
|
2791
|
+
items.push(action.mode === "vite" ? "vite" : "next");
|
|
2792
|
+
}
|
|
2793
|
+
if (action.type === "inject_vite_config") items.push("vite");
|
|
2794
|
+
}
|
|
2795
|
+
const uniqueItems = [...new Set(items)];
|
|
2796
|
+
const summary = buildSummary(
|
|
2797
|
+
actionsPerformed,
|
|
2798
|
+
dependencyResults,
|
|
2799
|
+
uniqueItems
|
|
2800
|
+
);
|
|
2801
|
+
return {
|
|
2802
|
+
success,
|
|
2803
|
+
actionsPerformed,
|
|
2804
|
+
dependencyResults,
|
|
2805
|
+
summary
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
// src/commands/install-ui.tsx
|
|
2810
|
+
import { ruleRegistry as ruleRegistry2 } from "uilint-eslint";
|
|
2811
|
+
|
|
2812
|
+
// src/commands/install/installers/genstyleguide.ts
|
|
2813
|
+
import { join as join10 } from "path";
|
|
2814
|
+
var genstyleguideInstaller = {
|
|
2815
|
+
id: "genstyleguide",
|
|
2816
|
+
name: "/genstyleguide command",
|
|
2817
|
+
description: "Cursor command to generate UI style guides",
|
|
2818
|
+
icon: "\u{1F4DD}",
|
|
2819
|
+
isApplicable(project) {
|
|
2820
|
+
return true;
|
|
2821
|
+
},
|
|
2822
|
+
getTargets(project) {
|
|
2823
|
+
const commandPath = join10(project.cursorDir.path, "commands", "genstyleguide.md");
|
|
2824
|
+
const isInstalled = project.commands.genstyleguide;
|
|
2825
|
+
return [
|
|
2826
|
+
{
|
|
2827
|
+
id: "genstyleguide-command",
|
|
2828
|
+
label: ".cursor/commands/genstyleguide.md",
|
|
2829
|
+
path: commandPath,
|
|
2830
|
+
isInstalled
|
|
2831
|
+
}
|
|
2832
|
+
];
|
|
2833
|
+
},
|
|
2834
|
+
plan(targets, config, project) {
|
|
2835
|
+
const actions = [];
|
|
2836
|
+
const commandsDir = join10(project.cursorDir.path, "commands");
|
|
2837
|
+
if (!project.cursorDir.exists) {
|
|
2838
|
+
actions.push({
|
|
2839
|
+
type: "create_directory",
|
|
2840
|
+
path: project.cursorDir.path
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
actions.push({
|
|
2844
|
+
type: "create_directory",
|
|
2845
|
+
path: commandsDir
|
|
2846
|
+
});
|
|
2847
|
+
actions.push({
|
|
2848
|
+
type: "create_file",
|
|
2849
|
+
path: join10(commandsDir, "genstyleguide.md"),
|
|
2850
|
+
content: GENSTYLEGUIDE_COMMAND_MD
|
|
2851
|
+
});
|
|
2852
|
+
return {
|
|
2853
|
+
actions,
|
|
2854
|
+
dependencies: []
|
|
2855
|
+
};
|
|
2856
|
+
},
|
|
2857
|
+
async *execute(targets, config, project) {
|
|
2858
|
+
yield {
|
|
2859
|
+
type: "start",
|
|
2860
|
+
message: "Installing /genstyleguide command"
|
|
2861
|
+
};
|
|
2862
|
+
yield {
|
|
2863
|
+
type: "progress",
|
|
2864
|
+
message: "Creating .cursor/commands directory"
|
|
2865
|
+
};
|
|
2866
|
+
yield {
|
|
2867
|
+
type: "progress",
|
|
2868
|
+
message: "Writing command file",
|
|
2869
|
+
detail: "\u2192 .cursor/commands/genstyleguide.md"
|
|
2870
|
+
};
|
|
2871
|
+
yield {
|
|
2872
|
+
type: "complete",
|
|
2873
|
+
message: "Installed /genstyleguide command"
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
|
|
2878
|
+
// src/commands/install/installers/skill.ts
|
|
2879
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2880
|
+
import { join as join11 } from "path";
|
|
2881
|
+
var skillInstaller = {
|
|
2882
|
+
id: "skill",
|
|
2883
|
+
name: "UI Consistency Agent skill",
|
|
2884
|
+
description: "Claude Code skill for enforcing UI consistency",
|
|
2885
|
+
icon: "\u26A1",
|
|
2886
|
+
isApplicable(project) {
|
|
2887
|
+
return true;
|
|
2888
|
+
},
|
|
2889
|
+
getTargets(project) {
|
|
2890
|
+
const skillsDir = join11(project.cursorDir.path, "skills", "ui-consistency-enforcer");
|
|
2891
|
+
const skillMdPath = join11(skillsDir, "SKILL.md");
|
|
2892
|
+
const isInstalled = existsSync11(skillMdPath);
|
|
2893
|
+
return [
|
|
2894
|
+
{
|
|
2895
|
+
id: "ui-consistency-skill",
|
|
2896
|
+
label: ".cursor/skills/ui-consistency-enforcer",
|
|
2897
|
+
path: skillsDir,
|
|
2898
|
+
isInstalled,
|
|
2899
|
+
hint: "Agent skill for UI consistency checks"
|
|
2900
|
+
}
|
|
2901
|
+
];
|
|
2902
|
+
},
|
|
2903
|
+
plan(targets, config, project) {
|
|
2904
|
+
const actions = [];
|
|
2905
|
+
if (!project.cursorDir.exists) {
|
|
2906
|
+
actions.push({
|
|
2907
|
+
type: "create_directory",
|
|
2908
|
+
path: project.cursorDir.path
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
const skillsDir = join11(project.cursorDir.path, "skills");
|
|
2912
|
+
actions.push({
|
|
2913
|
+
type: "create_directory",
|
|
2914
|
+
path: skillsDir
|
|
2915
|
+
});
|
|
2916
|
+
try {
|
|
2917
|
+
const skill = loadSkill("ui-consistency-enforcer");
|
|
2918
|
+
const skillDir = join11(skillsDir, skill.name);
|
|
2919
|
+
actions.push({
|
|
2920
|
+
type: "create_directory",
|
|
2921
|
+
path: skillDir
|
|
2922
|
+
});
|
|
2923
|
+
for (const file of skill.files) {
|
|
2924
|
+
const filePath = join11(skillDir, file.relativePath);
|
|
2925
|
+
const fileDir = join11(
|
|
2926
|
+
skillDir,
|
|
2927
|
+
file.relativePath.split("/").slice(0, -1).join("/")
|
|
2928
|
+
);
|
|
2929
|
+
if (fileDir !== skillDir && file.relativePath.includes("/")) {
|
|
2930
|
+
actions.push({
|
|
2931
|
+
type: "create_directory",
|
|
2932
|
+
path: fileDir
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
actions.push({
|
|
2936
|
+
type: "create_file",
|
|
2937
|
+
path: filePath,
|
|
2938
|
+
content: file.content
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
} catch (error) {
|
|
2942
|
+
console.warn("Failed to load ui-consistency-enforcer skill:", error);
|
|
2943
|
+
}
|
|
2944
|
+
return {
|
|
2945
|
+
actions,
|
|
2946
|
+
dependencies: []
|
|
2947
|
+
};
|
|
2948
|
+
},
|
|
2949
|
+
async *execute(targets, config, project) {
|
|
2950
|
+
yield {
|
|
2951
|
+
type: "start",
|
|
2952
|
+
message: "Installing UI Consistency Agent skill"
|
|
2953
|
+
};
|
|
2954
|
+
yield {
|
|
2955
|
+
type: "progress",
|
|
2956
|
+
message: "Creating .cursor/skills directory"
|
|
2957
|
+
};
|
|
2958
|
+
try {
|
|
2959
|
+
const skill = loadSkill("ui-consistency-enforcer");
|
|
2960
|
+
for (const file of skill.files) {
|
|
2961
|
+
yield {
|
|
2962
|
+
type: "progress",
|
|
2963
|
+
message: "Writing skill file",
|
|
2964
|
+
detail: `\u2192 ${file.relativePath}`
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
yield {
|
|
2968
|
+
type: "complete",
|
|
2969
|
+
message: "Installed UI Consistency Agent skill"
|
|
2970
|
+
};
|
|
2971
|
+
} catch (error) {
|
|
2972
|
+
yield {
|
|
2973
|
+
type: "error",
|
|
2974
|
+
message: "Failed to install skill",
|
|
2975
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
};
|
|
2980
|
+
|
|
2981
|
+
// src/commands/install/installers/eslint.ts
|
|
2982
|
+
import { join as join12 } from "path";
|
|
2983
|
+
import { ruleRegistry } from "uilint-eslint";
|
|
2984
|
+
import { createRequire } from "module";
|
|
2985
|
+
var require2 = createRequire(import.meta.url);
|
|
2986
|
+
function toInstallSpecifier(pkgName) {
|
|
2987
|
+
return pkgName;
|
|
2988
|
+
}
|
|
2989
|
+
var eslintInstaller = {
|
|
2990
|
+
id: "eslint",
|
|
2991
|
+
name: "ESLint plugin",
|
|
2992
|
+
description: "Lint UI consistency with ESLint rules",
|
|
2993
|
+
icon: "\u{1F50D}",
|
|
2994
|
+
isApplicable(project) {
|
|
2995
|
+
return project.packages.some((pkg) => pkg.eslintConfigPath !== null);
|
|
2996
|
+
},
|
|
2997
|
+
getTargets(project) {
|
|
2998
|
+
return project.packages.filter((pkg) => pkg.eslintConfigPath !== null).map((pkg) => ({
|
|
2999
|
+
id: `eslint-${pkg.name}`,
|
|
3000
|
+
label: pkg.name,
|
|
3001
|
+
path: pkg.path,
|
|
3002
|
+
hint: pkg.eslintConfigFilename || "ESLint config detected",
|
|
3003
|
+
isInstalled: pkg.hasUilintRules
|
|
3004
|
+
}));
|
|
3005
|
+
},
|
|
3006
|
+
async configure(targets, project) {
|
|
3007
|
+
const selectedRules = ruleRegistry;
|
|
3008
|
+
return {
|
|
3009
|
+
selectedRules
|
|
3010
|
+
};
|
|
3011
|
+
},
|
|
3012
|
+
plan(targets, config, project) {
|
|
3013
|
+
const actions = [];
|
|
3014
|
+
const dependencies = [];
|
|
3015
|
+
const eslintConfig = config;
|
|
3016
|
+
const { selectedRules } = eslintConfig;
|
|
3017
|
+
for (const target of targets) {
|
|
3018
|
+
const pkgInfo = project.packages.find((p) => p.path === target.path);
|
|
3019
|
+
if (!pkgInfo || !pkgInfo.eslintConfigPath) continue;
|
|
3020
|
+
const rulesDir = join12(target.path, ".uilint", "rules");
|
|
3021
|
+
actions.push({
|
|
3022
|
+
type: "create_directory",
|
|
3023
|
+
path: rulesDir
|
|
3024
|
+
});
|
|
3025
|
+
dependencies.push({
|
|
3026
|
+
packagePath: target.path,
|
|
3027
|
+
packageManager: project.packageManager,
|
|
3028
|
+
packages: [toInstallSpecifier("uilint-eslint"), "typescript-eslint"]
|
|
3029
|
+
});
|
|
3030
|
+
actions.push({
|
|
3031
|
+
type: "inject_eslint",
|
|
3032
|
+
packagePath: target.path,
|
|
3033
|
+
configPath: pkgInfo.eslintConfigPath,
|
|
3034
|
+
rules: selectedRules,
|
|
3035
|
+
hasExistingRules: pkgInfo.hasUilintRules
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
const gitignorePath = join12(project.workspaceRoot, ".gitignore");
|
|
3039
|
+
actions.push({
|
|
3040
|
+
type: "append_to_file",
|
|
3041
|
+
path: gitignorePath,
|
|
3042
|
+
content: "\n# UILint cache\n.uilint/.cache\n",
|
|
3043
|
+
ifNotContains: ".uilint/.cache"
|
|
3044
|
+
});
|
|
3045
|
+
return { actions, dependencies };
|
|
3046
|
+
},
|
|
3047
|
+
async *execute(targets, config, project) {
|
|
3048
|
+
const eslintConfig = config;
|
|
3049
|
+
yield {
|
|
3050
|
+
type: "start",
|
|
3051
|
+
message: "Installing ESLint plugin"
|
|
3052
|
+
};
|
|
3053
|
+
for (const target of targets) {
|
|
3054
|
+
yield {
|
|
3055
|
+
type: "progress",
|
|
3056
|
+
message: `Configuring ESLint in ${target.label}`,
|
|
3057
|
+
detail: "\u2192 Adding uilint-eslint to dependencies"
|
|
3058
|
+
};
|
|
3059
|
+
yield {
|
|
3060
|
+
type: "progress",
|
|
3061
|
+
message: `Injecting ${eslintConfig.selectedRules.length} rules`,
|
|
3062
|
+
detail: `\u2192 ${target.hint}`
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
yield {
|
|
3066
|
+
type: "complete",
|
|
3067
|
+
message: `ESLint plugin installed in ${targets.length} package(s)`
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
|
|
3072
|
+
// src/commands/install/installers/next-overlay.ts
|
|
3073
|
+
var nextOverlayInstaller = {
|
|
3074
|
+
id: "next",
|
|
3075
|
+
name: "Next.js overlay",
|
|
3076
|
+
description: "Alt+Click UI inspector for Next.js App Router",
|
|
3077
|
+
icon: "\u{1F537}",
|
|
3078
|
+
isApplicable(project) {
|
|
3079
|
+
return project.nextApps.length > 0;
|
|
3080
|
+
},
|
|
3081
|
+
getTargets(project) {
|
|
3082
|
+
return project.nextApps.map((app) => ({
|
|
3083
|
+
id: `next-${app.projectPath}`,
|
|
3084
|
+
label: app.projectPath.split("/").pop() || app.projectPath,
|
|
3085
|
+
path: app.projectPath,
|
|
3086
|
+
hint: "App Router",
|
|
3087
|
+
isInstalled: false
|
|
3088
|
+
// TODO: Detect if already installed
|
|
3089
|
+
}));
|
|
3090
|
+
},
|
|
3091
|
+
plan(targets, config, project) {
|
|
3092
|
+
const actions = [];
|
|
3093
|
+
const dependencies = [];
|
|
3094
|
+
if (targets.length === 0) return { actions, dependencies };
|
|
3095
|
+
const target = targets[0];
|
|
3096
|
+
const appInfo = project.nextApps.find((app) => app.projectPath === target.path);
|
|
3097
|
+
if (!appInfo) return { actions, dependencies };
|
|
3098
|
+
const { projectPath, detection } = appInfo;
|
|
3099
|
+
actions.push({
|
|
3100
|
+
type: "install_next_routes",
|
|
3101
|
+
projectPath,
|
|
3102
|
+
appRoot: detection.appRoot
|
|
3103
|
+
});
|
|
3104
|
+
dependencies.push({
|
|
3105
|
+
packagePath: projectPath,
|
|
3106
|
+
packageManager: project.packageManager,
|
|
3107
|
+
packages: ["uilint-react", "uilint-core", "jsx-loc-plugin"]
|
|
3108
|
+
});
|
|
3109
|
+
actions.push({
|
|
3110
|
+
type: "inject_react",
|
|
3111
|
+
projectPath,
|
|
3112
|
+
appRoot: detection.appRoot,
|
|
3113
|
+
mode: "next"
|
|
3114
|
+
});
|
|
3115
|
+
actions.push({
|
|
3116
|
+
type: "inject_next_config",
|
|
3117
|
+
projectPath
|
|
3118
|
+
});
|
|
3119
|
+
return { actions, dependencies };
|
|
3120
|
+
},
|
|
3121
|
+
async *execute(targets, config, project) {
|
|
3122
|
+
if (targets.length === 0) return;
|
|
3123
|
+
const target = targets[0];
|
|
3124
|
+
yield {
|
|
3125
|
+
type: "start",
|
|
3126
|
+
message: "Installing Next.js overlay"
|
|
3127
|
+
};
|
|
3128
|
+
yield {
|
|
3129
|
+
type: "progress",
|
|
3130
|
+
message: `Installing in ${target.label}`,
|
|
3131
|
+
detail: "\u2192 Adding API routes"
|
|
3132
|
+
};
|
|
3133
|
+
yield {
|
|
3134
|
+
type: "progress",
|
|
3135
|
+
message: "Installing dependencies",
|
|
3136
|
+
detail: "\u2192 uilint-react, uilint-core, jsx-loc-plugin"
|
|
3137
|
+
};
|
|
3138
|
+
yield {
|
|
3139
|
+
type: "progress",
|
|
3140
|
+
message: "Injecting devtools component",
|
|
3141
|
+
detail: "\u2192 <uilint-devtools /> in root layout"
|
|
3142
|
+
};
|
|
3143
|
+
yield {
|
|
3144
|
+
type: "progress",
|
|
3145
|
+
message: "Configuring jsx-loc-plugin",
|
|
3146
|
+
detail: "\u2192 next.config.js"
|
|
3147
|
+
};
|
|
3148
|
+
yield {
|
|
3149
|
+
type: "complete",
|
|
3150
|
+
message: "Next.js overlay installed"
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
3154
|
+
|
|
3155
|
+
// src/commands/install/installers/vite-overlay.ts
|
|
3156
|
+
var viteOverlayInstaller = {
|
|
3157
|
+
id: "vite",
|
|
3158
|
+
name: "Vite overlay",
|
|
3159
|
+
description: "Alt+Click UI inspector for Vite + React apps",
|
|
3160
|
+
icon: "\u26A1",
|
|
3161
|
+
isApplicable(project) {
|
|
3162
|
+
return project.viteApps.length > 0;
|
|
3163
|
+
},
|
|
3164
|
+
getTargets(project) {
|
|
3165
|
+
return project.viteApps.map((app) => ({
|
|
3166
|
+
id: `vite-${app.projectPath}`,
|
|
3167
|
+
label: app.projectPath.split("/").pop() || app.projectPath,
|
|
3168
|
+
path: app.projectPath,
|
|
3169
|
+
hint: "React + Vite",
|
|
3170
|
+
isInstalled: false
|
|
3171
|
+
// TODO: Detect if already installed
|
|
3172
|
+
}));
|
|
3173
|
+
},
|
|
3174
|
+
plan(targets, config, project) {
|
|
3175
|
+
const actions = [];
|
|
3176
|
+
const dependencies = [];
|
|
3177
|
+
if (targets.length === 0) return { actions, dependencies };
|
|
3178
|
+
const target = targets[0];
|
|
3179
|
+
const appInfo = project.viteApps.find((app) => app.projectPath === target.path);
|
|
3180
|
+
if (!appInfo) return { actions, dependencies };
|
|
3181
|
+
const { projectPath, detection } = appInfo;
|
|
3182
|
+
dependencies.push({
|
|
3183
|
+
packagePath: projectPath,
|
|
3184
|
+
packageManager: project.packageManager,
|
|
3185
|
+
packages: ["uilint-react", "uilint-core", "jsx-loc-plugin"]
|
|
3186
|
+
});
|
|
3187
|
+
actions.push({
|
|
3188
|
+
type: "inject_react",
|
|
3189
|
+
projectPath,
|
|
3190
|
+
appRoot: detection.entryRoot,
|
|
3191
|
+
mode: "vite"
|
|
3192
|
+
});
|
|
3193
|
+
actions.push({
|
|
3194
|
+
type: "inject_vite_config",
|
|
3195
|
+
projectPath
|
|
3196
|
+
});
|
|
3197
|
+
return { actions, dependencies };
|
|
3198
|
+
},
|
|
3199
|
+
async *execute(targets, config, project) {
|
|
3200
|
+
if (targets.length === 0) return;
|
|
3201
|
+
const target = targets[0];
|
|
3202
|
+
yield {
|
|
3203
|
+
type: "start",
|
|
3204
|
+
message: "Installing Vite overlay"
|
|
3205
|
+
};
|
|
3206
|
+
yield {
|
|
3207
|
+
type: "progress",
|
|
3208
|
+
message: `Installing in ${target.label}`,
|
|
3209
|
+
detail: "\u2192 Adding dependencies"
|
|
3210
|
+
};
|
|
3211
|
+
yield {
|
|
3212
|
+
type: "progress",
|
|
3213
|
+
message: "Installing dependencies",
|
|
3214
|
+
detail: "\u2192 uilint-react, uilint-core, jsx-loc-plugin"
|
|
3215
|
+
};
|
|
3216
|
+
yield {
|
|
3217
|
+
type: "progress",
|
|
3218
|
+
message: "Injecting devtools component",
|
|
3219
|
+
detail: "\u2192 <uilint-devtools /> in entry file"
|
|
3220
|
+
};
|
|
3221
|
+
yield {
|
|
3222
|
+
type: "progress",
|
|
3223
|
+
message: "Configuring jsx-loc-plugin",
|
|
3224
|
+
detail: "\u2192 vite.config.ts"
|
|
3225
|
+
};
|
|
3226
|
+
yield {
|
|
3227
|
+
type: "complete",
|
|
3228
|
+
message: "Vite overlay installed"
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
};
|
|
3232
|
+
|
|
3233
|
+
// src/commands/install/installers/index.ts
|
|
3234
|
+
registerInstaller(genstyleguideInstaller);
|
|
3235
|
+
registerInstaller(skillInstaller);
|
|
3236
|
+
registerInstaller(eslintInstaller);
|
|
3237
|
+
registerInstaller(nextOverlayInstaller);
|
|
3238
|
+
registerInstaller(viteOverlayInstaller);
|
|
3239
|
+
|
|
3240
|
+
// src/commands/install-ui.tsx
|
|
3241
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
3242
|
+
function selectionsToUserChoices(selections, project) {
|
|
3243
|
+
const items = [];
|
|
3244
|
+
const choices = { items };
|
|
3245
|
+
for (const selection of selections) {
|
|
3246
|
+
if (!selection.selected || selection.targets.length === 0) continue;
|
|
3247
|
+
const { installer, targets } = selection;
|
|
3248
|
+
if (installer.id === "genstyleguide") {
|
|
3249
|
+
items.push("genstyleguide");
|
|
3250
|
+
} else if (installer.id === "skill") {
|
|
3251
|
+
items.push("skill");
|
|
3252
|
+
} else if (installer.id === "eslint") {
|
|
3253
|
+
items.push("eslint");
|
|
3254
|
+
choices.eslint = {
|
|
3255
|
+
packagePaths: targets.map((t) => t.path),
|
|
3256
|
+
selectedRules: ruleRegistry2
|
|
3257
|
+
};
|
|
3258
|
+
} else if (installer.id === "next") {
|
|
3259
|
+
items.push("next");
|
|
3260
|
+
const target = targets[0];
|
|
3261
|
+
const appInfo = project.nextApps.find(
|
|
3262
|
+
(app) => app.projectPath === target?.path
|
|
3263
|
+
);
|
|
3264
|
+
if (appInfo) {
|
|
3265
|
+
choices.next = {
|
|
3266
|
+
projectPath: appInfo.projectPath,
|
|
3267
|
+
detection: appInfo.detection
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
} else if (installer.id === "vite") {
|
|
3271
|
+
items.push("vite");
|
|
3272
|
+
const target = targets[0];
|
|
3273
|
+
const appInfo = project.viteApps.find(
|
|
3274
|
+
(app) => app.projectPath === target?.path
|
|
3275
|
+
);
|
|
3276
|
+
if (appInfo) {
|
|
3277
|
+
choices.vite = {
|
|
3278
|
+
projectPath: appInfo.projectPath,
|
|
3279
|
+
detection: appInfo.detection
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
return choices;
|
|
3285
|
+
}
|
|
3286
|
+
function isInteractiveTerminal() {
|
|
3287
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
3288
|
+
}
|
|
3289
|
+
async function installUI(options = {}, executeOptions = {}) {
|
|
3290
|
+
const projectPath = process.cwd();
|
|
3291
|
+
if (!isInteractiveTerminal()) {
|
|
3292
|
+
console.error("\n\u2717 Interactive mode requires a TTY terminal.");
|
|
3293
|
+
console.error("Run uilint install in an interactive terminal.\n");
|
|
3294
|
+
process.exit(1);
|
|
3295
|
+
}
|
|
3296
|
+
const projectPromise = analyze(projectPath);
|
|
3297
|
+
const { waitUntilExit } = render(
|
|
3298
|
+
/* @__PURE__ */ jsx5(
|
|
3299
|
+
InstallApp,
|
|
3300
|
+
{
|
|
3301
|
+
projectPromise,
|
|
3302
|
+
onComplete: async (selections) => {
|
|
3303
|
+
const project = await projectPromise;
|
|
3304
|
+
const choices = selectionsToUserChoices(selections, project);
|
|
3305
|
+
if (choices.items.length === 0) {
|
|
3306
|
+
console.log("\nNo items selected for installation");
|
|
3307
|
+
process.exit(0);
|
|
3308
|
+
}
|
|
3309
|
+
const { createPlan } = await import("./plan-PX7FFJ25.js");
|
|
3310
|
+
const plan = createPlan(project, choices, { force: options.force });
|
|
3311
|
+
const result = await execute(plan, executeOptions);
|
|
3312
|
+
if (result.success) {
|
|
3313
|
+
console.log("\n\u2713 Installation completed successfully!");
|
|
3314
|
+
} else {
|
|
3315
|
+
console.log("\n\u26A0 Installation completed with errors");
|
|
3316
|
+
}
|
|
3317
|
+
process.exit(result.success ? 0 : 1);
|
|
3318
|
+
},
|
|
3319
|
+
onError: (error) => {
|
|
3320
|
+
console.error("\n\u2717 Error:", error.message);
|
|
3321
|
+
process.exit(1);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
)
|
|
3325
|
+
);
|
|
3326
|
+
await waitUntilExit();
|
|
3327
|
+
}
|
|
3328
|
+
export {
|
|
3329
|
+
installUI
|
|
3330
|
+
};
|
|
3331
|
+
//# sourceMappingURL=install-ui-OEFHX4FG.js.map
|