@vue-skuilder/cli 0.1.3 → 0.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.
@@ -1,6 +1,78 @@
1
1
  import inquirer from 'inquirer';
2
2
  import chalk from 'chalk';
3
- import { CliOptions, ProjectConfig, PREDEFINED_THEMES, ThemeConfig } from '../types.js';
3
+ import { CliOptions, ProjectConfig, PREDEFINED_THEMES } from '../types.js';
4
+
5
+ /**
6
+ * Convert hex color to closest ANSI color code
7
+ */
8
+ function hexToAnsi(hex: string): string {
9
+ // Remove # if present
10
+ hex = hex.replace('#', '');
11
+
12
+ // Convert hex to RGB
13
+ const r = parseInt(hex.substr(0, 2), 16);
14
+ const g = parseInt(hex.substr(2, 2), 16);
15
+ const b = parseInt(hex.substr(4, 2), 16);
16
+
17
+ // Convert to 256-color ANSI
18
+ const ansiCode = 16 + (36 * Math.round(r / 255 * 5)) + (6 * Math.round(g / 255 * 5)) + Math.round(b / 255 * 5);
19
+ return `\x1b[48;5;${ansiCode}m`;
20
+ }
21
+
22
+ /**
23
+ * Create a color swatch for terminal display
24
+ */
25
+ function createColorSwatch(hex: string, label: string): string {
26
+ const colorCode = hexToAnsi(hex);
27
+ const reset = '\x1b[0m';
28
+ return `${colorCode} ${reset} ${label}`;
29
+ }
30
+
31
+ /**
32
+ * Create theme preview with color swatches
33
+ */
34
+ function createThemePreview(themeName: string): string {
35
+ const theme = PREDEFINED_THEMES[themeName];
36
+ const lightColors = theme.light.colors;
37
+
38
+ const primarySwatch = createColorSwatch(lightColors.primary, 'Primary');
39
+ const secondarySwatch = createColorSwatch(lightColors.secondary, 'Secondary');
40
+ const accentSwatch = createColorSwatch(lightColors.accent, 'Accent');
41
+
42
+ return `${primarySwatch} ${secondarySwatch} ${accentSwatch}`;
43
+ }
44
+
45
+ /**
46
+ * Display comprehensive theme preview after selection
47
+ */
48
+ export function displayThemePreview(themeName: string): void {
49
+ const theme = PREDEFINED_THEMES[themeName];
50
+
51
+ console.log(chalk.cyan('\nšŸŽØ Theme Color Palette:'));
52
+ console.log(chalk.white(` ${theme.name.toUpperCase()} THEME`));
53
+
54
+ // Light theme colors
55
+ console.log(chalk.white('\n Light Mode:'));
56
+ const lightColors = theme.light.colors;
57
+ console.log(` ${createColorSwatch(lightColors.primary, `Primary: ${lightColors.primary}`)}`);
58
+ console.log(` ${createColorSwatch(lightColors.secondary, `Secondary: ${lightColors.secondary}`)}`);
59
+ console.log(` ${createColorSwatch(lightColors.accent, `Accent: ${lightColors.accent}`)}`);
60
+ console.log(` ${createColorSwatch(lightColors.success, `Success: ${lightColors.success}`)}`);
61
+ console.log(` ${createColorSwatch(lightColors.warning, `Warning: ${lightColors.warning}`)}`);
62
+ console.log(` ${createColorSwatch(lightColors.error, `Error: ${lightColors.error}`)}`);
63
+
64
+ // Dark theme colors
65
+ console.log(chalk.white('\n Dark Mode:'));
66
+ const darkColors = theme.dark.colors;
67
+ console.log(` ${createColorSwatch(darkColors.primary, `Primary: ${darkColors.primary}`)}`);
68
+ console.log(` ${createColorSwatch(darkColors.secondary, `Secondary: ${darkColors.secondary}`)}`);
69
+ console.log(` ${createColorSwatch(darkColors.accent, `Accent: ${darkColors.accent}`)}`);
70
+ console.log(` ${createColorSwatch(darkColors.success, `Success: ${darkColors.success}`)}`);
71
+ console.log(` ${createColorSwatch(darkColors.warning, `Warning: ${darkColors.warning}`)}`);
72
+ console.log(` ${createColorSwatch(darkColors.error, `Error: ${darkColors.error}`)}`);
73
+
74
+ console.log(chalk.gray(`\n Default mode: ${theme.defaultMode}`));
75
+ }
4
76
 
5
77
  export async function gatherProjectConfig(
6
78
  projectName: string,
@@ -65,19 +137,19 @@ export async function gatherProjectConfig(
65
137
  message: 'Select theme:',
66
138
  choices: [
67
139
  {
68
- name: 'Default (Material Blue)',
140
+ name: `Default (Material Blue) ${createThemePreview('default')}`,
69
141
  value: 'default'
70
142
  },
71
143
  {
72
- name: 'Medical (Healthcare Green)',
144
+ name: `Medical (Healthcare Green) ${createThemePreview('medical')}`,
73
145
  value: 'medical'
74
146
  },
75
147
  {
76
- name: 'Educational (Academic Orange)',
148
+ name: `Educational (Academic Orange) ${createThemePreview('educational')}`,
77
149
  value: 'educational'
78
150
  },
79
151
  {
80
- name: 'Corporate (Professional Gray)',
152
+ name: `Corporate (Professional Gray) ${createThemePreview('corporate')}`,
81
153
  value: 'corporate'
82
154
  }
83
155
  ],
@@ -93,6 +165,9 @@ export async function gatherProjectConfig(
93
165
  course: answers.courseId,
94
166
  theme: PREDEFINED_THEMES[answers.themeName]
95
167
  };
168
+
169
+ // Show comprehensive theme preview
170
+ displayThemePreview(answers.themeName);
96
171
  } else {
97
172
  // Non-interactive mode: use provided options
98
173
  config = {
@@ -130,7 +205,7 @@ export async function confirmProjectCreation(
130
205
  console.log(` Course ID: ${chalk.white(config.course)}`);
131
206
  }
132
207
 
133
- console.log(` Theme: ${chalk.white(config.theme.name)}`);
208
+ console.log(` Theme: ${chalk.white(config.theme.name)} ${createThemePreview(config.theme.name)}`);
134
209
  console.log(` Directory: ${chalk.white(projectPath)}`);
135
210
 
136
211
  const { confirmed } = await inquirer.prompt([
@@ -145,48 +220,7 @@ export async function confirmProjectCreation(
145
220
  return confirmed;
146
221
  }
147
222
 
148
- export async function promptForCustomTheme(): Promise<ThemeConfig> {
149
- console.log(chalk.cyan('\nšŸŽØ Custom Theme Configuration\n'));
150
-
151
- const answers = await inquirer.prompt([
152
- {
153
- type: 'input',
154
- name: 'name',
155
- message: 'Theme name:',
156
- validate: (input: string) => input.trim().length > 0 || 'Theme name is required'
157
- },
158
- {
159
- type: 'input',
160
- name: 'primary',
161
- message: 'Primary color (hex):',
162
- default: '#1976D2',
163
- validate: validateHexColor
164
- },
165
- {
166
- type: 'input',
167
- name: 'secondary',
168
- message: 'Secondary color (hex):',
169
- default: '#424242',
170
- validate: validateHexColor
171
- },
172
- {
173
- type: 'input',
174
- name: 'accent',
175
- message: 'Accent color (hex):',
176
- default: '#82B1FF',
177
- validate: validateHexColor
178
- }
179
- ]);
180
223
 
181
- return {
182
- name: answers.name,
183
- colors: {
184
- primary: answers.primary,
185
- secondary: answers.secondary,
186
- accent: answers.accent
187
- }
188
- };
189
- }
190
224
 
191
225
  function formatProjectName(projectName: string): string {
192
226
  return projectName
@@ -195,10 +229,3 @@ function formatProjectName(projectName: string): string {
195
229
  .join(' ');
196
230
  }
197
231
 
198
- function validateHexColor(input: string): boolean | string {
199
- const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
200
- if (!hexColorRegex.test(input)) {
201
- return 'Please enter a valid hex color (e.g., #1976D2)';
202
- }
203
- return true;
204
- }
@@ -1,5 +1,4 @@
1
- import { promises as fs } from 'fs';
2
- import { existsSync } from 'fs';
1
+ import { promises as fs, existsSync } from 'fs';
3
2
  import path from 'path';
4
3
  import { fileURLToPath } from 'url';
5
4
  import chalk from 'chalk';
@@ -14,7 +13,7 @@ const __dirname = path.dirname(__filename);
14
13
  export async function findStandaloneUiPath(): Promise<string> {
15
14
  // Start from CLI package root and work upward
16
15
  let currentDir = path.join(__dirname, '..', '..');
17
-
16
+
18
17
  while (currentDir !== path.dirname(currentDir)) {
19
18
  const nodeModulesPath = path.join(currentDir, 'node_modules', '@vue-skuilder', 'standalone-ui');
20
19
  if (existsSync(nodeModulesPath)) {
@@ -22,8 +21,10 @@ export async function findStandaloneUiPath(): Promise<string> {
22
21
  }
23
22
  currentDir = path.dirname(currentDir);
24
23
  }
25
-
26
- throw new Error('Could not find @vue-skuilder/standalone-ui package. Please ensure it is installed.');
24
+
25
+ throw new Error(
26
+ 'Could not find @vue-skuilder/standalone-ui package. Please ensure it is installed.'
27
+ );
27
28
  }
28
29
 
29
30
  /**
@@ -35,18 +36,18 @@ export async function copyDirectory(
35
36
  excludePatterns: string[] = ['node_modules', 'dist', '.git', 'cypress']
36
37
  ): Promise<void> {
37
38
  const entries = await fs.readdir(source, { withFileTypes: true });
38
-
39
+
39
40
  await fs.mkdir(destination, { recursive: true });
40
-
41
+
41
42
  for (const entry of entries) {
42
43
  const sourcePath = path.join(source, entry.name);
43
44
  const destPath = path.join(destination, entry.name);
44
-
45
+
45
46
  // Skip excluded patterns
46
- if (excludePatterns.some(pattern => entry.name.includes(pattern))) {
47
+ if (excludePatterns.some((pattern) => entry.name.includes(pattern))) {
47
48
  continue;
48
49
  }
49
-
50
+
50
51
  if (entry.isDirectory()) {
51
52
  await copyDirectory(sourcePath, destPath, excludePatterns);
52
53
  } else {
@@ -65,12 +66,12 @@ export async function transformPackageJson(
65
66
  ): Promise<void> {
66
67
  const content = await fs.readFile(packageJsonPath, 'utf-8');
67
68
  const packageJson = JSON.parse(content);
68
-
69
+
69
70
  // Update basic project info
70
71
  packageJson.name = projectName;
71
72
  packageJson.description = `Skuilder course application: ${projectName}`;
72
73
  packageJson.version = '1.0.0';
73
-
74
+
74
75
  // Transform workspace dependencies to published versions
75
76
  if (packageJson.dependencies) {
76
77
  for (const [depName, version] of Object.entries(packageJson.dependencies)) {
@@ -80,13 +81,87 @@ export async function transformPackageJson(
80
81
  }
81
82
  }
82
83
  }
83
-
84
+
85
+ // Add missing terser devDependency for build minification
86
+ if (packageJson.devDependencies && !packageJson.devDependencies['terser']) {
87
+ packageJson.devDependencies['terser'] = '^5.39.0';
88
+ }
89
+
84
90
  // Remove CLI-specific fields that don't belong in generated projects
85
91
  delete packageJson.publishConfig;
86
-
92
+
87
93
  await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
88
94
  }
89
95
 
96
+ /**
97
+ * Create a vite.config.ts to work with published packages instead of workspace sources
98
+ *
99
+ * // [ ] This should be revised so that it works from the existing vite.config.ts in standalone-ui. As is, it recreates 95% of the same config.
100
+ */
101
+ export async function createViteConfig(viteConfigPath: string): Promise<void> {
102
+ // Create a clean vite config for standalone projects
103
+ const transformedContent = `// packages/standalone-ui/vite.config.ts
104
+ import { defineConfig } from 'vite';
105
+ import vue from '@vitejs/plugin-vue';
106
+ import { fileURLToPath, URL } from 'node:url';
107
+
108
+ export default defineConfig({
109
+ plugins: [vue()],
110
+ resolve: {
111
+ alias: {
112
+ // Alias for internal src paths
113
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
114
+
115
+ // Add events alias if needed (often required by dependencies)
116
+ events: 'events',
117
+ },
118
+ extensions: ['.js', '.ts', '.json', '.vue'],
119
+ dedupe: [
120
+ // Ensure single instances of core libs and published packages
121
+ 'vue',
122
+ 'vuetify',
123
+ 'pinia',
124
+ 'vue-router',
125
+ '@vue-skuilder/db',
126
+ '@vue-skuilder/common',
127
+ '@vue-skuilder/common-ui',
128
+ '@vue-skuilder/courses',
129
+ ],
130
+ },
131
+ // --- Dependencies optimization ---
132
+ optimizeDeps: {
133
+ // Help Vite pre-bundle dependencies from published packages
134
+ include: [
135
+ '@vue-skuilder/common-ui',
136
+ '@vue-skuilder/db',
137
+ '@vue-skuilder/common',
138
+ '@vue-skuilder/courses',
139
+ ],
140
+ },
141
+ server: {
142
+ port: 5173, // Use standard Vite port for standalone projects
143
+ },
144
+ build: {
145
+ sourcemap: true,
146
+ target: 'es2020',
147
+ minify: 'terser',
148
+ terserOptions: {
149
+ keep_classnames: true,
150
+ },
151
+ },
152
+ // Add define block for process polyfills
153
+ define: {
154
+ global: 'window',
155
+ 'process.env': process.env,
156
+ 'process.browser': true,
157
+ 'process.version': JSON.stringify(process.version),
158
+ },
159
+ });
160
+ `;
161
+
162
+ await fs.writeFile(viteConfigPath, transformedContent);
163
+ }
164
+
90
165
  /**
91
166
  * Generate skuilder.config.json based on project configuration
92
167
  */
@@ -96,35 +171,156 @@ export async function generateSkuilderConfig(
96
171
  ): Promise<void> {
97
172
  const skuilderConfig: SkuilderConfig = {
98
173
  title: config.title,
99
- dataLayerType: config.dataLayerType
174
+ dataLayerType: config.dataLayerType,
100
175
  };
101
-
176
+
102
177
  if (config.course) {
103
178
  skuilderConfig.course = config.course;
104
179
  }
105
-
180
+
106
181
  if (config.couchdbUrl) {
107
182
  skuilderConfig.couchdbUrl = config.couchdbUrl;
108
183
  }
109
-
184
+
110
185
  if (config.theme) {
111
186
  skuilderConfig.theme = config.theme;
112
187
  }
113
-
188
+
114
189
  await fs.writeFile(configPath, JSON.stringify(skuilderConfig, null, 2));
115
190
  }
116
191
 
192
+ /**
193
+ * Generate .gitignore file for the project
194
+ */
195
+ export async function generateGitignore(gitignorePath: string): Promise<void> {
196
+ const gitignoreContent = `# Dependencies
197
+ node_modules/
198
+ /.pnp
199
+ .pnp.js
200
+
201
+ # Production builds
202
+ /dist
203
+ /build
204
+
205
+ # Local env files
206
+ .env
207
+ .env.local
208
+ .env.development.local
209
+ .env.test.local
210
+ .env.production.local
211
+
212
+ # Log files
213
+ npm-debug.log*
214
+ yarn-debug.log*
215
+ yarn-error.log*
216
+ pnpm-debug.log*
217
+ lerna-debug.log*
218
+
219
+ # Runtime data
220
+ pids
221
+ *.pid
222
+ *.seed
223
+ *.pid.lock
224
+
225
+ # Coverage directory used by tools like istanbul
226
+ coverage/
227
+ *.lcov
228
+
229
+ # nyc test coverage
230
+ .nyc_output
231
+
232
+ # Dependency directories
233
+ jspm_packages/
234
+
235
+ # TypeScript cache
236
+ *.tsbuildinfo
237
+
238
+ # Optional npm cache directory
239
+ .npm
240
+
241
+ # Optional eslint cache
242
+ .eslintcache
243
+
244
+ # Microbundle cache
245
+ .rpt2_cache/
246
+ .rts2_cache_cjs/
247
+ .rts2_cache_es/
248
+ .rts2_cache_umd/
249
+
250
+ # Optional REPL history
251
+ .node_repl_history
252
+
253
+ # Output of 'npm pack'
254
+ *.tgz
255
+
256
+ # Yarn Integrity file
257
+ .yarn-integrity
258
+
259
+ # parcel-bundler cache (https://parceljs.org/)
260
+ .cache
261
+ .parcel-cache
262
+
263
+ # Next.js build output
264
+ .next
265
+
266
+ # Nuxt.js build / generate output
267
+ .nuxt
268
+ dist
269
+
270
+ # Gatsby files
271
+ .cache/
272
+ public
273
+
274
+ # Storybook build outputs
275
+ .out
276
+ .storybook-out
277
+
278
+ # Temporary folders
279
+ tmp/
280
+ temp/
281
+
282
+ # Editor directories and files
283
+ .vscode/
284
+ .idea
285
+ .DS_Store
286
+ *.suo
287
+ *.ntvs*
288
+ *.njsproj
289
+ *.sln
290
+ *.sw?
291
+
292
+ # OS generated files
293
+ Thumbs.db
294
+
295
+ # Cypress
296
+ /cypress/videos/
297
+ /cypress/screenshots/
298
+
299
+ # Local development
300
+ .env.development
301
+ .env.production
302
+
303
+ # Package manager lockfiles (uncomment if you want to ignore them)
304
+ # package-lock.json
305
+ # yarn.lock
306
+ # pnpm-lock.yaml
307
+
308
+ # Skuilder specific
309
+ /src/data/local-*.json
310
+ `;
311
+
312
+ await fs.writeFile(gitignorePath, gitignoreContent);
313
+ }
314
+
117
315
  /**
118
316
  * Generate project README.md
119
317
  */
120
- export async function generateReadme(
121
- readmePath: string,
122
- config: ProjectConfig
123
- ): Promise<void> {
124
- const dataLayerInfo = config.dataLayerType === 'static'
125
- ? 'This project uses a static data layer with JSON files.'
126
- : `This project connects to CouchDB at: ${config.couchdbUrl || '[URL not specified]'}`;
127
-
318
+ export async function generateReadme(readmePath: string, config: ProjectConfig): Promise<void> {
319
+ const dataLayerInfo =
320
+ config.dataLayerType === 'static'
321
+ ? 'This project uses a static data layer with JSON files.'
322
+ : `This project connects to CouchDB at: ${config.couchdbUrl || '[URL not specified]'}`;
323
+
128
324
  const readme = `# ${config.title}
129
325
 
130
326
  A Skuilder course application built with Vue 3, Vuetify, and Pinia.
@@ -160,10 +356,42 @@ Course configuration is managed in \`skuilder.config.json\`. You can modify:
160
356
 
161
357
  ## Theme
162
358
 
163
- Current theme: **${config.theme.name}**
164
- - Primary: ${config.theme.colors.primary}
165
- - Secondary: ${config.theme.colors.secondary}
166
- - Accent: ${config.theme.colors.accent}
359
+ Current theme: **${config.theme.name}** (${config.theme.defaultMode} mode)
360
+ - Primary: ${config.theme.light.colors.primary}
361
+ - Secondary: ${config.theme.light.colors.secondary}
362
+ - Accent: ${config.theme.light.colors.accent}
363
+
364
+ This theme includes both light and dark variants. The application will use the ${config.theme.defaultMode} theme by default, but users can toggle between light and dark modes in their settings.
365
+
366
+ ### Theme Customization
367
+
368
+ To customize the theme colors, edit the \`theme\` section in \`skuilder.config.json\`:
369
+
370
+ \`\`\`json
371
+ {
372
+ "theme": {
373
+ "name": "custom",
374
+ "defaultMode": "light",
375
+ "light": {
376
+ "dark": false,
377
+ "colors": {
378
+ "primary": "#your-color",
379
+ "secondary": "#your-color",
380
+ "accent": "#your-color"
381
+ // ... other semantic colors
382
+ }
383
+ },
384
+ "dark": {
385
+ "dark": true,
386
+ "colors": {
387
+ // ... dark variant colors
388
+ }
389
+ }
390
+ }
391
+ }
392
+ \`\`\`
393
+
394
+ The theme system supports all Vuetify semantic colors including error, success, warning, info, background, surface, and text colors. Changes to the configuration file are applied automatically on restart.
167
395
 
168
396
  ## Testing
169
397
 
@@ -195,21 +423,31 @@ export async function processTemplate(
195
423
  ): Promise<void> {
196
424
  console.log(chalk.blue('šŸ“¦ Locating standalone-ui template...'));
197
425
  const templatePath = await findStandaloneUiPath();
198
-
426
+
199
427
  console.log(chalk.blue('šŸ“‚ Copying project files...'));
200
428
  await copyDirectory(templatePath, projectPath);
201
-
429
+
202
430
  console.log(chalk.blue('āš™ļø Configuring package.json...'));
203
431
  const packageJsonPath = path.join(projectPath, 'package.json');
204
432
  await transformPackageJson(packageJsonPath, config.projectName, cliVersion);
205
-
433
+
434
+ console.log(chalk.blue('šŸ”§ Creating vite.config.ts...'));
435
+ const viteConfigPath = path.join(projectPath, 'vite.config.ts');
436
+ if (existsSync(viteConfigPath)) {
437
+ await createViteConfig(viteConfigPath);
438
+ }
439
+
206
440
  console.log(chalk.blue('šŸ”§ Generating configuration...'));
207
441
  const configPath = path.join(projectPath, 'skuilder.config.json');
208
442
  await generateSkuilderConfig(configPath, config);
209
-
443
+
210
444
  console.log(chalk.blue('šŸ“ Creating README...'));
211
445
  const readmePath = path.join(projectPath, 'README.md');
212
446
  await generateReadme(readmePath, config);
213
-
447
+
448
+ console.log(chalk.blue('šŸ“„ Generating .gitignore...'));
449
+ const gitignorePath = path.join(projectPath, '.gitignore');
450
+ await generateGitignore(gitignorePath);
451
+
214
452
  console.log(chalk.green('āœ… Template processing complete!'));
215
- }
453
+ }