ui-thing 0.2.9 → 0.3.0
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/.claude/settings.local.json +5 -0
- package/.prettierrc +1 -1
- package/CHANGELOG.md +32 -0
- package/bun.lock +171 -134
- package/package.json +15 -14
- package/src/commands/add.ts +9 -5
- package/src/commands/block.ts +5 -3
- package/src/commands/list.ts +48 -0
- package/src/commands/prose.ts +7 -3
- package/src/commands/remove.ts +109 -0
- package/src/commands/theme.ts +5 -14
- package/src/commands/update.ts +81 -0
- package/src/index.ts +6 -0
- package/src/utils/config.ts +9 -16
- package/src/utils/constants.ts +1 -0
- package/src/utils/fetchBlockCategories.ts +13 -11
- package/src/utils/fetchBlocks.ts +13 -11
- package/src/utils/fetchComponents.ts +13 -11
- package/src/utils/fetchProseComponents.ts +13 -11
- package/src/utils/installPackages.ts +30 -4
- package/src/utils/installValidator.ts +3 -3
- package/src/utils/logger.ts +9 -0
- package/src/utils/uiConfigPrompt.ts +3 -6
- package/tests/commands/list.test.ts +111 -0
- package/tests/commands/remove.test.ts +151 -0
- package/tests/commands/update.test.ts +127 -0
- package/tests/utils/fetchBlockCategories.test.ts +14 -2
- package/tests/utils/fetchBlocks.test.ts +14 -2
- package/tests/utils/fetchComponents.test.ts +10 -4
- package/tests/utils/fetchProseComponents.test.ts +14 -2
- package/tests/utils/installPackages.test.ts +36 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1348
- package/dist/index.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
|
+
import { consola } from "consola";
|
|
2
3
|
import dotenv from "dotenv";
|
|
3
4
|
import ora from "ora";
|
|
4
5
|
|
|
@@ -6,16 +7,17 @@ import { ProseComponent } from "../types";
|
|
|
6
7
|
|
|
7
8
|
dotenv.config();
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
* Fetch prose components from UI Thing API.
|
|
11
|
-
*/
|
|
12
|
-
export const fetchProseComponents = async () => {
|
|
10
|
+
export const fetchProseComponents = async (): Promise<ProseComponent[]> => {
|
|
13
11
|
const spinner = ora("Fetching prose components...").start();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
try {
|
|
13
|
+
const { data } = await axios.get<ProseComponent[]>(
|
|
14
|
+
process.env.PROSE_COMPONENTS_API || "https://uithing.com/api/prose"
|
|
15
|
+
);
|
|
16
|
+
spinner.succeed("Prose components fetched.");
|
|
17
|
+
return data;
|
|
18
|
+
} catch {
|
|
19
|
+
spinner.fail("Failed to fetch prose components.");
|
|
20
|
+
consola.error("Could not reach the UI Thing API. Check your network connection.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
21
23
|
};
|
|
@@ -2,6 +2,26 @@ import { execa } from "execa";
|
|
|
2
2
|
import _ from "lodash";
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
|
|
5
|
+
const PM_ADD_CMD: Record<string, string> = {
|
|
6
|
+
yarn: "add",
|
|
7
|
+
bun: "add",
|
|
8
|
+
pnpm: "add",
|
|
9
|
+
npm: "install",
|
|
10
|
+
deno: "add",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const PM_DEV_FLAG: Record<string, string> = {
|
|
14
|
+
yarn: "-D",
|
|
15
|
+
bun: "-D",
|
|
16
|
+
pnpm: "-D",
|
|
17
|
+
npm: "-D",
|
|
18
|
+
deno: "--dev",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Deno 2.x requires `npm:` prefix for npm packages
|
|
22
|
+
const formatPkgs = (pm: string, pkgs: string[]) =>
|
|
23
|
+
pm === "deno" ? pkgs.map((p) => (p.startsWith("npm:") ? p : `npm:${p}`)) : pkgs;
|
|
24
|
+
|
|
5
25
|
export const installPackages = async (
|
|
6
26
|
packageManager: string,
|
|
7
27
|
deps?: string[] | string,
|
|
@@ -14,18 +34,24 @@ export const installPackages = async (
|
|
|
14
34
|
devDeps = [devDeps];
|
|
15
35
|
}
|
|
16
36
|
|
|
37
|
+
const addCmd = PM_ADD_CMD[packageManager] ?? "install";
|
|
38
|
+
const devFlag = PM_DEV_FLAG[packageManager] ?? "-D";
|
|
39
|
+
|
|
17
40
|
const depsSpinner = ora("Installing dependencies...").start();
|
|
18
41
|
if (!_.isUndefined(deps) && !_.isEmpty(deps)) {
|
|
19
|
-
await execa(packageManager, [packageManager
|
|
42
|
+
await execa(packageManager, [addCmd, ...formatPkgs(packageManager, deps)]);
|
|
20
43
|
}
|
|
21
44
|
depsSpinner.text = "Installing dev dependencies...";
|
|
22
45
|
if (!_.isUndefined(devDeps) && !_.isEmpty(devDeps)) {
|
|
23
|
-
await execa(packageManager, [
|
|
46
|
+
await execa(packageManager, [addCmd, devFlag, ...formatPkgs(packageManager, devDeps)]);
|
|
24
47
|
}
|
|
25
48
|
|
|
26
|
-
// we should check to see if there is a postinstall script and run it
|
|
27
49
|
depsSpinner.text = "Running nuxt prepare...";
|
|
28
|
-
|
|
50
|
+
if (packageManager === "deno") {
|
|
51
|
+
await execa("deno", ["run", "-A", "npm:nuxt", "prepare"]);
|
|
52
|
+
} else {
|
|
53
|
+
await execa`npx -y nuxt prepare`;
|
|
54
|
+
}
|
|
29
55
|
|
|
30
56
|
depsSpinner.succeed("Installed dependencies!");
|
|
31
57
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import prompts from "prompts";
|
|
2
2
|
|
|
3
3
|
import { installPackages } from "./installPackages";
|
|
4
|
+
import { logger } from "./logger";
|
|
4
5
|
|
|
5
6
|
export const installValidator = async (packageManager: string) => {
|
|
6
|
-
// Depending on the selected validator, install the corresponding packages
|
|
7
7
|
const validatorPackages = {
|
|
8
8
|
yup: ["yup", "@vee-validate/yup"],
|
|
9
9
|
zod: ["zod", "@vee-validate/zod"],
|
|
@@ -23,10 +23,10 @@ export const installValidator = async (packageManager: string) => {
|
|
|
23
23
|
],
|
|
24
24
|
});
|
|
25
25
|
if (!validator) {
|
|
26
|
-
|
|
26
|
+
logger.warn("No validator package selected");
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
logger.info(`Selected ${validator} as the validator package`);
|
|
30
30
|
|
|
31
31
|
if (validatorPackages[validator]) {
|
|
32
32
|
await installPackages(packageManager, validatorPackages[validator]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { consola } from "consola";
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
info: (msg: string, ...args: unknown[]) => consola.info(msg, ...args),
|
|
5
|
+
error: (msg: string, ...args: unknown[]) => consola.error(msg, ...args),
|
|
6
|
+
warn: (msg: string, ...args: unknown[]) => consola.warn(msg, ...args),
|
|
7
|
+
success: (msg: string, ...args: unknown[]) => consola.success(msg, ...args),
|
|
8
|
+
log: (msg: string, ...args: unknown[]) => consola.log(msg, ...args),
|
|
9
|
+
};
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
-
import kleur from "kleur";
|
|
3
2
|
import prompts from "prompts";
|
|
4
3
|
|
|
5
4
|
import { CSS_THEME_OPTIONS, PACKAGE_MANAGER_CHOICES } from "./constants";
|
|
5
|
+
import { logger } from "./logger";
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Prompts the user for UI configuration values.
|
|
9
|
-
*/
|
|
10
7
|
export const initPrompts = async (nuxtVersion: number) => {
|
|
11
8
|
const response = await prompts([
|
|
12
9
|
{
|
|
@@ -67,8 +64,8 @@ export const initPrompts = async (nuxtVersion: number) => {
|
|
|
67
64
|
]);
|
|
68
65
|
|
|
69
66
|
if (!response || Object.keys(response).length < 9) {
|
|
70
|
-
|
|
71
|
-
return process.exit(
|
|
67
|
+
logger.error("Incomplete configuration submitted. Exiting...");
|
|
68
|
+
return process.exit(1);
|
|
72
69
|
}
|
|
73
70
|
return response;
|
|
74
71
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { runListCommand } from "../../src/commands/list";
|
|
4
|
+
import { Component } from "../../src/types";
|
|
5
|
+
|
|
6
|
+
vi.mock("../../src/utils/fetchComponents");
|
|
7
|
+
vi.mock("../../src/utils/config");
|
|
8
|
+
vi.mock("../../src/utils/fileExists");
|
|
9
|
+
vi.mock("../../src/utils/logger");
|
|
10
|
+
|
|
11
|
+
const mockComponents: Component[] = [
|
|
12
|
+
{
|
|
13
|
+
name: "Button",
|
|
14
|
+
value: "button",
|
|
15
|
+
files: [{ fileName: "Button.vue", dirPath: "app/components/Ui", fileContent: "" }],
|
|
16
|
+
utils: [],
|
|
17
|
+
composables: [],
|
|
18
|
+
plugins: [],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "Input",
|
|
22
|
+
value: "input",
|
|
23
|
+
files: [{ fileName: "Input.vue", dirPath: "app/components/Ui", fileContent: "" }],
|
|
24
|
+
utils: [],
|
|
25
|
+
composables: [],
|
|
26
|
+
plugins: [],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "Card",
|
|
30
|
+
value: "card",
|
|
31
|
+
files: [{ fileName: "Card/Card.vue", dirPath: "app/components/Ui", fileContent: "" }],
|
|
32
|
+
utils: [],
|
|
33
|
+
composables: [],
|
|
34
|
+
plugins: [],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
describe("commands/list", () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.clearAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.restoreAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should list all components when --installed is not passed", async () => {
|
|
48
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
49
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
50
|
+
const { logger } = await import("../../src/utils/logger");
|
|
51
|
+
|
|
52
|
+
vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
|
|
53
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
54
|
+
|
|
55
|
+
await runListCommand({ installed: false });
|
|
56
|
+
|
|
57
|
+
expect(vi.mocked(logger.log)).toHaveBeenCalledTimes(3);
|
|
58
|
+
expect(vi.mocked(logger.success)).toHaveBeenCalledWith("3 component(s) found.");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should filter to installed components when --installed is passed", async () => {
|
|
62
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
63
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
64
|
+
const { fileExists } = await import("../../src/utils/fileExists");
|
|
65
|
+
const { logger } = await import("../../src/utils/logger");
|
|
66
|
+
|
|
67
|
+
vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
|
|
68
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
69
|
+
// Only Button is "installed"
|
|
70
|
+
vi.mocked(fileExists)
|
|
71
|
+
.mockResolvedValueOnce(true)
|
|
72
|
+
.mockResolvedValueOnce(false)
|
|
73
|
+
.mockResolvedValueOnce(false);
|
|
74
|
+
|
|
75
|
+
await runListCommand({ installed: true });
|
|
76
|
+
|
|
77
|
+
expect(vi.mocked(logger.log)).toHaveBeenCalledTimes(1);
|
|
78
|
+
expect(vi.mocked(logger.log)).toHaveBeenCalledWith("Button (button)");
|
|
79
|
+
expect(vi.mocked(logger.success)).toHaveBeenCalledWith("1 component(s) found.");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should show 'No components installed.' when --installed and none found", async () => {
|
|
83
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
84
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
85
|
+
const { fileExists } = await import("../../src/utils/fileExists");
|
|
86
|
+
const { logger } = await import("../../src/utils/logger");
|
|
87
|
+
|
|
88
|
+
vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
|
|
89
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
90
|
+
vi.mocked(fileExists).mockResolvedValue(false);
|
|
91
|
+
|
|
92
|
+
await runListCommand({ installed: true });
|
|
93
|
+
|
|
94
|
+
expect(vi.mocked(logger.info)).toHaveBeenCalledWith("No components installed.");
|
|
95
|
+
expect(vi.mocked(logger.log)).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should show 'No components available.' when API returns empty array", async () => {
|
|
99
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
100
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
101
|
+
const { logger } = await import("../../src/utils/logger");
|
|
102
|
+
|
|
103
|
+
vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
|
|
104
|
+
vi.mocked(fetchComponents).mockResolvedValue([]);
|
|
105
|
+
|
|
106
|
+
await runListCommand({ installed: false });
|
|
107
|
+
|
|
108
|
+
expect(vi.mocked(logger.info)).toHaveBeenCalledWith("No components available.");
|
|
109
|
+
expect(vi.mocked(logger.log)).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { runRemoveCommand } from "../../src/commands/remove";
|
|
5
|
+
import { Component } from "../../src/types";
|
|
6
|
+
|
|
7
|
+
vi.mock("../../src/utils/fetchComponents");
|
|
8
|
+
vi.mock("../../src/utils/config");
|
|
9
|
+
vi.mock("../../src/utils/fileExists");
|
|
10
|
+
vi.mock("../../src/utils/logger");
|
|
11
|
+
vi.mock("../../src/utils/printFancyBoxMessage");
|
|
12
|
+
vi.mock("prompts");
|
|
13
|
+
vi.mock("node:fs/promises");
|
|
14
|
+
|
|
15
|
+
const mockComponents: Component[] = [
|
|
16
|
+
{
|
|
17
|
+
name: "Button",
|
|
18
|
+
value: "button",
|
|
19
|
+
files: [{ fileName: "Button.vue", dirPath: "app/components/Ui", fileContent: "" }],
|
|
20
|
+
utils: [],
|
|
21
|
+
composables: [],
|
|
22
|
+
plugins: [],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "Input",
|
|
26
|
+
value: "input",
|
|
27
|
+
files: [{ fileName: "Input.vue", dirPath: "app/components/Ui", fileContent: "" }],
|
|
28
|
+
utils: [],
|
|
29
|
+
composables: [],
|
|
30
|
+
plugins: [],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
describe("commands/remove", () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
vi.restoreAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should remove component files that exist", async () => {
|
|
44
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
45
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
46
|
+
const { fileExists } = await import("../../src/utils/fileExists");
|
|
47
|
+
const { logger } = await import("../../src/utils/logger");
|
|
48
|
+
const prompts = await import("prompts");
|
|
49
|
+
|
|
50
|
+
vi.mocked(getUIConfig).mockResolvedValue({
|
|
51
|
+
componentsLocation: "app/components/Ui",
|
|
52
|
+
utilsLocation: "app/utils",
|
|
53
|
+
composablesLocation: "app/composables",
|
|
54
|
+
} as any);
|
|
55
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
56
|
+
vi.mocked(fileExists).mockResolvedValue(true);
|
|
57
|
+
vi.mocked(prompts.default).mockResolvedValue({ confirmed: true });
|
|
58
|
+
vi.mocked(fs.unlink).mockResolvedValue(undefined);
|
|
59
|
+
|
|
60
|
+
await runRemoveCommand(["button"]);
|
|
61
|
+
|
|
62
|
+
expect(vi.mocked(fs.unlink)).toHaveBeenCalledWith(expect.stringContaining("Button.vue"));
|
|
63
|
+
expect(vi.mocked(logger.success)).toHaveBeenCalledWith("Removed: Button.vue");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should skip files that do not exist and warn", async () => {
|
|
67
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
68
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
69
|
+
const { fileExists } = await import("../../src/utils/fileExists");
|
|
70
|
+
const { logger } = await import("../../src/utils/logger");
|
|
71
|
+
const prompts = await import("prompts");
|
|
72
|
+
|
|
73
|
+
vi.mocked(getUIConfig).mockResolvedValue({
|
|
74
|
+
componentsLocation: "app/components/Ui",
|
|
75
|
+
utilsLocation: "app/utils",
|
|
76
|
+
composablesLocation: "app/composables",
|
|
77
|
+
} as any);
|
|
78
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
79
|
+
vi.mocked(fileExists).mockResolvedValue(false);
|
|
80
|
+
vi.mocked(prompts.default).mockResolvedValue({ confirmed: true });
|
|
81
|
+
|
|
82
|
+
await runRemoveCommand(["button"]);
|
|
83
|
+
|
|
84
|
+
expect(vi.mocked(fs.unlink)).not.toHaveBeenCalled();
|
|
85
|
+
expect(vi.mocked(logger.warn)).toHaveBeenCalledWith(expect.stringContaining("Button.vue"));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should cancel when user declines confirmation", async () => {
|
|
89
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
90
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
91
|
+
const { fileExists } = await import("../../src/utils/fileExists");
|
|
92
|
+
const { logger } = await import("../../src/utils/logger");
|
|
93
|
+
const prompts = await import("prompts");
|
|
94
|
+
|
|
95
|
+
vi.mocked(getUIConfig).mockResolvedValue({
|
|
96
|
+
componentsLocation: "app/components/Ui",
|
|
97
|
+
utilsLocation: "app/utils",
|
|
98
|
+
composablesLocation: "app/composables",
|
|
99
|
+
} as any);
|
|
100
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
101
|
+
vi.mocked(fileExists).mockResolvedValue(true);
|
|
102
|
+
vi.mocked(prompts.default).mockResolvedValue({ confirmed: false });
|
|
103
|
+
|
|
104
|
+
await runRemoveCommand(["button"]);
|
|
105
|
+
|
|
106
|
+
expect(vi.mocked(fs.unlink)).not.toHaveBeenCalled();
|
|
107
|
+
expect(vi.mocked(logger.info)).toHaveBeenCalledWith("Cancelled.");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should exit with error when no specified names are found in registry", async () => {
|
|
111
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
112
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
113
|
+
const { logger } = await import("../../src/utils/logger");
|
|
114
|
+
|
|
115
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
116
|
+
|
|
117
|
+
vi.mocked(getUIConfig).mockResolvedValue({
|
|
118
|
+
componentsLocation: "app/components/Ui",
|
|
119
|
+
utilsLocation: "app/utils",
|
|
120
|
+
composablesLocation: "app/composables",
|
|
121
|
+
} as any);
|
|
122
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
123
|
+
|
|
124
|
+
await runRemoveCommand(["nonexistent"]);
|
|
125
|
+
|
|
126
|
+
expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
|
|
127
|
+
"None of the specified components were found in the registry."
|
|
128
|
+
);
|
|
129
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should show info and return when no components are installed", async () => {
|
|
133
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
134
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
135
|
+
const { fileExists } = await import("../../src/utils/fileExists");
|
|
136
|
+
const { logger } = await import("../../src/utils/logger");
|
|
137
|
+
|
|
138
|
+
vi.mocked(getUIConfig).mockResolvedValue({
|
|
139
|
+
componentsLocation: "app/components/Ui",
|
|
140
|
+
utilsLocation: "app/utils",
|
|
141
|
+
composablesLocation: "app/composables",
|
|
142
|
+
} as any);
|
|
143
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
144
|
+
vi.mocked(fileExists).mockResolvedValue(false);
|
|
145
|
+
|
|
146
|
+
await runRemoveCommand([]);
|
|
147
|
+
|
|
148
|
+
expect(vi.mocked(logger.info)).toHaveBeenCalledWith("No installed components found.");
|
|
149
|
+
expect(vi.mocked(fs.unlink)).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { runUpdateCommand } from "../../src/commands/update";
|
|
4
|
+
import { Component } from "../../src/types";
|
|
5
|
+
|
|
6
|
+
vi.mock("../../src/utils/fetchComponents");
|
|
7
|
+
vi.mock("../../src/utils/config");
|
|
8
|
+
vi.mock("../../src/utils/fileExists");
|
|
9
|
+
vi.mock("../../src/utils/writeFile");
|
|
10
|
+
vi.mock("../../src/utils/logger");
|
|
11
|
+
vi.mock("../../src/utils/printFancyBoxMessage");
|
|
12
|
+
vi.mock("prompts");
|
|
13
|
+
|
|
14
|
+
const mockComponents: Component[] = [
|
|
15
|
+
{
|
|
16
|
+
name: "Button",
|
|
17
|
+
value: "button",
|
|
18
|
+
files: [{ fileName: "Button.vue", dirPath: "app/components/Ui", fileContent: "<button />" }],
|
|
19
|
+
utils: [],
|
|
20
|
+
composables: [],
|
|
21
|
+
plugins: [],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "Input",
|
|
25
|
+
value: "input",
|
|
26
|
+
files: [{ fileName: "Input.vue", dirPath: "app/components/Ui", fileContent: "<input />" }],
|
|
27
|
+
utils: [],
|
|
28
|
+
composables: [],
|
|
29
|
+
plugins: [],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
describe("commands/update", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should update a named component and overwrite its files", async () => {
|
|
43
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
44
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
45
|
+
const { writeFile } = await import("../../src/utils/writeFile");
|
|
46
|
+
const { logger } = await import("../../src/utils/logger");
|
|
47
|
+
|
|
48
|
+
vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
|
|
49
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
50
|
+
|
|
51
|
+
await runUpdateCommand(["button"]);
|
|
52
|
+
|
|
53
|
+
expect(vi.mocked(writeFile)).toHaveBeenCalledWith(
|
|
54
|
+
expect.stringContaining("Button.vue"),
|
|
55
|
+
"<button />"
|
|
56
|
+
);
|
|
57
|
+
expect(vi.mocked(logger.success)).toHaveBeenCalledWith("Updated: Button.vue");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should warn for unknown component names", async () => {
|
|
61
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
62
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
63
|
+
const { logger } = await import("../../src/utils/logger");
|
|
64
|
+
|
|
65
|
+
vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
|
|
66
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
67
|
+
|
|
68
|
+
await runUpdateCommand(["nonexistent"]);
|
|
69
|
+
|
|
70
|
+
expect(vi.mocked(logger.warn)).toHaveBeenCalledWith(expect.stringContaining("nonexistent"));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should prompt for selection when no names given and components are installed", async () => {
|
|
74
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
75
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
76
|
+
const { fileExists } = await import("../../src/utils/fileExists");
|
|
77
|
+
const { writeFile } = await import("../../src/utils/writeFile");
|
|
78
|
+
const prompts = await import("prompts");
|
|
79
|
+
|
|
80
|
+
vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
|
|
81
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
82
|
+
vi.mocked(fileExists).mockResolvedValueOnce(true).mockResolvedValueOnce(false);
|
|
83
|
+
vi.mocked(prompts.default).mockResolvedValue({ selected: ["button"] });
|
|
84
|
+
|
|
85
|
+
await runUpdateCommand([]);
|
|
86
|
+
|
|
87
|
+
expect(vi.mocked(writeFile)).toHaveBeenCalledWith(
|
|
88
|
+
expect.stringContaining("Button.vue"),
|
|
89
|
+
"<button />"
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should exit cleanly when user cancels the prompt", async () => {
|
|
94
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
95
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
96
|
+
const { fileExists } = await import("../../src/utils/fileExists");
|
|
97
|
+
const { writeFile } = await import("../../src/utils/writeFile");
|
|
98
|
+
const prompts = await import("prompts");
|
|
99
|
+
|
|
100
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
101
|
+
|
|
102
|
+
vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
|
|
103
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
104
|
+
vi.mocked(fileExists).mockResolvedValue(true);
|
|
105
|
+
vi.mocked(prompts.default).mockResolvedValue({ selected: [] });
|
|
106
|
+
|
|
107
|
+
await runUpdateCommand([]);
|
|
108
|
+
|
|
109
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
110
|
+
expect(vi.mocked(writeFile)).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should show info message when no installed components found", async () => {
|
|
114
|
+
const { fetchComponents } = await import("../../src/utils/fetchComponents");
|
|
115
|
+
const { getUIConfig } = await import("../../src/utils/config");
|
|
116
|
+
const { fileExists } = await import("../../src/utils/fileExists");
|
|
117
|
+
const { logger } = await import("../../src/utils/logger");
|
|
118
|
+
|
|
119
|
+
vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
|
|
120
|
+
vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
|
|
121
|
+
vi.mocked(fileExists).mockResolvedValue(false);
|
|
122
|
+
|
|
123
|
+
await runUpdateCommand([]);
|
|
124
|
+
|
|
125
|
+
expect(vi.mocked(logger.info)).toHaveBeenCalledWith("No installed components found.");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -42,10 +42,22 @@ describe("utils/fetchBlockCategories", () => {
|
|
|
42
42
|
delete process.env.BLOCK_CATEGORIES_API;
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
it("should
|
|
45
|
+
it("should call process.exit(1) on API error", async () => {
|
|
46
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
46
47
|
mockAxios.onGet("https://uithing.com/api/blocks/categories").reply(404);
|
|
47
48
|
|
|
48
|
-
await
|
|
49
|
+
await fetchBlockCategories();
|
|
50
|
+
|
|
51
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should call process.exit(1) on network error", async () => {
|
|
55
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
56
|
+
mockAxios.onGet("https://uithing.com/api/blocks/categories").networkError();
|
|
57
|
+
|
|
58
|
+
await fetchBlockCategories();
|
|
59
|
+
|
|
60
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
49
61
|
});
|
|
50
62
|
|
|
51
63
|
it("should return empty array when no categories exist", async () => {
|
|
@@ -51,9 +51,21 @@ describe("utils/fetchBlocks", () => {
|
|
|
51
51
|
delete process.env.BLOCKS_API;
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it("should
|
|
54
|
+
it("should call process.exit(1) on API error", async () => {
|
|
55
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
55
56
|
mockAxios.onGet("https://uithing.com/api/blocks").reply(500);
|
|
56
57
|
|
|
57
|
-
await
|
|
58
|
+
await fetchBlocks();
|
|
59
|
+
|
|
60
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should call process.exit(1) on network error", async () => {
|
|
64
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
65
|
+
mockAxios.onGet("https://uithing.com/api/blocks").networkError();
|
|
66
|
+
|
|
67
|
+
await fetchBlocks();
|
|
68
|
+
|
|
69
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
58
70
|
});
|
|
59
71
|
});
|
|
@@ -69,16 +69,22 @@ describe("utils/fetchComponents", () => {
|
|
|
69
69
|
delete process.env.COMPONENTS_API;
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it("should
|
|
72
|
+
it("should call process.exit(1) on API error", async () => {
|
|
73
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
73
74
|
mockAxios.onGet("https://uithing.com/api/components").reply(500);
|
|
74
75
|
|
|
75
|
-
await
|
|
76
|
+
await fetchComponents();
|
|
77
|
+
|
|
78
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
76
79
|
});
|
|
77
80
|
|
|
78
|
-
it("should
|
|
81
|
+
it("should call process.exit(1) on network error", async () => {
|
|
82
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
79
83
|
mockAxios.onGet("https://uithing.com/api/components").networkError();
|
|
80
84
|
|
|
81
|
-
await
|
|
85
|
+
await fetchComponents();
|
|
86
|
+
|
|
87
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
82
88
|
});
|
|
83
89
|
|
|
84
90
|
it("should return empty array when API returns no data", async () => {
|
|
@@ -54,9 +54,21 @@ describe("utils/fetchProseComponents", () => {
|
|
|
54
54
|
delete process.env.PROSE_COMPONENTS_API;
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it("should
|
|
57
|
+
it("should call process.exit(1) on API error", async () => {
|
|
58
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
58
59
|
mockAxios.onGet("https://uithing.com/api/prose").reply(500);
|
|
59
60
|
|
|
60
|
-
await
|
|
61
|
+
await fetchProseComponents();
|
|
62
|
+
|
|
63
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should call process.exit(1) on network error", async () => {
|
|
67
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
68
|
+
mockAxios.onGet("https://uithing.com/api/prose").networkError();
|
|
69
|
+
|
|
70
|
+
await fetchProseComponents();
|
|
71
|
+
|
|
72
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
61
73
|
});
|
|
62
74
|
});
|