@xbg.solutions/create-frontend 1.1.1

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/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/lib/index.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // create-frontend is a CLI tool package with .js/.cjs scripts.
3
+ // No TypeScript modules to re-export.
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,+DAA+D;AAC/D,sCAAsC"}
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@xbg.solutions/create-frontend",
3
+ "version": "1.1.1",
4
+ "description": "Setup wizard and generators for XBG frontend projects",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "build:watch": "tsc --watch",
9
+ "clean": "rm -rf lib"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "dependencies": {
15
+ "chalk": "^4.1.2"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.11.0",
19
+ "typescript": "^5.8.2"
20
+ }
21
+ }
@@ -0,0 +1,602 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Component Generator
5
+ *
6
+ * Generates a new Svelte component with TypeScript support, tests, and documentation.
7
+ *
8
+ * Usage:
9
+ * node __scripts__/generate-component.js <ComponentName> [options]
10
+ * npm run generate:component <ComponentName> [options]
11
+ *
12
+ * Options:
13
+ * --path <path> Custom path (default: src/lib/components/ui)
14
+ * --type <type> Component type: ui|page|layout|feature (default: ui)
15
+ * --with-test Generate test file
16
+ * --with-story Generate Storybook story
17
+ * --with-docs Generate documentation
18
+ *
19
+ * Examples:
20
+ * node __scripts__/generate-component.js UserProfile --type=feature --with-test
21
+ * node __scripts__/generate-component.js Button --path=src/lib/components/custom
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ // Parse command line arguments
28
+ const args = process.argv.slice(2);
29
+ const componentName = args[0];
30
+
31
+ if (!componentName) {
32
+ console.error('โŒ Component name is required!');
33
+ console.log('Usage: node __scripts__/generate-component.js <ComponentName> [options]');
34
+ process.exit(1);
35
+ }
36
+
37
+ // Parse options
38
+ const options = {
39
+ path: 'src/lib/components/ui',
40
+ type: 'ui',
41
+ withTest: false,
42
+ withStory: false,
43
+ withDocs: false
44
+ };
45
+
46
+ args.slice(1).forEach(arg => {
47
+ if (arg.startsWith('--path=')) {
48
+ options.path = arg.split('=')[1];
49
+ } else if (arg.startsWith('--type=')) {
50
+ options.type = arg.split('=')[1];
51
+ } else if (arg === '--with-test') {
52
+ options.withTest = true;
53
+ } else if (arg === '--with-story') {
54
+ options.withStory = true;
55
+ } else if (arg === '--with-docs') {
56
+ options.withDocs = true;
57
+ }
58
+ });
59
+
60
+ // Validate component name
61
+ if (!/^[A-Z][a-zA-Z0-9]*$/.test(componentName)) {
62
+ console.error('โŒ Component name must be PascalCase (e.g., UserProfile, DataTable)');
63
+ process.exit(1);
64
+ }
65
+
66
+ // Generate file names
67
+ const kebabName = componentName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
68
+ const componentDir = path.join(options.path, kebabName);
69
+ const componentFile = path.join(componentDir, `${componentName}.svelte`);
70
+ const indexFile = path.join(componentDir, 'index.ts');
71
+ const testFile = path.join(componentDir, `${componentName}.test.ts`);
72
+ const storyFile = path.join(componentDir, `${componentName}.stories.ts`);
73
+ const docFile = path.join(componentDir, `${componentName}.md`);
74
+
75
+ // Create directory
76
+ if (!fs.existsSync(componentDir)) {
77
+ fs.mkdirSync(componentDir, { recursive: true });
78
+ }
79
+
80
+ // Generate component template based on type
81
+ function generateComponentTemplate() {
82
+ const templates = {
83
+ ui: `<script lang="ts">
84
+ import { cn } from '$lib/utils/cn';
85
+ import type { ComponentProps } from 'svelte';
86
+
87
+ type $$Props = ComponentProps<'div'> & {
88
+ variant?: 'default' | 'primary' | 'secondary';
89
+ size?: 'sm' | 'md' | 'lg';
90
+ };
91
+
92
+ let className: $$Props['class'] = undefined;
93
+ export { className as class };
94
+ export let variant: $$Props['variant'] = 'default';
95
+ export let size: $$Props['size'] = 'md';
96
+
97
+ const variants = {
98
+ default: 'bg-white border-gray-200',
99
+ primary: 'bg-blue-500 text-white',
100
+ secondary: 'bg-gray-100 text-gray-900',
101
+ };
102
+
103
+ const sizes = {
104
+ sm: 'p-2 text-sm',
105
+ md: 'p-4 text-base',
106
+ lg: 'p-6 text-lg',
107
+ };
108
+ </script>
109
+
110
+ <div
111
+ class={cn(
112
+ 'rounded-lg border transition-colors',
113
+ variants[variant],
114
+ sizes[size],
115
+ className
116
+ )}
117
+ {...$$restProps}
118
+ >
119
+ <slot />
120
+ </div>`,
121
+
122
+ page: `<script lang="ts">
123
+ import { page } from '$app/stores';
124
+ import { onMount } from 'svelte';
125
+ import { authService } from '$lib/services/auth';
126
+ import { Button } from '$lib/components/ui/button';
127
+ import { Card, CardHeader, CardTitle, CardContent } from '$lib/components/ui/card';
128
+
129
+ // Page data and state
130
+ let loading = false;
131
+ let error: string | null = null;
132
+
133
+ // Authentication
134
+ const user = authService.getUser();
135
+ const claims = authService.getUserClaims();
136
+
137
+ onMount(() => {
138
+ loadData();
139
+ });
140
+
141
+ async function loadData() {
142
+ loading = true;
143
+ error = null;
144
+
145
+ try {
146
+ // Load page data
147
+ } catch (err) {
148
+ error = 'Failed to load data';
149
+ console.error('${componentName} error:', err);
150
+ } finally {
151
+ loading = false;
152
+ }
153
+ }
154
+ </script>
155
+
156
+ <svelte:head>
157
+ <title>${componentName}</title>
158
+ <meta name="description" content="${componentName} page" />
159
+ </svelte:head>
160
+
161
+ <div class="container mx-auto py-6">
162
+ <div class="mb-6">
163
+ <h1 class="text-3xl font-bold">${componentName}</h1>
164
+ <p class="text-gray-600">Page description goes here</p>
165
+ </div>
166
+
167
+ {#if loading}
168
+ <div class="flex justify-center py-12">
169
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
170
+ </div>
171
+ {:else if error}
172
+ <Card>
173
+ <CardContent class="p-6">
174
+ <p class="text-red-600">{error}</p>
175
+ <Button on:click={loadData} variant="outline" class="mt-4">
176
+ Try Again
177
+ </Button>
178
+ </CardContent>
179
+ </Card>
180
+ {:else}
181
+ <Card>
182
+ <CardHeader>
183
+ <CardTitle>Content</CardTitle>
184
+ </CardHeader>
185
+ <CardContent>
186
+ <p>Your page content goes here.</p>
187
+ </CardContent>
188
+ </Card>
189
+ {/if}
190
+ </div>`,
191
+
192
+ layout: `<script lang="ts">
193
+ import { page } from '$app/stores';
194
+ import { authService } from '$lib/services/auth';
195
+ import { Button } from '$lib/components/ui/button';
196
+
197
+ // Layout props
198
+ export let title: string = '';
199
+ export let showHeader: boolean = true;
200
+ export let showSidebar: boolean = true;
201
+
202
+ const user = authService.getUser();
203
+ </script>
204
+
205
+ <div class="min-h-screen bg-gray-50">
206
+ {#if showHeader}
207
+ <header class="bg-white shadow-sm border-b">
208
+ <div class="container mx-auto px-4">
209
+ <div class="flex items-center justify-between h-16">
210
+ <div class="flex items-center">
211
+ <h1 class="text-xl font-semibold">{title || '${componentName}'}</h1>
212
+ </div>
213
+
214
+ <div class="flex items-center space-x-4">
215
+ {#if $user}
216
+ <span class="text-sm text-gray-600">Welcome, {$user.email}</span>
217
+ <Button variant="outline" size="sm" on:click={() => authService.logout()}>
218
+ Logout
219
+ </Button>
220
+ {:else}
221
+ <Button href="/login" size="sm">
222
+ Login
223
+ </Button>
224
+ {/if}
225
+ </div>
226
+ </div>
227
+ </div>
228
+ </header>
229
+ {/if}
230
+
231
+ <div class="flex">
232
+ {#if showSidebar}
233
+ <aside class="w-64 bg-white shadow-sm min-h-screen">
234
+ <nav class="p-4">
235
+ <div class="space-y-2">
236
+ <a href="/dashboard" class="block px-3 py-2 rounded-md text-sm font-medium hover:bg-gray-100">
237
+ Dashboard
238
+ </a>
239
+ <!-- Add more navigation items -->
240
+ </div>
241
+ </nav>
242
+ </aside>
243
+ {/if}
244
+
245
+ <main class="flex-1 p-6">
246
+ <slot />
247
+ </main>
248
+ </div>
249
+ </div>`,
250
+
251
+ feature: `<script lang="ts">
252
+ import { createEventDispatcher, onMount } from 'svelte';
253
+ import { writable, derived } from 'svelte/store';
254
+ import { apiService } from '$lib/services/api';
255
+ import { toast } from '$lib/services/toast';
256
+ import { handleError } from '$lib/utils/error-handler';
257
+ import { Button } from '$lib/components/ui/button';
258
+ import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '$lib/components/ui/card';
259
+
260
+ // Props
261
+ export let data: any = null;
262
+ export let readonly: boolean = false;
263
+
264
+ // Events
265
+ const dispatch = createEventDispatcher<{
266
+ save: { data: any };
267
+ delete: { id: string };
268
+ cancel: void;
269
+ }>();
270
+
271
+ // State management
272
+ const loading = writable(false);
273
+ const error = writable<string | null>(null);
274
+ const formData = writable(data || {});
275
+
276
+ // Derived state
277
+ const hasChanges = derived(
278
+ formData,
279
+ ($formData) => JSON.stringify($formData) !== JSON.stringify(data)
280
+ );
281
+
282
+ onMount(() => {
283
+ if (data?.id) {
284
+ loadData();
285
+ }
286
+ });
287
+
288
+ async function loadData() {
289
+ if (!data?.id) return;
290
+
291
+ loading.set(true);
292
+ error.set(null);
293
+
294
+ try {
295
+ const response = await apiService.get(\`/api/items/\${data.id}\`);
296
+ formData.set(response.data);
297
+ } catch (err) {
298
+ error.set(handleError(err, 'Failed to load data'));
299
+ } finally {
300
+ loading.set(false);
301
+ }
302
+ }
303
+
304
+ async function handleSave() {
305
+ if (readonly) return;
306
+
307
+ loading.set(true);
308
+ error.set(null);
309
+
310
+ try {
311
+ const response = data?.id
312
+ ? await apiService.put(\`/api/items/\${data.id}\`, $formData)
313
+ : await apiService.post('/api/items', $formData);
314
+
315
+ dispatch('save', { data: response.data });
316
+ toast.success(\`\${componentName} saved successfully\`);
317
+ } catch (err) {
318
+ error.set(handleError(err, \`Failed to save \${componentName.toLowerCase()}\`));
319
+ } finally {
320
+ loading.set(false);
321
+ }
322
+ }
323
+
324
+ async function handleDelete() {
325
+ if (readonly || !data?.id) return;
326
+
327
+ if (!confirm('Are you sure you want to delete this item?')) return;
328
+
329
+ loading.set(true);
330
+ error.set(null);
331
+
332
+ try {
333
+ await apiService.delete(\`/api/items/\${data.id}\`);
334
+ dispatch('delete', { id: data.id });
335
+ toast.success(\`\${componentName} deleted successfully\`);
336
+ } catch (err) {
337
+ error.set(handleError(err, \`Failed to delete \${componentName.toLowerCase()}\`));
338
+ } finally {
339
+ loading.set(false);
340
+ }
341
+ }
342
+
343
+ function handleCancel() {
344
+ if ($hasChanges && !confirm('You have unsaved changes. Are you sure you want to cancel?')) {
345
+ return;
346
+ }
347
+ formData.set(data || {});
348
+ dispatch('cancel');
349
+ }
350
+ </script>
351
+
352
+ <Card>
353
+ <CardHeader>
354
+ <CardTitle>{data?.id ? 'Edit' : 'Create'} ${componentName}</CardTitle>
355
+ </CardHeader>
356
+
357
+ <CardContent>
358
+ {#if $error}
359
+ <div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
360
+ <p class="text-red-600 text-sm">{$error}</p>
361
+ </div>
362
+ {/if}
363
+
364
+ <!-- Form fields go here -->
365
+ <div class="space-y-4">
366
+ <p class="text-gray-500">Add your form fields here</p>
367
+ </div>
368
+ </CardContent>
369
+
370
+ {#if !readonly}
371
+ <CardFooter class="flex justify-between">
372
+ <div>
373
+ {#if data?.id}
374
+ <Button variant="destructive" on:click={handleDelete} disabled={$loading}>
375
+ Delete
376
+ </Button>
377
+ {/if}
378
+ </div>
379
+
380
+ <div class="flex space-x-2">
381
+ <Button variant="outline" on:click={handleCancel} disabled={$loading}>
382
+ Cancel
383
+ </Button>
384
+ <Button on:click={handleSave} disabled={$loading || !$hasChanges}>
385
+ {$loading ? 'Saving...' : 'Save'}
386
+ </Button>
387
+ </div>
388
+ </CardFooter>
389
+ {/if}
390
+ </Card>`
391
+ };
392
+
393
+ return templates[options.type] || templates.ui;
394
+ }
395
+
396
+ // Generate index file
397
+ function generateIndexTemplate() {
398
+ return `export { default as ${componentName} } from './${componentName}.svelte';
399
+ export type { ComponentProps as ${componentName}Props } from './${componentName}.svelte';`;
400
+ }
401
+
402
+ // Generate test file
403
+ function generateTestTemplate() {
404
+ return `import { render, fireEvent, screen } from '@testing-library/svelte';
405
+ import { expect, test, vi } from 'vitest';
406
+ import ${componentName} from './${componentName}.svelte';
407
+
408
+ test('renders ${componentName} correctly', () => {
409
+ render(${componentName});
410
+
411
+ // Add your test assertions here
412
+ expect(screen.getByRole('generic')).toBeInTheDocument();
413
+ });
414
+
415
+ test('handles props correctly', () => {
416
+ const props = {
417
+ // Add test props
418
+ };
419
+
420
+ render(${componentName}, { props });
421
+
422
+ // Test prop handling
423
+ });
424
+
425
+ test('emits events correctly', async () => {
426
+ const { component } = render(${componentName});
427
+
428
+ const handleEvent = vi.fn();
429
+ component.$on('event', handleEvent);
430
+
431
+ // Trigger event and test
432
+ // await fireEvent.click(screen.getByRole('button'));
433
+ // expect(handleEvent).toHaveBeenCalledWith(expect.objectContaining({
434
+ // detail: expectedData
435
+ // }));
436
+ });
437
+
438
+ test('handles accessibility requirements', () => {
439
+ render(${componentName});
440
+
441
+ // Test keyboard navigation
442
+ // Test screen reader compatibility
443
+ // Test focus management
444
+ });`;
445
+ }
446
+
447
+ // Generate Storybook story
448
+ function generateStoryTemplate() {
449
+ return `import type { Meta, StoryObj } from '@storybook/svelte';
450
+ import ${componentName} from './${componentName}.svelte';
451
+
452
+ const meta: Meta<${componentName}> = {
453
+ title: 'Components/${options.type}/${componentName}',
454
+ component: ${componentName},
455
+ parameters: {
456
+ layout: 'centered',
457
+ docs: {
458
+ description: {
459
+ component: '${componentName} component description.'
460
+ }
461
+ }
462
+ },
463
+ tags: ['autodocs'],
464
+ argTypes: {
465
+ // Define argTypes for Storybook controls
466
+ }
467
+ };
468
+
469
+ export default meta;
470
+ type Story = StoryObj<meta>;
471
+
472
+ export const Default: Story = {
473
+ args: {
474
+ // Default props
475
+ }
476
+ };
477
+
478
+ export const Variants: Story = {
479
+ args: {
480
+ // Variant props
481
+ }
482
+ };
483
+
484
+ export const Interactive: Story = {
485
+ args: {
486
+ // Interactive props
487
+ },
488
+ play: async ({ canvasElement }) => {
489
+ // Interaction testing
490
+ }
491
+ };`;
492
+ }
493
+
494
+ // Generate documentation
495
+ function generateDocTemplate() {
496
+ return `# ${componentName}
497
+
498
+ ${componentName} component description.
499
+
500
+ ## Usage
501
+
502
+ \`\`\`svelte
503
+ <script>
504
+ import { ${componentName} } from '$lib/components/${options.path.replace('src/lib/components/', '')}/${kebabName}';
505
+ </script>
506
+
507
+ <${componentName}>
508
+ Content goes here
509
+ </${componentName}>
510
+ \`\`\`
511
+
512
+ ## Props
513
+
514
+ | Prop | Type | Default | Description |
515
+ |------|------|---------|-------------|
516
+ | \`class\` | \`string\` | \`undefined\` | Additional CSS classes |
517
+
518
+ ## Events
519
+
520
+ | Event | Detail | Description |
521
+ |-------|--------|-------------|
522
+ | \`click\` | \`MouseEvent\` | Fired when component is clicked |
523
+
524
+ ## Slots
525
+
526
+ | Slot | Props | Description |
527
+ |------|-------|-------------|
528
+ | default | - | Main content slot |
529
+
530
+ ## Examples
531
+
532
+ ### Basic Usage
533
+
534
+ \`\`\`svelte
535
+ <${componentName}>
536
+ Hello World
537
+ </${componentName}>
538
+ \`\`\`
539
+
540
+ ### With Props
541
+
542
+ \`\`\`svelte
543
+ <${componentName} variant="primary" size="lg">
544
+ Large Primary Component
545
+ </${componentName}>
546
+ \`\`\`
547
+
548
+ ## Accessibility
549
+
550
+ - Follows WAI-ARIA guidelines
551
+ - Supports keyboard navigation
552
+ - Compatible with screen readers
553
+
554
+ ## Styling
555
+
556
+ This component uses Tailwind CSS classes and can be customized using the \`class\` prop or by modifying the component's default styles.`;
557
+ }
558
+
559
+ // Write files
560
+ try {
561
+ console.log(`๐Ÿš€ Generating ${componentName} component...`);
562
+
563
+ // Write main component file
564
+ fs.writeFileSync(componentFile, generateComponentTemplate());
565
+ console.log(`โœ… Created ${componentFile}`);
566
+
567
+ // Write index file
568
+ fs.writeFileSync(indexFile, generateIndexTemplate());
569
+ console.log(`โœ… Created ${indexFile}`);
570
+
571
+ // Write test file if requested
572
+ if (options.withTest) {
573
+ fs.writeFileSync(testFile, generateTestTemplate());
574
+ console.log(`โœ… Created ${testFile}`);
575
+ }
576
+
577
+ // Write story file if requested
578
+ if (options.withStory) {
579
+ fs.writeFileSync(storyFile, generateStoryTemplate());
580
+ console.log(`โœ… Created ${storyFile}`);
581
+ }
582
+
583
+ // Write documentation if requested
584
+ if (options.withDocs) {
585
+ fs.writeFileSync(docFile, generateDocTemplate());
586
+ console.log(`โœ… Created ${docFile}`);
587
+ }
588
+
589
+ console.log(`\n๐ŸŽ‰ ${componentName} component generated successfully!`);
590
+ console.log(`\n๐Ÿ“ Files created in: ${componentDir}`);
591
+ console.log(`\n๐Ÿ“– Import your component:`);
592
+ console.log(` import { ${componentName} } from '$lib/components/${options.path.replace('src/lib/components/', '')}/${kebabName}';`);
593
+
594
+ if (options.withTest) {
595
+ console.log(`\n๐Ÿงช Run tests:`);
596
+ console.log(` npm test ${testFile}`);
597
+ }
598
+
599
+ } catch (error) {
600
+ console.error('โŒ Error generating component:', error.message);
601
+ process.exit(1);
602
+ }