create-ripple 0.1.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/LICENSE +21 -0
- package/README.md +50 -0
- package/package.json +51 -0
- package/src/commands/create.js +144 -0
- package/src/constants.js +20 -0
- package/src/index.js +52 -0
- package/src/lib/project-creator.js +206 -0
- package/src/lib/prompts.js +136 -0
- package/src/lib/templates.js +56 -0
- package/src/lib/validation.js +155 -0
- package/tests/integration/cli.test.js +179 -0
- package/tests/integration/project-creator.test.js +215 -0
- package/tests/unit/prompts.test.js +207 -0
- package/tests/unit/templates.test.js +163 -0
- package/tests/unit/validation.test.js +192 -0
- package/vitest.config.js +22 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as prompts from 'prompts';
|
|
3
|
+
|
|
4
|
+
// Mock prompts module
|
|
5
|
+
vi.mock('prompts', () => ({
|
|
6
|
+
default: vi.fn()
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
// Mock kleur colors
|
|
10
|
+
vi.mock('kleur/colors', () => ({
|
|
11
|
+
red: vi.fn((text) => text)
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Mock process.exit
|
|
15
|
+
const mockExit = vi.fn();
|
|
16
|
+
Object.defineProperty(process, 'exit', {
|
|
17
|
+
value: mockExit,
|
|
18
|
+
writable: true
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Mock console.log
|
|
22
|
+
const mockConsoleLog = vi.fn();
|
|
23
|
+
global.console = { ...console, log: mockConsoleLog };
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
promptProjectName,
|
|
27
|
+
promptTemplate,
|
|
28
|
+
promptOverwrite,
|
|
29
|
+
promptPackageManager,
|
|
30
|
+
promptTypeScript,
|
|
31
|
+
promptGitInit
|
|
32
|
+
} from '../../src/lib/prompts.js';
|
|
33
|
+
|
|
34
|
+
describe('Prompts', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
vi.resetAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('promptProjectName', () => {
|
|
44
|
+
it('should return project name when valid input provided', async () => {
|
|
45
|
+
prompts.default.mockResolvedValue({ projectName: 'my-app' });
|
|
46
|
+
|
|
47
|
+
const result = await promptProjectName();
|
|
48
|
+
expect(result).toBe('my-app');
|
|
49
|
+
expect(prompts.default).toHaveBeenCalledWith({
|
|
50
|
+
type: 'text',
|
|
51
|
+
name: 'projectName',
|
|
52
|
+
message: 'What is your project named?',
|
|
53
|
+
initial: 'my-ripple-app',
|
|
54
|
+
validate: expect.any(Function)
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should use custom default name', async () => {
|
|
59
|
+
prompts.default.mockResolvedValue({ projectName: 'custom-app' });
|
|
60
|
+
|
|
61
|
+
await promptProjectName('custom-default');
|
|
62
|
+
expect(prompts.default).toHaveBeenCalledWith(
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
initial: 'custom-default'
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should exit when user cancels', async () => {
|
|
70
|
+
prompts.default.mockResolvedValue({});
|
|
71
|
+
|
|
72
|
+
await promptProjectName();
|
|
73
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
74
|
+
expect(mockConsoleLog).toHaveBeenCalledWith('✖ Operation cancelled');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should validate project name input', async () => {
|
|
78
|
+
prompts.default.mockResolvedValue({ projectName: 'valid-name' });
|
|
79
|
+
|
|
80
|
+
await promptProjectName();
|
|
81
|
+
const call = prompts.default.mock.calls[0][0];
|
|
82
|
+
const validate = call.validate;
|
|
83
|
+
|
|
84
|
+
expect(validate('valid-name')).toBe(true);
|
|
85
|
+
expect(validate('Invalid Name!')).toBe(
|
|
86
|
+
'Project name can only contain lowercase letters, numbers, hyphens, dots, and underscores'
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('promptTemplate', () => {
|
|
92
|
+
it('should return selected template', async () => {
|
|
93
|
+
prompts.default.mockResolvedValue({ template: 'basic' });
|
|
94
|
+
|
|
95
|
+
const result = await promptTemplate();
|
|
96
|
+
expect(result).toBe('basic');
|
|
97
|
+
expect(prompts.default).toHaveBeenCalledWith({
|
|
98
|
+
type: 'select',
|
|
99
|
+
name: 'template',
|
|
100
|
+
message: 'Which template would you like to use?',
|
|
101
|
+
choices: expect.any(Array),
|
|
102
|
+
initial: 0
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should exit when user cancels', async () => {
|
|
107
|
+
prompts.default.mockResolvedValue({});
|
|
108
|
+
|
|
109
|
+
await promptTemplate();
|
|
110
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
111
|
+
expect(mockConsoleLog).toHaveBeenCalledWith('✖ Operation cancelled');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('promptOverwrite', () => {
|
|
116
|
+
it('should return overwrite decision', async () => {
|
|
117
|
+
prompts.default.mockResolvedValue({ overwrite: true });
|
|
118
|
+
|
|
119
|
+
const result = await promptOverwrite('test-project');
|
|
120
|
+
expect(result).toBe(true);
|
|
121
|
+
expect(prompts.default).toHaveBeenCalledWith({
|
|
122
|
+
type: 'confirm',
|
|
123
|
+
name: 'overwrite',
|
|
124
|
+
message: 'Directory "test-project" already exists. Continue anyway?',
|
|
125
|
+
initial: false
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should exit when user cancels', async () => {
|
|
130
|
+
prompts.default.mockResolvedValue({});
|
|
131
|
+
|
|
132
|
+
await promptOverwrite('test-project');
|
|
133
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('promptPackageManager', () => {
|
|
138
|
+
it('should return selected package manager', async () => {
|
|
139
|
+
prompts.default.mockResolvedValue({ packageManager: 'pnpm' });
|
|
140
|
+
|
|
141
|
+
const result = await promptPackageManager();
|
|
142
|
+
expect(result).toBe('pnpm');
|
|
143
|
+
expect(prompts.default).toHaveBeenCalledWith({
|
|
144
|
+
type: 'select',
|
|
145
|
+
name: 'packageManager',
|
|
146
|
+
message: 'Which package manager would you like to use?',
|
|
147
|
+
choices: [
|
|
148
|
+
{ title: 'npm', value: 'npm', description: 'Use npm for dependency management' },
|
|
149
|
+
{ title: 'yarn', value: 'yarn', description: 'Use Yarn for dependency management' },
|
|
150
|
+
{ title: 'pnpm', value: 'pnpm', description: 'Use pnpm for dependency management' }
|
|
151
|
+
],
|
|
152
|
+
initial: 0
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should exit when user cancels', async () => {
|
|
157
|
+
prompts.default.mockResolvedValue({});
|
|
158
|
+
|
|
159
|
+
await promptPackageManager();
|
|
160
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('promptTypeScript', () => {
|
|
165
|
+
it('should return TypeScript preference', async () => {
|
|
166
|
+
prompts.default.mockResolvedValue({ typescript: false });
|
|
167
|
+
|
|
168
|
+
const result = await promptTypeScript();
|
|
169
|
+
expect(result).toBe(false);
|
|
170
|
+
expect(prompts.default).toHaveBeenCalledWith({
|
|
171
|
+
type: 'confirm',
|
|
172
|
+
name: 'typescript',
|
|
173
|
+
message: 'Would you like to use TypeScript?',
|
|
174
|
+
initial: true
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should exit when user cancels', async () => {
|
|
179
|
+
prompts.default.mockResolvedValue({});
|
|
180
|
+
|
|
181
|
+
await promptTypeScript();
|
|
182
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('promptGitInit', () => {
|
|
187
|
+
it('should return Git initialization preference', async () => {
|
|
188
|
+
prompts.default.mockResolvedValue({ gitInit: false });
|
|
189
|
+
|
|
190
|
+
const result = await promptGitInit();
|
|
191
|
+
expect(result).toBe(false);
|
|
192
|
+
expect(prompts.default).toHaveBeenCalledWith({
|
|
193
|
+
type: 'confirm',
|
|
194
|
+
name: 'gitInit',
|
|
195
|
+
message: 'Initialize a new Git repository?',
|
|
196
|
+
initial: true
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should exit when user cancels', async () => {
|
|
201
|
+
prompts.default.mockResolvedValue({});
|
|
202
|
+
|
|
203
|
+
await promptGitInit();
|
|
204
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import {
|
|
4
|
+
getTemplate,
|
|
5
|
+
getTemplateNames,
|
|
6
|
+
getTemplateChoices,
|
|
7
|
+
validateTemplate,
|
|
8
|
+
getTemplatePath,
|
|
9
|
+
} from '../../src/lib/templates.js';
|
|
10
|
+
|
|
11
|
+
// Mock the constants
|
|
12
|
+
vi.mock('../../src/constants.js', () => ({
|
|
13
|
+
TEMPLATES: [
|
|
14
|
+
{
|
|
15
|
+
name: 'basic',
|
|
16
|
+
display: 'Basic Ripple App',
|
|
17
|
+
description: 'A minimal Ripple application with Vite and TypeScript'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'advanced',
|
|
21
|
+
display: 'Advanced Ripple App',
|
|
22
|
+
description: 'A full-featured Ripple application'
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
TEMPLATES_DIR: '/mock/templates'
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Mock fs.existsSync - ensure consistent behavior across environments
|
|
29
|
+
vi.mock('node:fs', () => {
|
|
30
|
+
const mockFn = vi.fn();
|
|
31
|
+
return {
|
|
32
|
+
default: {
|
|
33
|
+
existsSync: mockFn
|
|
34
|
+
},
|
|
35
|
+
existsSync: mockFn
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('getTemplate', () => {
|
|
40
|
+
it('should return template by name', () => {
|
|
41
|
+
const template = getTemplate('basic');
|
|
42
|
+
expect(template).toEqual({
|
|
43
|
+
name: 'basic',
|
|
44
|
+
display: 'Basic Ripple App',
|
|
45
|
+
description: 'A minimal Ripple application with Vite and TypeScript'
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return null for non-existent template', () => {
|
|
50
|
+
const template = getTemplate('non-existent');
|
|
51
|
+
expect(template).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return null for undefined template name', () => {
|
|
55
|
+
const template = getTemplate();
|
|
56
|
+
expect(template).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('getTemplateNames', () => {
|
|
61
|
+
it('should return array of template names', () => {
|
|
62
|
+
const names = getTemplateNames();
|
|
63
|
+
expect(names).toEqual(['basic', 'advanced']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return array even if no templates exist', () => {
|
|
67
|
+
const names = getTemplateNames();
|
|
68
|
+
expect(Array.isArray(names)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('getTemplateChoices', () => {
|
|
73
|
+
it('should return formatted choices for prompts', () => {
|
|
74
|
+
const choices = getTemplateChoices();
|
|
75
|
+
expect(choices).toEqual([
|
|
76
|
+
{
|
|
77
|
+
title: 'Basic Ripple App',
|
|
78
|
+
description: 'A minimal Ripple application with Vite and TypeScript',
|
|
79
|
+
value: 'basic'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
title: 'Advanced Ripple App',
|
|
83
|
+
description: 'A full-featured Ripple application',
|
|
84
|
+
value: 'advanced'
|
|
85
|
+
}
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return array even if templates are defined', () => {
|
|
90
|
+
const choices = getTemplateChoices();
|
|
91
|
+
expect(Array.isArray(choices)).toBe(true);
|
|
92
|
+
expect(choices.length).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('validateTemplate', () => {
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
vi.clearAllMocks();
|
|
99
|
+
// Reset the mock to a default state for CI compatibility
|
|
100
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
101
|
+
mockExistsSync.mockReturnValue(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should return true for valid existing template', () => {
|
|
105
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
106
|
+
mockExistsSync.mockReturnValue(true);
|
|
107
|
+
const isValid = validateTemplate('basic');
|
|
108
|
+
expect(isValid).toBe(true);
|
|
109
|
+
expect(mockExistsSync).toHaveBeenCalledWith('/mock/templates/basic');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return false for valid template that does not exist on filesystem', () => {
|
|
113
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
114
|
+
mockExistsSync.mockReturnValue(false);
|
|
115
|
+
const isValid = validateTemplate('basic');
|
|
116
|
+
expect(isValid).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return false for invalid template name', () => {
|
|
120
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
121
|
+
const isValid = validateTemplate('non-existent');
|
|
122
|
+
expect(isValid).toBe(false);
|
|
123
|
+
expect(mockExistsSync).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should return false for undefined template name', () => {
|
|
127
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
128
|
+
const isValid = validateTemplate();
|
|
129
|
+
expect(isValid).toBe(false);
|
|
130
|
+
expect(mockExistsSync).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should return false for null template name', () => {
|
|
134
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
135
|
+
const isValid = validateTemplate(null);
|
|
136
|
+
expect(isValid).toBe(false);
|
|
137
|
+
expect(mockExistsSync).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should return false for empty string template name', () => {
|
|
141
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
142
|
+
const isValid = validateTemplate('');
|
|
143
|
+
expect(isValid).toBe(false);
|
|
144
|
+
expect(mockExistsSync).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('getTemplatePath', () => {
|
|
149
|
+
it('should return correct template path', () => {
|
|
150
|
+
const path = getTemplatePath('basic');
|
|
151
|
+
expect(path).toBe('/mock/templates/basic');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should return path even for non-existent template', () => {
|
|
155
|
+
const path = getTemplatePath('non-existent');
|
|
156
|
+
expect(path).toBe('/mock/templates/non-existent');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should handle special characters in template name', () => {
|
|
160
|
+
const path = getTemplatePath('my-template.name');
|
|
161
|
+
expect(path).toBe('/mock/templates/my-template.name');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateProjectName, sanitizeDirectoryName, validateDirectoryPath } from '../../src/lib/validation.js';
|
|
3
|
+
|
|
4
|
+
describe('validateProjectName', () => {
|
|
5
|
+
it('should validate correct project names', () => {
|
|
6
|
+
const validNames = [
|
|
7
|
+
'my-app',
|
|
8
|
+
'my.app',
|
|
9
|
+
'my_app',
|
|
10
|
+
'myapp',
|
|
11
|
+
'my-awesome-app',
|
|
12
|
+
'app123',
|
|
13
|
+
'a',
|
|
14
|
+
'a'.repeat(214) // max length
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
validNames.forEach(name => {
|
|
18
|
+
const result = validateProjectName(name);
|
|
19
|
+
expect(result.valid).toBe(true);
|
|
20
|
+
expect(result.message).toBe('');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should reject invalid project names', () => {
|
|
25
|
+
const invalidCases = [
|
|
26
|
+
{ name: '', expectedMessage: 'Project name cannot be empty' },
|
|
27
|
+
{ name: ' ', expectedMessage: 'Project name cannot be empty' },
|
|
28
|
+
{ name: null, expectedMessage: 'Project name is required' },
|
|
29
|
+
{ name: undefined, expectedMessage: 'Project name is required' },
|
|
30
|
+
{ name: 123, expectedMessage: 'Project name is required' },
|
|
31
|
+
{ name: 'a'.repeat(215), expectedMessage: 'Project name must be less than 214 characters' }
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
invalidCases.forEach(({ name, expectedMessage }) => {
|
|
35
|
+
const result = validateProjectName(name);
|
|
36
|
+
expect(result.valid).toBe(false);
|
|
37
|
+
expect(result.message).toBe(expectedMessage);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should reject names with invalid characters', () => {
|
|
42
|
+
const invalidNames = [
|
|
43
|
+
'My-App', // uppercase
|
|
44
|
+
'my app', // space
|
|
45
|
+
'my@app', // special character
|
|
46
|
+
'my/app', // slash
|
|
47
|
+
'my\\app', // backslash
|
|
48
|
+
'my:app', // colon
|
|
49
|
+
'my*app', // asterisk
|
|
50
|
+
'my?app', // question mark
|
|
51
|
+
'my"app', // quote
|
|
52
|
+
'my<app', // less than
|
|
53
|
+
'my>app' // greater than
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
invalidNames.forEach(name => {
|
|
57
|
+
const result = validateProjectName(name);
|
|
58
|
+
expect(result.valid).toBe(false);
|
|
59
|
+
expect(result.message).toBe(
|
|
60
|
+
'Project name can only contain lowercase letters, numbers, hyphens, dots, and underscores'
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should reject names starting with dot or underscore', () => {
|
|
66
|
+
const invalidNames = ['.my-app', '_my-app'];
|
|
67
|
+
|
|
68
|
+
invalidNames.forEach(name => {
|
|
69
|
+
const result = validateProjectName(name);
|
|
70
|
+
expect(result.valid).toBe(false);
|
|
71
|
+
expect(result.message).toBe('Project name cannot start with a dot or underscore');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should reject names ending with dot', () => {
|
|
76
|
+
const result = validateProjectName('my-app.');
|
|
77
|
+
expect(result.valid).toBe(false);
|
|
78
|
+
expect(result.message).toBe('Project name cannot end with a dot');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should reject names with consecutive dots', () => {
|
|
82
|
+
const result = validateProjectName('my..app');
|
|
83
|
+
expect(result.valid).toBe(false);
|
|
84
|
+
expect(result.message).toBe('Project name cannot contain consecutive dots');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should reject reserved names', () => {
|
|
88
|
+
const reservedNames = [
|
|
89
|
+
'node_modules',
|
|
90
|
+
'favicon.ico',
|
|
91
|
+
'con',
|
|
92
|
+
'prn',
|
|
93
|
+
'aux',
|
|
94
|
+
'nul',
|
|
95
|
+
'com1',
|
|
96
|
+
'com2',
|
|
97
|
+
'lpt1',
|
|
98
|
+
'lpt9'
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
reservedNames.forEach(name => {
|
|
102
|
+
const result = validateProjectName(name);
|
|
103
|
+
expect(result.valid).toBe(false);
|
|
104
|
+
expect(result.message).toBe(`"${name}" is a reserved name and cannot be used`);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle case insensitive reserved names', () => {
|
|
109
|
+
const result = validateProjectName('con'); // use lowercase since validation requires lowercase
|
|
110
|
+
expect(result.valid).toBe(false);
|
|
111
|
+
expect(result.message).toBe('"con" is a reserved name and cannot be used');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('sanitizeDirectoryName', () => {
|
|
116
|
+
it('should sanitize directory names correctly', () => {
|
|
117
|
+
const testCases = [
|
|
118
|
+
{ input: 'My App', expected: 'my-app' },
|
|
119
|
+
{ input: 'my@app#name', expected: 'my-app-name' },
|
|
120
|
+
{ input: '---my-app---', expected: 'my-app' },
|
|
121
|
+
{ input: 'my___app', expected: 'my-app' },
|
|
122
|
+
{ input: 'MY-APP', expected: 'my-app' },
|
|
123
|
+
{ input: 'app123!@#', expected: 'app123' },
|
|
124
|
+
{ input: ' spaces ', expected: 'spaces' },
|
|
125
|
+
{ input: 'special$%^chars', expected: 'special-chars' }
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
testCases.forEach(({ input, expected }) => {
|
|
129
|
+
const result = sanitizeDirectoryName(input);
|
|
130
|
+
expect(result).toBe(expected);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle edge cases', () => {
|
|
135
|
+
expect(sanitizeDirectoryName('')).toBe('');
|
|
136
|
+
expect(sanitizeDirectoryName('---')).toBe('');
|
|
137
|
+
expect(sanitizeDirectoryName('123')).toBe('123');
|
|
138
|
+
expect(sanitizeDirectoryName('a')).toBe('a');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('validateDirectoryPath', () => {
|
|
143
|
+
it('should validate correct directory paths', () => {
|
|
144
|
+
const validPaths = [
|
|
145
|
+
'my-app',
|
|
146
|
+
'./my-app',
|
|
147
|
+
'../my-app',
|
|
148
|
+
'path/to/my-app',
|
|
149
|
+
'/home/user/projects/my-app'
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
validPaths.forEach(path => {
|
|
153
|
+
const result = validateDirectoryPath(path);
|
|
154
|
+
expect(result.valid).toBe(true);
|
|
155
|
+
expect(result.message).toBe('');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should reject invalid directory paths', () => {
|
|
160
|
+
const invalidCases = [
|
|
161
|
+
{ path: '', expectedMessage: 'Directory path is required' },
|
|
162
|
+
{ path: null, expectedMessage: 'Directory path is required' },
|
|
163
|
+
{ path: undefined, expectedMessage: 'Directory path is required' },
|
|
164
|
+
{ path: 123, expectedMessage: 'Directory path is required' },
|
|
165
|
+
{ path: '/', expectedMessage: 'Cannot create project in root directory' }
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
invalidCases.forEach(({ path, expectedMessage }) => {
|
|
169
|
+
const result = validateDirectoryPath(path);
|
|
170
|
+
expect(result.valid).toBe(false);
|
|
171
|
+
expect(result.message).toBe(expectedMessage);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should reject paths with invalid characters', () => {
|
|
176
|
+
const invalidPaths = [
|
|
177
|
+
'my<app',
|
|
178
|
+
'my>app',
|
|
179
|
+
'my:app',
|
|
180
|
+
'my"app',
|
|
181
|
+
'my|app',
|
|
182
|
+
'my?app',
|
|
183
|
+
'my*app'
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
invalidPaths.forEach(path => {
|
|
187
|
+
const result = validateDirectoryPath(path);
|
|
188
|
+
expect(result.valid).toBe(false);
|
|
189
|
+
expect(result.message).toBe('Directory path contains invalid characters');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
package/vitest.config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
include: ['tests/**/*.test.js'],
|
|
6
|
+
environment: 'node',
|
|
7
|
+
globals: true,
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
reporter: ['text', 'json', 'html'],
|
|
11
|
+
exclude: [
|
|
12
|
+
'node_modules/',
|
|
13
|
+
'tests/',
|
|
14
|
+
'coverage/',
|
|
15
|
+
'**/*.test.js',
|
|
16
|
+
'**/*.config.js'
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
testTimeout: 30000, // 30 seconds for integration tests
|
|
20
|
+
hookTimeout: 10000 // 10 seconds for setup/teardown
|
|
21
|
+
}
|
|
22
|
+
});
|