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.
@@ -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
+ });
@@ -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
+ });