novaui-cli 1.1.2 → 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 +33 -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 +51 -46
- package/src/commands/add.js +33 -36
- package/src/commands/init.js +232 -137
- package/src/constants.js +86 -5
- package/src/themes/index.js +45 -14
- package/src/utils/config.js +11 -11
- package/src/utils/deps.js +24 -20
- package/src/utils/fetch.js +9 -9
- package/src/utils/fs-helpers.js +8 -8
- package/src/utils/fuzzy.js +99 -0
- package/src/utils/preflight.js +172 -0
- package/src/utils/version-check.js +136 -0
- package/src/components.json +0 -6
- package/src/global.css +0 -50
- package/src/tailwind.config.js +0 -54
package/src/constants.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
// Allow overriding branch via environment variable (useful for testing)
|
|
2
|
+
const BRANCH = process.env.NOVAUI_BRANCH || 'main';
|
|
2
3
|
|
|
3
|
-
export const
|
|
4
|
+
export const BASE_URL = `https://raw.githubusercontent.com/KaloyanBehov/novaui/${BRANCH}`;
|
|
4
5
|
|
|
5
|
-
export const
|
|
6
|
+
export const REGISTRY_URL = `${BASE_URL}/packages/registry/registry.json`;
|
|
7
|
+
|
|
8
|
+
export const THEMES_BASE_URL = `${BASE_URL}/packages/themes`;
|
|
9
|
+
|
|
10
|
+
export const CONFIG_FILENAME = 'components.json';
|
|
11
|
+
|
|
12
|
+
export const FETCH_TIMEOUT_MS = 15000;
|
|
6
13
|
|
|
7
14
|
export const DEFAULT_CONFIG = {
|
|
8
15
|
globalCss: 'global.css',
|
|
9
16
|
componentsUi: 'components/ui',
|
|
10
17
|
lib: 'lib',
|
|
11
18
|
theme: 'default',
|
|
12
|
-
}
|
|
19
|
+
};
|
|
13
20
|
|
|
14
21
|
export const UTILS_CONTENT = `import { type ClassValue, clsx } from "clsx"
|
|
15
22
|
import { twMerge } from "tailwind-merge"
|
|
@@ -17,4 +24,78 @@ import { twMerge } from "tailwind-merge"
|
|
|
17
24
|
export function cn(...inputs: ClassValue[]) {
|
|
18
25
|
return twMerge(clsx(inputs))
|
|
19
26
|
}
|
|
20
|
-
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
// Default theme bundled as fallback (always available, no network required)
|
|
30
|
+
export const DEFAULT_THEME_CSS = `@tailwind base;
|
|
31
|
+
@tailwind components;
|
|
32
|
+
@tailwind utilities;
|
|
33
|
+
|
|
34
|
+
@layer base {
|
|
35
|
+
:root {
|
|
36
|
+
--background: 0 0% 100%;
|
|
37
|
+
--foreground: 240 10% 3.9%;
|
|
38
|
+
--card: 0 0% 100%;
|
|
39
|
+
--card-foreground: 240 10% 3.9%;
|
|
40
|
+
--popover: 0 0% 100%;
|
|
41
|
+
--popover-foreground: 240 10% 3.9%;
|
|
42
|
+
--primary: 240 5.9% 10%;
|
|
43
|
+
--primary-foreground: 0 0% 98%;
|
|
44
|
+
--secondary: 240 4.8% 95.9%;
|
|
45
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
46
|
+
--muted: 240 4.8% 95.9%;
|
|
47
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
48
|
+
--accent: 240 4.8% 95.9%;
|
|
49
|
+
--accent-foreground: 240 5.9% 10%;
|
|
50
|
+
--destructive: 0 84.2% 60.2%;
|
|
51
|
+
--destructive-foreground: 0 0% 98%;
|
|
52
|
+
--border: 240 5.9% 90%;
|
|
53
|
+
--input: 240 5.9% 90%;
|
|
54
|
+
--ring: 240 5.9% 10%;
|
|
55
|
+
--radius: 0.5rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.dark {
|
|
59
|
+
--background: 240 10% 3.9%;
|
|
60
|
+
--foreground: 0 0% 98%;
|
|
61
|
+
--card: 240 10% 3.9%;
|
|
62
|
+
--card-foreground: 0 0% 98%;
|
|
63
|
+
--popover: 240 10% 3.9%;
|
|
64
|
+
--popover-foreground: 0 0% 98%;
|
|
65
|
+
--primary: 0 0% 98%;
|
|
66
|
+
--primary-foreground: 240 5.9% 10%;
|
|
67
|
+
--secondary: 240 3.7% 15.9%;
|
|
68
|
+
--secondary-foreground: 0 0% 98%;
|
|
69
|
+
--muted: 240 3.7% 15.9%;
|
|
70
|
+
--muted-foreground: 240 5% 64.9%;
|
|
71
|
+
--accent: 240 3.7% 15.9%;
|
|
72
|
+
--accent-foreground: 0 0% 98%;
|
|
73
|
+
--destructive: 0 62.8% 30.6%;
|
|
74
|
+
--destructive-foreground: 0 0% 98%;
|
|
75
|
+
--border: 240 3.7% 15.9%;
|
|
76
|
+
--input: 240 3.7% 15.9%;
|
|
77
|
+
--ring: 240 4.9% 83.9%;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
export const BABEL_CONFIG_CONTENT = `module.exports = function (api) {
|
|
83
|
+
api.cache(true);
|
|
84
|
+
return {
|
|
85
|
+
presets: [
|
|
86
|
+
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
|
87
|
+
"nativewind/babel",
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
export const METRO_CONFIG_CONTENT = (
|
|
94
|
+
globalCssPath
|
|
95
|
+
) => `const { getDefaultConfig } = require("expo/metro-config");
|
|
96
|
+
const { withNativeWind } = require('nativewind/metro');
|
|
97
|
+
|
|
98
|
+
const config = getDefaultConfig(__dirname)
|
|
99
|
+
|
|
100
|
+
module.exports = withNativeWind(config, { input: './${globalCssPath}' })
|
|
101
|
+
`;
|
package/src/themes/index.js
CHANGED
|
@@ -1,17 +1,48 @@
|
|
|
1
|
-
import { DEFAULT_THEME_CSS } from '
|
|
2
|
-
import {
|
|
3
|
-
import { SUNSET_THEME_CSS } from './sunset.js'
|
|
1
|
+
import { THEMES_BASE_URL, DEFAULT_THEME_CSS } from '../constants.js'
|
|
2
|
+
import { fetchWithTimeout } from '../utils/fetch.js'
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
ocean: OCEAN_THEME_CSS,
|
|
8
|
-
sunset: SUNSET_THEME_CSS,
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const THEME_KEYS = Object.keys(THEMES)
|
|
4
|
+
// Available theme names (fetched from GitHub)
|
|
5
|
+
export const THEME_KEYS = ['default', 'ocean', 'sunset']
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Fetch theme CSS content from GitHub.
|
|
9
|
+
* Falls back to bundled default theme if fetch fails or theme is 'default'.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} themeName - The theme name (default, ocean, sunset)
|
|
12
|
+
* @returns {Promise<string>} The theme CSS content
|
|
13
|
+
*/
|
|
14
|
+
export async function getThemeCssContent(themeName) {
|
|
15
|
+
const normalized = (typeof themeName === 'string' ? themeName : 'default').trim().toLowerCase()
|
|
16
|
+
const theme = THEME_KEYS.includes(normalized) ? normalized : 'default'
|
|
17
|
+
|
|
18
|
+
// Use bundled default theme (no network call needed)
|
|
19
|
+
if (theme === 'default') {
|
|
20
|
+
return DEFAULT_THEME_CSS
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fetch other themes from GitHub
|
|
24
|
+
const themeUrl = `${THEMES_BASE_URL}/${theme}.js`
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetchWithTimeout(themeUrl)
|
|
28
|
+
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Failed to fetch theme: ${response.status}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const content = await response.text()
|
|
34
|
+
|
|
35
|
+
// Extract the CSS content from the export statement
|
|
36
|
+
// Example: export const OCEAN_THEME_CSS = `...`
|
|
37
|
+
const match = content.match(/export const \w+_THEME_CSS = `([^`]*)`/s)
|
|
38
|
+
if (match && match[1]) {
|
|
39
|
+
return match[1]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw new Error('Invalid theme format')
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Fallback to default theme on error
|
|
45
|
+
console.warn(`Warning: Could not fetch "${theme}" theme, using default. ${error.message}`)
|
|
46
|
+
return DEFAULT_THEME_CSS
|
|
47
|
+
}
|
|
17
48
|
}
|
package/src/utils/config.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { CONFIG_FILENAME, DEFAULT_CONFIG } from '../constants.js'
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CONFIG_FILENAME, DEFAULT_CONFIG } from '../constants.js';
|
|
4
4
|
|
|
5
5
|
/** Load components.json from cwd; returns null if missing or invalid. */
|
|
6
6
|
export function loadConfig(cwd) {
|
|
7
|
-
const configPath = path.join(cwd, CONFIG_FILENAME)
|
|
8
|
-
if (!fs.existsSync(configPath)) return null
|
|
7
|
+
const configPath = path.join(cwd, CONFIG_FILENAME);
|
|
8
|
+
if (!fs.existsSync(configPath)) return null;
|
|
9
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 }
|
|
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
13
|
} catch {
|
|
14
|
-
return null
|
|
14
|
+
return null;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Write components.json to cwd. */
|
|
19
19
|
export function writeConfig(cwd, config) {
|
|
20
|
-
const configPath = path.join(cwd, CONFIG_FILENAME)
|
|
21
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8')
|
|
20
|
+
const configPath = path.join(cwd, CONFIG_FILENAME);
|
|
21
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
22
22
|
}
|
package/src/utils/deps.js
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
4
|
|
|
5
5
|
export function detectPackageManager() {
|
|
6
|
-
const userAgent = process.env.npm_config_user_agent || ''
|
|
7
|
-
if (userAgent.startsWith('yarn')) return { command: 'yarn', baseArgs: ['add'] }
|
|
8
|
-
if (userAgent.startsWith('pnpm')) return { command: 'pnpm', baseArgs: ['add'] }
|
|
9
|
-
if (userAgent.startsWith('bun')) return { command: 'bun', baseArgs: ['add'] }
|
|
10
|
-
return { command: 'npm', baseArgs: ['install'] }
|
|
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
11
|
}
|
|
12
12
|
|
|
13
|
-
export function installPackages(packages) {
|
|
14
|
-
if (!Array.isArray(packages) || packages.length === 0) return
|
|
15
|
-
const {
|
|
16
|
-
|
|
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' });
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export function getInstallHint(packages) {
|
|
20
|
-
const {
|
|
21
|
-
|
|
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(' ')}`;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
/** Returns which of the requested deps are not listed in package.json (dependencies or devDependencies). */
|
|
25
29
|
export function getMissingDeps(cwd, deps) {
|
|
26
|
-
const pkgPath = path.join(cwd, 'package.json')
|
|
30
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
27
31
|
if (!fs.existsSync(pkgPath)) {
|
|
28
|
-
return [...deps]
|
|
32
|
+
return [...deps];
|
|
29
33
|
}
|
|
30
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
31
35
|
const installed = new Set([
|
|
32
36
|
...Object.keys(pkg.dependencies || {}),
|
|
33
37
|
...Object.keys(pkg.devDependencies || {}),
|
|
34
|
-
])
|
|
35
|
-
return deps.filter(d => !installed.has(d))
|
|
38
|
+
]);
|
|
39
|
+
return deps.filter((d) => !installed.has(d));
|
|
36
40
|
}
|
package/src/utils/fetch.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { FETCH_TIMEOUT_MS } from '../constants.js'
|
|
1
|
+
import { FETCH_TIMEOUT_MS } from '../constants.js';
|
|
2
2
|
|
|
3
3
|
export async function fetchWithTimeout(url) {
|
|
4
|
-
const controller = new AbortController()
|
|
5
|
-
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
4
|
+
const controller = new AbortController();
|
|
5
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
6
6
|
try {
|
|
7
|
-
return await fetch(url, { signal: controller.signal })
|
|
7
|
+
return await fetch(url, { signal: controller.signal });
|
|
8
8
|
} catch (error) {
|
|
9
9
|
if (error && error.name === 'AbortError') {
|
|
10
|
-
throw new Error(`Request timed out after ${FETCH_TIMEOUT_MS}ms: ${url}`)
|
|
10
|
+
throw new Error(`Request timed out after ${FETCH_TIMEOUT_MS}ms: ${url}`);
|
|
11
11
|
}
|
|
12
|
-
throw error
|
|
12
|
+
throw error;
|
|
13
13
|
} finally {
|
|
14
|
-
clearTimeout(timeout)
|
|
14
|
+
clearTimeout(timeout);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function formatError(error) {
|
|
19
|
-
if (error instanceof Error && error.message) return error.message
|
|
20
|
-
return String(error)
|
|
19
|
+
if (error instanceof Error && error.message) return error.message;
|
|
20
|
+
return String(error);
|
|
21
21
|
}
|
package/src/utils/fs-helpers.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import pc from 'picocolors'
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
3
|
|
|
4
4
|
export function ensureDir(dir) {
|
|
5
5
|
if (!fs.existsSync(dir)) {
|
|
6
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
6
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function writeIfNotExists(filePath, content, label) {
|
|
11
11
|
if (fs.existsSync(filePath)) {
|
|
12
|
-
console.log(pc.dim(` ℹ ${label} already exists, skipping.`))
|
|
13
|
-
return false
|
|
12
|
+
console.log(pc.dim(` ℹ ${label} already exists, skipping.`));
|
|
13
|
+
return false;
|
|
14
14
|
}
|
|
15
|
-
fs.writeFileSync(filePath, content, 'utf8')
|
|
16
|
-
console.log(pc.green(` ✓ Created ${label}`))
|
|
17
|
-
return true
|
|
15
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
16
|
+
console.log(pc.green(` ✓ Created ${label}`));
|
|
17
|
+
return true;
|
|
18
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
|
+
}
|