generator-bitloops 0.3.30 → 0.3.32

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.30",
3
+ "version": "0.3.32",
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
@@ -10,6 +10,8 @@ const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
11
 
12
12
  const DOT = '.';
13
+ const MAX_RETRIES = 5;
14
+ const RETRY_DELAY_MS = 2000;
13
15
  const STORYBOOK_FOLDER = '.storybook';
14
16
  const PLATFORM_NEXT_FOLDER = 'platform-next';
15
17
  const PLATFORM_NEXT_SRC_FOLDER = `${PLATFORM_NEXT_FOLDER}/src`;
@@ -53,6 +55,18 @@ function deleteFileIfExists(filePath) {
53
55
  }
54
56
  }
55
57
 
58
+ /** Runs spawnSync and throws if exit code is non-zero (so steps can be retried). */
59
+ function runSync(command, args, options = {}) {
60
+ const result = spawnSync(command, args, { stdio: 'inherit', ...options });
61
+ if (result.status !== 0) {
62
+ const msg = result.error
63
+ ? result.error.message
64
+ : `Exit code ${result.status}`;
65
+ throw new Error(`${command} ${args.join(' ')} failed: ${msg}`);
66
+ }
67
+ return result;
68
+ }
69
+
56
70
  export default class extends Generator {
57
71
  constructor(args, opts) {
58
72
  super(args, opts);
@@ -201,17 +215,45 @@ export default class extends Generator {
201
215
  this.log('Installing Next.js...');
202
216
  const patchPackages = ''; //'next@14 react@18 react-dom@18';
203
217
  const additionalPackages = `react-tooltip ${patchPackages} class-variance-authority tailwind-merge`;
204
- await new Promise((resolve, error) => {
205
- exec(
206
- `npx ${createNextAppCommand.join(' ')} && cd ${toKebabCase(
207
- this.options.project,
208
- )} && pnpm add ${additionalPackages}`,
209
- ).on('exit', (code) => {
218
+ const installCommand = `npx ${createNextAppCommand.join(' ')} && cd ${toKebabCase(
219
+ this.options.project,
220
+ )} && pnpm add ${additionalPackages}`;
221
+ const INSTALL_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
222
+ await new Promise((resolve, reject) => {
223
+ const child = exec(installCommand, {
224
+ maxBuffer: 10 * 1024 * 1024, // 10MB to avoid EPIPE when output is large
225
+ });
226
+ const timeoutId = setTimeout(() => {
227
+ child.kill('SIGTERM');
228
+ reject(
229
+ new Error(
230
+ `Next.js install did not finish within ${INSTALL_TIMEOUT_MS / 60000} minutes. Try again or run the command manually.`,
231
+ ),
232
+ );
233
+ }, INSTALL_TIMEOUT_MS);
234
+ child.on('exit', (code) => {
235
+ clearTimeout(timeoutId);
236
+ if (code !== 0) {
237
+ reject(
238
+ new Error(
239
+ `Next.js install exited with code ${code}. Check the output above for errors.`,
240
+ ),
241
+ );
242
+ return;
243
+ }
210
244
  this.destinationRoot(
211
245
  this.destinationPath(toKebabCase(this.options.project)),
212
246
  );
213
247
  resolve();
214
248
  });
249
+ child.on('error', (err) => {
250
+ clearTimeout(timeoutId);
251
+ reject(
252
+ new Error(
253
+ `Next.js install failed: ${err.message}. You may have hit a network or spawn issue; try again.`,
254
+ ),
255
+ );
256
+ });
215
257
  });
216
258
  };
217
259
 
@@ -221,6 +263,7 @@ export default class extends Generator {
221
263
  this.log('Installing Storybook...');
222
264
  const versionsRaw = execSync('npm view storybook versions --json', {
223
265
  encoding: 'utf-8',
266
+ timeout: 30000, // 30s – avoid hanging on slow/unresponsive registry
224
267
  });
225
268
  const versions = JSON.parse(versionsRaw);
226
269
 
@@ -246,25 +289,20 @@ export default class extends Generator {
246
289
 
247
290
  this.log(`Latest stable 10.x version: ${latest10}`);
248
291
  // Initializing storybook with nextjs+vite
249
- spawnSync(
250
- 'npx',
251
- [
252
- '-y',
253
- `storybook@${latest10}`,
254
- 'init',
255
- '--no-dev',
256
- '--yes',
257
- '--type',
258
- 'nextjs',
259
- '--builder',
260
- 'vite',
261
- ],
262
- { stdio: 'inherit', cwd: this.destinationRoot() },
263
- );
292
+ runSync('npx', [
293
+ '-y',
294
+ `storybook@${latest10}`,
295
+ 'init',
296
+ '--no-dev',
297
+ '--yes',
298
+ '--type',
299
+ 'nextjs',
300
+ '--builder',
301
+ 'vite',
302
+ ], { cwd: this.destinationRoot() });
264
303
  this.log('Storybook installed!');
265
304
  // Verifies the correct nextjs-vite framework is used
266
- spawnSync('pnpm', ['add', '-D', '@storybook/nextjs-vite@^10'], {
267
- stdio: 'inherit',
305
+ runSync('pnpm', ['add', '-D', '@storybook/nextjs-vite@^10'], {
268
306
  cwd: this.destinationRoot(),
269
307
  });
270
308
  this.log('@storybook/nextjs-vite installed!');
@@ -275,23 +313,18 @@ export default class extends Generator {
275
313
  // Conditionally add Cypress
276
314
  if (this.options.cypress) {
277
315
  this.log('Installing Cypress...');
278
- spawnSync('pnpm', ['add', '-D', 'cypress'], {
279
- stdio: 'inherit',
316
+ runSync('pnpm', ['add', '-D', 'cypress'], {
280
317
  cwd: this.destinationRoot(),
281
318
  });
282
319
  this.log('Cypress installed!');
283
320
  if (this.options.bitloops) {
284
- spawnSync(
285
- 'pnpm',
286
- [
287
- 'add',
288
- '-D',
289
- 'mochawesome',
290
- 'mochawesome-merge',
291
- 'mochawesome-report-generator',
292
- ],
293
- { stdio: 'inherit', cwd: this.destinationRoot() },
294
- );
321
+ runSync('pnpm', [
322
+ 'add',
323
+ '-D',
324
+ 'mochawesome',
325
+ 'mochawesome-merge',
326
+ 'mochawesome-report-generator',
327
+ ], { cwd: this.destinationRoot() });
295
328
  }
296
329
  }
297
330
  };
@@ -300,8 +333,7 @@ export default class extends Generator {
300
333
  // Conditionally add i18n packages
301
334
  if (this.options.i18n) {
302
335
  this.log('Installing i18n packages...');
303
- spawnSync('pnpm', ['add', 'i18next', 'i18next-icu', 'react-i18next'], {
304
- stdio: 'inherit',
336
+ runSync('pnpm', ['add', 'i18next', 'i18next-icu', 'react-i18next'], {
305
337
  cwd: this.destinationRoot(),
306
338
  });
307
339
  this.log('i18n packages installed!');
@@ -312,8 +344,7 @@ export default class extends Generator {
312
344
  // Conditionally add Base UI
313
345
  if (this.options.baseUi) {
314
346
  this.log('Installing Base UI...');
315
- spawnSync('pnpm', ['add', '@base-ui/react@^1.1.0'], {
316
- stdio: 'inherit',
347
+ runSync('pnpm', ['add', '@base-ui/react@^1.1.0'], {
317
348
  cwd: this.destinationRoot(),
318
349
  });
319
350
  this.log('Base UI installed!');
@@ -324,8 +355,7 @@ export default class extends Generator {
324
355
  // Conditionally add Redux Toolkit and React Redux
325
356
  if (this.options.redux) {
326
357
  this.log('Installing Redux Toolkit and React Redux...');
327
- spawnSync('pnpm', ['add', '@reduxjs/toolkit', 'react-redux'], {
328
- stdio: 'inherit',
358
+ runSync('pnpm', ['add', '@reduxjs/toolkit', 'react-redux'], {
329
359
  cwd: this.destinationRoot(),
330
360
  });
331
361
  this.log('Redux Toolkit and React Redux installed!');
@@ -336,25 +366,21 @@ export default class extends Generator {
336
366
  // Conditionally add Vitest and related testing packages
337
367
  if (this.options.vitest) {
338
368
  this.log('Installing Vitest and testing packages...');
339
- spawnSync(
340
- 'pnpm',
341
- [
342
- 'add',
343
- '-D',
344
- 'vitest',
345
- '@vitest/ui',
346
- '@vitest/coverage-v8',
347
- '@vitest/browser-playwright',
348
- '@testing-library/react',
349
- '@testing-library/jest-dom',
350
- '@testing-library/user-event',
351
- '@vitejs/plugin-react',
352
- 'vite',
353
- 'jsdom',
354
- 'playwright',
355
- ],
356
- { stdio: 'inherit', cwd: this.destinationRoot() },
357
- );
369
+ runSync('pnpm', [
370
+ 'add',
371
+ '-D',
372
+ 'vitest',
373
+ '@vitest/ui',
374
+ '@vitest/coverage-v8',
375
+ '@vitest/browser-playwright',
376
+ '@testing-library/react',
377
+ '@testing-library/jest-dom',
378
+ '@testing-library/user-event',
379
+ '@vitejs/plugin-react',
380
+ 'vite',
381
+ 'jsdom',
382
+ 'playwright',
383
+ ], { cwd: this.destinationRoot() });
358
384
  this.log('Vitest and testing packages installed!');
359
385
  }
360
386
  };
@@ -363,8 +389,7 @@ export default class extends Generator {
363
389
  // Conditionally add web-vitals
364
390
  if (this.options.webVitals) {
365
391
  this.log('Installing web-vitals...');
366
- spawnSync('pnpm', ['add', 'web-vitals'], {
367
- stdio: 'inherit',
392
+ runSync('pnpm', ['add', 'web-vitals'], {
368
393
  cwd: this.destinationRoot(),
369
394
  });
370
395
  this.log('web-vitals installed!');
@@ -375,8 +400,7 @@ export default class extends Generator {
375
400
  // Conditionally add Zod
376
401
  if (this.options.zod) {
377
402
  this.log('Installing Zod...');
378
- spawnSync('pnpm', ['add', 'zod'], {
379
- stdio: 'inherit',
403
+ runSync('pnpm', ['add', 'zod'], {
380
404
  cwd: this.destinationRoot(),
381
405
  });
382
406
  this.log('Zod installed!');
@@ -387,8 +411,7 @@ export default class extends Generator {
387
411
  // Conditionally add @next/bundle-analyzer
388
412
  if (this.options.bundleAnalyzer) {
389
413
  this.log('Installing @next/bundle-analyzer...');
390
- spawnSync('pnpm', ['add', '-D', '@next/bundle-analyzer'], {
391
- stdio: 'inherit',
414
+ runSync('pnpm', ['add', '-D', '@next/bundle-analyzer'], {
392
415
  cwd: this.destinationRoot(),
393
416
  });
394
417
  this.log('@next/bundle-analyzer installed!');
@@ -399,8 +422,7 @@ export default class extends Generator {
399
422
  // Conditionally add react-icons
400
423
  if (this.options.reactIcons) {
401
424
  this.log('Installing react-icons...');
402
- spawnSync('pnpm', ['add', 'react-icons'], {
403
- stdio: 'inherit',
425
+ runSync('pnpm', ['add', 'react-icons'], {
404
426
  cwd: this.destinationRoot(),
405
427
  });
406
428
  this.log('react-icons installed!');
@@ -411,13 +433,11 @@ export default class extends Generator {
411
433
  // Conditionally add Mock Service Worker
412
434
  if (this.options.msw) {
413
435
  this.log('Installing Mock Service Worker (MSW)...');
414
- spawnSync('pnpm', ['add', '-D', 'msw'], {
415
- stdio: 'inherit',
436
+ runSync('pnpm', ['add', '-D', 'msw'], {
416
437
  cwd: this.destinationRoot(),
417
438
  });
418
439
  // Initialize MSW
419
- spawnSync('npx', ['msw', 'init', 'public/', '--save'], {
420
- stdio: 'inherit',
440
+ runSync('npx', ['msw', 'init', 'public/', '--save'], {
421
441
  cwd: this.destinationRoot(),
422
442
  });
423
443
  this.log('MSW installed!');
@@ -428,8 +448,7 @@ export default class extends Generator {
428
448
  // Conditionally add babel-plugin-react-compiler
429
449
  if (this.options.reactCompiler) {
430
450
  this.log('Installing babel-plugin-react-compiler...');
431
- spawnSync('pnpm', ['add', '-D', 'babel-plugin-react-compiler'], {
432
- stdio: 'inherit',
451
+ runSync('pnpm', ['add', '-D', 'babel-plugin-react-compiler'], {
433
452
  cwd: this.destinationRoot(),
434
453
  });
435
454
  this.log('babel-plugin-react-compiler installed!');
@@ -440,8 +459,7 @@ export default class extends Generator {
440
459
  // Add intl-messageformat when i18n is enabled
441
460
  if (this.options.i18n) {
442
461
  this.log('Installing intl-messageformat...');
443
- spawnSync('pnpm', ['add', 'intl-messageformat'], {
444
- stdio: 'inherit',
462
+ runSync('pnpm', ['add', 'intl-messageformat'], {
445
463
  cwd: this.destinationRoot(),
446
464
  });
447
465
  this.log('intl-messageformat installed!');
@@ -598,8 +616,7 @@ export default class extends Generator {
598
616
  const path = 'cypress/helpers/index.ts';
599
617
  this.fs.copyTpl(this.templatePath(path), this.destinationPath(path));
600
618
  }
601
- spawnSync('pnpm', ['add', '-D', 'react-aria-components'], {
602
- stdio: 'inherit',
619
+ runSync('pnpm', ['add', '-D', 'react-aria-components'], {
603
620
  cwd: this.destinationRoot(),
604
621
  });
605
622
  }
@@ -640,21 +657,44 @@ export default class extends Generator {
640
657
 
641
658
  this.commitChanges = async function () {
642
659
  this.log('Committing changes to git...');
643
- await new Promise((resolve) => {
644
- exec(
660
+ await new Promise((resolve, reject) => {
661
+ const child = exec(
645
662
  `cd ${toKebabCase(
646
663
  this.options.project,
647
664
  )} && git add . && git commit -m "Initial setup"`,
648
- ).on('exit', (code) => {
665
+ );
666
+ child.on('exit', (code) => {
649
667
  if (code !== 0) {
650
668
  this.log('Error committing changes to git! ', code);
651
- resolve();
669
+ } else {
670
+ this.log('Git changes committed!');
652
671
  }
653
- this.log('Git changes committed!');
654
672
  resolve();
655
673
  });
674
+ child.on('error', (err) => {
675
+ this.log('Git commit failed: ', err.message);
676
+ resolve(); // do not fail the whole run if git commit fails
677
+ });
656
678
  });
657
679
  };
680
+
681
+ /** Runs a step (sync or async), retrying up to MAX_RETRIES times on failure. */
682
+ this.withRetry = async function (fn, stepName) {
683
+ let lastErr;
684
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
685
+ try {
686
+ return await Promise.resolve(fn());
687
+ } catch (err) {
688
+ lastErr = err;
689
+ if (attempt === MAX_RETRIES) break;
690
+ this.log(
691
+ `Step "${stepName}" failed (attempt ${attempt}/${MAX_RETRIES}): ${err.message}. Retrying in ${RETRY_DELAY_MS / 1000}s...`,
692
+ );
693
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
694
+ }
695
+ }
696
+ throw lastErr;
697
+ };
658
698
  }
659
699
 
660
700
  initializing() {
@@ -681,25 +721,25 @@ export default class extends Generator {
681
721
  }
682
722
 
683
723
  async main() {
684
- await this.installNextJS();
685
- this.installCypress();
686
- this.installI18n();
687
- this.installIntlMessageFormat();
688
- this.installBaseUi();
689
- this.installRedux();
690
- this.installVitest();
691
- this.installWebVitals();
692
- this.installZod();
693
- this.installBundleAnalyzer();
694
- this.installReactIcons();
695
- this.installMsw();
696
- this.installReactCompiler();
697
- this.installPrimitives();
698
- this.installStorybook();
699
- await this.patchFiles();
700
- await this.patchPackageJsonScripts();
724
+ await this.withRetry(() => this.installNextJS(), 'Next.js install');
725
+ await this.withRetry(() => this.installCypress(), 'Cypress');
726
+ await this.withRetry(() => this.installI18n(), 'i18n');
727
+ await this.withRetry(() => this.installIntlMessageFormat(), 'intl-messageformat');
728
+ await this.withRetry(() => this.installBaseUi(), 'Base UI');
729
+ await this.withRetry(() => this.installRedux(), 'Redux');
730
+ await this.withRetry(() => this.installVitest(), 'Vitest');
731
+ await this.withRetry(() => this.installWebVitals(), 'web-vitals');
732
+ await this.withRetry(() => this.installZod(), 'Zod');
733
+ await this.withRetry(() => this.installBundleAnalyzer(), 'bundle-analyzer');
734
+ await this.withRetry(() => this.installReactIcons(), 'react-icons');
735
+ await this.withRetry(() => this.installMsw(), 'MSW');
736
+ await this.withRetry(() => this.installReactCompiler(), 'react-compiler');
737
+ await this.withRetry(() => this.installPrimitives(), 'Primitives');
738
+ await this.withRetry(() => this.installStorybook(), 'Storybook');
739
+ await this.withRetry(() => this.patchFiles(), 'Patch files');
740
+ await this.withRetry(() => this.patchPackageJsonScripts(), 'Patch package.json');
701
741
  if (this.options.git) {
702
- await this.commitChanges();
742
+ await this.withRetry(() => this.commitChanges(), 'Git commit');
703
743
  }
704
744
  }
705
745
 
@@ -23,7 +23,7 @@ const config: StorybookConfig = {
23
23
  if (config.resolve) {
24
24
  config.resolve.alias = {
25
25
  ...config.resolve.alias,
26
- '@/primitives': path.resolve(__dirname, '../platform-vite/src'),
26
+ '@primitives': path.resolve(__dirname, '../platform-vite/src'),
27
27
  };
28
28
  }
29
29
  return config;
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
- "lib": ["dom", "dom.iterable", "esnext"],
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
5
9
  "allowJs": true,
6
10
  "skipLibCheck": true,
7
11
  "strict": true,
@@ -20,17 +24,42 @@
20
24
  ],
21
25
  "baseUrl": ".",
22
26
  "paths": {
23
- "@/*": ["src/*"],
24
- "@/components/*": ["src/components/*"],
25
- "@/assets/*": ["src/assets/*"],
26
- "@/primitives": ["platform-next/src"],
27
- "@/primitives/*": ["platform-next/src/*"],
28
- "@/hooks/*": ["src/hooks/*"],
29
- "@/lib/*": ["src/lib/*"],
30
- "@/types/*": ["src/types/*"],
31
- "@/utils/*": ["src/utils/*"]
27
+ "@/*": [
28
+ "src/*"
29
+ ],
30
+ "@/components/*": [
31
+ "src/components/*"
32
+ ],
33
+ "@/assets/*": [
34
+ "src/assets/*"
35
+ ],
36
+ "@primitives": [
37
+ "platform-next/src"
38
+ ],
39
+ "@primitives/*": [
40
+ "platform-next/src/*"
41
+ ],
42
+ "@/hooks/*": [
43
+ "src/hooks/*"
44
+ ],
45
+ "@/lib/*": [
46
+ "src/lib/*"
47
+ ],
48
+ "@/types/*": [
49
+ "src/types/*"
50
+ ],
51
+ "@/utils/*": [
52
+ "src/utils/*"
53
+ ]
32
54
  }
33
55
  },
34
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
35
- "exclude": ["node_modules"]
36
- }
56
+ "include": [
57
+ "next-env.d.ts",
58
+ "**/*.ts",
59
+ "**/*.tsx",
60
+ ".next/types/**/*.ts"
61
+ ],
62
+ "exclude": [
63
+ "node_modules"
64
+ ]
65
+ }