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
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
checkPackageJson,
|
|
8
|
+
checkReactNativeProject,
|
|
9
|
+
checkBabelConfig,
|
|
10
|
+
checkNativeWindInBabel,
|
|
11
|
+
checkReactNativeVersion,
|
|
12
|
+
checkNativeWindVersion,
|
|
13
|
+
runInitPreflightChecks,
|
|
14
|
+
runAddPreflightChecks,
|
|
15
|
+
} from '../utils/preflight.js'
|
|
16
|
+
|
|
17
|
+
// Test helpers
|
|
18
|
+
function makeTmpDir() {
|
|
19
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'novaui-preflight-test-'))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cleanTmpDir(dir) {
|
|
23
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('checkPackageJson', () => {
|
|
27
|
+
let tmp
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmp = makeTmpDir()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
cleanTmpDir(tmp)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('throws when package.json does not exist', () => {
|
|
38
|
+
expect(() => checkPackageJson(tmp)).toThrow(/package.json not found/)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('returns parsed package.json when valid', () => {
|
|
42
|
+
const pkg = { name: 'test-app', version: '1.0.0' }
|
|
43
|
+
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify(pkg))
|
|
44
|
+
|
|
45
|
+
const result = checkPackageJson(tmp)
|
|
46
|
+
expect(result).toEqual(pkg)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('throws when package.json is invalid JSON', () => {
|
|
50
|
+
fs.writeFileSync(path.join(tmp, 'package.json'), 'not valid json')
|
|
51
|
+
|
|
52
|
+
expect(() => checkPackageJson(tmp)).toThrow(/invalid or cannot be parsed/)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('checkReactNativeProject', () => {
|
|
57
|
+
let consoleLogSpy
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
consoleLogSpy.mockRestore()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('warns when react-native is not in dependencies', () => {
|
|
68
|
+
const pkg = { dependencies: {} }
|
|
69
|
+
checkReactNativeProject(pkg)
|
|
70
|
+
|
|
71
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
72
|
+
expect.stringContaining('react-native or expo not found')
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('does not warn when react-native is present', () => {
|
|
77
|
+
const pkg = { dependencies: { 'react-native': '0.80.0' } }
|
|
78
|
+
checkReactNativeProject(pkg)
|
|
79
|
+
|
|
80
|
+
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
|
81
|
+
expect.stringContaining('react-native or expo not found')
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('does not warn when expo is present', () => {
|
|
86
|
+
const pkg = { dependencies: { expo: '~54.0.0' } }
|
|
87
|
+
checkReactNativeProject(pkg)
|
|
88
|
+
|
|
89
|
+
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
|
90
|
+
expect.stringContaining('react-native or expo not found')
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('checks devDependencies too', () => {
|
|
95
|
+
const pkg = { devDependencies: { 'react-native': '0.80.0' } }
|
|
96
|
+
checkReactNativeProject(pkg)
|
|
97
|
+
|
|
98
|
+
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
|
99
|
+
expect.stringContaining('react-native or expo not found')
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('checkBabelConfig', () => {
|
|
105
|
+
let tmp
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
tmp = makeTmpDir()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
cleanTmpDir(tmp)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns path to babel.config.js when it exists', () => {
|
|
116
|
+
const babelPath = path.join(tmp, 'babel.config.js')
|
|
117
|
+
fs.writeFileSync(babelPath, 'module.exports = {}')
|
|
118
|
+
|
|
119
|
+
const result = checkBabelConfig(tmp)
|
|
120
|
+
expect(result).toBe(babelPath)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('returns path to .babelrc when it exists', () => {
|
|
124
|
+
const babelPath = path.join(tmp, '.babelrc')
|
|
125
|
+
fs.writeFileSync(babelPath, '{}')
|
|
126
|
+
|
|
127
|
+
const result = checkBabelConfig(tmp)
|
|
128
|
+
expect(result).toBe(babelPath)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('returns null when no babel config exists', () => {
|
|
132
|
+
const result = checkBabelConfig(tmp)
|
|
133
|
+
expect(result).toBeNull()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('prefers babel.config.js over .babelrc', () => {
|
|
137
|
+
const jsPath = path.join(tmp, 'babel.config.js')
|
|
138
|
+
const rcPath = path.join(tmp, '.babelrc')
|
|
139
|
+
fs.writeFileSync(jsPath, 'module.exports = {}')
|
|
140
|
+
fs.writeFileSync(rcPath, '{}')
|
|
141
|
+
|
|
142
|
+
const result = checkBabelConfig(tmp)
|
|
143
|
+
expect(result).toBe(jsPath)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('checkNativeWindInBabel', () => {
|
|
148
|
+
let tmp
|
|
149
|
+
let consoleLogSpy
|
|
150
|
+
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
tmp = makeTmpDir()
|
|
153
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
cleanTmpDir(tmp)
|
|
158
|
+
consoleLogSpy.mockRestore()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('returns false and warns when babel config not found', () => {
|
|
162
|
+
const result = checkNativeWindInBabel(tmp)
|
|
163
|
+
|
|
164
|
+
expect(result).toBe(false)
|
|
165
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
166
|
+
expect.stringContaining('babel.config.js not found')
|
|
167
|
+
)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('returns true when nativewind/babel is found', () => {
|
|
171
|
+
const babelPath = path.join(tmp, 'babel.config.js')
|
|
172
|
+
fs.writeFileSync(
|
|
173
|
+
babelPath,
|
|
174
|
+
'module.exports = { plugins: ["nativewind/babel"] }'
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const result = checkNativeWindInBabel(tmp)
|
|
178
|
+
expect(result).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('returns false and warns when nativewind/babel is not found', () => {
|
|
182
|
+
const babelPath = path.join(tmp, 'babel.config.js')
|
|
183
|
+
fs.writeFileSync(babelPath, 'module.exports = { plugins: [] }')
|
|
184
|
+
|
|
185
|
+
const result = checkNativeWindInBabel(tmp)
|
|
186
|
+
|
|
187
|
+
expect(result).toBe(false)
|
|
188
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
189
|
+
expect.stringContaining('NativeWind not detected in Babel config')
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('handles babel config read errors gracefully', () => {
|
|
194
|
+
const babelPath = path.join(tmp, 'babel.config.js')
|
|
195
|
+
fs.writeFileSync(babelPath, 'module.exports = {}')
|
|
196
|
+
fs.chmodSync(babelPath, 0o000) // Make unreadable
|
|
197
|
+
|
|
198
|
+
const result = checkNativeWindInBabel(tmp)
|
|
199
|
+
|
|
200
|
+
expect(result).toBe(false)
|
|
201
|
+
|
|
202
|
+
// Cleanup
|
|
203
|
+
fs.chmodSync(babelPath, 0o644)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('checkReactNativeVersion', () => {
|
|
208
|
+
let consoleLogSpy
|
|
209
|
+
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
afterEach(() => {
|
|
215
|
+
consoleLogSpy.mockRestore()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('returns true for React Native 0.72+', () => {
|
|
219
|
+
const pkg = { dependencies: { 'react-native': '0.72.0' } }
|
|
220
|
+
const result = checkReactNativeVersion(pkg)
|
|
221
|
+
|
|
222
|
+
expect(result).toBe(true)
|
|
223
|
+
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
|
224
|
+
expect.stringContaining('React Native version is below 0.72')
|
|
225
|
+
)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('returns true for React Native 0.80+', () => {
|
|
229
|
+
const pkg = { dependencies: { 'react-native': '0.80.5' } }
|
|
230
|
+
const result = checkReactNativeVersion(pkg)
|
|
231
|
+
|
|
232
|
+
expect(result).toBe(true)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('warns for React Native < 0.72', () => {
|
|
236
|
+
const pkg = { dependencies: { 'react-native': '0.71.0' } }
|
|
237
|
+
const result = checkReactNativeVersion(pkg)
|
|
238
|
+
|
|
239
|
+
expect(result).toBe(false)
|
|
240
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
241
|
+
expect.stringContaining('React Native version is below 0.72')
|
|
242
|
+
)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('returns true when react-native not in dependencies', () => {
|
|
246
|
+
const pkg = { dependencies: {} }
|
|
247
|
+
const result = checkReactNativeVersion(pkg)
|
|
248
|
+
|
|
249
|
+
expect(result).toBe(true)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('handles version with caret (^)', () => {
|
|
253
|
+
const pkg = { dependencies: { 'react-native': '^0.72.0' } }
|
|
254
|
+
const result = checkReactNativeVersion(pkg)
|
|
255
|
+
|
|
256
|
+
expect(result).toBe(true)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('handles version with tilde (~)', () => {
|
|
260
|
+
const pkg = { dependencies: { 'react-native': '~0.80.0' } }
|
|
261
|
+
const result = checkReactNativeVersion(pkg)
|
|
262
|
+
|
|
263
|
+
expect(result).toBe(true)
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('checkNativeWindVersion', () => {
|
|
268
|
+
let consoleLogSpy
|
|
269
|
+
|
|
270
|
+
beforeEach(() => {
|
|
271
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
afterEach(() => {
|
|
275
|
+
consoleLogSpy.mockRestore()
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('returns false and warns when nativewind not found', () => {
|
|
279
|
+
const pkg = { dependencies: {} }
|
|
280
|
+
const result = checkNativeWindVersion(pkg)
|
|
281
|
+
|
|
282
|
+
expect(result).toBe(false)
|
|
283
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
284
|
+
expect.stringContaining('NativeWind not found in package.json')
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('returns true for NativeWind 4.x', () => {
|
|
289
|
+
const pkg = { dependencies: { nativewind: '^4.0.0' } }
|
|
290
|
+
const result = checkNativeWindVersion(pkg)
|
|
291
|
+
|
|
292
|
+
expect(result).toBe(true)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('returns true for NativeWind 5.x', () => {
|
|
296
|
+
const pkg = { dependencies: { nativewind: '^5.0.0' } }
|
|
297
|
+
const result = checkNativeWindVersion(pkg)
|
|
298
|
+
|
|
299
|
+
expect(result).toBe(true)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('warns for NativeWind < 4', () => {
|
|
303
|
+
const pkg = { dependencies: { nativewind: '^3.0.0' } }
|
|
304
|
+
const result = checkNativeWindVersion(pkg)
|
|
305
|
+
|
|
306
|
+
expect(result).toBe(false)
|
|
307
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
308
|
+
expect.stringContaining('NativeWind version is below 4.0')
|
|
309
|
+
)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('checks devDependencies', () => {
|
|
313
|
+
const pkg = { devDependencies: { nativewind: '^4.0.0' } }
|
|
314
|
+
const result = checkNativeWindVersion(pkg)
|
|
315
|
+
|
|
316
|
+
expect(result).toBe(true)
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('runInitPreflightChecks', () => {
|
|
321
|
+
let tmp
|
|
322
|
+
let consoleLogSpy
|
|
323
|
+
|
|
324
|
+
beforeEach(() => {
|
|
325
|
+
tmp = makeTmpDir()
|
|
326
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
afterEach(() => {
|
|
330
|
+
cleanTmpDir(tmp)
|
|
331
|
+
consoleLogSpy.mockRestore()
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('runs all checks successfully for valid setup', () => {
|
|
335
|
+
fs.writeFileSync(
|
|
336
|
+
path.join(tmp, 'package.json'),
|
|
337
|
+
JSON.stringify({
|
|
338
|
+
dependencies: {
|
|
339
|
+
'react-native': '0.80.0',
|
|
340
|
+
nativewind: '^4.0.0',
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
)
|
|
344
|
+
fs.writeFileSync(
|
|
345
|
+
path.join(tmp, 'babel.config.js'),
|
|
346
|
+
'module.exports = { plugins: ["nativewind/babel"] }'
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
expect(() => runInitPreflightChecks(tmp)).not.toThrow()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('throws when package.json is missing', () => {
|
|
353
|
+
expect(() => runInitPreflightChecks(tmp)).toThrow(/package.json not found/)
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe('runAddPreflightChecks', () => {
|
|
358
|
+
let tmp
|
|
359
|
+
|
|
360
|
+
beforeEach(() => {
|
|
361
|
+
tmp = makeTmpDir()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
afterEach(() => {
|
|
365
|
+
cleanTmpDir(tmp)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('returns package json on success', () => {
|
|
369
|
+
const pkg = { name: 'test', version: '1.0.0' }
|
|
370
|
+
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify(pkg))
|
|
371
|
+
|
|
372
|
+
const result = runAddPreflightChecks(tmp)
|
|
373
|
+
expect(result).toEqual(pkg)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('throws when package.json is missing', () => {
|
|
377
|
+
expect(() => runAddPreflightChecks(tmp)).toThrow(/package.json not found/)
|
|
378
|
+
})
|
|
379
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { getVersionInfo } from '../utils/version-check.js'
|
|
3
|
+
import { getCliVersion } from '../utils/version.js'
|
|
4
|
+
|
|
5
|
+
describe('getVersionInfo', () => {
|
|
6
|
+
it('returns cli and node versions', () => {
|
|
7
|
+
const info = getVersionInfo()
|
|
8
|
+
|
|
9
|
+
expect(info).toHaveProperty('cli')
|
|
10
|
+
expect(info).toHaveProperty('node')
|
|
11
|
+
expect(typeof info.cli).toBe('string')
|
|
12
|
+
expect(typeof info.node).toBe('string')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('includes valid node version format', () => {
|
|
16
|
+
const info = getVersionInfo()
|
|
17
|
+
expect(info.node).toMatch(/^v\d+\.\d+\.\d+/)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('includes valid cli version or unknown', () => {
|
|
21
|
+
const info = getVersionInfo()
|
|
22
|
+
expect(info.cli).toMatch(/^\d+\.\d+\.\d+|unknown/)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('matches current node version', () => {
|
|
26
|
+
const info = getVersionInfo()
|
|
27
|
+
expect(info.node).toBe(process.version)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('matches getCliVersion output', () => {
|
|
31
|
+
const info = getVersionInfo()
|
|
32
|
+
const cliVersion = getCliVersion()
|
|
33
|
+
expect(info.cli).toBe(cliVersion)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Note: Testing checkForUpdates is complex due to async nature and external dependencies
|
|
38
|
+
// These tests would require mocking fs, fetch, and time-based caching
|
|
39
|
+
// For production, integration tests would be more appropriate
|
|
40
|
+
describe('version checking (unit tests)', () => {
|
|
41
|
+
it('version comparison logic works correctly', () => {
|
|
42
|
+
// This tests the internal compareVersions logic indirectly
|
|
43
|
+
// by ensuring version info is structured correctly
|
|
44
|
+
const info = getVersionInfo()
|
|
45
|
+
|
|
46
|
+
expect(info.cli).toBeDefined()
|
|
47
|
+
expect(info.node).toBeDefined()
|
|
48
|
+
|
|
49
|
+
// Versions should be parseable
|
|
50
|
+
if (info.cli !== 'unknown') {
|
|
51
|
+
const parts = info.cli.split('.')
|
|
52
|
+
expect(parts.length).toBeGreaterThanOrEqual(3)
|
|
53
|
+
parts.forEach(part => {
|
|
54
|
+
expect(parseInt(part, 10)).not.toBeNaN()
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
package/src/bin.js
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import pc from 'picocolors'
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import pc from 'picocolors';
|
|
7
7
|
|
|
8
8
|
// ─── Module imports ──────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
10
|
+
import { getThemeCssContent } from './themes/index.js';
|
|
11
|
+
import { add, pickComponentsInteractively } from './commands/add.js';
|
|
12
|
+
import { ASCII_BANNER, askTheme, init } from './commands/init.js';
|
|
13
|
+
import { CONFIG_FILENAME, DEFAULT_CONFIG, DEFAULT_THEME_CSS, UTILS_CONTENT } from './constants.js';
|
|
14
|
+
import { loadConfig, writeConfig } from './utils/config.js';
|
|
15
|
+
import { detectPackageManager, getInstallHint, getMissingDeps } from './utils/deps.js';
|
|
16
|
+
import { fetchWithTimeout, formatError } from './utils/fetch.js';
|
|
17
|
+
import { ensureDir, writeIfNotExists } from './utils/fs-helpers.js';
|
|
18
|
+
import { getTailwindConfigContent } from './utils/tailwind.js';
|
|
19
|
+
import { assertValidComponentConfig } from './utils/validate.js';
|
|
20
|
+
import { getCliVersion } from './utils/version.js';
|
|
21
|
+
import { checkForUpdates } from './utils/version-check.js';
|
|
21
22
|
|
|
22
23
|
// ─── Derived constants ───────────────────────────────────────────────────────
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
// Export default theme CSS for backward compatibility (tests)
|
|
26
|
+
export const GLOBAL_CSS_CONTENT = DEFAULT_THEME_CSS;
|
|
25
27
|
|
|
26
28
|
// ─── Re-exports (backward compatibility for tests) ───────────────────────────
|
|
27
29
|
|
|
@@ -37,35 +39,38 @@ export {
|
|
|
37
39
|
formatError,
|
|
38
40
|
getCliVersion,
|
|
39
41
|
getInstallHint,
|
|
40
|
-
getThemeCssContent,
|
|
41
42
|
getMissingDeps,
|
|
42
43
|
getTailwindConfigContent,
|
|
44
|
+
getThemeCssContent,
|
|
43
45
|
init,
|
|
44
46
|
loadConfig,
|
|
45
47
|
UTILS_CONTENT,
|
|
46
48
|
writeConfig,
|
|
47
49
|
writeIfNotExists,
|
|
48
|
-
}
|
|
50
|
+
};
|
|
49
51
|
|
|
50
52
|
// ─── CLI entry point ─────────────────────────────────────────────────────────
|
|
51
53
|
|
|
52
54
|
const isDirectRun = (() => {
|
|
53
|
-
if (!process.argv[1]) return false
|
|
55
|
+
if (!process.argv[1]) return false;
|
|
54
56
|
try {
|
|
55
|
-
return fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
57
|
+
return fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
56
58
|
} catch {
|
|
57
|
-
return process.argv[1].endsWith('/bin.js')
|
|
59
|
+
return process.argv[1].endsWith('/bin.js');
|
|
58
60
|
}
|
|
59
|
-
})()
|
|
61
|
+
})();
|
|
60
62
|
|
|
61
63
|
if (isDirectRun) {
|
|
62
|
-
const program = new Command()
|
|
64
|
+
const program = new Command();
|
|
63
65
|
|
|
64
66
|
program
|
|
65
67
|
.name('novaui')
|
|
66
68
|
.description('NovaUI – React Native + NativeWind UI component library')
|
|
67
69
|
.version(getCliVersion(), '-v, --version', 'Show CLI version')
|
|
68
70
|
.addHelpText('beforeAll', ASCII_BANNER)
|
|
71
|
+
.hook('preAction', async () => {
|
|
72
|
+
await checkForUpdates().catch(() => {});
|
|
73
|
+
});
|
|
69
74
|
|
|
70
75
|
// ─── init ─────────────────────────────────────────────────────────────────
|
|
71
76
|
|
|
@@ -73,15 +78,15 @@ if (isDirectRun) {
|
|
|
73
78
|
.command('init')
|
|
74
79
|
.description('Set up NovaUI (config, Tailwind, global.css, utils)')
|
|
75
80
|
.option('-y, --yes', 'Skip prompts and use default configuration')
|
|
76
|
-
.action(async options => {
|
|
81
|
+
.action(async (options) => {
|
|
77
82
|
try {
|
|
78
|
-
await init({ yes: options.yes })
|
|
83
|
+
await init({ yes: options.yes });
|
|
79
84
|
} catch (error) {
|
|
80
|
-
console.error('')
|
|
81
|
-
console.error(pc.red(` ✗ Error: ${formatError(error)}`))
|
|
82
|
-
process.exit(1)
|
|
85
|
+
console.error('');
|
|
86
|
+
console.error(pc.red(` ✗ Error: ${formatError(error)}`));
|
|
87
|
+
process.exit(1);
|
|
83
88
|
}
|
|
84
|
-
})
|
|
89
|
+
});
|
|
85
90
|
|
|
86
91
|
// ─── add ──────────────────────────────────────────────────────────────────
|
|
87
92
|
|
|
@@ -90,39 +95,39 @@ if (isDirectRun) {
|
|
|
90
95
|
.description('Add one or more components (e.g. button card input)')
|
|
91
96
|
.option('--force', 'Overwrite existing component files')
|
|
92
97
|
.action(async (components, options) => {
|
|
93
|
-
const force = options.force || false
|
|
98
|
+
const force = options.force || false;
|
|
94
99
|
try {
|
|
95
100
|
if (components.length === 0) {
|
|
96
101
|
if (process.stdin.isTTY !== true) {
|
|
97
|
-
throw new Error('Missing component name. Usage: novaui add <component-name>')
|
|
102
|
+
throw new Error('Missing component name. Usage: novaui add <component-name>');
|
|
98
103
|
}
|
|
99
104
|
// Interactive multi-select when no component names given
|
|
100
|
-
const selected = await pickComponentsInteractively()
|
|
105
|
+
const selected = await pickComponentsInteractively();
|
|
101
106
|
for (const name of selected) {
|
|
102
|
-
await add(name, { force })
|
|
107
|
+
await add(name, { force });
|
|
103
108
|
}
|
|
104
109
|
} else {
|
|
105
110
|
// Support batch: novaui add button card input
|
|
106
111
|
for (const name of components) {
|
|
107
|
-
await add(name, { force })
|
|
112
|
+
await add(name, { force });
|
|
108
113
|
}
|
|
109
114
|
}
|
|
110
115
|
} catch (error) {
|
|
111
|
-
console.error('')
|
|
112
|
-
console.error(pc.red(` ✗ Error: ${formatError(error)}`))
|
|
113
|
-
process.exit(1)
|
|
116
|
+
console.error('');
|
|
117
|
+
console.error(pc.red(` ✗ Error: ${formatError(error)}`));
|
|
118
|
+
process.exit(1);
|
|
114
119
|
}
|
|
115
|
-
})
|
|
120
|
+
});
|
|
116
121
|
|
|
117
122
|
// ─── Default: show help when no command given ─────────────────────────────
|
|
118
123
|
|
|
119
124
|
program.action(() => {
|
|
120
|
-
program.help()
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
program.parseAsync(process.argv).catch(error => {
|
|
124
|
-
console.error('')
|
|
125
|
-
console.error(pc.red(` ✗ Error: ${formatError(error)}`))
|
|
126
|
-
process.exit(1)
|
|
127
|
-
})
|
|
125
|
+
program.help();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
129
|
+
console.error('');
|
|
130
|
+
console.error(pc.red(` ✗ Error: ${formatError(error)}`));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
});
|
|
128
133
|
}
|