renusify 3.1.3 → 3.1.5
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/package.json +1 -1
- package/plugins/auto-loader.mjs +223 -191
- package/plugins/trans/DateTime.js +135 -74
- package/tests/DateTime.test.js +596 -0
- package/tests/autoloader.test.js +905 -0
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
// Simple test utilities
|
|
5
|
+
const colors = {
|
|
6
|
+
reset: "\x1b[0m",
|
|
7
|
+
green: "\x1b[32m",
|
|
8
|
+
red: "\x1b[31m",
|
|
9
|
+
yellow: "\x1b[33m",
|
|
10
|
+
blue: "\x1b[34m",
|
|
11
|
+
gray: "\x1b[90m",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let testCount = 0;
|
|
15
|
+
let passCount = 0;
|
|
16
|
+
let failCount = 0;
|
|
17
|
+
let currentSuite = "";
|
|
18
|
+
|
|
19
|
+
function suite(name, fn) {
|
|
20
|
+
currentSuite = name;
|
|
21
|
+
console.log(`\n${colors.blue}${name}${colors.reset}`);
|
|
22
|
+
fn();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function test(description, fn) {
|
|
26
|
+
testCount++;
|
|
27
|
+
try {
|
|
28
|
+
fn();
|
|
29
|
+
passCount++;
|
|
30
|
+
console.log(` ${colors.green}✓${colors.reset} ${description}`);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
failCount++;
|
|
33
|
+
console.log(` ${colors.red}✗${colors.reset} ${description}`);
|
|
34
|
+
console.log(` ${colors.red}${error.message}${colors.reset}`);
|
|
35
|
+
if (error.stack) {
|
|
36
|
+
console.log(
|
|
37
|
+
` ${colors.gray}${error.stack.split("\n").slice(1, 3).join("\n ")}${colors.reset}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function assert(condition, message) {
|
|
44
|
+
if (!condition) {
|
|
45
|
+
throw new Error(message || "Assertion failed");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertEqual(actual, expected, message) {
|
|
50
|
+
if (actual !== expected) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
message ||
|
|
53
|
+
`Expected ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertDeepEqual(actual, expected, message) {
|
|
59
|
+
const actualStr = JSON.stringify(actual);
|
|
60
|
+
const expectedStr = JSON.stringify(expected);
|
|
61
|
+
if (actualStr !== expectedStr) {
|
|
62
|
+
throw new Error(message || `Expected ${expectedStr}, but got ${actualStr}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function assertMatch(actual, pattern, message) {
|
|
67
|
+
if (!pattern.test(actual)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
message || `Expected "${actual}" to match pattern ${pattern}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assertIncludes(str, substring, message) {
|
|
75
|
+
if (!str.includes(substring)) {
|
|
76
|
+
throw new Error(message || `Expected "${str}" to include "${substring}"`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function assertNotIncludes(str, substring, message) {
|
|
81
|
+
if (str.includes(substring)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
message || `Expected "${str}" to not include "${substring}"`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Mock console methods
|
|
89
|
+
const originalConsoleWarn = console.warn;
|
|
90
|
+
let consoleWarnCalled = false;
|
|
91
|
+
let consoleWarnMessages = [];
|
|
92
|
+
|
|
93
|
+
function mockConsoleWarn() {
|
|
94
|
+
consoleWarnCalled = false;
|
|
95
|
+
consoleWarnMessages = [];
|
|
96
|
+
console.warn = (...args) => {
|
|
97
|
+
consoleWarnCalled = true;
|
|
98
|
+
consoleWarnMessages.push(args.join(" "));
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function restoreConsole() {
|
|
103
|
+
console.warn = originalConsoleWarn;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Helper functions to extract internal functions from the plugin
|
|
107
|
+
// Since the plugin exports a factory function, we need to create instances for testing
|
|
108
|
+
|
|
109
|
+
function createMockConfig(command = "build", root = "/mock/project") {
|
|
110
|
+
return {
|
|
111
|
+
command,
|
|
112
|
+
root,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createTestFile(content) {
|
|
117
|
+
const tmpPath = path.join("/tmp", `test-${Date.now()}-${Math.random()}.js`);
|
|
118
|
+
fs.writeFileSync(tmpPath, content, "utf-8");
|
|
119
|
+
return tmpPath;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cleanupTestFile(filePath) {
|
|
123
|
+
try {
|
|
124
|
+
if (fs.existsSync(filePath)) {
|
|
125
|
+
fs.unlinkSync(filePath);
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// ignore
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Test suites
|
|
133
|
+
|
|
134
|
+
suite("parseExports", () => {
|
|
135
|
+
test("should parse simple export * as syntax", () => {
|
|
136
|
+
const content = `export * as rBtn from './button/index.js'`;
|
|
137
|
+
const tmpFile = createTestFile(content);
|
|
138
|
+
|
|
139
|
+
// We need to test the parseExports function, but it's internal
|
|
140
|
+
// So we'll create a mock version for testing
|
|
141
|
+
const parseExports = (filePath, exclude = []) => {
|
|
142
|
+
try {
|
|
143
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
144
|
+
const clean = content
|
|
145
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
146
|
+
.replace(/\/\/.*$/gm, "");
|
|
147
|
+
|
|
148
|
+
const regex =
|
|
149
|
+
/export\s+\*\s+as\s+([a-zA-Z_]\w*)\s+from\s+['"]([^'"]+)['"]/g;
|
|
150
|
+
const map = new Map();
|
|
151
|
+
let m;
|
|
152
|
+
while ((m = regex.exec(clean)) !== null) {
|
|
153
|
+
const [, name, relPath] = m;
|
|
154
|
+
if (exclude.includes(name)) continue;
|
|
155
|
+
let p = relPath.replace(/^\.\//, "");
|
|
156
|
+
if (!p.match(/\.(js|vue|ts|mjs)$/)) p = `${p}/index.js`;
|
|
157
|
+
map.set(name, p);
|
|
158
|
+
}
|
|
159
|
+
return map;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return new Map();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result = parseExports(tmpFile);
|
|
166
|
+
assertEqual(result.size, 1);
|
|
167
|
+
assertEqual(result.get("rBtn"), "button/index.js");
|
|
168
|
+
|
|
169
|
+
cleanupTestFile(tmpFile);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("should parse multiple exports", () => {
|
|
173
|
+
const content = `
|
|
174
|
+
export * as rBtn from './button/index.js'
|
|
175
|
+
export * as rCard from './card/index.js'
|
|
176
|
+
export * as rInput from './form/input/index.js'
|
|
177
|
+
`.trim();
|
|
178
|
+
const tmpFile = createTestFile(content);
|
|
179
|
+
|
|
180
|
+
const parseExports = (filePath, exclude = []) => {
|
|
181
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
182
|
+
const clean = content
|
|
183
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
184
|
+
.replace(/\/\/.*$/gm, "");
|
|
185
|
+
const regex =
|
|
186
|
+
/export\s+\*\s+as\s+([a-zA-Z_]\w*)\s+from\s+['"]([^'"]+)['"]/g;
|
|
187
|
+
const map = new Map();
|
|
188
|
+
let m;
|
|
189
|
+
while ((m = regex.exec(clean)) !== null) {
|
|
190
|
+
const [, name, relPath] = m;
|
|
191
|
+
if (exclude.includes(name)) continue;
|
|
192
|
+
let p = relPath.replace(/^\.\//, "");
|
|
193
|
+
if (!p.match(/\.(js|vue|ts|mjs)$/)) p = `${p}/index.js`;
|
|
194
|
+
map.set(name, p);
|
|
195
|
+
}
|
|
196
|
+
return map;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const result = parseExports(tmpFile);
|
|
200
|
+
assertEqual(result.size, 3);
|
|
201
|
+
assertEqual(result.get("rBtn"), "button/index.js");
|
|
202
|
+
assertEqual(result.get("rCard"), "card/index.js");
|
|
203
|
+
assertEqual(result.get("rInput"), "form/input/index.js");
|
|
204
|
+
|
|
205
|
+
cleanupTestFile(tmpFile);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("should exclude specified components", () => {
|
|
209
|
+
const content = `
|
|
210
|
+
export * as rBtn from './button/index.js'
|
|
211
|
+
export * as _register from './register.js'
|
|
212
|
+
export * as rCard from './card/index.js'
|
|
213
|
+
`.trim();
|
|
214
|
+
const tmpFile = createTestFile(content);
|
|
215
|
+
|
|
216
|
+
const parseExports = (filePath, exclude = []) => {
|
|
217
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
218
|
+
const clean = content
|
|
219
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
220
|
+
.replace(/\/\/.*$/gm, "");
|
|
221
|
+
const regex =
|
|
222
|
+
/export\s+\*\s+as\s+([a-zA-Z_]\w*)\s+from\s+['"]([^'"]+)['"]/g;
|
|
223
|
+
const map = new Map();
|
|
224
|
+
let m;
|
|
225
|
+
while ((m = regex.exec(clean)) !== null) {
|
|
226
|
+
const [, name, relPath] = m;
|
|
227
|
+
if (exclude.includes(name)) continue;
|
|
228
|
+
let p = relPath.replace(/^\.\//, "");
|
|
229
|
+
if (!p.match(/\.(js|vue|ts|mjs)$/)) p = `${p}/index.js`;
|
|
230
|
+
map.set(name, p);
|
|
231
|
+
}
|
|
232
|
+
return map;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const result = parseExports(tmpFile, ["_register"]);
|
|
236
|
+
assertEqual(result.size, 2);
|
|
237
|
+
assertEqual(result.get("rBtn"), "button/index.js");
|
|
238
|
+
assertEqual(result.get("rCard"), "card/index.js");
|
|
239
|
+
assert(!result.has("_register"));
|
|
240
|
+
|
|
241
|
+
cleanupTestFile(tmpFile);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("should handle comments", () => {
|
|
245
|
+
const content = `
|
|
246
|
+
// This is a comment
|
|
247
|
+
export * as rBtn from './button/index.js'
|
|
248
|
+
/* Multi-line
|
|
249
|
+
comment */
|
|
250
|
+
export * as rCard from './card/index.js'
|
|
251
|
+
`.trim();
|
|
252
|
+
const tmpFile = createTestFile(content);
|
|
253
|
+
|
|
254
|
+
const parseExports = (filePath, exclude = []) => {
|
|
255
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
256
|
+
const clean = content
|
|
257
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
258
|
+
.replace(/\/\/.*$/gm, "");
|
|
259
|
+
const regex =
|
|
260
|
+
/export\s+\*\s+as\s+([a-zA-Z_]\w*)\s+from\s+['"]([^'"]+)['"]/g;
|
|
261
|
+
const map = new Map();
|
|
262
|
+
let m;
|
|
263
|
+
while ((m = regex.exec(clean)) !== null) {
|
|
264
|
+
const [, name, relPath] = m;
|
|
265
|
+
if (exclude.includes(name)) continue;
|
|
266
|
+
let p = relPath.replace(/^\.\//, "");
|
|
267
|
+
if (!p.match(/\.(js|vue|ts|mjs)$/)) p = `${p}/index.js`;
|
|
268
|
+
map.set(name, p);
|
|
269
|
+
}
|
|
270
|
+
return map;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const result = parseExports(tmpFile);
|
|
274
|
+
assertEqual(result.size, 2);
|
|
275
|
+
|
|
276
|
+
cleanupTestFile(tmpFile);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
suite("kebab2camel", () => {
|
|
281
|
+
const kebab2camel = (s) => {
|
|
282
|
+
return s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
test("should convert simple kebab-case to camelCase", () => {
|
|
286
|
+
assertEqual(kebab2camel("r-btn"), "rBtn");
|
|
287
|
+
assertEqual(kebab2camel("r-card"), "rCard");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("should handle multiple hyphens", () => {
|
|
291
|
+
assertEqual(kebab2camel("r-btn-group"), "rBtnGroup");
|
|
292
|
+
assertEqual(kebab2camel("r-form-input"), "rFormInput");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("should handle numbers", () => {
|
|
296
|
+
assertEqual(kebab2camel("r-h1"), "rH1");
|
|
297
|
+
assertEqual(kebab2camel("r-v2"), "rV2");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("should leave already camelCase unchanged", () => {
|
|
301
|
+
assertEqual(kebab2camel("rBtn"), "rBtn");
|
|
302
|
+
assertEqual(kebab2camel("rCard"), "rCard");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("should handle no hyphens", () => {
|
|
306
|
+
assertEqual(kebab2camel("rbutton"), "rbutton");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
suite("scanFile", () => {
|
|
311
|
+
const kebab2camel = (s) =>
|
|
312
|
+
s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
313
|
+
|
|
314
|
+
const createMockState = () => ({
|
|
315
|
+
usedComponents: new Set(),
|
|
316
|
+
usedDirectives: new Set(),
|
|
317
|
+
componentsMap: new Map([
|
|
318
|
+
["rBtn", "button/index.js"],
|
|
319
|
+
["rCard", "card/index.js"],
|
|
320
|
+
["rBtnGroup", "button/buttonGroup.js"],
|
|
321
|
+
]),
|
|
322
|
+
directivesMap: new Map([
|
|
323
|
+
["vRipple", "ripple.js"],
|
|
324
|
+
["vTooltip", "tooltip.js"],
|
|
325
|
+
]),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const scanFile = (filePath, state, prefix = "r") => {
|
|
329
|
+
try {
|
|
330
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
331
|
+
const tmplMatch = content.match(
|
|
332
|
+
/<template(?:\s[^>]*)?>([\s\S]*?)<\/template>/,
|
|
333
|
+
);
|
|
334
|
+
if (!tmplMatch) return;
|
|
335
|
+
|
|
336
|
+
const tmpl = tmplMatch[1];
|
|
337
|
+
|
|
338
|
+
const tagRe = /<\s*\/?([a-zA-Z][a-zA-Z0-9-]*)/g;
|
|
339
|
+
let m;
|
|
340
|
+
while ((m = tagRe.exec(tmpl)) !== null) {
|
|
341
|
+
const tag = m[1];
|
|
342
|
+
if (!tag.startsWith(prefix)) continue;
|
|
343
|
+
const camel = kebab2camel(tag);
|
|
344
|
+
if (state.componentsMap.has(camel)) state.usedComponents.add(camel);
|
|
345
|
+
else if (state.componentsMap.has(tag)) state.usedComponents.add(tag);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const dirRe = /\bv-([a-zA-Z][a-zA-Z0-9-]*)/g;
|
|
349
|
+
const builtins = new Set([
|
|
350
|
+
"model",
|
|
351
|
+
"if",
|
|
352
|
+
"else",
|
|
353
|
+
"else-if",
|
|
354
|
+
"for",
|
|
355
|
+
"show",
|
|
356
|
+
"bind",
|
|
357
|
+
"on",
|
|
358
|
+
"slot",
|
|
359
|
+
"text",
|
|
360
|
+
"html",
|
|
361
|
+
"cloak",
|
|
362
|
+
"once",
|
|
363
|
+
"memo",
|
|
364
|
+
"pre",
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
while ((m = dirRe.exec(tmpl)) !== null) {
|
|
368
|
+
const d = m[1];
|
|
369
|
+
if (builtins.has(d)) continue;
|
|
370
|
+
const camel = kebab2camel(d);
|
|
371
|
+
const candidates = [
|
|
372
|
+
d,
|
|
373
|
+
camel,
|
|
374
|
+
`v${camel.charAt(0).toUpperCase()}${camel.slice(1)}`,
|
|
375
|
+
];
|
|
376
|
+
for (const c of candidates) {
|
|
377
|
+
if (state.directivesMap.has(c)) {
|
|
378
|
+
state.usedDirectives.add(c);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
// ignore
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
test("should detect kebab-case components", () => {
|
|
389
|
+
const vueContent = `
|
|
390
|
+
<template>
|
|
391
|
+
<div>
|
|
392
|
+
<r-btn>Click</r-btn>
|
|
393
|
+
<r-card>Content</r-card>
|
|
394
|
+
</div>
|
|
395
|
+
</template>
|
|
396
|
+
`.trim();
|
|
397
|
+
const tmpFile = createTestFile(vueContent);
|
|
398
|
+
const state = createMockState();
|
|
399
|
+
|
|
400
|
+
scanFile(tmpFile, state);
|
|
401
|
+
|
|
402
|
+
assert(state.usedComponents.has("rBtn"));
|
|
403
|
+
assert(state.usedComponents.has("rCard"));
|
|
404
|
+
assertEqual(state.usedComponents.size, 2);
|
|
405
|
+
|
|
406
|
+
cleanupTestFile(tmpFile);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("should detect self-closing components", () => {
|
|
410
|
+
const vueContent = `
|
|
411
|
+
<template>
|
|
412
|
+
<div>
|
|
413
|
+
<r-btn />
|
|
414
|
+
<r-card/>
|
|
415
|
+
</div>
|
|
416
|
+
</template>
|
|
417
|
+
`.trim();
|
|
418
|
+
const tmpFile = createTestFile(vueContent);
|
|
419
|
+
const state = createMockState();
|
|
420
|
+
|
|
421
|
+
scanFile(tmpFile, state);
|
|
422
|
+
|
|
423
|
+
assert(state.usedComponents.has("rBtn"));
|
|
424
|
+
assert(state.usedComponents.has("rCard"));
|
|
425
|
+
|
|
426
|
+
cleanupTestFile(tmpFile);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("should detect directives", () => {
|
|
430
|
+
const vueContent = `
|
|
431
|
+
<template>
|
|
432
|
+
<div v-ripple>
|
|
433
|
+
<span v-tooltip="'Hello'">Text</span>
|
|
434
|
+
</div>
|
|
435
|
+
</template>
|
|
436
|
+
`.trim();
|
|
437
|
+
const tmpFile = createTestFile(vueContent);
|
|
438
|
+
const state = createMockState();
|
|
439
|
+
|
|
440
|
+
scanFile(tmpFile, state);
|
|
441
|
+
|
|
442
|
+
assert(state.usedDirectives.has("vRipple"));
|
|
443
|
+
assert(state.usedDirectives.has("vTooltip"));
|
|
444
|
+
|
|
445
|
+
cleanupTestFile(tmpFile);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("should ignore built-in Vue directives", () => {
|
|
449
|
+
const vueContent = `
|
|
450
|
+
<template>
|
|
451
|
+
<div v-if="show" v-for="item in items" v-model="value">
|
|
452
|
+
<span v-show="visible" v-bind:class="className">Text</span>
|
|
453
|
+
</div>
|
|
454
|
+
</template>
|
|
455
|
+
`.trim();
|
|
456
|
+
const tmpFile = createTestFile(vueContent);
|
|
457
|
+
const state = createMockState();
|
|
458
|
+
|
|
459
|
+
scanFile(tmpFile, state);
|
|
460
|
+
|
|
461
|
+
assertEqual(state.usedDirectives.size, 0);
|
|
462
|
+
|
|
463
|
+
cleanupTestFile(tmpFile);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("should ignore non-prefixed components", () => {
|
|
467
|
+
const vueContent = `
|
|
468
|
+
<template>
|
|
469
|
+
<div>
|
|
470
|
+
<button>Native</button>
|
|
471
|
+
<span>Text</span>
|
|
472
|
+
<r-btn>Renusify</r-btn>
|
|
473
|
+
</div>
|
|
474
|
+
</template>
|
|
475
|
+
`.trim();
|
|
476
|
+
const tmpFile = createTestFile(vueContent);
|
|
477
|
+
const state = createMockState();
|
|
478
|
+
|
|
479
|
+
scanFile(tmpFile, state);
|
|
480
|
+
|
|
481
|
+
assertEqual(state.usedComponents.size, 1);
|
|
482
|
+
assert(state.usedComponents.has("rBtn"));
|
|
483
|
+
|
|
484
|
+
cleanupTestFile(tmpFile);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("should handle files without template", () => {
|
|
488
|
+
const vueContent = `
|
|
489
|
+
<script>
|
|
490
|
+
export default {
|
|
491
|
+
name: 'MyComponent'
|
|
492
|
+
}
|
|
493
|
+
</script>
|
|
494
|
+
`.trim();
|
|
495
|
+
const tmpFile = createTestFile(vueContent);
|
|
496
|
+
const state = createMockState();
|
|
497
|
+
|
|
498
|
+
scanFile(tmpFile, state);
|
|
499
|
+
|
|
500
|
+
assertEqual(state.usedComponents.size, 0);
|
|
501
|
+
assertEqual(state.usedDirectives.size, 0);
|
|
502
|
+
|
|
503
|
+
cleanupTestFile(tmpFile);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
suite("generateImportStatements", () => {
|
|
508
|
+
const generateImportStatements = (state) => {
|
|
509
|
+
const comps = [...state.usedComponents].sort();
|
|
510
|
+
const dirs = [...state.usedDirectives].sort();
|
|
511
|
+
|
|
512
|
+
if (comps.length === 0 && dirs.length === 0) return "";
|
|
513
|
+
|
|
514
|
+
let code = "\n";
|
|
515
|
+
if (comps.length > 0) {
|
|
516
|
+
code += `import {\n ${comps.join(",\n ")}\n} from 'renusify/components/index.js'\n`;
|
|
517
|
+
}
|
|
518
|
+
if (dirs.length > 0) {
|
|
519
|
+
const dirNames = dirs.map((name) => {
|
|
520
|
+
if (name.startsWith("v") || name.startsWith("V")) {
|
|
521
|
+
return name.charAt(1).toLowerCase() + name.slice(2);
|
|
522
|
+
}
|
|
523
|
+
return name;
|
|
524
|
+
});
|
|
525
|
+
code += `import { ${dirNames.join(", ")} } from 'renusify/directive/index.js'\n`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return code;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
test("should generate import for single component", () => {
|
|
532
|
+
const state = {
|
|
533
|
+
usedComponents: new Set(["rBtn"]),
|
|
534
|
+
usedDirectives: new Set(),
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const result = generateImportStatements(state);
|
|
538
|
+
assertIncludes(result, "import {");
|
|
539
|
+
assertIncludes(result, "rBtn");
|
|
540
|
+
assertIncludes(result, "from 'renusify/components/index.js'");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("should generate imports for multiple components", () => {
|
|
544
|
+
const state = {
|
|
545
|
+
usedComponents: new Set(["rBtn", "rCard", "rInput"]),
|
|
546
|
+
usedDirectives: new Set(),
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const result = generateImportStatements(state);
|
|
550
|
+
assertIncludes(result, "rBtn");
|
|
551
|
+
assertIncludes(result, "rCard");
|
|
552
|
+
assertIncludes(result, "rInput");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test("should generate imports for directives", () => {
|
|
556
|
+
const state = {
|
|
557
|
+
usedComponents: new Set(),
|
|
558
|
+
usedDirectives: new Set(["vRipple", "vTooltip"]),
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const result = generateImportStatements(state);
|
|
562
|
+
assertIncludes(result, "ripple");
|
|
563
|
+
assertIncludes(result, "tooltip");
|
|
564
|
+
assertIncludes(result, "from 'renusify/directive/index.js'");
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("should generate imports for both components and directives", () => {
|
|
568
|
+
const state = {
|
|
569
|
+
usedComponents: new Set(["rBtn", "rCard"]),
|
|
570
|
+
usedDirectives: new Set(["vRipple"]),
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const result = generateImportStatements(state);
|
|
574
|
+
assertIncludes(result, "rBtn");
|
|
575
|
+
assertIncludes(result, "rCard");
|
|
576
|
+
assertIncludes(result, "ripple");
|
|
577
|
+
assertIncludes(result, "components/index.js");
|
|
578
|
+
assertIncludes(result, "directive/index.js");
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("should return empty string when no components or directives", () => {
|
|
582
|
+
const state = {
|
|
583
|
+
usedComponents: new Set(),
|
|
584
|
+
usedDirectives: new Set(),
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const result = generateImportStatements(state);
|
|
588
|
+
assertEqual(result, "");
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("should sort components alphabetically", () => {
|
|
592
|
+
const state = {
|
|
593
|
+
usedComponents: new Set(["rCard", "rAvatar", "rBtn"]),
|
|
594
|
+
usedDirectives: new Set(),
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const result = generateImportStatements(state);
|
|
598
|
+
const rAvatarIndex = result.indexOf("rAvatar");
|
|
599
|
+
const rBtnIndex = result.indexOf("rBtn");
|
|
600
|
+
const rCardIndex = result.indexOf("rCard");
|
|
601
|
+
|
|
602
|
+
assert(
|
|
603
|
+
rAvatarIndex < rBtnIndex && rBtnIndex < rCardIndex,
|
|
604
|
+
"Components should be sorted alphabetically",
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
suite("generateComponentsObject", () => {
|
|
610
|
+
const generateComponentsObject = (state) => {
|
|
611
|
+
const comps = [...state.usedComponents].sort();
|
|
612
|
+
if (comps.length === 0) return null;
|
|
613
|
+
return `components: {\n ${comps.join(",\n ")}\n }`;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
test("should generate components object with single component", () => {
|
|
617
|
+
const state = { usedComponents: new Set(["rBtn"]) };
|
|
618
|
+
const result = generateComponentsObject(state);
|
|
619
|
+
|
|
620
|
+
assertIncludes(result, "components:");
|
|
621
|
+
assertIncludes(result, "rBtn");
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("should generate components object with multiple components", () => {
|
|
625
|
+
const state = { usedComponents: new Set(["rBtn", "rCard", "rInput"]) };
|
|
626
|
+
const result = generateComponentsObject(state);
|
|
627
|
+
|
|
628
|
+
assertIncludes(result, "rBtn");
|
|
629
|
+
assertIncludes(result, "rCard");
|
|
630
|
+
assertIncludes(result, "rInput");
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("should return null when no components", () => {
|
|
634
|
+
const state = { usedComponents: new Set() };
|
|
635
|
+
const result = generateComponentsObject(state);
|
|
636
|
+
|
|
637
|
+
assertEqual(result, null);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("should sort components alphabetically", () => {
|
|
641
|
+
const state = { usedComponents: new Set(["rCard", "rAvatar", "rBtn"]) };
|
|
642
|
+
const result = generateComponentsObject(state);
|
|
643
|
+
|
|
644
|
+
const rAvatarIndex = result.indexOf("rAvatar");
|
|
645
|
+
const rBtnIndex = result.indexOf("rBtn");
|
|
646
|
+
const rCardIndex = result.indexOf("rCard");
|
|
647
|
+
|
|
648
|
+
assert(rAvatarIndex < rBtnIndex && rBtnIndex < rCardIndex);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
suite("generateDirectivesObject", () => {
|
|
653
|
+
const generateDirectivesObject = (state) => {
|
|
654
|
+
const dirs = [...state.usedDirectives].sort();
|
|
655
|
+
if (dirs.length === 0) return null;
|
|
656
|
+
const dirNames = dirs.map((name) => {
|
|
657
|
+
if (name.startsWith("v") || name.startsWith("V")) {
|
|
658
|
+
return name.charAt(1).toLowerCase() + name.slice(2);
|
|
659
|
+
}
|
|
660
|
+
return name;
|
|
661
|
+
});
|
|
662
|
+
return `directives: { ${dirNames.join(", ")} }`;
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
test("should generate directives object with single directive", () => {
|
|
666
|
+
const state = { usedDirectives: new Set(["vRipple"]) };
|
|
667
|
+
const result = generateDirectivesObject(state);
|
|
668
|
+
|
|
669
|
+
assertIncludes(result, "directives:");
|
|
670
|
+
assertIncludes(result, "ripple");
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("should generate directives object with multiple directives", () => {
|
|
674
|
+
const state = { usedDirectives: new Set(["vRipple", "vTooltip"]) };
|
|
675
|
+
const result = generateDirectivesObject(state);
|
|
676
|
+
|
|
677
|
+
assertIncludes(result, "ripple");
|
|
678
|
+
assertIncludes(result, "tooltip");
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("should return null when no directives", () => {
|
|
682
|
+
const state = { usedDirectives: new Set() };
|
|
683
|
+
const result = generateDirectivesObject(state);
|
|
684
|
+
|
|
685
|
+
assertEqual(result, null);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("should convert directive names properly", () => {
|
|
689
|
+
const state = { usedDirectives: new Set(["vRipple", "VTooltip"]) };
|
|
690
|
+
const result = generateDirectivesObject(state);
|
|
691
|
+
|
|
692
|
+
assertIncludes(result, "ripple");
|
|
693
|
+
assertIncludes(result, "tooltip");
|
|
694
|
+
assertNotIncludes(result, "vRipple");
|
|
695
|
+
assertNotIncludes(result, "VTooltip");
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
suite("removeProperty", () => {
|
|
700
|
+
function removeProperty(optionsStr, propName) {
|
|
701
|
+
const propRegex = new RegExp(`,?\\s*${propName}\\s*:\\s*`);
|
|
702
|
+
const match = propRegex.exec(optionsStr);
|
|
703
|
+
|
|
704
|
+
if (!match) return optionsStr;
|
|
705
|
+
|
|
706
|
+
const startIndex = match.index;
|
|
707
|
+
const valueStart = startIndex + match[0].length;
|
|
708
|
+
|
|
709
|
+
let endIndex = valueStart;
|
|
710
|
+
const firstChar = optionsStr[valueStart];
|
|
711
|
+
|
|
712
|
+
if (firstChar === "{") {
|
|
713
|
+
let braceCount = 1;
|
|
714
|
+
let i = valueStart + 1;
|
|
715
|
+
while (i < optionsStr.length && braceCount > 0) {
|
|
716
|
+
if (optionsStr[i] === "{") braceCount++;
|
|
717
|
+
else if (optionsStr[i] === "}") braceCount--;
|
|
718
|
+
i++;
|
|
719
|
+
}
|
|
720
|
+
endIndex = i;
|
|
721
|
+
} else if (firstChar === "[") {
|
|
722
|
+
let bracketCount = 1;
|
|
723
|
+
let i = valueStart + 1;
|
|
724
|
+
while (i < optionsStr.length && bracketCount > 0) {
|
|
725
|
+
if (optionsStr[i] === "[") bracketCount++;
|
|
726
|
+
else if (optionsStr[i] === "]") bracketCount--;
|
|
727
|
+
i++;
|
|
728
|
+
}
|
|
729
|
+
endIndex = i;
|
|
730
|
+
} else {
|
|
731
|
+
let i = valueStart;
|
|
732
|
+
while (
|
|
733
|
+
i < optionsStr.length &&
|
|
734
|
+
optionsStr[i] !== "," &&
|
|
735
|
+
optionsStr[i] !== "}"
|
|
736
|
+
) {
|
|
737
|
+
i++;
|
|
738
|
+
}
|
|
739
|
+
endIndex = i;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (endIndex < optionsStr.length && optionsStr[endIndex] === ",") {
|
|
743
|
+
endIndex++;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return optionsStr.slice(0, startIndex) + optionsStr.slice(endIndex);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
test("should remove simple property", () => {
|
|
750
|
+
const options = "theme: 'dark', locale: 'en'";
|
|
751
|
+
const result = removeProperty(options, "theme");
|
|
752
|
+
|
|
753
|
+
assertNotIncludes(result, "theme");
|
|
754
|
+
assertIncludes(result, "locale");
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test("should remove property with object value", () => {
|
|
758
|
+
const options = "components: { rBtn, rCard }, theme: 'dark'";
|
|
759
|
+
const result = removeProperty(options, "components");
|
|
760
|
+
|
|
761
|
+
assertNotIncludes(result, "components");
|
|
762
|
+
assertNotIncludes(result, "rBtn");
|
|
763
|
+
assertIncludes(result, "theme");
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test("should remove property with array value", () => {
|
|
767
|
+
const options = "plugins: [plugin1, plugin2], theme: 'dark'";
|
|
768
|
+
const result = removeProperty(options, "plugins");
|
|
769
|
+
|
|
770
|
+
assertNotIncludes(result, "plugins");
|
|
771
|
+
assertIncludes(result, "theme");
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test("should handle nested braces", () => {
|
|
775
|
+
const options = "components: { nested: { deep: true } }, theme: 'dark'";
|
|
776
|
+
const result = removeProperty(options, "components");
|
|
777
|
+
|
|
778
|
+
assertNotIncludes(result, "components");
|
|
779
|
+
assertNotIncludes(result, "nested");
|
|
780
|
+
assertIncludes(result, "theme");
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test("should return unchanged if property not found", () => {
|
|
784
|
+
const options = "theme: 'dark', locale: 'en'";
|
|
785
|
+
const result = removeProperty(options, "notfound");
|
|
786
|
+
|
|
787
|
+
assertEqual(result, options);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
test("should handle trailing comma", () => {
|
|
791
|
+
const options = "components: { rBtn }, theme: 'dark'";
|
|
792
|
+
const result = removeProperty(options, "components");
|
|
793
|
+
|
|
794
|
+
assertNotIncludes(result, "components");
|
|
795
|
+
assertIncludes(result, "theme");
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
suite("Plugin Integration", () => {
|
|
800
|
+
test("should test with actual components from components/index.js", () => {
|
|
801
|
+
const componentsPath = path.join(process.cwd(), "components/index.js");
|
|
802
|
+
|
|
803
|
+
if (!fs.existsSync(componentsPath)) {
|
|
804
|
+
console.log(
|
|
805
|
+
` ${colors.yellow}⚠${colors.reset} Skipping - components/index.js not found`,
|
|
806
|
+
);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const parseExports = (filePath, exclude = []) => {
|
|
811
|
+
try {
|
|
812
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
813
|
+
const clean = content
|
|
814
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
815
|
+
.replace(/\/\/.*$/gm, "");
|
|
816
|
+
const regex =
|
|
817
|
+
/export\s+\*\s+as\s+([a-zA-Z_]\w*)\s+from\s+['"]([^'"]+)['"]/g;
|
|
818
|
+
const map = new Map();
|
|
819
|
+
let m;
|
|
820
|
+
while ((m = regex.exec(clean)) !== null) {
|
|
821
|
+
const [, name, relPath] = m;
|
|
822
|
+
if (exclude.includes(name)) continue;
|
|
823
|
+
let p = relPath.replace(/^\.\//, "");
|
|
824
|
+
if (!p.match(/\.(js|vue|ts|mjs)$/)) p = `${p}/index.js`;
|
|
825
|
+
map.set(name, p);
|
|
826
|
+
}
|
|
827
|
+
return map;
|
|
828
|
+
} catch (err) {
|
|
829
|
+
return new Map();
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const componentsMap = parseExports(componentsPath, ["_register"]);
|
|
834
|
+
|
|
835
|
+
assert(componentsMap.size > 0, "Should parse components from index.js");
|
|
836
|
+
assert(
|
|
837
|
+
componentsMap.has("rBtn") ||
|
|
838
|
+
componentsMap.has("rButton") ||
|
|
839
|
+
componentsMap.size > 10,
|
|
840
|
+
"Should contain expected components",
|
|
841
|
+
);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test("should handle transform of main.js-like content", () => {
|
|
845
|
+
const mockCode = `
|
|
846
|
+
import { createApp } from 'vue'
|
|
847
|
+
import App from './App.vue'
|
|
848
|
+
import renusify from 'renusify'
|
|
849
|
+
|
|
850
|
+
const app = createApp(App)
|
|
851
|
+
app.use(renusify, {
|
|
852
|
+
theme: 'dark'
|
|
853
|
+
})
|
|
854
|
+
app.mount('#app')
|
|
855
|
+
`.trim();
|
|
856
|
+
|
|
857
|
+
const state = {
|
|
858
|
+
usedComponents: new Set(["rBtn", "rCard"]),
|
|
859
|
+
usedDirectives: new Set(["vRipple"]),
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const generateImportStatements = (state) => {
|
|
863
|
+
const comps = [...state.usedComponents].sort();
|
|
864
|
+
const dirs = [...state.usedDirectives].sort();
|
|
865
|
+
if (comps.length === 0 && dirs.length === 0) return "";
|
|
866
|
+
let code = "\n";
|
|
867
|
+
if (comps.length > 0) {
|
|
868
|
+
code += `import {\n ${comps.join(",\n ")}\n} from 'renusify/components/index.js'\n`;
|
|
869
|
+
}
|
|
870
|
+
if (dirs.length > 0) {
|
|
871
|
+
const dirNames = dirs.map((name) => {
|
|
872
|
+
if (name.startsWith("v") || name.startsWith("V")) {
|
|
873
|
+
return name.charAt(1).toLowerCase() + name.slice(2);
|
|
874
|
+
}
|
|
875
|
+
return name;
|
|
876
|
+
});
|
|
877
|
+
code += `import { ${dirNames.join(", ")} } from 'renusify/directive/index.js'\n`;
|
|
878
|
+
}
|
|
879
|
+
return code;
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const imports = generateImportStatements(state);
|
|
883
|
+
|
|
884
|
+
assertIncludes(imports, "rBtn");
|
|
885
|
+
assertIncludes(imports, "rCard");
|
|
886
|
+
assertIncludes(imports, "ripple");
|
|
887
|
+
assertIncludes(imports, "components/index.js");
|
|
888
|
+
assertIncludes(imports, "directive/index.js");
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// Run all tests
|
|
893
|
+
console.log(
|
|
894
|
+
`\n${colors.blue}==================================${colors.reset}`,
|
|
895
|
+
);
|
|
896
|
+
console.log(`${colors.blue}Auto-Loader Plugin Tests Complete${colors.reset}`);
|
|
897
|
+
console.log(`${colors.blue}==================================${colors.reset}`);
|
|
898
|
+
console.log(`\nTotal: ${testCount} tests`);
|
|
899
|
+
console.log(
|
|
900
|
+
`${colors.green}Passed: ${passCount}${colors.reset} | ${colors.red}Failed: ${failCount}${colors.reset}`,
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
if (failCount > 0) {
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|