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,150 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import { findSimilarComponents, formatComponentNotFoundError } from '../utils/fuzzy.js'
4
+
5
+ describe('findSimilarComponents', () => {
6
+ const availableComponents = [
7
+ 'button',
8
+ 'card',
9
+ 'dialog',
10
+ 'drawer',
11
+ 'dropdown-menu',
12
+ 'alert',
13
+ 'alert-dialog',
14
+ 'badge',
15
+ 'breadcrumb',
16
+ 'calendar',
17
+ ]
18
+
19
+ it('finds exact typo matches', () => {
20
+ const result = findSimilarComponents('buton', availableComponents)
21
+ expect(result).toContain('button')
22
+ })
23
+
24
+ it('finds similar names with single character difference', () => {
25
+ const result = findSimilarComponents('crad', availableComponents)
26
+ expect(result).toContain('card')
27
+ })
28
+
29
+ it('finds names starting with same letter', () => {
30
+ const result = findSimilarComponents('but', availableComponents)
31
+ expect(result[0]).toBe('button')
32
+ })
33
+
34
+ it('returns empty array for very different inputs', () => {
35
+ const result = findSimilarComponents('xyz123', availableComponents)
36
+ expect(result).toEqual([])
37
+ })
38
+
39
+ it('limits results to maxResults parameter', () => {
40
+ const result = findSimilarComponents('a', availableComponents, 2)
41
+ expect(result.length).toBeLessThanOrEqual(2)
42
+ })
43
+
44
+ it('sorts by similarity (closest match first)', () => {
45
+ const result = findSimilarComponents('dilog', availableComponents)
46
+ expect(result[0]).toBe('dialog')
47
+ })
48
+
49
+ it('finds close matches with reasonable threshold', () => {
50
+ const result = findSimilarComponents('buton', availableComponents)
51
+ // Should find button with 1 character difference
52
+ expect(result).toContain('button')
53
+ })
54
+
55
+ it('handles case insensitivity', () => {
56
+ const result = findSimilarComponents('BUTTON', availableComponents)
57
+ expect(result).toContain('button')
58
+ })
59
+
60
+ it('handles whitespace in input', () => {
61
+ const result = findSimilarComponents(' button ', availableComponents)
62
+ expect(result).toContain('button')
63
+ })
64
+
65
+ it('suggests exact matches and similar names', () => {
66
+ const result = findSimilarComponents('alert', availableComponents)
67
+ expect(result).toContain('alert')
68
+ // May also include alert-dialog if distance is close enough
69
+ expect(result.length).toBeGreaterThan(0)
70
+ })
71
+
72
+ it('returns up to 5 results by default', () => {
73
+ const result = findSimilarComponents('a', availableComponents)
74
+ expect(result.length).toBeLessThanOrEqual(5)
75
+ })
76
+
77
+ it('prefers similar matches', () => {
78
+ const result = findSimilarComponents('butn', availableComponents)
79
+ // Should find button as closest match
80
+ expect(result[0]).toBe('button')
81
+ })
82
+
83
+ it('handles component with multiple word separators', () => {
84
+ const result = findSimilarComponents('alertdialog', availableComponents)
85
+ expect(result).toContain('alert-dialog')
86
+ })
87
+ })
88
+
89
+ describe('formatComponentNotFoundError', () => {
90
+ const availableComponents = ['button', 'card', 'dialog', 'alert', 'badge']
91
+
92
+ it('includes the component name in error message', () => {
93
+ const error = formatComponentNotFoundError('buton', availableComponents)
94
+ expect(error).toContain('buton')
95
+ expect(error).toContain('not found in registry')
96
+ })
97
+
98
+ it('suggests similar component names', () => {
99
+ const error = formatComponentNotFoundError('buton', availableComponents)
100
+ expect(error).toContain('Did you mean one of these?')
101
+ expect(error).toContain('button')
102
+ })
103
+
104
+ it('lists all available components', () => {
105
+ const error = formatComponentNotFoundError('xyz', availableComponents)
106
+ expect(error).toContain('Available components:')
107
+ expect(error).toContain('button')
108
+ expect(error).toContain('card')
109
+ expect(error).toContain('dialog')
110
+ })
111
+
112
+ it('formats available components in columns', () => {
113
+ const manyComponents = Array.from({ length: 20 }, (_, i) => `component-${i}`)
114
+ const error = formatComponentNotFoundError('xyz', manyComponents)
115
+
116
+ // Should have multiple lines
117
+ const lines = error.split('\n')
118
+ expect(lines.length).toBeGreaterThan(5)
119
+ })
120
+
121
+ it('does not suggest anything for very different names', () => {
122
+ const error = formatComponentNotFoundError('xyz123abc', availableComponents)
123
+ expect(error).not.toContain('Did you mean')
124
+ expect(error).toContain('Available components:')
125
+ })
126
+
127
+ it('handles empty component list gracefully', () => {
128
+ const error = formatComponentNotFoundError('button', [])
129
+ expect(error).toContain('not found in registry')
130
+ expect(error).toContain('Available components:')
131
+ })
132
+
133
+ it('suggests multiple matches when applicable', () => {
134
+ const components = ['alert', 'alert-dialog', 'alert-box']
135
+ const error = formatComponentNotFoundError('aler', components)
136
+ expect(error).toContain('alert')
137
+ expect(error).toContain('Did you mean one of these?')
138
+ })
139
+
140
+ it('includes formatting with bullet points for suggestions', () => {
141
+ const error = formatComponentNotFoundError('buton', availableComponents)
142
+ expect(error).toContain('•')
143
+ })
144
+
145
+ it('handles long component names', () => {
146
+ const components = ['very-long-component-name-that-exceeds-twenty-chars']
147
+ const error = formatComponentNotFoundError('verylong', components)
148
+ expect(error).toContain('very-long-component-name-that-exceeds-twenty-chars')
149
+ })
150
+ })
@@ -0,0 +1,520 @@
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
+ DEFAULT_CONFIG,
8
+ CONFIG_FILENAME,
9
+ GLOBAL_CSS_CONTENT,
10
+ UTILS_CONTENT,
11
+ getTailwindConfigContent,
12
+ detectPackageManager,
13
+ ensureDir,
14
+ writeIfNotExists,
15
+ getMissingDeps,
16
+ loadConfig,
17
+ writeConfig,
18
+ assertValidComponentConfig,
19
+ fetchWithTimeout,
20
+ formatError,
21
+ getCliVersion,
22
+ getInstallHint,
23
+ init,
24
+ add,
25
+ } from "../bin.js"
26
+
27
+ // ─── Test helpers ───────────────────────────────────────────────────────────
28
+
29
+ function makeTmpDir() {
30
+ return fs.mkdtempSync(path.join(os.tmpdir(), "novaui-test-"))
31
+ }
32
+
33
+ function cleanTmpDir(dir) {
34
+ fs.rmSync(dir, { recursive: true, force: true })
35
+ }
36
+
37
+ // ─── DEFAULT_CONFIG ─────────────────────────────────────────────────────────
38
+
39
+ describe("DEFAULT_CONFIG", () => {
40
+ it("has the expected default paths", () => {
41
+ expect(DEFAULT_CONFIG).toEqual({
42
+ globalCss: "global.css",
43
+ componentsUi: "components/ui",
44
+ lib: "lib",
45
+ theme: "default",
46
+ })
47
+ })
48
+ })
49
+
50
+ // ─── detectPackageManager ───────────────────────────────────────────────────
51
+
52
+ describe("detectPackageManager", () => {
53
+ const originalAgent = process.env.npm_config_user_agent
54
+
55
+ afterEach(() => {
56
+ if (originalAgent !== undefined) {
57
+ process.env.npm_config_user_agent = originalAgent
58
+ } else {
59
+ delete process.env.npm_config_user_agent
60
+ }
61
+ })
62
+
63
+ it("returns npm by default", () => {
64
+ process.env.npm_config_user_agent = ""
65
+ const result = detectPackageManager()
66
+ expect(result.command).toBe("npm")
67
+ expect(result.baseArgs).toEqual(["install"])
68
+ })
69
+
70
+ it("detects yarn", () => {
71
+ process.env.npm_config_user_agent = "yarn/1.22.0 npm/? node/v18.0.0"
72
+ const result = detectPackageManager()
73
+ expect(result.command).toBe("yarn")
74
+ expect(result.baseArgs).toEqual(["add"])
75
+ })
76
+
77
+ it("detects pnpm", () => {
78
+ process.env.npm_config_user_agent = "pnpm/8.0.0 npm/? node/v18.0.0"
79
+ const result = detectPackageManager()
80
+ expect(result.command).toBe("pnpm")
81
+ expect(result.baseArgs).toEqual(["add"])
82
+ })
83
+
84
+ it("detects bun", () => {
85
+ process.env.npm_config_user_agent = "bun/1.0.0"
86
+ const result = detectPackageManager()
87
+ expect(result.command).toBe("bun")
88
+ expect(result.baseArgs).toEqual(["add"])
89
+ })
90
+ })
91
+
92
+ // ─── ensureDir ──────────────────────────────────────────────────────────────
93
+
94
+ describe("ensureDir", () => {
95
+ let tmp
96
+
97
+ beforeEach(() => {
98
+ tmp = makeTmpDir()
99
+ })
100
+
101
+ afterEach(() => {
102
+ cleanTmpDir(tmp)
103
+ })
104
+
105
+ it("creates a directory that does not exist", () => {
106
+ const dir = path.join(tmp, "a", "b", "c")
107
+ expect(fs.existsSync(dir)).toBe(false)
108
+ ensureDir(dir)
109
+ expect(fs.existsSync(dir)).toBe(true)
110
+ expect(fs.statSync(dir).isDirectory()).toBe(true)
111
+ })
112
+
113
+ it("does nothing if directory already exists", () => {
114
+ const dir = path.join(tmp, "existing")
115
+ fs.mkdirSync(dir)
116
+ ensureDir(dir)
117
+ expect(fs.existsSync(dir)).toBe(true)
118
+ })
119
+ })
120
+
121
+ // ─── writeIfNotExists ───────────────────────────────────────────────────────
122
+
123
+ describe("writeIfNotExists", () => {
124
+ let tmp
125
+
126
+ beforeEach(() => {
127
+ tmp = makeTmpDir()
128
+ })
129
+
130
+ afterEach(() => {
131
+ cleanTmpDir(tmp)
132
+ })
133
+
134
+ it("creates a file when it does not exist", () => {
135
+ const filePath = path.join(tmp, "test.txt")
136
+ const result = writeIfNotExists(filePath, "hello", "test.txt")
137
+ expect(result).toBe(true)
138
+ expect(fs.readFileSync(filePath, "utf8")).toBe("hello")
139
+ })
140
+
141
+ it("does not overwrite an existing file", () => {
142
+ const filePath = path.join(tmp, "test.txt")
143
+ fs.writeFileSync(filePath, "original")
144
+ const result = writeIfNotExists(filePath, "new content", "test.txt")
145
+ expect(result).toBe(false)
146
+ expect(fs.readFileSync(filePath, "utf8")).toBe("original")
147
+ })
148
+ })
149
+
150
+ // ─── getMissingDeps ─────────────────────────────────────────────────────────
151
+
152
+ describe("getMissingDeps", () => {
153
+ let tmp
154
+
155
+ beforeEach(() => {
156
+ tmp = makeTmpDir()
157
+ })
158
+
159
+ afterEach(() => {
160
+ cleanTmpDir(tmp)
161
+ })
162
+
163
+ it("returns all deps when no package.json exists", () => {
164
+ const result = getMissingDeps(tmp, ["react", "react-native"])
165
+ expect(result).toEqual(["react", "react-native"])
166
+ })
167
+
168
+ it("returns only missing deps", () => {
169
+ const pkgJson = {
170
+ dependencies: { react: "^18.0.0", clsx: "^2.0.0" },
171
+ devDependencies: { typescript: "^5.0.0" },
172
+ }
173
+ fs.writeFileSync(path.join(tmp, "package.json"), JSON.stringify(pkgJson))
174
+
175
+ const result = getMissingDeps(tmp, ["react", "nativewind", "typescript", "tailwindcss"])
176
+ expect(result).toEqual(["nativewind", "tailwindcss"])
177
+ })
178
+
179
+ it("returns empty array when all deps are present", () => {
180
+ const pkgJson = {
181
+ dependencies: { react: "^18.0.0", nativewind: "^4.0.0" },
182
+ }
183
+ fs.writeFileSync(path.join(tmp, "package.json"), JSON.stringify(pkgJson))
184
+
185
+ const result = getMissingDeps(tmp, ["react", "nativewind"])
186
+ expect(result).toEqual([])
187
+ })
188
+
189
+ it("checks devDependencies too", () => {
190
+ const pkgJson = {
191
+ devDependencies: { vitest: "^1.0.0" },
192
+ }
193
+ fs.writeFileSync(path.join(tmp, "package.json"), JSON.stringify(pkgJson))
194
+
195
+ const result = getMissingDeps(tmp, ["vitest"])
196
+ expect(result).toEqual([])
197
+ })
198
+ })
199
+
200
+ // ─── loadConfig / writeConfig ───────────────────────────────────────────────
201
+
202
+ describe("loadConfig / writeConfig", () => {
203
+ let tmp
204
+
205
+ beforeEach(() => {
206
+ tmp = makeTmpDir()
207
+ })
208
+
209
+ afterEach(() => {
210
+ cleanTmpDir(tmp)
211
+ })
212
+
213
+ it("returns null when no config file exists", () => {
214
+ expect(loadConfig(tmp)).toBeNull()
215
+ })
216
+
217
+ it("writes and reads config correctly", () => {
218
+ const config = {
219
+ globalCss: "app/global.css",
220
+ componentsUi: "app/components/ui",
221
+ lib: "app/lib",
222
+ }
223
+ writeConfig(tmp, config)
224
+
225
+ const configPath = path.join(tmp, CONFIG_FILENAME)
226
+ expect(fs.existsSync(configPath)).toBe(true)
227
+
228
+ const loaded = loadConfig(tmp)
229
+ expect(loaded).toEqual({ ...DEFAULT_CONFIG, ...config })
230
+ })
231
+
232
+ it("merges with DEFAULT_CONFIG on partial config", () => {
233
+ const partial = { globalCss: "custom/global.css" }
234
+ fs.writeFileSync(
235
+ path.join(tmp, CONFIG_FILENAME),
236
+ JSON.stringify(partial),
237
+ "utf8"
238
+ )
239
+
240
+ const loaded = loadConfig(tmp)
241
+ expect(loaded.globalCss).toBe("custom/global.css")
242
+ expect(loaded.componentsUi).toBe(DEFAULT_CONFIG.componentsUi)
243
+ expect(loaded.lib).toBe(DEFAULT_CONFIG.lib)
244
+ })
245
+
246
+ it("returns null for invalid JSON", () => {
247
+ fs.writeFileSync(path.join(tmp, CONFIG_FILENAME), "not json!", "utf8")
248
+ expect(loadConfig(tmp)).toBeNull()
249
+ })
250
+ })
251
+
252
+ // ─── getTailwindConfigContent ───────────────────────────────────────────────
253
+
254
+ describe("getTailwindConfigContent", () => {
255
+ it("generates valid tailwind config with default paths", () => {
256
+ const content = getTailwindConfigContent(DEFAULT_CONFIG)
257
+ expect(content).toContain("module.exports")
258
+ expect(content).toContain('require("nativewind/preset")')
259
+ expect(content).toContain("hsl(var(--primary))")
260
+ expect(content).toContain("hsl(var(--background))")
261
+ expect(content).toContain("var(--radius)")
262
+ expect(content).toContain("./src/**/*.{js,jsx,ts,tsx}")
263
+ })
264
+
265
+ it("does not duplicate paths when componentsUi is under src/", () => {
266
+ const content = getTailwindConfigContent(DEFAULT_CONFIG)
267
+ const matches = content.match(/\.\/src\//g)
268
+ expect(matches).toHaveLength(1)
269
+ expect(content).not.toContain("./src/components/ui/**/*.{js,jsx,ts,tsx}")
270
+ })
271
+
272
+ it("includes custom componentsUi path when outside src/", () => {
273
+ const config = { ...DEFAULT_CONFIG, componentsUi: "app/ui" }
274
+ const content = getTailwindConfigContent(config)
275
+ expect(content).toContain("./app/ui/**/*.{js,jsx,ts,tsx}")
276
+ expect(content).toContain("./src/**/*.{js,jsx,ts,tsx}")
277
+ })
278
+ })
279
+
280
+ // ─── assertValidComponentConfig ─────────────────────────────────────────────
281
+
282
+ describe("assertValidComponentConfig", () => {
283
+ it("accepts a valid config with files", () => {
284
+ expect(() =>
285
+ assertValidComponentConfig("button", {
286
+ files: ["src/components/ui/button.tsx"],
287
+ })
288
+ ).not.toThrow()
289
+ })
290
+
291
+ it("accepts a valid config with files and dependencies", () => {
292
+ expect(() =>
293
+ assertValidComponentConfig("button", {
294
+ files: ["src/components/ui/button.tsx"],
295
+ dependencies: ["class-variance-authority"],
296
+ })
297
+ ).not.toThrow()
298
+ })
299
+
300
+ it("throws for null config", () => {
301
+ expect(() => assertValidComponentConfig("button", null)).toThrow(
302
+ /invalid/i
303
+ )
304
+ })
305
+
306
+ it("throws for missing files array", () => {
307
+ expect(() =>
308
+ assertValidComponentConfig("button", { dependencies: [] })
309
+ ).toThrow(/files/i)
310
+ })
311
+
312
+ it("throws for empty string in files array", () => {
313
+ expect(() =>
314
+ assertValidComponentConfig("button", { files: ["valid.tsx", ""] })
315
+ ).toThrow(/files/i)
316
+ })
317
+
318
+ it("throws for non-string in files array", () => {
319
+ expect(() =>
320
+ assertValidComponentConfig("button", { files: [123] })
321
+ ).toThrow(/files/i)
322
+ })
323
+
324
+ it("throws for invalid dependencies", () => {
325
+ expect(() =>
326
+ assertValidComponentConfig("button", {
327
+ files: ["button.tsx"],
328
+ dependencies: [123],
329
+ })
330
+ ).toThrow(/dependencies/i)
331
+ })
332
+
333
+ it("throws for empty string in dependencies", () => {
334
+ expect(() =>
335
+ assertValidComponentConfig("button", {
336
+ files: ["button.tsx"],
337
+ dependencies: ["valid", ""],
338
+ })
339
+ ).toThrow(/dependencies/i)
340
+ })
341
+ })
342
+
343
+ // ─── GLOBAL_CSS_CONTENT ─────────────────────────────────────────────────────
344
+
345
+ describe("GLOBAL_CSS_CONTENT", () => {
346
+ it("contains tailwind directives", () => {
347
+ expect(GLOBAL_CSS_CONTENT).toContain("@tailwind base")
348
+ expect(GLOBAL_CSS_CONTENT).toContain("@tailwind components")
349
+ expect(GLOBAL_CSS_CONTENT).toContain("@tailwind utilities")
350
+ })
351
+
352
+ it("contains light theme variables", () => {
353
+ expect(GLOBAL_CSS_CONTENT).toContain(":root")
354
+ expect(GLOBAL_CSS_CONTENT).toContain("--background:")
355
+ expect(GLOBAL_CSS_CONTENT).toContain("--primary:")
356
+ expect(GLOBAL_CSS_CONTENT).toContain("--radius:")
357
+ })
358
+
359
+ it("contains dark theme variables", () => {
360
+ expect(GLOBAL_CSS_CONTENT).toContain(".dark")
361
+ })
362
+ })
363
+
364
+ // ─── UTILS_CONTENT ──────────────────────────────────────────────────────────
365
+
366
+ describe("UTILS_CONTENT", () => {
367
+ it("exports the cn function", () => {
368
+ expect(UTILS_CONTENT).toContain("export function cn")
369
+ expect(UTILS_CONTENT).toContain("twMerge")
370
+ expect(UTILS_CONTENT).toContain("clsx")
371
+ })
372
+ })
373
+
374
+ // ─── formatError ────────────────────────────────────────────────────────────
375
+
376
+ describe("formatError", () => {
377
+ it("extracts message from Error objects", () => {
378
+ expect(formatError(new Error("test error"))).toBe("test error")
379
+ })
380
+
381
+ it("converts non-Error values to string", () => {
382
+ expect(formatError("string error")).toBe("string error")
383
+ expect(formatError(42)).toBe("42")
384
+ expect(formatError(null)).toBe("null")
385
+ })
386
+ })
387
+
388
+ // ─── getCliVersion ──────────────────────────────────────────────────────────
389
+
390
+ describe("getCliVersion", () => {
391
+ it("returns a valid semver-like version string", () => {
392
+ const version = getCliVersion()
393
+ expect(version).not.toBe("unknown")
394
+ expect(version).toMatch(/^\d+\.\d+\.\d+/)
395
+ })
396
+ })
397
+
398
+ // ─── getInstallHint ─────────────────────────────────────────────────────────
399
+
400
+ describe("getInstallHint", () => {
401
+ const originalAgent = process.env.npm_config_user_agent
402
+
403
+ afterEach(() => {
404
+ if (originalAgent !== undefined) {
405
+ process.env.npm_config_user_agent = originalAgent
406
+ } else {
407
+ delete process.env.npm_config_user_agent
408
+ }
409
+ })
410
+
411
+ it("generates npm install hint", () => {
412
+ process.env.npm_config_user_agent = ""
413
+ const hint = getInstallHint(["react", "react-native"])
414
+ expect(hint).toBe("npm install react react-native")
415
+ })
416
+
417
+ it("generates yarn add hint", () => {
418
+ process.env.npm_config_user_agent = "yarn/1.22.0"
419
+ const hint = getInstallHint(["react"])
420
+ expect(hint).toBe("yarn add react")
421
+ })
422
+
423
+ it("generates pnpm add hint", () => {
424
+ process.env.npm_config_user_agent = "pnpm/8.0.0"
425
+ const hint = getInstallHint(["clsx", "tailwind-merge"])
426
+ expect(hint).toBe("pnpm add clsx tailwind-merge")
427
+ })
428
+
429
+ it("generates bun add hint", () => {
430
+ process.env.npm_config_user_agent = "bun/1.0.0"
431
+ const hint = getInstallHint(["react"])
432
+ expect(hint).toBe("bun add react")
433
+ })
434
+ })
435
+
436
+ // ─── fetchWithTimeout ───────────────────────────────────────────────────────
437
+
438
+ describe("fetchWithTimeout", () => {
439
+ let originalFetch
440
+
441
+ beforeEach(() => {
442
+ originalFetch = globalThis.fetch
443
+ })
444
+
445
+ afterEach(() => {
446
+ globalThis.fetch = originalFetch
447
+ })
448
+
449
+ it("returns response on successful fetch", async () => {
450
+ globalThis.fetch = vi.fn(async () => ({
451
+ ok: true,
452
+ json: async () => ({ data: "test" }),
453
+ }))
454
+
455
+ const response = await fetchWithTimeout("https://example.com/data.json")
456
+ expect(response.ok).toBe(true)
457
+ expect(await response.json()).toEqual({ data: "test" })
458
+ })
459
+
460
+ it("passes the abort signal to fetch", async () => {
461
+ globalThis.fetch = vi.fn(async (url, options) => {
462
+ expect(options).toHaveProperty("signal")
463
+ expect(options.signal).toBeInstanceOf(AbortSignal)
464
+ return { ok: true }
465
+ })
466
+
467
+ await fetchWithTimeout("https://example.com/data.json")
468
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1)
469
+ })
470
+
471
+ it("throws descriptive error on abort/timeout", async () => {
472
+ globalThis.fetch = vi.fn(async (url, options) => {
473
+ const err = new Error("The operation was aborted")
474
+ err.name = "AbortError"
475
+ throw err
476
+ })
477
+
478
+ await expect(fetchWithTimeout("https://example.com/slow")).rejects.toThrow(
479
+ /timed out/i
480
+ )
481
+ })
482
+
483
+ it("re-throws non-abort errors as-is", async () => {
484
+ globalThis.fetch = vi.fn(async () => {
485
+ throw new TypeError("fetch failed")
486
+ })
487
+
488
+ await expect(
489
+ fetchWithTimeout("https://example.com/fail")
490
+ ).rejects.toThrow("fetch failed")
491
+ })
492
+ })
493
+
494
+ // ─── Exports smoke test ─────────────────────────────────────────────────────
495
+
496
+ describe("exports", () => {
497
+ it("exports all expected constants", () => {
498
+ expect(typeof DEFAULT_CONFIG).toBe("object")
499
+ expect(typeof CONFIG_FILENAME).toBe("string")
500
+ expect(typeof GLOBAL_CSS_CONTENT).toBe("string")
501
+ expect(typeof UTILS_CONTENT).toBe("string")
502
+ })
503
+
504
+ it("exports all expected functions", () => {
505
+ expect(typeof getTailwindConfigContent).toBe("function")
506
+ expect(typeof detectPackageManager).toBe("function")
507
+ expect(typeof ensureDir).toBe("function")
508
+ expect(typeof writeIfNotExists).toBe("function")
509
+ expect(typeof getMissingDeps).toBe("function")
510
+ expect(typeof loadConfig).toBe("function")
511
+ expect(typeof writeConfig).toBe("function")
512
+ expect(typeof assertValidComponentConfig).toBe("function")
513
+ expect(typeof fetchWithTimeout).toBe("function")
514
+ expect(typeof formatError).toBe("function")
515
+ expect(typeof getCliVersion).toBe("function")
516
+ expect(typeof getInstallHint).toBe("function")
517
+ expect(typeof init).toBe("function")
518
+ expect(typeof add).toBe("function")
519
+ })
520
+ })