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,56 @@
1
+ import { join } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { TEMPLATES, TEMPLATES_DIR } from '../constants.js';
4
+
5
+ /**
6
+ * Get template by name
7
+ * @param {string} templateName - The template name
8
+ * @returns {object|null} - Template object or null if not found
9
+ */
10
+ export function getTemplate(templateName) {
11
+ return TEMPLATES.find((template) => template.name === templateName) || null;
12
+ }
13
+
14
+ /**
15
+ * Get all available template names
16
+ * @returns {string[]} - Array of template names
17
+ */
18
+ export function getTemplateNames() {
19
+ return TEMPLATES.map((template) => template.name);
20
+ }
21
+
22
+ /**
23
+ * Get template choices for prompts
24
+ * @returns {object[]} - Array of choice objects for prompts
25
+ */
26
+ export function getTemplateChoices() {
27
+ return TEMPLATES.map((template) => ({
28
+ title: template.display,
29
+ description: template.description,
30
+ value: template.name
31
+ }));
32
+ }
33
+
34
+ /**
35
+ * Validate if template exists
36
+ * @param {string} templateName - The template name to validate
37
+ * @returns {boolean} - True if template exists
38
+ */
39
+ export function validateTemplate(templateName) {
40
+ if (!templateName) return false;
41
+
42
+ const template = getTemplate(templateName);
43
+ if (!template) return false;
44
+
45
+ const templatePath = join(TEMPLATES_DIR, templateName);
46
+ return existsSync(templatePath);
47
+ }
48
+
49
+ /**
50
+ * Get template directory path
51
+ * @param {string} templateName - The template name
52
+ * @returns {string} - Absolute path to template directory
53
+ */
54
+ export function getTemplatePath(templateName) {
55
+ return join(TEMPLATES_DIR, templateName);
56
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Validation utilities for project creation
3
+ */
4
+
5
+ /**
6
+ * Validates a project name according to npm package naming rules
7
+ * @param {string} name - The project name to validate
8
+ * @returns {object} - Object with valid boolean and message string
9
+ */
10
+ export function validateProjectName(name) {
11
+ if (typeof name !== 'string' || name === null || name === undefined) {
12
+ return {
13
+ valid: false,
14
+ message: 'Project name is required'
15
+ };
16
+ }
17
+
18
+ name = name.trim();
19
+
20
+ if (name.length === 0) {
21
+ return {
22
+ valid: false,
23
+ message: 'Project name cannot be empty'
24
+ };
25
+ }
26
+
27
+ // Check length (npm package names have a 214 character limit)
28
+ if (name.length > 214) {
29
+ return {
30
+ valid: false,
31
+ message: 'Project name must be less than 214 characters'
32
+ };
33
+ }
34
+
35
+ // Check for valid characters (npm allows lowercase letters, numbers, hyphens, and dots)
36
+ if (!/^[a-z0-9._-]+$/.test(name)) {
37
+ return {
38
+ valid: false,
39
+ message:
40
+ 'Project name can only contain lowercase letters, numbers, hyphens, dots, and underscores'
41
+ };
42
+ }
43
+
44
+ // Cannot start with dot or underscore
45
+ if (name.startsWith('.') || name.startsWith('_')) {
46
+ return {
47
+ valid: false,
48
+ message: 'Project name cannot start with a dot or underscore'
49
+ };
50
+ }
51
+
52
+ // Cannot end with dot
53
+ if (name.endsWith('.')) {
54
+ return {
55
+ valid: false,
56
+ message: 'Project name cannot end with a dot'
57
+ };
58
+ }
59
+
60
+ // Cannot contain consecutive dots
61
+ if (name.includes('..')) {
62
+ return {
63
+ valid: false,
64
+ message: 'Project name cannot contain consecutive dots'
65
+ };
66
+ }
67
+
68
+ // Reserved names
69
+ const reservedNames = [
70
+ 'node_modules',
71
+ 'favicon.ico',
72
+ 'con',
73
+ 'prn',
74
+ 'aux',
75
+ 'nul',
76
+ 'com1',
77
+ 'com2',
78
+ 'com3',
79
+ 'com4',
80
+ 'com5',
81
+ 'com6',
82
+ 'com7',
83
+ 'com8',
84
+ 'com9',
85
+ 'lpt1',
86
+ 'lpt2',
87
+ 'lpt3',
88
+ 'lpt4',
89
+ 'lpt5',
90
+ 'lpt6',
91
+ 'lpt7',
92
+ 'lpt8',
93
+ 'lpt9'
94
+ ];
95
+
96
+ if (reservedNames.includes(name.toLowerCase())) {
97
+ return {
98
+ valid: false,
99
+ message: `"${name}" is a reserved name and cannot be used`
100
+ };
101
+ }
102
+
103
+ return {
104
+ valid: true,
105
+ message: ''
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Converts a project name to a valid directory name
111
+ * @param {string} name - The project name
112
+ * @returns {string} - A valid directory name
113
+ */
114
+ export function sanitizeDirectoryName(name) {
115
+ return name
116
+ .toLowerCase()
117
+ .replace(/[^a-z0-9-]/g, '-')
118
+ .replace(/^-+|-+$/g, '')
119
+ .replace(/-+/g, '-');
120
+ }
121
+
122
+ /**
123
+ * Validates directory path and checks if it's writable
124
+ * @param {string} path - The directory path to validate
125
+ * @returns {object} - Object with valid boolean and message string
126
+ */
127
+ export function validateDirectoryPath(path) {
128
+ if (!path || typeof path !== 'string') {
129
+ return {
130
+ valid: false,
131
+ message: 'Directory path is required'
132
+ };
133
+ }
134
+
135
+ // Check if path is absolute or relative
136
+ if (path.startsWith('/') && path.length < 2) {
137
+ return {
138
+ valid: false,
139
+ message: 'Cannot create project in root directory'
140
+ };
141
+ }
142
+
143
+ // Check for invalid characters in path
144
+ if (/[<>:"|?*]/.test(path)) {
145
+ return {
146
+ valid: false,
147
+ message: 'Directory path contains invalid characters'
148
+ };
149
+ }
150
+
151
+ return {
152
+ valid: true,
153
+ message: ''
154
+ };
155
+ }
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { dirname } from 'node:path';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ const CLI_PATH = join(__dirname, '../../src/index.js');
13
+
14
+ describe('CLI Integration Tests', () => {
15
+ let testDir;
16
+
17
+ beforeEach(() => {
18
+ testDir = join(tmpdir(), `cli-test-${Date.now()}`);
19
+ mkdirSync(testDir, { recursive: true });
20
+ });
21
+
22
+ afterEach(() => {
23
+ if (existsSync(testDir)) {
24
+ rmSync(testDir, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ // Helper function to run CLI commands
29
+ const runCLI = (args = [], input = '', timeout = 10000) => {
30
+ return new Promise((resolve, reject) => {
31
+ const child = spawn('node', [CLI_PATH, ...args], {
32
+ cwd: testDir,
33
+ stdio: 'pipe'
34
+ });
35
+
36
+ let stdout = '';
37
+ let stderr = '';
38
+
39
+ child.stdout.on('data', (data) => {
40
+ stdout += data.toString();
41
+ });
42
+
43
+ child.stderr.on('data', (data) => {
44
+ stderr += data.toString();
45
+ });
46
+
47
+ child.on('close', (code) => {
48
+ resolve({ code, stdout, stderr });
49
+ });
50
+
51
+ child.on('error', reject);
52
+
53
+ if (input) {
54
+ child.stdin.write(input);
55
+ }
56
+ child.stdin.end();
57
+
58
+ setTimeout(() => {
59
+ child.kill();
60
+ reject(new Error('Command timed out'));
61
+ }, timeout);
62
+ });
63
+ };
64
+
65
+ it('should show help when --help flag is used', async () => {
66
+ const result = await runCLI(['--help']);
67
+
68
+ expect(result.code).toBe(0);
69
+ expect(result.stdout).toContain('Interactive CLI tool for creating Ripple applications');
70
+ expect(result.stdout).toContain('Usage: create-ripple-app');
71
+ expect(result.stdout).toContain('Arguments:');
72
+ expect(result.stdout).toContain('Options:');
73
+ expect(result.stdout).toContain('--template');
74
+ expect(result.stdout).toContain('--package-manager');
75
+ });
76
+
77
+ it('should show version when --version flag is used', async () => {
78
+ const result = await runCLI(['--version']);
79
+
80
+ expect(result.code).toBe(0);
81
+ expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
82
+ });
83
+
84
+ it('should create project with all arguments provided', async () => {
85
+ const projectName = 'test-cli-project';
86
+ const result = await runCLI([
87
+ projectName,
88
+ '--template', 'basic',
89
+ '--package-manager', 'npm',
90
+ '--no-git',
91
+ '--yes'
92
+ ]);
93
+
94
+ expect(result.code).toBe(0);
95
+ expect(result.stdout).toContain('Welcome to Create Ripple App');
96
+ expect(result.stdout).toContain('Creating Ripple app');
97
+ expect(result.stdout).toContain('Project created successfully');
98
+ expect(result.stdout).toContain('Next steps:');
99
+ expect(result.stdout).toContain(`cd ${projectName}`);
100
+ expect(result.stdout).toContain('npm install');
101
+ expect(result.stdout).toContain('npm run dev');
102
+
103
+ expect(existsSync(join(testDir, projectName))).toBe(true);
104
+ expect(existsSync(join(testDir, projectName, 'package.json'))).toBe(true);
105
+ });
106
+
107
+ it('should handle invalid template gracefully', async () => {
108
+ const result = await runCLI([
109
+ 'test-project',
110
+ '--template', 'invalid-template',
111
+ '--yes'
112
+ ]);
113
+
114
+ expect(result.code).toBe(1);
115
+ expect(result.stderr).toContain('Template "invalid-template" not found');
116
+ expect(result.stderr).toContain('Available templates:');
117
+ });
118
+
119
+ it('should handle invalid project name gracefully', async () => {
120
+ const result = await runCLI([
121
+ 'Invalid Project Name!',
122
+ '--yes'
123
+ ]);
124
+
125
+ expect(result.code).toBe(1);
126
+ expect(result.stderr).toContain('Project name can only contain lowercase letters');
127
+ });
128
+
129
+ it('should show different package manager commands based on selection', async () => {
130
+ const projectName = 'test-pnpm-project';
131
+ const result = await runCLI([
132
+ projectName,
133
+ '--template', 'basic',
134
+ '--package-manager', 'pnpm',
135
+ '--yes'
136
+ ]);
137
+
138
+ expect(result.code).toBe(0);
139
+ expect(result.stdout).toContain('pnpm install');
140
+ expect(result.stdout).toContain('pnpm dev');
141
+ });
142
+
143
+ it('should handle yarn package manager', async () => {
144
+ const projectName = 'test-yarn-project';
145
+ const result = await runCLI([
146
+ projectName,
147
+ '--template', 'basic',
148
+ '--package-manager', 'yarn',
149
+ '--yes'
150
+ ]);
151
+
152
+ expect(result.code).toBe(0);
153
+ expect(result.stdout).toContain('yarn install');
154
+ expect(result.stdout).toContain('yarn dev');
155
+ });
156
+
157
+ it('should handle project directory that already exists (with --yes)', async () => {
158
+ const projectName = 'existing-project';
159
+ const projectPath = join(testDir, projectName);
160
+
161
+ mkdirSync(projectPath, { recursive: true });
162
+ writeFileSync(join(projectPath, 'existing-file.txt'), 'test');
163
+
164
+ const result = await runCLI([
165
+ projectName,
166
+ '--template', 'basic',
167
+ '--yes'
168
+ ]);
169
+
170
+ expect(result.code).toBe(0);
171
+ expect(result.stdout).toContain('Project created successfully');
172
+ });
173
+
174
+ it('should validate all required dependencies are available', async () => {
175
+ const result = await runCLI(['--help']);
176
+ expect(result.code).toBe(0);
177
+ expect(result.stderr).toBe('');
178
+ });
179
+ });
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { createProject } from '../../src/lib/project-creator.js';
6
+
7
+ // Mock ora for cleaner test output
8
+ vi.mock('ora', () => ({
9
+ default: () => ({
10
+ start: () => ({ succeed: vi.fn(), fail: vi.fn(), warn: vi.fn() }),
11
+ succeed: vi.fn(),
12
+ fail: vi.fn(),
13
+ warn: vi.fn()
14
+ })
15
+ }));
16
+
17
+ // Mock execSync to prevent actual git commands during tests
18
+ vi.mock('node:child_process', () => ({
19
+ default: {
20
+ execSync: vi.fn()
21
+ },
22
+ execSync: vi.fn()
23
+ }));
24
+
25
+ describe('createProject integration tests', () => {
26
+ let testDir;
27
+ let projectPath;
28
+ let templatePath;
29
+
30
+ beforeEach(() => {
31
+ // Create a temporary test directory
32
+ testDir = join(tmpdir(), `create-ripple-test-${Date.now()}`);
33
+ mkdirSync(testDir, { recursive: true });
34
+
35
+ projectPath = join(testDir, 'test-project');
36
+ templatePath = join(testDir, 'template');
37
+
38
+ // Create a mock template directory structure
39
+ mkdirSync(templatePath, { recursive: true });
40
+ mkdirSync(join(templatePath, 'src'), { recursive: true });
41
+
42
+ // Create mock template files
43
+ writeFileSync(
44
+ join(templatePath, 'package.json'),
45
+ JSON.stringify({
46
+ name: 'vite-template-ripple',
47
+ version: '0.0.0',
48
+ type: 'module',
49
+ scripts: {
50
+ dev: 'vite',
51
+ build: 'vite build'
52
+ },
53
+ dependencies: {
54
+ ripple: '^0.2.29'
55
+ },
56
+ devDependencies: {
57
+ 'vite-plugin-ripple': '^0.2.29',
58
+ prettier: '^3.6.2'
59
+ }
60
+ }, null, 2)
61
+ );
62
+
63
+ writeFileSync(join(templatePath, 'index.html'), '<!DOCTYPE html><html></html>');
64
+ writeFileSync(join(templatePath, 'src', 'App.ripple'), '<h1>Hello Ripple!</h1>');
65
+ writeFileSync(join(templatePath, 'README.md'), '# Template Project');
66
+
67
+ // Mock the getTemplatePath function
68
+ vi.doMock('../../src/lib/templates.js', () => ({
69
+ getTemplatePath: () => templatePath
70
+ }));
71
+ });
72
+
73
+ afterEach(() => {
74
+ // Clean up test directory
75
+ if (existsSync(testDir)) {
76
+ rmSync(testDir, { recursive: true, force: true });
77
+ }
78
+ vi.clearAllMocks();
79
+ });
80
+
81
+ it('should create a project successfully', async () => {
82
+ await createProject({
83
+ projectName: 'test-project',
84
+ projectPath,
85
+ template: 'basic',
86
+ packageManager: 'npm',
87
+ typescript: true,
88
+ gitInit: false
89
+ });
90
+
91
+ // Verify project directory was created
92
+ expect(existsSync(projectPath)).toBe(true);
93
+
94
+ // Verify files were copied
95
+ expect(existsSync(join(projectPath, 'package.json'))).toBe(true);
96
+ expect(existsSync(join(projectPath, 'index.html'))).toBe(true);
97
+ expect(existsSync(join(projectPath, 'src', 'App.ripple'))).toBe(true);
98
+ expect(existsSync(join(projectPath, 'README.md'))).toBe(true);
99
+
100
+ // Verify package.json was updated
101
+ const packageJson = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf-8'));
102
+ expect(packageJson.name).toBe('test-project');
103
+ expect(packageJson.description).toBe('A Ripple application created with create-ripple-app');
104
+ expect(packageJson.version).toBe('1.0.0');
105
+ });
106
+
107
+ it('should update package.json with correct package manager', async () => {
108
+ await createProject({
109
+ projectName: 'test-pnpm-project',
110
+ projectPath,
111
+ template: 'basic',
112
+ packageManager: 'pnpm',
113
+ typescript: true,
114
+ gitInit: false
115
+ });
116
+
117
+ const packageJson = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf-8'));
118
+ expect(packageJson.packageManager).toBe('pnpm@9.0.0');
119
+ });
120
+
121
+ it('should not add packageManager field for npm', async () => {
122
+ await createProject({
123
+ projectName: 'test-npm-project',
124
+ projectPath,
125
+ template: 'basic',
126
+ packageManager: 'npm',
127
+ typescript: true,
128
+ gitInit: false
129
+ });
130
+
131
+ const packageJson = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf-8'));
132
+ expect(packageJson.packageManager).toBeUndefined();
133
+ });
134
+
135
+ it('should update dependency versions', async () => {
136
+ await createProject({
137
+ projectName: 'test-deps-project',
138
+ projectPath,
139
+ template: 'basic',
140
+ packageManager: 'npm',
141
+ typescript: true,
142
+ gitInit: false
143
+ });
144
+
145
+ const packageJson = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf-8'));
146
+ expect(packageJson.dependencies.ripple).toBe('^0.2.35');
147
+ expect(packageJson.devDependencies['vite-plugin-ripple']).toBe('^0.2.29');
148
+ });
149
+
150
+ it('should handle missing template directory', async () => {
151
+ const invalidTemplatePath = join(testDir, 'non-existent-template');
152
+
153
+ vi.doMock('../../src/lib/templates.js', () => ({
154
+ getTemplatePath: () => invalidTemplatePath
155
+ }));
156
+
157
+ await expect(
158
+ createProject({
159
+ projectName: 'test-project',
160
+ projectPath,
161
+ template: 'non-existent',
162
+ packageManager: 'npm',
163
+ typescript: true,
164
+ gitInit: false
165
+ })
166
+ ).rejects.toThrow('Template "non-existent" not found');
167
+ });
168
+
169
+ it('should filter out unwanted files during copy', async () => {
170
+ // Add files that should be filtered out
171
+ mkdirSync(join(templatePath, 'node_modules'), { recursive: true });
172
+ writeFileSync(join(templatePath, 'node_modules', 'some-package.js'), 'module content');
173
+ writeFileSync(join(templatePath, 'package-lock.json'), '{}');
174
+ writeFileSync(join(templatePath, 'yarn.lock'), 'yarn lock content');
175
+ writeFileSync(join(templatePath, 'pnpm-lock.yaml'), 'pnpm lock content');
176
+
177
+ await createProject({
178
+ projectName: 'test-filter-project',
179
+ projectPath,
180
+ template: 'basic',
181
+ packageManager: 'npm',
182
+ typescript: true,
183
+ gitInit: false
184
+ });
185
+
186
+ // Verify filtered files were not copied
187
+ expect(existsSync(join(projectPath, 'node_modules'))).toBe(false);
188
+ expect(existsSync(join(projectPath, 'package-lock.json'))).toBe(false);
189
+ expect(existsSync(join(projectPath, 'yarn.lock'))).toBe(false);
190
+ expect(existsSync(join(projectPath, 'pnpm-lock.yaml'))).toBe(false);
191
+
192
+ // Verify other files were copied
193
+ expect(existsSync(join(projectPath, 'package.json'))).toBe(true);
194
+ expect(existsSync(join(projectPath, 'index.html'))).toBe(true);
195
+ });
196
+
197
+ it('should handle project creation in existing directory', async () => {
198
+ // Create the directory first
199
+ mkdirSync(projectPath, { recursive: true });
200
+ writeFileSync(join(projectPath, 'existing-file.txt'), 'existing content');
201
+
202
+ await createProject({
203
+ projectName: 'test-existing-project',
204
+ projectPath,
205
+ template: 'basic',
206
+ packageManager: 'npm',
207
+ typescript: true,
208
+ gitInit: false
209
+ });
210
+
211
+ // Verify project was created successfully
212
+ expect(existsSync(join(projectPath, 'package.json'))).toBe(true);
213
+ expect(existsSync(join(projectPath, 'existing-file.txt'))).toBe(true);
214
+ });
215
+ });