generator-bitloops 0.3.26 → 0.3.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generator-bitloops",
3
- "version": "0.3.26",
3
+ "version": "0.3.28",
4
4
  "description": "Next.js with TypeScript, Tailwind, Storybook and Cypress generator by Bitloops",
5
5
  "license": "MIT",
6
6
  "author": "Bitloops S.A.",
package/setup/index.js CHANGED
@@ -12,6 +12,8 @@ const __dirname = path.dirname(__filename);
12
12
  const DOT = '.';
13
13
  const PLATFORM_NEXT_FOLDER = 'platform-next';
14
14
  const PLATFORM_NEXT_SRC_FOLDER = `${PLATFORM_NEXT_FOLDER}/src`;
15
+ const PLATFORM_VITE_FOLDER = 'platform-vite';
16
+ const PLATFORM_VITE_SRC_FOLDER = `${PLATFORM_VITE_FOLDER}/src`;
15
17
 
16
18
  function isKebabCase(str) {
17
19
  // Check if the string is empty
@@ -118,6 +120,60 @@ export default class extends Generator {
118
120
  default: false,
119
121
  });
120
122
 
123
+ this.option('baseUi', {
124
+ type: Boolean,
125
+ description: 'Add Base UI React components (@base-ui/react)',
126
+ default: false,
127
+ });
128
+
129
+ this.option('redux', {
130
+ type: Boolean,
131
+ description: 'Add Redux Toolkit and React Redux for state management',
132
+ default: false,
133
+ });
134
+
135
+ this.option('vitest', {
136
+ type: Boolean,
137
+ description: 'Add Vitest testing framework with coverage and UI',
138
+ default: false,
139
+ });
140
+
141
+ this.option('webVitals', {
142
+ type: Boolean,
143
+ description: 'Add web-vitals for performance monitoring',
144
+ default: false,
145
+ });
146
+
147
+ this.option('zod', {
148
+ type: Boolean,
149
+ description: 'Add Zod for schema validation',
150
+ default: false,
151
+ });
152
+
153
+ this.option('bundleAnalyzer', {
154
+ type: Boolean,
155
+ description: 'Add @next/bundle-analyzer for bundle analysis',
156
+ default: false,
157
+ });
158
+
159
+ this.option('reactIcons', {
160
+ type: Boolean,
161
+ description: 'Add react-icons library',
162
+ default: false,
163
+ });
164
+
165
+ this.option('msw', {
166
+ type: Boolean,
167
+ description: 'Add Mock Service Worker (MSW) for API mocking',
168
+ default: false,
169
+ });
170
+
171
+ this.option('reactCompiler', {
172
+ type: Boolean,
173
+ description: 'Add babel-plugin-react-compiler',
174
+ default: false,
175
+ });
176
+
121
177
  this.installNextJS = async function () {
122
178
  // Clone Next.js template with Tailwind if specified, using the project name
123
179
  const createNextAppCommand = ['-y', 'create-next-app@latest'];
@@ -253,31 +309,196 @@ export default class extends Generator {
253
309
  }
254
310
  };
255
311
 
256
- this.installPrimitives = function () {
257
- // Conditionally add Primitives
258
- if (this.options.primitives) {
259
- this.log('Installing Primitives...');
312
+ this.installBaseUi = function () {
313
+ // Conditionally add Base UI
314
+ if (this.options.baseUi) {
315
+ this.log('Installing Base UI...');
316
+ spawnSync(
317
+ 'pnpm',
318
+ ['add', '@base-ui/react@^1.1.0'],
319
+ { stdio: 'inherit', cwd: this.destinationRoot() },
320
+ );
321
+ this.log('Base UI installed!');
322
+ }
323
+ };
260
324
 
261
- const platformNextIndexPath = `${PLATFORM_NEXT_SRC_FOLDER}/index.ts`;
262
- deleteFileIfExists(this.destinationPath(platformNextIndexPath));
263
- this.fs.copyTpl(
264
- this.templatePath(platformNextIndexPath),
265
- this.destinationPath(platformNextIndexPath),
325
+ this.installRedux = function () {
326
+ // Conditionally add Redux Toolkit and React Redux
327
+ if (this.options.redux) {
328
+ this.log('Installing Redux Toolkit and React Redux...');
329
+ spawnSync(
330
+ 'pnpm',
331
+ ['add', '@reduxjs/toolkit', 'react-redux'],
332
+ { stdio: 'inherit', cwd: this.destinationRoot() },
266
333
  );
334
+ this.log('Redux Toolkit and React Redux installed!');
335
+ }
336
+ };
267
337
 
268
- const platformNextImgPath = `${PLATFORM_NEXT_SRC_FOLDER}/Img.tsx`;
269
- deleteFileIfExists(this.destinationPath(platformNextImgPath));
270
- this.fs.copyTpl(
271
- this.templatePath(platformNextImgPath),
272
- this.destinationPath(platformNextImgPath),
338
+ this.installVitest = function () {
339
+ // Conditionally add Vitest and related testing packages
340
+ if (this.options.vitest) {
341
+ this.log('Installing Vitest and testing packages...');
342
+ spawnSync(
343
+ 'pnpm',
344
+ [
345
+ 'add',
346
+ '-D',
347
+ 'vitest',
348
+ '@vitest/ui',
349
+ '@vitest/coverage-v8',
350
+ '@vitest/browser-playwright',
351
+ '@testing-library/react',
352
+ '@testing-library/jest-dom',
353
+ '@testing-library/user-event',
354
+ '@vitejs/plugin-react',
355
+ 'vite',
356
+ 'jsdom',
357
+ 'playwright',
358
+ ],
359
+ { stdio: 'inherit', cwd: this.destinationRoot() },
273
360
  );
361
+ this.log('Vitest and testing packages installed!');
362
+ }
363
+ };
274
364
 
275
- const platformNextTypesPath = `${PLATFORM_NEXT_SRC_FOLDER}/types.ts`;
276
- deleteFileIfExists(this.destinationPath(platformNextTypesPath));
277
- this.fs.copyTpl(
278
- this.templatePath(platformNextTypesPath),
279
- this.destinationPath(platformNextTypesPath),
365
+ this.installWebVitals = function () {
366
+ // Conditionally add web-vitals
367
+ if (this.options.webVitals) {
368
+ this.log('Installing web-vitals...');
369
+ spawnSync(
370
+ 'pnpm',
371
+ ['add', 'web-vitals'],
372
+ { stdio: 'inherit', cwd: this.destinationRoot() },
280
373
  );
374
+ this.log('web-vitals installed!');
375
+ }
376
+ };
377
+
378
+ this.installZod = function () {
379
+ // Conditionally add Zod
380
+ if (this.options.zod) {
381
+ this.log('Installing Zod...');
382
+ spawnSync(
383
+ 'pnpm',
384
+ ['add', 'zod'],
385
+ { stdio: 'inherit', cwd: this.destinationRoot() },
386
+ );
387
+ this.log('Zod installed!');
388
+ }
389
+ };
390
+
391
+ this.installBundleAnalyzer = function () {
392
+ // Conditionally add @next/bundle-analyzer
393
+ if (this.options.bundleAnalyzer) {
394
+ this.log('Installing @next/bundle-analyzer...');
395
+ spawnSync(
396
+ 'pnpm',
397
+ ['add', '-D', '@next/bundle-analyzer'],
398
+ { stdio: 'inherit', cwd: this.destinationRoot() },
399
+ );
400
+ this.log('@next/bundle-analyzer installed!');
401
+ }
402
+ };
403
+
404
+ this.installReactIcons = function () {
405
+ // Conditionally add react-icons
406
+ if (this.options.reactIcons) {
407
+ this.log('Installing react-icons...');
408
+ spawnSync(
409
+ 'pnpm',
410
+ ['add', 'react-icons'],
411
+ { stdio: 'inherit', cwd: this.destinationRoot() },
412
+ );
413
+ this.log('react-icons installed!');
414
+ }
415
+ };
416
+
417
+ this.installMsw = function () {
418
+ // Conditionally add Mock Service Worker
419
+ if (this.options.msw) {
420
+ this.log('Installing Mock Service Worker (MSW)...');
421
+ spawnSync(
422
+ 'pnpm',
423
+ ['add', '-D', 'msw'],
424
+ { stdio: 'inherit', cwd: this.destinationRoot() },
425
+ );
426
+ // Initialize MSW
427
+ spawnSync(
428
+ 'npx',
429
+ ['msw', 'init', 'public/', '--save'],
430
+ { stdio: 'inherit', cwd: this.destinationRoot() },
431
+ );
432
+ this.log('MSW installed!');
433
+ }
434
+ };
435
+
436
+ this.installReactCompiler = function () {
437
+ // Conditionally add babel-plugin-react-compiler
438
+ if (this.options.reactCompiler) {
439
+ this.log('Installing babel-plugin-react-compiler...');
440
+ spawnSync(
441
+ 'pnpm',
442
+ ['add', '-D', 'babel-plugin-react-compiler'],
443
+ { stdio: 'inherit', cwd: this.destinationRoot() },
444
+ );
445
+ this.log('babel-plugin-react-compiler installed!');
446
+ }
447
+ };
448
+
449
+ this.installIntlMessageFormat = function () {
450
+ // Add intl-messageformat when i18n is enabled
451
+ if (this.options.i18n) {
452
+ this.log('Installing intl-messageformat...');
453
+ spawnSync(
454
+ 'pnpm',
455
+ ['add', 'intl-messageformat'],
456
+ { stdio: 'inherit', cwd: this.destinationRoot() },
457
+ );
458
+ this.log('intl-messageformat installed!');
459
+ }
460
+ };
461
+
462
+ this.installPrimitives = function () {
463
+ // Conditionally add Primitives
464
+ if (this.options.primitives) {
465
+ this.log('Installing Primitives...');
466
+
467
+ // Platform Next files
468
+ const platformNextFiles = [
469
+ `${PLATFORM_NEXT_SRC_FOLDER}/index.ts`,
470
+ `${PLATFORM_NEXT_SRC_FOLDER}/Img.tsx`,
471
+ `${PLATFORM_NEXT_SRC_FOLDER}/Link.tsx`,
472
+ `${PLATFORM_NEXT_SRC_FOLDER}/setup.ts`,
473
+ `${PLATFORM_NEXT_SRC_FOLDER}/router/index.ts`,
474
+ `${PLATFORM_NEXT_SRC_FOLDER}/router/useNextRouter.ts`,
475
+ ];
476
+
477
+ platformNextFiles.forEach((filePath) => {
478
+ deleteFileIfExists(this.destinationPath(filePath));
479
+ this.fs.copyTpl(
480
+ this.templatePath(filePath),
481
+ this.destinationPath(filePath),
482
+ );
483
+ });
484
+
485
+ // Platform Vite files
486
+ const platformViteFiles = [
487
+ `${PLATFORM_VITE_SRC_FOLDER}/index.ts`,
488
+ `${PLATFORM_VITE_SRC_FOLDER}/Img.tsx`,
489
+ `${PLATFORM_VITE_SRC_FOLDER}/Link.tsx`,
490
+ `${PLATFORM_VITE_SRC_FOLDER}/setup.ts`,
491
+ `${PLATFORM_VITE_SRC_FOLDER}/router/index.ts`,
492
+ `${PLATFORM_VITE_SRC_FOLDER}/router/useViteRouter.ts`,
493
+ ];
494
+
495
+ platformViteFiles.forEach((filePath) => {
496
+ deleteFileIfExists(this.destinationPath(filePath));
497
+ this.fs.copyTpl(
498
+ this.templatePath(filePath),
499
+ this.destinationPath(filePath),
500
+ );
501
+ });
281
502
 
282
503
  this.log('Primitives installed!');
283
504
  }
@@ -383,6 +604,39 @@ export default class extends Generator {
383
604
  }
384
605
  };
385
606
 
607
+ this.patchPackageJsonScripts = async function () {
608
+ this.log('Patching package.json scripts...');
609
+ const packageJsonPath = this.destinationPath('package.json');
610
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
611
+
612
+ // Add vitest scripts if vitest is enabled
613
+ if (this.options.vitest) {
614
+ packageJson.scripts.test = 'vitest';
615
+ packageJson.scripts['test:ui'] = 'vitest --ui';
616
+ packageJson.scripts['test:coverage'] = 'vitest run --coverage';
617
+ }
618
+
619
+ // Add type-check script if typescript is enabled
620
+ if (this.options.typescript) {
621
+ packageJson.scripts['type-check'] = 'tsc --noEmit';
622
+ }
623
+
624
+ // Add analyze script if bundleAnalyzer is enabled
625
+ if (this.options.bundleAnalyzer) {
626
+ packageJson.scripts.analyze = 'ANALYZE=true next build';
627
+ }
628
+
629
+ // Add msw configuration if msw is enabled
630
+ if (this.options.msw) {
631
+ packageJson.msw = {
632
+ workerDirectory: ['public'],
633
+ };
634
+ }
635
+
636
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
637
+ this.log('package.json scripts patched!');
638
+ };
639
+
386
640
  this.commitChanges = async function () {
387
641
  this.log('Committing changes to git...');
388
642
  await new Promise((resolve) => {
@@ -429,9 +683,20 @@ export default class extends Generator {
429
683
  await this.installNextJS();
430
684
  this.installCypress();
431
685
  this.installI18n();
686
+ this.installIntlMessageFormat();
687
+ this.installBaseUi();
688
+ this.installRedux();
689
+ this.installVitest();
690
+ this.installWebVitals();
691
+ this.installZod();
692
+ this.installBundleAnalyzer();
693
+ this.installReactIcons();
694
+ this.installMsw();
695
+ this.installReactCompiler();
432
696
  this.installPrimitives();
433
697
  this.installStorybook();
434
698
  await this.patchFiles();
699
+ await this.patchPackageJsonScripts();
435
700
  if (this.options.git) {
436
701
  await this.commitChanges();
437
702
  }
@@ -1,7 +1,8 @@
1
- 'use client';
2
- import NextImage from 'next/image';
3
- import type { ImgProps } from './types';
1
+ "use client";
2
+ import NextImage from "next/image";
3
+ import type { ImgProps } from "../../src/lib/types/primitives.types";
4
+
4
5
  export function Img(props: ImgProps) {
5
- const { responsive, ...rest } = props;
6
+ const { ...rest } = props;
6
7
  return <NextImage {...rest} />;
7
- }
8
+ }
@@ -0,0 +1,6 @@
1
+ "use client";
2
+ import NextLink from "next/link";
3
+ import type { LinkProps } from "../../src/lib/types/primitives.types";
4
+ export function Link(props: LinkProps) {
5
+ return <NextLink {...props} />;
6
+ }
@@ -1,2 +1,3 @@
1
- export * from './types';
2
- export * from './Img';
1
+ export * from "../../src/lib/types/primitives.types";
2
+ export * from "./Img";
3
+ export * from "./Link";
@@ -0,0 +1 @@
1
+ export { useNextRouter } from './useNextRouter';
@@ -0,0 +1,14 @@
1
+ "use client";
2
+
3
+ import { useRouter as useNextNavigationRouter } from 'next/navigation';
4
+ import type { Router } from '@/lib/router/types';
5
+
6
+ export function useNextRouter(): Router {
7
+ const nextRouter = useNextNavigationRouter();
8
+
9
+ return {
10
+ push: (url: string) => nextRouter.push(url),
11
+ replace: (url: string) => nextRouter.replace(url),
12
+ back: () => nextRouter.back(),
13
+ };
14
+ }
@@ -0,0 +1,7 @@
1
+ "use client";
2
+
3
+ import { setRouterImplementation } from '@/lib/router/useRouter';
4
+ import { useNextRouter } from './router/useNextRouter';
5
+
6
+ // Set up Next.js router implementation
7
+ setRouterImplementation(useNextRouter);
@@ -0,0 +1,18 @@
1
+ /* eslint-disable @next/next/no-img-element */
2
+ import type { ImgProps } from "../../src/lib/types/primitives.types";
3
+
4
+ export function Img(props: ImgProps) {
5
+ const { src, alt, className, style, loading, decoding } =
6
+ props;
7
+
8
+ return (
9
+ <img
10
+ src={src}
11
+ alt={alt}
12
+ className={className}
13
+ style={style}
14
+ loading={loading}
15
+ decoding={decoding}
16
+ />
17
+ );
18
+ }
@@ -0,0 +1,19 @@
1
+ import type { LinkProps } from "../../src/lib/types/primitives.types";
2
+
3
+ export function Link(props: LinkProps) {
4
+ const { href, children, target, rel, className, style, onClick } = props;
5
+
6
+ return (
7
+ <a
8
+ href={href}
9
+ target={target}
10
+ rel={rel}
11
+ className={className}
12
+ style={style}
13
+ onClick={onClick}
14
+ aria-label={props["aria-label"]}
15
+ >
16
+ {children}
17
+ </a>
18
+ );
19
+ }
@@ -0,0 +1,3 @@
1
+ export * from "../../src/lib/types/primitives.types";
2
+ export * from "./Img";
3
+ export * from "./Link";
@@ -0,0 +1 @@
1
+ export { useViteRouter } from "./useViteRouter";
@@ -0,0 +1,17 @@
1
+ import type { Router } from "@/lib/router/types";
2
+
3
+ export function useViteRouter(): Router {
4
+ return {
5
+ push: (url: string) => {
6
+ console.log("[Storybook Router] Push to:", url);
7
+ // In Storybook, we don't actually navigate
8
+ // This could be enhanced with Storybook actions addon
9
+ },
10
+ replace: (url: string) => {
11
+ console.log("[Storybook Router] Replace with:", url);
12
+ },
13
+ back: () => {
14
+ console.log("[Storybook Router] Go back");
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,5 @@
1
+ import { setRouterImplementation } from "@/lib/router/useRouter";
2
+ import { useViteRouter } from "./router/useViteRouter";
3
+
4
+ // Set up Vite/Storybook router implementation
5
+ setRouterImplementation(useViteRouter);
@@ -1,28 +0,0 @@
1
- export type ImgProps = {
2
- src: string; alt: string;
3
- width?: number; height?: number; fill?: boolean;
4
- sizes?: string; priority?: boolean; quality?: number;
5
- className?: string; style?: React.CSSProperties;
6
- loading?: 'eager'|'lazy'; decoding?: 'auto'|'sync'|'async';
7
- responsive?: { sources: Array<{ media: string; srcSet: string }> }; // optional <picture>
8
- };
9
-
10
- export type LinkProps = {
11
- href: string; children: React.ReactNode;
12
- prefetch?: boolean; replace?: boolean; scroll?: boolean;
13
- target?: React.HTMLAttributeAnchorTarget; rel?: string;
14
- className?: string; style?: React.CSSProperties; 'aria-label'?: string;
15
- };
16
-
17
- export type MetaProps = {
18
- title?: string;
19
- description?: string;
20
- lang?: string;
21
- openGraph?: { title?: string; description?: string; image?: string; url?: string; type?: string };
22
- twitter?: { card?: string; title?: string; description?: string; image?: string };
23
- icons?: { icon?: string; apple?: string };
24
- };
25
-
26
- export type FontSpec =
27
- | { kind: 'next'; fonts: Array<{ variable: string; loader: () => any }> } // Next native loaders
28
- | { kind: 'css'; hrefs: string[]; className?: string };