enterprise-ui-architect-cli 1.0.0 → 2.0.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/assets/SKILL.md +98 -0
- package/assets/commands/verify-i18n.ts +213 -0
- package/assets/commands/verify-imports.ts +256 -0
- package/assets/data/anti-patterns.csv +3 -0
- package/assets/data/charts.csv +1 -1
- package/assets/data/industries.csv +5 -5
- package/assets/data/pre-delivery-checklist.csv +6 -0
- package/assets/data/review-rubric.csv +2 -2
- package/assets/scripts/search.py +21 -9
- package/assets/src/index.ts +116 -0
- package/assets/templates/base/quick-reference.md +18 -0
- package/dist/commands/verify-i18n.d.ts +7 -0
- package/dist/commands/verify-i18n.d.ts.map +1 -0
- package/dist/commands/verify-i18n.js +175 -0
- package/dist/commands/verify-i18n.js.map +1 -0
- package/dist/commands/verify-imports.d.ts +7 -0
- package/dist/commands/verify-imports.d.ts.map +1 -0
- package/dist/commands/verify-imports.js +222 -0
- package/dist/commands/verify-imports.js.map +1 -0
- package/dist/index.js +42 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/assets/SKILL.md
CHANGED
|
@@ -512,6 +512,104 @@ Avoid:
|
|
|
512
512
|
- business-specific styling inside generic UI primitives
|
|
513
513
|
- using `item` prop on MUI v7 `Grid`
|
|
514
514
|
|
|
515
|
+
## Package Import Discipline
|
|
516
|
+
|
|
517
|
+
When adding or modifying imports in any module:
|
|
518
|
+
|
|
519
|
+
### 1. Verify Package Exists
|
|
520
|
+
Before writing a new import statement, check `package.json` (and `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml`) to confirm the package is installed.
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
# Quick check
|
|
524
|
+
grep '"package-name"' package.json
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### 2. If Package Is Missing
|
|
528
|
+
**Do NOT install automatically.** Present the user with:
|
|
529
|
+
- The missing package name
|
|
530
|
+
- The import path that triggered the need
|
|
531
|
+
- The recommended install command
|
|
532
|
+
- Ask for explicit confirmation
|
|
533
|
+
|
|
534
|
+
Example prompt:
|
|
535
|
+
```
|
|
536
|
+
⚠️ Missing package: @mui/x-data-grid
|
|
537
|
+
Required by: src/features/admin/components/DataTable.tsx
|
|
538
|
+
Install: npm install @mui/x-data-grid
|
|
539
|
+
Proceed? (y/n)
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### 3. Install Only After Confirmation
|
|
543
|
+
If user confirms, install with the project's package manager:
|
|
544
|
+
- Detect from lockfile: `package-lock.json` → npm, `yarn.lock` → yarn, `pnpm-lock.yaml` → pnpm
|
|
545
|
+
- Respect existing version constraints in `package.json`
|
|
546
|
+
- Update lockfile alongside install
|
|
547
|
+
|
|
548
|
+
### 4. Post-Install Verification
|
|
549
|
+
After install:
|
|
550
|
+
1. Run TypeScript check: `npx tsc --noEmit`
|
|
551
|
+
2. Verify the import resolves without error
|
|
552
|
+
3. Confirm no unused imports were added (tree-shaking friendly)
|
|
553
|
+
|
|
554
|
+
### 5. Anti-Pattern
|
|
555
|
+
- Adding imports for packages not in `package.json`
|
|
556
|
+
- Auto-installing without user consent
|
|
557
|
+
- Using different package managers within the same repo
|
|
558
|
+
- Adding unused dependencies "just in case"
|
|
559
|
+
|
|
560
|
+
## Translation Discipline (i18n)
|
|
561
|
+
|
|
562
|
+
When building multi-language admin dashboards with `next-intl`, `react-i18next`, or similar:
|
|
563
|
+
|
|
564
|
+
### 1. Always Use `t()` for User-Facing Text
|
|
565
|
+
Every string visible to users must go through the translation function. No hardcoded labels, buttons, placeholders, or error messages.
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
// ✅ Correct
|
|
569
|
+
<Button>{t("form.save")}</Button>
|
|
570
|
+
<CustomTextField label={t("auth.username")} />
|
|
571
|
+
|
|
572
|
+
// ❌ Wrong
|
|
573
|
+
<Button>Save</Button>
|
|
574
|
+
<CustomTextField label="Username" />
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### 2. Add Keys to All Locale Files
|
|
578
|
+
When you introduce a new `t("key")` call, you **must** add that key to **every** `messages/*.json` file before finishing.
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
# Verify all keys exist in every locale
|
|
582
|
+
enterprise-ui verify-i18n --src ./src
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### 3. Namespace Convention
|
|
586
|
+
Use `useTranslations("namespace")` for page-scoped keys. This keeps locale files organized and prevents key collisions.
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
const t = useTranslations("admin.users");
|
|
590
|
+
// keys become: admin.users.title, admin.users.column.name, etc.
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### 4. Dynamic Keys
|
|
594
|
+
Avoid dynamic template literals for translation keys when possible:
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
// ✅ Prefer explicit mapping
|
|
598
|
+
const statusKey = status === "active" ? "status.active" : "status.inactive";
|
|
599
|
+
<Chip label={t(statusKey)} />
|
|
600
|
+
|
|
601
|
+
// ❌ Avoid
|
|
602
|
+
<Chip label={t(`status.${status}`)} />
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
If dynamic keys are unavoidable, ensure all possible values are documented and present in locale files.
|
|
606
|
+
|
|
607
|
+
### 5. Pre-Delivery Check
|
|
608
|
+
Before marking a feature complete:
|
|
609
|
+
- [ ] All `t()` keys exist in every locale file
|
|
610
|
+
- [ ] No hardcoded user-facing strings remain
|
|
611
|
+
- [ ] `enterprise-ui verify-i18n` passes with zero missing keys
|
|
612
|
+
|
|
515
613
|
## Keyboard Navigation
|
|
516
614
|
MUI components require specific keyboard patterns:
|
|
517
615
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import { resolve, dirname, join, sep, extname } from "path";
|
|
3
|
+
|
|
4
|
+
interface VerifyOptions {
|
|
5
|
+
srcDir: string;
|
|
6
|
+
messagesDir: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface KeyLocation {
|
|
10
|
+
key: string;
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function findMessagesDir(startDir: string): string | null {
|
|
16
|
+
let dir = resolve(startDir);
|
|
17
|
+
while (dir !== dirname(dir)) {
|
|
18
|
+
const messagesPath = join(dir, "messages");
|
|
19
|
+
if (existsSync(messagesPath)) return messagesPath;
|
|
20
|
+
dir = dirname(dir);
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getSourceFiles(dir: string): string[] {
|
|
26
|
+
const results: string[] = [];
|
|
27
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const fullPath = join(dir, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
if (
|
|
33
|
+
entry.name === "node_modules" ||
|
|
34
|
+
entry.name === "dist" ||
|
|
35
|
+
entry.name === "build" ||
|
|
36
|
+
entry.name === ".next" ||
|
|
37
|
+
entry.name.startsWith(".")
|
|
38
|
+
) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
results.push(...getSourceFiles(fullPath));
|
|
42
|
+
} else if (entry.isFile()) {
|
|
43
|
+
const ext = extname(entry.name);
|
|
44
|
+
if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") {
|
|
45
|
+
results.push(fullPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function loadTranslations(messagesDir: string): Record<string, Record<string, unknown>> {
|
|
54
|
+
const files = readdirSync(messagesDir).filter((f) => f.endsWith(".json"));
|
|
55
|
+
const translations: Record<string, Record<string, unknown>> = {};
|
|
56
|
+
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const locale = file.replace(".json", "");
|
|
59
|
+
const content = readFileSync(join(messagesDir, file), "utf-8");
|
|
60
|
+
translations[locale] = JSON.parse(content);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return translations;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
67
|
+
const parts = path.split(".");
|
|
68
|
+
let current: unknown = obj;
|
|
69
|
+
|
|
70
|
+
for (const part of parts) {
|
|
71
|
+
if (current === null || current === undefined) return undefined;
|
|
72
|
+
if (typeof current !== "object") return undefined;
|
|
73
|
+
current = (current as Record<string, unknown>)[part];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return current;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractNamespace(content: string): string | null {
|
|
80
|
+
const match = /useTranslations\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/.exec(content);
|
|
81
|
+
return match ? match[1] : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isInsideStringLiteral(line: string, matchIndex: number): boolean {
|
|
85
|
+
// Check if the character before t( is inside a string literal
|
|
86
|
+
const before = line.slice(0, matchIndex);
|
|
87
|
+
let inSingle = false;
|
|
88
|
+
let inDouble = false;
|
|
89
|
+
let escaped = false;
|
|
90
|
+
|
|
91
|
+
for (const ch of before) {
|
|
92
|
+
if (escaped) {
|
|
93
|
+
escaped = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (ch === "\\") {
|
|
97
|
+
escaped = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (ch === '"' && !inSingle) {
|
|
101
|
+
inDouble = !inDouble;
|
|
102
|
+
} else if (ch === "'" && !inDouble) {
|
|
103
|
+
inSingle = !inSingle;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return inSingle || inDouble;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractTranslationKeys(content: string, namespace: string | null): KeyLocation[] {
|
|
111
|
+
const lines = content.split("\n");
|
|
112
|
+
const keys: KeyLocation[] = [];
|
|
113
|
+
const seen = new Set<string>();
|
|
114
|
+
|
|
115
|
+
const regex = /\bt\s*\(\s*["'`]([a-zA-Z0-9_.-]+)["'`]/g;
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < lines.length; i++) {
|
|
118
|
+
const line = lines[i];
|
|
119
|
+
// Skip comment-only lines
|
|
120
|
+
const codePart = line.split("//")[0];
|
|
121
|
+
if (!codePart.includes("t(")) continue;
|
|
122
|
+
|
|
123
|
+
let match: RegExpExecArray | null;
|
|
124
|
+
while ((match = regex.exec(line)) !== null) {
|
|
125
|
+
if (isInsideStringLiteral(line, match.index)) continue;
|
|
126
|
+
|
|
127
|
+
const rawKey = match[1];
|
|
128
|
+
const key = namespace ? `${namespace}.${rawKey}` : rawKey;
|
|
129
|
+
if (!seen.has(key)) {
|
|
130
|
+
seen.add(key);
|
|
131
|
+
keys.push({ key, file: "", line: i + 1 });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return keys;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function verifyI18nCommand(options: Partial<VerifyOptions> = {}): void {
|
|
140
|
+
const srcDir = options.srcDir || process.cwd();
|
|
141
|
+
const messagesDir = options.messagesDir || findMessagesDir(srcDir);
|
|
142
|
+
|
|
143
|
+
if (!messagesDir) {
|
|
144
|
+
console.error("❌ messages/ directory not found. Run this from a project root.");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const translations = loadTranslations(messagesDir);
|
|
149
|
+
const locales = Object.keys(translations);
|
|
150
|
+
|
|
151
|
+
if (locales.length === 0) {
|
|
152
|
+
console.error("❌ No translation files found in messages/ directory.");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const files = getSourceFiles(srcDir);
|
|
157
|
+
const allKeys: Map<string, Array<{ file: string; line: number }>> = new Map();
|
|
158
|
+
|
|
159
|
+
for (const file of files) {
|
|
160
|
+
const content = readFileSync(file, "utf-8");
|
|
161
|
+
const namespace = extractNamespace(content);
|
|
162
|
+
const keys = extractTranslationKeys(content, namespace);
|
|
163
|
+
for (const { key, line } of keys) {
|
|
164
|
+
const list = allKeys.get(key) || [];
|
|
165
|
+
list.push({ file, line });
|
|
166
|
+
allKeys.set(key, list);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (allKeys.size === 0) {
|
|
171
|
+
console.log("ℹ️ No translation keys found in source files.");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const missingByLocale: Map<string, KeyLocation[]> = new Map();
|
|
176
|
+
|
|
177
|
+
for (const [key, locations] of allKeys) {
|
|
178
|
+
for (const locale of locales) {
|
|
179
|
+
const value = getNestedValue(translations[locale], key);
|
|
180
|
+
if (value === undefined) {
|
|
181
|
+
const list = missingByLocale.get(locale) || [];
|
|
182
|
+
list.push({ key, file: locations[0].file, line: locations[0].line });
|
|
183
|
+
missingByLocale.set(locale, list);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const totalMissing = Array.from(missingByLocale.values()).reduce((sum, list) => sum + list.length, 0);
|
|
189
|
+
|
|
190
|
+
if (totalMissing === 0) {
|
|
191
|
+
console.log(`✅ All ${allKeys.size} translation key(s) exist in every locale (${locales.join(", ")}).`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log(`⚠️ Found ${totalMissing} missing translation key(s):\n`);
|
|
196
|
+
|
|
197
|
+
for (const [locale, missing] of missingByLocale) {
|
|
198
|
+
if (missing.length === 0) continue;
|
|
199
|
+
console.log(` 🌐 ${locale}.json (${missing.length} missing):`);
|
|
200
|
+
for (const { key, file, line } of missing.slice(0, 10)) {
|
|
201
|
+
const relPath = file.replace(srcDir + sep, "").replace(/^\//, "");
|
|
202
|
+
console.log(` - ${key}`);
|
|
203
|
+
console.log(` used in: ${relPath}:${line}`);
|
|
204
|
+
}
|
|
205
|
+
if (missing.length > 10) {
|
|
206
|
+
console.log(` ... and ${missing.length - 10} more`);
|
|
207
|
+
}
|
|
208
|
+
console.log("");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(`Add the missing keys to all ${locales.length} locale files.\n`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import { resolve, dirname, join, sep, extname } from "path";
|
|
3
|
+
|
|
4
|
+
interface VerifyOptions {
|
|
5
|
+
srcDir: string;
|
|
6
|
+
packageJsonPath: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ImportInfo {
|
|
10
|
+
path: string;
|
|
11
|
+
line: number;
|
|
12
|
+
source: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const NODE_BUILTINS = new Set([
|
|
16
|
+
"assert", "async_hooks", "buffer", "child_process", "cluster", "console",
|
|
17
|
+
"constants", "crypto", "dgram", "diagnostics_channel", "dns", "domain",
|
|
18
|
+
"events", "fs", "http", "http2", "https", "inspector", "module", "net",
|
|
19
|
+
"os", "path", "perf_hooks", "process", "punycode", "querystring", "readline",
|
|
20
|
+
"repl", "stream", "string_decoder", "sys", "timers", "timers/promises",
|
|
21
|
+
"tls", "trace_events", "tty", "url", "util", "v8", "vm", "wasi", "worker_threads",
|
|
22
|
+
"zlib", "node:test",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function findPackageJson(startDir: string): string | null {
|
|
26
|
+
let dir = resolve(startDir);
|
|
27
|
+
while (dir !== dirname(dir)) {
|
|
28
|
+
const pkgPath = join(dir, "package.json");
|
|
29
|
+
if (existsSync(pkgPath)) return pkgPath;
|
|
30
|
+
dir = dirname(dir);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findTsConfig(startDir: string): string | null {
|
|
36
|
+
let dir = resolve(startDir);
|
|
37
|
+
while (dir !== dirname(dir)) {
|
|
38
|
+
const tsPath = join(dir, "tsconfig.json");
|
|
39
|
+
if (existsSync(tsPath)) return tsPath;
|
|
40
|
+
dir = dirname(dir);
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getInstalledPackages(packageJsonPath: string): Set<string> {
|
|
46
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
47
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
48
|
+
const devDeps = Object.keys(pkg.devDependencies || {});
|
|
49
|
+
const peerDeps = Object.keys(pkg.peerDependencies || {});
|
|
50
|
+
const optionalDeps = Object.keys(pkg.optionalDependencies || {});
|
|
51
|
+
return new Set([...deps, ...devDeps, ...peerDeps, ...optionalDeps]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getPathAliases(tsConfigPath: string): string[] {
|
|
55
|
+
try {
|
|
56
|
+
const raw = readFileSync(tsConfigPath, "utf-8");
|
|
57
|
+
// Extract path aliases via regex — tolerant to comments and trailing commas
|
|
58
|
+
const aliases: string[] = [];
|
|
59
|
+
// Match patterns like "@core/*" or "@/components/*" inside "paths": { ... }
|
|
60
|
+
const pathsMatch = raw.match(/"paths"\s*:\s*\{([\s\S]*?)\}/);
|
|
61
|
+
if (pathsMatch) {
|
|
62
|
+
const pathsBlock = pathsMatch[1];
|
|
63
|
+
const keyRegex = /"(@[^"]+)"\s*:/g;
|
|
64
|
+
let m: RegExpExecArray | null;
|
|
65
|
+
while ((m = keyRegex.exec(pathsBlock)) !== null) {
|
|
66
|
+
aliases.push(m[1]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return aliases.length > 0 ? aliases : ["@/*"];
|
|
70
|
+
} catch {
|
|
71
|
+
return ["@/*"];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isPathAlias(source: string, aliases: string[]): boolean {
|
|
76
|
+
for (const alias of aliases) {
|
|
77
|
+
const prefix = alias.replace(/\/\*$/, "");
|
|
78
|
+
if (alias.endsWith("/*")) {
|
|
79
|
+
if (source === prefix || source.startsWith(prefix + "/")) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
if (source === alias) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getSourceFiles(dir: string): string[] {
|
|
92
|
+
const results: string[] = [];
|
|
93
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
94
|
+
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const fullPath = join(dir, entry.name);
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
if (
|
|
99
|
+
entry.name === "node_modules" ||
|
|
100
|
+
entry.name === "dist" ||
|
|
101
|
+
entry.name === "build" ||
|
|
102
|
+
entry.name === ".next" ||
|
|
103
|
+
entry.name.startsWith(".")
|
|
104
|
+
) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
results.push(...getSourceFiles(fullPath));
|
|
108
|
+
} else if (entry.isFile()) {
|
|
109
|
+
const ext = extname(entry.name);
|
|
110
|
+
if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") {
|
|
111
|
+
results.push(fullPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractImports(filePath: string): ImportInfo[] {
|
|
120
|
+
const content = readFileSync(filePath, "utf-8");
|
|
121
|
+
const lines = content.split("\n");
|
|
122
|
+
const imports: ImportInfo[] = [];
|
|
123
|
+
|
|
124
|
+
// Matches single-line imports: import ... from "..."
|
|
125
|
+
const singleLineRegex = /^(?:import\s+.*?from\s+|import\s*\(|require\s*\()["']([^"';]+)["'];?/;
|
|
126
|
+
|
|
127
|
+
// Matches multi-line import end: from "..."
|
|
128
|
+
const multiLineEndRegex = /from\s+["']([^"';]+)["'];?/;
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
131
|
+
const line = lines[i].trim();
|
|
132
|
+
const singleMatch = singleLineRegex.exec(line);
|
|
133
|
+
if (singleMatch) {
|
|
134
|
+
imports.push({ path: filePath, line: i + 1, source: singleMatch[1] });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Multi-line import: starts with "import" but no "from" on same line
|
|
138
|
+
if (line.startsWith("import") && !line.includes("from") && !line.includes("(")) {
|
|
139
|
+
// Look ahead up to 5 lines for "from '...'"
|
|
140
|
+
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
141
|
+
const nextLine = lines[j].trim();
|
|
142
|
+
const multiMatch = multiLineEndRegex.exec(nextLine);
|
|
143
|
+
if (multiMatch) {
|
|
144
|
+
imports.push({ path: filePath, line: j + 1, source: multiMatch[1] });
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (nextLine.endsWith(";")) break; // End of import without from
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return imports;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolvePackageName(source: string): string | null {
|
|
156
|
+
if (source.startsWith(".") || source.startsWith("/")) return null;
|
|
157
|
+
if (source.startsWith("node:")) return null;
|
|
158
|
+
if (source.startsWith("@/")) return null;
|
|
159
|
+
|
|
160
|
+
if (source.startsWith("@")) {
|
|
161
|
+
const parts = source.split("/");
|
|
162
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const idx = source.indexOf("/");
|
|
166
|
+
return idx > 0 ? source.slice(0, idx) : source;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isNodeBuiltin(pkg: string): boolean {
|
|
170
|
+
return NODE_BUILTINS.has(pkg) || NODE_BUILTINS.has(`node:${pkg}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function detectPackageManager(lockFiles: string[]): "npm" | "yarn" | "pnpm" | "bun" | "unknown" {
|
|
174
|
+
if (lockFiles.some((f) => f.endsWith("bun.lockb"))) return "bun";
|
|
175
|
+
if (lockFiles.some((f) => f.endsWith("pnpm-lock.yaml"))) return "pnpm";
|
|
176
|
+
if (lockFiles.some((f) => f.endsWith("yarn.lock"))) return "yarn";
|
|
177
|
+
if (lockFiles.some((f) => f.endsWith("package-lock.json"))) return "npm";
|
|
178
|
+
return "unknown";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function verifyImportsCommand(options: Partial<VerifyOptions> = {}): void {
|
|
182
|
+
const srcDir = options.srcDir || process.cwd();
|
|
183
|
+
const packageJsonPath = options.packageJsonPath || findPackageJson(srcDir);
|
|
184
|
+
|
|
185
|
+
if (!packageJsonPath) {
|
|
186
|
+
console.error("❌ package.json not found. Run this from a project root.");
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const installed = getInstalledPackages(packageJsonPath);
|
|
191
|
+
const projectRoot = dirname(packageJsonPath);
|
|
192
|
+
|
|
193
|
+
const tsConfigPath = findTsConfig(srcDir);
|
|
194
|
+
const pathAliases = tsConfigPath ? getPathAliases(tsConfigPath) : [];
|
|
195
|
+
|
|
196
|
+
const lockFiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb"].map((f) =>
|
|
197
|
+
join(projectRoot, f)
|
|
198
|
+
);
|
|
199
|
+
const existingLocks = lockFiles.filter((f) => existsSync(f));
|
|
200
|
+
const pkgManager = detectPackageManager(existingLocks);
|
|
201
|
+
|
|
202
|
+
const files = getSourceFiles(srcDir);
|
|
203
|
+
|
|
204
|
+
const allImports: ImportInfo[] = [];
|
|
205
|
+
for (const file of files) {
|
|
206
|
+
allImports.push(...extractImports(file));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const missing: Map<string, ImportInfo[]> = new Map();
|
|
210
|
+
|
|
211
|
+
for (const imp of allImports) {
|
|
212
|
+
if (isPathAlias(imp.source, pathAliases)) continue;
|
|
213
|
+
|
|
214
|
+
const pkg = resolvePackageName(imp.source);
|
|
215
|
+
if (!pkg) continue;
|
|
216
|
+
if (isNodeBuiltin(pkg)) continue;
|
|
217
|
+
if (installed.has(pkg)) continue;
|
|
218
|
+
|
|
219
|
+
const list = missing.get(pkg) || [];
|
|
220
|
+
list.push(imp);
|
|
221
|
+
missing.set(pkg, list);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (missing.size === 0) {
|
|
225
|
+
console.log(`✅ All imports resolve to installed packages (${allImports.length} imports checked).`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(`⚠️ Found ${missing.size} missing package(s):\n`);
|
|
230
|
+
|
|
231
|
+
for (const [pkg, imports] of missing) {
|
|
232
|
+
console.log(` 📦 ${pkg}`);
|
|
233
|
+
console.log(` Used in:`);
|
|
234
|
+
for (const imp of imports.slice(0, 3)) {
|
|
235
|
+
const relPath = imp.path.replace(projectRoot + sep, "").replace(/^\//, "");
|
|
236
|
+
console.log(` - ${relPath}:${imp.line} → import from "${imp.source}"`);
|
|
237
|
+
}
|
|
238
|
+
if (imports.length > 3) {
|
|
239
|
+
console.log(` ... and ${imports.length - 3} more`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const installCmd =
|
|
243
|
+
pkgManager === "bun"
|
|
244
|
+
? `bun add ${pkg}`
|
|
245
|
+
: pkgManager === "pnpm"
|
|
246
|
+
? `pnpm add ${pkg}`
|
|
247
|
+
: pkgManager === "yarn"
|
|
248
|
+
? `yarn add ${pkg}`
|
|
249
|
+
: `npm install ${pkg}`;
|
|
250
|
+
|
|
251
|
+
console.log(` Install: ${installCmd}\n`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(`Run the install command(s) above, then re-run this check.\n`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
@@ -53,3 +53,6 @@ id,area,bad_pattern,why_bad,fix
|
|
|
53
53
|
52,Charts,No empty state for charts with zero data points,Empty chart looks broken or crashes chart library,Show empty state illustration or zero baseline when no data
|
|
54
54
|
53,Charts,Using real-time updates without transition animations,Jarring jumps in chart data poor perceived performance,Use Recharts animation or smooth transitions for live data updates
|
|
55
55
|
54,Charts,Fetching all chart data on every small filter change,Excessive API calls slow performance backend overload,Debounce filter changes use TanStack Query staleTime cache filter state in URL
|
|
56
|
+
55,Dependencies,Adding new imports without verifying the package is installed,Build fails in CI or for teammates TypeScript errors runtime crashes silent failures,Always check package.json before adding an import If package is missing ask user for confirmation then install with the correct package manager
|
|
57
|
+
57,Dependencies,Adding translation keys in source code without updating all locale files,Users see raw keys or missing text in unsupported languages breaks i18n contract,When adding t(key) always add the key to every messages locale file Run verify-i18n to check
|
|
58
|
+
56,i18n,Adding translation keys in code without updating all locale files,Users see raw keys or missing text in unsupported languages breaks i18n contract,When adding t(key) always add the key to every messages locale file Run verify-i18n to check
|
package/assets/data/charts.csv
CHANGED
|
@@ -11,7 +11,7 @@ id,chart_type,library,use_case,admin_context,best_for_data,avoid_when
|
|
|
11
11
|
10,Heatmap,ApexCharts / MUI X-Charts,Density matrix,User activity by hour/day correlation matrix,Finding patterns in dense data,Simple sparse data
|
|
12
12
|
11,Scatter Plot,Recharts / ApexCharts,Correlation analysis,Price vs sales customer age vs spend,Finding relationships clusters outliers,Single variable data
|
|
13
13
|
12,Bubble Chart,Recharts / ApexCharts,3-variable correlation,Deal size vs probability vs revenue impact,Adding size dimension to scatter,Too many bubbles overlap
|
|
14
|
-
13,Candlestick,ApexCharts
|
|
14
|
+
13,Candlestick,ApexCharts,Financial OHLC,Stock price crypto forex trading data,Open high low close financial data,Non-financial contexts
|
|
15
15
|
14,Sparkline,Recharts / ApexCharts,Mini trend inline,Table row mini chart card header trend,Quick trend in small space,Detailed analysis needed
|
|
16
16
|
15,Gauge Chart,ApexCharts / MUI X-Charts,Single KPI progress,CPU usage disk space quota utilization,0-100% progress toward goal,Multiple metrics
|
|
17
17
|
16,Funnel Chart,Recharts / ApexCharts,Conversion stages,Sales pipeline recruitment funnel drop-off,Sequential stages with drop-off,Non-sequential data
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
id,industry,display_name,pattern,style_priority,color_mood,typography_mood,key_effects,anti_patterns
|
|
2
2
|
1,saas,SaaS / B2B Platform,Feature-Rich Dashboard + Onboarding,bento-grid minimalism soft-ui,Clean blues + white + accent purple,Inter / Public Sans sans-serif,Micro-interactions smooth transitions 200ms subtle shadows,Skeuomorphism bright neon colors cluttered hero slow animations
|
|
3
|
-
2,fintech,Fintech / Banking,Data-Dense Dashboard + Transaction Table,minimalism dark-mode
|
|
3
|
+
2,fintech,Fintech / Banking,Data-Dense Dashboard + Transaction Table,minimalism dark-mode glassmorphism,Deep navy + emerald green + gold accents,Plus Jakarta Sans / DM Sans modern geometric,Real-time data updates subtle pulse animations card hover elevation,Playful colors rounded corners everywhere emoji icons insufficient contrast
|
|
4
4
|
3,healthcare,Healthcare / Medical,Patient List + Appointment Calendar + Charts,minimalism accessible soft-ui,Soft teal + white + warm gray + alert red,Open Sans / Roboto highly readable,Clear visual hierarchy status badges with icons gentle transitions,Small text poor contrast complex medical jargon without tooltips missing loading states
|
|
5
5
|
4,ecommerce,E-commerce Admin,Order Management + Inventory Grid + Sales Charts,bento-grid data-dense,Orange accent + white + dark sidebar + success green,Inter / Source Sans Pro clean functional,Quick actions contextual menus real-time stock indicators,Bright sales-y colors distracting animations missing empty states for out-of-stock
|
|
6
|
-
5,logistics,Logistics / Supply Chain,Map + Shipment Tracker + Fleet Table,real-time
|
|
6
|
+
5,logistics,Logistics / Supply Chain,Map + Shipment Tracker + Fleet Table,real-time glassmorphism,Deep blue + bright cyan + warning amber + map green,Roboto Mono / Inter monospace for tracking IDs,Map integrations timeline views live status indicators,Cluttered maps poor mobile experience missing offline indicators
|
|
7
7
|
6,hr,HR / People Management,Employee Directory + Org Chart + Payroll Table,minimalism soft-ui bento-grid,Warm purple + soft gray + white + status colors,Work Sans / Lato friendly professional,Profile cards org tree visualizations approval workflows,Overly casual fonts missing privacy indicators complex navigation too many clicks
|
|
8
8
|
7,crm,CRM / Sales,Pipeline Board + Contact List + Activity Feed,bento-grid soft-ui,Electric blue + white + warm gray + deal stage colors,Inter / SF Pro clean crisp,Drag-drop pipeline activity timelines win/loss indicators,Missing empty pipeline states poor mobile card layout overwhelming notifications
|
|
9
9
|
8,erp,ERP / Manufacturing,Production Dashboard + Inventory + BOM Table,data-dense executive,Industrial gray + safety orange + machine blue + alert red,IBM Plex Sans / Roboto technical precise,Machine status gauges production line visuals alert banners,Overly decorative fonts cluttered tables missing real-time indicators slow refresh
|
|
10
10
|
9,education,Education / LMS,Course List + Student Progress + Gradebook,minimalism accessible soft-ui,Academic blue + warm white + success green + caution amber,Lora / Open Sans readable elegant,Progress bars achievement badges calendar views,Childish fonts excessive gamification poor accessibility missing progress persistence
|
|
11
|
-
10,government,Government / Public Sector,Case Management + Document List + Reporting,minimalism accessible
|
|
12
|
-
11,cybersecurity,Cybersecurity / SOC,Alert Feed + Threat Map + Incident Table,dark-mode
|
|
11
|
+
10,government,Government / Public Sector,Case Management + Document List + Reporting,minimalism accessible,Official blue + white + gray + priority red,Merriweather / Open Sans formal trustworthy,Clear status workflows document version tracking audit trails,Political colors overly modern flashy design missing WCAG compliance complex language
|
|
12
|
+
11,cybersecurity,Cybersecurity / SOC,Alert Feed + Threat Map + Incident Table,dark-mode ai-native,Deep black + alert red + cyber cyan + warning amber,Fira Code / Inter monospace for logs technical,Dark theme real-time alerts threat level indicators SOC timeline,Light theme by default poor alert visibility cluttered dashboards missing severity colors
|
|
13
13
|
12,real-estate,Real Estate / Property,Property Grid + Map + Lead Pipeline,bento-grid soft-ui,Premium navy + gold accent + white + status green,Playfair Display / Inter luxury clean,Property cards map pins lead scoring visual comparison tools,Excessive imagery poor table performance missing price formatting cluttered filters
|
|
14
|
-
13,energy,Energy / Utilities,Grid Monitor + Meter Readings + Outage Map,real-time
|
|
14
|
+
13,energy,Energy / Utilities,Grid Monitor + Meter Readings + Outage Map,real-time data-dense,Power blue + grid yellow + outage red + eco green,Roboto / Source Sans Pro technical functional,Real-time gauges geographic outage maps consumption charts,Missing time-series data poor mobile map experience slow data refresh
|
|
15
15
|
14,media,Media / Content Management,Asset Library + Editorial Calendar + Analytics,minimalism bento-grid,Dark charcoal + vibrant accent + white + video red,Montserrat / Inter modern dynamic,Media previews drag-drop upload editorial timeline engagement charts,Cluttered asset grids missing metadata poor search missing preview thumbnails
|
|
16
16
|
15,nonprofit,Nonprofit / NGO,Donor CRM + Campaign Tracker + Impact Dashboard,soft-ui accessible minimalism,Hope blue + growth green + warm white + heart red,Merriweather / Lora trustworthy warm,Donor profiles campaign progress impact visualization volunteer management,Overly corporate design missing donation CTAs poor mobile donation flow complex reporting
|
|
@@ -34,3 +34,9 @@ id,page_type,check,severity,why_it_matters
|
|
|
34
34
|
33,auth,Password visibility toggle present,Medium,Improves UX for password entry
|
|
35
35
|
34,auth,Form focuses first input on load,Low,Small UX improvement for keyboard users
|
|
36
36
|
35,auth,Social login buttons match brand colors,Low,Visual consistency with external services
|
|
37
|
+
36,dependencies,All new imports resolve to installed packages,High,Missing packages break builds and CI
|
|
38
|
+
37,dependencies,No unused imports or dead dependencies,Medium,Bloats bundle and confuses developers
|
|
39
|
+
38,dependencies,Package manager lockfile is in sync with package.json,High,Prevents inconsistent installs across environments
|
|
40
|
+
39,i18n,All translation keys exist in every locale file,High,Missing keys show raw text to users
|
|
41
|
+
40,i18n,No hardcoded user-facing strings outside t() calls,High,Breaks multi-language support
|
|
42
|
+
41,i18n,New translation keys added to all messages files before commit,High,Prevents incomplete translations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
id,category,score_1_3,score_4_6,score_7_8,score_9_10
|
|
2
|
-
1,Premium Admin Visual Quality,Layout is broken or unrecognizable as admin UI; no cards; random spacing,Cards present but inconsistent; spacing varies; some hierarchy; looks like generic
|
|
3
|
-
2,
|
|
2
|
+
1,Premium Admin Visual Quality,Layout is broken or unrecognizable as admin UI; no cards; random spacing,Cards present but inconsistent; spacing varies; some hierarchy; looks like generic MUI,Clean card hierarchy; consistent spacing; feels like admin panel; status chips styled,Premium Enterprise admin feel; balanced rhythm; excellent hierarchy; polished status and actions; visually cohesive
|
|
3
|
+
2,MUI Architecture Quality,No typed props; inline everything; no reusable components; raw MUI dumped,Some abstractions; partial types; mixed patterns; inconsistent API surfaces,Good component boundaries; typed props; consistent Form/Table patterns; token usage,Excellent architecture; predictable APIs; disciplined abstractions; token-first; fully typed
|
|
4
4
|
3,Component Reusability,Everything inline; copy-paste patterns; no shared UI primitives,Some shared components but inconsistent props; magic numbers; hardcoded text,Reusable PageLayout Card Table Form primitives; configurable via props,Highly reusable system; composable patterns; design-token driven; minimal duplication
|
|
5
5
|
4,Form Quality,No validation; no loading; no feedback; inline styles; missing labels,Basic validation; some feedback; inconsistent layout; partial loading state,Full typed Form; validation rules; helper text; dirty guard; responsive grid,Enterprise form discipline; accessible labels; clear errors; submit loading; cancel/reset; sectioned layout
|
|
6
6
|
5,Table Quality,No pagination; no sorting; no rowKey; no empty state; inline columns,Basic table with some features; missing loading or error state; hardcoded columns,Typed columns; loading/empty/error; pagination; sorting; row actions; responsive plan,Production table standard; server-side ready; accessible headers; status tags; formatted values; bulk actions
|