vueless 1.0.2-beta.58 → 1.0.2-beta.59
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/bin/commands/init.js +64 -5
- package/bin/constants.js +27 -17
- package/composables/tests/useUI.test.ts +494 -0
- package/constants.js +1 -0
- package/package.json +1 -1
- package/plugin-vite.js +5 -0
- package/utils/node/helper.js +64 -0
package/bin/commands/init.js
CHANGED
|
@@ -2,15 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
import { cwd } from "node:process";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
6
|
-
import { writeFile, rename } from "node:fs/promises";
|
|
5
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { writeFile, rename, readFile } from "node:fs/promises";
|
|
7
7
|
import { styleText } from "node:util";
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
SUPPRESS_TS_CHECK,
|
|
11
|
+
COMPONENTS_INDEX_EXPORT,
|
|
12
|
+
COMPONENTS_INDEX_COMMENT,
|
|
13
|
+
DEFAULT_VUELESS_CONFIG_CONTENT,
|
|
14
|
+
} from "../constants.js";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
JAVASCRIPT_EXT,
|
|
18
|
+
TYPESCRIPT_EXT,
|
|
19
|
+
CONFIG_INDEX_FILE_NAME,
|
|
20
|
+
VUELESS_CONFIG_FILE_NAME,
|
|
21
|
+
} from "../../constants.js";
|
|
11
22
|
|
|
12
23
|
const vuelessInitOptions = ["--ts", "--js"];
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Initializes Vueless in the project by creating a default config file and .vueless directory.
|
|
27
|
+
* @param {string[]} options - The function options.
|
|
28
|
+
* @param {boolean} options.includes("--ts") - If true, creates a TypeScript config file.
|
|
29
|
+
* @param {boolean} options.includes("--js") - If true, creates a JavaScript config file.
|
|
30
|
+
*/
|
|
14
31
|
export async function vuelessInit(options) {
|
|
15
32
|
const isValidOptions = options.every((option) => vuelessInitOptions.includes(option));
|
|
16
33
|
|
|
@@ -20,13 +37,16 @@ export async function vuelessInit(options) {
|
|
|
20
37
|
return;
|
|
21
38
|
}
|
|
22
39
|
|
|
23
|
-
const
|
|
40
|
+
const hasTypeScript = await detectTypeScript();
|
|
41
|
+
const fileExt = options.includes("--ts") || hasTypeScript ? TYPESCRIPT_EXT : JAVASCRIPT_EXT;
|
|
42
|
+
|
|
24
43
|
const formattedDestPath = path.format({
|
|
25
44
|
dir: cwd(),
|
|
26
45
|
name: VUELESS_CONFIG_FILE_NAME,
|
|
27
46
|
ext: fileExt,
|
|
28
47
|
});
|
|
29
48
|
|
|
49
|
+
/* Backup existing config if it exists. */
|
|
30
50
|
if (existsSync(formattedDestPath)) {
|
|
31
51
|
const timestamp = new Date().valueOf();
|
|
32
52
|
const renamedTarget = `${VUELESS_CONFIG_FILE_NAME}-backup-${timestamp}${fileExt}`;
|
|
@@ -42,6 +62,7 @@ export async function vuelessInit(options) {
|
|
|
42
62
|
);
|
|
43
63
|
}
|
|
44
64
|
|
|
65
|
+
/* Create a default config file. */
|
|
45
66
|
await writeFile(formattedDestPath, DEFAULT_VUELESS_CONFIG_CONTENT, "utf-8");
|
|
46
67
|
|
|
47
68
|
console.log(
|
|
@@ -50,4 +71,42 @@ export async function vuelessInit(options) {
|
|
|
50
71
|
`The '${formattedDestPath.split(path.sep).at(-1)}' was created in the project root directory.`,
|
|
51
72
|
),
|
|
52
73
|
);
|
|
74
|
+
|
|
75
|
+
/* Create .vueless directory and index file. */
|
|
76
|
+
const vuelessDir = path.join(cwd(), ".vueless");
|
|
77
|
+
const destPath = path.join(vuelessDir, `${CONFIG_INDEX_FILE_NAME}${fileExt}`);
|
|
78
|
+
|
|
79
|
+
if (!existsSync(vuelessDir)) {
|
|
80
|
+
mkdirSync(vuelessDir);
|
|
81
|
+
console.log(
|
|
82
|
+
styleText("green", "The '.vueless' directory was created in the project root directory."),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const suppressTsCheck = fileExt === TYPESCRIPT_EXT ? `${SUPPRESS_TS_CHECK}\n` : "";
|
|
87
|
+
|
|
88
|
+
await writeFile(
|
|
89
|
+
destPath,
|
|
90
|
+
`${suppressTsCheck}${COMPONENTS_INDEX_COMMENT}\n${COMPONENTS_INDEX_EXPORT}\n`,
|
|
91
|
+
"utf-8",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Detects if TypeScript is a dependency in the project's package.json
|
|
97
|
+
* @returns {Promise<boolean>} True if TypeScript is found in dependencies or devDependencies
|
|
98
|
+
*/
|
|
99
|
+
async function detectTypeScript() {
|
|
100
|
+
try {
|
|
101
|
+
const packageJsonPath = path.join(cwd(), "package.json");
|
|
102
|
+
const packageJsonContent = await readFile(packageJsonPath, "utf-8");
|
|
103
|
+
const pkg = JSON.parse(packageJsonContent);
|
|
104
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
105
|
+
|
|
106
|
+
return Boolean(deps.typescript);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error("Failed to detect TypeScript:", error);
|
|
109
|
+
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
53
112
|
}
|
package/bin/constants.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export const SRC_COMPONENTS_PATH = "/src/components";
|
|
2
2
|
export const COMPONENTS_PATH = "/components";
|
|
3
3
|
|
|
4
|
-
export const DEFAULT_VUELESS_CONFIG_CONTENT = `
|
|
4
|
+
export const DEFAULT_VUELESS_CONFIG_CONTENT = `import { componentConfigs } from "./.vueless";
|
|
5
|
+
|
|
5
6
|
export default {
|
|
6
7
|
/**
|
|
7
8
|
* Global settings.
|
|
@@ -14,6 +15,18 @@ export default {
|
|
|
14
15
|
disabledOpacity: 50,
|
|
15
16
|
colorMode: "auto",
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Component settings.
|
|
20
|
+
*/
|
|
21
|
+
components: /*tw*/ {
|
|
22
|
+
...componentConfigs,
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Directive settings.
|
|
27
|
+
*/
|
|
28
|
+
directives: {},
|
|
29
|
+
|
|
17
30
|
/**
|
|
18
31
|
* Light theme CSS variable settings.
|
|
19
32
|
*/
|
|
@@ -153,22 +166,19 @@ export default {
|
|
|
153
166
|
"--vl-bg-accented": "--vl-neutral-700",
|
|
154
167
|
"--vl-bg-inverted": "--vl-neutral-100",
|
|
155
168
|
},
|
|
169
|
+
};
|
|
170
|
+
`;
|
|
156
171
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
*/
|
|
160
|
-
directives: {},
|
|
172
|
+
export const SUPPRESS_TS_CHECK = `// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
173
|
+
// @ts-nocheck`;
|
|
161
174
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
175
|
+
export const COMPONENTS_INDEX_COMMENT = `/**
|
|
176
|
+
* ⚠️ This file is auto-generated — do not edit it manually.
|
|
177
|
+
* It gets updated automatically whenever the Vite server restarts.
|
|
178
|
+
*
|
|
179
|
+
* This file imports all component config files from the current directory.
|
|
180
|
+
* Only files following the naming pattern "U[Component].config.[ts|js]" will be included.
|
|
181
|
+
* Example: "UButton.config.ts"
|
|
182
|
+
*/`;
|
|
166
183
|
|
|
167
|
-
|
|
168
|
-
* TailwindMerge settings for custom Tailwind CSS classes.
|
|
169
|
-
* All lists of rules available here:
|
|
170
|
-
* https://github.com/dcastil/tailwind-merge/blob/main/src/lib/default-config.ts
|
|
171
|
-
*/
|
|
172
|
-
tailwindMerge: {},
|
|
173
|
-
};
|
|
174
|
-
`;
|
|
184
|
+
export const COMPONENTS_INDEX_EXPORT = `export const componentConfigs = {};`;
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { computed, nextTick } from "vue";
|
|
3
|
+
import { mount } from "@vue/test-utils";
|
|
4
|
+
|
|
5
|
+
// TODO: Autogenerated, need to be reviewed
|
|
6
|
+
|
|
7
|
+
// Mock the ui utils
|
|
8
|
+
vi.mock("../../utils/ui.ts", () => ({
|
|
9
|
+
cx: vi.fn((classes) => (Array.isArray(classes) ? classes.filter(Boolean).join(" ") : classes)),
|
|
10
|
+
cva: vi.fn((config) => {
|
|
11
|
+
// Return a spy function that can be called and tracked
|
|
12
|
+
const cvaSpy = vi.fn((props) => {
|
|
13
|
+
if (!config.variants) return config.base || "";
|
|
14
|
+
|
|
15
|
+
let classes = config.base || "";
|
|
16
|
+
|
|
17
|
+
// Apply variants
|
|
18
|
+
Object.entries(config.variants).forEach(([key, variants]) => {
|
|
19
|
+
const value = props[key];
|
|
20
|
+
|
|
21
|
+
if (value && variants[value]) {
|
|
22
|
+
classes += ` ${variants[value]}`;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return classes.trim();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return cvaSpy;
|
|
30
|
+
}),
|
|
31
|
+
setColor: vi.fn((classes, color) => classes?.replace(/{color}/g, color)),
|
|
32
|
+
vuelessConfig: { components: {}, unstyled: false },
|
|
33
|
+
getMergedConfig: vi.fn((args) => {
|
|
34
|
+
// Create a spy function that returns the expected merged config
|
|
35
|
+
const { defaultConfig, globalConfig, propsConfig } = args;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...defaultConfig,
|
|
39
|
+
...globalConfig,
|
|
40
|
+
...propsConfig,
|
|
41
|
+
};
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Mock Vue functions
|
|
46
|
+
vi.mock("vue", async () => {
|
|
47
|
+
const actual = await vi.importActual("vue");
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...actual,
|
|
51
|
+
getCurrentInstance: vi.fn(),
|
|
52
|
+
useAttrs: vi.fn(),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
import useUI from "../useUI.ts";
|
|
57
|
+
import * as uiUtils from "../../utils/ui.ts";
|
|
58
|
+
import { getCurrentInstance, useAttrs } from "vue";
|
|
59
|
+
|
|
60
|
+
// Test component for integration testing
|
|
61
|
+
const TestComponent = {
|
|
62
|
+
template: '<div :data-test="getDataTest()" v-bind="bodyAttrs">Test</div>',
|
|
63
|
+
setup() {
|
|
64
|
+
const defaultConfig = {
|
|
65
|
+
body: {
|
|
66
|
+
base: "base-class",
|
|
67
|
+
variants: {
|
|
68
|
+
variant: {
|
|
69
|
+
primary: "primary-class",
|
|
70
|
+
secondary: "secondary-class",
|
|
71
|
+
},
|
|
72
|
+
size: {
|
|
73
|
+
sm: "small-class",
|
|
74
|
+
md: "medium-class",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaults: {
|
|
79
|
+
variant: "primary",
|
|
80
|
+
size: "md",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return useUI(defaultConfig);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
describe("useUI", () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
vi.clearAllMocks();
|
|
91
|
+
// Reset vuelessConfig
|
|
92
|
+
uiUtils.vuelessConfig.components = {};
|
|
93
|
+
uiUtils.vuelessConfig.unstyled = false;
|
|
94
|
+
|
|
95
|
+
// Setup default mocks
|
|
96
|
+
vi.mocked(getCurrentInstance).mockReturnValue({
|
|
97
|
+
type: { __name: "TestComponent" },
|
|
98
|
+
props: { dataTest: "test", color: "primary", config: {} },
|
|
99
|
+
parent: null,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
vi.mocked(useAttrs).mockReturnValue({
|
|
103
|
+
class: "",
|
|
104
|
+
style: "",
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
vi.restoreAllMocks();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("Basic Functionality", () => {
|
|
113
|
+
it("should return config, getDataTest, and attribute objects", () => {
|
|
114
|
+
const defaultConfig = {
|
|
115
|
+
body: { base: "test-class" },
|
|
116
|
+
defaults: { variant: "primary" },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const result = useUI(defaultConfig);
|
|
120
|
+
|
|
121
|
+
expect(result).toHaveProperty("config");
|
|
122
|
+
expect(result).toHaveProperty("getDataTest");
|
|
123
|
+
expect(result).toHaveProperty("bodyAttrs");
|
|
124
|
+
expect(typeof result.getDataTest).toBe("function");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should merge default config with global and props config", async () => {
|
|
128
|
+
const defaultConfig = {
|
|
129
|
+
body: { base: "default-class" },
|
|
130
|
+
defaults: { variant: "primary" },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const globalConfig = {
|
|
134
|
+
body: { base: "global-class" },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const propsConfig = {
|
|
138
|
+
body: { base: "props-class" },
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Set global config
|
|
142
|
+
uiUtils.vuelessConfig.components = {
|
|
143
|
+
TestComponent: globalConfig,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Mock getCurrentInstance to return props config
|
|
147
|
+
vi.mocked(getCurrentInstance).mockReturnValue({
|
|
148
|
+
type: { __name: "TestComponent" },
|
|
149
|
+
props: { config: propsConfig, dataTest: "test" },
|
|
150
|
+
parent: null,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Call useUI to trigger the config merging
|
|
154
|
+
useUI(defaultConfig);
|
|
155
|
+
|
|
156
|
+
expect(uiUtils.getMergedConfig).toHaveBeenCalledWith({
|
|
157
|
+
defaultConfig,
|
|
158
|
+
globalConfig,
|
|
159
|
+
propsConfig,
|
|
160
|
+
unstyled: false,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("CVA Integration", () => {
|
|
166
|
+
it("should generate classes using CVA when config has variants", () => {
|
|
167
|
+
const defaultConfig = {
|
|
168
|
+
body: {
|
|
169
|
+
base: "base-class",
|
|
170
|
+
variants: {
|
|
171
|
+
variant: {
|
|
172
|
+
primary: "primary-class",
|
|
173
|
+
secondary: "secondary-class",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Call useUI to trigger CVA usage
|
|
180
|
+
useUI(defaultConfig);
|
|
181
|
+
|
|
182
|
+
expect(uiUtils.cva).toHaveBeenCalledWith(defaultConfig.body);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should handle string-based config values", () => {
|
|
186
|
+
const defaultConfig = {
|
|
187
|
+
body: "simple-string-class",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const result = useUI(defaultConfig);
|
|
191
|
+
const bodyAttrs = result.bodyAttrs;
|
|
192
|
+
|
|
193
|
+
expect(bodyAttrs.value.class).toContain("simple-string-class");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("Color Handling", () => {
|
|
198
|
+
it("should replace {color} placeholders in classes", () => {
|
|
199
|
+
// Mock getCurrentInstance to return color prop
|
|
200
|
+
vi.mocked(getCurrentInstance).mockReturnValue({
|
|
201
|
+
type: { __name: "TestComponent" },
|
|
202
|
+
props: { color: "blue", dataTest: "test" },
|
|
203
|
+
parent: null,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Test the setColor function directly
|
|
207
|
+
expect(uiUtils.setColor).toBeDefined();
|
|
208
|
+
|
|
209
|
+
// The setColor function should replace {color} placeholders
|
|
210
|
+
const testClasses = "text-{color} bg-{color}";
|
|
211
|
+
const coloredClasses = uiUtils.setColor(testClasses, "blue");
|
|
212
|
+
|
|
213
|
+
expect(coloredClasses).toBe("text-blue bg-blue");
|
|
214
|
+
|
|
215
|
+
// Test that useUI can be called with color-containing config
|
|
216
|
+
const defaultConfig = {
|
|
217
|
+
wrapper: {
|
|
218
|
+
base: "text-{color} bg-{color}",
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const result = useUI(defaultConfig);
|
|
223
|
+
|
|
224
|
+
expect(result).toHaveProperty("config");
|
|
225
|
+
expect(result).toHaveProperty("getDataTest");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("getDataTest Function", () => {
|
|
230
|
+
it("should return data-test value when dataTest prop is provided", () => {
|
|
231
|
+
const result = useUI({});
|
|
232
|
+
const dataTest = result.getDataTest();
|
|
233
|
+
|
|
234
|
+
expect(dataTest).toBe("test");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should return data-test with suffix when suffix is provided", () => {
|
|
238
|
+
const result = useUI({});
|
|
239
|
+
const dataTest = result.getDataTest("button");
|
|
240
|
+
|
|
241
|
+
expect(dataTest).toBe("test-button");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should return null when dataTest prop is not provided", () => {
|
|
245
|
+
// Mock getCurrentInstance to return no dataTest
|
|
246
|
+
vi.mocked(getCurrentInstance).mockReturnValue({
|
|
247
|
+
type: { __name: "TestComponent" },
|
|
248
|
+
props: {},
|
|
249
|
+
parent: null,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const result = useUI({});
|
|
253
|
+
const dataTest = result.getDataTest();
|
|
254
|
+
|
|
255
|
+
expect(dataTest).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("Nested Component Handling", () => {
|
|
260
|
+
it("should handle nested component references like {UIcon}", () => {
|
|
261
|
+
const defaultConfig = {
|
|
262
|
+
icon: "{UIcon}",
|
|
263
|
+
button: "btn {UIcon} end",
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const result = useUI(defaultConfig);
|
|
267
|
+
|
|
268
|
+
// The nested component pattern should be processed
|
|
269
|
+
expect(result).toHaveProperty("iconAttrs");
|
|
270
|
+
expect(result).toHaveProperty("buttonAttrs");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("Reactivity", () => {
|
|
275
|
+
it("should update config when props.config changes", async () => {
|
|
276
|
+
const component = mount(TestComponent);
|
|
277
|
+
|
|
278
|
+
// Initial state
|
|
279
|
+
expect(component.vm.config).toBeDefined();
|
|
280
|
+
|
|
281
|
+
// Change props
|
|
282
|
+
await component.setProps({
|
|
283
|
+
config: {
|
|
284
|
+
body: { base: "new-class" },
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await nextTick();
|
|
289
|
+
|
|
290
|
+
// Config should be updated
|
|
291
|
+
expect(uiUtils.getMergedConfig).toHaveBeenCalled();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("Extends Pattern Handling", () => {
|
|
296
|
+
it("should handle extends pattern {>key} syntax", () => {
|
|
297
|
+
const defaultConfig = {
|
|
298
|
+
button: "base-class {>icon}",
|
|
299
|
+
icon: "icon-class",
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const result = useUI(defaultConfig);
|
|
303
|
+
|
|
304
|
+
expect(result).toHaveProperty("buttonAttrs");
|
|
305
|
+
expect(result).toHaveProperty("iconAttrs");
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("Mutated Props", () => {
|
|
310
|
+
it("should use mutated props for class generation", () => {
|
|
311
|
+
const defaultConfig = {
|
|
312
|
+
body: {
|
|
313
|
+
base: "base-class",
|
|
314
|
+
variants: {
|
|
315
|
+
hasIcon: {
|
|
316
|
+
true: "with-icon",
|
|
317
|
+
false: "without-icon",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const mutatedProps = computed(() => ({
|
|
324
|
+
hasIcon: true,
|
|
325
|
+
}));
|
|
326
|
+
|
|
327
|
+
const result = useUI(defaultConfig, mutatedProps);
|
|
328
|
+
|
|
329
|
+
expect(result).toHaveProperty("bodyAttrs");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("Component Name Detection", () => {
|
|
334
|
+
it("should detect component name from type.__name", () => {
|
|
335
|
+
vi.mocked(getCurrentInstance).mockReturnValue({
|
|
336
|
+
type: { __name: "UButton" },
|
|
337
|
+
props: { dataTest: "test" },
|
|
338
|
+
parent: null,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const result = useUI({});
|
|
342
|
+
|
|
343
|
+
expect(result).toBeDefined();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("should detect component name from parent when internal component", () => {
|
|
347
|
+
vi.mocked(getCurrentInstance).mockReturnValue({
|
|
348
|
+
type: { internal: true },
|
|
349
|
+
props: { dataTest: "test" },
|
|
350
|
+
parent: {
|
|
351
|
+
type: { __name: "UButton" },
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const result = useUI({});
|
|
356
|
+
|
|
357
|
+
expect(result).toBeDefined();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("Attribute Generation", () => {
|
|
362
|
+
it("should generate proper attributes for each config key", () => {
|
|
363
|
+
const defaultConfig = {
|
|
364
|
+
wrapper: { base: "wrapper-class" },
|
|
365
|
+
content: { base: "content-class" },
|
|
366
|
+
footer: "footer-class",
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const result = useUI(defaultConfig);
|
|
370
|
+
|
|
371
|
+
expect(result).toHaveProperty("wrapperAttrs");
|
|
372
|
+
expect(result).toHaveProperty("contentAttrs");
|
|
373
|
+
expect(result).toHaveProperty("footerAttrs");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("should include config in attributes for nested components", () => {
|
|
377
|
+
const defaultConfig = {
|
|
378
|
+
icon: {
|
|
379
|
+
base: "{UIcon}",
|
|
380
|
+
defaults: {
|
|
381
|
+
size: "sm",
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const result = useUI(defaultConfig);
|
|
387
|
+
const iconAttrs = result.iconAttrs;
|
|
388
|
+
|
|
389
|
+
expect(iconAttrs.value).toHaveProperty("config");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe("Edge Cases", () => {
|
|
394
|
+
it("should handle empty config", () => {
|
|
395
|
+
const result = useUI({});
|
|
396
|
+
|
|
397
|
+
expect(result.config).toBeDefined();
|
|
398
|
+
expect(result.getDataTest).toBeDefined();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("should handle null/undefined config values", () => {
|
|
402
|
+
const defaultConfig = {
|
|
403
|
+
body: "",
|
|
404
|
+
icon: undefined,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const result = useUI(defaultConfig);
|
|
408
|
+
|
|
409
|
+
expect(result).toHaveProperty("bodyAttrs");
|
|
410
|
+
expect(result).toHaveProperty("iconAttrs");
|
|
411
|
+
|
|
412
|
+
// Should not throw errors when accessing attributes
|
|
413
|
+
expect(() => result.bodyAttrs.value).not.toThrow();
|
|
414
|
+
expect(() => result.iconAttrs.value).not.toThrow();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("should handle unstyled mode", () => {
|
|
418
|
+
uiUtils.vuelessConfig.unstyled = true;
|
|
419
|
+
|
|
420
|
+
const defaultConfig = {
|
|
421
|
+
body: { base: "styled-class" },
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
useUI(defaultConfig);
|
|
425
|
+
|
|
426
|
+
expect(uiUtils.getMergedConfig).toHaveBeenCalledWith(
|
|
427
|
+
expect.objectContaining({
|
|
428
|
+
unstyled: true,
|
|
429
|
+
}),
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("should handle config with only defaults", () => {
|
|
434
|
+
const defaultConfig = {
|
|
435
|
+
defaults: {
|
|
436
|
+
variant: "primary",
|
|
437
|
+
size: "md",
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const result = useUI(defaultConfig);
|
|
442
|
+
|
|
443
|
+
expect(result.config).toBeDefined();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("should handle deep config changes", async () => {
|
|
447
|
+
const component = mount(TestComponent);
|
|
448
|
+
|
|
449
|
+
// Change nested config
|
|
450
|
+
await component.setProps({
|
|
451
|
+
config: {
|
|
452
|
+
body: {
|
|
453
|
+
base: "new-base",
|
|
454
|
+
variants: {
|
|
455
|
+
variant: {
|
|
456
|
+
custom: "custom-class",
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await nextTick();
|
|
464
|
+
|
|
465
|
+
expect(uiUtils.getMergedConfig).toHaveBeenCalled();
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
describe("Integration Tests", () => {
|
|
470
|
+
it("should work with real component mounting", () => {
|
|
471
|
+
const component = mount(TestComponent);
|
|
472
|
+
|
|
473
|
+
expect(component.exists()).toBe(true);
|
|
474
|
+
expect(component.attributes("data-test")).toBe("test");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("should handle prop changes in mounted component", async () => {
|
|
478
|
+
const component = mount(TestComponent);
|
|
479
|
+
|
|
480
|
+
// The component should render with the default dataTest from the mock
|
|
481
|
+
expect(component.exists()).toBe(true);
|
|
482
|
+
|
|
483
|
+
// Test that the component can handle config changes
|
|
484
|
+
await component.setProps({
|
|
485
|
+
config: {
|
|
486
|
+
body: { base: "new-config-class" },
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// The component should still exist and function after prop changes
|
|
491
|
+
expect(component.exists()).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
});
|
package/constants.js
CHANGED
|
@@ -396,6 +396,7 @@ export const VUELESS_TAILWIND_SAFELIST = `${VUELESS_CACHE_DIR}/tailwind/safelist
|
|
|
396
396
|
export const VUELESS_CONFIGS_CACHED_DIR = `${VUELESS_CACHE_DIR}/configs`;
|
|
397
397
|
export const VUELESS_MERGED_CONFIGS_CACHED_DIR = `${VUELESS_CACHE_DIR}/mergedConfigs`;
|
|
398
398
|
export const VUELESS_CONFIG_FILE_NAME = "vueless.config";
|
|
399
|
+
export const CONFIG_INDEX_FILE_NAME = "index";
|
|
399
400
|
|
|
400
401
|
/* System error codes */
|
|
401
402
|
export const DEFAULT_EXIT_CODE = 0;
|
package/package.json
CHANGED
package/plugin-vite.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
getVueDirs,
|
|
27
27
|
getVuelessConfigDirs,
|
|
28
28
|
cacheMergedConfigs,
|
|
29
|
+
autoImportUserConfigs,
|
|
29
30
|
} from "./utils/node/helper.js";
|
|
30
31
|
import {
|
|
31
32
|
INTERNAL_ENV,
|
|
@@ -125,6 +126,10 @@ export const Vueless = function (options = {}) {
|
|
|
125
126
|
await cacheMergedConfigs(vuelessSrcDir);
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
if (!isInternalEnv) {
|
|
130
|
+
await autoImportUserConfigs();
|
|
131
|
+
}
|
|
132
|
+
|
|
128
133
|
await buildWebTypes(vuelessSrcDir);
|
|
129
134
|
await setCustomPropTypes(vuelessSrcDir);
|
|
130
135
|
|
package/utils/node/helper.js
CHANGED
|
@@ -13,6 +13,12 @@ import {
|
|
|
13
13
|
VUELESS_MERGED_CONFIGS_CACHED_DIR,
|
|
14
14
|
} from "../../constants.js";
|
|
15
15
|
|
|
16
|
+
import {
|
|
17
|
+
SUPPRESS_TS_CHECK,
|
|
18
|
+
COMPONENTS_INDEX_EXPORT,
|
|
19
|
+
COMPONENTS_INDEX_COMMENT,
|
|
20
|
+
} from "../../bin/constants.js";
|
|
21
|
+
|
|
16
22
|
export async function getDirFiles(dirPath, ext, { recursive = true, exclude = [] } = {}) {
|
|
17
23
|
let fileNames = [];
|
|
18
24
|
|
|
@@ -148,3 +154,61 @@ export async function buildTSFile(entryPath, configOutFile) {
|
|
|
148
154
|
loader: { ".ts": "ts" },
|
|
149
155
|
});
|
|
150
156
|
}
|
|
157
|
+
|
|
158
|
+
export async function autoImportUserConfigs() {
|
|
159
|
+
const vuelessConfigDir = path.join(cwd(), ".vueless");
|
|
160
|
+
|
|
161
|
+
const indexTsPath = path.join(vuelessConfigDir, "index.ts");
|
|
162
|
+
const indexJsPath = path.join(vuelessConfigDir, "index.js");
|
|
163
|
+
|
|
164
|
+
const hasTsIndex = existsSync(indexTsPath);
|
|
165
|
+
const indexFilePath = hasTsIndex ? indexTsPath : indexJsPath;
|
|
166
|
+
|
|
167
|
+
const configFiles = await getDirFiles(vuelessConfigDir, ".ts", {
|
|
168
|
+
recursive: true,
|
|
169
|
+
exclude: ["index.ts", "index.js"],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const componentConfigFiles = configFiles.filter((filePath) => {
|
|
173
|
+
const fileName = path.basename(filePath);
|
|
174
|
+
|
|
175
|
+
return /^U\w+\.config\.(ts|js)$/.test(fileName);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const imports = [];
|
|
179
|
+
const componentEntries = [];
|
|
180
|
+
|
|
181
|
+
if (componentConfigFiles.length) {
|
|
182
|
+
for (const configFilePath of componentConfigFiles) {
|
|
183
|
+
const fileName = path.basename(configFilePath, path.extname(configFilePath));
|
|
184
|
+
const componentName = fileName.replace(".config", "");
|
|
185
|
+
const relativePath = path.relative(vuelessConfigDir, configFilePath);
|
|
186
|
+
const importPath = "./" + relativePath.replace(/\\/g, "/");
|
|
187
|
+
|
|
188
|
+
imports.push(`import ${componentName} from "${importPath}";`);
|
|
189
|
+
componentEntries.push(` ${componentName},`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!existsSync(vuelessConfigDir)) {
|
|
194
|
+
await mkdir(vuelessConfigDir, { recursive: true });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await writeFile(
|
|
198
|
+
indexFilePath,
|
|
199
|
+
generateConfigIndexContent(imports, componentEntries, hasTsIndex),
|
|
200
|
+
"utf-8",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function generateConfigIndexContent(imports = [], componentEntries = [], isTypeScript) {
|
|
205
|
+
const importsSection = imports.length ? `\n${imports.join("\n")}\n\n` : "";
|
|
206
|
+
const entriesSection = componentEntries.length ? `\n${componentEntries.join("\n")}\n` : "";
|
|
207
|
+
const suppressTsCheck = isTypeScript ? `${SUPPRESS_TS_CHECK}\n` : "";
|
|
208
|
+
|
|
209
|
+
return `${suppressTsCheck}${COMPONENTS_INDEX_COMMENT}\n${importsSection}${COMPONENTS_INDEX_EXPORT.replace(
|
|
210
|
+
"{}",
|
|
211
|
+
`{${entriesSection}}`,
|
|
212
|
+
)}
|
|
213
|
+
`;
|
|
214
|
+
}
|