expo-app-ui 1.0.1 → 1.0.2
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 +52 -30
- package/bin/expo-app-ui.js +3 -2
- package/package.json +13 -5
- package/src/commands/add.js +92 -8
- package/src/core/dependencyDetector.js +82 -0
- package/src/core/templateProcessor.js +13 -3
- package/src/utils/prompt.js +42 -0
package/README.md
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
-
# Expo UI
|
|
1
|
+
# Expo App UI
|
|
2
2
|
|
|
3
3
|
A UI component library for Expo React Native. Copy components directly into your project and customize them to your needs.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 📚 Documentation
|
|
6
|
+
|
|
7
|
+
**👉 [View Full Documentation →](https://expo-apps-ui.vercel.app)**
|
|
8
|
+
|
|
9
|
+
For complete documentation, usage examples, API references, and detailed instructions, visit our documentation site:
|
|
10
|
+
|
|
11
|
+
**https://expo-apps-ui.vercel.app**
|
|
12
|
+
|
|
13
|
+
The documentation includes:
|
|
14
|
+
- 📖 Getting Started Guide
|
|
15
|
+
- 🎨 Component Documentation
|
|
16
|
+
- 🛠️ CLI Commands Reference
|
|
17
|
+
- 💡 Usage Examples
|
|
18
|
+
- 🔧 Configuration Guides
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### Installation
|
|
6
23
|
|
|
7
24
|
You can use this library directly with npx:
|
|
8
25
|
|
|
@@ -26,14 +43,14 @@ expo-app-ui add <component-name>
|
|
|
26
43
|
|
|
27
44
|
### Adding Components
|
|
28
45
|
|
|
29
|
-
|
|
46
|
+
Add a component to your project:
|
|
30
47
|
|
|
31
48
|
```bash
|
|
32
49
|
npx expo-app-ui add custom-text
|
|
33
50
|
```
|
|
34
51
|
|
|
35
52
|
This will:
|
|
36
|
-
- Copy the component template to `components/ui/
|
|
53
|
+
- Copy the component template to `components/ui/custom-text.tsx` in your project
|
|
37
54
|
- Automatically detect and add required dependencies (helpers, constants)
|
|
38
55
|
- Preserve all imports and dependencies
|
|
39
56
|
- Allow you to customize the component as needed
|
|
@@ -44,44 +61,33 @@ npx expo-app-ui add button
|
|
|
44
61
|
# Automatically adds normalizeSize helper and theme constants if needed
|
|
45
62
|
```
|
|
46
63
|
|
|
47
|
-
### Adding Helpers
|
|
48
|
-
|
|
49
|
-
To add a helper utility:
|
|
64
|
+
### Adding Helpers, Constants, and Contexts
|
|
50
65
|
|
|
51
66
|
```bash
|
|
67
|
+
# Add a helper
|
|
52
68
|
npx expo-app-ui add normalizeSize
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
This will copy the helper to `helpers/normalizeSize.ts` in your project.
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
To add constants (like theme):
|
|
60
|
-
|
|
61
|
-
```bash
|
|
70
|
+
# Add constants
|
|
62
71
|
npx expo-app-ui add theme
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
This will copy the constants to `constants/theme.ts` in your project.
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
# Add contexts (e.g., top loading bar)
|
|
74
|
+
npx expo-app-ui add top-loading-bar
|
|
75
|
+
```
|
|
68
76
|
|
|
69
77
|
### Listing Available Items
|
|
70
78
|
|
|
71
|
-
To see all available components, helpers, and constants:
|
|
72
|
-
|
|
73
79
|
```bash
|
|
74
80
|
npx expo-app-ui list
|
|
75
81
|
```
|
|
76
82
|
|
|
77
83
|
### Overwriting Existing Files
|
|
78
84
|
|
|
79
|
-
To replace an existing component, helper, or constant:
|
|
80
|
-
|
|
81
85
|
```bash
|
|
82
86
|
npx expo-app-ui add custom-text --overwrite
|
|
83
87
|
```
|
|
84
88
|
|
|
89
|
+
> 📖 **For detailed usage instructions, examples, and API documentation, visit [expo-apps-ui.vercel.app](https://expo-apps-ui.vercel.app)**
|
|
90
|
+
|
|
85
91
|
## Project Structure
|
|
86
92
|
|
|
87
93
|
After adding components, your project structure will look like:
|
|
@@ -105,9 +111,12 @@ your-project/
|
|
|
105
111
|
The CLI automatically detects when a component requires:
|
|
106
112
|
- `normalizeSize` helper (from `@/helper/normalizeSize`)
|
|
107
113
|
- `theme` constants (from `@/constants/theme`)
|
|
114
|
+
- Related components or contexts
|
|
108
115
|
|
|
109
116
|
When you add a component that uses these dependencies, they will be automatically added to your project.
|
|
110
117
|
|
|
118
|
+
> 📖 **Learn more about auto-dependency detection in the [documentation](https://expo-apps-ui.vercel.app/docs/cli)**
|
|
119
|
+
|
|
111
120
|
## Component Templates
|
|
112
121
|
|
|
113
122
|
Components are copied directly into your project, so you have full control:
|
|
@@ -119,12 +128,13 @@ Components are copied directly into your project, so you have full control:
|
|
|
119
128
|
|
|
120
129
|
## Path Aliases
|
|
121
130
|
|
|
122
|
-
Components use path aliases like `@/components/ui/
|
|
131
|
+
Components use path aliases like `@/components/ui/custom-text` and `@/constants/theme`. Make sure your Expo project has these configured:
|
|
123
132
|
|
|
124
133
|
**tsconfig.json:**
|
|
125
134
|
```json
|
|
126
135
|
{
|
|
127
136
|
"compilerOptions": {
|
|
137
|
+
"baseUrl": ".",
|
|
128
138
|
"paths": {
|
|
129
139
|
"@/*": ["./*"]
|
|
130
140
|
}
|
|
@@ -149,6 +159,8 @@ module.exports = {
|
|
|
149
159
|
};
|
|
150
160
|
```
|
|
151
161
|
|
|
162
|
+
> 📖 **See the [Getting Started guide](https://expo-apps-ui.vercel.app/docs/getting-started) for detailed setup instructions**
|
|
163
|
+
|
|
152
164
|
## Available Components
|
|
153
165
|
|
|
154
166
|
- `custom-text` - A customizable Text component with font, color, and spacing props
|
|
@@ -159,6 +171,11 @@ module.exports = {
|
|
|
159
171
|
- `progress-bar` - A progress bar component with variants
|
|
160
172
|
- `marquee` - A scrolling marquee component
|
|
161
173
|
- `otp-input` - An OTP input component
|
|
174
|
+
- `loading-bar` - An animated top loading bar component
|
|
175
|
+
|
|
176
|
+
## Available Contexts
|
|
177
|
+
|
|
178
|
+
- `top-loading-bar-context` - React Context for managing top loading bar state
|
|
162
179
|
|
|
163
180
|
## Available Helpers
|
|
164
181
|
|
|
@@ -168,15 +185,20 @@ module.exports = {
|
|
|
168
185
|
|
|
169
186
|
- `theme` - Theme constants including colors, fonts, and sizes
|
|
170
187
|
|
|
188
|
+
> 📖 **View complete documentation, props, examples, and usage for all components at [expo-apps-ui.vercel.app/docs/components](https://expo-apps-ui.vercel.app/docs/components)**
|
|
189
|
+
|
|
171
190
|
## Contributing
|
|
172
191
|
|
|
173
|
-
To add new components, helpers, or
|
|
192
|
+
To add new components, helpers, constants, or contexts:
|
|
193
|
+
|
|
194
|
+
1. **Components**: Create a new `.tsx` file in the `templates/components/ui/` directory
|
|
195
|
+
2. **Contexts**: Create a new `.tsx` file in the `templates/context/` directory
|
|
196
|
+
3. **Helpers**: Create a new `.ts` file in the `templates/helpers/` directory
|
|
197
|
+
4. **Constants**: Create a new `.ts` file in the `templates/constants/` directory
|
|
198
|
+
5. Use kebab-case for filenames (e.g., `my-component.tsx`)
|
|
199
|
+
6. The item will be automatically available via the CLI
|
|
174
200
|
|
|
175
|
-
|
|
176
|
-
2. **Helpers**: Create a new `.ts` file in the `templates/helpers/` directory
|
|
177
|
-
3. **Constants**: Create a new `.ts` file in the `templates/constants/` directory
|
|
178
|
-
4. Use kebab-case for filenames (e.g., `my-component.tsx`)
|
|
179
|
-
5. The item will be automatically available via the CLI
|
|
201
|
+
> 📖 **For contribution guidelines and best practices, visit the [documentation](https://expo-apps-ui.vercel.app)**
|
|
180
202
|
|
|
181
203
|
## License
|
|
182
204
|
|
package/bin/expo-app-ui.js
CHANGED
|
@@ -45,8 +45,9 @@ program
|
|
|
45
45
|
logger.error(`"${name}" not found.`);
|
|
46
46
|
logger.info('Run "npx expo-app-ui list" to see available items.');
|
|
47
47
|
} else if (error instanceof FileExistsError) {
|
|
48
|
-
|
|
49
|
-
logger.
|
|
48
|
+
// User was prompted and chose not to overwrite, or non-interactive mode
|
|
49
|
+
logger.warning(`File already exists: ${error.filePath}`);
|
|
50
|
+
logger.info('Use --overwrite to replace it, or run the command again and choose "y" when prompted.');
|
|
50
51
|
} else if (error instanceof InvalidInputError) {
|
|
51
52
|
logger.error(error.message);
|
|
52
53
|
} else if (error instanceof CLIError) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-ui",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "A UI component library for Expo React Native",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A UI component library for Expo React Native. Copy components directly into your project and customize them to your needs. Documentation: https://expo-apps-ui.vercel.app",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"expo-app-ui": "bin/expo-app-ui.js"
|
|
@@ -18,12 +18,20 @@
|
|
|
18
18
|
"keywords": [
|
|
19
19
|
"expo",
|
|
20
20
|
"react-native",
|
|
21
|
+
"android",
|
|
22
|
+
"ios",
|
|
21
23
|
"ui",
|
|
22
24
|
"components",
|
|
23
25
|
"cli",
|
|
24
|
-
"component-library"
|
|
26
|
+
"component-library",
|
|
27
|
+
"expo-ui",
|
|
28
|
+
"react-native-components",
|
|
29
|
+
"mobile-ui",
|
|
30
|
+
"expo-components",
|
|
31
|
+
"ui-library",
|
|
32
|
+
"react-native-ui"
|
|
25
33
|
],
|
|
26
|
-
"author": "",
|
|
34
|
+
"author": "Krish Panchani",
|
|
27
35
|
"license": "MIT",
|
|
28
36
|
"repository": {
|
|
29
37
|
"type": "git",
|
|
@@ -32,7 +40,7 @@
|
|
|
32
40
|
"bugs": {
|
|
33
41
|
"url": "https://github.com/Krish-Panchani/expo-app-ui/issues"
|
|
34
42
|
},
|
|
35
|
-
|
|
43
|
+
"homepage": "https://expo-apps-ui.vercel.app",
|
|
36
44
|
"engines": {
|
|
37
45
|
"node": ">=14.0.0"
|
|
38
46
|
},
|
package/src/commands/add.js
CHANGED
|
@@ -3,9 +3,26 @@ const fs = require('fs-extra');
|
|
|
3
3
|
const { toKebabCase, toPascalCase, getPackageDir } = require('../utils/pathUtils');
|
|
4
4
|
const { TemplateNotFoundError } = require('../utils/errors');
|
|
5
5
|
const { readTemplate, writeFile, listTemplates } = require('../core/templateProcessor');
|
|
6
|
-
const { detectDependencies } = require('../core/dependencyDetector');
|
|
6
|
+
const { detectDependencies, detectRelatedContext, detectRelatedComponent } = require('../core/dependencyDetector');
|
|
7
7
|
const Logger = require('../utils/logger');
|
|
8
8
|
const Config = require('../utils/config');
|
|
9
|
+
const { promptOverwrite } = require('../utils/prompt');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper to create a file exists handler with prompt
|
|
13
|
+
*/
|
|
14
|
+
function createFileExistsHandler(options) {
|
|
15
|
+
return async (filePath) => {
|
|
16
|
+
if (options.overwrite) {
|
|
17
|
+
return true; // Already confirmed via --overwrite flag
|
|
18
|
+
}
|
|
19
|
+
// Check if we're in interactive mode (not silent and TTY)
|
|
20
|
+
if (!options.silent && process.stdin.isTTY) {
|
|
21
|
+
return await promptOverwrite(filePath);
|
|
22
|
+
}
|
|
23
|
+
return false; // Non-interactive, don't overwrite
|
|
24
|
+
};
|
|
25
|
+
}
|
|
9
26
|
|
|
10
27
|
/**
|
|
11
28
|
* Add a helper file
|
|
@@ -25,11 +42,13 @@ async function addHelper(helperName, options = {}) {
|
|
|
25
42
|
logger.debug(`Looking for template: ${templatePath}`);
|
|
26
43
|
|
|
27
44
|
const content = await readTemplate(templatePath);
|
|
45
|
+
const handleFileExists = createFileExistsHandler(options);
|
|
28
46
|
const validatedPath = await writeFile(
|
|
29
47
|
targetPath,
|
|
30
48
|
content,
|
|
31
49
|
projectRoot,
|
|
32
|
-
options.overwrite || false
|
|
50
|
+
options.overwrite || false,
|
|
51
|
+
handleFileExists
|
|
33
52
|
);
|
|
34
53
|
|
|
35
54
|
if (!options.silent) {
|
|
@@ -91,11 +110,13 @@ async function addConstant(constantName, options = {}) {
|
|
|
91
110
|
}
|
|
92
111
|
}
|
|
93
112
|
|
|
113
|
+
const handleFileExists = createFileExistsHandler(options);
|
|
94
114
|
const validatedPath = await writeFile(
|
|
95
115
|
targetPath,
|
|
96
116
|
content,
|
|
97
117
|
projectRoot,
|
|
98
|
-
options.overwrite || false
|
|
118
|
+
options.overwrite || false,
|
|
119
|
+
handleFileExists
|
|
99
120
|
);
|
|
100
121
|
|
|
101
122
|
if (!options.silent) {
|
|
@@ -161,6 +182,15 @@ async function addComponent(componentName, options = {}) {
|
|
|
161
182
|
}
|
|
162
183
|
}
|
|
163
184
|
|
|
185
|
+
// Check for related context
|
|
186
|
+
const relatedContext = detectRelatedContext(kebabName, templatesDir);
|
|
187
|
+
if (relatedContext) {
|
|
188
|
+
const contextPath = path.join(projectRoot, 'context', `${relatedContext}.tsx`);
|
|
189
|
+
if (!fs.existsSync(contextPath)) {
|
|
190
|
+
dependenciesToAdd.push(`${relatedContext} context`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
164
194
|
// Show summary of what will be added
|
|
165
195
|
if (dependenciesToAdd.length > 0) {
|
|
166
196
|
logger.info(`\nDetected dependencies: ${dependenciesToAdd.join(', ')}`);
|
|
@@ -192,11 +222,26 @@ async function addComponent(componentName, options = {}) {
|
|
|
192
222
|
}
|
|
193
223
|
}
|
|
194
224
|
|
|
225
|
+
// Add related context if needed
|
|
226
|
+
if (relatedContext) {
|
|
227
|
+
const contextPath = path.join(projectRoot, 'context', `${relatedContext}.tsx`);
|
|
228
|
+
if (!fs.existsSync(contextPath)) {
|
|
229
|
+
await addContext(relatedContext, {
|
|
230
|
+
logger,
|
|
231
|
+
config,
|
|
232
|
+
silent: false,
|
|
233
|
+
overwrite: false,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const handleFileExists = createFileExistsHandler(options);
|
|
195
239
|
const validatedPath = await writeFile(
|
|
196
240
|
targetPath,
|
|
197
241
|
content,
|
|
198
242
|
projectRoot,
|
|
199
|
-
options.overwrite || false
|
|
243
|
+
options.overwrite || false,
|
|
244
|
+
handleFileExists
|
|
200
245
|
);
|
|
201
246
|
|
|
202
247
|
logger.success(`Added ${componentName} component to ${path.relative(projectRoot, validatedPath)}`);
|
|
@@ -240,17 +285,53 @@ async function addContext(contextName, options = {}) {
|
|
|
240
285
|
const kebabName = toKebabCase(contextName);
|
|
241
286
|
const templatePath = path.join(templatesDir, 'context', `${kebabName}.tsx`);
|
|
242
287
|
const contextDir = path.join(projectRoot, 'context');
|
|
243
|
-
const targetPath = path.join(contextDir, `${
|
|
288
|
+
const targetPath = path.join(contextDir, `${kebabName}.tsx`);
|
|
244
289
|
|
|
245
290
|
try {
|
|
246
291
|
logger.debug(`Looking for template: ${templatePath}`);
|
|
247
292
|
|
|
248
293
|
const content = await readTemplate(templatePath);
|
|
294
|
+
|
|
295
|
+
// Detect dependencies including related component
|
|
296
|
+
const dependencies = detectDependencies(content);
|
|
297
|
+
|
|
298
|
+
// Check for related component
|
|
299
|
+
const relatedComponent = detectRelatedComponent(kebabName, templatesDir);
|
|
300
|
+
const dependenciesToAdd = [];
|
|
301
|
+
|
|
302
|
+
if (relatedComponent) {
|
|
303
|
+
const componentPath = path.join(config.getComponentsDir(), `${relatedComponent}.tsx`);
|
|
304
|
+
if (!fs.existsSync(componentPath)) {
|
|
305
|
+
dependenciesToAdd.push(`${relatedComponent} component`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Show summary of what will be added
|
|
310
|
+
if (dependenciesToAdd.length > 0) {
|
|
311
|
+
logger.info(`\nDetected dependencies: ${dependenciesToAdd.join(', ')}`);
|
|
312
|
+
logger.debug('Adding required dependencies...\n');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Add related component if needed
|
|
316
|
+
if (relatedComponent) {
|
|
317
|
+
const componentPath = path.join(config.getComponentsDir(), `${relatedComponent}.tsx`);
|
|
318
|
+
if (!fs.existsSync(componentPath)) {
|
|
319
|
+
await addComponent(relatedComponent, {
|
|
320
|
+
logger,
|
|
321
|
+
config,
|
|
322
|
+
silent: false,
|
|
323
|
+
overwrite: false,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const handleFileExists = createFileExistsHandler(options);
|
|
249
329
|
const validatedPath = await writeFile(
|
|
250
330
|
targetPath,
|
|
251
331
|
content,
|
|
252
332
|
projectRoot,
|
|
253
|
-
options.overwrite || false
|
|
333
|
+
options.overwrite || false,
|
|
334
|
+
handleFileExists
|
|
254
335
|
);
|
|
255
336
|
|
|
256
337
|
if (!options.silent) {
|
|
@@ -277,6 +358,7 @@ async function addTopLoadingBar(options = {}) {
|
|
|
277
358
|
const packageDir = getPackageDir();
|
|
278
359
|
const templatesDir = path.join(packageDir, 'templates');
|
|
279
360
|
const overwrite = options.overwrite || false;
|
|
361
|
+
const handleFileExists = createFileExistsHandler(options);
|
|
280
362
|
|
|
281
363
|
logger.info('Adding Top Loading Bar (context + component)...\n');
|
|
282
364
|
|
|
@@ -291,7 +373,8 @@ async function addTopLoadingBar(options = {}) {
|
|
|
291
373
|
targetComponentPath,
|
|
292
374
|
content,
|
|
293
375
|
projectRoot,
|
|
294
|
-
overwrite
|
|
376
|
+
overwrite,
|
|
377
|
+
handleFileExists
|
|
295
378
|
);
|
|
296
379
|
logger.success(`Added loading-bar component to ${path.relative(projectRoot, validatedPath)}`);
|
|
297
380
|
} else {
|
|
@@ -309,7 +392,8 @@ async function addTopLoadingBar(options = {}) {
|
|
|
309
392
|
targetContextPath,
|
|
310
393
|
content,
|
|
311
394
|
projectRoot,
|
|
312
|
-
overwrite
|
|
395
|
+
overwrite,
|
|
396
|
+
handleFileExists
|
|
313
397
|
);
|
|
314
398
|
logger.success(`Added top-loading-bar-context to ${path.relative(projectRoot, validatedPath)}`);
|
|
315
399
|
} else {
|
|
@@ -8,12 +8,16 @@ function detectDependencies(content) {
|
|
|
8
8
|
return {
|
|
9
9
|
needsNormalizeSize: false,
|
|
10
10
|
needsTheme: false,
|
|
11
|
+
needsComponent: null,
|
|
12
|
+
needsContext: null,
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const dependencies = {
|
|
15
17
|
needsNormalizeSize: false,
|
|
16
18
|
needsTheme: false,
|
|
19
|
+
needsComponent: null,
|
|
20
|
+
needsContext: null,
|
|
17
21
|
};
|
|
18
22
|
|
|
19
23
|
// More precise pattern matching
|
|
@@ -23,10 +27,88 @@ function detectDependencies(content) {
|
|
|
23
27
|
dependencies.needsNormalizeSize = normalizeSizePattern.test(content);
|
|
24
28
|
dependencies.needsTheme = themePattern.test(content);
|
|
25
29
|
|
|
30
|
+
// Detect component imports in context files
|
|
31
|
+
const componentImportPattern = /from\s+['"]@\/components\/ui\/([^'"]+)['"]/;
|
|
32
|
+
const componentMatch = content.match(componentImportPattern);
|
|
33
|
+
if (componentMatch) {
|
|
34
|
+
dependencies.needsComponent = componentMatch[1]; // e.g., "loading-bar"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Detect context imports in component files
|
|
38
|
+
const contextImportPattern = /from\s+['"]@\/context\/([^'"]+)['"]/;
|
|
39
|
+
const contextMatch = content.match(contextImportPattern);
|
|
40
|
+
if (contextMatch) {
|
|
41
|
+
dependencies.needsContext = contextMatch[1]; // e.g., "top-loading-bar-context"
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
return dependencies;
|
|
27
45
|
}
|
|
28
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Detect related context for a component
|
|
49
|
+
* @param {string} componentName - Component name in kebab-case
|
|
50
|
+
* @param {string} templatesDir - Templates directory
|
|
51
|
+
* @returns {string|null} Related context name or null
|
|
52
|
+
*/
|
|
53
|
+
function detectRelatedContext(componentName, templatesDir) {
|
|
54
|
+
const fs = require('fs-extra');
|
|
55
|
+
const path = require('path');
|
|
56
|
+
|
|
57
|
+
// Check all context files to see if any imports this component
|
|
58
|
+
const contextDir = path.join(templatesDir, 'context');
|
|
59
|
+
if (!fs.existsSync(contextDir)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const contextFiles = fs.readdirSync(contextDir).filter(file => file.endsWith('.tsx'));
|
|
64
|
+
|
|
65
|
+
for (const contextFile of contextFiles) {
|
|
66
|
+
const contextPath = path.join(contextDir, contextFile);
|
|
67
|
+
const content = fs.readFileSync(contextPath, 'utf-8');
|
|
68
|
+
|
|
69
|
+
// Check if this context imports the component
|
|
70
|
+
// Escape special regex characters in componentName
|
|
71
|
+
const escapedComponentName = componentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
72
|
+
const componentImportPattern = new RegExp(`@/components/ui/${escapedComponentName}`);
|
|
73
|
+
if (componentImportPattern.test(content)) {
|
|
74
|
+
return path.basename(contextFile, '.tsx');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Detect related component for a context
|
|
83
|
+
* @param {string} contextName - Context name in kebab-case
|
|
84
|
+
* @param {string} templatesDir - Templates directory
|
|
85
|
+
* @returns {string|null} Related component name or null
|
|
86
|
+
*/
|
|
87
|
+
function detectRelatedComponent(contextName, templatesDir) {
|
|
88
|
+
const fs = require('fs-extra');
|
|
89
|
+
const path = require('path');
|
|
90
|
+
|
|
91
|
+
const contextPath = path.join(templatesDir, 'context', `${contextName}.tsx`);
|
|
92
|
+
if (!fs.existsSync(contextPath)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const content = fs.readFileSync(contextPath, 'utf-8');
|
|
97
|
+
|
|
98
|
+
// Extract component name from import
|
|
99
|
+
const componentImportPattern = /from\s+['"]@\/components\/ui\/([^'"]+)['"]/;
|
|
100
|
+
const match = content.match(componentImportPattern);
|
|
101
|
+
|
|
102
|
+
if (match) {
|
|
103
|
+
return match[1]; // e.g., "loading-bar"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
29
109
|
module.exports = {
|
|
30
110
|
detectDependencies,
|
|
111
|
+
detectRelatedContext,
|
|
112
|
+
detectRelatedComponent,
|
|
31
113
|
};
|
|
32
114
|
|
|
@@ -30,16 +30,26 @@ async function readTemplate(templatePath) {
|
|
|
30
30
|
* @param {string} content - File content
|
|
31
31
|
* @param {string} baseDir - Base directory for validation
|
|
32
32
|
* @param {boolean} overwrite - Whether to overwrite existing file
|
|
33
|
-
* @
|
|
33
|
+
* @param {Function} onExists - Optional callback when file exists (returns Promise<boolean>)
|
|
34
|
+
* @throws {FileExistsError} If file exists and overwrite is false and onExists is not provided
|
|
34
35
|
* @throws {CLIError} If path validation fails
|
|
35
36
|
*/
|
|
36
|
-
async function writeFile(targetPath, content, baseDir, overwrite = false) {
|
|
37
|
+
async function writeFile(targetPath, content, baseDir, overwrite = false, onExists = null) {
|
|
37
38
|
// Validate path
|
|
38
39
|
const validatedPath = validatePath(targetPath, baseDir);
|
|
39
40
|
|
|
40
41
|
// Check if file exists
|
|
41
42
|
if (fs.existsSync(validatedPath) && !overwrite) {
|
|
42
|
-
|
|
43
|
+
// If callback is provided, use it to ask user
|
|
44
|
+
if (onExists && typeof onExists === 'function') {
|
|
45
|
+
const shouldOverwrite = await onExists(validatedPath);
|
|
46
|
+
if (!shouldOverwrite) {
|
|
47
|
+
throw new FileExistsError(validatedPath);
|
|
48
|
+
}
|
|
49
|
+
// User confirmed, proceed with overwrite
|
|
50
|
+
} else {
|
|
51
|
+
throw new FileExistsError(validatedPath);
|
|
52
|
+
}
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
// Ensure directory exists
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prompt user for confirmation
|
|
5
|
+
* @param {string} message - Message to display
|
|
6
|
+
* @returns {Promise<boolean>} User's response (true for yes, false for no)
|
|
7
|
+
*/
|
|
8
|
+
function promptUser(message) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
rl.question(`${message} (y/N): `, (answer) => {
|
|
16
|
+
rl.close();
|
|
17
|
+
// Default to 'no' if empty or anything other than 'y'/'Y'/'yes'/'YES'
|
|
18
|
+
const normalized = answer.trim().toLowerCase();
|
|
19
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Prompt user to overwrite existing file
|
|
26
|
+
* @param {string} filePath - Path to the file that exists
|
|
27
|
+
* @returns {Promise<boolean>} true if user wants to overwrite, false otherwise
|
|
28
|
+
*/
|
|
29
|
+
async function promptOverwrite(filePath) {
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
32
|
+
|
|
33
|
+
return await promptUser(
|
|
34
|
+
`File "${relativePath}" already exists. Do you want to overwrite it?`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
promptUser,
|
|
40
|
+
promptOverwrite,
|
|
41
|
+
};
|
|
42
|
+
|