novaui-cli 1.1.1 → 1.1.3
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 +111 -0
- package/package.json +40 -13
- package/src/__tests__/commands.test.js +491 -0
- package/src/__tests__/fuzzy.test.js +150 -0
- package/src/__tests__/helpers.test.js +520 -0
- package/src/__tests__/preflight.test.js +379 -0
- package/src/__tests__/version-check.test.js +58 -0
- package/src/bin.js +103 -590
- package/src/commands/add.js +168 -0
- package/src/commands/init.js +330 -0
- package/src/constants.js +101 -0
- package/src/themes/default.js +51 -0
- package/src/themes/index.js +48 -0
- package/src/themes/ocean.js +51 -0
- package/src/themes/sunset.js +51 -0
- package/src/utils/config.js +22 -0
- package/src/utils/deps.js +40 -0
- package/src/utils/fetch.js +21 -0
- package/src/utils/fs-helpers.js +18 -0
- package/src/utils/fuzzy.js +99 -0
- package/src/utils/preflight.js +172 -0
- package/src/utils/tailwind.js +60 -0
- package/src/utils/validate.js +17 -0
- package/src/utils/version-check.js +136 -0
- package/src/utils/version.js +17 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const OCEAN_THEME_CSS = `@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 210 40% 98%;
|
|
8
|
+
--foreground: 210 30% 16%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 210 30% 16%;
|
|
11
|
+
--popover: 0 0% 100%;
|
|
12
|
+
--popover-foreground: 210 30% 16%;
|
|
13
|
+
--primary: 200 95% 46%;
|
|
14
|
+
--primary-foreground: 0 0% 100%;
|
|
15
|
+
--secondary: 197 46% 92%;
|
|
16
|
+
--secondary-foreground: 210 25% 24%;
|
|
17
|
+
--muted: 197 46% 92%;
|
|
18
|
+
--muted-foreground: 210 16% 42%;
|
|
19
|
+
--accent: 189 70% 88%;
|
|
20
|
+
--accent-foreground: 210 28% 20%;
|
|
21
|
+
--destructive: 0 75% 55%;
|
|
22
|
+
--destructive-foreground: 0 0% 100%;
|
|
23
|
+
--border: 197 30% 84%;
|
|
24
|
+
--input: 197 30% 84%;
|
|
25
|
+
--ring: 200 95% 46%;
|
|
26
|
+
--radius: 0.75rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dark {
|
|
30
|
+
--background: 212 48% 10%;
|
|
31
|
+
--foreground: 210 20% 94%;
|
|
32
|
+
--card: 212 45% 13%;
|
|
33
|
+
--card-foreground: 210 20% 94%;
|
|
34
|
+
--popover: 212 45% 13%;
|
|
35
|
+
--popover-foreground: 210 20% 94%;
|
|
36
|
+
--primary: 196 100% 60%;
|
|
37
|
+
--primary-foreground: 210 50% 12%;
|
|
38
|
+
--secondary: 212 30% 20%;
|
|
39
|
+
--secondary-foreground: 210 20% 94%;
|
|
40
|
+
--muted: 212 30% 20%;
|
|
41
|
+
--muted-foreground: 209 15% 68%;
|
|
42
|
+
--accent: 202 45% 24%;
|
|
43
|
+
--accent-foreground: 210 20% 94%;
|
|
44
|
+
--destructive: 0 65% 45%;
|
|
45
|
+
--destructive-foreground: 0 0% 100%;
|
|
46
|
+
--border: 212 28% 24%;
|
|
47
|
+
--input: 212 28% 24%;
|
|
48
|
+
--ring: 196 100% 60%;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const SUNSET_THEME_CSS = `@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 24 100% 98%;
|
|
8
|
+
--foreground: 12 30% 18%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 12 30% 18%;
|
|
11
|
+
--popover: 0 0% 100%;
|
|
12
|
+
--popover-foreground: 12 30% 18%;
|
|
13
|
+
--primary: 18 95% 53%;
|
|
14
|
+
--primary-foreground: 0 0% 100%;
|
|
15
|
+
--secondary: 22 78% 92%;
|
|
16
|
+
--secondary-foreground: 16 32% 26%;
|
|
17
|
+
--muted: 22 78% 92%;
|
|
18
|
+
--muted-foreground: 18 20% 46%;
|
|
19
|
+
--accent: 340 84% 93%;
|
|
20
|
+
--accent-foreground: 336 32% 28%;
|
|
21
|
+
--destructive: 0 73% 54%;
|
|
22
|
+
--destructive-foreground: 0 0% 100%;
|
|
23
|
+
--border: 20 58% 84%;
|
|
24
|
+
--input: 20 58% 84%;
|
|
25
|
+
--ring: 18 95% 53%;
|
|
26
|
+
--radius: 0.75rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dark {
|
|
30
|
+
--background: 16 46% 12%;
|
|
31
|
+
--foreground: 24 60% 95%;
|
|
32
|
+
--card: 15 42% 16%;
|
|
33
|
+
--card-foreground: 24 60% 95%;
|
|
34
|
+
--popover: 15 42% 16%;
|
|
35
|
+
--popover-foreground: 24 60% 95%;
|
|
36
|
+
--primary: 24 100% 64%;
|
|
37
|
+
--primary-foreground: 16 46% 12%;
|
|
38
|
+
--secondary: 11 28% 24%;
|
|
39
|
+
--secondary-foreground: 24 60% 95%;
|
|
40
|
+
--muted: 11 28% 24%;
|
|
41
|
+
--muted-foreground: 20 24% 72%;
|
|
42
|
+
--accent: 332 42% 30%;
|
|
43
|
+
--accent-foreground: 24 60% 95%;
|
|
44
|
+
--destructive: 0 62% 45%;
|
|
45
|
+
--destructive-foreground: 0 0% 100%;
|
|
46
|
+
--border: 14 24% 28%;
|
|
47
|
+
--input: 14 24% 28%;
|
|
48
|
+
--ring: 24 100% 64%;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CONFIG_FILENAME, DEFAULT_CONFIG } from '../constants.js';
|
|
4
|
+
|
|
5
|
+
/** Load components.json from cwd; returns null if missing or invalid. */
|
|
6
|
+
export function loadConfig(cwd) {
|
|
7
|
+
const configPath = path.join(cwd, CONFIG_FILENAME);
|
|
8
|
+
if (!fs.existsSync(configPath)) return null;
|
|
9
|
+
try {
|
|
10
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
11
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
12
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Write components.json to cwd. */
|
|
19
|
+
export function writeConfig(cwd, config) {
|
|
20
|
+
const configPath = path.join(cwd, CONFIG_FILENAME);
|
|
21
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
22
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export function detectPackageManager() {
|
|
6
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
7
|
+
if (userAgent.startsWith('yarn')) return { command: 'yarn', baseArgs: ['add'], devFlag: '--dev' };
|
|
8
|
+
if (userAgent.startsWith('pnpm')) return { command: 'pnpm', baseArgs: ['add'], devFlag: '--save-dev' };
|
|
9
|
+
if (userAgent.startsWith('bun')) return { command: 'bun', baseArgs: ['add'], devFlag: '--dev' };
|
|
10
|
+
return { command: 'npm', baseArgs: ['install'], devFlag: '--save-dev' };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function installPackages(packages, options = {}) {
|
|
14
|
+
if (!Array.isArray(packages) || packages.length === 0) return;
|
|
15
|
+
const { dev = false } = options;
|
|
16
|
+
const { command, baseArgs, devFlag } = detectPackageManager();
|
|
17
|
+
const args = dev ? [...baseArgs, devFlag, ...packages] : [...baseArgs, ...packages];
|
|
18
|
+
execFileSync(command, args, { stdio: 'inherit' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getInstallHint(packages, options = {}) {
|
|
22
|
+
const { dev = false } = options;
|
|
23
|
+
const { command, baseArgs, devFlag } = detectPackageManager();
|
|
24
|
+
const args = dev ? [...baseArgs, devFlag, ...packages] : [...baseArgs, ...packages];
|
|
25
|
+
return `${command} ${args.join(' ')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Returns which of the requested deps are not listed in package.json (dependencies or devDependencies). */
|
|
29
|
+
export function getMissingDeps(cwd, deps) {
|
|
30
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
31
|
+
if (!fs.existsSync(pkgPath)) {
|
|
32
|
+
return [...deps];
|
|
33
|
+
}
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
35
|
+
const installed = new Set([
|
|
36
|
+
...Object.keys(pkg.dependencies || {}),
|
|
37
|
+
...Object.keys(pkg.devDependencies || {}),
|
|
38
|
+
]);
|
|
39
|
+
return deps.filter((d) => !installed.has(d));
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FETCH_TIMEOUT_MS } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
export async function fetchWithTimeout(url) {
|
|
4
|
+
const controller = new AbortController();
|
|
5
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
6
|
+
try {
|
|
7
|
+
return await fetch(url, { signal: controller.signal });
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error && error.name === 'AbortError') {
|
|
10
|
+
throw new Error(`Request timed out after ${FETCH_TIMEOUT_MS}ms: ${url}`);
|
|
11
|
+
}
|
|
12
|
+
throw error;
|
|
13
|
+
} finally {
|
|
14
|
+
clearTimeout(timeout);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatError(error) {
|
|
19
|
+
if (error instanceof Error && error.message) return error.message;
|
|
20
|
+
return String(error);
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
|
|
4
|
+
export function ensureDir(dir) {
|
|
5
|
+
if (!fs.existsSync(dir)) {
|
|
6
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function writeIfNotExists(filePath, content, label) {
|
|
11
|
+
if (fs.existsSync(filePath)) {
|
|
12
|
+
console.log(pc.dim(` ℹ ${label} already exists, skipping.`));
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
16
|
+
console.log(pc.green(` ✓ Created ${label}`));
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fuzzy matching utilities for better error messages
|
|
3
|
+
* Helps users find components when they mistype names
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate Levenshtein distance between two strings
|
|
8
|
+
* Used for finding similar component names
|
|
9
|
+
*/
|
|
10
|
+
function levenshteinDistance(str1, str2) {
|
|
11
|
+
const len1 = str1.length
|
|
12
|
+
const len2 = str2.length
|
|
13
|
+
const matrix = []
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i <= len1; i++) {
|
|
16
|
+
matrix[i] = [i]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (let j = 0; j <= len2; j++) {
|
|
20
|
+
matrix[0][j] = j
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (let i = 1; i <= len1; i++) {
|
|
24
|
+
for (let j = 1; j <= len2; j++) {
|
|
25
|
+
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1
|
|
26
|
+
matrix[i][j] = Math.min(
|
|
27
|
+
matrix[i - 1][j] + 1,
|
|
28
|
+
matrix[i][j - 1] + 1,
|
|
29
|
+
matrix[i - 1][j - 1] + cost
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return matrix[len1][len2]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find similar component names based on typo distance
|
|
39
|
+
* Returns up to maxResults similar names, sorted by similarity
|
|
40
|
+
*/
|
|
41
|
+
export function findSimilarComponents(input, availableComponents, maxResults = 5) {
|
|
42
|
+
const normalizedInput = input.toLowerCase().trim()
|
|
43
|
+
|
|
44
|
+
const scored = availableComponents
|
|
45
|
+
.map(name => ({
|
|
46
|
+
name,
|
|
47
|
+
distance: levenshteinDistance(normalizedInput, name.toLowerCase()),
|
|
48
|
+
startsWithSame: name.toLowerCase().startsWith(normalizedInput[0]),
|
|
49
|
+
}))
|
|
50
|
+
.filter(item => item.distance <= 4) // Only suggest if reasonably close
|
|
51
|
+
.sort((a, b) => {
|
|
52
|
+
if (a.distance !== b.distance) {
|
|
53
|
+
return a.distance - b.distance
|
|
54
|
+
}
|
|
55
|
+
if (a.startsWithSame !== b.startsWithSame) {
|
|
56
|
+
return a.startsWithSame ? -1 : 1
|
|
57
|
+
}
|
|
58
|
+
return a.name.localeCompare(b.name)
|
|
59
|
+
})
|
|
60
|
+
.slice(0, maxResults)
|
|
61
|
+
.map(item => item.name)
|
|
62
|
+
|
|
63
|
+
return scored
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format a helpful error message for missing components
|
|
68
|
+
*/
|
|
69
|
+
export function formatComponentNotFoundError(componentName, availableComponents) {
|
|
70
|
+
const similar = findSimilarComponents(componentName, availableComponents)
|
|
71
|
+
|
|
72
|
+
let message = `Component "${componentName}" not found in registry.`
|
|
73
|
+
|
|
74
|
+
if (similar.length > 0) {
|
|
75
|
+
message += '\n\n Did you mean one of these?'
|
|
76
|
+
similar.forEach(name => {
|
|
77
|
+
message += `\n • ${name}`
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
message += '\n\n Available components:'
|
|
82
|
+
|
|
83
|
+
const sorted = [...availableComponents].sort()
|
|
84
|
+
const columns = 3
|
|
85
|
+
const itemsPerColumn = Math.ceil(sorted.length / columns)
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < itemsPerColumn; i++) {
|
|
88
|
+
let row = ' '
|
|
89
|
+
for (let col = 0; col < columns; col++) {
|
|
90
|
+
const idx = i + col * itemsPerColumn
|
|
91
|
+
if (idx < sorted.length) {
|
|
92
|
+
row += sorted[idx].padEnd(20)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
message += '\n' + row.trimEnd()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return message
|
|
99
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import pc from 'picocolors'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pre-flight checks to ensure the project environment is ready for NovaUI
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function checkPackageJson(cwd) {
|
|
10
|
+
const pkgPath = path.join(cwd, 'package.json')
|
|
11
|
+
if (!fs.existsSync(pkgPath)) {
|
|
12
|
+
throw new Error('package.json not found. Please run this command in a valid Node.js project.')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
17
|
+
return pkg
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new Error('package.json is invalid or cannot be parsed.')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function checkReactNativeProject(pkg) {
|
|
24
|
+
const deps = {
|
|
25
|
+
...pkg.dependencies,
|
|
26
|
+
...pkg.devDependencies,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!deps['react-native'] && !deps['expo']) {
|
|
30
|
+
console.log('')
|
|
31
|
+
console.log(pc.yellow(' ⚠ Warning: react-native or expo not found in dependencies.'))
|
|
32
|
+
console.log(pc.dim(' NovaUI is designed for React Native projects.'))
|
|
33
|
+
console.log('')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function checkBabelConfig(cwd) {
|
|
38
|
+
const babelPaths = [
|
|
39
|
+
path.join(cwd, 'babel.config.js'),
|
|
40
|
+
path.join(cwd, 'babel.config.json'),
|
|
41
|
+
path.join(cwd, '.babelrc'),
|
|
42
|
+
path.join(cwd, '.babelrc.js'),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
for (const babelPath of babelPaths) {
|
|
46
|
+
if (fs.existsSync(babelPath)) {
|
|
47
|
+
return babelPath
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function checkNativeWindInBabel(cwd) {
|
|
55
|
+
const babelPath = checkBabelConfig(cwd)
|
|
56
|
+
|
|
57
|
+
if (!babelPath) {
|
|
58
|
+
console.log('')
|
|
59
|
+
console.log(pc.yellow(' ⚠ Warning: babel.config.js not found.'))
|
|
60
|
+
console.log(pc.dim(' NativeWind requires Babel configuration.'))
|
|
61
|
+
console.log('')
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const content = fs.readFileSync(babelPath, 'utf-8')
|
|
67
|
+
const hasNativeWind = content.includes('nativewind/babel') || content.includes('"nativewind/babel"')
|
|
68
|
+
|
|
69
|
+
if (!hasNativeWind) {
|
|
70
|
+
console.log('')
|
|
71
|
+
console.log(pc.yellow(' ⚠ Warning: NativeWind not detected in Babel config.'))
|
|
72
|
+
console.log(pc.dim(' Add "nativewind/babel" to your Babel plugins:'))
|
|
73
|
+
console.log('')
|
|
74
|
+
console.log(pc.cyan(' module.exports = {'))
|
|
75
|
+
console.log(pc.cyan(' plugins: ["nativewind/babel"],'))
|
|
76
|
+
console.log(pc.cyan(' }'))
|
|
77
|
+
console.log('')
|
|
78
|
+
console.log(pc.dim(' See: https://www.nativewind.dev/docs/getting-started/installation'))
|
|
79
|
+
console.log('')
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return true
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.log('')
|
|
86
|
+
console.log(pc.yellow(' ⚠ Warning: Could not read babel.config.js'))
|
|
87
|
+
console.log('')
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function checkReactNativeVersion(pkg) {
|
|
93
|
+
const deps = {
|
|
94
|
+
...pkg.dependencies,
|
|
95
|
+
...pkg.devDependencies,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rnVersion = deps['react-native']
|
|
99
|
+
if (rnVersion) {
|
|
100
|
+
const versionMatch = rnVersion.match(/(\d+)\.(\d+)/)
|
|
101
|
+
if (versionMatch) {
|
|
102
|
+
const [, major, minor] = versionMatch
|
|
103
|
+
const majorNum = parseInt(major, 10)
|
|
104
|
+
const minorNum = parseInt(minor, 10)
|
|
105
|
+
|
|
106
|
+
if (majorNum < 0 || (majorNum === 0 && minorNum < 72)) {
|
|
107
|
+
console.log('')
|
|
108
|
+
console.log(pc.yellow(' ⚠ Warning: React Native version is below 0.72'))
|
|
109
|
+
console.log(pc.dim(' NovaUI requires React Native >= 0.72'))
|
|
110
|
+
console.log('')
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function checkNativeWindVersion(pkg) {
|
|
120
|
+
const deps = {
|
|
121
|
+
...pkg.dependencies,
|
|
122
|
+
...pkg.devDependencies,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const nwVersion = deps['nativewind']
|
|
126
|
+
if (!nwVersion) {
|
|
127
|
+
console.log('')
|
|
128
|
+
console.log(pc.yellow(' ⚠ Warning: NativeWind not found in package.json'))
|
|
129
|
+
console.log(pc.dim(' NovaUI requires NativeWind >= 4.0'))
|
|
130
|
+
console.log(pc.dim(' Install: npm install nativewind'))
|
|
131
|
+
console.log(pc.dim(' See: https://www.nativewind.dev/docs/getting-started/installation'))
|
|
132
|
+
console.log('')
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check if version 4 or higher
|
|
137
|
+
const versionMatch = nwVersion.match(/(\d+)/)
|
|
138
|
+
if (versionMatch) {
|
|
139
|
+
const [, major] = versionMatch
|
|
140
|
+
const majorNum = parseInt(major, 10)
|
|
141
|
+
|
|
142
|
+
if (majorNum < 4) {
|
|
143
|
+
console.log('')
|
|
144
|
+
console.log(pc.yellow(' ⚠ Warning: NativeWind version is below 4.0'))
|
|
145
|
+
console.log(pc.dim(' NovaUI requires NativeWind >= 4.0'))
|
|
146
|
+
console.log(pc.dim(' Upgrade: npm install nativewind@latest'))
|
|
147
|
+
console.log('')
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Run all pre-flight checks before init command
|
|
157
|
+
*/
|
|
158
|
+
export function runInitPreflightChecks(cwd) {
|
|
159
|
+
const pkg = checkPackageJson(cwd)
|
|
160
|
+
checkReactNativeProject(pkg)
|
|
161
|
+
checkNativeWindVersion(pkg)
|
|
162
|
+
checkNativeWindInBabel(cwd)
|
|
163
|
+
checkReactNativeVersion(pkg)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Run minimal pre-flight checks before add command
|
|
168
|
+
*/
|
|
169
|
+
export function runAddPreflightChecks(cwd) {
|
|
170
|
+
const pkg = checkPackageJson(cwd)
|
|
171
|
+
return pkg
|
|
172
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function getTailwindConfigContent(config) {
|
|
2
|
+
const normalized = config.componentsUi.replace(/^\.\//, '')
|
|
3
|
+
const contentPaths = ['"./App.{js,jsx,ts,tsx}"', '"./src/**/*.{js,jsx,ts,tsx}"']
|
|
4
|
+
if (!normalized.startsWith('src/')) {
|
|
5
|
+
contentPaths.push(`"./${normalized}/**/*.{js,jsx,ts,tsx}"`)
|
|
6
|
+
}
|
|
7
|
+
return `/** @type {import('tailwindcss').Config} */
|
|
8
|
+
module.exports = {
|
|
9
|
+
content: [
|
|
10
|
+
${contentPaths.join(',\n ')},
|
|
11
|
+
],
|
|
12
|
+
presets: [require("nativewind/preset")],
|
|
13
|
+
theme: {
|
|
14
|
+
extend: {
|
|
15
|
+
colors: {
|
|
16
|
+
border: "hsl(var(--border))",
|
|
17
|
+
input: "hsl(var(--input))",
|
|
18
|
+
ring: "hsl(var(--ring))",
|
|
19
|
+
background: "hsl(var(--background))",
|
|
20
|
+
foreground: "hsl(var(--foreground))",
|
|
21
|
+
primary: {
|
|
22
|
+
DEFAULT: "hsl(var(--primary))",
|
|
23
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
24
|
+
},
|
|
25
|
+
secondary: {
|
|
26
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
27
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
28
|
+
},
|
|
29
|
+
destructive: {
|
|
30
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
31
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
32
|
+
},
|
|
33
|
+
muted: {
|
|
34
|
+
DEFAULT: "hsl(var(--muted))",
|
|
35
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
36
|
+
},
|
|
37
|
+
accent: {
|
|
38
|
+
DEFAULT: "hsl(var(--accent))",
|
|
39
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
40
|
+
},
|
|
41
|
+
popover: {
|
|
42
|
+
DEFAULT: "hsl(var(--popover))",
|
|
43
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
44
|
+
},
|
|
45
|
+
card: {
|
|
46
|
+
DEFAULT: "hsl(var(--card))",
|
|
47
|
+
foreground: "hsl(var(--card-foreground))",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
borderRadius: {
|
|
51
|
+
lg: "var(--radius)",
|
|
52
|
+
md: "calc(var(--radius) - 2px)",
|
|
53
|
+
sm: "calc(var(--radius) - 4px)",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
plugins: [],
|
|
58
|
+
};
|
|
59
|
+
`
|
|
60
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function assertValidComponentConfig(componentName, componentConfig) {
|
|
2
|
+
if (!componentConfig || typeof componentConfig !== 'object') {
|
|
3
|
+
throw new Error(`Registry entry for "${componentName}" is invalid.`)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { files, dependencies } = componentConfig
|
|
7
|
+
if (!Array.isArray(files) || files.some(file => typeof file !== 'string' || file.trim() === '')) {
|
|
8
|
+
throw new Error(`Registry entry for "${componentName}" must include a valid "files" array.`)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (
|
|
12
|
+
dependencies !== undefined &&
|
|
13
|
+
(!Array.isArray(dependencies) || dependencies.some(dep => typeof dep !== 'string' || dep.trim() === ''))
|
|
14
|
+
) {
|
|
15
|
+
throw new Error(`Registry entry for "${componentName}" has an invalid "dependencies" array.`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import pc from 'picocolors'
|
|
2
|
+
import { getCliVersion } from './version.js'
|
|
3
|
+
import { fetchWithTimeout } from './fetch.js'
|
|
4
|
+
|
|
5
|
+
const NPM_REGISTRY_URL = 'https://registry.npmjs.org/novaui-cli/latest'
|
|
6
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
7
|
+
const CACHE_FILE = '.novaui-version-check'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compare two semantic versions
|
|
11
|
+
* Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
|
12
|
+
*/
|
|
13
|
+
function compareVersions(v1, v2) {
|
|
14
|
+
const parts1 = v1.split('.').map(Number)
|
|
15
|
+
const parts2 = v2.split('.').map(Number)
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
18
|
+
const part1 = parts1[i] || 0
|
|
19
|
+
const part2 = parts2[i] || 0
|
|
20
|
+
|
|
21
|
+
if (part1 > part2) return 1
|
|
22
|
+
if (part1 < part2) return -1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fetch the latest version from npm registry
|
|
30
|
+
*/
|
|
31
|
+
async function getLatestVersion() {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetchWithTimeout(NPM_REGISTRY_URL)
|
|
34
|
+
if (!response.ok) return null
|
|
35
|
+
|
|
36
|
+
const data = await response.json()
|
|
37
|
+
return data.version || null
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if we should run the version check based on cache
|
|
45
|
+
*/
|
|
46
|
+
function shouldCheckVersion(cwd) {
|
|
47
|
+
const fs = require('node:fs')
|
|
48
|
+
const path = require('node:path')
|
|
49
|
+
const os = require('node:os')
|
|
50
|
+
|
|
51
|
+
const cacheDir = path.join(os.homedir(), '.novaui')
|
|
52
|
+
const cachePath = path.join(cacheDir, CACHE_FILE)
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(cachePath)) {
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'))
|
|
60
|
+
const lastCheck = new Date(cache.lastCheck).getTime()
|
|
61
|
+
const now = Date.now()
|
|
62
|
+
|
|
63
|
+
return (now - lastCheck) > CHECK_INTERVAL_MS
|
|
64
|
+
} catch {
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Update the version check cache
|
|
71
|
+
*/
|
|
72
|
+
function updateCache(latestVersion) {
|
|
73
|
+
const fs = require('node:fs')
|
|
74
|
+
const path = require('node:path')
|
|
75
|
+
const os = require('node:os')
|
|
76
|
+
|
|
77
|
+
const cacheDir = path.join(os.homedir(), '.novaui')
|
|
78
|
+
if (!fs.existsSync(cacheDir)) {
|
|
79
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const cachePath = path.join(cacheDir, CACHE_FILE)
|
|
83
|
+
const cache = {
|
|
84
|
+
lastCheck: new Date().toISOString(),
|
|
85
|
+
latestVersion,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check for CLI updates and warn user if outdated
|
|
93
|
+
* Runs once per day to avoid slowing down commands
|
|
94
|
+
*/
|
|
95
|
+
export async function checkForUpdates() {
|
|
96
|
+
const currentVersion = getCliVersion()
|
|
97
|
+
|
|
98
|
+
if (currentVersion === 'unknown') {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Only check once per day
|
|
103
|
+
const cwd = process.cwd()
|
|
104
|
+
if (!shouldCheckVersion(cwd)) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const latestVersion = await getLatestVersion()
|
|
109
|
+
|
|
110
|
+
if (!latestVersion) {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
updateCache(latestVersion)
|
|
115
|
+
|
|
116
|
+
if (compareVersions(latestVersion, currentVersion) > 0) {
|
|
117
|
+
console.log('')
|
|
118
|
+
console.log(pc.yellow(` ⚠ A new version of novaui-cli is available!`))
|
|
119
|
+
console.log(pc.dim(` Current: ${currentVersion}`))
|
|
120
|
+
console.log(pc.dim(` Latest: ${latestVersion}`))
|
|
121
|
+
console.log('')
|
|
122
|
+
console.log(pc.cyan(` Update with: npm install -g novaui-cli@latest`))
|
|
123
|
+
console.log('')
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get version info for error reporting
|
|
129
|
+
*/
|
|
130
|
+
export function getVersionInfo() {
|
|
131
|
+
const currentVersion = getCliVersion()
|
|
132
|
+
return {
|
|
133
|
+
cli: currentVersion,
|
|
134
|
+
node: process.version,
|
|
135
|
+
}
|
|
136
|
+
}
|