uilint 0.2.7 → 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 +279 -4407
- 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,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/install/constants.ts
|
|
4
|
+
var GENSTYLEGUIDE_COMMAND_MD = `# React Style Guide Generator
|
|
5
|
+
|
|
6
|
+
Analyze the React UI codebase to produce a **prescriptive, semantic** style guide. Focus on consistency, intent, and relationships\u2014not specific values.
|
|
7
|
+
|
|
8
|
+
## Philosophy
|
|
9
|
+
|
|
10
|
+
1. **Identify the intended architecture** from the best patterns in use
|
|
11
|
+
2. **Prescribe semantic rules** \u2014 about consistency and relationships, not pixels
|
|
12
|
+
3. **Stay general** \u2014 "primary buttons should be visually consistent" not "buttons use px-4"
|
|
13
|
+
4. **Focus on intent** \u2014 what should FEEL the same, not what values to use
|
|
14
|
+
|
|
15
|
+
## Analysis Steps
|
|
16
|
+
|
|
17
|
+
### 1. Detect the Stack
|
|
18
|
+
- Framework: Next.js (App Router? Pages?), Vite, CRA
|
|
19
|
+
- Component system: shadcn, MUI, Chakra, Radix, custom
|
|
20
|
+
- Styling: Tailwind, CSS Modules, styled-components
|
|
21
|
+
- Forms: react-hook-form, Formik, native
|
|
22
|
+
- State: React context, Zustand, Redux, Jotai
|
|
23
|
+
|
|
24
|
+
### 2. Identify Best Patterns
|
|
25
|
+
Examine the **best-written** components. Look at:
|
|
26
|
+
- \`components/ui/*\` \u2014 the design system
|
|
27
|
+
- Recently modified files \u2014 current standards
|
|
28
|
+
- Shared layouts \u2014 structural patterns
|
|
29
|
+
|
|
30
|
+
### 3. Infer Visual Hierarchy & Intent
|
|
31
|
+
Understand the design language:
|
|
32
|
+
- What distinguishes primary vs secondary actions?
|
|
33
|
+
- How is visual hierarchy established?
|
|
34
|
+
- What creates consistency across similar elements?
|
|
35
|
+
|
|
36
|
+
## Output Format
|
|
37
|
+
|
|
38
|
+
Generate at \`<nextjs app root>/.uilint/styleguide.md\`:
|
|
39
|
+
\`\`\`yaml
|
|
40
|
+
# Stack
|
|
41
|
+
framework:
|
|
42
|
+
styling:
|
|
43
|
+
components:
|
|
44
|
+
component_path:
|
|
45
|
+
forms:
|
|
46
|
+
|
|
47
|
+
# Component Usage (MUST use these)
|
|
48
|
+
use:
|
|
49
|
+
buttons:
|
|
50
|
+
inputs:
|
|
51
|
+
modals:
|
|
52
|
+
cards:
|
|
53
|
+
feedback:
|
|
54
|
+
icons:
|
|
55
|
+
links:
|
|
56
|
+
|
|
57
|
+
# Semantic Rules (consistency & relationships)
|
|
58
|
+
semantics:
|
|
59
|
+
hierarchy:
|
|
60
|
+
- <e.g., "primary actions must be visually distinct from secondary">
|
|
61
|
+
- <e.g., "destructive actions should be visually cautionary">
|
|
62
|
+
- <e.g., "page titles should be visually heavier than section titles">
|
|
63
|
+
consistency:
|
|
64
|
+
- <e.g., "all primary buttons should share the same visual weight">
|
|
65
|
+
- <e.g., "form inputs should have uniform height and padding">
|
|
66
|
+
- <e.g., "card padding should be consistent across the app">
|
|
67
|
+
- <e.g., "interactive elements should have consistent hover/focus states">
|
|
68
|
+
spacing:
|
|
69
|
+
- <e.g., "use the spacing scale \u2014 no arbitrary values">
|
|
70
|
+
- <e.g., "related elements should be closer than unrelated">
|
|
71
|
+
- <e.g., "section spacing should be larger than element spacing">
|
|
72
|
+
layout:
|
|
73
|
+
- <e.g., "use gap for sibling spacing, not margin">
|
|
74
|
+
- <e.g., "containers should have consistent max-width and padding">
|
|
75
|
+
|
|
76
|
+
# Patterns (structural, not values)
|
|
77
|
+
patterns:
|
|
78
|
+
forms: <e.g., "FormField + Controller + zod schema">
|
|
79
|
+
conditionals: <e.g., "cn() for class merging">
|
|
80
|
+
loading: <e.g., "Skeleton for content, Spinner for actions">
|
|
81
|
+
errors: <e.g., "ErrorBoundary at route, inline for forms">
|
|
82
|
+
responsive: <e.g., "mobile-first, standard breakpoints only">
|
|
83
|
+
|
|
84
|
+
# Component Authoring
|
|
85
|
+
authoring:
|
|
86
|
+
- <e.g., "forwardRef for interactive components">
|
|
87
|
+
- <e.g., "variants via CVA or component props, not className overrides">
|
|
88
|
+
- <e.g., "extract when used 2+ times">
|
|
89
|
+
- <e.g., "'use client' only when needed">
|
|
90
|
+
|
|
91
|
+
# Forbidden
|
|
92
|
+
forbidden:
|
|
93
|
+
- <e.g., "inline style={{}}">
|
|
94
|
+
- <e.g., "raw HTML elements when component exists">
|
|
95
|
+
- <e.g., "arbitrary values \u2014 use scale">
|
|
96
|
+
- <e.g., "className overrides that break visual consistency">
|
|
97
|
+
- <e.g., "one-off spacing that doesn't match siblings">
|
|
98
|
+
|
|
99
|
+
# Legacy (if migration in progress)
|
|
100
|
+
legacy:
|
|
101
|
+
- <e.g., "old: CSS modules \u2192 new: Tailwind">
|
|
102
|
+
- <e.g., "old: Formik \u2192 new: react-hook-form">
|
|
103
|
+
|
|
104
|
+
# Conventions
|
|
105
|
+
conventions:
|
|
106
|
+
-
|
|
107
|
+
-
|
|
108
|
+
-
|
|
109
|
+
\`\`\`
|
|
110
|
+
|
|
111
|
+
## Rules
|
|
112
|
+
|
|
113
|
+
- **Semantic over specific**: "consistent padding" not "p-4"
|
|
114
|
+
- **Relationships over absolutes**: "heavier than" not "font-bold"
|
|
115
|
+
- **Intent over implementation**: "visually distinct" not "blue background"
|
|
116
|
+
- **Prescriptive**: Define target state, not current state
|
|
117
|
+
- **Terse**: No prose. Fragments and short phrases only.
|
|
118
|
+
- **Actionable**: Every rule should be human-verifiable
|
|
119
|
+
- **Omit if N/A**: Skip sections that don't apply
|
|
120
|
+
- **Max 5 items** per section \u2014 highest impact only
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
// src/utils/skill-loader.ts
|
|
124
|
+
import { readFileSync, readdirSync, statSync, existsSync } from "fs";
|
|
125
|
+
import { join, dirname, relative } from "path";
|
|
126
|
+
import { fileURLToPath } from "url";
|
|
127
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
128
|
+
var __dirname = dirname(__filename);
|
|
129
|
+
function getSkillsDir() {
|
|
130
|
+
const devPath = join(__dirname, "..", "..", "skills");
|
|
131
|
+
const prodPath = join(__dirname, "..", "skills");
|
|
132
|
+
if (existsSync(devPath)) {
|
|
133
|
+
return devPath;
|
|
134
|
+
}
|
|
135
|
+
if (existsSync(prodPath)) {
|
|
136
|
+
return prodPath;
|
|
137
|
+
}
|
|
138
|
+
throw new Error(
|
|
139
|
+
"Could not find skills directory. This is a bug in uilint installation."
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
function collectFiles(dir, baseDir) {
|
|
143
|
+
const files = [];
|
|
144
|
+
const entries = readdirSync(dir);
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
const fullPath = join(dir, entry);
|
|
147
|
+
const stat = statSync(fullPath);
|
|
148
|
+
if (stat.isDirectory()) {
|
|
149
|
+
files.push(...collectFiles(fullPath, baseDir));
|
|
150
|
+
} else if (stat.isFile()) {
|
|
151
|
+
const relativePath = relative(baseDir, fullPath);
|
|
152
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
153
|
+
files.push({ relativePath, content });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return files;
|
|
157
|
+
}
|
|
158
|
+
function loadSkill(name) {
|
|
159
|
+
const skillsDir = getSkillsDir();
|
|
160
|
+
const skillDir = join(skillsDir, name);
|
|
161
|
+
if (!existsSync(skillDir)) {
|
|
162
|
+
throw new Error(`Skill "${name}" not found in ${skillsDir}`);
|
|
163
|
+
}
|
|
164
|
+
const skillMdPath = join(skillDir, "SKILL.md");
|
|
165
|
+
if (!existsSync(skillMdPath)) {
|
|
166
|
+
throw new Error(`Skill "${name}" is missing SKILL.md`);
|
|
167
|
+
}
|
|
168
|
+
const files = collectFiles(skillDir, skillDir);
|
|
169
|
+
return { name, files };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
GENSTYLEGUIDE_COMMAND_MD,
|
|
174
|
+
loadSkill
|
|
175
|
+
};
|
|
176
|
+
//# sourceMappingURL=chunk-2RNDQVEK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/install/constants.ts","../src/utils/skill-loader.ts"],"sourcesContent":["/**\n * Constants for the install command\n *\n * Contains file content templates for commands.\n */\n\n// ============================================================================\n// Cursor Commands\n// ============================================================================\n\nexport const GENSTYLEGUIDE_COMMAND_MD = `# React Style Guide Generator\n\nAnalyze the React UI codebase to produce a **prescriptive, semantic** style guide. Focus on consistency, intent, and relationships—not specific values.\n\n## Philosophy\n\n1. **Identify the intended architecture** from the best patterns in use\n2. **Prescribe semantic rules** — about consistency and relationships, not pixels\n3. **Stay general** — \"primary buttons should be visually consistent\" not \"buttons use px-4\"\n4. **Focus on intent** — what should FEEL the same, not what values to use\n\n## Analysis Steps\n\n### 1. Detect the Stack\n- Framework: Next.js (App Router? Pages?), Vite, CRA\n- Component system: shadcn, MUI, Chakra, Radix, custom\n- Styling: Tailwind, CSS Modules, styled-components\n- Forms: react-hook-form, Formik, native\n- State: React context, Zustand, Redux, Jotai\n\n### 2. Identify Best Patterns\nExamine the **best-written** components. Look at:\n- \\`components/ui/*\\` — the design system\n- Recently modified files — current standards\n- Shared layouts — structural patterns\n\n### 3. Infer Visual Hierarchy & Intent\nUnderstand the design language:\n- What distinguishes primary vs secondary actions?\n- How is visual hierarchy established?\n- What creates consistency across similar elements?\n\n## Output Format\n\nGenerate at \\`<nextjs app root>/.uilint/styleguide.md\\`:\n\\`\\`\\`yaml\n# Stack\nframework: \nstyling: \ncomponents: \ncomponent_path: \nforms: \n\n# Component Usage (MUST use these)\nuse:\n buttons: \n inputs: \n modals: \n cards: \n feedback: \n icons: \n links: \n\n# Semantic Rules (consistency & relationships)\nsemantics:\n hierarchy:\n - <e.g., \"primary actions must be visually distinct from secondary\">\n - <e.g., \"destructive actions should be visually cautionary\">\n - <e.g., \"page titles should be visually heavier than section titles\">\n consistency:\n - <e.g., \"all primary buttons should share the same visual weight\">\n - <e.g., \"form inputs should have uniform height and padding\">\n - <e.g., \"card padding should be consistent across the app\">\n - <e.g., \"interactive elements should have consistent hover/focus states\">\n spacing:\n - <e.g., \"use the spacing scale — no arbitrary values\">\n - <e.g., \"related elements should be closer than unrelated\">\n - <e.g., \"section spacing should be larger than element spacing\">\n layout:\n - <e.g., \"use gap for sibling spacing, not margin\">\n - <e.g., \"containers should have consistent max-width and padding\">\n\n# Patterns (structural, not values)\npatterns:\n forms: <e.g., \"FormField + Controller + zod schema\">\n conditionals: <e.g., \"cn() for class merging\">\n loading: <e.g., \"Skeleton for content, Spinner for actions\">\n errors: <e.g., \"ErrorBoundary at route, inline for forms\">\n responsive: <e.g., \"mobile-first, standard breakpoints only\">\n\n# Component Authoring\nauthoring:\n - <e.g., \"forwardRef for interactive components\">\n - <e.g., \"variants via CVA or component props, not className overrides\">\n - <e.g., \"extract when used 2+ times\">\n - <e.g., \"'use client' only when needed\">\n\n# Forbidden\nforbidden:\n - <e.g., \"inline style={{}}\">\n - <e.g., \"raw HTML elements when component exists\">\n - <e.g., \"arbitrary values — use scale\">\n - <e.g., \"className overrides that break visual consistency\">\n - <e.g., \"one-off spacing that doesn't match siblings\">\n\n# Legacy (if migration in progress)\nlegacy:\n - <e.g., \"old: CSS modules → new: Tailwind\">\n - <e.g., \"old: Formik → new: react-hook-form\">\n\n# Conventions\nconventions:\n - \n - \n - \n\\`\\`\\`\n\n## Rules\n\n- **Semantic over specific**: \"consistent padding\" not \"p-4\"\n- **Relationships over absolutes**: \"heavier than\" not \"font-bold\"\n- **Intent over implementation**: \"visually distinct\" not \"blue background\"\n- **Prescriptive**: Define target state, not current state\n- **Terse**: No prose. Fragments and short phrases only.\n- **Actionable**: Every rule should be human-verifiable\n- **Omit if N/A**: Skip sections that don't apply\n- **Max 5 items** per section — highest impact only\n`;\n\n","/**\n * Skill Loader Utility\n *\n * Loads Agent Skill files from the bundled skills directory for installation\n * into user projects. Skills follow the Agent Skills specification\n * (agentskills.io).\n */\n\nimport { readFileSync, readdirSync, statSync, existsSync } from \"fs\";\nimport { join, dirname, relative } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n/**\n * Represents a file in a skill directory\n */\nexport interface SkillFile {\n /** Relative path within the skill directory */\n relativePath: string;\n /** File content */\n content: string;\n}\n\n/**\n * Represents a complete skill ready for installation\n */\nexport interface Skill {\n /** Skill name (directory name) */\n name: string;\n /** All files in the skill */\n files: SkillFile[];\n}\n\n/**\n * Get the path to the bundled skills directory\n */\nfunction getSkillsDir(): string {\n // In development: packages/uilint/skills/\n // In production (installed): node_modules/uilint/dist/ -> ../skills/\n const devPath = join(__dirname, \"..\", \"..\", \"skills\");\n const prodPath = join(__dirname, \"..\", \"skills\");\n\n if (existsSync(devPath)) {\n return devPath;\n }\n if (existsSync(prodPath)) {\n return prodPath;\n }\n\n throw new Error(\n \"Could not find skills directory. This is a bug in uilint installation.\"\n );\n}\n\n/**\n * Recursively collect all files in a directory\n */\nfunction collectFiles(dir: string, baseDir: string): SkillFile[] {\n const files: SkillFile[] = [];\n const entries = readdirSync(dir);\n\n for (const entry of entries) {\n const fullPath = join(dir, entry);\n const stat = statSync(fullPath);\n\n if (stat.isDirectory()) {\n files.push(...collectFiles(fullPath, baseDir));\n } else if (stat.isFile()) {\n const relativePath = relative(baseDir, fullPath);\n const content = readFileSync(fullPath, \"utf-8\");\n files.push({ relativePath, content });\n }\n }\n\n return files;\n}\n\n/**\n * Load a specific skill by name\n */\nexport function loadSkill(name: string): Skill {\n const skillsDir = getSkillsDir();\n const skillDir = join(skillsDir, name);\n\n if (!existsSync(skillDir)) {\n throw new Error(`Skill \"${name}\" not found in ${skillsDir}`);\n }\n\n const skillMdPath = join(skillDir, \"SKILL.md\");\n if (!existsSync(skillMdPath)) {\n throw new Error(`Skill \"${name}\" is missing SKILL.md`);\n }\n\n const files = collectFiles(skillDir, skillDir);\n\n return { name, files };\n}\n\n/**\n * Load all available skills\n */\nexport function loadAllSkills(): Skill[] {\n const skillsDir = getSkillsDir();\n const entries = readdirSync(skillsDir);\n const skills: Skill[] = [];\n\n for (const entry of entries) {\n const skillDir = join(skillsDir, entry);\n const stat = statSync(skillDir);\n\n if (stat.isDirectory()) {\n const skillMdPath = join(skillDir, \"SKILL.md\");\n if (existsSync(skillMdPath)) {\n skills.push(loadSkill(entry));\n }\n }\n }\n\n return skills;\n}\n\n/**\n * Get the list of available skill names\n */\nexport function getAvailableSkillNames(): string[] {\n const skillsDir = getSkillsDir();\n const entries = readdirSync(skillsDir);\n const names: string[] = [];\n\n for (const entry of entries) {\n const skillDir = join(skillsDir, entry);\n const stat = statSync(skillDir);\n\n if (stat.isDirectory()) {\n const skillMdPath = join(skillDir, \"SKILL.md\");\n if (existsSync(skillMdPath)) {\n names.push(entry);\n }\n }\n }\n\n return names;\n}\n"],"mappings":";;;AAUO,IAAM,2BAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACFxC,SAAS,cAAc,aAAa,UAAU,kBAAkB;AAChE,SAAS,MAAM,SAAS,gBAAgB;AACxC,SAAS,qBAAqB;AAE9B,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,QAAQ,UAAU;AAyBpC,SAAS,eAAuB;AAG9B,QAAM,UAAU,KAAK,WAAW,MAAM,MAAM,QAAQ;AACpD,QAAM,WAAW,KAAK,WAAW,MAAM,QAAQ;AAE/C,MAAI,WAAW,OAAO,GAAG;AACvB,WAAO;AAAA,EACT;AACA,MAAI,WAAW,QAAQ,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,QAAM,IAAI;AAAA,IACR;AAAA,EACF;AACF;AAKA,SAAS,aAAa,KAAa,SAA8B;AAC/D,QAAM,QAAqB,CAAC;AAC5B,QAAM,UAAU,YAAY,GAAG;AAE/B,aAAW,SAAS,SAAS;AAC3B,UAAM,WAAW,KAAK,KAAK,KAAK;AAChC,UAAM,OAAO,SAAS,QAAQ;AAE9B,QAAI,KAAK,YAAY,GAAG;AACtB,YAAM,KAAK,GAAG,aAAa,UAAU,OAAO,CAAC;AAAA,IAC/C,WAAW,KAAK,OAAO,GAAG;AACxB,YAAM,eAAe,SAAS,SAAS,QAAQ;AAC/C,YAAM,UAAU,aAAa,UAAU,OAAO;AAC9C,YAAM,KAAK,EAAE,cAAc,QAAQ,CAAC;AAAA,IACtC;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,UAAU,MAAqB;AAC7C,QAAM,YAAY,aAAa;AAC/B,QAAM,WAAW,KAAK,WAAW,IAAI;AAErC,MAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,UAAM,IAAI,MAAM,UAAU,IAAI,kBAAkB,SAAS,EAAE;AAAA,EAC7D;AAEA,QAAM,cAAc,KAAK,UAAU,UAAU;AAC7C,MAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,UAAM,IAAI,MAAM,UAAU,IAAI,uBAAuB;AAAA,EACvD;AAEA,QAAM,QAAQ,aAAa,UAAU,QAAQ;AAE7C,SAAO,EAAE,MAAM,MAAM;AACvB;","names":[]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/next-detect.ts
|
|
4
|
+
import { existsSync, readdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
function fileExists(projectPath, relPath) {
|
|
7
|
+
return existsSync(join(projectPath, relPath));
|
|
8
|
+
}
|
|
9
|
+
function detectNextAppRouter(projectPath) {
|
|
10
|
+
const roots = ["app", join("src", "app")];
|
|
11
|
+
const candidates = [];
|
|
12
|
+
let chosenRoot = null;
|
|
13
|
+
for (const root of roots) {
|
|
14
|
+
if (existsSync(join(projectPath, root))) {
|
|
15
|
+
chosenRoot = root;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (!chosenRoot) return null;
|
|
20
|
+
const entryCandidates = [
|
|
21
|
+
join(chosenRoot, "layout.tsx"),
|
|
22
|
+
join(chosenRoot, "layout.jsx"),
|
|
23
|
+
join(chosenRoot, "layout.ts"),
|
|
24
|
+
join(chosenRoot, "layout.js"),
|
|
25
|
+
// Fallbacks (less ideal, but can work):
|
|
26
|
+
join(chosenRoot, "page.tsx"),
|
|
27
|
+
join(chosenRoot, "page.jsx")
|
|
28
|
+
];
|
|
29
|
+
for (const rel of entryCandidates) {
|
|
30
|
+
if (fileExists(projectPath, rel)) candidates.push(rel);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
appRoot: chosenRoot,
|
|
34
|
+
appRootAbs: join(projectPath, chosenRoot),
|
|
35
|
+
candidates
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
39
|
+
"node_modules",
|
|
40
|
+
".git",
|
|
41
|
+
".next",
|
|
42
|
+
"dist",
|
|
43
|
+
"build",
|
|
44
|
+
"out",
|
|
45
|
+
".turbo",
|
|
46
|
+
".vercel",
|
|
47
|
+
".cursor",
|
|
48
|
+
"coverage",
|
|
49
|
+
".uilint"
|
|
50
|
+
]);
|
|
51
|
+
function findNextAppRouterProjects(rootDir, options) {
|
|
52
|
+
const maxDepth = options?.maxDepth ?? 4;
|
|
53
|
+
const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS;
|
|
54
|
+
const results = [];
|
|
55
|
+
const visited = /* @__PURE__ */ new Set();
|
|
56
|
+
function walk(dir, depth) {
|
|
57
|
+
if (depth > maxDepth) return;
|
|
58
|
+
if (visited.has(dir)) return;
|
|
59
|
+
visited.add(dir);
|
|
60
|
+
const detection = detectNextAppRouter(dir);
|
|
61
|
+
if (detection) {
|
|
62
|
+
results.push({ projectPath: dir, detection });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let entries = [];
|
|
66
|
+
try {
|
|
67
|
+
entries = readdirSync(dir, { withFileTypes: true }).map((d) => ({
|
|
68
|
+
name: d.name,
|
|
69
|
+
isDirectory: d.isDirectory()
|
|
70
|
+
}));
|
|
71
|
+
} catch {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const ent of entries) {
|
|
75
|
+
if (!ent.isDirectory) continue;
|
|
76
|
+
if (ignoreDirs.has(ent.name)) continue;
|
|
77
|
+
if (ent.name.startsWith(".") && ent.name !== ".") continue;
|
|
78
|
+
walk(join(dir, ent.name), depth + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
walk(rootDir, 0);
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
detectNextAppRouter,
|
|
87
|
+
findNextAppRouterProjects
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=chunk-RHTG6DUD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/next-detect.ts"],"sourcesContent":["import { existsSync, readdirSync } from \"fs\";\nimport { join } from \"path\";\n\nexport interface NextAppRouterDetection {\n /**\n * Relative path to the Next App Router root dir (either \"app\" or \"src/app\").\n */\n appRoot: string;\n /**\n * Absolute path to the App Router root dir.\n */\n appRootAbs: string;\n /**\n * Candidate entry files (relative paths) that are good injection targets.\n */\n candidates: string[];\n}\n\nfunction fileExists(projectPath: string, relPath: string): boolean {\n return existsSync(join(projectPath, relPath));\n}\n\nexport function detectNextAppRouter(\n projectPath: string\n): NextAppRouterDetection | null {\n const roots = [\"app\", join(\"src\", \"app\")];\n const candidates: string[] = [];\n\n let chosenRoot: string | null = null;\n for (const root of roots) {\n if (existsSync(join(projectPath, root))) {\n chosenRoot = root;\n break;\n }\n }\n\n if (!chosenRoot) return null;\n\n // Prioritize layout files (Next App Router canonical integration point).\n const entryCandidates = [\n join(chosenRoot, \"layout.tsx\"),\n join(chosenRoot, \"layout.jsx\"),\n join(chosenRoot, \"layout.ts\"),\n join(chosenRoot, \"layout.js\"),\n // Fallbacks (less ideal, but can work):\n join(chosenRoot, \"page.tsx\"),\n join(chosenRoot, \"page.jsx\"),\n ];\n\n for (const rel of entryCandidates) {\n if (fileExists(projectPath, rel)) candidates.push(rel);\n }\n\n // If nothing exists, still return detection so routes can be installed.\n return {\n appRoot: chosenRoot,\n appRootAbs: join(projectPath, chosenRoot),\n candidates,\n };\n}\n\nexport interface NextAppRouterProjectMatch {\n /**\n * Absolute path to the Next project root (dir containing app/ or src/app/).\n */\n projectPath: string;\n detection: NextAppRouterDetection;\n}\n\nconst DEFAULT_IGNORE_DIRS = new Set([\n \"node_modules\",\n \".git\",\n \".next\",\n \"dist\",\n \"build\",\n \"out\",\n \".turbo\",\n \".vercel\",\n \".cursor\",\n \"coverage\",\n \".uilint\",\n]);\n\n/**\n * Best-effort monorepo discovery for Next.js App Router apps.\n *\n * Walks down from `rootDir` looking for directories that contain `app/` or\n * `src/app/`. Skips common large/irrelevant dirs.\n */\nexport function findNextAppRouterProjects(\n rootDir: string,\n options?: { maxDepth?: number; ignoreDirs?: Set<string> }\n): NextAppRouterProjectMatch[] {\n const maxDepth = options?.maxDepth ?? 4;\n const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS;\n const results: NextAppRouterProjectMatch[] = [];\n const visited = new Set<string>();\n\n function walk(dir: string, depth: number) {\n if (depth > maxDepth) return;\n if (visited.has(dir)) return;\n visited.add(dir);\n\n const detection = detectNextAppRouter(dir);\n if (detection) {\n results.push({ projectPath: dir, detection });\n // Don't descend further once we found a project root (avoid nested hits).\n return;\n }\n\n let entries: Array<{ name: string; isDirectory: boolean }> = [];\n try {\n entries = readdirSync(dir, { withFileTypes: true }).map((d) => ({\n name: d.name,\n isDirectory: d.isDirectory(),\n }));\n } catch {\n return;\n }\n\n for (const ent of entries) {\n if (!ent.isDirectory) continue;\n if (ignoreDirs.has(ent.name)) continue;\n // Skip hidden dirs by default (except `src` which matters)\n if (ent.name.startsWith(\".\") && ent.name !== \".\") continue;\n walk(join(dir, ent.name), depth + 1);\n }\n }\n\n walk(rootDir, 0);\n return results;\n}\n"],"mappings":";;;AAAA,SAAS,YAAY,mBAAmB;AACxC,SAAS,YAAY;AAiBrB,SAAS,WAAW,aAAqB,SAA0B;AACjE,SAAO,WAAW,KAAK,aAAa,OAAO,CAAC;AAC9C;AAEO,SAAS,oBACd,aAC+B;AAC/B,QAAM,QAAQ,CAAC,OAAO,KAAK,OAAO,KAAK,CAAC;AACxC,QAAM,aAAuB,CAAC;AAE9B,MAAI,aAA4B;AAChC,aAAW,QAAQ,OAAO;AACxB,QAAI,WAAW,KAAK,aAAa,IAAI,CAAC,GAAG;AACvC,mBAAa;AACb;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,WAAY,QAAO;AAGxB,QAAM,kBAAkB;AAAA,IACtB,KAAK,YAAY,YAAY;AAAA,IAC7B,KAAK,YAAY,YAAY;AAAA,IAC7B,KAAK,YAAY,WAAW;AAAA,IAC5B,KAAK,YAAY,WAAW;AAAA;AAAA,IAE5B,KAAK,YAAY,UAAU;AAAA,IAC3B,KAAK,YAAY,UAAU;AAAA,EAC7B;AAEA,aAAW,OAAO,iBAAiB;AACjC,QAAI,WAAW,aAAa,GAAG,EAAG,YAAW,KAAK,GAAG;AAAA,EACvD;AAGA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,YAAY,KAAK,aAAa,UAAU;AAAA,IACxC;AAAA,EACF;AACF;AAUA,IAAM,sBAAsB,oBAAI,IAAI;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAQM,SAAS,0BACd,SACA,SAC6B;AAC7B,QAAM,WAAW,SAAS,YAAY;AACtC,QAAM,aAAa,SAAS,cAAc;AAC1C,QAAM,UAAuC,CAAC;AAC9C,QAAM,UAAU,oBAAI,IAAY;AAEhC,WAAS,KAAK,KAAa,OAAe;AACxC,QAAI,QAAQ,SAAU;AACtB,QAAI,QAAQ,IAAI,GAAG,EAAG;AACtB,YAAQ,IAAI,GAAG;AAEf,UAAM,YAAY,oBAAoB,GAAG;AACzC,QAAI,WAAW;AACb,cAAQ,KAAK,EAAE,aAAa,KAAK,UAAU,CAAC;AAE5C;AAAA,IACF;AAEA,QAAI,UAAyD,CAAC;AAC9D,QAAI;AACF,gBAAU,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,EAAE,IAAI,CAAC,OAAO;AAAA,QAC9D,MAAM,EAAE;AAAA,QACR,aAAa,EAAE,YAAY;AAAA,MAC7B,EAAE;AAAA,IACJ,QAAQ;AACN;AAAA,IACF;AAEA,eAAW,OAAO,SAAS;AACzB,UAAI,CAAC,IAAI,YAAa;AACtB,UAAI,WAAW,IAAI,IAAI,IAAI,EAAG;AAE9B,UAAI,IAAI,KAAK,WAAW,GAAG,KAAK,IAAI,SAAS,IAAK;AAClD,WAAK,KAAK,KAAK,IAAI,IAAI,GAAG,QAAQ,CAAC;AAAA,IACrC;AAAA,EACF;AAEA,OAAK,SAAS,CAAC;AACf,SAAO;AACT;","names":[]}
|