@metacells/mcellui-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2529 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2529 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command9 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import chalk2 from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
import prompts from "prompts";
|
|
11
|
+
import fs2 from "fs-extra";
|
|
12
|
+
import path2 from "path";
|
|
13
|
+
|
|
14
|
+
// src/utils/project.ts
|
|
15
|
+
import fs from "fs-extra";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import createJiti from "jiti";
|
|
19
|
+
import chalk from "chalk";
|
|
20
|
+
|
|
21
|
+
// src/utils/config-schema.ts
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
var themePresetSchema = z.enum([
|
|
24
|
+
"zinc",
|
|
25
|
+
"slate",
|
|
26
|
+
"stone",
|
|
27
|
+
"blue",
|
|
28
|
+
"green",
|
|
29
|
+
"rose",
|
|
30
|
+
"orange",
|
|
31
|
+
"violet"
|
|
32
|
+
]);
|
|
33
|
+
var radiusPresetSchema = z.enum(["none", "sm", "md", "lg", "full"]);
|
|
34
|
+
var colorSchemeSchema = z.enum(["light", "dark", "system"]);
|
|
35
|
+
var stylePresetSchema = z.enum(["default", "ios", "material"]);
|
|
36
|
+
var hexColorSchema = z.string().regex(
|
|
37
|
+
/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
|
|
38
|
+
"Must be a valid hex color (e.g., #ff0000 or #f00)"
|
|
39
|
+
);
|
|
40
|
+
var optionalHexColorSchema = hexColorSchema.optional();
|
|
41
|
+
var themeColorsSchema = z.object({
|
|
42
|
+
background: optionalHexColorSchema,
|
|
43
|
+
foreground: optionalHexColorSchema,
|
|
44
|
+
backgroundSubtle: optionalHexColorSchema,
|
|
45
|
+
foregroundMuted: optionalHexColorSchema,
|
|
46
|
+
card: optionalHexColorSchema,
|
|
47
|
+
cardForeground: optionalHexColorSchema,
|
|
48
|
+
primary: optionalHexColorSchema,
|
|
49
|
+
primaryForeground: optionalHexColorSchema,
|
|
50
|
+
secondary: optionalHexColorSchema,
|
|
51
|
+
secondaryForeground: optionalHexColorSchema,
|
|
52
|
+
muted: optionalHexColorSchema,
|
|
53
|
+
mutedForeground: optionalHexColorSchema,
|
|
54
|
+
accent: optionalHexColorSchema,
|
|
55
|
+
accentForeground: optionalHexColorSchema,
|
|
56
|
+
destructive: optionalHexColorSchema,
|
|
57
|
+
destructiveForeground: optionalHexColorSchema,
|
|
58
|
+
border: optionalHexColorSchema,
|
|
59
|
+
input: optionalHexColorSchema,
|
|
60
|
+
ring: optionalHexColorSchema,
|
|
61
|
+
success: optionalHexColorSchema,
|
|
62
|
+
warning: optionalHexColorSchema
|
|
63
|
+
}).partial();
|
|
64
|
+
var fontsSchema = z.object({
|
|
65
|
+
sans: z.string().optional(),
|
|
66
|
+
heading: z.string().optional(),
|
|
67
|
+
mono: z.string().optional()
|
|
68
|
+
}).partial();
|
|
69
|
+
var componentOverridesSchema = z.object({
|
|
70
|
+
button: z.object({
|
|
71
|
+
defaultVariant: z.enum(["default", "secondary", "outline", "ghost", "destructive"]).optional(),
|
|
72
|
+
defaultSize: z.enum(["sm", "md", "lg"]).optional()
|
|
73
|
+
}).optional(),
|
|
74
|
+
input: z.object({
|
|
75
|
+
defaultSize: z.enum(["sm", "md", "lg"]).optional()
|
|
76
|
+
}).optional(),
|
|
77
|
+
card: z.object({
|
|
78
|
+
defaultVariant: z.enum(["default", "elevated", "outlined"]).optional()
|
|
79
|
+
}).optional(),
|
|
80
|
+
badge: z.object({
|
|
81
|
+
defaultVariant: z.enum(["default", "secondary", "destructive", "outline"]).optional(),
|
|
82
|
+
defaultSize: z.enum(["sm", "md", "lg"]).optional()
|
|
83
|
+
}).optional(),
|
|
84
|
+
avatar: z.object({
|
|
85
|
+
defaultSize: z.enum(["xs", "sm", "md", "lg", "xl"]).optional()
|
|
86
|
+
}).optional()
|
|
87
|
+
}).partial();
|
|
88
|
+
var pathSchema = z.string().refine(
|
|
89
|
+
(val) => val.startsWith("./") || val.startsWith("../"),
|
|
90
|
+
"Path must be relative (start with ./ or ../)"
|
|
91
|
+
);
|
|
92
|
+
var aliasesSchema = z.object({
|
|
93
|
+
components: z.string().optional(),
|
|
94
|
+
utils: z.string().optional()
|
|
95
|
+
}).partial();
|
|
96
|
+
var nativeUIConfigSchema = z.object({
|
|
97
|
+
// Runtime Configuration
|
|
98
|
+
theme: themePresetSchema.optional(),
|
|
99
|
+
radius: radiusPresetSchema.optional(),
|
|
100
|
+
colorScheme: colorSchemeSchema.optional(),
|
|
101
|
+
colors: themeColorsSchema.optional(),
|
|
102
|
+
lightColors: themeColorsSchema.optional(),
|
|
103
|
+
darkColors: themeColorsSchema.optional(),
|
|
104
|
+
fonts: fontsSchema.optional(),
|
|
105
|
+
haptics: z.boolean().optional(),
|
|
106
|
+
components: componentOverridesSchema.optional(),
|
|
107
|
+
// CLI Configuration
|
|
108
|
+
componentsPath: pathSchema.optional(),
|
|
109
|
+
utilsPath: pathSchema.optional(),
|
|
110
|
+
style: stylePresetSchema.optional(),
|
|
111
|
+
aliases: aliasesSchema.optional()
|
|
112
|
+
}).strict();
|
|
113
|
+
function validateConfig(config) {
|
|
114
|
+
const result = nativeUIConfigSchema.safeParse(config);
|
|
115
|
+
if (result.success) {
|
|
116
|
+
return { success: true, data: result.data };
|
|
117
|
+
}
|
|
118
|
+
const errors = result.error.issues.map((issue) => {
|
|
119
|
+
const path10 = issue.path.join(".");
|
|
120
|
+
return path10 ? `${path10}: ${issue.message}` : issue.message;
|
|
121
|
+
});
|
|
122
|
+
return { success: false, errors };
|
|
123
|
+
}
|
|
124
|
+
function validateConfigOrThrow(config, configPath) {
|
|
125
|
+
const result = validateConfig(config);
|
|
126
|
+
if (!result.success) {
|
|
127
|
+
const errorList = result.errors.map((e) => ` - ${e}`).join("\n");
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Invalid configuration in ${configPath}:
|
|
130
|
+
${errorList}
|
|
131
|
+
|
|
132
|
+
See https://nativeui.dev/docs/config for valid options.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return result.data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/utils/config.ts
|
|
139
|
+
var defaultConfig = {
|
|
140
|
+
// Runtime defaults
|
|
141
|
+
theme: "zinc",
|
|
142
|
+
radius: "md",
|
|
143
|
+
colorScheme: "system",
|
|
144
|
+
colors: {},
|
|
145
|
+
lightColors: {},
|
|
146
|
+
darkColors: {},
|
|
147
|
+
fonts: {},
|
|
148
|
+
haptics: true,
|
|
149
|
+
animationPreset: "subtle",
|
|
150
|
+
components: {},
|
|
151
|
+
// CLI defaults
|
|
152
|
+
componentsPath: "./components/ui",
|
|
153
|
+
utilsPath: "./lib/utils",
|
|
154
|
+
style: "default",
|
|
155
|
+
aliases: {
|
|
156
|
+
components: "@/components",
|
|
157
|
+
utils: "@/lib/utils"
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function resolveConfig(config = {}) {
|
|
161
|
+
return {
|
|
162
|
+
theme: config.theme ?? defaultConfig.theme,
|
|
163
|
+
radius: config.radius ?? defaultConfig.radius,
|
|
164
|
+
colorScheme: config.colorScheme ?? defaultConfig.colorScheme,
|
|
165
|
+
colors: config.colors ?? defaultConfig.colors,
|
|
166
|
+
lightColors: config.lightColors ?? defaultConfig.lightColors,
|
|
167
|
+
darkColors: config.darkColors ?? defaultConfig.darkColors,
|
|
168
|
+
fonts: config.fonts ?? defaultConfig.fonts,
|
|
169
|
+
haptics: config.haptics ?? defaultConfig.haptics,
|
|
170
|
+
animationPreset: config.animationPreset ?? defaultConfig.animationPreset,
|
|
171
|
+
components: {
|
|
172
|
+
...defaultConfig.components,
|
|
173
|
+
...config.components
|
|
174
|
+
},
|
|
175
|
+
componentsPath: config.componentsPath ?? defaultConfig.componentsPath,
|
|
176
|
+
utilsPath: config.utilsPath ?? defaultConfig.utilsPath,
|
|
177
|
+
style: config.style ?? defaultConfig.style,
|
|
178
|
+
aliases: {
|
|
179
|
+
...defaultConfig.aliases,
|
|
180
|
+
...config.aliases
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/utils/project.ts
|
|
186
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
187
|
+
async function getProjectRoot(cwd) {
|
|
188
|
+
let dir = cwd;
|
|
189
|
+
while (dir !== path.dirname(dir)) {
|
|
190
|
+
if (await fs.pathExists(path.join(dir, "package.json"))) {
|
|
191
|
+
return dir;
|
|
192
|
+
}
|
|
193
|
+
dir = path.dirname(dir);
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
async function detectProjectType(projectRoot) {
|
|
198
|
+
try {
|
|
199
|
+
const packageJson = await fs.readJson(path.join(projectRoot, "package.json"));
|
|
200
|
+
const deps = {
|
|
201
|
+
...packageJson.dependencies,
|
|
202
|
+
...packageJson.devDependencies
|
|
203
|
+
};
|
|
204
|
+
if (deps["expo"]) {
|
|
205
|
+
return "expo";
|
|
206
|
+
}
|
|
207
|
+
if (deps["react-native"]) {
|
|
208
|
+
return "react-native";
|
|
209
|
+
}
|
|
210
|
+
return "unknown";
|
|
211
|
+
} catch {
|
|
212
|
+
return "unknown";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function getConfig(projectRoot) {
|
|
216
|
+
const tsConfigPath = path.join(projectRoot, "nativeui.config.ts");
|
|
217
|
+
if (await fs.pathExists(tsConfigPath)) {
|
|
218
|
+
return loadTsConfig(tsConfigPath);
|
|
219
|
+
}
|
|
220
|
+
const jsConfigPath = path.join(projectRoot, "nativeui.config.js");
|
|
221
|
+
if (await fs.pathExists(jsConfigPath)) {
|
|
222
|
+
return loadJsConfig(jsConfigPath);
|
|
223
|
+
}
|
|
224
|
+
const jsonConfigPath = path.join(projectRoot, "nativeui.config.json");
|
|
225
|
+
if (await fs.pathExists(jsonConfigPath)) {
|
|
226
|
+
try {
|
|
227
|
+
const rawConfig = await fs.readJson(jsonConfigPath);
|
|
228
|
+
const validatedConfig = validateConfigOrThrow(rawConfig, jsonConfigPath);
|
|
229
|
+
return resolveConfig(validatedConfig);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error instanceof Error && error.message.includes("Invalid configuration")) {
|
|
232
|
+
console.error(chalk.red(error.message));
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
async function loadTsConfig(configPath) {
|
|
241
|
+
try {
|
|
242
|
+
const jiti = createJiti(__filename2, {
|
|
243
|
+
interopDefault: true
|
|
244
|
+
});
|
|
245
|
+
const rawConfig = jiti(configPath);
|
|
246
|
+
const config = rawConfig.default || rawConfig;
|
|
247
|
+
const validatedConfig = validateConfigOrThrow(config, configPath);
|
|
248
|
+
return resolveConfig(validatedConfig);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof Error && error.message.includes("Invalid configuration")) {
|
|
251
|
+
console.error(chalk.red(error.message));
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
console.error(chalk.red("Failed to load config:"), error);
|
|
255
|
+
return getDefaultConfig();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function loadJsConfig(configPath) {
|
|
259
|
+
try {
|
|
260
|
+
const rawConfig = await import(configPath);
|
|
261
|
+
const config = rawConfig.default || rawConfig;
|
|
262
|
+
const validatedConfig = validateConfigOrThrow(config, configPath);
|
|
263
|
+
return resolveConfig(validatedConfig);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error instanceof Error && error.message.includes("Invalid configuration")) {
|
|
266
|
+
console.error(chalk.red(error.message));
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
console.error(chalk.red("Failed to load config:"), error);
|
|
270
|
+
return getDefaultConfig();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function getDefaultConfig() {
|
|
274
|
+
return resolveConfig({});
|
|
275
|
+
}
|
|
276
|
+
async function readPackageJson(projectRoot) {
|
|
277
|
+
try {
|
|
278
|
+
return await fs.readJson(path.join(projectRoot, "package.json"));
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function detectPackageManager(projectRoot) {
|
|
284
|
+
if (await fs.pathExists(path.join(projectRoot, "bun.lockb"))) {
|
|
285
|
+
return "bun";
|
|
286
|
+
}
|
|
287
|
+
if (await fs.pathExists(path.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
288
|
+
return "pnpm";
|
|
289
|
+
}
|
|
290
|
+
if (await fs.pathExists(path.join(projectRoot, "yarn.lock"))) {
|
|
291
|
+
return "yarn";
|
|
292
|
+
}
|
|
293
|
+
return "npm";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/commands/init.ts
|
|
297
|
+
var initCommand = new Command().name("init").description("Initialize nativeui in your project").option("-y, --yes", "Skip prompts and use defaults").option("--cwd <path>", "Working directory", process.cwd()).action(async (options) => {
|
|
298
|
+
const spinner = ora();
|
|
299
|
+
try {
|
|
300
|
+
const cwd = path2.resolve(options.cwd);
|
|
301
|
+
const projectRoot = await getProjectRoot(cwd);
|
|
302
|
+
if (!projectRoot) {
|
|
303
|
+
console.log(chalk2.red("Could not find a valid Expo/React Native project."));
|
|
304
|
+
console.log(chalk2.dim("Make sure you run this command in a project with package.json"));
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
console.log();
|
|
308
|
+
console.log(chalk2.bold("Welcome to nativeui!"));
|
|
309
|
+
console.log(chalk2.dim("The copy-paste component library for Expo/React Native"));
|
|
310
|
+
console.log();
|
|
311
|
+
const projectType = await detectProjectType(projectRoot);
|
|
312
|
+
console.log(chalk2.dim(`Detected: ${projectType}`));
|
|
313
|
+
const configPath = path2.join(projectRoot, "nativeui.config.ts");
|
|
314
|
+
if (await fs2.pathExists(configPath)) {
|
|
315
|
+
console.log(chalk2.yellow("Project already initialized."));
|
|
316
|
+
console.log(chalk2.dim(`Config found at: ${configPath}`));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
let config = {
|
|
320
|
+
componentsPath: "./components/ui",
|
|
321
|
+
utilsPath: "./lib/utils",
|
|
322
|
+
style: "default"
|
|
323
|
+
};
|
|
324
|
+
if (!options.yes) {
|
|
325
|
+
const response = await prompts([
|
|
326
|
+
{
|
|
327
|
+
type: "text",
|
|
328
|
+
name: "componentsPath",
|
|
329
|
+
message: "Where should components be installed?",
|
|
330
|
+
initial: config.componentsPath
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
type: "text",
|
|
334
|
+
name: "utilsPath",
|
|
335
|
+
message: "Where should utilities be installed?",
|
|
336
|
+
initial: config.utilsPath
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
type: "select",
|
|
340
|
+
name: "style",
|
|
341
|
+
message: "Which style preset?",
|
|
342
|
+
choices: [
|
|
343
|
+
{ title: "Default", value: "default" },
|
|
344
|
+
{ title: "iOS", value: "ios" },
|
|
345
|
+
{ title: "Material", value: "material" }
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
]);
|
|
349
|
+
config = { ...config, ...response };
|
|
350
|
+
}
|
|
351
|
+
spinner.start("Creating configuration...");
|
|
352
|
+
const configContent = `/**
|
|
353
|
+
* NativeUI Configuration
|
|
354
|
+
*
|
|
355
|
+
* This file defines your app's design system.
|
|
356
|
+
* All components will automatically use these values.
|
|
357
|
+
*/
|
|
358
|
+
|
|
359
|
+
import { defineConfig } from '@nativeui/core';
|
|
360
|
+
|
|
361
|
+
export default defineConfig({
|
|
362
|
+
// ============================================
|
|
363
|
+
// Theme Configuration (runtime)
|
|
364
|
+
// ============================================
|
|
365
|
+
|
|
366
|
+
// Color theme preset: 'zinc' | 'slate' | 'stone' | 'blue' | 'green' | 'rose' | 'orange' | 'violet'
|
|
367
|
+
theme: 'blue',
|
|
368
|
+
|
|
369
|
+
// Border radius preset: 'none' | 'sm' | 'md' | 'lg' | 'full'
|
|
370
|
+
radius: 'md',
|
|
371
|
+
|
|
372
|
+
// Default color scheme: 'light' | 'dark' | 'system'
|
|
373
|
+
colorScheme: 'system',
|
|
374
|
+
|
|
375
|
+
// Custom color overrides (optional)
|
|
376
|
+
// colors: {
|
|
377
|
+
// primary: '#6366F1',
|
|
378
|
+
// },
|
|
379
|
+
|
|
380
|
+
// Component defaults (optional)
|
|
381
|
+
// components: {
|
|
382
|
+
// button: { defaultVariant: 'default', defaultSize: 'md' },
|
|
383
|
+
// },
|
|
384
|
+
|
|
385
|
+
// ============================================
|
|
386
|
+
// CLI Configuration (used by npx nativeui add)
|
|
387
|
+
// ============================================
|
|
388
|
+
|
|
389
|
+
// Path where components will be installed
|
|
390
|
+
componentsPath: '${config.componentsPath}',
|
|
391
|
+
|
|
392
|
+
// Path where utilities (cn, etc.) will be installed
|
|
393
|
+
utilsPath: '${config.utilsPath}',
|
|
394
|
+
|
|
395
|
+
// Style preset: 'default' | 'ios' | 'material'
|
|
396
|
+
style: '${config.style}',
|
|
397
|
+
|
|
398
|
+
// Path aliases for imports
|
|
399
|
+
aliases: {
|
|
400
|
+
components: '@/components',
|
|
401
|
+
utils: '@/lib/utils',
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
`;
|
|
405
|
+
await fs2.writeFile(configPath, configContent);
|
|
406
|
+
spinner.succeed("Configuration created");
|
|
407
|
+
spinner.start("Creating directories...");
|
|
408
|
+
const componentsDir = path2.join(projectRoot, config.componentsPath);
|
|
409
|
+
const utilsDir = path2.join(projectRoot, config.utilsPath);
|
|
410
|
+
await fs2.ensureDir(componentsDir);
|
|
411
|
+
await fs2.ensureDir(utilsDir);
|
|
412
|
+
spinner.succeed("Directories created");
|
|
413
|
+
spinner.start("Installing utilities...");
|
|
414
|
+
const cnContent = `import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native';
|
|
415
|
+
|
|
416
|
+
type Style = ViewStyle | TextStyle | ImageStyle;
|
|
417
|
+
type StyleInput = Style | Style[] | null | undefined | false;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Merge styles utility
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* \`\`\`tsx
|
|
424
|
+
* const styles = cn(
|
|
425
|
+
* baseStyles.container,
|
|
426
|
+
* isActive && activeStyles,
|
|
427
|
+
* { padding: 16 }
|
|
428
|
+
* );
|
|
429
|
+
* \`\`\`
|
|
430
|
+
*/
|
|
431
|
+
export function cn(...inputs: StyleInput[]): Style {
|
|
432
|
+
const styles: Style[] = [];
|
|
433
|
+
|
|
434
|
+
for (const input of inputs) {
|
|
435
|
+
if (!input) continue;
|
|
436
|
+
|
|
437
|
+
if (Array.isArray(input)) {
|
|
438
|
+
const flattened = StyleSheet.flatten(input);
|
|
439
|
+
if (flattened) styles.push(flattened);
|
|
440
|
+
} else {
|
|
441
|
+
styles.push(input);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return StyleSheet.flatten(styles);
|
|
446
|
+
}
|
|
447
|
+
`;
|
|
448
|
+
await fs2.writeFile(path2.join(utilsDir, "cn.ts"), cnContent);
|
|
449
|
+
await fs2.writeFile(
|
|
450
|
+
path2.join(utilsDir, "index.ts"),
|
|
451
|
+
`export { cn } from './cn';
|
|
452
|
+
`
|
|
453
|
+
);
|
|
454
|
+
spinner.succeed("Utilities installed");
|
|
455
|
+
console.log();
|
|
456
|
+
console.log(chalk2.green("Success!") + " nativeui initialized.");
|
|
457
|
+
console.log();
|
|
458
|
+
console.log("Next steps:");
|
|
459
|
+
console.log(chalk2.dim(" 1."), "Add your first component:");
|
|
460
|
+
console.log(chalk2.cyan(" npx nativeui add button"));
|
|
461
|
+
console.log();
|
|
462
|
+
console.log(chalk2.dim(" 2."), "Browse available components:");
|
|
463
|
+
console.log(chalk2.cyan(" npx nativeui list"));
|
|
464
|
+
console.log();
|
|
465
|
+
} catch (error) {
|
|
466
|
+
spinner.fail("Failed to initialize");
|
|
467
|
+
console.error(error);
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// src/commands/add.ts
|
|
473
|
+
import { Command as Command2 } from "commander";
|
|
474
|
+
import chalk3 from "chalk";
|
|
475
|
+
import ora2 from "ora";
|
|
476
|
+
import prompts2 from "prompts";
|
|
477
|
+
import fs4 from "fs-extra";
|
|
478
|
+
import path4 from "path";
|
|
479
|
+
|
|
480
|
+
// src/utils/registry.ts
|
|
481
|
+
import fs3 from "fs-extra";
|
|
482
|
+
import path3 from "path";
|
|
483
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
484
|
+
var __filename3 = fileURLToPath2(import.meta.url);
|
|
485
|
+
var __dirname2 = path3.dirname(__filename3);
|
|
486
|
+
var REGISTRY_URL = process.env.NATIVEUI_REGISTRY_URL;
|
|
487
|
+
function getLocalRegistryPath() {
|
|
488
|
+
return path3.resolve(__dirname2, "..", "..", "registry");
|
|
489
|
+
}
|
|
490
|
+
function isLocalMode() {
|
|
491
|
+
if (REGISTRY_URL) return false;
|
|
492
|
+
const localPath = getLocalRegistryPath();
|
|
493
|
+
return fs3.pathExistsSync(path3.join(localPath, "registry.json"));
|
|
494
|
+
}
|
|
495
|
+
async function getRegistry() {
|
|
496
|
+
if (isLocalMode()) {
|
|
497
|
+
return getLocalRegistry();
|
|
498
|
+
}
|
|
499
|
+
return getRemoteRegistry();
|
|
500
|
+
}
|
|
501
|
+
async function getLocalRegistry() {
|
|
502
|
+
const registryPath = path3.join(getLocalRegistryPath(), "registry.json");
|
|
503
|
+
try {
|
|
504
|
+
const registry = await fs3.readJson(registryPath);
|
|
505
|
+
return registry.components;
|
|
506
|
+
} catch (error) {
|
|
507
|
+
console.error("Failed to load local registry:", error);
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function getRemoteRegistry() {
|
|
512
|
+
if (!REGISTRY_URL) {
|
|
513
|
+
throw new Error("NATIVEUI_REGISTRY_URL not configured");
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const response = await fetch(`${REGISTRY_URL}/registry.json`);
|
|
517
|
+
const registry = await response.json();
|
|
518
|
+
return registry.components;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
console.error("Failed to fetch remote registry:", error);
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async function fetchComponent(name) {
|
|
525
|
+
const registry = await getRegistry();
|
|
526
|
+
const item = registry.find((i) => i.name === name);
|
|
527
|
+
if (!item) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
if (isLocalMode()) {
|
|
531
|
+
return fetchLocalComponent(item);
|
|
532
|
+
}
|
|
533
|
+
return fetchRemoteComponent(item);
|
|
534
|
+
}
|
|
535
|
+
async function fetchLocalComponent(item) {
|
|
536
|
+
const registryPath = getLocalRegistryPath();
|
|
537
|
+
const files = await Promise.all(
|
|
538
|
+
item.files.map(async (filePath) => {
|
|
539
|
+
const fullPath = path3.join(registryPath, filePath);
|
|
540
|
+
const content = await fs3.readFile(fullPath, "utf-8");
|
|
541
|
+
const name = path3.basename(filePath);
|
|
542
|
+
return { name, content };
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
return {
|
|
546
|
+
name: item.name,
|
|
547
|
+
files,
|
|
548
|
+
dependencies: item.dependencies,
|
|
549
|
+
devDependencies: item.devDependencies,
|
|
550
|
+
registryDependencies: item.registryDependencies
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async function fetchRemoteComponent(item) {
|
|
554
|
+
if (!REGISTRY_URL) {
|
|
555
|
+
throw new Error("NATIVEUI_REGISTRY_URL not configured");
|
|
556
|
+
}
|
|
557
|
+
const files = await Promise.all(
|
|
558
|
+
item.files.map(async (filePath) => {
|
|
559
|
+
const response = await fetch(`${REGISTRY_URL}/${filePath}`);
|
|
560
|
+
const content = await response.text();
|
|
561
|
+
const name = path3.basename(filePath);
|
|
562
|
+
return { name, content };
|
|
563
|
+
})
|
|
564
|
+
);
|
|
565
|
+
return {
|
|
566
|
+
name: item.name,
|
|
567
|
+
files,
|
|
568
|
+
dependencies: item.dependencies,
|
|
569
|
+
devDependencies: item.devDependencies,
|
|
570
|
+
registryDependencies: item.registryDependencies
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/commands/add.ts
|
|
575
|
+
var addCommand = new Command2().name("add").description("Add a component to your project").argument("[components...]", "Components to add").option("-y, --yes", "Skip confirmation").option("-o, --overwrite", "Overwrite existing files").option("--cwd <path>", "Working directory", process.cwd()).action(async (components, options) => {
|
|
576
|
+
const spinner = ora2();
|
|
577
|
+
try {
|
|
578
|
+
const cwd = path4.resolve(options.cwd);
|
|
579
|
+
const projectRoot = await getProjectRoot(cwd);
|
|
580
|
+
if (!projectRoot) {
|
|
581
|
+
console.log(chalk3.red("Could not find a valid project."));
|
|
582
|
+
console.log(chalk3.dim("Run `npx nativeui init` first."));
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
const config = await getConfig(projectRoot);
|
|
586
|
+
if (!config) {
|
|
587
|
+
console.log(chalk3.red("Project not initialized."));
|
|
588
|
+
console.log(chalk3.dim("Run `npx nativeui init` first."));
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
if (!components.length) {
|
|
592
|
+
const registry = await getRegistry();
|
|
593
|
+
if (!registry.length) {
|
|
594
|
+
console.log(chalk3.red("No components found in registry."));
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
const { selected } = await prompts2({
|
|
598
|
+
type: "multiselect",
|
|
599
|
+
name: "selected",
|
|
600
|
+
message: "Which components would you like to add?",
|
|
601
|
+
choices: registry.map((item) => ({
|
|
602
|
+
title: `${item.name} ${chalk3.dim(`(${item.status})`)}`,
|
|
603
|
+
value: item.name,
|
|
604
|
+
description: item.description
|
|
605
|
+
})),
|
|
606
|
+
hint: "- Space to select, Enter to confirm"
|
|
607
|
+
});
|
|
608
|
+
if (!selected?.length) {
|
|
609
|
+
console.log(chalk3.dim("No components selected."));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
components = selected;
|
|
613
|
+
}
|
|
614
|
+
console.log();
|
|
615
|
+
console.log(chalk3.bold("Adding components:"));
|
|
616
|
+
components.forEach((c) => console.log(chalk3.dim(` - ${c}`)));
|
|
617
|
+
console.log();
|
|
618
|
+
const allDependencies = [];
|
|
619
|
+
const allDevDependencies = [];
|
|
620
|
+
for (const componentName of components) {
|
|
621
|
+
spinner.start(`Fetching ${componentName}...`);
|
|
622
|
+
try {
|
|
623
|
+
const component = await fetchComponent(componentName);
|
|
624
|
+
if (!component) {
|
|
625
|
+
spinner.fail(`Component "${componentName}" not found`);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const targetDir = path4.join(projectRoot, config.componentsPath);
|
|
629
|
+
for (const file of component.files) {
|
|
630
|
+
const targetPath = path4.join(targetDir, file.name);
|
|
631
|
+
if (await fs4.pathExists(targetPath) && !options.overwrite) {
|
|
632
|
+
spinner.warn(`${file.name} already exists (use --overwrite)`);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
await fs4.ensureDir(targetDir);
|
|
636
|
+
const transformedContent = transformImports(file.content, config);
|
|
637
|
+
await fs4.writeFile(targetPath, transformedContent);
|
|
638
|
+
}
|
|
639
|
+
spinner.succeed(`Added ${componentName}`);
|
|
640
|
+
if (component.dependencies?.length) {
|
|
641
|
+
allDependencies.push(...component.dependencies);
|
|
642
|
+
}
|
|
643
|
+
if (component.devDependencies?.length) {
|
|
644
|
+
allDevDependencies.push(...component.devDependencies);
|
|
645
|
+
}
|
|
646
|
+
if (component.registryDependencies?.length) {
|
|
647
|
+
console.log(
|
|
648
|
+
chalk3.dim(` Requires: ${component.registryDependencies.join(", ")}`)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
} catch (error) {
|
|
652
|
+
spinner.fail(`Failed to add ${componentName}`);
|
|
653
|
+
console.error(chalk3.dim(String(error)));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const uniqueDeps = [...new Set(allDependencies)];
|
|
657
|
+
const uniqueDevDeps = [...new Set(allDevDependencies)];
|
|
658
|
+
if (uniqueDeps.length || uniqueDevDeps.length) {
|
|
659
|
+
console.log();
|
|
660
|
+
console.log(chalk3.bold("Install dependencies:"));
|
|
661
|
+
if (uniqueDeps.length) {
|
|
662
|
+
console.log(chalk3.cyan(` npx expo install ${uniqueDeps.join(" ")}`));
|
|
663
|
+
}
|
|
664
|
+
if (uniqueDevDeps.length) {
|
|
665
|
+
console.log(chalk3.cyan(` npm install -D ${uniqueDevDeps.join(" ")}`));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
console.log();
|
|
669
|
+
console.log(chalk3.green("Done!"));
|
|
670
|
+
} catch (error) {
|
|
671
|
+
spinner.fail("Failed");
|
|
672
|
+
console.error(error);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
function transformImports(code, config) {
|
|
677
|
+
let transformed = code;
|
|
678
|
+
const utilsAlias = config.aliases?.utils || "@/lib/utils";
|
|
679
|
+
if (utilsAlias === "@/lib/utils") {
|
|
680
|
+
return transformed;
|
|
681
|
+
}
|
|
682
|
+
transformed = transformed.replace(
|
|
683
|
+
/from ['"]@\/lib\/utils['"]/g,
|
|
684
|
+
`from '${utilsAlias}'`
|
|
685
|
+
);
|
|
686
|
+
return transformed;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/commands/list.ts
|
|
690
|
+
import { Command as Command3 } from "commander";
|
|
691
|
+
import chalk4 from "chalk";
|
|
692
|
+
import ora3 from "ora";
|
|
693
|
+
var listCommand = new Command3().name("list").description("List available components").option("-c, --category <category>", "Filter by category").action(async (options) => {
|
|
694
|
+
const spinner = ora3("Fetching components...").start();
|
|
695
|
+
try {
|
|
696
|
+
const registry = await getRegistry();
|
|
697
|
+
spinner.stop();
|
|
698
|
+
console.log();
|
|
699
|
+
console.log(chalk4.bold("Available Components"));
|
|
700
|
+
console.log();
|
|
701
|
+
const categories = /* @__PURE__ */ new Map();
|
|
702
|
+
for (const item of registry) {
|
|
703
|
+
const category = item.category || "Other";
|
|
704
|
+
if (options.category && category.toLowerCase() !== options.category.toLowerCase()) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (!categories.has(category)) {
|
|
708
|
+
categories.set(category, []);
|
|
709
|
+
}
|
|
710
|
+
categories.get(category).push(item);
|
|
711
|
+
}
|
|
712
|
+
for (const [category, items] of categories) {
|
|
713
|
+
console.log(chalk4.cyan.bold(`${category}`));
|
|
714
|
+
for (const item of items) {
|
|
715
|
+
const status = item.status === "stable" ? "" : chalk4.yellow(` [${item.status}]`);
|
|
716
|
+
console.log(` ${chalk4.white(item.name)}${status}`);
|
|
717
|
+
console.log(chalk4.dim(` ${item.description}`));
|
|
718
|
+
}
|
|
719
|
+
console.log();
|
|
720
|
+
}
|
|
721
|
+
console.log(chalk4.dim("Add a component: npx nativeui add <component>"));
|
|
722
|
+
console.log();
|
|
723
|
+
} catch (error) {
|
|
724
|
+
spinner.fail("Failed to fetch components");
|
|
725
|
+
console.error(error);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// src/commands/doctor.ts
|
|
731
|
+
import { Command as Command4 } from "commander";
|
|
732
|
+
import chalk5 from "chalk";
|
|
733
|
+
import fs5 from "fs-extra";
|
|
734
|
+
import path5 from "path";
|
|
735
|
+
var REQUIRED_PEER_DEPS = [
|
|
736
|
+
{ name: "react-native-reanimated", minVersion: "3.0.0" },
|
|
737
|
+
{ name: "react-native-gesture-handler", minVersion: "2.0.0" },
|
|
738
|
+
{ name: "react-native-safe-area-context", minVersion: "4.0.0" }
|
|
739
|
+
];
|
|
740
|
+
var RECOMMENDED_DEPS = [
|
|
741
|
+
{ name: "@nativeui/core", reason: "Theme system and utilities" }
|
|
742
|
+
];
|
|
743
|
+
function parseVersion(version) {
|
|
744
|
+
const cleaned = version.replace(/^[\^~>=<]+/, "").replace(/^workspace:\*?/, "");
|
|
745
|
+
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
746
|
+
if (!match) return null;
|
|
747
|
+
return {
|
|
748
|
+
major: parseInt(match[1], 10),
|
|
749
|
+
minor: parseInt(match[2], 10),
|
|
750
|
+
patch: parseInt(match[3], 10)
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function meetsMinVersion(current, minimum) {
|
|
754
|
+
const currentParsed = parseVersion(current);
|
|
755
|
+
const minimumParsed = parseVersion(minimum);
|
|
756
|
+
if (!currentParsed || !minimumParsed) return true;
|
|
757
|
+
if (currentParsed.major > minimumParsed.major) return true;
|
|
758
|
+
if (currentParsed.major < minimumParsed.major) return false;
|
|
759
|
+
if (currentParsed.minor > minimumParsed.minor) return true;
|
|
760
|
+
if (currentParsed.minor < minimumParsed.minor) return false;
|
|
761
|
+
return currentParsed.patch >= minimumParsed.patch;
|
|
762
|
+
}
|
|
763
|
+
async function checkProjectStructure(projectRoot) {
|
|
764
|
+
const packageJson = await readPackageJson(projectRoot);
|
|
765
|
+
if (!packageJson) {
|
|
766
|
+
return {
|
|
767
|
+
name: "Project Structure",
|
|
768
|
+
status: "fail",
|
|
769
|
+
message: "No package.json found",
|
|
770
|
+
fix: "Run this command in a valid Node.js project"
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
name: "Project Structure",
|
|
775
|
+
status: "pass",
|
|
776
|
+
message: "Valid package.json found"
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
async function checkProjectType(projectRoot) {
|
|
780
|
+
const projectType = await detectProjectType(projectRoot);
|
|
781
|
+
if (projectType === "unknown") {
|
|
782
|
+
return {
|
|
783
|
+
name: "Project Type",
|
|
784
|
+
status: "fail",
|
|
785
|
+
message: "Not an Expo or React Native project",
|
|
786
|
+
fix: "nativeui requires Expo or React Native"
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
name: "Project Type",
|
|
791
|
+
status: "pass",
|
|
792
|
+
message: projectType === "expo" ? "Expo project detected" : "React Native project detected"
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
async function checkInitialized(projectRoot) {
|
|
796
|
+
const configFiles = [
|
|
797
|
+
"nativeui.config.ts",
|
|
798
|
+
"nativeui.config.js",
|
|
799
|
+
"nativeui.config.json"
|
|
800
|
+
];
|
|
801
|
+
let foundConfig = null;
|
|
802
|
+
for (const file of configFiles) {
|
|
803
|
+
if (await fs5.pathExists(path5.join(projectRoot, file))) {
|
|
804
|
+
foundConfig = file;
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (!foundConfig) {
|
|
809
|
+
return {
|
|
810
|
+
name: "NativeUI Config",
|
|
811
|
+
status: "fail",
|
|
812
|
+
message: "nativeui.config.ts not found",
|
|
813
|
+
fix: "Run: npx nativeui init"
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
const configPath = path5.join(projectRoot, foundConfig);
|
|
818
|
+
const content = await fs5.readFile(configPath, "utf-8");
|
|
819
|
+
if (foundConfig.endsWith(".ts") || foundConfig.endsWith(".js")) {
|
|
820
|
+
const hasDefineConfig = content.includes("defineConfig");
|
|
821
|
+
const hasExport = content.includes("export default");
|
|
822
|
+
if (!hasExport) {
|
|
823
|
+
return {
|
|
824
|
+
name: "NativeUI Config",
|
|
825
|
+
status: "warn",
|
|
826
|
+
message: "Config file missing default export",
|
|
827
|
+
fix: "Add: export default defineConfig({ ... })"
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if (!hasDefineConfig) {
|
|
831
|
+
return {
|
|
832
|
+
name: "NativeUI Config",
|
|
833
|
+
status: "warn",
|
|
834
|
+
message: "Config not using defineConfig helper",
|
|
835
|
+
fix: "Wrap config with: defineConfig({ ... })"
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (foundConfig.endsWith(".json")) {
|
|
840
|
+
try {
|
|
841
|
+
JSON.parse(content);
|
|
842
|
+
} catch {
|
|
843
|
+
return {
|
|
844
|
+
name: "NativeUI Config",
|
|
845
|
+
status: "fail",
|
|
846
|
+
message: "Invalid JSON in config file",
|
|
847
|
+
fix: "Check JSON syntax in nativeui.config.json"
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
name: "NativeUI Config",
|
|
853
|
+
status: "pass",
|
|
854
|
+
message: `Found ${foundConfig}`
|
|
855
|
+
};
|
|
856
|
+
} catch (error) {
|
|
857
|
+
return {
|
|
858
|
+
name: "NativeUI Config",
|
|
859
|
+
status: "fail",
|
|
860
|
+
message: "Could not read config file",
|
|
861
|
+
fix: error instanceof Error ? error.message : "Check file permissions"
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function checkPaths(projectRoot) {
|
|
866
|
+
const defaultComponentsPath = "./components/ui";
|
|
867
|
+
const defaultUtilsPath = "./lib/utils";
|
|
868
|
+
let componentsPathValue = defaultComponentsPath;
|
|
869
|
+
let utilsPathValue = defaultUtilsPath;
|
|
870
|
+
const configFiles = ["nativeui.config.ts", "nativeui.config.js", "nativeui.config.json"];
|
|
871
|
+
for (const file of configFiles) {
|
|
872
|
+
const configPath = path5.join(projectRoot, file);
|
|
873
|
+
if (await fs5.pathExists(configPath)) {
|
|
874
|
+
try {
|
|
875
|
+
const content = await fs5.readFile(configPath, "utf-8");
|
|
876
|
+
const componentsMatch = content.match(/componentsPath:\s*['"]([^'"]+)['"]/);
|
|
877
|
+
const utilsMatch = content.match(/utilsPath:\s*['"]([^'"]+)['"]/);
|
|
878
|
+
if (componentsMatch) componentsPathValue = componentsMatch[1];
|
|
879
|
+
if (utilsMatch) utilsPathValue = utilsMatch[1];
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const componentsPath = path5.join(projectRoot, componentsPathValue);
|
|
886
|
+
const utilsPath = path5.join(projectRoot, utilsPathValue);
|
|
887
|
+
const componentsExist = await fs5.pathExists(componentsPath);
|
|
888
|
+
const utilsExist = await fs5.pathExists(utilsPath);
|
|
889
|
+
if (!componentsExist && !utilsExist) {
|
|
890
|
+
return {
|
|
891
|
+
name: "Component Paths",
|
|
892
|
+
status: "warn",
|
|
893
|
+
message: "Component and utils directories not created yet",
|
|
894
|
+
fix: `Add a component: npx nativeui add button`
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (!componentsExist) {
|
|
898
|
+
return {
|
|
899
|
+
name: "Component Paths",
|
|
900
|
+
status: "warn",
|
|
901
|
+
message: `Components directory not found: ${componentsPathValue}`,
|
|
902
|
+
fix: "Add a component to create it: npx nativeui add button"
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
name: "Component Paths",
|
|
907
|
+
status: "pass",
|
|
908
|
+
message: `Components: ${componentsPathValue}`
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
async function checkPeerDependencies(projectRoot) {
|
|
912
|
+
const packageJson = await readPackageJson(projectRoot);
|
|
913
|
+
const results = [];
|
|
914
|
+
if (!packageJson) return results;
|
|
915
|
+
const deps = {
|
|
916
|
+
...packageJson.dependencies,
|
|
917
|
+
...packageJson.devDependencies
|
|
918
|
+
};
|
|
919
|
+
for (const { name, minVersion } of REQUIRED_PEER_DEPS) {
|
|
920
|
+
const version = deps[name];
|
|
921
|
+
if (!version) {
|
|
922
|
+
results.push({
|
|
923
|
+
name: `Dependency: ${name}`,
|
|
924
|
+
status: "fail",
|
|
925
|
+
message: "Not installed",
|
|
926
|
+
fix: `Run: npx expo install ${name}`
|
|
927
|
+
});
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if (!meetsMinVersion(version, minVersion)) {
|
|
931
|
+
results.push({
|
|
932
|
+
name: `Dependency: ${name}`,
|
|
933
|
+
status: "warn",
|
|
934
|
+
message: `Version ${version} may be outdated (recommended: >=${minVersion})`,
|
|
935
|
+
fix: `Run: npx expo install ${name}`
|
|
936
|
+
});
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
results.push({
|
|
940
|
+
name: `Dependency: ${name}`,
|
|
941
|
+
status: "pass",
|
|
942
|
+
message: `Installed (${version})`
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
return results;
|
|
946
|
+
}
|
|
947
|
+
async function checkRecommendedDependencies(projectRoot) {
|
|
948
|
+
const packageJson = await readPackageJson(projectRoot);
|
|
949
|
+
const results = [];
|
|
950
|
+
if (!packageJson) return results;
|
|
951
|
+
const deps = {
|
|
952
|
+
...packageJson.dependencies,
|
|
953
|
+
...packageJson.devDependencies
|
|
954
|
+
};
|
|
955
|
+
for (const { name, reason } of RECOMMENDED_DEPS) {
|
|
956
|
+
const version = deps[name];
|
|
957
|
+
if (!version) {
|
|
958
|
+
results.push({
|
|
959
|
+
name: `Dependency: ${name}`,
|
|
960
|
+
status: "warn",
|
|
961
|
+
message: `Not installed - ${reason}`,
|
|
962
|
+
fix: `Run: npm install ${name}`
|
|
963
|
+
});
|
|
964
|
+
} else {
|
|
965
|
+
results.push({
|
|
966
|
+
name: `Dependency: ${name}`,
|
|
967
|
+
status: "pass",
|
|
968
|
+
message: `Installed (${version})`
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return results;
|
|
973
|
+
}
|
|
974
|
+
async function checkBabelConfig(projectRoot) {
|
|
975
|
+
const babelConfigPaths = [
|
|
976
|
+
"babel.config.js",
|
|
977
|
+
"babel.config.cjs",
|
|
978
|
+
"babel.config.mjs",
|
|
979
|
+
".babelrc",
|
|
980
|
+
".babelrc.js"
|
|
981
|
+
];
|
|
982
|
+
let babelConfigPath = null;
|
|
983
|
+
let babelContent = null;
|
|
984
|
+
for (const configFile of babelConfigPaths) {
|
|
985
|
+
const fullPath = path5.join(projectRoot, configFile);
|
|
986
|
+
if (await fs5.pathExists(fullPath)) {
|
|
987
|
+
babelConfigPath = configFile;
|
|
988
|
+
babelContent = await fs5.readFile(fullPath, "utf-8");
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (!babelConfigPath || !babelContent) {
|
|
993
|
+
return {
|
|
994
|
+
name: "Babel Config",
|
|
995
|
+
status: "warn",
|
|
996
|
+
message: "No babel.config.js found",
|
|
997
|
+
fix: "Create babel.config.js with Reanimated plugin"
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
const hasReanimatedPlugin = babelContent.includes("react-native-reanimated/plugin") || babelContent.includes("'react-native-reanimated/plugin'") || babelContent.includes('"react-native-reanimated/plugin"');
|
|
1001
|
+
if (!hasReanimatedPlugin) {
|
|
1002
|
+
return {
|
|
1003
|
+
name: "Babel Config",
|
|
1004
|
+
status: "fail",
|
|
1005
|
+
message: "Reanimated plugin not configured",
|
|
1006
|
+
fix: `Add 'react-native-reanimated/plugin' to plugins in ${babelConfigPath}`
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
name: "Babel Config",
|
|
1011
|
+
status: "pass",
|
|
1012
|
+
message: "Reanimated plugin configured"
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
async function checkTypeScript(projectRoot) {
|
|
1016
|
+
const tsconfigPath = path5.join(projectRoot, "tsconfig.json");
|
|
1017
|
+
if (!await fs5.pathExists(tsconfigPath)) {
|
|
1018
|
+
return {
|
|
1019
|
+
name: "TypeScript",
|
|
1020
|
+
status: "warn",
|
|
1021
|
+
message: "No tsconfig.json found",
|
|
1022
|
+
fix: "TypeScript is recommended but not required"
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
const tsconfig = await fs5.readJson(tsconfigPath);
|
|
1027
|
+
const hasPathAliases = tsconfig.compilerOptions?.paths;
|
|
1028
|
+
if (!hasPathAliases) {
|
|
1029
|
+
return {
|
|
1030
|
+
name: "TypeScript",
|
|
1031
|
+
status: "warn",
|
|
1032
|
+
message: "No path aliases configured",
|
|
1033
|
+
fix: "Consider adding @/* path alias for cleaner imports"
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
const hasComponentsAlias = hasPathAliases["@/components/*"] || hasPathAliases["@/components"];
|
|
1037
|
+
const hasUtilsAlias = hasPathAliases["@/lib/*"] || hasPathAliases["@/*"];
|
|
1038
|
+
if (!hasComponentsAlias) {
|
|
1039
|
+
return {
|
|
1040
|
+
name: "TypeScript",
|
|
1041
|
+
status: "warn",
|
|
1042
|
+
message: "Missing @/components path alias",
|
|
1043
|
+
fix: 'Add "@/components/*": ["./components/*"] to tsconfig.json paths'
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
name: "TypeScript",
|
|
1048
|
+
status: "pass",
|
|
1049
|
+
message: "Configured with path aliases"
|
|
1050
|
+
};
|
|
1051
|
+
} catch {
|
|
1052
|
+
return {
|
|
1053
|
+
name: "TypeScript",
|
|
1054
|
+
status: "warn",
|
|
1055
|
+
message: "Could not parse tsconfig.json"
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
async function checkExpoGo(projectRoot) {
|
|
1060
|
+
const projectType = await detectProjectType(projectRoot);
|
|
1061
|
+
if (projectType !== "expo") return null;
|
|
1062
|
+
const packageJson = await readPackageJson(projectRoot);
|
|
1063
|
+
if (!packageJson) return null;
|
|
1064
|
+
const deps = {
|
|
1065
|
+
...packageJson.dependencies,
|
|
1066
|
+
...packageJson.devDependencies
|
|
1067
|
+
};
|
|
1068
|
+
const incompatiblePackages = [
|
|
1069
|
+
"react-native-mmkv",
|
|
1070
|
+
"@shopify/flash-list"
|
|
1071
|
+
// Now compatible, but leaving as example pattern
|
|
1072
|
+
];
|
|
1073
|
+
const foundIncompatible = incompatiblePackages.filter((pkg) => deps[pkg]);
|
|
1074
|
+
if (foundIncompatible.length > 0) {
|
|
1075
|
+
return {
|
|
1076
|
+
name: "Expo Go Compatibility",
|
|
1077
|
+
status: "warn",
|
|
1078
|
+
message: `Packages may require dev build: ${foundIncompatible.join(", ")}`,
|
|
1079
|
+
fix: "Run: npx expo prebuild for native features"
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
name: "Expo Go Compatibility",
|
|
1084
|
+
status: "pass",
|
|
1085
|
+
message: "No known incompatibilities detected"
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
function printReport(report) {
|
|
1089
|
+
console.log();
|
|
1090
|
+
console.log(chalk5.bold("NativeUI Doctor"));
|
|
1091
|
+
console.log(chalk5.dim("Checking your project setup..."));
|
|
1092
|
+
console.log();
|
|
1093
|
+
console.log(chalk5.dim("Project:"), report.projectRoot);
|
|
1094
|
+
console.log(chalk5.dim("Type:"), report.projectType);
|
|
1095
|
+
console.log(chalk5.dim("Package Manager:"), report.packageManager);
|
|
1096
|
+
console.log();
|
|
1097
|
+
console.log(chalk5.bold("Checks"));
|
|
1098
|
+
console.log();
|
|
1099
|
+
for (const check of report.checks) {
|
|
1100
|
+
const icon = check.status === "pass" ? chalk5.green("\u2713") : check.status === "warn" ? chalk5.yellow("!") : chalk5.red("\u2717");
|
|
1101
|
+
const statusColor = check.status === "pass" ? chalk5.green : check.status === "warn" ? chalk5.yellow : chalk5.red;
|
|
1102
|
+
console.log(` ${icon} ${chalk5.white(check.name)}`);
|
|
1103
|
+
console.log(` ${statusColor(check.message)}`);
|
|
1104
|
+
if (check.fix && check.status !== "pass") {
|
|
1105
|
+
console.log(chalk5.dim(` Fix: ${check.fix}`));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
console.log();
|
|
1109
|
+
const summaryParts = [];
|
|
1110
|
+
if (report.passed > 0) {
|
|
1111
|
+
summaryParts.push(chalk5.green(`${report.passed} passed`));
|
|
1112
|
+
}
|
|
1113
|
+
if (report.warnings > 0) {
|
|
1114
|
+
summaryParts.push(chalk5.yellow(`${report.warnings} warnings`));
|
|
1115
|
+
}
|
|
1116
|
+
if (report.failed > 0) {
|
|
1117
|
+
summaryParts.push(chalk5.red(`${report.failed} failed`));
|
|
1118
|
+
}
|
|
1119
|
+
console.log(chalk5.bold("Summary:"), summaryParts.join(", "));
|
|
1120
|
+
console.log();
|
|
1121
|
+
if (report.failed > 0) {
|
|
1122
|
+
console.log(chalk5.red("Some checks failed. Please fix the issues above."));
|
|
1123
|
+
} else if (report.warnings > 0) {
|
|
1124
|
+
console.log(chalk5.yellow("Your project has some warnings but should work."));
|
|
1125
|
+
} else {
|
|
1126
|
+
console.log(chalk5.green("Your project is properly configured!"));
|
|
1127
|
+
}
|
|
1128
|
+
console.log();
|
|
1129
|
+
}
|
|
1130
|
+
var doctorCommand = new Command4().name("doctor").description("Check project setup and diagnose common issues").option("--cwd <path>", "Working directory", process.cwd()).option("--json", "Output results as JSON").action(async (options) => {
|
|
1131
|
+
try {
|
|
1132
|
+
const cwd = path5.resolve(options.cwd);
|
|
1133
|
+
const projectRoot = await getProjectRoot(cwd);
|
|
1134
|
+
if (!projectRoot) {
|
|
1135
|
+
console.log(chalk5.red("Could not find a valid project."));
|
|
1136
|
+
console.log(chalk5.dim("Make sure you run this command in a project directory."));
|
|
1137
|
+
process.exit(1);
|
|
1138
|
+
}
|
|
1139
|
+
const projectType = await detectProjectType(projectRoot);
|
|
1140
|
+
const packageManager = await detectPackageManager(projectRoot);
|
|
1141
|
+
const checks = [];
|
|
1142
|
+
checks.push(await checkProjectStructure(projectRoot));
|
|
1143
|
+
checks.push(await checkProjectType(projectRoot));
|
|
1144
|
+
checks.push(await checkInitialized(projectRoot));
|
|
1145
|
+
checks.push(await checkPaths(projectRoot));
|
|
1146
|
+
checks.push(...await checkPeerDependencies(projectRoot));
|
|
1147
|
+
checks.push(...await checkRecommendedDependencies(projectRoot));
|
|
1148
|
+
checks.push(await checkBabelConfig(projectRoot));
|
|
1149
|
+
checks.push(await checkTypeScript(projectRoot));
|
|
1150
|
+
const expoGoCheck = await checkExpoGo(projectRoot);
|
|
1151
|
+
if (expoGoCheck) {
|
|
1152
|
+
checks.push(expoGoCheck);
|
|
1153
|
+
}
|
|
1154
|
+
const passed = checks.filter((c) => c.status === "pass").length;
|
|
1155
|
+
const warnings = checks.filter((c) => c.status === "warn").length;
|
|
1156
|
+
const failed = checks.filter((c) => c.status === "fail").length;
|
|
1157
|
+
const report = {
|
|
1158
|
+
projectRoot,
|
|
1159
|
+
projectType,
|
|
1160
|
+
packageManager,
|
|
1161
|
+
checks,
|
|
1162
|
+
passed,
|
|
1163
|
+
warnings,
|
|
1164
|
+
failed
|
|
1165
|
+
};
|
|
1166
|
+
if (options.json) {
|
|
1167
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1168
|
+
} else {
|
|
1169
|
+
printReport(report);
|
|
1170
|
+
}
|
|
1171
|
+
if (failed > 0) {
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
console.error(chalk5.red("Doctor check failed:"), error);
|
|
1176
|
+
process.exit(1);
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
// src/commands/diff.ts
|
|
1181
|
+
import { Command as Command5 } from "commander";
|
|
1182
|
+
import chalk6 from "chalk";
|
|
1183
|
+
import ora4 from "ora";
|
|
1184
|
+
import fs6 from "fs-extra";
|
|
1185
|
+
import path6 from "path";
|
|
1186
|
+
import crypto from "crypto";
|
|
1187
|
+
var diffCommand = new Command5().name("diff").description("Check for component updates against the registry").option("--cwd <path>", "Working directory", process.cwd()).option("--json", "Output as JSON").option("-v, --verbose", "Show detailed diff information").action(async (options) => {
|
|
1188
|
+
const spinner = ora4();
|
|
1189
|
+
try {
|
|
1190
|
+
const cwd = path6.resolve(options.cwd);
|
|
1191
|
+
const projectRoot = await getProjectRoot(cwd);
|
|
1192
|
+
if (!projectRoot) {
|
|
1193
|
+
console.log(chalk6.red("Could not find a valid project."));
|
|
1194
|
+
console.log(chalk6.dim("Run `npx nativeui init` first."));
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
const config = await getConfig(projectRoot);
|
|
1198
|
+
if (!config) {
|
|
1199
|
+
console.log(chalk6.red("Project not initialized."));
|
|
1200
|
+
console.log(chalk6.dim("Run `npx nativeui init` first."));
|
|
1201
|
+
process.exit(1);
|
|
1202
|
+
}
|
|
1203
|
+
spinner.start("Scanning installed components...");
|
|
1204
|
+
const componentsDir = path6.join(projectRoot, config.componentsPath);
|
|
1205
|
+
const installedFiles = await getInstalledComponents(componentsDir);
|
|
1206
|
+
if (installedFiles.length === 0) {
|
|
1207
|
+
spinner.info("No components installed yet.");
|
|
1208
|
+
console.log(chalk6.dim("\nAdd components with: npx nativeui add <component>"));
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
spinner.text = "Fetching registry...";
|
|
1212
|
+
const registry = await getRegistry();
|
|
1213
|
+
const registryMap = /* @__PURE__ */ new Map();
|
|
1214
|
+
for (const item of registry) {
|
|
1215
|
+
for (const file of item.files) {
|
|
1216
|
+
const fileName = path6.basename(file);
|
|
1217
|
+
registryMap.set(fileName, item);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
spinner.text = "Comparing components...";
|
|
1221
|
+
const results = [];
|
|
1222
|
+
for (const localFile of installedFiles) {
|
|
1223
|
+
const fileName = path6.basename(localFile);
|
|
1224
|
+
const registryItem = registryMap.get(fileName);
|
|
1225
|
+
if (!registryItem) {
|
|
1226
|
+
results.push({
|
|
1227
|
+
name: fileName.replace(/\.tsx?$/, ""),
|
|
1228
|
+
status: "local-only",
|
|
1229
|
+
localFile
|
|
1230
|
+
});
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
const component = await fetchComponent(registryItem.name);
|
|
1235
|
+
if (!component) {
|
|
1236
|
+
results.push({
|
|
1237
|
+
name: registryItem.name,
|
|
1238
|
+
status: "error",
|
|
1239
|
+
error: "Failed to fetch from registry"
|
|
1240
|
+
});
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
const registryFile = component.files.find((f) => f.name === fileName);
|
|
1244
|
+
if (!registryFile) {
|
|
1245
|
+
results.push({
|
|
1246
|
+
name: registryItem.name,
|
|
1247
|
+
status: "error",
|
|
1248
|
+
error: "File not found in registry"
|
|
1249
|
+
});
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
const localContent = await fs6.readFile(localFile, "utf-8");
|
|
1253
|
+
const transformedRegistryContent = transformImports2(registryFile.content, config);
|
|
1254
|
+
const localHash = hashContent(localContent);
|
|
1255
|
+
const registryHash = hashContent(transformedRegistryContent);
|
|
1256
|
+
if (localHash === registryHash) {
|
|
1257
|
+
results.push({
|
|
1258
|
+
name: registryItem.name,
|
|
1259
|
+
status: "up-to-date",
|
|
1260
|
+
localFile
|
|
1261
|
+
});
|
|
1262
|
+
} else {
|
|
1263
|
+
results.push({
|
|
1264
|
+
name: registryItem.name,
|
|
1265
|
+
status: "changed",
|
|
1266
|
+
localFile
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
results.push({
|
|
1271
|
+
name: registryItem.name,
|
|
1272
|
+
status: "error",
|
|
1273
|
+
error: String(error)
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
spinner.stop();
|
|
1278
|
+
if (options.json) {
|
|
1279
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
printResults(results, options.verbose);
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
spinner.fail("Failed to check for updates");
|
|
1285
|
+
console.error(error);
|
|
1286
|
+
process.exit(1);
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
async function getInstalledComponents(componentsDir) {
|
|
1290
|
+
if (!await fs6.pathExists(componentsDir)) {
|
|
1291
|
+
return [];
|
|
1292
|
+
}
|
|
1293
|
+
const files = await fs6.readdir(componentsDir);
|
|
1294
|
+
const componentFiles = [];
|
|
1295
|
+
for (const file of files) {
|
|
1296
|
+
if (file.endsWith(".tsx") || file.endsWith(".ts")) {
|
|
1297
|
+
if (file === "index.ts" || file === "index.tsx") {
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
componentFiles.push(path6.join(componentsDir, file));
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return componentFiles;
|
|
1304
|
+
}
|
|
1305
|
+
function hashContent(content) {
|
|
1306
|
+
const normalized = content.replace(/\r\n/g, "\n").replace(/\s+$/gm, "").trim();
|
|
1307
|
+
return crypto.createHash("md5").update(normalized).digest("hex");
|
|
1308
|
+
}
|
|
1309
|
+
function transformImports2(code, config) {
|
|
1310
|
+
let transformed = code;
|
|
1311
|
+
const utilsAlias = config.aliases?.utils || "@/lib/utils";
|
|
1312
|
+
if (utilsAlias === "@/lib/utils") {
|
|
1313
|
+
return transformed;
|
|
1314
|
+
}
|
|
1315
|
+
transformed = transformed.replace(
|
|
1316
|
+
/from ['"]@\/lib\/utils['"]/g,
|
|
1317
|
+
`from '${utilsAlias}'`
|
|
1318
|
+
);
|
|
1319
|
+
return transformed;
|
|
1320
|
+
}
|
|
1321
|
+
function printResults(results, verbose) {
|
|
1322
|
+
const upToDate = results.filter((r) => r.status === "up-to-date");
|
|
1323
|
+
const changed = results.filter((r) => r.status === "changed");
|
|
1324
|
+
const localOnly = results.filter((r) => r.status === "local-only");
|
|
1325
|
+
const errors = results.filter((r) => r.status === "error");
|
|
1326
|
+
console.log();
|
|
1327
|
+
console.log(chalk6.bold("Component Diff Report"));
|
|
1328
|
+
console.log(chalk6.dim("\u2500".repeat(40)));
|
|
1329
|
+
console.log();
|
|
1330
|
+
const total = results.length;
|
|
1331
|
+
console.log(`Found ${chalk6.bold(total)} installed component${total !== 1 ? "s" : ""}`);
|
|
1332
|
+
console.log();
|
|
1333
|
+
if (changed.length > 0) {
|
|
1334
|
+
console.log(chalk6.yellow(`\u26A1 ${changed.length} update${changed.length !== 1 ? "s" : ""} available:`));
|
|
1335
|
+
for (const item of changed) {
|
|
1336
|
+
console.log(` ${chalk6.yellow("\u25CF")} ${item.name}`);
|
|
1337
|
+
}
|
|
1338
|
+
console.log();
|
|
1339
|
+
console.log(chalk6.dim(" Update with: npx nativeui add <component> --overwrite"));
|
|
1340
|
+
console.log();
|
|
1341
|
+
}
|
|
1342
|
+
if (upToDate.length > 0) {
|
|
1343
|
+
console.log(chalk6.green(`\u2713 ${upToDate.length} up to date:`));
|
|
1344
|
+
if (verbose) {
|
|
1345
|
+
for (const item of upToDate) {
|
|
1346
|
+
console.log(` ${chalk6.green("\u25CF")} ${item.name}`);
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
const names = upToDate.map((r) => r.name).join(", ");
|
|
1350
|
+
console.log(chalk6.dim(` ${names}`));
|
|
1351
|
+
}
|
|
1352
|
+
console.log();
|
|
1353
|
+
}
|
|
1354
|
+
if (localOnly.length > 0) {
|
|
1355
|
+
console.log(chalk6.blue(`\u25D0 ${localOnly.length} local-only (not in registry):`));
|
|
1356
|
+
for (const item of localOnly) {
|
|
1357
|
+
console.log(` ${chalk6.blue("\u25CF")} ${item.name}`);
|
|
1358
|
+
}
|
|
1359
|
+
console.log();
|
|
1360
|
+
}
|
|
1361
|
+
if (errors.length > 0) {
|
|
1362
|
+
console.log(chalk6.red(`\u2717 ${errors.length} error${errors.length !== 1 ? "s" : ""}:`));
|
|
1363
|
+
for (const item of errors) {
|
|
1364
|
+
console.log(` ${chalk6.red("\u25CF")} ${item.name}: ${chalk6.dim(item.error)}`);
|
|
1365
|
+
}
|
|
1366
|
+
console.log();
|
|
1367
|
+
}
|
|
1368
|
+
if (changed.length === 0 && errors.length === 0) {
|
|
1369
|
+
console.log(chalk6.green("All registry components are up to date!"));
|
|
1370
|
+
} else if (changed.length > 0) {
|
|
1371
|
+
console.log(
|
|
1372
|
+
chalk6.yellow(`Run ${chalk6.cyan("npx nativeui add " + changed.map((c) => c.name).join(" ") + " --overwrite")} to update`)
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/commands/update.ts
|
|
1378
|
+
import { Command as Command6 } from "commander";
|
|
1379
|
+
import chalk7 from "chalk";
|
|
1380
|
+
import ora5 from "ora";
|
|
1381
|
+
import prompts3 from "prompts";
|
|
1382
|
+
import fs7 from "fs-extra";
|
|
1383
|
+
import path7 from "path";
|
|
1384
|
+
import crypto2 from "crypto";
|
|
1385
|
+
var updateCommand = new Command6().name("update").description("Update installed components to latest registry versions").argument("[components...]", "Specific components to update (default: all outdated)").option("-y, --yes", "Skip confirmation prompt").option("--all", "Update all components (including up-to-date)").option("--dry-run", "Show what would be updated without making changes").option("--cwd <path>", "Working directory", process.cwd()).action(async (components, options) => {
|
|
1386
|
+
const spinner = ora5();
|
|
1387
|
+
try {
|
|
1388
|
+
const cwd = path7.resolve(options.cwd);
|
|
1389
|
+
const projectRoot = await getProjectRoot(cwd);
|
|
1390
|
+
if (!projectRoot) {
|
|
1391
|
+
console.log(chalk7.red("Could not find a valid project."));
|
|
1392
|
+
console.log(chalk7.dim("Run `npx nativeui init` first."));
|
|
1393
|
+
process.exit(1);
|
|
1394
|
+
}
|
|
1395
|
+
const config = await getConfig(projectRoot);
|
|
1396
|
+
if (!config) {
|
|
1397
|
+
console.log(chalk7.red("Project not initialized."));
|
|
1398
|
+
console.log(chalk7.dim("Run `npx nativeui init` first."));
|
|
1399
|
+
process.exit(1);
|
|
1400
|
+
}
|
|
1401
|
+
spinner.start("Checking for updates...");
|
|
1402
|
+
const componentsDir = path7.join(projectRoot, config.componentsPath);
|
|
1403
|
+
const diffs = await getComponentDiffs(componentsDir, config);
|
|
1404
|
+
if (diffs.length === 0) {
|
|
1405
|
+
spinner.info("No components installed yet.");
|
|
1406
|
+
console.log(chalk7.dim("\nAdd components with: npx nativeui add <component>"));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
spinner.stop();
|
|
1410
|
+
let toUpdate;
|
|
1411
|
+
if (components.length > 0) {
|
|
1412
|
+
toUpdate = diffs.filter((d) => components.includes(d.name));
|
|
1413
|
+
const notFound = components.filter((c) => !diffs.some((d) => d.name === c));
|
|
1414
|
+
if (notFound.length > 0) {
|
|
1415
|
+
console.log(chalk7.yellow(`Components not found: ${notFound.join(", ")}`));
|
|
1416
|
+
}
|
|
1417
|
+
} else if (options.all) {
|
|
1418
|
+
toUpdate = diffs;
|
|
1419
|
+
} else {
|
|
1420
|
+
toUpdate = diffs.filter((d) => d.hasUpdate);
|
|
1421
|
+
}
|
|
1422
|
+
if (toUpdate.length === 0) {
|
|
1423
|
+
console.log(chalk7.green("\n\u2713 All components are up to date!"));
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
console.log();
|
|
1427
|
+
console.log(chalk7.bold(`Components to update (${toUpdate.length}):`));
|
|
1428
|
+
for (const comp of toUpdate) {
|
|
1429
|
+
const status = comp.hasUpdate ? chalk7.yellow("\u25CF") : chalk7.green("\u25CF");
|
|
1430
|
+
const label = comp.hasUpdate ? chalk7.dim("(update available)") : chalk7.dim("(re-sync)");
|
|
1431
|
+
console.log(` ${status} ${comp.name} ${label}`);
|
|
1432
|
+
}
|
|
1433
|
+
console.log();
|
|
1434
|
+
if (options.dryRun) {
|
|
1435
|
+
console.log(chalk7.dim("Dry run mode - no changes made."));
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
if (!options.yes) {
|
|
1439
|
+
const { confirm } = await prompts3({
|
|
1440
|
+
type: "confirm",
|
|
1441
|
+
name: "confirm",
|
|
1442
|
+
message: `Update ${toUpdate.length} component${toUpdate.length !== 1 ? "s" : ""}?`,
|
|
1443
|
+
initial: true
|
|
1444
|
+
});
|
|
1445
|
+
if (!confirm) {
|
|
1446
|
+
console.log(chalk7.dim("Cancelled."));
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
console.log();
|
|
1451
|
+
const allDependencies = [];
|
|
1452
|
+
const allDevDependencies = [];
|
|
1453
|
+
let successCount = 0;
|
|
1454
|
+
let failCount = 0;
|
|
1455
|
+
for (const comp of toUpdate) {
|
|
1456
|
+
spinner.start(`Updating ${comp.name}...`);
|
|
1457
|
+
try {
|
|
1458
|
+
const component = await fetchComponent(comp.name);
|
|
1459
|
+
if (!component) {
|
|
1460
|
+
spinner.fail(`${comp.name}: Not found in registry`);
|
|
1461
|
+
failCount++;
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
const targetDir = path7.join(projectRoot, config.componentsPath);
|
|
1465
|
+
for (const file of component.files) {
|
|
1466
|
+
const targetPath = path7.join(targetDir, file.name);
|
|
1467
|
+
await fs7.ensureDir(targetDir);
|
|
1468
|
+
const transformedContent = transformImports3(file.content, config);
|
|
1469
|
+
await fs7.writeFile(targetPath, transformedContent);
|
|
1470
|
+
}
|
|
1471
|
+
spinner.succeed(`Updated ${comp.name}`);
|
|
1472
|
+
successCount++;
|
|
1473
|
+
if (component.dependencies?.length) {
|
|
1474
|
+
allDependencies.push(...component.dependencies);
|
|
1475
|
+
}
|
|
1476
|
+
if (component.devDependencies?.length) {
|
|
1477
|
+
allDevDependencies.push(...component.devDependencies);
|
|
1478
|
+
}
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
spinner.fail(`${comp.name}: ${String(error)}`);
|
|
1481
|
+
failCount++;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
console.log();
|
|
1485
|
+
if (successCount > 0) {
|
|
1486
|
+
console.log(chalk7.green(`\u2713 Updated ${successCount} component${successCount !== 1 ? "s" : ""}`));
|
|
1487
|
+
}
|
|
1488
|
+
if (failCount > 0) {
|
|
1489
|
+
console.log(chalk7.red(`\u2717 Failed to update ${failCount} component${failCount !== 1 ? "s" : ""}`));
|
|
1490
|
+
}
|
|
1491
|
+
const uniqueDeps = [...new Set(allDependencies)];
|
|
1492
|
+
const uniqueDevDeps = [...new Set(allDevDependencies)];
|
|
1493
|
+
if (uniqueDeps.length || uniqueDevDeps.length) {
|
|
1494
|
+
console.log();
|
|
1495
|
+
console.log(chalk7.bold("Install/update dependencies:"));
|
|
1496
|
+
if (uniqueDeps.length) {
|
|
1497
|
+
console.log(chalk7.cyan(` npx expo install ${uniqueDeps.join(" ")}`));
|
|
1498
|
+
}
|
|
1499
|
+
if (uniqueDevDeps.length) {
|
|
1500
|
+
console.log(chalk7.cyan(` npm install -D ${uniqueDevDeps.join(" ")}`));
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
spinner.fail("Failed to update");
|
|
1505
|
+
console.error(error);
|
|
1506
|
+
process.exit(1);
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
async function getComponentDiffs(componentsDir, config) {
|
|
1510
|
+
if (!await fs7.pathExists(componentsDir)) {
|
|
1511
|
+
return [];
|
|
1512
|
+
}
|
|
1513
|
+
const registry = await getRegistry();
|
|
1514
|
+
const registryMap = /* @__PURE__ */ new Map();
|
|
1515
|
+
for (const item of registry) {
|
|
1516
|
+
for (const file of item.files) {
|
|
1517
|
+
const fileName = path7.basename(file);
|
|
1518
|
+
registryMap.set(fileName, item);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
const files = await fs7.readdir(componentsDir);
|
|
1522
|
+
const results = [];
|
|
1523
|
+
for (const file of files) {
|
|
1524
|
+
if (!file.endsWith(".tsx") && !file.endsWith(".ts")) {
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
if (file === "index.ts" || file === "index.tsx") {
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
const registryItem = registryMap.get(file);
|
|
1531
|
+
if (!registryItem) {
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
const localFile = path7.join(componentsDir, file);
|
|
1535
|
+
const hasUpdate = await checkForUpdate(localFile, registryItem, config);
|
|
1536
|
+
results.push({
|
|
1537
|
+
name: registryItem.name,
|
|
1538
|
+
localFile,
|
|
1539
|
+
hasUpdate
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
return results;
|
|
1543
|
+
}
|
|
1544
|
+
async function checkForUpdate(localFile, registryItem, config) {
|
|
1545
|
+
try {
|
|
1546
|
+
const component = await fetchComponent(registryItem.name);
|
|
1547
|
+
if (!component) return false;
|
|
1548
|
+
const fileName = path7.basename(localFile);
|
|
1549
|
+
const registryFile = component.files.find((f) => f.name === fileName);
|
|
1550
|
+
if (!registryFile) return false;
|
|
1551
|
+
const localContent = await fs7.readFile(localFile, "utf-8");
|
|
1552
|
+
const transformedRegistryContent = transformImports3(registryFile.content, config);
|
|
1553
|
+
const localHash = hashContent2(localContent);
|
|
1554
|
+
const registryHash = hashContent2(transformedRegistryContent);
|
|
1555
|
+
return localHash !== registryHash;
|
|
1556
|
+
} catch {
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function hashContent2(content) {
|
|
1561
|
+
const normalized = content.replace(/\r\n/g, "\n").replace(/\s+$/gm, "").trim();
|
|
1562
|
+
return crypto2.createHash("md5").update(normalized).digest("hex");
|
|
1563
|
+
}
|
|
1564
|
+
function transformImports3(code, config) {
|
|
1565
|
+
let transformed = code;
|
|
1566
|
+
const utilsAlias = config.aliases?.utils || "@/lib/utils";
|
|
1567
|
+
if (utilsAlias === "@/lib/utils") {
|
|
1568
|
+
return transformed;
|
|
1569
|
+
}
|
|
1570
|
+
transformed = transformed.replace(
|
|
1571
|
+
/from ['"]@\/lib\/utils['"]/g,
|
|
1572
|
+
`from '${utilsAlias}'`
|
|
1573
|
+
);
|
|
1574
|
+
return transformed;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/commands/create.ts
|
|
1578
|
+
import { Command as Command7 } from "commander";
|
|
1579
|
+
import chalk8 from "chalk";
|
|
1580
|
+
import ora6 from "ora";
|
|
1581
|
+
import prompts4 from "prompts";
|
|
1582
|
+
import fs8 from "fs-extra";
|
|
1583
|
+
import path8 from "path";
|
|
1584
|
+
var createCommand = new Command7().name("create").description("Scaffold a new custom component").argument("<name>", "Component name (e.g., my-button, ProfileCard)").option("-t, --template <type>", "Component template: basic, animated, pressable, input", "basic").option("--forward-ref", "Include forwardRef pattern").option("-y, --yes", "Skip confirmation").option("--cwd <path>", "Working directory", process.cwd()).action(async (name, options) => {
|
|
1585
|
+
const spinner = ora6();
|
|
1586
|
+
try {
|
|
1587
|
+
const cwd = path8.resolve(options.cwd);
|
|
1588
|
+
const projectRoot = await getProjectRoot(cwd);
|
|
1589
|
+
if (!projectRoot) {
|
|
1590
|
+
console.log(chalk8.red("Could not find a valid project."));
|
|
1591
|
+
console.log(chalk8.dim("Run `npx nativeui init` first."));
|
|
1592
|
+
process.exit(1);
|
|
1593
|
+
}
|
|
1594
|
+
const config = await getConfig(projectRoot);
|
|
1595
|
+
if (!config) {
|
|
1596
|
+
console.log(chalk8.red("Project not initialized."));
|
|
1597
|
+
console.log(chalk8.dim("Run `npx nativeui init` first."));
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1600
|
+
const componentName = toPascalCase(name);
|
|
1601
|
+
const fileName = toKebabCase(name) + ".tsx";
|
|
1602
|
+
const targetDir = path8.join(projectRoot, config.componentsPath);
|
|
1603
|
+
const targetPath = path8.join(targetDir, fileName);
|
|
1604
|
+
if (await fs8.pathExists(targetPath)) {
|
|
1605
|
+
console.log(chalk8.red(`Component already exists: ${fileName}`));
|
|
1606
|
+
console.log(chalk8.dim(`Path: ${targetPath}`));
|
|
1607
|
+
process.exit(1);
|
|
1608
|
+
}
|
|
1609
|
+
if (!options.yes) {
|
|
1610
|
+
console.log();
|
|
1611
|
+
console.log(chalk8.bold("Create new component:"));
|
|
1612
|
+
console.log(` Name: ${chalk8.cyan(componentName)}`);
|
|
1613
|
+
console.log(` File: ${chalk8.dim(fileName)}`);
|
|
1614
|
+
console.log(` Path: ${chalk8.dim(targetPath)}`);
|
|
1615
|
+
console.log(` Template: ${chalk8.dim(options.template)}`);
|
|
1616
|
+
console.log(` ForwardRef: ${chalk8.dim(options.forwardRef ? "Yes" : "No")}`);
|
|
1617
|
+
console.log();
|
|
1618
|
+
const { confirm } = await prompts4({
|
|
1619
|
+
type: "confirm",
|
|
1620
|
+
name: "confirm",
|
|
1621
|
+
message: "Create this component?",
|
|
1622
|
+
initial: true
|
|
1623
|
+
});
|
|
1624
|
+
if (!confirm) {
|
|
1625
|
+
console.log(chalk8.dim("Cancelled."));
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
spinner.start("Creating component...");
|
|
1630
|
+
const code = generateComponent(componentName, options.template, options.forwardRef);
|
|
1631
|
+
await fs8.ensureDir(targetDir);
|
|
1632
|
+
await fs8.writeFile(targetPath, code);
|
|
1633
|
+
spinner.succeed(`Created ${fileName}`);
|
|
1634
|
+
console.log();
|
|
1635
|
+
console.log(chalk8.bold("Next steps:"));
|
|
1636
|
+
console.log(` 1. Edit your component: ${chalk8.cyan(targetPath)}`);
|
|
1637
|
+
console.log(` 2. Import it in your app:`);
|
|
1638
|
+
console.log(chalk8.dim(` import { ${componentName} } from '${config.aliases?.components || "@/components"}/ui/${toKebabCase(name)}';`));
|
|
1639
|
+
console.log();
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
spinner.fail("Failed to create component");
|
|
1642
|
+
console.error(error);
|
|
1643
|
+
process.exit(1);
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
function toPascalCase(str) {
|
|
1647
|
+
return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toUpperCase());
|
|
1648
|
+
}
|
|
1649
|
+
function toKebabCase(str) {
|
|
1650
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
1651
|
+
}
|
|
1652
|
+
function generateComponent(name, template, forwardRef) {
|
|
1653
|
+
const templates = {
|
|
1654
|
+
basic: () => generateBasicComponent(name, forwardRef),
|
|
1655
|
+
animated: () => generateAnimatedComponent(name, forwardRef),
|
|
1656
|
+
pressable: () => generatePressableComponent(name, forwardRef),
|
|
1657
|
+
input: () => generateInputComponent(name, forwardRef)
|
|
1658
|
+
};
|
|
1659
|
+
return templates[template]();
|
|
1660
|
+
}
|
|
1661
|
+
function generateBasicComponent(name, forwardRef) {
|
|
1662
|
+
if (forwardRef) {
|
|
1663
|
+
return `import React, { forwardRef } from 'react';
|
|
1664
|
+
import { View, Text, StyleSheet, ViewProps } from 'react-native';
|
|
1665
|
+
import { useTheme } from '@nativeui/core';
|
|
1666
|
+
|
|
1667
|
+
export interface ${name}Props extends ViewProps {
|
|
1668
|
+
/**
|
|
1669
|
+
* Example prop - replace with your own
|
|
1670
|
+
*/
|
|
1671
|
+
title?: string;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* ${name}
|
|
1676
|
+
*
|
|
1677
|
+
* A custom component created with nativeui create.
|
|
1678
|
+
*
|
|
1679
|
+
* @example
|
|
1680
|
+
* \`\`\`tsx
|
|
1681
|
+
* <${name} title="Hello World" />
|
|
1682
|
+
* \`\`\`
|
|
1683
|
+
*/
|
|
1684
|
+
export const ${name} = forwardRef<View, ${name}Props>(
|
|
1685
|
+
({ title, style, ...props }, ref) => {
|
|
1686
|
+
const { colors, spacing, radius } = useTheme();
|
|
1687
|
+
|
|
1688
|
+
return (
|
|
1689
|
+
<View
|
|
1690
|
+
ref={ref}
|
|
1691
|
+
style={[
|
|
1692
|
+
styles.container,
|
|
1693
|
+
{
|
|
1694
|
+
backgroundColor: colors.card,
|
|
1695
|
+
padding: spacing[4],
|
|
1696
|
+
borderRadius: radius.md,
|
|
1697
|
+
},
|
|
1698
|
+
style,
|
|
1699
|
+
]}
|
|
1700
|
+
{...props}
|
|
1701
|
+
>
|
|
1702
|
+
{title && (
|
|
1703
|
+
<Text style={[styles.title, { color: colors.foreground }]}>
|
|
1704
|
+
{title}
|
|
1705
|
+
</Text>
|
|
1706
|
+
)}
|
|
1707
|
+
</View>
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
);
|
|
1711
|
+
|
|
1712
|
+
${name}.displayName = '${name}';
|
|
1713
|
+
|
|
1714
|
+
const styles = StyleSheet.create({
|
|
1715
|
+
container: {},
|
|
1716
|
+
title: {
|
|
1717
|
+
fontSize: 16,
|
|
1718
|
+
fontWeight: '500',
|
|
1719
|
+
},
|
|
1720
|
+
});
|
|
1721
|
+
`;
|
|
1722
|
+
}
|
|
1723
|
+
return `import React from 'react';
|
|
1724
|
+
import { View, Text, StyleSheet, ViewProps } from 'react-native';
|
|
1725
|
+
import { useTheme } from '@nativeui/core';
|
|
1726
|
+
|
|
1727
|
+
export interface ${name}Props extends ViewProps {
|
|
1728
|
+
/**
|
|
1729
|
+
* Example prop - replace with your own
|
|
1730
|
+
*/
|
|
1731
|
+
title?: string;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* ${name}
|
|
1736
|
+
*
|
|
1737
|
+
* A custom component created with nativeui create.
|
|
1738
|
+
*
|
|
1739
|
+
* @example
|
|
1740
|
+
* \`\`\`tsx
|
|
1741
|
+
* <${name} title="Hello World" />
|
|
1742
|
+
* \`\`\`
|
|
1743
|
+
*/
|
|
1744
|
+
export function ${name}({ title, style, ...props }: ${name}Props) {
|
|
1745
|
+
const { colors, spacing, radius } = useTheme();
|
|
1746
|
+
|
|
1747
|
+
return (
|
|
1748
|
+
<View
|
|
1749
|
+
style={[
|
|
1750
|
+
styles.container,
|
|
1751
|
+
{
|
|
1752
|
+
backgroundColor: colors.card,
|
|
1753
|
+
padding: spacing[4],
|
|
1754
|
+
borderRadius: radius.md,
|
|
1755
|
+
},
|
|
1756
|
+
style,
|
|
1757
|
+
]}
|
|
1758
|
+
{...props}
|
|
1759
|
+
>
|
|
1760
|
+
{title && (
|
|
1761
|
+
<Text style={[styles.title, { color: colors.foreground }]}>
|
|
1762
|
+
{title}
|
|
1763
|
+
</Text>
|
|
1764
|
+
)}
|
|
1765
|
+
</View>
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
const styles = StyleSheet.create({
|
|
1770
|
+
container: {},
|
|
1771
|
+
title: {
|
|
1772
|
+
fontSize: 16,
|
|
1773
|
+
fontWeight: '500',
|
|
1774
|
+
},
|
|
1775
|
+
});
|
|
1776
|
+
`;
|
|
1777
|
+
}
|
|
1778
|
+
function generateAnimatedComponent(name, forwardRef) {
|
|
1779
|
+
const refPart = forwardRef ? `export const ${name} = forwardRef<Animated.View, ${name}Props>(
|
|
1780
|
+
({ title, style, ...props }, ref) => {` : `export function ${name}({ title, style, ...props }: ${name}Props) {`;
|
|
1781
|
+
const closePart = forwardRef ? ` }
|
|
1782
|
+
);
|
|
1783
|
+
|
|
1784
|
+
${name}.displayName = '${name}';` : `}`;
|
|
1785
|
+
return `import React${forwardRef ? ", { forwardRef }" : ""} from 'react';
|
|
1786
|
+
import { StyleSheet, ViewProps } from 'react-native';
|
|
1787
|
+
import Animated, {
|
|
1788
|
+
useAnimatedStyle,
|
|
1789
|
+
useSharedValue,
|
|
1790
|
+
withSpring,
|
|
1791
|
+
} from 'react-native-reanimated';
|
|
1792
|
+
import { useTheme } from '@nativeui/core';
|
|
1793
|
+
|
|
1794
|
+
export interface ${name}Props extends ViewProps {
|
|
1795
|
+
/**
|
|
1796
|
+
* Example prop - replace with your own
|
|
1797
|
+
*/
|
|
1798
|
+
title?: string;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
/**
|
|
1802
|
+
* ${name}
|
|
1803
|
+
*
|
|
1804
|
+
* An animated component created with nativeui create.
|
|
1805
|
+
*
|
|
1806
|
+
* @example
|
|
1807
|
+
* \`\`\`tsx
|
|
1808
|
+
* <${name} title="Hello World" />
|
|
1809
|
+
* \`\`\`
|
|
1810
|
+
*/
|
|
1811
|
+
${refPart}
|
|
1812
|
+
const { colors, spacing, radius } = useTheme();
|
|
1813
|
+
const scale = useSharedValue(1);
|
|
1814
|
+
|
|
1815
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
1816
|
+
transform: [{ scale: scale.value }],
|
|
1817
|
+
}));
|
|
1818
|
+
|
|
1819
|
+
return (
|
|
1820
|
+
<Animated.View
|
|
1821
|
+
${forwardRef ? "ref={ref}" : ""}
|
|
1822
|
+
style={[
|
|
1823
|
+
styles.container,
|
|
1824
|
+
{
|
|
1825
|
+
backgroundColor: colors.card,
|
|
1826
|
+
padding: spacing[4],
|
|
1827
|
+
borderRadius: radius.md,
|
|
1828
|
+
},
|
|
1829
|
+
animatedStyle,
|
|
1830
|
+
style,
|
|
1831
|
+
]}
|
|
1832
|
+
{...props}
|
|
1833
|
+
>
|
|
1834
|
+
{title && (
|
|
1835
|
+
<Animated.Text style={[styles.title, { color: colors.foreground }]}>
|
|
1836
|
+
{title}
|
|
1837
|
+
</Animated.Text>
|
|
1838
|
+
)}
|
|
1839
|
+
</Animated.View>
|
|
1840
|
+
);
|
|
1841
|
+
${closePart}
|
|
1842
|
+
|
|
1843
|
+
const styles = StyleSheet.create({
|
|
1844
|
+
container: {},
|
|
1845
|
+
title: {
|
|
1846
|
+
fontSize: 16,
|
|
1847
|
+
fontWeight: '500',
|
|
1848
|
+
},
|
|
1849
|
+
});
|
|
1850
|
+
`;
|
|
1851
|
+
}
|
|
1852
|
+
function generatePressableComponent(name, forwardRef) {
|
|
1853
|
+
if (forwardRef) {
|
|
1854
|
+
return `import React, { forwardRef } from 'react';
|
|
1855
|
+
import { Pressable, Text, StyleSheet, PressableProps, View } from 'react-native';
|
|
1856
|
+
import Animated, {
|
|
1857
|
+
useAnimatedStyle,
|
|
1858
|
+
useSharedValue,
|
|
1859
|
+
withTiming,
|
|
1860
|
+
} from 'react-native-reanimated';
|
|
1861
|
+
import { useTheme } from '@nativeui/core';
|
|
1862
|
+
|
|
1863
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
1864
|
+
|
|
1865
|
+
export interface ${name}Props extends Omit<PressableProps, 'style'> {
|
|
1866
|
+
/**
|
|
1867
|
+
* Button label
|
|
1868
|
+
*/
|
|
1869
|
+
label?: string;
|
|
1870
|
+
/**
|
|
1871
|
+
* Custom style
|
|
1872
|
+
*/
|
|
1873
|
+
style?: PressableProps['style'];
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* ${name}
|
|
1878
|
+
*
|
|
1879
|
+
* A pressable component created with nativeui create.
|
|
1880
|
+
*
|
|
1881
|
+
* @example
|
|
1882
|
+
* \`\`\`tsx
|
|
1883
|
+
* <${name} label="Press me" onPress={() => console.log('Pressed!')} />
|
|
1884
|
+
* \`\`\`
|
|
1885
|
+
*/
|
|
1886
|
+
export const ${name} = forwardRef<View, ${name}Props>(
|
|
1887
|
+
({ label, style, onPressIn, onPressOut, ...props }, ref) => {
|
|
1888
|
+
const { colors, spacing, radius } = useTheme();
|
|
1889
|
+
const scale = useSharedValue(1);
|
|
1890
|
+
|
|
1891
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
1892
|
+
transform: [{ scale: scale.value }],
|
|
1893
|
+
}));
|
|
1894
|
+
|
|
1895
|
+
const handlePressIn = (e: any) => {
|
|
1896
|
+
scale.value = withTiming(0.97, { duration: 100 });
|
|
1897
|
+
onPressIn?.(e);
|
|
1898
|
+
};
|
|
1899
|
+
|
|
1900
|
+
const handlePressOut = (e: any) => {
|
|
1901
|
+
scale.value = withTiming(1, { duration: 100 });
|
|
1902
|
+
onPressOut?.(e);
|
|
1903
|
+
};
|
|
1904
|
+
|
|
1905
|
+
return (
|
|
1906
|
+
<AnimatedPressable
|
|
1907
|
+
ref={ref}
|
|
1908
|
+
onPressIn={handlePressIn}
|
|
1909
|
+
onPressOut={handlePressOut}
|
|
1910
|
+
style={[
|
|
1911
|
+
styles.container,
|
|
1912
|
+
{
|
|
1913
|
+
backgroundColor: colors.primary,
|
|
1914
|
+
paddingVertical: spacing[3],
|
|
1915
|
+
paddingHorizontal: spacing[4],
|
|
1916
|
+
borderRadius: radius.md,
|
|
1917
|
+
},
|
|
1918
|
+
animatedStyle,
|
|
1919
|
+
style,
|
|
1920
|
+
]}
|
|
1921
|
+
{...props}
|
|
1922
|
+
>
|
|
1923
|
+
{label && (
|
|
1924
|
+
<Text style={[styles.label, { color: colors.primaryForeground }]}>
|
|
1925
|
+
{label}
|
|
1926
|
+
</Text>
|
|
1927
|
+
)}
|
|
1928
|
+
</AnimatedPressable>
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
);
|
|
1932
|
+
|
|
1933
|
+
${name}.displayName = '${name}';
|
|
1934
|
+
|
|
1935
|
+
const styles = StyleSheet.create({
|
|
1936
|
+
container: {
|
|
1937
|
+
alignItems: 'center',
|
|
1938
|
+
justifyContent: 'center',
|
|
1939
|
+
},
|
|
1940
|
+
label: {
|
|
1941
|
+
fontSize: 16,
|
|
1942
|
+
fontWeight: '600',
|
|
1943
|
+
},
|
|
1944
|
+
});
|
|
1945
|
+
`;
|
|
1946
|
+
}
|
|
1947
|
+
return `import React from 'react';
|
|
1948
|
+
import { Pressable, Text, StyleSheet, PressableProps } from 'react-native';
|
|
1949
|
+
import Animated, {
|
|
1950
|
+
useAnimatedStyle,
|
|
1951
|
+
useSharedValue,
|
|
1952
|
+
withTiming,
|
|
1953
|
+
} from 'react-native-reanimated';
|
|
1954
|
+
import { useTheme } from '@nativeui/core';
|
|
1955
|
+
|
|
1956
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
1957
|
+
|
|
1958
|
+
export interface ${name}Props extends Omit<PressableProps, 'style'> {
|
|
1959
|
+
/**
|
|
1960
|
+
* Button label
|
|
1961
|
+
*/
|
|
1962
|
+
label?: string;
|
|
1963
|
+
/**
|
|
1964
|
+
* Custom style
|
|
1965
|
+
*/
|
|
1966
|
+
style?: PressableProps['style'];
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* ${name}
|
|
1971
|
+
*
|
|
1972
|
+
* A pressable component created with nativeui create.
|
|
1973
|
+
*
|
|
1974
|
+
* @example
|
|
1975
|
+
* \`\`\`tsx
|
|
1976
|
+
* <${name} label="Press me" onPress={() => console.log('Pressed!')} />
|
|
1977
|
+
* \`\`\`
|
|
1978
|
+
*/
|
|
1979
|
+
export function ${name}({ label, style, onPressIn, onPressOut, ...props }: ${name}Props) {
|
|
1980
|
+
const { colors, spacing, radius } = useTheme();
|
|
1981
|
+
const scale = useSharedValue(1);
|
|
1982
|
+
|
|
1983
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
1984
|
+
transform: [{ scale: scale.value }],
|
|
1985
|
+
}));
|
|
1986
|
+
|
|
1987
|
+
const handlePressIn = (e: any) => {
|
|
1988
|
+
scale.value = withTiming(0.97, { duration: 100 });
|
|
1989
|
+
onPressIn?.(e);
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
const handlePressOut = (e: any) => {
|
|
1993
|
+
scale.value = withTiming(1, { duration: 100 });
|
|
1994
|
+
onPressOut?.(e);
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
return (
|
|
1998
|
+
<AnimatedPressable
|
|
1999
|
+
onPressIn={handlePressIn}
|
|
2000
|
+
onPressOut={handlePressOut}
|
|
2001
|
+
style={[
|
|
2002
|
+
styles.container,
|
|
2003
|
+
{
|
|
2004
|
+
backgroundColor: colors.primary,
|
|
2005
|
+
paddingVertical: spacing[3],
|
|
2006
|
+
paddingHorizontal: spacing[4],
|
|
2007
|
+
borderRadius: radius.md,
|
|
2008
|
+
},
|
|
2009
|
+
animatedStyle,
|
|
2010
|
+
style,
|
|
2011
|
+
]}
|
|
2012
|
+
{...props}
|
|
2013
|
+
>
|
|
2014
|
+
{label && (
|
|
2015
|
+
<Text style={[styles.label, { color: colors.primaryForeground }]}>
|
|
2016
|
+
{label}
|
|
2017
|
+
</Text>
|
|
2018
|
+
)}
|
|
2019
|
+
</AnimatedPressable>
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
const styles = StyleSheet.create({
|
|
2024
|
+
container: {
|
|
2025
|
+
alignItems: 'center',
|
|
2026
|
+
justifyContent: 'center',
|
|
2027
|
+
},
|
|
2028
|
+
label: {
|
|
2029
|
+
fontSize: 16,
|
|
2030
|
+
fontWeight: '600',
|
|
2031
|
+
},
|
|
2032
|
+
});
|
|
2033
|
+
`;
|
|
2034
|
+
}
|
|
2035
|
+
function generateInputComponent(name, forwardRef) {
|
|
2036
|
+
if (forwardRef) {
|
|
2037
|
+
return `import React, { forwardRef, useState } from 'react';
|
|
2038
|
+
import { TextInput, View, Text, StyleSheet, TextInputProps } from 'react-native';
|
|
2039
|
+
import Animated, {
|
|
2040
|
+
useAnimatedStyle,
|
|
2041
|
+
useSharedValue,
|
|
2042
|
+
withTiming,
|
|
2043
|
+
} from 'react-native-reanimated';
|
|
2044
|
+
import { useTheme } from '@nativeui/core';
|
|
2045
|
+
|
|
2046
|
+
export interface ${name}Props extends TextInputProps {
|
|
2047
|
+
/**
|
|
2048
|
+
* Input label
|
|
2049
|
+
*/
|
|
2050
|
+
label?: string;
|
|
2051
|
+
/**
|
|
2052
|
+
* Error message
|
|
2053
|
+
*/
|
|
2054
|
+
error?: string;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
/**
|
|
2058
|
+
* ${name}
|
|
2059
|
+
*
|
|
2060
|
+
* A custom input component created with nativeui create.
|
|
2061
|
+
*
|
|
2062
|
+
* @example
|
|
2063
|
+
* \`\`\`tsx
|
|
2064
|
+
* <${name}
|
|
2065
|
+
* label="Email"
|
|
2066
|
+
* placeholder="Enter your email"
|
|
2067
|
+
* keyboardType="email-address"
|
|
2068
|
+
* />
|
|
2069
|
+
* \`\`\`
|
|
2070
|
+
*/
|
|
2071
|
+
export const ${name} = forwardRef<TextInput, ${name}Props>(
|
|
2072
|
+
({ label, error, style, onFocus, onBlur, ...props }, ref) => {
|
|
2073
|
+
const { colors, spacing, radius } = useTheme();
|
|
2074
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
2075
|
+
const borderColor = useSharedValue(colors.border);
|
|
2076
|
+
|
|
2077
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
2078
|
+
borderColor: borderColor.value,
|
|
2079
|
+
}));
|
|
2080
|
+
|
|
2081
|
+
const handleFocus = (e: any) => {
|
|
2082
|
+
setIsFocused(true);
|
|
2083
|
+
borderColor.value = withTiming(colors.primary, { duration: 150 });
|
|
2084
|
+
onFocus?.(e);
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
const handleBlur = (e: any) => {
|
|
2088
|
+
setIsFocused(false);
|
|
2089
|
+
borderColor.value = withTiming(
|
|
2090
|
+
error ? colors.destructive : colors.border,
|
|
2091
|
+
{ duration: 150 }
|
|
2092
|
+
);
|
|
2093
|
+
onBlur?.(e);
|
|
2094
|
+
};
|
|
2095
|
+
|
|
2096
|
+
return (
|
|
2097
|
+
<View style={styles.container}>
|
|
2098
|
+
{label && (
|
|
2099
|
+
<Text style={[styles.label, { color: colors.foreground }]}>
|
|
2100
|
+
{label}
|
|
2101
|
+
</Text>
|
|
2102
|
+
)}
|
|
2103
|
+
<Animated.View
|
|
2104
|
+
style={[
|
|
2105
|
+
styles.inputContainer,
|
|
2106
|
+
{
|
|
2107
|
+
backgroundColor: colors.background,
|
|
2108
|
+
borderRadius: radius.md,
|
|
2109
|
+
borderWidth: 1,
|
|
2110
|
+
},
|
|
2111
|
+
animatedStyle,
|
|
2112
|
+
]}
|
|
2113
|
+
>
|
|
2114
|
+
<TextInput
|
|
2115
|
+
ref={ref}
|
|
2116
|
+
style={[
|
|
2117
|
+
styles.input,
|
|
2118
|
+
{
|
|
2119
|
+
color: colors.foreground,
|
|
2120
|
+
paddingHorizontal: spacing[3],
|
|
2121
|
+
paddingVertical: spacing[2],
|
|
2122
|
+
},
|
|
2123
|
+
style,
|
|
2124
|
+
]}
|
|
2125
|
+
placeholderTextColor={colors.mutedForeground}
|
|
2126
|
+
onFocus={handleFocus}
|
|
2127
|
+
onBlur={handleBlur}
|
|
2128
|
+
{...props}
|
|
2129
|
+
/>
|
|
2130
|
+
</Animated.View>
|
|
2131
|
+
{error && (
|
|
2132
|
+
<Text style={[styles.error, { color: colors.destructive }]}>
|
|
2133
|
+
{error}
|
|
2134
|
+
</Text>
|
|
2135
|
+
)}
|
|
2136
|
+
</View>
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2139
|
+
);
|
|
2140
|
+
|
|
2141
|
+
${name}.displayName = '${name}';
|
|
2142
|
+
|
|
2143
|
+
const styles = StyleSheet.create({
|
|
2144
|
+
container: {
|
|
2145
|
+
gap: 4,
|
|
2146
|
+
},
|
|
2147
|
+
label: {
|
|
2148
|
+
fontSize: 14,
|
|
2149
|
+
fontWeight: '500',
|
|
2150
|
+
},
|
|
2151
|
+
inputContainer: {},
|
|
2152
|
+
input: {
|
|
2153
|
+
fontSize: 16,
|
|
2154
|
+
minHeight: 44,
|
|
2155
|
+
},
|
|
2156
|
+
error: {
|
|
2157
|
+
fontSize: 12,
|
|
2158
|
+
},
|
|
2159
|
+
});
|
|
2160
|
+
`;
|
|
2161
|
+
}
|
|
2162
|
+
return `import React, { useState } from 'react';
|
|
2163
|
+
import { TextInput, View, Text, StyleSheet, TextInputProps } from 'react-native';
|
|
2164
|
+
import Animated, {
|
|
2165
|
+
useAnimatedStyle,
|
|
2166
|
+
useSharedValue,
|
|
2167
|
+
withTiming,
|
|
2168
|
+
} from 'react-native-reanimated';
|
|
2169
|
+
import { useTheme } from '@nativeui/core';
|
|
2170
|
+
|
|
2171
|
+
export interface ${name}Props extends TextInputProps {
|
|
2172
|
+
/**
|
|
2173
|
+
* Input label
|
|
2174
|
+
*/
|
|
2175
|
+
label?: string;
|
|
2176
|
+
/**
|
|
2177
|
+
* Error message
|
|
2178
|
+
*/
|
|
2179
|
+
error?: string;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* ${name}
|
|
2184
|
+
*
|
|
2185
|
+
* A custom input component created with nativeui create.
|
|
2186
|
+
*
|
|
2187
|
+
* @example
|
|
2188
|
+
* \`\`\`tsx
|
|
2189
|
+
* <${name}
|
|
2190
|
+
* label="Email"
|
|
2191
|
+
* placeholder="Enter your email"
|
|
2192
|
+
* keyboardType="email-address"
|
|
2193
|
+
* />
|
|
2194
|
+
* \`\`\`
|
|
2195
|
+
*/
|
|
2196
|
+
export function ${name}({ label, error, style, onFocus, onBlur, ...props }: ${name}Props) {
|
|
2197
|
+
const { colors, spacing, radius } = useTheme();
|
|
2198
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
2199
|
+
const borderColor = useSharedValue(colors.border);
|
|
2200
|
+
|
|
2201
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
2202
|
+
borderColor: borderColor.value,
|
|
2203
|
+
}));
|
|
2204
|
+
|
|
2205
|
+
const handleFocus = (e: any) => {
|
|
2206
|
+
setIsFocused(true);
|
|
2207
|
+
borderColor.value = withTiming(colors.primary, { duration: 150 });
|
|
2208
|
+
onFocus?.(e);
|
|
2209
|
+
};
|
|
2210
|
+
|
|
2211
|
+
const handleBlur = (e: any) => {
|
|
2212
|
+
setIsFocused(false);
|
|
2213
|
+
borderColor.value = withTiming(
|
|
2214
|
+
error ? colors.destructive : colors.border,
|
|
2215
|
+
{ duration: 150 }
|
|
2216
|
+
);
|
|
2217
|
+
onBlur?.(e);
|
|
2218
|
+
};
|
|
2219
|
+
|
|
2220
|
+
return (
|
|
2221
|
+
<View style={styles.container}>
|
|
2222
|
+
{label && (
|
|
2223
|
+
<Text style={[styles.label, { color: colors.foreground }]}>
|
|
2224
|
+
{label}
|
|
2225
|
+
</Text>
|
|
2226
|
+
)}
|
|
2227
|
+
<Animated.View
|
|
2228
|
+
style={[
|
|
2229
|
+
styles.inputContainer,
|
|
2230
|
+
{
|
|
2231
|
+
backgroundColor: colors.background,
|
|
2232
|
+
borderRadius: radius.md,
|
|
2233
|
+
borderWidth: 1,
|
|
2234
|
+
},
|
|
2235
|
+
animatedStyle,
|
|
2236
|
+
]}
|
|
2237
|
+
>
|
|
2238
|
+
<TextInput
|
|
2239
|
+
style={[
|
|
2240
|
+
styles.input,
|
|
2241
|
+
{
|
|
2242
|
+
color: colors.foreground,
|
|
2243
|
+
paddingHorizontal: spacing[3],
|
|
2244
|
+
paddingVertical: spacing[2],
|
|
2245
|
+
},
|
|
2246
|
+
style,
|
|
2247
|
+
]}
|
|
2248
|
+
placeholderTextColor={colors.mutedForeground}
|
|
2249
|
+
onFocus={handleFocus}
|
|
2250
|
+
onBlur={handleBlur}
|
|
2251
|
+
{...props}
|
|
2252
|
+
/>
|
|
2253
|
+
</Animated.View>
|
|
2254
|
+
{error && (
|
|
2255
|
+
<Text style={[styles.error, { color: colors.destructive }]}>
|
|
2256
|
+
{error}
|
|
2257
|
+
</Text>
|
|
2258
|
+
)}
|
|
2259
|
+
</View>
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
const styles = StyleSheet.create({
|
|
2264
|
+
container: {
|
|
2265
|
+
gap: 4,
|
|
2266
|
+
},
|
|
2267
|
+
label: {
|
|
2268
|
+
fontSize: 14,
|
|
2269
|
+
fontWeight: '500',
|
|
2270
|
+
},
|
|
2271
|
+
inputContainer: {},
|
|
2272
|
+
input: {
|
|
2273
|
+
fontSize: 16,
|
|
2274
|
+
minHeight: 44,
|
|
2275
|
+
},
|
|
2276
|
+
error: {
|
|
2277
|
+
fontSize: 12,
|
|
2278
|
+
},
|
|
2279
|
+
});
|
|
2280
|
+
`;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// src/commands/pick.ts
|
|
2284
|
+
import { Command as Command8 } from "commander";
|
|
2285
|
+
import chalk9 from "chalk";
|
|
2286
|
+
import prompts5 from "prompts";
|
|
2287
|
+
import ora7 from "ora";
|
|
2288
|
+
import fs9 from "fs-extra";
|
|
2289
|
+
import path9 from "path";
|
|
2290
|
+
function groupByCategory(items) {
|
|
2291
|
+
const groups = {};
|
|
2292
|
+
for (const item of items) {
|
|
2293
|
+
const category = item.category || "other";
|
|
2294
|
+
if (!groups[category]) {
|
|
2295
|
+
groups[category] = [];
|
|
2296
|
+
}
|
|
2297
|
+
groups[category].push(item);
|
|
2298
|
+
}
|
|
2299
|
+
const categoryOrder = [
|
|
2300
|
+
"primitive",
|
|
2301
|
+
"input",
|
|
2302
|
+
"data-display",
|
|
2303
|
+
"overlay",
|
|
2304
|
+
"navigation",
|
|
2305
|
+
"mobile",
|
|
2306
|
+
"media",
|
|
2307
|
+
"layout",
|
|
2308
|
+
"form",
|
|
2309
|
+
"block",
|
|
2310
|
+
"screen",
|
|
2311
|
+
"other"
|
|
2312
|
+
];
|
|
2313
|
+
return categoryOrder.filter((cat) => groups[cat]?.length > 0).map((cat) => ({
|
|
2314
|
+
name: formatCategoryName(cat),
|
|
2315
|
+
items: groups[cat].sort((a, b) => a.name.localeCompare(b.name))
|
|
2316
|
+
}));
|
|
2317
|
+
}
|
|
2318
|
+
function formatCategoryName(category) {
|
|
2319
|
+
const names = {
|
|
2320
|
+
primitive: "\u{1F9F1} Primitives",
|
|
2321
|
+
input: "\u{1F4DD} Inputs & Forms",
|
|
2322
|
+
"data-display": "\u{1F4CA} Data Display",
|
|
2323
|
+
overlay: "\u{1F3AD} Overlays & Feedback",
|
|
2324
|
+
navigation: "\u{1F9ED} Navigation",
|
|
2325
|
+
mobile: "\u{1F4F1} Mobile Patterns",
|
|
2326
|
+
media: "\u{1F5BC}\uFE0F Media",
|
|
2327
|
+
layout: "\u{1F4D0} Layout",
|
|
2328
|
+
form: "\u{1F4CB} Form System",
|
|
2329
|
+
block: "\u{1F3D7}\uFE0F Blocks",
|
|
2330
|
+
screen: "\u{1F4FA} Screens",
|
|
2331
|
+
other: "\u{1F4E6} Other"
|
|
2332
|
+
};
|
|
2333
|
+
return names[category] || category;
|
|
2334
|
+
}
|
|
2335
|
+
function formatComponentChoice(item, installed) {
|
|
2336
|
+
const isInstalled = installed.has(item.name);
|
|
2337
|
+
const status = isInstalled ? chalk9.green(" \u2713") : "";
|
|
2338
|
+
const description = item.description || "";
|
|
2339
|
+
return {
|
|
2340
|
+
title: `${item.name}${status}`,
|
|
2341
|
+
description: description.length > 60 ? description.slice(0, 57) + "..." : description,
|
|
2342
|
+
value: item.name,
|
|
2343
|
+
disabled: isInstalled
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
function transformImports4(code, config) {
|
|
2347
|
+
let transformed = code;
|
|
2348
|
+
const utilsAlias = config.aliases?.utils || "@/lib/utils";
|
|
2349
|
+
if (utilsAlias === "@/lib/utils") {
|
|
2350
|
+
return transformed;
|
|
2351
|
+
}
|
|
2352
|
+
transformed = transformed.replace(
|
|
2353
|
+
/from ['"]@\/lib\/utils['"]/g,
|
|
2354
|
+
`from '${utilsAlias}'`
|
|
2355
|
+
);
|
|
2356
|
+
return transformed;
|
|
2357
|
+
}
|
|
2358
|
+
var pickCommand = new Command8().name("pick").description("Interactively browse and select components to add").option("--all", "Show all components without category selection").option("-o, --overwrite", "Overwrite existing files").option("--cwd <path>", "Working directory", process.cwd()).action(async (options) => {
|
|
2359
|
+
console.log(chalk9.bold("\n\u{1F3A8} nativeui Component Picker\n"));
|
|
2360
|
+
const cwd = path9.resolve(options.cwd);
|
|
2361
|
+
const projectRoot = await getProjectRoot(cwd);
|
|
2362
|
+
if (!projectRoot) {
|
|
2363
|
+
console.log(chalk9.red("Could not find a valid project."));
|
|
2364
|
+
console.log(chalk9.dim("Run `npx nativeui init` first.\n"));
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
const config = await getConfig(projectRoot);
|
|
2368
|
+
if (!config) {
|
|
2369
|
+
console.log(chalk9.yellow("No nativeui.config.ts found. Run `npx nativeui init` first.\n"));
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
const spinner = ora7("Loading component registry...").start();
|
|
2373
|
+
const registry = await getRegistry();
|
|
2374
|
+
if (!registry.length) {
|
|
2375
|
+
spinner.fail("Could not load registry");
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
spinner.succeed(`Loaded ${registry.length} components`);
|
|
2379
|
+
const componentsDir = path9.join(projectRoot, config.componentsPath);
|
|
2380
|
+
const installed = /* @__PURE__ */ new Set();
|
|
2381
|
+
if (fs9.existsSync(componentsDir)) {
|
|
2382
|
+
const files = fs9.readdirSync(componentsDir);
|
|
2383
|
+
for (const file of files) {
|
|
2384
|
+
if (file.endsWith(".tsx")) {
|
|
2385
|
+
installed.add(file.replace(".tsx", ""));
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
const categories = groupByCategory(registry);
|
|
2390
|
+
let selectedCategory = null;
|
|
2391
|
+
if (!options.all) {
|
|
2392
|
+
const categoryChoices = categories.map((cat) => ({
|
|
2393
|
+
title: `${cat.name} (${cat.items.length})`,
|
|
2394
|
+
value: cat
|
|
2395
|
+
}));
|
|
2396
|
+
categoryChoices.unshift({
|
|
2397
|
+
title: "\u{1F4E6} All Components",
|
|
2398
|
+
value: { name: "All", items: registry }
|
|
2399
|
+
});
|
|
2400
|
+
const categoryResponse = await prompts5({
|
|
2401
|
+
type: "select",
|
|
2402
|
+
name: "category",
|
|
2403
|
+
message: "Select a category",
|
|
2404
|
+
choices: categoryChoices
|
|
2405
|
+
});
|
|
2406
|
+
if (!categoryResponse.category) {
|
|
2407
|
+
console.log(chalk9.yellow("\nCancelled.\n"));
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
selectedCategory = categoryResponse.category;
|
|
2411
|
+
} else {
|
|
2412
|
+
selectedCategory = { name: "All", items: registry };
|
|
2413
|
+
}
|
|
2414
|
+
if (!selectedCategory) return;
|
|
2415
|
+
const componentChoices = selectedCategory.items.map(
|
|
2416
|
+
(item) => formatComponentChoice(item, installed)
|
|
2417
|
+
);
|
|
2418
|
+
const availableCount = componentChoices.filter((c) => !c.disabled).length;
|
|
2419
|
+
if (availableCount === 0) {
|
|
2420
|
+
console.log(chalk9.green("\n\u2713 All components in this category are already installed!\n"));
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
console.log(chalk9.dim(`
|
|
2424
|
+
${installed.size} already installed, ${availableCount} available
|
|
2425
|
+
`));
|
|
2426
|
+
const componentResponse = await prompts5({
|
|
2427
|
+
type: "multiselect",
|
|
2428
|
+
name: "components",
|
|
2429
|
+
message: "Select components to install (space to select, enter to confirm)",
|
|
2430
|
+
choices: componentChoices,
|
|
2431
|
+
hint: "- Space to select. Return to submit",
|
|
2432
|
+
instructions: false
|
|
2433
|
+
});
|
|
2434
|
+
if (!componentResponse.components || componentResponse.components.length === 0) {
|
|
2435
|
+
console.log(chalk9.yellow("\nNo components selected.\n"));
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
const selectedComponents = componentResponse.components;
|
|
2439
|
+
console.log(chalk9.bold("\nComponents to install:"));
|
|
2440
|
+
for (const name of selectedComponents) {
|
|
2441
|
+
console.log(chalk9.cyan(` \u2022 ${name}`));
|
|
2442
|
+
}
|
|
2443
|
+
const confirmResponse = await prompts5({
|
|
2444
|
+
type: "confirm",
|
|
2445
|
+
name: "proceed",
|
|
2446
|
+
message: `Install ${selectedComponents.length} component(s)?`,
|
|
2447
|
+
initial: true
|
|
2448
|
+
});
|
|
2449
|
+
if (!confirmResponse.proceed) {
|
|
2450
|
+
console.log(chalk9.yellow("\nCancelled.\n"));
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2453
|
+
console.log("");
|
|
2454
|
+
let successCount = 0;
|
|
2455
|
+
const allDependencies = [];
|
|
2456
|
+
const allDevDependencies = [];
|
|
2457
|
+
for (const name of selectedComponents) {
|
|
2458
|
+
const installSpinner = ora7(`Installing ${name}...`).start();
|
|
2459
|
+
try {
|
|
2460
|
+
const component = await fetchComponent(name);
|
|
2461
|
+
if (!component) {
|
|
2462
|
+
installSpinner.fail(`Component "${name}" not found`);
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
const targetDir = path9.join(projectRoot, config.componentsPath);
|
|
2466
|
+
for (const file of component.files) {
|
|
2467
|
+
const targetPath = path9.join(targetDir, file.name);
|
|
2468
|
+
if (await fs9.pathExists(targetPath) && !options.overwrite) {
|
|
2469
|
+
installSpinner.warn(`${name}: ${file.name} already exists (use --overwrite)`);
|
|
2470
|
+
continue;
|
|
2471
|
+
}
|
|
2472
|
+
await fs9.ensureDir(targetDir);
|
|
2473
|
+
const transformedContent = transformImports4(file.content, config);
|
|
2474
|
+
await fs9.writeFile(targetPath, transformedContent);
|
|
2475
|
+
}
|
|
2476
|
+
installSpinner.succeed(`Installed ${chalk9.green(name)}`);
|
|
2477
|
+
successCount++;
|
|
2478
|
+
if (component.dependencies?.length) {
|
|
2479
|
+
allDependencies.push(...component.dependencies);
|
|
2480
|
+
}
|
|
2481
|
+
if (component.devDependencies?.length) {
|
|
2482
|
+
allDevDependencies.push(...component.devDependencies);
|
|
2483
|
+
}
|
|
2484
|
+
if (component.registryDependencies?.length) {
|
|
2485
|
+
console.log(chalk9.dim(` Requires: ${component.registryDependencies.join(", ")}`));
|
|
2486
|
+
}
|
|
2487
|
+
} catch (error) {
|
|
2488
|
+
installSpinner.fail(`Failed to install ${name}: ${error}`);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
console.log(
|
|
2492
|
+
chalk9.bold.green(`
|
|
2493
|
+
\u2713 Successfully installed ${successCount}/${selectedComponents.length} components
|
|
2494
|
+
`)
|
|
2495
|
+
);
|
|
2496
|
+
const uniqueDeps = [...new Set(allDependencies)];
|
|
2497
|
+
const uniqueDevDeps = [...new Set(allDevDependencies)];
|
|
2498
|
+
if (uniqueDeps.length || uniqueDevDeps.length) {
|
|
2499
|
+
console.log(chalk9.bold("Install dependencies:"));
|
|
2500
|
+
if (uniqueDeps.length) {
|
|
2501
|
+
console.log(chalk9.cyan(` npx expo install ${uniqueDeps.join(" ")}`));
|
|
2502
|
+
}
|
|
2503
|
+
if (uniqueDevDeps.length) {
|
|
2504
|
+
console.log(chalk9.cyan(` npm install -D ${uniqueDevDeps.join(" ")}`));
|
|
2505
|
+
}
|
|
2506
|
+
console.log("");
|
|
2507
|
+
}
|
|
2508
|
+
if (successCount > 0) {
|
|
2509
|
+
console.log(chalk9.dim("Import example:"));
|
|
2510
|
+
const firstComponent = selectedComponents[0];
|
|
2511
|
+
const pascalName = firstComponent.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
2512
|
+
const alias = config.aliases?.components || "@/components/ui";
|
|
2513
|
+
console.log(chalk9.dim(` import { ${pascalName} } from '${alias}/${firstComponent}';
|
|
2514
|
+
`));
|
|
2515
|
+
}
|
|
2516
|
+
});
|
|
2517
|
+
|
|
2518
|
+
// src/index.ts
|
|
2519
|
+
var program = new Command9();
|
|
2520
|
+
program.name("nativeui").description("Add beautiful UI components to your Expo/React Native project").version("0.0.1");
|
|
2521
|
+
program.addCommand(initCommand);
|
|
2522
|
+
program.addCommand(addCommand);
|
|
2523
|
+
program.addCommand(listCommand);
|
|
2524
|
+
program.addCommand(doctorCommand);
|
|
2525
|
+
program.addCommand(diffCommand);
|
|
2526
|
+
program.addCommand(updateCommand);
|
|
2527
|
+
program.addCommand(createCommand);
|
|
2528
|
+
program.addCommand(pickCommand);
|
|
2529
|
+
program.parse();
|