uidex 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +263 -263
- package/dist/cli/cli.cjs +3243 -0
- package/dist/cli/cli.cjs.map +1 -0
- package/dist/cloud/index.cjs +149 -0
- package/dist/cloud/index.cjs.map +1 -0
- package/dist/cloud/index.d.cts +108 -0
- package/dist/cloud/index.d.ts +108 -0
- package/dist/cloud/index.js +120 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/headless/index.cjs +3580 -0
- package/dist/headless/index.cjs.map +1 -0
- package/dist/headless/index.d.cts +214 -0
- package/dist/headless/index.d.ts +214 -0
- package/dist/headless/index.js +3562 -0
- package/dist/headless/index.js.map +1 -0
- package/dist/index.cjs +7977 -3301
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +898 -108
- package/dist/index.d.ts +898 -108
- package/dist/index.js +7934 -3270
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs +164 -24
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.d.cts +32 -55
- package/dist/playwright/index.d.ts +32 -55
- package/dist/playwright/index.js +148 -21
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/reporter.cjs +62 -28
- package/dist/playwright/reporter.cjs.map +1 -1
- package/dist/playwright/reporter.d.cts +24 -12
- package/dist/playwright/reporter.d.ts +24 -12
- package/dist/playwright/reporter.js +62 -28
- package/dist/playwright/reporter.js.map +1 -1
- package/dist/react/index.cjs +7970 -3267
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +670 -108
- package/dist/react/index.d.ts +670 -108
- package/dist/react/index.js +8016 -3274
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +3281 -0
- package/dist/scan/index.cjs.map +1 -0
- package/dist/scan/index.d.cts +373 -0
- package/dist/scan/index.d.ts +373 -0
- package/dist/scan/index.js +3224 -0
- package/dist/scan/index.js.map +1 -0
- package/package.json +74 -56
- package/templates/claude/audit.md +37 -0
- package/templates/claude/rules.md +212 -0
- package/claude/audit-command.md +0 -16
- package/claude/rules.md +0 -88
- package/dist/core/index.cjs +0 -3490
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -441
- package/dist/core/index.d.ts +0 -441
- package/dist/core/index.global.js +0 -3469
- package/dist/core/index.global.js.map +0 -1
- package/dist/core/index.js +0 -3444
- package/dist/core/index.js.map +0 -1
- package/dist/core/style.css +0 -971
- package/dist/scripts/cli.cjs +0 -1168
- package/uidex.schema.json +0 -93
package/dist/scripts/cli.cjs
DELETED
|
@@ -1,1168 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
-
for (let key of __getOwnPropNames(from))
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
-
}
|
|
15
|
-
return to;
|
|
16
|
-
};
|
|
17
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
-
mod
|
|
24
|
-
));
|
|
25
|
-
|
|
26
|
-
// scripts/init.ts
|
|
27
|
-
var fs = __toESM(require("fs"), 1);
|
|
28
|
-
var path = __toESM(require("path"), 1);
|
|
29
|
-
var readline = __toESM(require("readline"), 1);
|
|
30
|
-
|
|
31
|
-
// scripts/cli-utils.ts
|
|
32
|
-
var colors = {
|
|
33
|
-
reset: "\x1B[0m",
|
|
34
|
-
bold: "\x1B[1m",
|
|
35
|
-
dim: "\x1B[2m",
|
|
36
|
-
green: "\x1B[32m",
|
|
37
|
-
yellow: "\x1B[33m",
|
|
38
|
-
blue: "\x1B[34m",
|
|
39
|
-
cyan: "\x1B[36m",
|
|
40
|
-
red: "\x1B[31m"
|
|
41
|
-
};
|
|
42
|
-
function success(message) {
|
|
43
|
-
console.log(`${colors.green}\u2713${colors.reset} ${message}`);
|
|
44
|
-
}
|
|
45
|
-
function info(message) {
|
|
46
|
-
console.log(`${colors.blue}\u2139${colors.reset} ${message}`);
|
|
47
|
-
}
|
|
48
|
-
function warn(message) {
|
|
49
|
-
console.log(`${colors.yellow}\u26A0${colors.reset} ${message}`);
|
|
50
|
-
}
|
|
51
|
-
function error(message) {
|
|
52
|
-
console.log(`${colors.red}\u2717${colors.reset} ${message}`);
|
|
53
|
-
}
|
|
54
|
-
function heading(message) {
|
|
55
|
-
console.log(`
|
|
56
|
-
${colors.bold}${colors.cyan}${message}${colors.reset}
|
|
57
|
-
`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// scripts/init.ts
|
|
61
|
-
function log(message) {
|
|
62
|
-
console.log(message);
|
|
63
|
-
}
|
|
64
|
-
function detectProject() {
|
|
65
|
-
const cwd = process.cwd();
|
|
66
|
-
const packageJsonPath = path.join(cwd, "package.json");
|
|
67
|
-
let packageJson = {};
|
|
68
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
69
|
-
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
70
|
-
}
|
|
71
|
-
const deps = {
|
|
72
|
-
...packageJson.dependencies,
|
|
73
|
-
...packageJson.devDependencies
|
|
74
|
-
};
|
|
75
|
-
const name = packageJson.name || path.basename(cwd);
|
|
76
|
-
const usesTypeScript = fs.existsSync(path.join(cwd, "tsconfig.json")) || !!deps["typescript"];
|
|
77
|
-
const hasSrcDir = fs.existsSync(path.join(cwd, "src"));
|
|
78
|
-
const srcDir = hasSrcDir ? "src" : ".";
|
|
79
|
-
if (deps["next"]) {
|
|
80
|
-
const usesAppRouter = fs.existsSync(path.join(cwd, "src", "app")) || fs.existsSync(path.join(cwd, "app"));
|
|
81
|
-
return {
|
|
82
|
-
type: "nextjs",
|
|
83
|
-
name,
|
|
84
|
-
srcDir,
|
|
85
|
-
usesTypeScript,
|
|
86
|
-
usesAppRouter
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
if (deps["vite"] || fs.existsSync(path.join(cwd, "vite.config.ts")) || fs.existsSync(path.join(cwd, "vite.config.js"))) {
|
|
90
|
-
return {
|
|
91
|
-
type: "vite",
|
|
92
|
-
name,
|
|
93
|
-
srcDir,
|
|
94
|
-
usesTypeScript,
|
|
95
|
-
usesAppRouter: false
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
if (deps["react-scripts"]) {
|
|
99
|
-
return {
|
|
100
|
-
type: "cra",
|
|
101
|
-
name,
|
|
102
|
-
srcDir,
|
|
103
|
-
usesTypeScript,
|
|
104
|
-
usesAppRouter: false
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
type: "unknown",
|
|
109
|
-
name,
|
|
110
|
-
srcDir,
|
|
111
|
-
usesTypeScript,
|
|
112
|
-
usesAppRouter: false
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
function createConfigContent(project) {
|
|
116
|
-
const config = {
|
|
117
|
-
$schema: "node_modules/uidex/uidex.schema.json",
|
|
118
|
-
defaults: {
|
|
119
|
-
color: "#3b82f6",
|
|
120
|
-
borderStyle: "solid",
|
|
121
|
-
borderWidth: 2,
|
|
122
|
-
showLabel: true,
|
|
123
|
-
labelPosition: "top-left"
|
|
124
|
-
},
|
|
125
|
-
colors: {
|
|
126
|
-
primary: "#3b82f6",
|
|
127
|
-
secondary: "#8b5cf6",
|
|
128
|
-
success: "#10b981",
|
|
129
|
-
warning: "#f59e0b",
|
|
130
|
-
error: "#ef4444",
|
|
131
|
-
info: "#0ea5e9"
|
|
132
|
-
},
|
|
133
|
-
scanner: {
|
|
134
|
-
rootDir: project.srcDir,
|
|
135
|
-
include: ["**/*.tsx", "**/*.jsx"],
|
|
136
|
-
exclude: ["**/*.test.*", "**/*.spec.*", "**/*.gen.ts"],
|
|
137
|
-
outputPath: `${project.srcDir}/uidex.gen.ts`
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
return JSON.stringify(config, null, 2);
|
|
141
|
-
}
|
|
142
|
-
function addToGitignore(entry) {
|
|
143
|
-
const cwd = process.cwd();
|
|
144
|
-
const gitignorePath = path.join(cwd, ".gitignore");
|
|
145
|
-
let content = "";
|
|
146
|
-
if (fs.existsSync(gitignorePath)) {
|
|
147
|
-
content = fs.readFileSync(gitignorePath, "utf-8");
|
|
148
|
-
}
|
|
149
|
-
const lines = content.split("\n");
|
|
150
|
-
if (lines.some((line) => line.trim() === entry)) {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
const newContent = content.endsWith("\n") || content === "" ? `${content}${entry}
|
|
154
|
-
` : `${content}
|
|
155
|
-
${entry}
|
|
156
|
-
`;
|
|
157
|
-
fs.writeFileSync(gitignorePath, newContent, "utf-8");
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
function createPrompt() {
|
|
161
|
-
return readline.createInterface({
|
|
162
|
-
input: process.stdin,
|
|
163
|
-
output: process.stdout
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
async function askYesNo(rl, question, defaultYes = true) {
|
|
167
|
-
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
168
|
-
return new Promise((resolve4) => {
|
|
169
|
-
rl.question(`${question} ${colors.dim}${hint}${colors.reset} `, (answer) => {
|
|
170
|
-
const normalized = answer.trim().toLowerCase();
|
|
171
|
-
if (normalized === "") {
|
|
172
|
-
resolve4(defaultYes);
|
|
173
|
-
} else {
|
|
174
|
-
resolve4(normalized === "y" || normalized === "yes");
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
function getEntryPointHint(project) {
|
|
180
|
-
if (project.type === "nextjs") {
|
|
181
|
-
if (project.usesAppRouter) {
|
|
182
|
-
return project.srcDir === "src" ? "src/app/layout.tsx" : "app/layout.tsx";
|
|
183
|
-
}
|
|
184
|
-
return project.srcDir === "src" ? "src/pages/_app.tsx" : "pages/_app.tsx";
|
|
185
|
-
}
|
|
186
|
-
return project.srcDir === "src" ? "src/main.tsx" : "main.tsx";
|
|
187
|
-
}
|
|
188
|
-
async function init() {
|
|
189
|
-
heading("uidex init");
|
|
190
|
-
const cwd = process.cwd();
|
|
191
|
-
const configPath = path.join(cwd, ".uidex.json");
|
|
192
|
-
if (fs.existsSync(configPath)) {
|
|
193
|
-
warn(".uidex.json already exists");
|
|
194
|
-
const rl = createPrompt();
|
|
195
|
-
const overwrite = await askYesNo(rl, "Overwrite existing config?", false);
|
|
196
|
-
rl.close();
|
|
197
|
-
if (!overwrite) {
|
|
198
|
-
info("Keeping existing configuration");
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
const project = detectProject();
|
|
203
|
-
log(`Detected project: ${colors.bold}${project.name}${colors.reset}`);
|
|
204
|
-
log(`Project type: ${colors.bold}${project.type}${colors.reset}`);
|
|
205
|
-
log(
|
|
206
|
-
`Language: ${colors.bold}${project.usesTypeScript ? "TypeScript" : "JavaScript"}${colors.reset}`
|
|
207
|
-
);
|
|
208
|
-
if (project.type === "nextjs") {
|
|
209
|
-
log(
|
|
210
|
-
`Router: ${colors.bold}${project.usesAppRouter ? "App Router" : "Pages Router"}${colors.reset}`
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
heading("Creating configuration");
|
|
214
|
-
const configContent = createConfigContent(project);
|
|
215
|
-
fs.writeFileSync(configPath, configContent, "utf-8");
|
|
216
|
-
success("Created .uidex.json");
|
|
217
|
-
const genFile = "*.gen.ts";
|
|
218
|
-
if (addToGitignore(genFile)) {
|
|
219
|
-
success(`Added ${genFile} to .gitignore`);
|
|
220
|
-
} else {
|
|
221
|
-
info(`${genFile} already in .gitignore`);
|
|
222
|
-
}
|
|
223
|
-
heading("Next steps");
|
|
224
|
-
const entryPoint = getEntryPointHint(project);
|
|
225
|
-
log("1. Add data-uidex attributes to elements you want to track:");
|
|
226
|
-
log("");
|
|
227
|
-
log(` ${colors.green}<button${colors.reset} data-uidex="submit-btn"${colors.green}>${colors.reset}Submit${colors.green}</button>${colors.reset}`);
|
|
228
|
-
log("");
|
|
229
|
-
log("2. Run the scanner to generate the components registry:");
|
|
230
|
-
log("");
|
|
231
|
-
log(` ${colors.yellow}npx uidex-scan${colors.reset}`);
|
|
232
|
-
log("");
|
|
233
|
-
log("3. Import the generated file in your entry point:");
|
|
234
|
-
log("");
|
|
235
|
-
log(` ${colors.dim}// ${entryPoint}${colors.reset}`);
|
|
236
|
-
log(` ${colors.cyan}import${colors.reset} './${project.srcDir === "src" ? "" : "src/"}uidex.gen';`);
|
|
237
|
-
log("");
|
|
238
|
-
log("4. Add the UidexDevtools component anywhere in your app:");
|
|
239
|
-
log("");
|
|
240
|
-
log(` ${colors.cyan}import${colors.reset} { UidexDevtools } ${colors.cyan}from${colors.reset} 'uidex';`);
|
|
241
|
-
log("");
|
|
242
|
-
log(` ${colors.green}<UidexDevtools />${colors.reset}`);
|
|
243
|
-
log("");
|
|
244
|
-
success("Setup complete!");
|
|
245
|
-
log("");
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// scripts/scan.ts
|
|
249
|
-
var fs2 = __toESM(require("fs"), 1);
|
|
250
|
-
var path2 = __toESM(require("path"), 1);
|
|
251
|
-
|
|
252
|
-
// scripts/scanner-utils.ts
|
|
253
|
-
var UIDEX_PAGE_FILENAME = "UIDEX_PAGE.md";
|
|
254
|
-
var UIDEX_FEATURE_FILENAME = "UIDEX_FEATURE.md";
|
|
255
|
-
function parseFrontmatter(content) {
|
|
256
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
257
|
-
if (!match) {
|
|
258
|
-
return { frontmatter: {}, body: content };
|
|
259
|
-
}
|
|
260
|
-
const raw = match[1];
|
|
261
|
-
const body = match[2];
|
|
262
|
-
const frontmatter = {};
|
|
263
|
-
const lines = raw.split("\n");
|
|
264
|
-
let inComponents = false;
|
|
265
|
-
const components = [];
|
|
266
|
-
for (const line of lines) {
|
|
267
|
-
if (/^components:\s*$/.test(line.trimEnd())) {
|
|
268
|
-
inComponents = true;
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
if (inComponents) {
|
|
272
|
-
const itemMatch = line.match(/^\s+-\s+(.+)$/);
|
|
273
|
-
if (itemMatch) {
|
|
274
|
-
components.push(itemMatch[1].trim());
|
|
275
|
-
} else if (line.trim() !== "") {
|
|
276
|
-
inComponents = false;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
if (components.length > 0) {
|
|
281
|
-
frontmatter.components = components;
|
|
282
|
-
}
|
|
283
|
-
return { frontmatter, body };
|
|
284
|
-
}
|
|
285
|
-
function extractJSDocBlocks(content) {
|
|
286
|
-
const blocks = [];
|
|
287
|
-
const blockRegex = /\/\*\*[\s\S]*?\*\//g;
|
|
288
|
-
let match;
|
|
289
|
-
while ((match = blockRegex.exec(content)) !== null) {
|
|
290
|
-
const blockContent = match[0];
|
|
291
|
-
const uidexMatch = blockContent.match(/@uidex\s+(\S+)/);
|
|
292
|
-
if (!uidexMatch) continue;
|
|
293
|
-
const id = uidexMatch[1];
|
|
294
|
-
const description = extractJSDocDescription(blockContent, id);
|
|
295
|
-
if (!description) continue;
|
|
296
|
-
const blockStart = match.index;
|
|
297
|
-
const blockEnd = blockStart + blockContent.length;
|
|
298
|
-
const textBeforeEnd = content.substring(0, blockEnd);
|
|
299
|
-
const endLine = textBeforeEnd.split("\n").length;
|
|
300
|
-
blocks.push({ id, description, endLine });
|
|
301
|
-
}
|
|
302
|
-
return blocks;
|
|
303
|
-
}
|
|
304
|
-
function extractJSDocDescription(blockContent, id) {
|
|
305
|
-
const singleLineMatch = blockContent.match(
|
|
306
|
-
new RegExp(`@uidex\\s+${escapeRegex(id)}\\s+-\\s+(.+?)\\s*\\*\\/`)
|
|
307
|
-
);
|
|
308
|
-
if (singleLineMatch) {
|
|
309
|
-
return singleLineMatch[1].trim();
|
|
310
|
-
}
|
|
311
|
-
const lines = blockContent.split("\n");
|
|
312
|
-
const descriptionLines = [];
|
|
313
|
-
let foundUidex = false;
|
|
314
|
-
for (const line of lines) {
|
|
315
|
-
if (line.includes(`@uidex`) && line.includes(id)) {
|
|
316
|
-
foundUidex = true;
|
|
317
|
-
const inlineMatch = line.match(new RegExp(`@uidex\\s+${escapeRegex(id)}\\s+(.+)`));
|
|
318
|
-
if (inlineMatch) {
|
|
319
|
-
const inline = inlineMatch[1].replace(/\*\/$/, "").trim();
|
|
320
|
-
if (inline && !inline.startsWith("-")) {
|
|
321
|
-
descriptionLines.push(inline);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
if (foundUidex) {
|
|
327
|
-
if (line.includes("@") && /^\s*\*\s*@/.test(line)) {
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
if (line.includes("*/")) {
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
const contentMatch = line.match(/^\s*\*\s*(.*)$/);
|
|
334
|
-
if (contentMatch) {
|
|
335
|
-
const content = contentMatch[1].trim();
|
|
336
|
-
if (content) {
|
|
337
|
-
descriptionLines.push(content);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return descriptionLines.join(" ").trim();
|
|
343
|
-
}
|
|
344
|
-
function formatRoute(dir) {
|
|
345
|
-
const stripped = dir.replace(/^src\/app\/?/, "").replace(/^src\/pages\/?/, "").replace(/^app\/?/, "").replace(/^pages\/?/, "");
|
|
346
|
-
return "/" + stripped;
|
|
347
|
-
}
|
|
348
|
-
function parseAcceptanceCriteria(content) {
|
|
349
|
-
const lines = content.split("\n");
|
|
350
|
-
const criteria = [];
|
|
351
|
-
let inAcceptance = false;
|
|
352
|
-
for (const line of lines) {
|
|
353
|
-
if (/^##\s+Acceptance/.test(line)) {
|
|
354
|
-
inAcceptance = true;
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
if (inAcceptance) {
|
|
358
|
-
if (/^##\s+/.test(line)) break;
|
|
359
|
-
const match = line.match(/^-\s+(.+)$/);
|
|
360
|
-
if (match) criteria.push(match[1].trim());
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return criteria;
|
|
364
|
-
}
|
|
365
|
-
function parseMarkdownTitle(content) {
|
|
366
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
367
|
-
return match ? match[1].trim() : null;
|
|
368
|
-
}
|
|
369
|
-
function escapeRegex(str) {
|
|
370
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
371
|
-
}
|
|
372
|
-
function extractComponents(content) {
|
|
373
|
-
const results = [];
|
|
374
|
-
const jsDocBlocks = extractJSDocBlocks(content);
|
|
375
|
-
const regex = /data-uidex\s*=\s*(?:"([^"]+)"|'([^']+)'|\{["'`]([^"'`]+)["'`]\})/g;
|
|
376
|
-
const lines = content.split("\n");
|
|
377
|
-
lines.forEach((line, index) => {
|
|
378
|
-
let match;
|
|
379
|
-
regex.lastIndex = 0;
|
|
380
|
-
while ((match = regex.exec(line)) !== null) {
|
|
381
|
-
const id = match[1] || match[2] || match[3];
|
|
382
|
-
if (id) {
|
|
383
|
-
const lineNumber = index + 1;
|
|
384
|
-
const matchingBlocks = jsDocBlocks.filter(
|
|
385
|
-
(block) => block.id === id && block.endLine <= lineNumber && lineNumber - block.endLine <= 5
|
|
386
|
-
);
|
|
387
|
-
const matchingBlock = matchingBlocks.length > 0 ? matchingBlocks.reduce(
|
|
388
|
-
(closest, block) => block.endLine > closest.endLine ? block : closest
|
|
389
|
-
) : void 0;
|
|
390
|
-
results.push({
|
|
391
|
-
id,
|
|
392
|
-
line: lineNumber,
|
|
393
|
-
...matchingBlock?.description ? { doc: matchingBlock.description } : {}
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
return results;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// scripts/scan.ts
|
|
402
|
-
var DEFAULT_SOURCE = {
|
|
403
|
-
rootDir: "src",
|
|
404
|
-
include: ["**/*.tsx", "**/*.jsx"]
|
|
405
|
-
};
|
|
406
|
-
var DEFAULT_CONFIG = {
|
|
407
|
-
sources: [DEFAULT_SOURCE],
|
|
408
|
-
exclude: ["**/*.test.*", "**/*.spec.*", "**/node_modules/**", "**/*.gen.ts"],
|
|
409
|
-
outputPath: "src/uidex.gen.ts"
|
|
410
|
-
};
|
|
411
|
-
function loadConfig() {
|
|
412
|
-
const configPath = path2.resolve(process.cwd(), ".uidex.json");
|
|
413
|
-
if (!fs2.existsSync(configPath)) {
|
|
414
|
-
console.log("No .uidex.json found, using defaults");
|
|
415
|
-
return DEFAULT_CONFIG;
|
|
416
|
-
}
|
|
417
|
-
try {
|
|
418
|
-
const content = fs2.readFileSync(configPath, "utf-8");
|
|
419
|
-
const config = JSON.parse(content);
|
|
420
|
-
if (!config.scanner) {
|
|
421
|
-
console.log("No scanner config in .uidex.json, using defaults");
|
|
422
|
-
return DEFAULT_CONFIG;
|
|
423
|
-
}
|
|
424
|
-
const scanner = config.scanner;
|
|
425
|
-
return {
|
|
426
|
-
sources: scanner.sources?.length ? scanner.sources.map((source) => ({
|
|
427
|
-
rootDir: source.rootDir,
|
|
428
|
-
include: source.include,
|
|
429
|
-
exclude: source.exclude,
|
|
430
|
-
prefix: source.prefix
|
|
431
|
-
})) : DEFAULT_CONFIG.sources,
|
|
432
|
-
exclude: scanner.exclude ?? DEFAULT_CONFIG.exclude,
|
|
433
|
-
outputPath: scanner.outputPath ?? DEFAULT_CONFIG.outputPath
|
|
434
|
-
};
|
|
435
|
-
} catch (error2) {
|
|
436
|
-
console.error("Error reading .uidex.json:", error2);
|
|
437
|
-
return DEFAULT_CONFIG;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
function globToRegex(glob) {
|
|
441
|
-
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}/g, ".*");
|
|
442
|
-
return new RegExp(`^${escaped}$`);
|
|
443
|
-
}
|
|
444
|
-
function compilePatterns(patterns) {
|
|
445
|
-
return patterns.map(globToRegex);
|
|
446
|
-
}
|
|
447
|
-
function matchesPatterns(filePath, patterns) {
|
|
448
|
-
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
449
|
-
return patterns.some((regex) => regex.test(normalizedPath));
|
|
450
|
-
}
|
|
451
|
-
function walkDir(dir, baseDir = dir) {
|
|
452
|
-
const result = { files: [], pageDocs: /* @__PURE__ */ new Map(), featureDocs: /* @__PURE__ */ new Map() };
|
|
453
|
-
if (!fs2.existsSync(dir)) {
|
|
454
|
-
return result;
|
|
455
|
-
}
|
|
456
|
-
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
457
|
-
for (const entry of entries) {
|
|
458
|
-
const fullPath = path2.join(dir, entry.name);
|
|
459
|
-
if (entry.isDirectory()) {
|
|
460
|
-
if (entry.name === "node_modules") continue;
|
|
461
|
-
const sub = walkDir(fullPath, baseDir);
|
|
462
|
-
result.files.push(...sub.files);
|
|
463
|
-
for (const [k, v] of sub.pageDocs) {
|
|
464
|
-
result.pageDocs.set(k, v);
|
|
465
|
-
}
|
|
466
|
-
for (const [k, v] of sub.featureDocs) {
|
|
467
|
-
result.featureDocs.set(k, v);
|
|
468
|
-
}
|
|
469
|
-
} else if (entry.isFile()) {
|
|
470
|
-
if (entry.name === UIDEX_PAGE_FILENAME || entry.name === UIDEX_FEATURE_FILENAME) {
|
|
471
|
-
const relativeDir = path2.relative(baseDir, dir).replace(/\\/g, "/") || ".";
|
|
472
|
-
const content = fs2.readFileSync(fullPath, "utf-8");
|
|
473
|
-
if (entry.name === UIDEX_PAGE_FILENAME) {
|
|
474
|
-
result.pageDocs.set(relativeDir, content);
|
|
475
|
-
} else {
|
|
476
|
-
result.featureDocs.set(relativeDir, content);
|
|
477
|
-
}
|
|
478
|
-
} else {
|
|
479
|
-
const relativePath = path2.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
480
|
-
result.files.push(relativePath);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
return result;
|
|
485
|
-
}
|
|
486
|
-
function findFilesForSource(source, globalExclude) {
|
|
487
|
-
const rootDir = path2.resolve(process.cwd(), source.rootDir);
|
|
488
|
-
const walkResult = walkDir(rootDir);
|
|
489
|
-
const includeRegexes = compilePatterns(source.include);
|
|
490
|
-
const excludeRegexes = compilePatterns([...globalExclude, ...source.exclude ?? []]);
|
|
491
|
-
const files = walkResult.files.filter((file) => {
|
|
492
|
-
const matchesInclude = matchesPatterns(file, includeRegexes);
|
|
493
|
-
const matchesExclude = matchesPatterns(file, excludeRegexes);
|
|
494
|
-
return matchesInclude && !matchesExclude;
|
|
495
|
-
}).map((relativePath) => {
|
|
496
|
-
const outputPath = source.prefix ? `${source.prefix}/${relativePath}` : relativePath;
|
|
497
|
-
return {
|
|
498
|
-
relativePath,
|
|
499
|
-
fullPath: path2.join(rootDir, relativePath),
|
|
500
|
-
outputPath
|
|
501
|
-
};
|
|
502
|
-
});
|
|
503
|
-
function applyPrefix(docs) {
|
|
504
|
-
const result = /* @__PURE__ */ new Map();
|
|
505
|
-
for (const [dir, content] of docs) {
|
|
506
|
-
const outputDir = source.prefix ? `${source.prefix}/${dir}` : dir;
|
|
507
|
-
result.set(outputDir, content);
|
|
508
|
-
}
|
|
509
|
-
return result;
|
|
510
|
-
}
|
|
511
|
-
return {
|
|
512
|
-
files,
|
|
513
|
-
pageDocs: applyPrefix(walkResult.pageDocs),
|
|
514
|
-
featureDocs: applyPrefix(walkResult.featureDocs)
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
function buildPages(docs, sourceComponentIds, sourceRootDir) {
|
|
518
|
-
if (docs.size === 0) return [];
|
|
519
|
-
const docDirs = [...docs.keys()].sort((a, b) => b.length - a.length);
|
|
520
|
-
const pageComponentSets = /* @__PURE__ */ new Map();
|
|
521
|
-
for (const dir of docDirs) {
|
|
522
|
-
pageComponentSets.set(dir, /* @__PURE__ */ new Set());
|
|
523
|
-
}
|
|
524
|
-
for (const [id, filePaths] of sourceComponentIds) {
|
|
525
|
-
for (const filePath of filePaths) {
|
|
526
|
-
const fileDir = filePath.includes("/") ? filePath.substring(0, filePath.lastIndexOf("/")) : ".";
|
|
527
|
-
const nearestDir = docDirs.find(
|
|
528
|
-
(dir) => dir === "." || fileDir === dir || fileDir.startsWith(dir + "/")
|
|
529
|
-
);
|
|
530
|
-
if (nearestDir) {
|
|
531
|
-
pageComponentSets.get(nearestDir).add(id);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
return docDirs.map((dir) => {
|
|
536
|
-
const fullDir = dir === "." ? sourceRootDir : `${sourceRootDir}/${dir}`;
|
|
537
|
-
return {
|
|
538
|
-
dir: fullDir,
|
|
539
|
-
content: docs.get(dir),
|
|
540
|
-
componentIds: [...pageComponentSets.get(dir)].sort()
|
|
541
|
-
};
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
function buildFeatures(featureDocs, sourceRootDir) {
|
|
545
|
-
const features = [];
|
|
546
|
-
for (const [dir, { body, explicitComponents }] of featureDocs) {
|
|
547
|
-
const fullDir = dir === "." ? sourceRootDir : `${sourceRootDir}/${dir}`;
|
|
548
|
-
features.push({
|
|
549
|
-
dir: fullDir,
|
|
550
|
-
content: body,
|
|
551
|
-
componentIds: [...explicitComponents].sort()
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
return features;
|
|
555
|
-
}
|
|
556
|
-
function generateTestOutput(components, pages, features) {
|
|
557
|
-
const sortedIds = Object.keys(components).sort();
|
|
558
|
-
const idsArrayStr = sortedIds.map((id) => `"${id}"`).join(", ");
|
|
559
|
-
const pageRoutes = pages.map((p) => ({ ...p, route: formatRoute(p.dir) }));
|
|
560
|
-
const routesStr = pageRoutes.length > 0 ? `
|
|
561
|
-
export const routes = {
|
|
562
|
-
${pageRoutes.map((p) => ` "${p.dir}": "${p.route}"`).join(",\n")}
|
|
563
|
-
} as const;
|
|
564
|
-
|
|
565
|
-
export type Route = typeof routes[keyof typeof routes];
|
|
566
|
-
` : "";
|
|
567
|
-
const pagesStr = pageRoutes.length > 0 ? `
|
|
568
|
-
export const pages = [
|
|
569
|
-
${pageRoutes.map((p) => {
|
|
570
|
-
const ids = p.componentIds.map((id) => `"${id}"`).join(", ");
|
|
571
|
-
return ` { dir: "${p.dir}", route: "${p.route}", componentIds: [${ids}] as const }`;
|
|
572
|
-
}).join(",\n")}
|
|
573
|
-
] as const;
|
|
574
|
-
` : "";
|
|
575
|
-
const featuresStr = features.length > 0 ? `
|
|
576
|
-
export const features = [
|
|
577
|
-
${features.map((f) => {
|
|
578
|
-
const ids = f.componentIds.map((id) => `"${id}"`).join(", ");
|
|
579
|
-
return ` { dir: "${f.dir}", componentIds: [${ids}] as const }`;
|
|
580
|
-
}).join(",\n")}
|
|
581
|
-
] as const;
|
|
582
|
-
` : "";
|
|
583
|
-
return `// Auto-generated by uidex scanner \u2014 safe for test imports (no side effects)
|
|
584
|
-
// Do not edit this file manually
|
|
585
|
-
|
|
586
|
-
export const componentIds = [${idsArrayStr}] as const;
|
|
587
|
-
|
|
588
|
-
export type ComponentId = typeof componentIds[number];
|
|
589
|
-
${routesStr}${pagesStr}${featuresStr}`;
|
|
590
|
-
}
|
|
591
|
-
function generateOutput(components, pages, features) {
|
|
592
|
-
const sortedIds = Object.keys(components).sort();
|
|
593
|
-
const entriesStr = sortedIds.map((id) => {
|
|
594
|
-
const locations = components[id].map((loc) => {
|
|
595
|
-
const parts = [`filePath: "${loc.filePath}"`, `line: ${loc.line}`];
|
|
596
|
-
if (loc.doc) {
|
|
597
|
-
const escapedDoc = loc.doc.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
598
|
-
parts.push(`doc: "${escapedDoc}"`);
|
|
599
|
-
}
|
|
600
|
-
return `{ ${parts.join(", ")} }`;
|
|
601
|
-
}).join(", ");
|
|
602
|
-
return ` "${id}": [${locations}]`;
|
|
603
|
-
}).join(",\n");
|
|
604
|
-
const idsArrayStr = sortedIds.map((id) => `"${id}"`).join(", ");
|
|
605
|
-
function serializeDocEntries(entries) {
|
|
606
|
-
return entries.map((entry) => {
|
|
607
|
-
const escaped = entry.content.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
608
|
-
const ids = entry.componentIds.map((id) => `"${id}"`).join(", ");
|
|
609
|
-
return ` { dir: "${entry.dir}", content: \`${escaped}\`, componentIds: [${ids}] }`;
|
|
610
|
-
}).join(",\n");
|
|
611
|
-
}
|
|
612
|
-
const pagesStr = pages.length > 0 ? `
|
|
613
|
-
export const pages = [
|
|
614
|
-
${serializeDocEntries(pages)}
|
|
615
|
-
];
|
|
616
|
-
` : "";
|
|
617
|
-
const featuresStr = features.length > 0 ? `
|
|
618
|
-
export const features = [
|
|
619
|
-
${serializeDocEntries(features)}
|
|
620
|
-
];
|
|
621
|
-
` : "";
|
|
622
|
-
const hasPages = pages.length > 0;
|
|
623
|
-
const hasFeatures = features.length > 0;
|
|
624
|
-
const importParts = ["registerComponents"];
|
|
625
|
-
if (hasPages) importParts.push("registerPages");
|
|
626
|
-
if (hasFeatures) importParts.push("registerFeatures");
|
|
627
|
-
const imports = `import { ${importParts.join(", ")} } from 'uidex';`;
|
|
628
|
-
const regParts = ["registerComponents(components);"];
|
|
629
|
-
if (hasPages) regParts.push("registerPages(pages);");
|
|
630
|
-
if (hasFeatures) regParts.push("registerFeatures(features);");
|
|
631
|
-
const registrations = `// Auto-register
|
|
632
|
-
${regParts.join("\n")}
|
|
633
|
-
`;
|
|
634
|
-
return `"use client";
|
|
635
|
-
// Auto-generated by uidex scanner
|
|
636
|
-
// Do not edit this file manually
|
|
637
|
-
|
|
638
|
-
${imports}
|
|
639
|
-
|
|
640
|
-
export const components = {
|
|
641
|
-
${entriesStr}
|
|
642
|
-
};
|
|
643
|
-
|
|
644
|
-
export const componentIds = [${idsArrayStr}] as const;
|
|
645
|
-
|
|
646
|
-
export type ComponentId = typeof componentIds[number];
|
|
647
|
-
${pagesStr}${featuresStr}
|
|
648
|
-
${registrations}`;
|
|
649
|
-
}
|
|
650
|
-
function ensureOutputDir(outputPath) {
|
|
651
|
-
const dir = path2.dirname(outputPath);
|
|
652
|
-
if (!fs2.existsSync(dir)) {
|
|
653
|
-
fs2.mkdirSync(dir, { recursive: true });
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
function runScan(config) {
|
|
657
|
-
const components = {};
|
|
658
|
-
const pages = [];
|
|
659
|
-
const features = [];
|
|
660
|
-
let totalComponents = 0;
|
|
661
|
-
let totalFiles = 0;
|
|
662
|
-
for (const source of config.sources) {
|
|
663
|
-
const sourceResult = findFilesForSource(source, config.exclude);
|
|
664
|
-
totalFiles += sourceResult.files.length;
|
|
665
|
-
console.log(
|
|
666
|
-
`Scanning ${source.rootDir}: ${sourceResult.files.length} files${source.prefix ? ` (\u2192 ${source.prefix}/*)` : ""}`
|
|
667
|
-
);
|
|
668
|
-
const sourceComponentIds = /* @__PURE__ */ new Map();
|
|
669
|
-
for (const file of sourceResult.files) {
|
|
670
|
-
const content = fs2.readFileSync(file.fullPath, "utf-8");
|
|
671
|
-
const fileComponents = extractComponents(content);
|
|
672
|
-
for (const component of fileComponents) {
|
|
673
|
-
if (!components[component.id]) {
|
|
674
|
-
components[component.id] = [];
|
|
675
|
-
}
|
|
676
|
-
components[component.id].push({
|
|
677
|
-
filePath: file.outputPath,
|
|
678
|
-
line: component.line,
|
|
679
|
-
...component.doc ? { doc: component.doc } : {}
|
|
680
|
-
});
|
|
681
|
-
if (!sourceComponentIds.has(component.id)) {
|
|
682
|
-
sourceComponentIds.set(component.id, []);
|
|
683
|
-
}
|
|
684
|
-
sourceComponentIds.get(component.id).push(file.relativePath);
|
|
685
|
-
totalComponents++;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
const sourcePages = buildPages(
|
|
689
|
-
sourceResult.pageDocs,
|
|
690
|
-
sourceComponentIds,
|
|
691
|
-
source.rootDir
|
|
692
|
-
);
|
|
693
|
-
pages.push(...sourcePages);
|
|
694
|
-
const parsedFeatureDocs = /* @__PURE__ */ new Map();
|
|
695
|
-
for (const [dir, rawContent] of sourceResult.featureDocs) {
|
|
696
|
-
const { frontmatter, body } = parseFrontmatter(rawContent);
|
|
697
|
-
parsedFeatureDocs.set(dir, {
|
|
698
|
-
body,
|
|
699
|
-
explicitComponents: frontmatter.components ?? []
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
const sourceFeatures = buildFeatures(parsedFeatureDocs, source.rootDir);
|
|
703
|
-
features.push(...sourceFeatures);
|
|
704
|
-
}
|
|
705
|
-
return { components, pages, features, totalComponents, totalFiles };
|
|
706
|
-
}
|
|
707
|
-
function printSummary(result) {
|
|
708
|
-
console.log("");
|
|
709
|
-
const uniqueIds = Object.keys(result.components).length;
|
|
710
|
-
console.log(
|
|
711
|
-
`Found ${result.totalComponents} components with ${uniqueIds} unique IDs in ${result.totalFiles} files
|
|
712
|
-
`
|
|
713
|
-
);
|
|
714
|
-
if (uniqueIds > 0) {
|
|
715
|
-
console.log("Components found:");
|
|
716
|
-
for (const [id, locations] of Object.entries(result.components)) {
|
|
717
|
-
for (const loc of locations) {
|
|
718
|
-
console.log(` "${id}" at ${loc.filePath}:${loc.line}`);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
console.log("");
|
|
722
|
-
}
|
|
723
|
-
if (result.pages.length > 0) {
|
|
724
|
-
console.log(`Pages found: ${result.pages.length}`);
|
|
725
|
-
for (const page of result.pages) {
|
|
726
|
-
console.log(` ${page.dir}/ (${page.componentIds.length} components)`);
|
|
727
|
-
}
|
|
728
|
-
console.log("");
|
|
729
|
-
}
|
|
730
|
-
if (result.features.length > 0) {
|
|
731
|
-
console.log(`Features found: ${result.features.length}`);
|
|
732
|
-
for (const feature of result.features) {
|
|
733
|
-
console.log(` ${feature.dir}/ (${feature.componentIds.length} components)`);
|
|
734
|
-
}
|
|
735
|
-
console.log("");
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
function checkCoverage(result) {
|
|
739
|
-
const allComponentIds = Object.keys(result.components);
|
|
740
|
-
const coveredIds = /* @__PURE__ */ new Set([
|
|
741
|
-
...result.pages.flatMap((p) => p.componentIds),
|
|
742
|
-
...result.features.flatMap((f) => f.componentIds)
|
|
743
|
-
]);
|
|
744
|
-
const uncoveredIds = allComponentIds.filter((id) => !coveredIds.has(id));
|
|
745
|
-
const orphanedPages = result.pages.filter((p) => p.componentIds.length === 0);
|
|
746
|
-
let passed = true;
|
|
747
|
-
if (uncoveredIds.length > 0) {
|
|
748
|
-
passed = false;
|
|
749
|
-
console.log(`Components missing UIDEX_PAGE.md coverage (${uncoveredIds.length}):`);
|
|
750
|
-
for (const id of uncoveredIds.sort()) {
|
|
751
|
-
const locations = result.components[id];
|
|
752
|
-
for (const loc of locations) {
|
|
753
|
-
console.log(` "${id}" at ${loc.filePath}:${loc.line}`);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
console.log("");
|
|
757
|
-
}
|
|
758
|
-
if (orphanedPages.length > 0) {
|
|
759
|
-
passed = false;
|
|
760
|
-
console.log(`UIDEX_PAGE.md files with no components (${orphanedPages.length}):`);
|
|
761
|
-
for (const page of orphanedPages) {
|
|
762
|
-
console.log(` ${page.dir}/UIDEX_PAGE.md`);
|
|
763
|
-
}
|
|
764
|
-
console.log("");
|
|
765
|
-
}
|
|
766
|
-
if (passed) {
|
|
767
|
-
const totalDocs = result.pages.length + result.features.length;
|
|
768
|
-
console.log(
|
|
769
|
-
`All ${allComponentIds.length} components covered by ${totalDocs} UIDEX docs (${result.pages.length} pages, ${result.features.length} features)`
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
return passed;
|
|
773
|
-
}
|
|
774
|
-
function printConfig(config) {
|
|
775
|
-
console.log("Configuration:");
|
|
776
|
-
console.log(` Sources: ${config.sources.length}`);
|
|
777
|
-
for (const source of config.sources) {
|
|
778
|
-
const prefix = source.prefix ? ` (prefix: ${source.prefix})` : "";
|
|
779
|
-
console.log(` - ${source.rootDir}${prefix}`);
|
|
780
|
-
console.log(` Include: ${source.include.join(", ")}`);
|
|
781
|
-
if (source.exclude) {
|
|
782
|
-
console.log(` Exclude: ${source.exclude.join(", ")}`);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
console.log(` Global exclude: ${config.exclude.join(", ")}`);
|
|
786
|
-
console.log(` Output: ${config.outputPath}
|
|
787
|
-
`);
|
|
788
|
-
}
|
|
789
|
-
function scan() {
|
|
790
|
-
const isCheck = process.argv.includes("--check");
|
|
791
|
-
console.log(`uidex scanner${isCheck ? " (check mode)" : ""} starting...
|
|
792
|
-
`);
|
|
793
|
-
const config = loadConfig();
|
|
794
|
-
printConfig(config);
|
|
795
|
-
const result = runScan(config);
|
|
796
|
-
printSummary(result);
|
|
797
|
-
if (isCheck) {
|
|
798
|
-
const passed = checkCoverage(result);
|
|
799
|
-
process.exit(passed ? 0 : 1);
|
|
800
|
-
}
|
|
801
|
-
const outputPath = path2.resolve(process.cwd(), config.outputPath);
|
|
802
|
-
ensureOutputDir(outputPath);
|
|
803
|
-
const output = generateOutput(result.components, result.pages, result.features);
|
|
804
|
-
fs2.writeFileSync(outputPath, output, "utf-8");
|
|
805
|
-
console.log(`Generated: ${config.outputPath}`);
|
|
806
|
-
const testOutputPath = outputPath.replace(/\.ts$/, ".test.ts");
|
|
807
|
-
const testOutput = generateTestOutput(result.components, result.pages, result.features);
|
|
808
|
-
fs2.writeFileSync(testOutputPath, testOutput, "utf-8");
|
|
809
|
-
console.log(`Generated: ${config.outputPath.replace(/\.ts$/, ".test.ts")}`);
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// scripts/scaffold.ts
|
|
813
|
-
var fs3 = __toESM(require("fs"), 1);
|
|
814
|
-
var path3 = __toESM(require("path"), 1);
|
|
815
|
-
function toTestFileName(dir, title) {
|
|
816
|
-
if (title) {
|
|
817
|
-
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
818
|
-
}
|
|
819
|
-
const segments = dir.split("/").filter(Boolean);
|
|
820
|
-
return segments[segments.length - 1] || "unnamed";
|
|
821
|
-
}
|
|
822
|
-
function generateTestFile(entry, fixtureImport) {
|
|
823
|
-
const title = parseMarkdownTitle(entry.content) ?? entry.dir;
|
|
824
|
-
const criteria = parseAcceptanceCriteria(entry.content);
|
|
825
|
-
const componentList = entry.componentIds.map((id) => `"${id}"`).join(", ");
|
|
826
|
-
const docFile = entry.kind === "page" ? "UIDEX_PAGE.md" : "UIDEX_FEATURE.md";
|
|
827
|
-
let body = "";
|
|
828
|
-
body += `// Auto-generated by uidex scaffold from ${entry.dir}/${docFile}
|
|
829
|
-
`;
|
|
830
|
-
body += `// Fill in test implementations below
|
|
831
|
-
|
|
832
|
-
`;
|
|
833
|
-
body += `import { test, expect } from '${fixtureImport}';
|
|
834
|
-
|
|
835
|
-
`;
|
|
836
|
-
body += `test.describe('${title}', () => {
|
|
837
|
-
`;
|
|
838
|
-
if (entry.kind === "page") {
|
|
839
|
-
const route = formatRoute(entry.dir);
|
|
840
|
-
body += ` // Route: ${route}
|
|
841
|
-
`;
|
|
842
|
-
body += ` // Components: ${componentList}
|
|
843
|
-
|
|
844
|
-
`;
|
|
845
|
-
body += ` test.beforeEach(async ({ page }) => {
|
|
846
|
-
`;
|
|
847
|
-
body += ` await page.goto('${route}');
|
|
848
|
-
`;
|
|
849
|
-
body += ` });
|
|
850
|
-
`;
|
|
851
|
-
} else {
|
|
852
|
-
body += ` // Components: ${componentList}
|
|
853
|
-
`;
|
|
854
|
-
}
|
|
855
|
-
if (criteria.length > 0) {
|
|
856
|
-
body += "\n";
|
|
857
|
-
for (const criterion of criteria) {
|
|
858
|
-
const escaped = criterion.replace(/'/g, "\\'");
|
|
859
|
-
body += ` test.todo('${escaped}');
|
|
860
|
-
`;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
body += `});
|
|
864
|
-
`;
|
|
865
|
-
return body;
|
|
866
|
-
}
|
|
867
|
-
function scaffold(outputDir) {
|
|
868
|
-
const dir = outputDir ?? "e2e";
|
|
869
|
-
heading("uidex scaffold");
|
|
870
|
-
info(`Output directory: ${dir}/`);
|
|
871
|
-
const config = loadConfig();
|
|
872
|
-
const result = runScan(config);
|
|
873
|
-
if (result.pages.length === 0 && result.features.length === 0) {
|
|
874
|
-
warn("No pages or features found. Create UIDEX_PAGE.md or UIDEX_FEATURE.md files first.");
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
const absDir = path3.resolve(process.cwd(), dir);
|
|
878
|
-
if (!fs3.existsSync(absDir)) {
|
|
879
|
-
fs3.mkdirSync(absDir, { recursive: true });
|
|
880
|
-
}
|
|
881
|
-
const fixtureImport = "./fixtures";
|
|
882
|
-
let generated = 0;
|
|
883
|
-
let skipped = 0;
|
|
884
|
-
const entries = [
|
|
885
|
-
...result.pages.map((p) => ({ kind: "page", ...p })),
|
|
886
|
-
...result.features.map((f) => ({ kind: "feature", ...f }))
|
|
887
|
-
];
|
|
888
|
-
for (const entry of entries) {
|
|
889
|
-
const title = parseMarkdownTitle(entry.content);
|
|
890
|
-
const fileName = `${entry.kind}-${toTestFileName(entry.dir, title)}.spec.ts`;
|
|
891
|
-
const filePath = path3.join(absDir, fileName);
|
|
892
|
-
if (fs3.existsSync(filePath)) {
|
|
893
|
-
info(`Skipped ${fileName} (already exists)`);
|
|
894
|
-
skipped++;
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
const content = generateTestFile(entry, fixtureImport);
|
|
898
|
-
fs3.writeFileSync(filePath, content, "utf-8");
|
|
899
|
-
success(`Generated ${fileName}`);
|
|
900
|
-
generated++;
|
|
901
|
-
}
|
|
902
|
-
const fixturesPath = path3.join(absDir, "fixtures.ts");
|
|
903
|
-
if (!fs3.existsSync(fixturesPath)) {
|
|
904
|
-
fs3.writeFileSync(
|
|
905
|
-
fixturesPath,
|
|
906
|
-
`export { test, expect } from 'uidex/playwright';
|
|
907
|
-
`,
|
|
908
|
-
"utf-8"
|
|
909
|
-
);
|
|
910
|
-
success("Generated fixtures.ts");
|
|
911
|
-
generated++;
|
|
912
|
-
}
|
|
913
|
-
console.log("");
|
|
914
|
-
info(`${generated} file(s) generated, ${skipped} skipped`);
|
|
915
|
-
if (generated > 0) {
|
|
916
|
-
console.log("");
|
|
917
|
-
info("Next steps:");
|
|
918
|
-
console.log(" 1. Replace test.todo() calls with test implementations");
|
|
919
|
-
console.log(" 2. Use the uidex fixture for type-safe selectors:");
|
|
920
|
-
console.log("");
|
|
921
|
-
console.log(" test('add todo', async ({ uidex }) => {");
|
|
922
|
-
console.log(" await uidex('todo-input').fill('Buy milk');");
|
|
923
|
-
console.log(" await uidex('todo-add-button').click();");
|
|
924
|
-
console.log(" });");
|
|
925
|
-
console.log("");
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// scripts/claude-setup.ts
|
|
930
|
-
var fs4 = __toESM(require("fs"), 1);
|
|
931
|
-
var path4 = __toESM(require("path"), 1);
|
|
932
|
-
function readTemplate(filename) {
|
|
933
|
-
const candidates = [
|
|
934
|
-
// Built package: __dirname = <pkg>/dist/scripts → ../../claude/
|
|
935
|
-
path4.resolve(__dirname, "..", "..", "claude", filename),
|
|
936
|
-
// Dev mode: __dirname = <repo>/scripts → ../claude/
|
|
937
|
-
path4.resolve(__dirname, "..", "claude", filename)
|
|
938
|
-
];
|
|
939
|
-
for (const candidate of candidates) {
|
|
940
|
-
try {
|
|
941
|
-
return fs4.readFileSync(candidate, "utf-8");
|
|
942
|
-
} catch {
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
throw new Error(
|
|
946
|
-
`Template not found: ${filename}. The uidex package may be corrupted \u2014 try reinstalling.`
|
|
947
|
-
);
|
|
948
|
-
}
|
|
949
|
-
function ensureDir(dirPath) {
|
|
950
|
-
fs4.mkdirSync(dirPath, { recursive: true });
|
|
951
|
-
}
|
|
952
|
-
function addRules() {
|
|
953
|
-
const cwd = process.cwd();
|
|
954
|
-
const targetDir = path4.join(cwd, ".claude", "rules");
|
|
955
|
-
const targetPath = path4.join(targetDir, "uidex.md");
|
|
956
|
-
const content = readTemplate("rules.md");
|
|
957
|
-
ensureDir(targetDir);
|
|
958
|
-
fs4.writeFileSync(targetPath, content, "utf-8");
|
|
959
|
-
success("Added .claude/rules/uidex.md");
|
|
960
|
-
}
|
|
961
|
-
function removeRules() {
|
|
962
|
-
const targetPath = path4.join(process.cwd(), ".claude", "rules", "uidex.md");
|
|
963
|
-
try {
|
|
964
|
-
fs4.unlinkSync(targetPath);
|
|
965
|
-
success("Removed .claude/rules/uidex.md");
|
|
966
|
-
} catch (e) {
|
|
967
|
-
if (e.code === "ENOENT") {
|
|
968
|
-
info(".claude/rules/uidex.md does not exist");
|
|
969
|
-
} else {
|
|
970
|
-
throw e;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
function addSkill() {
|
|
975
|
-
const cwd = process.cwd();
|
|
976
|
-
const targetDir = path4.join(cwd, ".claude", "commands");
|
|
977
|
-
const targetPath = path4.join(targetDir, "uidex-audit.md");
|
|
978
|
-
const content = readTemplate("audit-command.md");
|
|
979
|
-
ensureDir(targetDir);
|
|
980
|
-
fs4.writeFileSync(targetPath, content, "utf-8");
|
|
981
|
-
success("Added .claude/commands/uidex-audit.md");
|
|
982
|
-
}
|
|
983
|
-
function removeSkill() {
|
|
984
|
-
const targetPath = path4.join(process.cwd(), ".claude", "commands", "uidex-audit.md");
|
|
985
|
-
try {
|
|
986
|
-
fs4.unlinkSync(targetPath);
|
|
987
|
-
success("Removed .claude/commands/uidex-audit.md");
|
|
988
|
-
} catch (e) {
|
|
989
|
-
if (e.code === "ENOENT") {
|
|
990
|
-
info(".claude/commands/uidex-audit.md does not exist");
|
|
991
|
-
} else {
|
|
992
|
-
throw e;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
var UIDEX_HOOK_COMMAND = "npx uidex scan --check";
|
|
997
|
-
function isUidexHookEntry(entry) {
|
|
998
|
-
return entry.hooks?.some((h) => h.command === UIDEX_HOOK_COMMAND) ?? false;
|
|
999
|
-
}
|
|
1000
|
-
function readSettings(settingsPath) {
|
|
1001
|
-
try {
|
|
1002
|
-
return JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
|
|
1003
|
-
} catch (e) {
|
|
1004
|
-
if (e.code === "ENOENT") {
|
|
1005
|
-
return {};
|
|
1006
|
-
}
|
|
1007
|
-
error(`Failed to parse .claude/settings.json: ${e.message}`);
|
|
1008
|
-
process.exit(1);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
function addHooks() {
|
|
1012
|
-
const cwd = process.cwd();
|
|
1013
|
-
const settingsDir = path4.join(cwd, ".claude");
|
|
1014
|
-
const settingsPath = path4.join(settingsDir, "settings.json");
|
|
1015
|
-
const settings = readSettings(settingsPath);
|
|
1016
|
-
if (!settings.hooks) {
|
|
1017
|
-
settings.hooks = {};
|
|
1018
|
-
}
|
|
1019
|
-
if (!Array.isArray(settings.hooks.PostToolUse)) {
|
|
1020
|
-
settings.hooks.PostToolUse = [];
|
|
1021
|
-
}
|
|
1022
|
-
if (settings.hooks.PostToolUse.some(isUidexHookEntry)) {
|
|
1023
|
-
info("uidex hook already configured in .claude/settings.json");
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
settings.hooks.PostToolUse.push({
|
|
1027
|
-
matcher: "Edit|Write",
|
|
1028
|
-
hooks: [
|
|
1029
|
-
{
|
|
1030
|
-
type: "command",
|
|
1031
|
-
command: UIDEX_HOOK_COMMAND,
|
|
1032
|
-
timeout: 30
|
|
1033
|
-
}
|
|
1034
|
-
]
|
|
1035
|
-
});
|
|
1036
|
-
ensureDir(settingsDir);
|
|
1037
|
-
fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
1038
|
-
success("Added uidex hook to .claude/settings.json");
|
|
1039
|
-
}
|
|
1040
|
-
function removeHooks() {
|
|
1041
|
-
const settingsPath = path4.join(process.cwd(), ".claude", "settings.json");
|
|
1042
|
-
const settings = readSettings(settingsPath);
|
|
1043
|
-
if (!Array.isArray(settings.hooks?.PostToolUse)) {
|
|
1044
|
-
info("No PostToolUse hooks configured");
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
const before = settings.hooks.PostToolUse.length;
|
|
1048
|
-
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
|
|
1049
|
-
(entry) => !isUidexHookEntry(entry)
|
|
1050
|
-
);
|
|
1051
|
-
const after = settings.hooks.PostToolUse.length;
|
|
1052
|
-
if (before === after) {
|
|
1053
|
-
info("No uidex hook found in .claude/settings.json");
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
if (settings.hooks.PostToolUse.length === 0) {
|
|
1057
|
-
delete settings.hooks.PostToolUse;
|
|
1058
|
-
}
|
|
1059
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
1060
|
-
delete settings.hooks;
|
|
1061
|
-
}
|
|
1062
|
-
fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
1063
|
-
success("Removed uidex hook from .claude/settings.json");
|
|
1064
|
-
}
|
|
1065
|
-
function claudeInit() {
|
|
1066
|
-
heading("uidex claude init");
|
|
1067
|
-
addRules();
|
|
1068
|
-
addSkill();
|
|
1069
|
-
addHooks();
|
|
1070
|
-
console.log("");
|
|
1071
|
-
success("Claude Code integration ready");
|
|
1072
|
-
}
|
|
1073
|
-
function claudeTeardown() {
|
|
1074
|
-
heading("uidex claude teardown");
|
|
1075
|
-
removeRules();
|
|
1076
|
-
removeSkill();
|
|
1077
|
-
removeHooks();
|
|
1078
|
-
console.log("");
|
|
1079
|
-
success("Claude Code integration removed");
|
|
1080
|
-
}
|
|
1081
|
-
function printClaudeHelp() {
|
|
1082
|
-
heading("uidex claude");
|
|
1083
|
-
console.log("Manage Claude Code integration for uidex.\n");
|
|
1084
|
-
console.log("Usage: uidex claude <command> [action]\n");
|
|
1085
|
-
console.log("Commands:");
|
|
1086
|
-
console.log(" init Add rules, skill, and hooks");
|
|
1087
|
-
console.log(" teardown Remove rules, skill, and hooks");
|
|
1088
|
-
console.log(" rules [add|remove] Manage .claude/rules/uidex.md");
|
|
1089
|
-
console.log(" skill [add|remove] Manage .claude/commands/uidex-audit.md");
|
|
1090
|
-
console.log(" hooks [add|remove] Manage .claude/settings.json hook");
|
|
1091
|
-
console.log("");
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// scripts/cli.ts
|
|
1095
|
-
function printHelp() {
|
|
1096
|
-
console.log(`
|
|
1097
|
-
${colors.bold}${colors.cyan}uidex${colors.reset}
|
|
1098
|
-
`);
|
|
1099
|
-
console.log("Usage: uidex <command>\n");
|
|
1100
|
-
console.log("Commands:");
|
|
1101
|
-
console.log(" init Initialize uidex in your project");
|
|
1102
|
-
console.log(" scan Run the component scanner");
|
|
1103
|
-
console.log(" scan --check Validate UIDEX_PAGE.md coverage");
|
|
1104
|
-
console.log(" scaffold [dir] Generate Playwright test stubs from pages/features");
|
|
1105
|
-
console.log(" claude Manage Claude Code integration");
|
|
1106
|
-
console.log("");
|
|
1107
|
-
}
|
|
1108
|
-
function parseAction(raw) {
|
|
1109
|
-
if (raw === void 0 || raw === "add") return "add";
|
|
1110
|
-
if (raw === "remove") return "remove";
|
|
1111
|
-
return null;
|
|
1112
|
-
}
|
|
1113
|
-
function handleClaude(args2) {
|
|
1114
|
-
const subcommand = args2[0];
|
|
1115
|
-
const action = parseAction(args2[1]);
|
|
1116
|
-
if (action === null) {
|
|
1117
|
-
console.error(`Unknown action: ${args2[1]}. Expected "add" or "remove".`);
|
|
1118
|
-
process.exit(1);
|
|
1119
|
-
}
|
|
1120
|
-
switch (subcommand) {
|
|
1121
|
-
case "init":
|
|
1122
|
-
claudeInit();
|
|
1123
|
-
break;
|
|
1124
|
-
case "teardown":
|
|
1125
|
-
claudeTeardown();
|
|
1126
|
-
break;
|
|
1127
|
-
case "rules":
|
|
1128
|
-
action === "remove" ? removeRules() : addRules();
|
|
1129
|
-
break;
|
|
1130
|
-
case "skill":
|
|
1131
|
-
action === "remove" ? removeSkill() : addSkill();
|
|
1132
|
-
break;
|
|
1133
|
-
case "hooks":
|
|
1134
|
-
action === "remove" ? removeHooks() : addHooks();
|
|
1135
|
-
break;
|
|
1136
|
-
default:
|
|
1137
|
-
printClaudeHelp();
|
|
1138
|
-
break;
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
var args = process.argv.slice(2);
|
|
1142
|
-
var command = args[0];
|
|
1143
|
-
switch (command) {
|
|
1144
|
-
case void 0:
|
|
1145
|
-
case "init":
|
|
1146
|
-
init().catch((err) => {
|
|
1147
|
-
console.error("Error during initialization:", err);
|
|
1148
|
-
process.exit(1);
|
|
1149
|
-
});
|
|
1150
|
-
break;
|
|
1151
|
-
case "scan":
|
|
1152
|
-
scan();
|
|
1153
|
-
break;
|
|
1154
|
-
case "scaffold":
|
|
1155
|
-
scaffold(args[1]);
|
|
1156
|
-
break;
|
|
1157
|
-
case "claude":
|
|
1158
|
-
handleClaude(args.slice(1));
|
|
1159
|
-
break;
|
|
1160
|
-
case "--help":
|
|
1161
|
-
case "-h":
|
|
1162
|
-
printHelp();
|
|
1163
|
-
break;
|
|
1164
|
-
default:
|
|
1165
|
-
console.error(`Unknown command: ${command}`);
|
|
1166
|
-
printHelp();
|
|
1167
|
-
process.exit(1);
|
|
1168
|
-
}
|