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,168 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import { multiselect, isCancel, cancel } from '@clack/prompts'
|
|
5
|
+
import pc from 'picocolors'
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_CONFIG, UTILS_CONTENT, BASE_URL, REGISTRY_URL } from '../constants.js'
|
|
8
|
+
import { loadConfig } from '../utils/config.js'
|
|
9
|
+
import { ensureDir } from '../utils/fs-helpers.js'
|
|
10
|
+
import { getMissingDeps, installPackages, getInstallHint } from '../utils/deps.js'
|
|
11
|
+
import { fetchWithTimeout } from '../utils/fetch.js'
|
|
12
|
+
import { assertValidComponentConfig } from '../utils/validate.js'
|
|
13
|
+
import { runAddPreflightChecks } from '../utils/preflight.js'
|
|
14
|
+
import { formatComponentNotFoundError } from '../utils/fuzzy.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch the registry from GitHub.
|
|
18
|
+
* Always gets the latest registry from the main branch.
|
|
19
|
+
*/
|
|
20
|
+
async function fetchRegistry() {
|
|
21
|
+
const spinner = ora({ text: 'Loading component registry...', stream: process.stderr }).start()
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetchWithTimeout(REGISTRY_URL)
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`Failed to fetch registry: ${response.status} ${response.statusText}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const registry = await response.json()
|
|
31
|
+
spinner.stop()
|
|
32
|
+
return registry
|
|
33
|
+
} catch (error) {
|
|
34
|
+
spinner.fail('Failed to load registry')
|
|
35
|
+
throw new Error(`Could not fetch registry from GitHub: ${error.message}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetch the component registry and present an interactive multi-select for
|
|
41
|
+
* the user to choose one or more components. Returns the selected names.
|
|
42
|
+
* Only called in TTY mode.
|
|
43
|
+
*/
|
|
44
|
+
export async function pickComponentsInteractively() {
|
|
45
|
+
const registry = await fetchRegistry()
|
|
46
|
+
|
|
47
|
+
if (!registry || typeof registry !== 'object' || Array.isArray(registry)) {
|
|
48
|
+
throw new Error('Registry is not a valid object.')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const selected = await multiselect({
|
|
52
|
+
message: 'Select components to add',
|
|
53
|
+
options: Object.keys(registry).map(name => ({ value: name, label: name })),
|
|
54
|
+
required: true,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (isCancel(selected)) {
|
|
58
|
+
cancel('Cancelled.')
|
|
59
|
+
process.exit(0)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return selected
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add a single component by name.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} componentName
|
|
69
|
+
* @param {{ force?: boolean }} [options]
|
|
70
|
+
*/
|
|
71
|
+
export async function add(componentName, options = {}) {
|
|
72
|
+
const { force = false } = options
|
|
73
|
+
|
|
74
|
+
if (!componentName) {
|
|
75
|
+
throw new Error('Missing component name. Usage: novaui@latest add <component-name>')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const cwd = process.cwd()
|
|
79
|
+
runAddPreflightChecks(cwd)
|
|
80
|
+
|
|
81
|
+
console.log('')
|
|
82
|
+
console.log(` ◆ NovaUI – Adding "${componentName}"...`)
|
|
83
|
+
console.log('')
|
|
84
|
+
|
|
85
|
+
// ─── 1. Fetch registry ────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const registry = await fetchRegistry()
|
|
88
|
+
|
|
89
|
+
if (!registry || typeof registry !== 'object' || Array.isArray(registry)) {
|
|
90
|
+
throw new Error('Registry is not a valid object.')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!registry[componentName]) {
|
|
94
|
+
throw new Error(formatComponentNotFoundError(componentName, Object.keys(registry)))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const componentConfig = registry[componentName]
|
|
98
|
+
assertValidComponentConfig(componentName, componentConfig)
|
|
99
|
+
|
|
100
|
+
const projectConfig = loadConfig(cwd) || DEFAULT_CONFIG
|
|
101
|
+
const targetBaseDir = path.join(cwd, projectConfig.componentsUi)
|
|
102
|
+
ensureDir(targetBaseDir)
|
|
103
|
+
|
|
104
|
+
// ─── 2. Ensure utils.ts exists ─────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const utilsDir = path.join(cwd, projectConfig.lib)
|
|
107
|
+
const utilsPath = path.join(utilsDir, 'utils.ts')
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(utilsPath)) {
|
|
110
|
+
ensureDir(utilsDir)
|
|
111
|
+
fs.writeFileSync(utilsPath, UTILS_CONTENT)
|
|
112
|
+
console.log(` ✓ Created ${projectConfig.lib}/utils.ts`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!loadConfig(cwd)) {
|
|
116
|
+
console.log('')
|
|
117
|
+
console.log(" ℹ No components.json found. Using default paths. Run 'npx novaui-cli@latest init' to customize.")
|
|
118
|
+
console.log('')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── 3. Fetch and write component files ────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
for (const file of componentConfig.files) {
|
|
124
|
+
const fileUrl = `${BASE_URL}/${file}`
|
|
125
|
+
const fileName = path.basename(file)
|
|
126
|
+
const destPath = path.join(targetBaseDir, fileName)
|
|
127
|
+
|
|
128
|
+
if (!force && fs.existsSync(destPath)) {
|
|
129
|
+
console.log(pc.dim(` ℹ ${fileName} already exists, skipping. (use --force to overwrite)`))
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const fileSpinner = ora({ text: ` Downloading ${fileName}...`, stream: process.stderr }).start()
|
|
134
|
+
const fileResponse = await fetchWithTimeout(fileUrl)
|
|
135
|
+
|
|
136
|
+
if (!fileResponse.ok) {
|
|
137
|
+
fileSpinner.fail(pc.red(` Failed to download ${fileName}`))
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const content = await fileResponse.text()
|
|
142
|
+
fs.writeFileSync(destPath, content, 'utf8')
|
|
143
|
+
fileSpinner.succeed(pc.green(` ✓ Added ${fileName}`))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── 4. Install component dependencies ────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
if (componentConfig.dependencies && componentConfig.dependencies.length > 0) {
|
|
149
|
+
const missingDeps = getMissingDeps(cwd, componentConfig.dependencies)
|
|
150
|
+
if (missingDeps.length === 0) {
|
|
151
|
+
console.log('')
|
|
152
|
+
console.log(' ✓ Component dependencies already in package.json, skipping install.')
|
|
153
|
+
} else {
|
|
154
|
+
console.log('')
|
|
155
|
+
console.log(` Installing dependencies: ${missingDeps.join(', ')}...`)
|
|
156
|
+
try {
|
|
157
|
+
installPackages(missingDeps)
|
|
158
|
+
} catch {
|
|
159
|
+
console.error(` ✗ Failed to install dependencies automatically.`)
|
|
160
|
+
console.error(pc.dim(` Run manually: ${getInstallHint(missingDeps)}`))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log('')
|
|
166
|
+
console.log(pc.green(` ✓ Successfully added "${componentName}"!`))
|
|
167
|
+
console.log('')
|
|
168
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
text,
|
|
4
|
+
select,
|
|
5
|
+
confirm,
|
|
6
|
+
isCancel,
|
|
7
|
+
cancel,
|
|
8
|
+
intro,
|
|
9
|
+
outro,
|
|
10
|
+
} from "@clack/prompts";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
|
|
13
|
+
import { THEME_KEYS, getThemeCssContent } from "../themes/index.js";
|
|
14
|
+
import { DEFAULT_CONFIG, UTILS_CONTENT, BABEL_CONFIG_CONTENT, METRO_CONFIG_CONTENT } from "../constants.js";
|
|
15
|
+
import { loadConfig, writeConfig } from "../utils/config.js";
|
|
16
|
+
import { ensureDir, writeIfNotExists } from "../utils/fs-helpers.js";
|
|
17
|
+
import {
|
|
18
|
+
getMissingDeps,
|
|
19
|
+
installPackages,
|
|
20
|
+
getInstallHint,
|
|
21
|
+
} from "../utils/deps.js";
|
|
22
|
+
import { getTailwindConfigContent } from "../utils/tailwind.js";
|
|
23
|
+
import { runInitPreflightChecks } from "../utils/preflight.js";
|
|
24
|
+
|
|
25
|
+
// ─── ASCII Banner ────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export const ASCII_BANNER = `
|
|
28
|
+
${pc.cyan(" ███╗ ██╗ ██████╗ ██╗ ██╗ █████╗ ██╗ ██╗██╗")}
|
|
29
|
+
${pc.cyan(" ████╗ ██║██╔═══██╗██║ ██║██╔══██╗ ██║ ██║██║")}
|
|
30
|
+
${pc.cyan(" ██╔██╗ ██║██║ ██║██║ ██║███████║ ██║ ██║██║")}
|
|
31
|
+
${pc.cyan(" ██║╚██╗██║██║ ██║╚██╗ ██╔╝██╔══██║ ██║ ██║██║")}
|
|
32
|
+
${pc.cyan(" ██║ ╚████║╚██████╔╝ ╚████╔╝ ██║ ██║ ╚██████╔╝██║")}
|
|
33
|
+
${pc.cyan(" ╚═╝ ╚═══╝ ╚═════╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝")}
|
|
34
|
+
${pc.dim(" React Native + NativeWind UI")}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function normalizeTheme(themeName) {
|
|
40
|
+
if (typeof themeName !== "string" || themeName.trim() === "")
|
|
41
|
+
return DEFAULT_CONFIG.theme;
|
|
42
|
+
const normalized = themeName.trim().toLowerCase();
|
|
43
|
+
if (!THEME_KEYS.includes(normalized)) return DEFAULT_CONFIG.theme;
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Prompt for a theme selection; returns default immediately in non-TTY mode. */
|
|
48
|
+
export async function askTheme(defaultTheme = DEFAULT_CONFIG.theme) {
|
|
49
|
+
const normalizedDefault = normalizeTheme(defaultTheme);
|
|
50
|
+
if (process.stdin.isTTY !== true) return normalizedDefault;
|
|
51
|
+
|
|
52
|
+
const theme = await select({
|
|
53
|
+
message: "Select a theme",
|
|
54
|
+
options: THEME_KEYS.map((k) => ({ value: k, label: k })),
|
|
55
|
+
initialValue: normalizedDefault,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (isCancel(theme)) {
|
|
59
|
+
cancel("Setup cancelled.");
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return theme;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── init command ─────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {object} [options]
|
|
70
|
+
* @param {boolean} [options.yes] Skip all prompts and use defaults (--yes flag)
|
|
71
|
+
*/
|
|
72
|
+
export async function init(options = {}) {
|
|
73
|
+
const { yes = false } = options;
|
|
74
|
+
const cwd = process.cwd();
|
|
75
|
+
|
|
76
|
+
runInitPreflightChecks(cwd);
|
|
77
|
+
|
|
78
|
+
const existingConfig = loadConfig(cwd);
|
|
79
|
+
const isInteractive = process.stdin.isTTY === true && !yes;
|
|
80
|
+
|
|
81
|
+
console.log(ASCII_BANNER);
|
|
82
|
+
|
|
83
|
+
if (isInteractive) {
|
|
84
|
+
intro(pc.bold("Welcome to NovaUI"));
|
|
85
|
+
console.log(pc.dim(" Let's set up your project in a few steps."));
|
|
86
|
+
console.log("");
|
|
87
|
+
} else {
|
|
88
|
+
console.log(pc.bold(" Welcome to NovaUI"));
|
|
89
|
+
console.log(pc.dim(" Let's set up your project in a few steps."));
|
|
90
|
+
console.log("");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let config;
|
|
94
|
+
|
|
95
|
+
if (existingConfig) {
|
|
96
|
+
console.log(pc.blue(" ⚙ Config"));
|
|
97
|
+
console.log(pc.dim(" components.json found in this directory."));
|
|
98
|
+
console.log("");
|
|
99
|
+
|
|
100
|
+
let reconfigure = false;
|
|
101
|
+
|
|
102
|
+
if (isInteractive) {
|
|
103
|
+
const answer = await confirm({ message: "Re-configure paths?" });
|
|
104
|
+
if (isCancel(answer)) {
|
|
105
|
+
cancel("Setup cancelled.");
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
reconfigure = answer === true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (reconfigure) {
|
|
112
|
+
const globalCss = await text({
|
|
113
|
+
message: "Where should global.css be placed?",
|
|
114
|
+
placeholder: DEFAULT_CONFIG.globalCss,
|
|
115
|
+
defaultValue: existingConfig.globalCss || DEFAULT_CONFIG.globalCss,
|
|
116
|
+
});
|
|
117
|
+
if (isCancel(globalCss)) {
|
|
118
|
+
cancel("Setup cancelled.");
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const componentsUi = await text({
|
|
123
|
+
message: "Where should UI components be placed?",
|
|
124
|
+
placeholder: DEFAULT_CONFIG.componentsUi,
|
|
125
|
+
defaultValue:
|
|
126
|
+
existingConfig.componentsUi || DEFAULT_CONFIG.componentsUi,
|
|
127
|
+
});
|
|
128
|
+
if (isCancel(componentsUi)) {
|
|
129
|
+
cancel("Setup cancelled.");
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const lib = await text({
|
|
134
|
+
message: "Where should lib (e.g. utils) be placed?",
|
|
135
|
+
placeholder: DEFAULT_CONFIG.lib,
|
|
136
|
+
defaultValue: existingConfig.lib || DEFAULT_CONFIG.lib,
|
|
137
|
+
});
|
|
138
|
+
if (isCancel(lib)) {
|
|
139
|
+
cancel("Setup cancelled.");
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const theme = await askTheme(existingConfig.theme);
|
|
144
|
+
|
|
145
|
+
config = {
|
|
146
|
+
globalCss: globalCss.replace(/\\/g, "/"),
|
|
147
|
+
componentsUi: componentsUi.replace(/\\/g, "/"),
|
|
148
|
+
lib: lib.replace(/\\/g, "/"),
|
|
149
|
+
theme,
|
|
150
|
+
};
|
|
151
|
+
writeConfig(cwd, config);
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(pc.green(" ✓ Updated components.json"));
|
|
154
|
+
} else {
|
|
155
|
+
config = existingConfig;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
console.log(pc.blue(" ⚙ Configure paths"));
|
|
159
|
+
console.log(
|
|
160
|
+
pc.dim(" Where should NovaUI put its files? Press Enter for defaults."),
|
|
161
|
+
);
|
|
162
|
+
console.log("");
|
|
163
|
+
|
|
164
|
+
if (isInteractive) {
|
|
165
|
+
const globalCss = await text({
|
|
166
|
+
message: "Path for global.css?",
|
|
167
|
+
placeholder: DEFAULT_CONFIG.globalCss,
|
|
168
|
+
defaultValue: DEFAULT_CONFIG.globalCss,
|
|
169
|
+
});
|
|
170
|
+
if (isCancel(globalCss)) {
|
|
171
|
+
cancel("Setup cancelled.");
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const componentsUi = await text({
|
|
176
|
+
message: "Path for UI components?",
|
|
177
|
+
placeholder: DEFAULT_CONFIG.componentsUi,
|
|
178
|
+
defaultValue: DEFAULT_CONFIG.componentsUi,
|
|
179
|
+
});
|
|
180
|
+
if (isCancel(componentsUi)) {
|
|
181
|
+
cancel("Setup cancelled.");
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const lib = await text({
|
|
186
|
+
message: "Path for lib (utils)?",
|
|
187
|
+
placeholder: DEFAULT_CONFIG.lib,
|
|
188
|
+
defaultValue: DEFAULT_CONFIG.lib,
|
|
189
|
+
});
|
|
190
|
+
if (isCancel(lib)) {
|
|
191
|
+
cancel("Setup cancelled.");
|
|
192
|
+
process.exit(0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const theme = await askTheme();
|
|
196
|
+
|
|
197
|
+
config = {
|
|
198
|
+
globalCss: globalCss.replace(/\\/g, "/"),
|
|
199
|
+
componentsUi: componentsUi.replace(/\\/g, "/"),
|
|
200
|
+
lib: lib.replace(/\\/g, "/"),
|
|
201
|
+
theme,
|
|
202
|
+
};
|
|
203
|
+
} else {
|
|
204
|
+
config = { ...DEFAULT_CONFIG };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
writeConfig(cwd, config);
|
|
208
|
+
console.log(pc.green(" ✓ Created components.json"));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Setup files ────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
console.log("");
|
|
214
|
+
console.log(pc.blue(" 📁 Setting up project"));
|
|
215
|
+
console.log("");
|
|
216
|
+
|
|
217
|
+
const utilsDir = path.join(cwd, config.lib);
|
|
218
|
+
ensureDir(utilsDir);
|
|
219
|
+
const utilsPath = path.join(utilsDir, "utils.ts");
|
|
220
|
+
writeIfNotExists(utilsPath, UTILS_CONTENT, `${config.lib}/utils.ts`);
|
|
221
|
+
|
|
222
|
+
const globalCssDir = path.dirname(path.join(cwd, config.globalCss));
|
|
223
|
+
ensureDir(globalCssDir);
|
|
224
|
+
const themeCssContent = await getThemeCssContent(normalizeTheme(config.theme));
|
|
225
|
+
writeIfNotExists(
|
|
226
|
+
path.join(cwd, config.globalCss),
|
|
227
|
+
themeCssContent,
|
|
228
|
+
config.globalCss,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const tailwindContent = getTailwindConfigContent(config);
|
|
232
|
+
writeIfNotExists(
|
|
233
|
+
path.join(cwd, "tailwind.config.js"),
|
|
234
|
+
tailwindContent,
|
|
235
|
+
"tailwind.config.js",
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
writeIfNotExists(
|
|
239
|
+
path.join(cwd, "babel.config.js"),
|
|
240
|
+
BABEL_CONFIG_CONTENT,
|
|
241
|
+
"babel.config.js",
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const metroContent = METRO_CONFIG_CONTENT(config.globalCss);
|
|
245
|
+
writeIfNotExists(
|
|
246
|
+
path.join(cwd, "metro.config.js"),
|
|
247
|
+
metroContent,
|
|
248
|
+
"metro.config.js",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// ─── Dependencies ──────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
const deps = [
|
|
254
|
+
"nativewind",
|
|
255
|
+
"react-native-reanimated",
|
|
256
|
+
"react-native-safe-area-context",
|
|
257
|
+
"clsx",
|
|
258
|
+
"tailwind-merge",
|
|
259
|
+
"class-variance-authority",
|
|
260
|
+
];
|
|
261
|
+
const devDeps = [
|
|
262
|
+
"tailwindcss@^3.4.17",
|
|
263
|
+
"prettier-plugin-tailwindcss@^0.5.11",
|
|
264
|
+
"babel-preset-expo",
|
|
265
|
+
];
|
|
266
|
+
const missingDeps = getMissingDeps(cwd, deps);
|
|
267
|
+
const missingDevDeps = getMissingDeps(cwd, devDeps.map(d => d.split('@')[0]));
|
|
268
|
+
|
|
269
|
+
console.log("");
|
|
270
|
+
console.log(pc.blue(" 📦 Dependencies"));
|
|
271
|
+
console.log("");
|
|
272
|
+
|
|
273
|
+
if (missingDeps.length === 0 && missingDevDeps.length === 0) {
|
|
274
|
+
console.log(
|
|
275
|
+
pc.dim(
|
|
276
|
+
" ✓ All required packages already in package.json, skipping install.",
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
} else {
|
|
280
|
+
if (missingDeps.length > 0) {
|
|
281
|
+
console.log(pc.dim(` Installing: ${missingDeps.join(", ")}`));
|
|
282
|
+
console.log("");
|
|
283
|
+
try {
|
|
284
|
+
installPackages(missingDeps);
|
|
285
|
+
} catch {
|
|
286
|
+
console.error("");
|
|
287
|
+
console.error(pc.yellow(" ✗ Install failed. Run manually:"));
|
|
288
|
+
console.error(pc.dim(` ${getInstallHint(missingDeps)}`));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (missingDevDeps.length > 0) {
|
|
293
|
+
const devDepsToInstall = devDeps.filter(d =>
|
|
294
|
+
missingDevDeps.includes(d.split('@')[0])
|
|
295
|
+
);
|
|
296
|
+
console.log(pc.dim(` Installing dev dependencies: ${devDepsToInstall.join(", ")}`));
|
|
297
|
+
console.log("");
|
|
298
|
+
try {
|
|
299
|
+
installPackages(devDepsToInstall, { dev: true });
|
|
300
|
+
} catch {
|
|
301
|
+
console.error("");
|
|
302
|
+
console.error(pc.yellow(" ✗ Dev dependencies install failed. Run manually:"));
|
|
303
|
+
console.error(pc.dim(` ${getInstallHint(devDepsToInstall, { dev: true })}`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ─── Success ────────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
console.log("");
|
|
311
|
+
console.log(pc.green(" ┌─────────────────────────────────────────┐"));
|
|
312
|
+
console.log(pc.green(" │ ✓ NovaUI is ready! │"));
|
|
313
|
+
console.log(pc.green(" └─────────────────────────────────────────┘"));
|
|
314
|
+
console.log("");
|
|
315
|
+
console.log(pc.bold(" Next steps:"));
|
|
316
|
+
console.log("");
|
|
317
|
+
console.log(
|
|
318
|
+
pc.dim(" 1. Import global CSS in your root entry (e.g. App.tsx):"),
|
|
319
|
+
);
|
|
320
|
+
console.log(pc.cyan(` import "${config.globalCss}"`));
|
|
321
|
+
console.log("");
|
|
322
|
+
console.log(pc.dim(" 2. Add components:"));
|
|
323
|
+
console.log(pc.cyan(" npx novaui-cli@latest add button"));
|
|
324
|
+
console.log(pc.cyan(" npx novaui-cli@latest add card"));
|
|
325
|
+
console.log("");
|
|
326
|
+
|
|
327
|
+
if (isInteractive) {
|
|
328
|
+
outro(pc.green("Setup complete!"));
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Allow overriding branch via environment variable (useful for testing)
|
|
2
|
+
const BRANCH = process.env.NOVAUI_BRANCH || 'main';
|
|
3
|
+
|
|
4
|
+
export const BASE_URL = `https://raw.githubusercontent.com/KaloyanBehov/novaui/${BRANCH}`;
|
|
5
|
+
|
|
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;
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_CONFIG = {
|
|
15
|
+
globalCss: 'global.css',
|
|
16
|
+
componentsUi: 'components/ui',
|
|
17
|
+
lib: 'lib',
|
|
18
|
+
theme: 'default',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const UTILS_CONTENT = `import { type ClassValue, clsx } from "clsx"
|
|
22
|
+
import { twMerge } from "tailwind-merge"
|
|
23
|
+
|
|
24
|
+
export function cn(...inputs: ClassValue[]) {
|
|
25
|
+
return twMerge(clsx(inputs))
|
|
26
|
+
}
|
|
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
|
+
`;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const DEFAULT_THEME_CSS = `@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 240 10% 3.9%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 240 10% 3.9%;
|
|
11
|
+
--popover: 0 0% 100%;
|
|
12
|
+
--popover-foreground: 240 10% 3.9%;
|
|
13
|
+
--primary: 240 5.9% 10%;
|
|
14
|
+
--primary-foreground: 0 0% 98%;
|
|
15
|
+
--secondary: 240 4.8% 95.9%;
|
|
16
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
17
|
+
--muted: 240 4.8% 95.9%;
|
|
18
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
19
|
+
--accent: 240 4.8% 95.9%;
|
|
20
|
+
--accent-foreground: 240 5.9% 10%;
|
|
21
|
+
--destructive: 0 84.2% 60.2%;
|
|
22
|
+
--destructive-foreground: 0 0% 98%;
|
|
23
|
+
--border: 240 5.9% 90%;
|
|
24
|
+
--input: 240 5.9% 90%;
|
|
25
|
+
--ring: 240 5.9% 10%;
|
|
26
|
+
--radius: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dark {
|
|
30
|
+
--background: 240 10% 3.9%;
|
|
31
|
+
--foreground: 0 0% 98%;
|
|
32
|
+
--card: 240 10% 3.9%;
|
|
33
|
+
--card-foreground: 0 0% 98%;
|
|
34
|
+
--popover: 240 10% 3.9%;
|
|
35
|
+
--popover-foreground: 0 0% 98%;
|
|
36
|
+
--primary: 0 0% 98%;
|
|
37
|
+
--primary-foreground: 240 5.9% 10%;
|
|
38
|
+
--secondary: 240 3.7% 15.9%;
|
|
39
|
+
--secondary-foreground: 0 0% 98%;
|
|
40
|
+
--muted: 240 3.7% 15.9%;
|
|
41
|
+
--muted-foreground: 240 5% 64.9%;
|
|
42
|
+
--accent: 240 3.7% 15.9%;
|
|
43
|
+
--accent-foreground: 0 0% 98%;
|
|
44
|
+
--destructive: 0 62.8% 30.6%;
|
|
45
|
+
--destructive-foreground: 0 0% 98%;
|
|
46
|
+
--border: 240 3.7% 15.9%;
|
|
47
|
+
--input: 240 3.7% 15.9%;
|
|
48
|
+
--ring: 240 4.9% 83.9%;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { THEMES_BASE_URL, DEFAULT_THEME_CSS } from '../constants.js'
|
|
2
|
+
import { fetchWithTimeout } from '../utils/fetch.js'
|
|
3
|
+
|
|
4
|
+
// Available theme names (fetched from GitHub)
|
|
5
|
+
export const THEME_KEYS = ['default', 'ocean', 'sunset']
|
|
6
|
+
|
|
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
|
+
}
|
|
48
|
+
}
|