wlmaker 1.1.2 → 1.2.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.
Files changed (2) hide show
  1. package/dist/cli.mjs +1109 -37
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { createRequire } from "module";
5
5
  import { Command } from "commander";
6
- import chalk3 from "chalk";
6
+ import chalk5 from "chalk";
7
7
 
8
8
  // src/core/create-bloc.ts
9
9
  import * as fs3 from "fs";
@@ -72,6 +72,20 @@ function updateBarrelFile(parentDir, name) {
72
72
  fs.writeFileSync(barrelPath, exportLine + "\n");
73
73
  }
74
74
  }
75
+ function updateSortedBarrelFile(tierDir, barrelFileName, exportLine) {
76
+ const barrelPath = path.join(tierDir, barrelFileName);
77
+ let lines = [];
78
+ if (fs.existsSync(barrelPath)) {
79
+ const content = fs.readFileSync(barrelPath, "utf8");
80
+ lines = content.split("\n").filter((l) => l.trim().length > 0);
81
+ }
82
+ if (lines.includes(exportLine)) {
83
+ return;
84
+ }
85
+ lines.push(exportLine);
86
+ lines.sort();
87
+ fs.writeFileSync(barrelPath, lines.join("\n") + "\n");
88
+ }
75
89
 
76
90
  // src/core/build-runner.ts
77
91
  import { spawn } from "child_process";
@@ -144,12 +158,70 @@ async function createBloc(name, options) {
144
158
  }
145
159
  }
146
160
 
147
- // src/interactive.ts
161
+ // src/core/create-widget.ts
162
+ import * as fs6 from "fs";
163
+ import * as path6 from "path";
164
+ import chalk2 from "chalk";
165
+ import { pascalCase as pascalCase2 } from "change-case";
166
+
167
+ // src/core/tier.ts
168
+ var VALID_TIERS = ["atom", "molecule", "organism", "template"];
169
+ var TIER_PATTERNS = {
170
+ atom: ["single-public", "single-factory", "subdirectory-parts"],
171
+ molecule: ["single", "subdirectory-parts", "subdirectory-widgets"],
172
+ organism: ["subdirectory-show", "single"],
173
+ template: ["simple", "config-data-callbacks"]
174
+ };
175
+ var TIER_PLURAL = {
176
+ atom: "atoms",
177
+ molecule: "molecules",
178
+ organism: "organisms",
179
+ template: "templates"
180
+ };
181
+ var TIER_LABELS = {
182
+ atom: "Atom",
183
+ molecule: "Molecule",
184
+ organism: "Organism",
185
+ template: "Template"
186
+ };
187
+ var PATTERN_LABELS = {
188
+ "single-public": "Single file, public constructor",
189
+ "single-factory": "Single file, private constructor + factories",
190
+ "subdirectory-parts": "Subdirectory with part files",
191
+ single: "Single file (StatefulWidget)",
192
+ "subdirectory-widgets": "Subdirectory with widgets/ and utils/",
193
+ "subdirectory-show": "Subdirectory with static show() + parts",
194
+ simple: "Template simple (i18n + body + skeleton)",
195
+ "config-data-callbacks": "Template with Config/Data/Callbacks"
196
+ };
197
+ function normalizeTier(input) {
198
+ const normalized = input.toLowerCase().replace(/s$/, "");
199
+ if (VALID_TIERS.includes(normalized)) {
200
+ return normalized;
201
+ }
202
+ throw new Error(
203
+ `Invalid tier "${input}". Valid tiers: ${VALID_TIERS.join(", ")}`
204
+ );
205
+ }
206
+ function tierPlural(tier) {
207
+ return TIER_PLURAL[tier];
208
+ }
209
+ function tierLabel(tier) {
210
+ return TIER_LABELS[tier];
211
+ }
212
+ function getValidPatterns(tier) {
213
+ return TIER_PATTERNS[tier];
214
+ }
215
+ function getDefaultPattern(tier) {
216
+ return TIER_PATTERNS[tier][0];
217
+ }
218
+ function patternLabel(pattern) {
219
+ return PATTERN_LABELS[pattern] ?? pattern;
220
+ }
221
+
222
+ // src/core/design-system-analyzer.ts
148
223
  import * as fs5 from "fs";
149
- import * as os from "os";
150
224
  import * as path5 from "path";
151
- import * as clack from "@clack/prompts";
152
- import chalk2 from "chalk";
153
225
 
154
226
  // src/core/project-analyzer.ts
155
227
  import * as fs4 from "fs";
@@ -253,14 +325,813 @@ function discoverProjects(searchDir, maxDepth = 2) {
253
325
  return projects.sort((a, b) => a.projectName.localeCompare(b.projectName));
254
326
  }
255
327
 
256
- // src/interactive.ts
328
+ // src/core/design-system-analyzer.ts
329
+ function detectDesignSystem(projectRoot) {
330
+ const monorepoRoot = findMonorepoRoot(projectRoot);
331
+ if (monorepoRoot) {
332
+ const dsDir = path5.join(
333
+ monorepoRoot,
334
+ "packages",
335
+ "design_system",
336
+ "lib",
337
+ "wl_design_system"
338
+ );
339
+ if (fs5.existsSync(dsDir)) {
340
+ const widgetbookDir = path5.join(monorepoRoot, "apps", "widgetbook");
341
+ return {
342
+ componentsDir: dsDir,
343
+ widgetbookDir: fs5.existsSync(widgetbookDir) ? widgetbookDir : void 0,
344
+ availableTiers: discoverTiers(dsDir)
345
+ };
346
+ }
347
+ }
348
+ const standaloneDir = path5.join(projectRoot, "lib", "wl_design_system");
349
+ if (fs5.existsSync(standaloneDir)) {
350
+ const widgetbookDir = path5.join(
351
+ path5.dirname(projectRoot),
352
+ "widgetbook"
353
+ );
354
+ return {
355
+ componentsDir: standaloneDir,
356
+ widgetbookDir: fs5.existsSync(widgetbookDir) ? widgetbookDir : void 0,
357
+ availableTiers: discoverTiers(standaloneDir)
358
+ };
359
+ }
360
+ return null;
361
+ }
362
+ function discoverTiers(componentsDir) {
363
+ const tiers = [];
364
+ const tierSet = new Set(VALID_TIERS.map((t) => tierPlural(t)));
365
+ try {
366
+ const entries = fs5.readdirSync(componentsDir, { withFileTypes: true });
367
+ for (const entry of entries) {
368
+ if (entry.isDirectory() && tierSet.has(entry.name)) {
369
+ tiers.push(entry.name);
370
+ }
371
+ }
372
+ } catch {
373
+ }
374
+ return tiers;
375
+ }
376
+ function widgetExists(componentsDir, tierPluralName, widgetName) {
377
+ const tierDir = path5.join(componentsDir, tierPluralName);
378
+ const flatFile = path5.join(tierDir, `${widgetName}.dart`);
379
+ if (fs5.existsSync(flatFile)) return true;
380
+ const subDir = path5.join(tierDir, widgetName);
381
+ if (fs5.existsSync(subDir)) return true;
382
+ return false;
383
+ }
384
+
385
+ // src/core/widget-templates.ts
386
+ function atomTemplateA(name, pascal) {
387
+ return `import 'package:flutter/material.dart';
388
+
389
+ class Wl${pascal} extends StatelessWidget {
390
+ const Wl${pascal}({super.key});
391
+
392
+ @override
393
+ Widget build(BuildContext context) {
394
+ final theme = context.theme;
395
+ return const SizedBox.shrink();
396
+ }
397
+ }
398
+ `;
399
+ }
400
+ function atomTemplateB(name, pascal) {
401
+ return `import 'package:flutter/material.dart';
402
+
403
+ enum Wl${pascal}Variant {
404
+ primary,
405
+ secondary,
406
+ }
407
+
408
+ class Wl${pascal} extends StatelessWidget {
409
+ final Wl${pascal}Variant variant;
410
+
411
+ const Wl${pascal}._internal({required this.variant, super.key});
412
+
413
+ factory Wl${pascal}.primary({Key? key}) =>
414
+ Wl${pascal}._internal(variant: Wl${pascal}Variant.primary, key: key);
415
+
416
+ factory Wl${pascal}.secondary({Key? key}) =>
417
+ Wl${pascal}._internal(variant: Wl${pascal}Variant.secondary, key: key);
418
+
419
+ @override
420
+ Widget build(BuildContext context) {
421
+ final theme = context.theme;
422
+ return const SizedBox.shrink();
423
+ }
424
+ }
425
+ `;
426
+ }
427
+ function atomTemplateC(name, pascal) {
428
+ return {
429
+ main: `import 'package:flutter/material.dart';
430
+
431
+ part '${name}_sizes.dart';
432
+
433
+ class Wl${pascal} extends StatelessWidget {
434
+ final Wl${pascal}Size size;
435
+
436
+ const Wl${pascal}({this.size = Wl${pascal}Size.md, super.key});
437
+
438
+ @override
439
+ Widget build(BuildContext context) {
440
+ final theme = context.theme;
441
+ return SizedBox(
442
+ width: size.width,
443
+ height: size.height,
444
+ child: const SizedBox.shrink(),
445
+ );
446
+ }
447
+ }
448
+ `,
449
+ sizes: `part of '${name}.dart';
450
+
451
+ enum _Wl${pascal}Size {
452
+ sm,
453
+ md,
454
+ lg,
455
+ }
456
+
457
+ extension _Wl${pascal}SizeExt on _Wl${pascal}Size {
458
+ double get width => switch (this) {
459
+ _Wl${pascal}Size.sm => 16,
460
+ _Wl${pascal}Size.md => 24,
461
+ _Wl${pascal}Size.lg => 32,
462
+ };
463
+
464
+ double get height => switch (this) {
465
+ _Wl${pascal}Size.sm => 16,
466
+ _Wl${pascal}Size.md => 24,
467
+ _Wl${pascal}Size.lg => 32,
468
+ };
469
+ }
470
+ `
471
+ };
472
+ }
473
+ function moleculeTemplateA(name, pascal) {
474
+ return `import 'package:flutter/material.dart';
475
+
476
+ enum Wl${pascal}Variant {
477
+ primary,
478
+ secondary,
479
+ }
480
+
481
+ class Wl${pascal} extends StatefulWidget {
482
+ final Wl${pascal}Variant variant;
483
+
484
+ const Wl${pascal}({
485
+ this.variant = Wl${pascal}Variant.primary,
486
+ super.key,
487
+ });
488
+
489
+ @override
490
+ State<Wl${pascal}> createState() => _Wl${pascal}State();
491
+ }
492
+
493
+ class _Wl${pascal}State extends State<Wl${pascal}> {
494
+ @override
495
+ Widget build(BuildContext context) {
496
+ final theme = context.theme;
497
+ return const SizedBox.shrink();
498
+ }
499
+ }
500
+ `;
501
+ }
502
+ function moleculeTemplateB(name, pascal) {
503
+ return {
504
+ main: `import 'package:flutter/material.dart';
505
+
506
+ part '${name}_type.dart';
507
+
508
+ class Wl${pascal} extends StatefulWidget {
509
+ final Wl${pascal}Variant variant;
510
+
511
+ const Wl${pascal}({
512
+ this.variant = Wl${pascal}Variant.primary,
513
+ super.key,
514
+ });
515
+
516
+ @override
517
+ State<Wl${pascal}> createState() => _Wl${pascal}State();
518
+ }
519
+
520
+ class _Wl${pascal}State extends State<Wl${pascal}> {
521
+ @override
522
+ Widget build(BuildContext context) {
523
+ final theme = context.theme;
524
+ return const SizedBox.shrink();
525
+ }
526
+ }
527
+ `,
528
+ type: `part of '${name}.dart';
529
+
530
+ enum Wl${pascal}Variant {
531
+ primary,
532
+ secondary,
533
+ }
534
+
535
+ extension Wl${pascal}VariantExt on Wl${pascal}Variant {
536
+ bool get isPrimary => this == Wl${pascal}Variant.primary;
537
+ }
538
+ `
539
+ };
540
+ }
541
+ function moleculeTemplateC(name, pascal) {
542
+ return `import 'package:flutter/material.dart';
543
+
544
+ class Wl${pascal} extends StatefulWidget {
545
+ const Wl${pascal}({super.key});
546
+
547
+ @override
548
+ State<Wl${pascal}> createState() => _Wl${pascal}State();
549
+ }
550
+
551
+ class _Wl${pascal}State extends State<Wl${pascal}> {
552
+ @override
553
+ Widget build(BuildContext context) {
554
+ final theme = context.theme;
555
+ return const SizedBox.shrink();
556
+ }
557
+ }
558
+ `;
559
+ }
560
+ function organismTemplateA(name, pascal) {
561
+ return {
562
+ main: `import 'package:flutter/material.dart';
563
+
564
+ part '${name}_content.dart';
565
+
566
+ class Wl${pascal} {
567
+ Wl${pascal}._();
568
+
569
+ static Future<T?> show<T>({
570
+ required BuildContext context,
571
+ }) {
572
+ return showDialog<T>(
573
+ context: context,
574
+ builder: (context) => _Wl${pascal}Content(),
575
+ );
576
+ }
577
+ }
578
+ `,
579
+ content: `part of '${name}.dart';
580
+
581
+ class _Wl${pascal}Content extends StatelessWidget {
582
+ @override
583
+ Widget build(BuildContext context) {
584
+ final theme = context.theme;
585
+ return const SizedBox.shrink();
586
+ }
587
+ }
588
+ `
589
+ };
590
+ }
591
+ function organismTemplateB(name, pascal) {
592
+ return `import 'package:flutter/material.dart';
593
+
594
+ class Wl${pascal} extends StatefulWidget {
595
+ const Wl${pascal}({super.key});
596
+
597
+ @override
598
+ State<Wl${pascal}> createState() => _Wl${pascal}State();
599
+ }
600
+
601
+ class _Wl${pascal}State extends State<Wl${pascal}> {
602
+ @override
603
+ Widget build(BuildContext context) {
604
+ final theme = context.theme;
605
+ return const SizedBox.shrink();
606
+ }
607
+ }
608
+ `;
609
+ }
610
+ function templateA(name, pascal) {
611
+ return {
612
+ main: `import 'package:flutter/material.dart';
613
+
614
+ part '${name}_i18n.dart';
615
+ part '${name}_body.dart';
616
+ part '${name}_skeleton.dart';
617
+
618
+ class Wl${pascal}Template extends StatefulWidget {
619
+ const Wl${pascal}Template({super.key});
620
+
621
+ @override
622
+ State<Wl${pascal}Template> createState() => _Wl${pascal}TemplateState();
623
+ }
624
+
625
+ class _Wl${pascal}TemplateState extends State<Wl${pascal}Template> {
626
+ bool _isLoading = true;
627
+
628
+ @override
629
+ void initState() {
630
+ super.initState();
631
+ _loadData();
632
+ }
633
+
634
+ Future<void> _loadData() async {
635
+ // TODO: Load data
636
+ setState(() => _isLoading = false);
637
+ }
638
+
639
+ @override
640
+ Widget build(BuildContext context) {
641
+ return Scaffold(
642
+ appBar: AppBar(title: Text(Wl${pascal}I18n.title)),
643
+ body: AnimatedSwitcher(
644
+ duration: const Duration(milliseconds: 300),
645
+ child: _isLoading
646
+ ? const _Wl${pascal}Skeleton()
647
+ : const _Wl${pascal}Body(),
648
+ ),
649
+ );
650
+ }
651
+ }
652
+ `,
653
+ i18n: `part of '${name}_template.dart';
654
+
655
+ class Wl${pascal}I18n extends Equatable {
656
+ final String title;
657
+
658
+ const Wl${pascal}I18n({
659
+ this.title = '',
660
+ });
661
+
662
+ static const empty = Wl${pascal}I18n();
663
+
664
+ @override
665
+ List<Object?> get props => [title];
666
+ }
667
+ `,
668
+ body: `part of '${name}_template.dart';
669
+
670
+ class _Wl${pascal}Body extends StatelessWidget {
671
+ const _Wl${pascal}Body();
672
+
673
+ @override
674
+ Widget build(BuildContext context) {
675
+ final theme = context.theme;
676
+ return const SizedBox.shrink();
677
+ }
678
+ }
679
+ `,
680
+ skeleton: `part of '${name}_template.dart';
681
+
682
+ class _Wl${pascal}Skeleton extends StatelessWidget {
683
+ const _Wl${pascal}Skeleton();
684
+
685
+ @override
686
+ Widget build(BuildContext context) {
687
+ return const Center(
688
+ child: CircularProgressIndicator(),
689
+ );
690
+ }
691
+ }
692
+ `
693
+ };
694
+ }
695
+ function templateB(name, pascal) {
696
+ return {
697
+ main: `import 'package:flutter/material.dart';
698
+
699
+ import 'contracts/${name}_callbacks.dart';
700
+ import 'contracts/${name}_config.dart';
701
+ import 'contracts/${name}_data.dart';
702
+
703
+ part '${name}_i18n.dart';
704
+ part '${name}_body.dart';
705
+ part '${name}_skeleton.dart';
706
+
707
+ class Wl${pascal}Template extends StatefulWidget {
708
+ final Wl${pascal}Config config;
709
+ final Wl${pascal}Callbacks callbacks;
710
+
711
+ const Wl${pascal}Template({
712
+ required this.config,
713
+ required this.callbacks,
714
+ super.key,
715
+ });
716
+
717
+ @override
718
+ State<Wl${pascal}Template> createState() => _Wl${pascal}TemplateState();
719
+ }
720
+
721
+ class _Wl${pascal}TemplateState extends State<Wl${pascal}Template> {
722
+ Wl${pascal}Data _data = const Wl${pascal}Data();
723
+ bool _isLoading = true;
724
+
725
+ @override
726
+ void initState() {
727
+ super.initState();
728
+ _loadData();
729
+ }
730
+
731
+ Future<void> _loadData() async {
732
+ // TODO: Load data
733
+ setState(() => _isLoading = false);
734
+ }
735
+
736
+ @override
737
+ Widget build(BuildContext context) {
738
+ return Scaffold(
739
+ appBar: AppBar(title: Text(widget.config.i18n.title)),
740
+ body: AnimatedSwitcher(
741
+ duration: const Duration(milliseconds: 300),
742
+ child: _isLoading
743
+ ? const _Wl${pascal}Skeleton()
744
+ : _Wl${pascal}Body(
745
+ data: _data,
746
+ callbacks: widget.callbacks,
747
+ ),
748
+ ),
749
+ );
750
+ }
751
+ }
752
+ `,
753
+ i18n: `part of '${name}_template.dart';
754
+
755
+ class Wl${pascal}I18n extends Equatable {
756
+ final String title;
757
+
758
+ const Wl${pascal}I18n({
759
+ this.title = '',
760
+ });
761
+
762
+ static const empty = Wl${pascal}I18n();
763
+
764
+ @override
765
+ List<Object?> get props => [title];
766
+ }
767
+ `,
768
+ body: `part of '${name}_template.dart';
769
+
770
+ class _Wl${pascal}Body extends StatelessWidget {
771
+ final Wl${pascal}Data data;
772
+ final Wl${pascal}Callbacks callbacks;
773
+
774
+ const _Wl${pascal}Body({
775
+ required this.data,
776
+ required this.callbacks,
777
+ });
778
+
779
+ @override
780
+ Widget build(BuildContext context) {
781
+ final theme = context.theme;
782
+ return const SizedBox.shrink();
783
+ }
784
+ }
785
+ `,
786
+ skeleton: `part of '${name}_template.dart';
787
+
788
+ class _Wl${pascal}Skeleton extends StatelessWidget {
789
+ const _Wl${pascal}Skeleton();
790
+
791
+ @override
792
+ Widget build(BuildContext context) {
793
+ return const Center(
794
+ child: CircularProgressIndicator(),
795
+ );
796
+ }
797
+ }
798
+ `,
799
+ callbacks: `import 'package:flutter/material.dart';
800
+
801
+ class Wl${pascal}Callbacks {
802
+ final VoidCallback onBack;
803
+ final VoidCallback onRetry;
804
+
805
+ const Wl${pascal}Callbacks({
806
+ required this.onBack,
807
+ required this.onRetry,
808
+ });
809
+ }
810
+ `,
811
+ config: `import 'package:flutter/material.dart';
812
+ import '../${name}_template.dart';
813
+
814
+ class Wl${pascal}Config extends Equatable {
815
+ final Wl${pascal}I18n i18n;
816
+ final bool showAppBar;
817
+
818
+ const Wl${pascal}Config({
819
+ this.i18n = Wl${pascal}I18n.empty,
820
+ this.showAppBar = true,
821
+ });
822
+
823
+ @override
824
+ List<Object?> get props => [i18n, showAppBar];
825
+ }
826
+ `,
827
+ data: `import 'package:flutter/material.dart';
828
+
829
+ class Wl${pascal}Data extends Equatable {
830
+ const Wl${pascal}Data();
831
+
832
+ @override
833
+ List<Object?> get props => [];
834
+ }
835
+ `
836
+ };
837
+ }
838
+ function useCaseTemplate(name, pascal, tierPlural3) {
839
+ return `import 'package:widgetbook/widgetbook.dart';
840
+ import 'package:wl_design_system/${tierPlural3}/wl_${name}.dart';
841
+
842
+ WidgetbookUseCase wl${pascal}UseCase(BuildContext context) {
843
+ return WidgetbookUseCase(
844
+ name: 'Wl${pascal}',
845
+ builder: (context) => const Wl${pascal}(),
846
+ );
847
+ }
848
+ `;
849
+ }
850
+
851
+ // src/core/create-widget.ts
257
852
  var SNAKE_CASE_REGEX2 = /^[a-z][a-z0-9_]*$/;
853
+ async function createWidget(name, tierInput, options) {
854
+ let cleanName = name;
855
+ if (cleanName.startsWith("wl_")) {
856
+ cleanName = cleanName.slice(3);
857
+ }
858
+ if (!cleanName || !SNAKE_CASE_REGEX2.test(cleanName)) {
859
+ throw new Error(
860
+ "Name must be snake_case (lowercase letters, digits, underscores)."
861
+ );
862
+ }
863
+ const tier = normalizeTier(tierInput);
864
+ const plural = tierPlural(tier);
865
+ const pascal = pascalCase2(cleanName);
866
+ const fileName = `wl_${cleanName}`;
867
+ const ds = detectDesignSystem(options.projectRoot);
868
+ if (!ds) {
869
+ throw new Error(
870
+ "Could not detect design system. Ensure wl_design_system/ directory exists."
871
+ );
872
+ }
873
+ const tierDir = path6.join(ds.componentsDir, plural);
874
+ if (!fs6.existsSync(tierDir)) {
875
+ fs6.mkdirSync(tierDir, { recursive: true });
876
+ }
877
+ const pattern = options.pattern ?? getDefaultPattern(tier);
878
+ const flatPath = path6.join(tierDir, `${fileName}.dart`);
879
+ const subDirPath = path6.join(tierDir, fileName);
880
+ if (fs6.existsSync(flatPath) || fs6.existsSync(subDirPath)) {
881
+ throw new Error(`Widget "${fileName}" already exists in ${plural}/.`);
882
+ }
883
+ switch (tier) {
884
+ case "atom":
885
+ createAtomFiles(tierDir, cleanName, pascal, fileName, pattern);
886
+ break;
887
+ case "molecule":
888
+ createMoleculeFiles(tierDir, cleanName, pascal, fileName, pattern);
889
+ break;
890
+ case "organism":
891
+ createOrganismFiles(tierDir, cleanName, pascal, fileName, pattern);
892
+ break;
893
+ case "template":
894
+ createTemplateFiles(tierDir, cleanName, pascal, fileName, pattern);
895
+ break;
896
+ }
897
+ const barrelFileName = `${plural}.dart`;
898
+ const exportLine = tier === "template" ? `export '${cleanName}/wl_${cleanName}_template.dart';` : `export '${fileName}.dart';`;
899
+ updateSortedBarrelFile(tierDir, barrelFileName, exportLine);
900
+ console.log(chalk2.green(`Widget "Wl${pascal}" created in ${plural}/`));
901
+ }
902
+ function createAtomFiles(tierDir, name, pascal, fileName, pattern) {
903
+ switch (pattern) {
904
+ case "single-public": {
905
+ fs6.writeFileSync(
906
+ path6.join(tierDir, `${fileName}.dart`),
907
+ atomTemplateA(name, pascal)
908
+ );
909
+ break;
910
+ }
911
+ case "single-factory": {
912
+ fs6.writeFileSync(
913
+ path6.join(tierDir, `${fileName}.dart`),
914
+ atomTemplateB(name, pascal)
915
+ );
916
+ break;
917
+ }
918
+ case "subdirectory-parts": {
919
+ const dir = path6.join(tierDir, fileName);
920
+ fs6.mkdirSync(dir, { recursive: true });
921
+ const tpl = atomTemplateC(name, pascal);
922
+ fs6.writeFileSync(path6.join(dir, `${fileName}.dart`), tpl.main);
923
+ fs6.writeFileSync(
924
+ path6.join(dir, `${fileName}_sizes.dart`),
925
+ tpl.sizes
926
+ );
927
+ break;
928
+ }
929
+ default:
930
+ throw new Error(`Unknown atom pattern: ${pattern}`);
931
+ }
932
+ }
933
+ function createMoleculeFiles(tierDir, name, pascal, fileName, pattern) {
934
+ switch (pattern) {
935
+ case "single": {
936
+ fs6.writeFileSync(
937
+ path6.join(tierDir, `${fileName}.dart`),
938
+ moleculeTemplateA(name, pascal)
939
+ );
940
+ break;
941
+ }
942
+ case "subdirectory-parts": {
943
+ const dir = path6.join(tierDir, fileName);
944
+ fs6.mkdirSync(dir, { recursive: true });
945
+ const tpl = moleculeTemplateB(name, pascal);
946
+ fs6.writeFileSync(path6.join(dir, `${fileName}.dart`), tpl.main);
947
+ fs6.writeFileSync(
948
+ path6.join(dir, `${fileName}_type.dart`),
949
+ tpl.type
950
+ );
951
+ break;
952
+ }
953
+ case "subdirectory-widgets": {
954
+ const dir = path6.join(tierDir, fileName);
955
+ fs6.mkdirSync(dir, { recursive: true });
956
+ fs6.mkdirSync(path6.join(dir, "widgets"), { recursive: true });
957
+ fs6.mkdirSync(path6.join(dir, "utils"), { recursive: true });
958
+ fs6.writeFileSync(
959
+ path6.join(dir, `${fileName}.dart`),
960
+ moleculeTemplateC(name, pascal)
961
+ );
962
+ break;
963
+ }
964
+ default:
965
+ throw new Error(`Unknown molecule pattern: ${pattern}`);
966
+ }
967
+ }
968
+ function createOrganismFiles(tierDir, name, pascal, fileName, pattern) {
969
+ switch (pattern) {
970
+ case "subdirectory-show": {
971
+ const dir = path6.join(tierDir, fileName);
972
+ fs6.mkdirSync(dir, { recursive: true });
973
+ const tpl = organismTemplateA(name, pascal);
974
+ fs6.writeFileSync(path6.join(dir, `${fileName}.dart`), tpl.main);
975
+ fs6.writeFileSync(
976
+ path6.join(dir, `${fileName}_content.dart`),
977
+ tpl.content
978
+ );
979
+ break;
980
+ }
981
+ case "single": {
982
+ fs6.writeFileSync(
983
+ path6.join(tierDir, `${fileName}.dart`),
984
+ organismTemplateB(name, pascal)
985
+ );
986
+ break;
987
+ }
988
+ default:
989
+ throw new Error(`Unknown organism pattern: ${pattern}`);
990
+ }
991
+ }
992
+ function createTemplateFiles(tierDir, name, pascal, fileName, pattern) {
993
+ const dir = path6.join(tierDir, name);
994
+ fs6.mkdirSync(dir, { recursive: true });
995
+ switch (pattern) {
996
+ case "simple": {
997
+ const tpl = templateA(name, pascal);
998
+ fs6.writeFileSync(
999
+ path6.join(dir, `wl_${name}_template.dart`),
1000
+ tpl.main
1001
+ );
1002
+ fs6.writeFileSync(
1003
+ path6.join(dir, `wl_${name}_i18n.dart`),
1004
+ tpl.i18n
1005
+ );
1006
+ fs6.writeFileSync(path6.join(dir, `wl_${name}_body.dart`), tpl.body);
1007
+ fs6.writeFileSync(
1008
+ path6.join(dir, `wl_${name}_skeleton.dart`),
1009
+ tpl.skeleton
1010
+ );
1011
+ break;
1012
+ }
1013
+ case "config-data-callbacks": {
1014
+ const tpl = templateB(name, pascal);
1015
+ fs6.writeFileSync(
1016
+ path6.join(dir, `wl_${name}_template.dart`),
1017
+ tpl.main
1018
+ );
1019
+ fs6.writeFileSync(
1020
+ path6.join(dir, `wl_${name}_i18n.dart`),
1021
+ tpl.i18n
1022
+ );
1023
+ fs6.writeFileSync(path6.join(dir, `wl_${name}_body.dart`), tpl.body);
1024
+ fs6.writeFileSync(
1025
+ path6.join(dir, `wl_${name}_skeleton.dart`),
1026
+ tpl.skeleton
1027
+ );
1028
+ const contractsDir = path6.join(dir, "contracts");
1029
+ fs6.mkdirSync(contractsDir, { recursive: true });
1030
+ fs6.writeFileSync(
1031
+ path6.join(contractsDir, `wl_${name}_callbacks.dart`),
1032
+ tpl.callbacks
1033
+ );
1034
+ fs6.writeFileSync(
1035
+ path6.join(contractsDir, `wl_${name}_config.dart`),
1036
+ tpl.config
1037
+ );
1038
+ fs6.writeFileSync(
1039
+ path6.join(contractsDir, `wl_${name}_data.dart`),
1040
+ tpl.data
1041
+ );
1042
+ break;
1043
+ }
1044
+ default:
1045
+ throw new Error(`Unknown template pattern: ${pattern}`);
1046
+ }
1047
+ }
1048
+
1049
+ // src/core/create-usecase.ts
1050
+ import * as fs7 from "fs";
1051
+ import * as path7 from "path";
1052
+ import chalk3 from "chalk";
1053
+ import { pascalCase as pascalCase3 } from "change-case";
1054
+ var SNAKE_CASE_REGEX3 = /^[a-z][a-z0-9_]*$/;
1055
+ async function createUseCase(name, tierInput, options) {
1056
+ let cleanName = name;
1057
+ if (cleanName.startsWith("wl_")) {
1058
+ cleanName = cleanName.slice(3);
1059
+ }
1060
+ if (!cleanName || !SNAKE_CASE_REGEX3.test(cleanName)) {
1061
+ throw new Error(
1062
+ "Name must be snake_case (lowercase letters, digits, underscores)."
1063
+ );
1064
+ }
1065
+ const tier = normalizeTier(tierInput);
1066
+ const plural = tierPlural(tier);
1067
+ const pascal = pascalCase3(cleanName);
1068
+ const fileName = `wl_${cleanName}`;
1069
+ const ds = detectDesignSystem(options.projectRoot);
1070
+ if (!ds) {
1071
+ throw new Error(
1072
+ "Could not detect design system. Ensure wl_design_system/ directory exists."
1073
+ );
1074
+ }
1075
+ if (!ds.widgetbookDir) {
1076
+ throw new Error(
1077
+ "Could not detect widgetbook package. Ensure apps/widgetbook/ exists."
1078
+ );
1079
+ }
1080
+ if (!widgetExists(ds.componentsDir, plural, fileName)) {
1081
+ throw new Error(
1082
+ `Widget "${fileName}" not found in ${plural}/. Create the widget first.`
1083
+ );
1084
+ }
1085
+ const useCasesDir = path7.join(
1086
+ ds.widgetbookDir,
1087
+ "lib",
1088
+ "usecases",
1089
+ "wl_design_system"
1090
+ );
1091
+ if (!fs7.existsSync(useCasesDir)) {
1092
+ fs7.mkdirSync(useCasesDir, { recursive: true });
1093
+ }
1094
+ const useCasePath = path7.join(useCasesDir, `${fileName}.dart`);
1095
+ if (fs7.existsSync(useCasePath)) {
1096
+ throw new Error(`Use-case "${fileName}.dart" already exists.`);
1097
+ }
1098
+ fs7.writeFileSync(
1099
+ useCasePath,
1100
+ useCaseTemplate(cleanName, pascal, plural)
1101
+ );
1102
+ console.log(
1103
+ chalk3.green(
1104
+ `Use-case for "Wl${pascal}" created in widgetbook/lib/usecases/wl_design_system/`
1105
+ )
1106
+ );
1107
+ if (options.buildRunner) {
1108
+ if (hasBuildRunner(ds.widgetbookDir)) {
1109
+ console.log(chalk3.blue("Running build_runner..."));
1110
+ await runBuildRunner(ds.widgetbookDir);
1111
+ console.log(chalk3.green("build_runner completed."));
1112
+ } else {
1113
+ console.log(
1114
+ chalk3.yellow(
1115
+ "Skipping build_runner (no build_runner dependency found in widgetbook)."
1116
+ )
1117
+ );
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ // src/interactive.ts
1123
+ import * as fs8 from "fs";
1124
+ import * as os from "os";
1125
+ import * as path8 from "path";
1126
+ import * as clack from "@clack/prompts";
1127
+ import chalk4 from "chalk";
1128
+ var SNAKE_CASE_REGEX4 = /^[a-z][a-z0-9_]*$/;
258
1129
  async function resolveProject() {
259
1130
  const s = clack.spinner();
260
1131
  s.start("Analyzing current directory...");
261
1132
  const cwdProject = analyzeProject(process.cwd());
262
1133
  if (cwdProject && (cwdProject.hasFreezed || cwdProject.hasBloc)) {
263
- s.stop(`Found ${chalk2.green(cwdProject.projectName)}`);
1134
+ s.stop(`Found ${chalk4.green(cwdProject.projectName)}`);
264
1135
  return cwdProject;
265
1136
  }
266
1137
  s.message("Looking for Melos monorepo...");
@@ -273,8 +1144,8 @@ async function resolveProject() {
273
1144
  }
274
1145
  }
275
1146
  s.message("Scanning for Flutter projects...");
276
- const homeDev = path5.join(os.homedir(), "Development");
277
- if (fs5.existsSync(homeDev)) {
1147
+ const homeDev = path8.join(os.homedir(), "Development");
1148
+ if (fs8.existsSync(homeDev)) {
278
1149
  const projects = discoverProjects(homeDev, 2);
279
1150
  if (projects.length > 0) {
280
1151
  s.stop(`Found ${projects.length} Flutter project(s)`);
@@ -282,12 +1153,12 @@ async function resolveProject() {
282
1153
  }
283
1154
  }
284
1155
  s.stop("No Flutter projects found");
285
- clack.outro(chalk2.red("Could not find any Flutter project with freezed or flutter_bloc."));
1156
+ clack.outro(chalk4.red("Could not find any Flutter project with freezed or flutter_bloc."));
286
1157
  return null;
287
1158
  }
288
1159
  async function selectPackage(projects) {
289
1160
  if (projects.length === 1) {
290
- clack.log.info(`Using ${chalk2.green(projects[0].projectName)}`);
1161
+ clack.log.info(`Using ${chalk4.green(projects[0].projectName)}`);
291
1162
  return projects[0];
292
1163
  }
293
1164
  const selected = await clack.select({
@@ -295,7 +1166,7 @@ async function selectPackage(projects) {
295
1166
  options: projects.map((p) => ({
296
1167
  value: p,
297
1168
  label: p.projectName,
298
- hint: path5.relative(os.homedir(), p.projectRoot)
1169
+ hint: path8.relative(os.homedir(), p.projectRoot)
299
1170
  }))
300
1171
  });
301
1172
  if (clack.isCancel(selected)) {
@@ -304,16 +1175,14 @@ async function selectPackage(projects) {
304
1175
  }
305
1176
  return selected;
306
1177
  }
307
- async function interactiveMode() {
308
- clack.intro(chalk2.bgCyan(chalk2.black(" wlmaker ")));
309
- const project = await resolveProject();
310
- if (!project) return;
1178
+ async function blocFlow(project) {
311
1179
  const name = await clack.text({
312
1180
  message: "BLoC name (snake_case)",
313
1181
  placeholder: "e.g. user_login",
314
1182
  validate: (value) => {
315
1183
  if (!value.trim()) return "Name is required";
316
- if (!SNAKE_CASE_REGEX2.test(value)) return "Must be snake_case (lowercase, digits, underscores)";
1184
+ if (!SNAKE_CASE_REGEX4.test(value))
1185
+ return "Must be snake_case (lowercase, digits, underscores)";
317
1186
  }
318
1187
  });
319
1188
  if (clack.isCancel(name)) {
@@ -345,15 +1214,18 @@ async function interactiveMode() {
345
1214
  clack.cancel("Cancelled");
346
1215
  return;
347
1216
  }
348
- targetDir = path5.resolve(customPath);
1217
+ targetDir = path8.resolve(customPath);
349
1218
  } else {
350
- targetDir = path5.join(project.projectRoot, "lib", "features", feature);
1219
+ targetDir = path8.join(project.projectRoot, "lib", "features", feature);
351
1220
  }
352
- } else if (fs5.existsSync(path5.join(project.projectRoot, "lib", "bloc"))) {
353
- targetDir = path5.join(project.projectRoot, "lib", "bloc");
354
- clack.log.info(`Target: ${chalk2.cyan("lib/bloc/")}`);
1221
+ } else if (fs8.existsSync(path8.join(project.projectRoot, "lib", "bloc"))) {
1222
+ targetDir = path8.join(project.projectRoot, "lib", "bloc");
1223
+ clack.log.info(`Target: ${chalk4.cyan("lib/bloc/")}`);
355
1224
  } else {
356
- clack.note("No lib/features/ directory found. Provide a target path manually.", "Info");
1225
+ clack.note(
1226
+ "No lib/features/ directory found. Provide a target path manually.",
1227
+ "Info"
1228
+ );
357
1229
  const customPath = await clack.text({
358
1230
  message: "Target directory path",
359
1231
  placeholder: "lib/features/auth",
@@ -365,7 +1237,7 @@ async function interactiveMode() {
365
1237
  clack.cancel("Cancelled");
366
1238
  return;
367
1239
  }
368
- targetDir = path5.resolve(customPath);
1240
+ targetDir = path8.resolve(customPath);
369
1241
  }
370
1242
  const defaultRun = project.hasBuildRunner;
371
1243
  const runBuildRunner2 = await clack.confirm({
@@ -384,10 +1256,180 @@ async function interactiveMode() {
384
1256
  buildRunner: runBuildRunner2
385
1257
  });
386
1258
  genSpinner.stop("BLoC generated");
387
- clack.outro(chalk2.green("Done!"));
1259
+ clack.outro(chalk4.green("Done!"));
388
1260
  } catch (error) {
389
1261
  genSpinner.stop("Failed");
390
- clack.outro(chalk2.red(`Error: ${error}`));
1262
+ clack.outro(chalk4.red(`Error: ${error}`));
1263
+ }
1264
+ }
1265
+ async function widgetFlow(project) {
1266
+ const ds = detectDesignSystem(project.projectRoot);
1267
+ if (!ds) {
1268
+ clack.outro(
1269
+ chalk4.red(
1270
+ "No design system detected. Ensure wl_design_system/ directory exists."
1271
+ )
1272
+ );
1273
+ return;
1274
+ }
1275
+ clack.log.info(
1276
+ `Design system: ${chalk4.cyan(path8.relative(project.projectRoot, ds.componentsDir))}`
1277
+ );
1278
+ const name = await clack.text({
1279
+ message: "Widget name (snake_case, without wl_ prefix)",
1280
+ placeholder: "e.g. toggle, badge, snackbar",
1281
+ validate: (value) => {
1282
+ if (!value.trim()) return "Name is required";
1283
+ const clean = value.startsWith("wl_") ? value.slice(3) : value;
1284
+ if (!SNAKE_CASE_REGEX4.test(clean))
1285
+ return "Must be snake_case (lowercase, digits, underscores)";
1286
+ }
1287
+ });
1288
+ if (clack.isCancel(name)) {
1289
+ clack.cancel("Cancelled");
1290
+ return;
1291
+ }
1292
+ const allTiers = VALID_TIERS.map((t) => ({
1293
+ value: t,
1294
+ label: tierLabel(t)
1295
+ }));
1296
+ const selectedTier = await clack.select({
1297
+ message: "Select tier",
1298
+ options: allTiers
1299
+ });
1300
+ if (clack.isCancel(selectedTier)) {
1301
+ clack.cancel("Cancelled");
1302
+ return;
1303
+ }
1304
+ const tier = selectedTier;
1305
+ const patterns = getValidPatterns(tier);
1306
+ let selectedPattern = getDefaultPattern(tier);
1307
+ if (patterns.length > 1) {
1308
+ const patternChoice = await clack.select({
1309
+ message: "Select pattern",
1310
+ options: patterns.map((p) => ({
1311
+ value: p,
1312
+ label: patternLabel(p)
1313
+ }))
1314
+ });
1315
+ if (clack.isCancel(patternChoice)) {
1316
+ clack.cancel("Cancelled");
1317
+ return;
1318
+ }
1319
+ selectedPattern = patternChoice;
1320
+ }
1321
+ const genSpinner = clack.spinner();
1322
+ genSpinner.start("Generating widget files...");
1323
+ try {
1324
+ await createWidget(name, tier, {
1325
+ projectRoot: project.projectRoot,
1326
+ pattern: selectedPattern
1327
+ });
1328
+ genSpinner.stop("Widget generated");
1329
+ clack.outro(chalk4.green("Done!"));
1330
+ } catch (error) {
1331
+ genSpinner.stop("Failed");
1332
+ clack.outro(chalk4.red(`Error: ${error}`));
1333
+ }
1334
+ }
1335
+ async function useCaseFlow(project) {
1336
+ const ds = detectDesignSystem(project.projectRoot);
1337
+ if (!ds) {
1338
+ clack.outro(
1339
+ chalk4.red(
1340
+ "No design system detected. Ensure wl_design_system/ directory exists."
1341
+ )
1342
+ );
1343
+ return;
1344
+ }
1345
+ if (!ds.widgetbookDir) {
1346
+ clack.outro(
1347
+ chalk4.red(
1348
+ "No widgetbook package detected. Ensure apps/widgetbook/ exists."
1349
+ )
1350
+ );
1351
+ return;
1352
+ }
1353
+ const name = await clack.text({
1354
+ message: "Widget name for use-case (snake_case, without wl_ prefix)",
1355
+ placeholder: "e.g. toggle, badge",
1356
+ validate: (value) => {
1357
+ if (!value.trim()) return "Name is required";
1358
+ const clean = value.startsWith("wl_") ? value.slice(3) : value;
1359
+ if (!SNAKE_CASE_REGEX4.test(clean))
1360
+ return "Must be snake_case (lowercase, digits, underscores)";
1361
+ }
1362
+ });
1363
+ if (clack.isCancel(name)) {
1364
+ clack.cancel("Cancelled");
1365
+ return;
1366
+ }
1367
+ const allTiers = VALID_TIERS.map((t) => ({
1368
+ value: t,
1369
+ label: tierLabel(t)
1370
+ }));
1371
+ const selectedTier = await clack.select({
1372
+ message: "Select widget tier",
1373
+ options: allTiers
1374
+ });
1375
+ if (clack.isCancel(selectedTier)) {
1376
+ clack.cancel("Cancelled");
1377
+ return;
1378
+ }
1379
+ const tier = selectedTier;
1380
+ const runBuildRunner2 = await clack.confirm({
1381
+ message: "Run build_runner after generation?",
1382
+ initialValue: true
1383
+ });
1384
+ if (clack.isCancel(runBuildRunner2)) {
1385
+ clack.cancel("Cancelled");
1386
+ return;
1387
+ }
1388
+ const genSpinner = clack.spinner();
1389
+ genSpinner.start("Generating use-case file...");
1390
+ try {
1391
+ await createUseCase(name, tier, {
1392
+ projectRoot: project.projectRoot,
1393
+ buildRunner: runBuildRunner2
1394
+ });
1395
+ genSpinner.stop("Use-case generated");
1396
+ clack.outro(chalk4.green("Done!"));
1397
+ } catch (error) {
1398
+ genSpinner.stop("Failed");
1399
+ clack.outro(chalk4.red(`Error: ${error}`));
1400
+ }
1401
+ }
1402
+ async function interactiveMode() {
1403
+ clack.intro(chalk4.bgCyan(chalk4.black(" wlmaker ")));
1404
+ const createType = await clack.select({
1405
+ message: "What do you want to create?",
1406
+ options: [
1407
+ { value: "bloc", label: "BLoC", hint: "State management" },
1408
+ { value: "widget", label: "Widget", hint: "Design system component" },
1409
+ {
1410
+ value: "usecase",
1411
+ label: "Widgetbook Use-Case",
1412
+ hint: "Component showcase"
1413
+ }
1414
+ ]
1415
+ });
1416
+ if (clack.isCancel(createType)) {
1417
+ clack.cancel("Cancelled");
1418
+ return;
1419
+ }
1420
+ const type = createType;
1421
+ const project = await resolveProject();
1422
+ if (!project) return;
1423
+ switch (type) {
1424
+ case "bloc":
1425
+ await blocFlow(project);
1426
+ break;
1427
+ case "widget":
1428
+ await widgetFlow(project);
1429
+ break;
1430
+ case "usecase":
1431
+ await useCaseFlow(project);
1432
+ break;
391
1433
  }
392
1434
  }
393
1435
 
@@ -395,19 +1437,49 @@ async function interactiveMode() {
395
1437
  var require2 = createRequire(import.meta.url);
396
1438
  var pkg = require2("../package.json");
397
1439
  var program = new Command();
398
- program.name("wlmaker").description("Create Flutter BLoCs with Freezed sealed classes from the terminal").version(pkg.version);
399
- program.command("bloc").description("Create a new BLoC with Freezed sealed classes").argument("[name]", "BLoC name in snake_case (e.g. user_login)").option("-d, --dir <path>", "target directory", process.cwd()).option("--no-build-runner", "skip build_runner execution").action(async (name, options) => {
400
- if (!name) {
401
- await interactiveMode();
402
- return;
1440
+ program.name("wlmaker").description(
1441
+ "Create Flutter BLoCs, Widgets, and Widgetbook Use-Cases from the terminal"
1442
+ ).version(pkg.version);
1443
+ program.command("bloc").description("Create a new BLoC with Freezed sealed classes").argument("[name]", "BLoC name in snake_case (e.g. user_login)").option("-d, --dir <path>", "target directory", process.cwd()).option("--no-build-runner", "skip build_runner execution").action(
1444
+ async (name, options) => {
1445
+ if (!name) {
1446
+ await interactiveMode();
1447
+ return;
1448
+ }
1449
+ try {
1450
+ await createBloc(name, options);
1451
+ } catch (error) {
1452
+ console.error(chalk5.red(`Error: ${error}`));
1453
+ process.exit(1);
1454
+ }
403
1455
  }
404
- try {
405
- await createBloc(name, options);
406
- } catch (error) {
407
- console.error(chalk3.red(`Error: ${error}`));
408
- process.exit(1);
1456
+ );
1457
+ program.command("widget").description("Create a new widget in the design system").argument("<name>", "Widget name in snake_case (e.g. toggle)").requiredOption("-t, --tier <tier>", "tier: atom, molecule, organism, template").option("-p, --pattern <pattern>", "pattern (default depends on tier)").option("-d, --dir <path>", "project root directory", process.cwd()).action(
1458
+ async (name, options) => {
1459
+ try {
1460
+ await createWidget(name, options.tier, {
1461
+ projectRoot: options.dir,
1462
+ pattern: options.pattern
1463
+ });
1464
+ } catch (error) {
1465
+ console.error(chalk5.red(`Error: ${error}`));
1466
+ process.exit(1);
1467
+ }
409
1468
  }
410
- });
1469
+ );
1470
+ program.command("usecase").description("Create a Widgetbook use-case for an existing widget").argument("<name>", "Widget name in snake_case (e.g. toggle)").requiredOption("-t, --tier <tier>", "tier: atom, molecule, organism, template").option("-d, --dir <path>", "project root directory", process.cwd()).option("--no-build-runner", "skip build_runner execution").action(
1471
+ async (name, options) => {
1472
+ try {
1473
+ await createUseCase(name, options.tier, {
1474
+ projectRoot: options.dir,
1475
+ buildRunner: options.buildRunner
1476
+ });
1477
+ } catch (error) {
1478
+ console.error(chalk5.red(`Error: ${error}`));
1479
+ process.exit(1);
1480
+ }
1481
+ }
1482
+ );
411
1483
  program.action(async () => {
412
1484
  await interactiveMode();
413
1485
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wlmaker",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Create Flutter BLoCs with Freezed sealed classes from the terminal",
5
5
  "keywords": [
6
6
  "flutter",