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.
@@ -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 fs from 'node:fs'
4
- import { fileURLToPath } from 'node:url'
5
- import { Command } from 'commander'
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 { DEFAULT_CONFIG, UTILS_CONTENT, CONFIG_FILENAME } from './constants.js'
11
- import { ensureDir, writeIfNotExists } from './utils/fs-helpers.js'
12
- import { loadConfig, writeConfig } from './utils/config.js'
13
- import { detectPackageManager, installPackages, getInstallHint, getMissingDeps } from './utils/deps.js'
14
- import { fetchWithTimeout, formatError } from './utils/fetch.js'
15
- import { getCliVersion } from './utils/version.js'
16
- import { getTailwindConfigContent } from './utils/tailwind.js'
17
- import { assertValidComponentConfig } from './utils/validate.js'
18
- import { init, askTheme, ASCII_BANNER } from './commands/init.js'
19
- import { add, pickComponentsInteractively } from './commands/add.js'
20
- import { getThemeCssContent } from './themes/index.js'
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
- export const GLOBAL_CSS_CONTENT = getThemeCssContent(DEFAULT_CONFIG.theme)
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
  }