expo-app-ui 1.0.0 → 1.0.1

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.
@@ -1,430 +1,93 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs-extra');
4
- const path = require('path');
5
- const { program } = require('commander');
6
- const chalk = require('chalk');
7
-
8
- // Get the directory where this package is installed
9
- const getPackageDir = () => {
10
- let currentDir = __dirname;
11
-
12
- while (currentDir !== path.dirname(currentDir)) {
13
- const packageJsonPath = path.join(currentDir, 'package.json');
14
- if (fs.existsSync(packageJsonPath)) {
15
- const pkg = fs.readJsonSync(packageJsonPath);
16
- if (pkg.bin && (pkg.bin['expo-app-ui'] || pkg.name === 'expo-app-ui')) {
17
- return currentDir;
18
- }
19
- }
20
- currentDir = path.dirname(currentDir);
21
- }
22
-
23
- return path.dirname(__dirname);
24
- };
25
-
26
- const packageDir = getPackageDir();
27
- const templatesDir = path.join(packageDir, 'templates');
28
-
29
- // Get the current working directory (user's project)
30
- const getProjectRoot = () => {
31
- let currentDir = process.cwd();
32
-
33
- while (currentDir !== path.dirname(currentDir)) {
34
- if (fs.existsSync(path.join(currentDir, 'package.json')) ||
35
- fs.existsSync(path.join(currentDir, 'app.json'))) {
36
- return currentDir;
37
- }
38
- currentDir = path.dirname(currentDir);
39
- }
40
-
41
- return process.cwd();
42
- };
43
-
44
- const projectRoot = getProjectRoot();
45
-
46
- // Function to convert component name to file name
47
- const toKebabCase = (str) => {
48
- return str
49
- .replace(/([a-z])([A-Z])/g, '$1-$2')
50
- .replace(/[\s_]+/g, '-')
51
- .toLowerCase();
52
- };
53
-
54
- // Function to convert kebab-case to PascalCase
55
- const toPascalCase = (str) => {
56
- return str
57
- .split('-')
58
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
59
- .join('');
60
- };
61
-
62
- // Detect dependencies in component content
63
- const detectDependencies = (content) => {
64
- const dependencies = {
65
- needsNormalizeSize: false,
66
- needsTheme: false,
67
- };
68
-
69
- if (content.includes('@/helper/normalizeSize') || content.includes('normalizeSize')) {
70
- dependencies.needsNormalizeSize = true;
71
- }
72
-
73
- if (content.includes('@/constants/theme') || content.includes('from "@/constants/theme"')) {
74
- dependencies.needsTheme = true;
75
- }
76
-
77
- return dependencies;
78
- };
79
-
80
- // Add helper file
81
- async function addHelper(helperName, options = {}) {
82
- const kebabName = toKebabCase(helperName);
83
- const templatePath = path.join(templatesDir, 'helpers', `${kebabName}.ts`);
84
- const helpersDir = path.join(projectRoot, 'helpers');
85
- const targetPath = path.join(helpersDir, `${kebabName}.ts`);
86
-
87
- try {
88
- if (!fs.existsSync(templatePath)) {
89
- console.error(chalk.red(`✖ Helper "${helperName}" not found.`));
90
-
91
- // List available helpers
92
- const helpersTemplateDir = path.join(templatesDir, 'helpers');
93
- if (fs.existsSync(helpersTemplateDir)) {
94
- const helpers = fs.readdirSync(helpersTemplateDir)
95
- .filter(file => file.endsWith('.ts') || file.endsWith('.tsx'))
96
- .map(file => path.basename(file, path.extname(file)));
97
-
98
- if (helpers.length > 0) {
99
- console.log(chalk.yellow('\nAvailable helpers:'));
100
- helpers.forEach(helper => {
101
- console.log(chalk.gray(` - ${helper}`));
102
- });
103
- }
104
- }
105
-
106
- process.exit(1);
107
- }
108
-
109
- await fs.ensureDir(helpersDir);
110
-
111
- const overwrite = process.argv.includes('--overwrite') || options.overwrite;
112
-
113
- if (fs.existsSync(targetPath) && !overwrite) {
114
- console.error(chalk.red(`✖ Helper "${helperName}" already exists at ${targetPath}`));
115
- console.log(chalk.yellow(' Use --overwrite to replace it.'));
116
- process.exit(1);
117
- }
118
-
119
- let content = await fs.readFile(templatePath, 'utf-8');
120
- await fs.writeFile(targetPath, content, 'utf-8');
121
-
122
- if (!options.silent) {
123
- console.log(chalk.green(`✓ Added ${helperName} helper to ${path.relative(projectRoot, targetPath)}`));
124
- }
125
-
126
- return true;
127
- } catch (error) {
128
- console.error(chalk.red(`✖ Error adding helper: ${error.message}`));
129
- process.exit(1);
130
- }
131
- }
132
-
133
- // Add constants file
134
- async function addConstant(constantName, options = {}) {
135
- const kebabName = toKebabCase(constantName);
136
- const templatePath = path.join(templatesDir, 'constants', `${kebabName}.ts`);
137
- const constantsDir = path.join(projectRoot, 'constants');
138
- const targetPath = path.join(constantsDir, `${kebabName}.ts`);
139
-
140
- try {
141
- if (!fs.existsSync(templatePath)) {
142
- console.error(chalk.red(`✖ Constant "${constantName}" not found.`));
143
-
144
- // List available constants
145
- const constantsTemplateDir = path.join(templatesDir, 'constants');
146
- if (fs.existsSync(constantsTemplateDir)) {
147
- const constants = fs.readdirSync(constantsTemplateDir)
148
- .filter(file => file.endsWith('.ts') || file.endsWith('.tsx'))
149
- .map(file => path.basename(file, path.extname(file)));
150
-
151
- if (constants.length > 0) {
152
- console.log(chalk.yellow('\nAvailable constants:'));
153
- constants.forEach(constant => {
154
- console.log(chalk.gray(` - ${constant}`));
155
- });
156
- }
157
- }
158
-
159
- process.exit(1);
160
- }
161
-
162
- await fs.ensureDir(constantsDir);
163
-
164
- const overwrite = process.argv.includes('--overwrite') || options.overwrite;
165
-
166
- if (fs.existsSync(targetPath) && !overwrite) {
167
- console.error(chalk.red(`✖ Constant "${constantName}" already exists at ${targetPath}`));
168
- console.log(chalk.yellow(' Use --overwrite to replace it.'));
169
- process.exit(1);
170
- }
171
-
172
- let content = await fs.readFile(templatePath, 'utf-8');
173
-
174
- // Auto-add normalizeSize dependency for theme.ts
175
- if (kebabName === 'theme') {
176
- const normalizeSizePath = path.join(projectRoot, 'helpers', 'normalizeSize.ts');
177
- if (!fs.existsSync(normalizeSizePath)) {
178
- if (!options.silent) {
179
- console.log(chalk.blue('ℹ Adding normalizeSize helper (required dependency for theme)...'));
180
- }
181
- await addHelper('normalizeSize', { silent: true, overwrite: false });
182
- }
183
-
184
- // Fix import path to use relative path
185
- const normalizeSizeExists = fs.existsSync(normalizeSizePath);
186
- if (normalizeSizeExists) {
187
- // The import in theme.ts already uses '../helpers/normalizeSize' which is correct
188
- // No need to change it
189
- }
190
- }
3
+ /**
4
+ * Expo App UI CLI - Production Ready
5
+ *
6
+ * A UI component library CLI for Expo React Native
7
+ * Copy components directly into your project and customize them
8
+ */
191
9
 
192
- await fs.writeFile(targetPath, content, 'utf-8');
193
-
194
- if (!options.silent) {
195
- console.log(chalk.green(`✓ Added ${constantName} constant to ${path.relative(projectRoot, targetPath)}`));
196
- }
197
-
198
- return true;
199
- } catch (error) {
200
- console.error(chalk.red(`✖ Error adding constant: ${error.message}`));
201
- process.exit(1);
202
- }
203
- }
204
-
205
- // Function to add a component
206
- async function addComponent(componentName) {
207
- const kebabName = toKebabCase(componentName);
208
- const templatePath = path.join(templatesDir, `${kebabName}.tsx`);
209
- const componentsDir = path.join(projectRoot, 'components', 'ui');
210
- const targetPath = path.join(componentsDir, `${toPascalCase(kebabName)}.tsx`);
211
-
212
- try {
213
- // Check if template exists
214
- if (!fs.existsSync(templatePath)) {
215
- console.error(chalk.red(`✖ Component "${componentName}" not found.`));
216
- console.log(chalk.yellow(`Available components:`));
217
-
218
- // List available components
219
- const templates = fs.readdirSync(templatesDir)
220
- .filter(file => (file.endsWith('.tsx') || file.endsWith('.ts')) &&
221
- !fs.statSync(path.join(templatesDir, file)).isDirectory())
222
- .map(file => path.basename(file, path.extname(file)));
223
-
224
- if (templates.length === 0) {
225
- console.log(chalk.gray(' No components available.'));
226
- } else {
227
- templates.forEach(template => {
228
- console.log(chalk.gray(` - ${template}`));
229
- });
230
- }
231
- process.exit(1);
232
- }
233
-
234
- // Ensure components/ui directory exists
235
- await fs.ensureDir(componentsDir);
236
-
237
- // Get overwrite option from command
238
- const overwrite = process.argv.includes('--overwrite');
239
-
240
- // Check if component already exists
241
- if (fs.existsSync(targetPath) && !overwrite) {
242
- console.error(chalk.red(`✖ Component "${componentName}" already exists at ${targetPath}`));
243
- console.log(chalk.yellow(' Use --overwrite to replace it.'));
244
- process.exit(1);
245
- }
246
-
247
- // Read template content
248
- let content = await fs.readFile(templatePath, 'utf-8');
249
-
250
- // Detect dependencies
251
- const dependencies = detectDependencies(content);
252
-
253
- // Auto-add dependencies if needed
254
- const dependenciesToAdd = [];
255
-
256
- if (dependencies.needsNormalizeSize) {
257
- const normalizeSizePath = path.join(projectRoot, 'helpers', 'normalizeSize.ts');
258
- if (!fs.existsSync(normalizeSizePath)) {
259
- dependenciesToAdd.push('normalizeSize helper');
260
- }
261
- }
262
-
263
- if (dependencies.needsTheme) {
264
- const themePath = path.join(projectRoot, 'constants', 'theme.ts');
265
- if (!fs.existsSync(themePath)) {
266
- dependenciesToAdd.push('theme constants');
267
- }
268
- }
269
-
270
- // Show summary of what will be added
271
- if (dependenciesToAdd.length > 0) {
272
- console.log(chalk.blue(`\nℹ Detected dependencies: ${dependenciesToAdd.join(', ')}`));
273
- console.log(chalk.gray(' Adding required dependencies...\n'));
274
- }
275
-
276
- // Add dependencies
277
- if (dependencies.needsNormalizeSize) {
278
- const normalizeSizePath = path.join(projectRoot, 'helpers', 'normalizeSize.ts');
279
- if (!fs.existsSync(normalizeSizePath)) {
280
- await addHelper('normalizeSize', { silent: true });
281
- }
282
- }
283
-
284
- if (dependencies.needsTheme) {
285
- const themePath = path.join(projectRoot, 'constants', 'theme.ts');
286
- if (!fs.existsSync(themePath)) {
287
- await addConstant('theme', { silent: true });
288
- }
289
- }
290
-
291
- // Fix relative imports (e.g., CustomText from "./CustomText")
292
- // This will be handled by the user's path aliases, so we keep @/ imports
293
-
294
- // Write to target location
295
- await fs.writeFile(targetPath, content, 'utf-8');
296
-
297
- console.log(chalk.green(`✓ Added ${componentName} component to ${path.relative(projectRoot, targetPath)}`));
298
-
299
- if (dependencies.needsNormalizeSize || dependencies.needsTheme) {
300
- console.log(chalk.gray('\n💡 Tip: Make sure your project has path aliases configured for @/ imports.'));
301
- }
302
-
303
- } catch (error) {
304
- console.error(chalk.red(`✖ Error adding component: ${error.message}`));
305
- process.exit(1);
306
- }
307
- }
308
-
309
- // Function to list available items
310
- function listItems() {
311
- try {
312
- const items = {
313
- components: [],
314
- helpers: [],
315
- constants: [],
316
- };
317
-
318
- // List components
319
- if (fs.existsSync(templatesDir)) {
320
- const files = fs.readdirSync(templatesDir);
321
- files.forEach(file => {
322
- const filePath = path.join(templatesDir, file);
323
- if (fs.statSync(filePath).isFile() && (file.endsWith('.tsx') || file.endsWith('.ts'))) {
324
- items.components.push(path.basename(file, path.extname(file)));
325
- }
326
- });
327
- }
328
-
329
- // List helpers
330
- const helpersDir = path.join(templatesDir, 'helpers');
331
- if (fs.existsSync(helpersDir)) {
332
- const files = fs.readdirSync(helpersDir);
333
- files.forEach(file => {
334
- if (file.endsWith('.ts') || file.endsWith('.tsx')) {
335
- items.helpers.push(path.basename(file, path.extname(file)));
336
- }
337
- });
338
- }
339
-
340
- // List constants
341
- const constantsDir = path.join(templatesDir, 'constants');
342
- if (fs.existsSync(constantsDir)) {
343
- const files = fs.readdirSync(constantsDir);
344
- files.forEach(file => {
345
- if (file.endsWith('.ts') || file.endsWith('.tsx')) {
346
- items.constants.push(path.basename(file, path.extname(file)));
347
- }
348
- });
349
- }
350
-
351
- if (items.components.length === 0 && items.helpers.length === 0 && items.constants.length === 0) {
352
- console.log(chalk.yellow('No items available.'));
353
- return;
354
- }
355
-
356
- if (items.components.length > 0) {
357
- console.log(chalk.blue('Available components:\n'));
358
- items.components.forEach(item => {
359
- console.log(chalk.gray(` - ${item}`));
360
- });
361
- console.log();
362
- }
363
-
364
- if (items.helpers.length > 0) {
365
- console.log(chalk.blue('Available helpers:\n'));
366
- items.helpers.forEach(item => {
367
- console.log(chalk.gray(` - ${item}`));
368
- });
369
- console.log();
370
- }
371
-
372
- if (items.constants.length > 0) {
373
- console.log(chalk.blue('Available constants:\n'));
374
- items.constants.forEach(item => {
375
- console.log(chalk.gray(` - ${item}`));
376
- });
377
- }
378
- } catch (error) {
379
- console.error(chalk.red(`✖ Error listing items: ${error.message}`));
380
- process.exit(1);
381
- }
382
- }
10
+ const { program } = require('commander');
11
+ const packageJson = require('../package.json');
12
+ const { handleAdd } = require('../src/commands/add');
13
+ const { handleList } = require('../src/commands/list');
14
+ const { CLIError, TemplateNotFoundError, FileExistsError, InvalidInputError } = require('../src/utils/errors');
15
+ const Logger = require('../src/utils/logger');
16
+
17
+ // Initialize logger
18
+ const logger = new Logger({
19
+ verbose: process.argv.includes('--verbose') || process.argv.includes('-v'),
20
+ silent: process.argv.includes('--silent') || process.argv.includes('-s'),
21
+ });
383
22
 
384
23
  // CLI setup
385
24
  program
386
- .name('expo-ui')
25
+ .name('expo-app-ui')
387
26
  .description('A UI component library for Expo React Native')
388
- .version('1.0.0');
27
+ .version(packageJson.version)
28
+ .option('-v, --verbose', 'verbose output')
29
+ .option('-s, --silent', 'silent mode');
389
30
 
31
+ // Add command
390
32
  program
391
33
  .option('--overwrite', 'Overwrite existing files')
392
34
  .command('add <name>')
393
35
  .description('Add a component, helper, or constant to your project')
394
36
  .action(async (name) => {
395
- // Determine type by checking templates
396
- const kebabName = toKebabCase(name);
397
-
398
- // Check if it's a component
399
- const componentPath = path.join(templatesDir, `${kebabName}.tsx`);
400
- if (fs.existsSync(componentPath)) {
401
- await addComponent(name);
402
- return;
403
- }
404
-
405
- // Check if it's a helper
406
- const helperPath = path.join(templatesDir, 'helpers', `${kebabName}.ts`);
407
- if (fs.existsSync(helperPath)) {
408
- await addHelper(name);
409
- return;
410
- }
411
-
412
- // Check if it's a constant
413
- const constantPath = path.join(templatesDir, 'constants', `${kebabName}.ts`);
414
- if (fs.existsSync(constantPath)) {
415
- await addConstant(name);
416
- return;
37
+ try {
38
+ await handleAdd(name, {
39
+ overwrite: program.opts().overwrite || false,
40
+ verbose: logger.verbose,
41
+ silent: logger.silent,
42
+ });
43
+ } catch (error) {
44
+ if (error instanceof TemplateNotFoundError) {
45
+ logger.error(`"${name}" not found.`);
46
+ logger.info('Run "npx expo-app-ui list" to see available items.');
47
+ } else if (error instanceof FileExistsError) {
48
+ logger.error(`File already exists: ${error.filePath}`);
49
+ logger.info('Use --overwrite to replace it.');
50
+ } else if (error instanceof InvalidInputError) {
51
+ logger.error(error.message);
52
+ } else if (error instanceof CLIError) {
53
+ logger.error(error.message);
54
+ } else {
55
+ logger.error(`Unexpected error: ${error.message}`);
56
+ if (logger.verbose) {
57
+ console.error(error.stack);
58
+ }
59
+ }
60
+ process.exit(1);
417
61
  }
418
-
419
- // Not found
420
- console.error(chalk.red(`✖ "${name}" not found.`));
421
- console.log(chalk.yellow('Run "npx expo-app-ui list" to see available items.'));
422
- process.exit(1);
423
62
  });
424
63
 
64
+ // List command
425
65
  program
426
66
  .command('list')
427
67
  .description('List all available components, helpers, and constants')
428
- .action(listItems);
68
+ .action(() => {
69
+ try {
70
+ handleList({
71
+ verbose: logger.verbose,
72
+ silent: logger.silent,
73
+ });
74
+ } catch (error) {
75
+ logger.error(`Error: ${error.message}`);
76
+ if (logger.verbose) {
77
+ console.error(error);
78
+ }
79
+ process.exit(1);
80
+ }
81
+ });
82
+
83
+ // Global error handler
84
+ process.on('unhandledRejection', (error) => {
85
+ logger.error(`Unhandled error: ${error.message}`);
86
+ if (logger.verbose) {
87
+ console.error(error);
88
+ }
89
+ process.exit(1);
90
+ });
429
91
 
92
+ // Parse arguments
430
93
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-app-ui",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A UI component library for Expo React Native",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
+ "src",
11
12
  "templates",
12
13
  "README.md"
13
14
  ],