novaui-cli 1.0.4 → 1.0.6
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 +118 -0
- package/package.json +3 -2
- package/src/bin.js +98 -18
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# novaui-cli
|
|
2
|
+
|
|
3
|
+
The CLI for **NovaUI** -- a React Native UI component library with 50+ components built on [NativeWind](https://www.nativewind.dev/) (Tailwind CSS for React Native).
|
|
4
|
+
|
|
5
|
+
Inspired by [shadcn/ui](https://ui.shadcn.com), adapted for mobile. Components are copied directly into your project -- you own the code, customize anything.
|
|
6
|
+
|
|
7
|
+
## Getting Started
|
|
8
|
+
|
|
9
|
+
### Initialize your project
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx novaui-cli init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The interactive setup walks you through configuring file paths:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
? Path for global.css? (src/global.css)
|
|
19
|
+
? Path for UI components? (src/components/ui)
|
|
20
|
+
? Path for lib (utils)? (src/lib)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This creates:
|
|
24
|
+
|
|
25
|
+
- **`components.json`** -- stores your configured paths
|
|
26
|
+
- **`tailwind.config.js`** -- NovaUI theme with colors, border radius, and NativeWind preset
|
|
27
|
+
- **`global.css`** -- light and dark theme CSS variables
|
|
28
|
+
- **`lib/utils.ts`** -- the `cn()` class merging utility
|
|
29
|
+
|
|
30
|
+
It also installs the required dependencies: `nativewind`, `tailwindcss`, `clsx`, `tailwind-merge`, `class-variance-authority`.
|
|
31
|
+
|
|
32
|
+
### Add components
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx novaui-cli add button
|
|
36
|
+
npx novaui-cli add card
|
|
37
|
+
npx novaui-cli add dialog
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Components are copied into your configured directory (default: `src/components/ui/`). Each component's dependencies are installed automatically.
|
|
41
|
+
|
|
42
|
+
### Use them
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { Button } from "./src/components/ui/button"
|
|
46
|
+
import { Card, CardHeader, CardTitle, CardContent } from "./src/components/ui/card"
|
|
47
|
+
|
|
48
|
+
export default function App() {
|
|
49
|
+
return (
|
|
50
|
+
<Card>
|
|
51
|
+
<CardHeader>
|
|
52
|
+
<CardTitle>Welcome to NovaUI</CardTitle>
|
|
53
|
+
</CardHeader>
|
|
54
|
+
<CardContent>
|
|
55
|
+
<Button onPress={() => console.log("Pressed!")}>
|
|
56
|
+
Get Started
|
|
57
|
+
</Button>
|
|
58
|
+
</CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Don't forget to import your `global.css` in your root entry file (e.g. `App.tsx`):
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import "./src/global.css"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
| Command | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `npx novaui-cli init` | Interactive setup -- config, Tailwind, global CSS, utils |
|
|
75
|
+
| `npx novaui-cli add <name>` | Add a component to your project |
|
|
76
|
+
| `npx novaui-cli --version` | Show CLI version |
|
|
77
|
+
| `npx novaui-cli --help` | Show help |
|
|
78
|
+
|
|
79
|
+
## Available Components
|
|
80
|
+
|
|
81
|
+
Accordion, Alert, Alert Dialog, Aspect Ratio, Avatar, Badge, Breadcrumb, Button, Button Group, Calendar, Card, Carousel, Chart, Checkbox, Collapsible, Combobox, Command, Context Menu, Dialog, Drawer, Dropdown Menu, Empty, Field, Hover Card, Input, Input Group, Input OTP, Item, Label, Menubar, Navigation Menu, Pagination, Popover, Progress, Radio Group, Resizable, Scroll Area, Select, Separator, Sheet, Skeleton, Spinner, Switch, Table, Tabs, Text, Textarea, Toggle, Toggle Group, Typography
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx novaui-cli add <name>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use the component filename without the extension: `button`, `card`, `alert-dialog`, `dropdown-menu`, etc.
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
Running `init` creates a `components.json` in your project root:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"globalCss": "src/global.css",
|
|
96
|
+
"componentsUi": "src/components/ui",
|
|
97
|
+
"lib": "src/lib"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Edit this file or run `npx novaui-cli init` again to change paths.
|
|
102
|
+
|
|
103
|
+
## Requirements
|
|
104
|
+
|
|
105
|
+
| Package | Version |
|
|
106
|
+
|---|---|
|
|
107
|
+
| react | >= 18 |
|
|
108
|
+
| react-native | >= 0.72 |
|
|
109
|
+
| nativewind | >= 4 |
|
|
110
|
+
| tailwindcss | >= 3 |
|
|
111
|
+
|
|
112
|
+
## Links
|
|
113
|
+
|
|
114
|
+
- [GitHub](https://github.com/KaloyanBehov/native-ui)
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "novaui-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "CLI for NovaUI - React Native component library with NativeWind",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"novaui": "./src/bin.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"src"
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
11
12
|
],
|
|
12
13
|
"keywords": [
|
|
13
14
|
"novaui",
|
package/src/bin.js
CHANGED
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
import fs from "node:fs"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import readline from "node:readline"
|
|
6
|
-
import {
|
|
6
|
+
import { execFileSync } from "node:child_process"
|
|
7
|
+
import { fileURLToPath } from "node:url"
|
|
7
8
|
|
|
8
9
|
const BASE_URL =
|
|
9
10
|
"https://raw.githubusercontent.com/KaloyanBehov/native-ui/main"
|
|
10
11
|
|
|
11
12
|
const CONFIG_FILENAME = "components.json"
|
|
13
|
+
const FETCH_TIMEOUT_MS = 15000
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
16
|
+
const __dirname = path.dirname(__filename)
|
|
17
|
+
const CLI_PACKAGE_JSON_PATH = path.resolve(__dirname, "../package.json")
|
|
12
18
|
|
|
13
19
|
const DEFAULT_CONFIG = {
|
|
14
20
|
globalCss: "src/global.css",
|
|
@@ -156,6 +162,7 @@ module.exports = {
|
|
|
156
162
|
plugins: [],
|
|
157
163
|
};
|
|
158
164
|
`
|
|
165
|
+
}
|
|
159
166
|
|
|
160
167
|
// ─── Utils ──────────────────────────────────────────────────────────────────
|
|
161
168
|
|
|
@@ -171,10 +178,21 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
171
178
|
|
|
172
179
|
function detectPackageManager() {
|
|
173
180
|
const userAgent = process.env.npm_config_user_agent || ""
|
|
174
|
-
if (userAgent.startsWith("yarn")) return "yarn add"
|
|
175
|
-
if (userAgent.startsWith("pnpm")) return "pnpm add"
|
|
176
|
-
if (userAgent.startsWith("bun")) return "bun add"
|
|
177
|
-
return "npm install"
|
|
181
|
+
if (userAgent.startsWith("yarn")) return { command: "yarn", baseArgs: ["add"] }
|
|
182
|
+
if (userAgent.startsWith("pnpm")) return { command: "pnpm", baseArgs: ["add"] }
|
|
183
|
+
if (userAgent.startsWith("bun")) return { command: "bun", baseArgs: ["add"] }
|
|
184
|
+
return { command: "npm", baseArgs: ["install"] }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function installPackages(packages) {
|
|
188
|
+
if (!Array.isArray(packages) || packages.length === 0) return
|
|
189
|
+
const { command, baseArgs } = detectPackageManager()
|
|
190
|
+
execFileSync(command, [...baseArgs, ...packages], { stdio: "inherit" })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getInstallHint(packages) {
|
|
194
|
+
const { command, baseArgs } = detectPackageManager()
|
|
195
|
+
return `${command} ${[...baseArgs, ...packages].join(" ")}`
|
|
178
196
|
}
|
|
179
197
|
|
|
180
198
|
function ensureDir(dir) {
|
|
@@ -188,7 +206,7 @@ function writeIfNotExists(filePath, content, label) {
|
|
|
188
206
|
console.log(style(c.dim, ` ℹ ${label} already exists, skipping.`))
|
|
189
207
|
return false
|
|
190
208
|
}
|
|
191
|
-
fs.writeFileSync(filePath, content)
|
|
209
|
+
fs.writeFileSync(filePath, content, "utf8")
|
|
192
210
|
console.log(style(c.green, ` ✓ Created ${label}`))
|
|
193
211
|
return true
|
|
194
212
|
}
|
|
@@ -209,6 +227,9 @@ function getMissingDeps(cwd, deps) {
|
|
|
209
227
|
|
|
210
228
|
/** Ask user a question; returns trimmed answer or default. */
|
|
211
229
|
function ask(question, defaultAnswer = "") {
|
|
230
|
+
if (process.stdin.isTTY !== true) {
|
|
231
|
+
return Promise.resolve(defaultAnswer)
|
|
232
|
+
}
|
|
212
233
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
213
234
|
const defaultPart = defaultAnswer ? style(c.dim, ` (${defaultAnswer})`) : ""
|
|
214
235
|
const prompt = style(c.cyan, " ? ") + question + defaultPart + style(c.dim, " ")
|
|
@@ -236,7 +257,55 @@ function loadConfig(cwd) {
|
|
|
236
257
|
/** Write components.json to cwd. */
|
|
237
258
|
function writeConfig(cwd, config) {
|
|
238
259
|
const configPath = path.join(cwd, CONFIG_FILENAME)
|
|
239
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
260
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8")
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getCliVersion() {
|
|
264
|
+
try {
|
|
265
|
+
const pkg = JSON.parse(fs.readFileSync(CLI_PACKAGE_JSON_PATH, "utf8"))
|
|
266
|
+
return pkg.version || "unknown"
|
|
267
|
+
} catch {
|
|
268
|
+
return "unknown"
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function assertValidComponentConfig(componentName, componentConfig) {
|
|
273
|
+
if (!componentConfig || typeof componentConfig !== "object") {
|
|
274
|
+
throw new Error(`Registry entry for "${componentName}" is invalid.`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const { files, dependencies } = componentConfig
|
|
278
|
+
if (!Array.isArray(files) || files.some((file) => typeof file !== "string" || file.trim() === "")) {
|
|
279
|
+
throw new Error(`Registry entry for "${componentName}" must include a valid "files" array.`)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (
|
|
283
|
+
dependencies !== undefined &&
|
|
284
|
+
(!Array.isArray(dependencies) ||
|
|
285
|
+
dependencies.some((dep) => typeof dep !== "string" || dep.trim() === ""))
|
|
286
|
+
) {
|
|
287
|
+
throw new Error(`Registry entry for "${componentName}" has an invalid "dependencies" array.`)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function fetchWithTimeout(url) {
|
|
292
|
+
const controller = new AbortController()
|
|
293
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
294
|
+
try {
|
|
295
|
+
return await fetch(url, { signal: controller.signal })
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error && error.name === "AbortError") {
|
|
298
|
+
throw new Error(`Request timed out after ${FETCH_TIMEOUT_MS}ms: ${url}`)
|
|
299
|
+
}
|
|
300
|
+
throw error
|
|
301
|
+
} finally {
|
|
302
|
+
clearTimeout(timeout)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function formatError(error) {
|
|
307
|
+
if (error instanceof Error && error.message) return error.message
|
|
308
|
+
return String(error)
|
|
240
309
|
}
|
|
241
310
|
|
|
242
311
|
// ─── Commands ───────────────────────────────────────────────────────────────
|
|
@@ -323,13 +392,12 @@ async function init() {
|
|
|
323
392
|
} else {
|
|
324
393
|
console.log(style(c.dim, ` Installing: ${missingDeps.join(", ")}`))
|
|
325
394
|
console.log("")
|
|
326
|
-
const installCmd = detectPackageManager()
|
|
327
395
|
try {
|
|
328
|
-
|
|
396
|
+
installPackages(missingDeps)
|
|
329
397
|
} catch {
|
|
330
398
|
console.error("")
|
|
331
399
|
console.error(style(c.yellow, " ✗ Install failed. Run manually:"))
|
|
332
|
-
console.error(style(c.dim, ` ${
|
|
400
|
+
console.error(style(c.dim, ` ${getInstallHint(missingDeps)}`))
|
|
333
401
|
}
|
|
334
402
|
}
|
|
335
403
|
|
|
@@ -364,13 +432,16 @@ async function add(componentName) {
|
|
|
364
432
|
|
|
365
433
|
// 1. Fetch registry
|
|
366
434
|
console.log(" Fetching registry...")
|
|
367
|
-
const registryResponse = await
|
|
435
|
+
const registryResponse = await fetchWithTimeout(`${BASE_URL}/registry.json`)
|
|
368
436
|
|
|
369
437
|
if (!registryResponse.ok) {
|
|
370
438
|
throw new Error(`Failed to fetch registry: ${registryResponse.statusText}`)
|
|
371
439
|
}
|
|
372
440
|
|
|
373
441
|
const registry = await registryResponse.json()
|
|
442
|
+
if (!registry || typeof registry !== "object" || Array.isArray(registry)) {
|
|
443
|
+
throw new Error("Registry response is not a valid object.")
|
|
444
|
+
}
|
|
374
445
|
|
|
375
446
|
if (!registry[componentName]) {
|
|
376
447
|
console.error(` ✗ Component "${componentName}" not found in registry.`)
|
|
@@ -381,6 +452,7 @@ async function add(componentName) {
|
|
|
381
452
|
}
|
|
382
453
|
|
|
383
454
|
const componentConfig = registry[componentName]
|
|
455
|
+
assertValidComponentConfig(componentName, componentConfig)
|
|
384
456
|
const projectConfig = loadConfig(cwd) || DEFAULT_CONFIG
|
|
385
457
|
const targetBaseDir = path.join(cwd, projectConfig.componentsUi)
|
|
386
458
|
ensureDir(targetBaseDir)
|
|
@@ -408,7 +480,7 @@ async function add(componentName) {
|
|
|
408
480
|
const destPath = path.join(targetBaseDir, fileName)
|
|
409
481
|
|
|
410
482
|
console.log(` Downloading ${fileName}...`)
|
|
411
|
-
const fileResponse = await
|
|
483
|
+
const fileResponse = await fetchWithTimeout(fileUrl)
|
|
412
484
|
|
|
413
485
|
if (!fileResponse.ok) {
|
|
414
486
|
console.error(` ✗ Failed to download ${fileName}`)
|
|
@@ -416,7 +488,7 @@ async function add(componentName) {
|
|
|
416
488
|
}
|
|
417
489
|
|
|
418
490
|
const content = await fileResponse.text()
|
|
419
|
-
fs.writeFileSync(destPath, content)
|
|
491
|
+
fs.writeFileSync(destPath, content, "utf8")
|
|
420
492
|
console.log(` ✓ Added ${fileName}`)
|
|
421
493
|
}
|
|
422
494
|
|
|
@@ -430,10 +502,7 @@ async function add(componentName) {
|
|
|
430
502
|
console.log("")
|
|
431
503
|
console.log(` Installing dependencies: ${missingDeps.join(", ")}...`)
|
|
432
504
|
try {
|
|
433
|
-
|
|
434
|
-
execSync(`${installCmd} ${missingDeps.join(" ")}`, {
|
|
435
|
-
stdio: "inherit",
|
|
436
|
-
})
|
|
505
|
+
installPackages(missingDeps)
|
|
437
506
|
} catch {
|
|
438
507
|
console.error(" ✗ Failed to install dependencies automatically.")
|
|
439
508
|
}
|
|
@@ -447,9 +516,12 @@ async function add(componentName) {
|
|
|
447
516
|
|
|
448
517
|
function showHelp() {
|
|
449
518
|
console.log(ASCII_BANNER)
|
|
519
|
+
console.log(style(c.dim, ` Version: ${getCliVersion()}`))
|
|
520
|
+
console.log("")
|
|
450
521
|
console.log(style(c.bold, " Usage"))
|
|
451
522
|
console.log(style(c.dim, " novaui init Set up NovaUI (config, Tailwind, global.css, utils)"))
|
|
452
523
|
console.log(style(c.dim, " novaui add <component> Add a component (e.g. button, card)"))
|
|
524
|
+
console.log(style(c.dim, " novaui --version Show CLI version"))
|
|
453
525
|
console.log("")
|
|
454
526
|
console.log(style(c.bold, " Examples"))
|
|
455
527
|
console.log(style(c.cyan, " npx novaui init"))
|
|
@@ -458,6 +530,10 @@ function showHelp() {
|
|
|
458
530
|
console.log("")
|
|
459
531
|
}
|
|
460
532
|
|
|
533
|
+
function showVersion() {
|
|
534
|
+
console.log(getCliVersion())
|
|
535
|
+
}
|
|
536
|
+
|
|
461
537
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
462
538
|
|
|
463
539
|
const command = process.argv[2]
|
|
@@ -476,6 +552,10 @@ async function main() {
|
|
|
476
552
|
case "-h":
|
|
477
553
|
showHelp()
|
|
478
554
|
break
|
|
555
|
+
case "--version":
|
|
556
|
+
case "-v":
|
|
557
|
+
showVersion()
|
|
558
|
+
break
|
|
479
559
|
default:
|
|
480
560
|
if (command) {
|
|
481
561
|
// Backwards compatible: treat unknown command as component name
|
|
@@ -487,7 +567,7 @@ async function main() {
|
|
|
487
567
|
}
|
|
488
568
|
} catch (error) {
|
|
489
569
|
console.error("")
|
|
490
|
-
console.error(` ✗ Error: ${error
|
|
570
|
+
console.error(` ✗ Error: ${formatError(error)}`)
|
|
491
571
|
process.exit(1)
|
|
492
572
|
}
|
|
493
573
|
}
|