create-hsi-app 0.5.2 → 0.6.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/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # create-hsi-app
2
2
 
3
- Scaffold a new Vite + React + TypeScript app from the frontend template.
3
+ Scaffold a new Vite or Next.js App Router SPA + React + TypeScript app from
4
+ the frontend template.
4
5
 
5
6
  ## Usage
6
7
 
@@ -29,4 +30,4 @@ bun create hsi-app@latest
29
30
  ```
30
31
 
31
32
  Full CLI usage, flags, and repo/install behavior:
32
- [docs/create-hsi-app.md](https://github.com/Hsiii/frontend-template/blob/main/docs/create-hsi-app.md)
33
+ [docs/CLI.md](https://github.com/Hsiii/frontend-template/blob/main/docs/CLI.md)
@@ -2,27 +2,41 @@
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import {
4
4
  existsSync,
5
+ mkdirSync,
5
6
  readdirSync,
6
7
  readFileSync,
7
8
  rmSync,
8
9
  writeFileSync,
9
10
  } from 'node:fs';
10
11
  import { basename, join, resolve } from 'node:path';
11
- import { stdin as input, stdout as output } from 'node:process';
12
- import readline from 'node:readline/promises';
12
+
13
+ import {
14
+ closePrompts,
15
+ confirm,
16
+ fail,
17
+ gap,
18
+ intro,
19
+ ready,
20
+ section,
21
+ select,
22
+ text,
23
+ warn,
24
+ } from './ui.mjs';
13
25
 
14
26
  const templateRepo = 'https://github.com/Hsiii/frontend-template.git';
15
- const templateTag = 'v0.5.2';
27
+ const templateTag = 'v0.6.1';
16
28
  const defaultAppName = 'my-app';
17
29
  const packageManagers = ['bun', 'npm', 'pnpm', 'yarn'];
30
+ const nextVersion = '16.2.7';
18
31
  const rawArgs = process.argv.slice(2);
19
32
  const parsedArgs = parseCliArgs(rawArgs);
20
33
  const selectedPackageManager = resolvePackageManager(parsedArgs);
21
- const shouldInstallDependencies = !(
34
+ let selectedFramework = resolveFramework(parsedArgs);
35
+ let shouldInstallDependencies = !(
22
36
  parsedArgs.noInstall || readNpmBooleanFlag('noinstall')
23
37
  );
24
38
  const shouldSkipRepoSetup = parsedArgs.noRepo || readNpmBooleanFlag('norepo');
25
- const isInteractive = input.isTTY && output.isTTY;
39
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
26
40
  const targetArg = parsedArgs.targetArg ?? '.';
27
41
  const targetPath = resolve(targetArg);
28
42
  const appName = toPackageName(basename(targetPath));
@@ -36,6 +50,13 @@ async function main() {
36
50
  fail(`Target directory is not empty: ${targetPath}`);
37
51
  }
38
52
 
53
+ intro(appName, targetPath);
54
+ selectedFramework = await planFramework();
55
+ const repoPlan = await planRepoSetup();
56
+ shouldInstallDependencies = await planInstallDependencies();
57
+ closePrompts();
58
+
59
+ section(`Cloning ${frameworkLabel(selectedFramework)} template`);
39
60
  run('git', [
40
61
  '-c',
41
62
  'advice.detachedHead=false',
@@ -56,35 +77,32 @@ async function main() {
56
77
 
57
78
  updatePackageJson();
58
79
  updateBunLock();
80
+ console.log();
81
+ section('Customizing project files');
82
+ console.log(`- framework: ${frameworkLabel(selectedFramework)}`);
83
+ console.log(`- package.json: name, version, scripts, packageManager`);
84
+ logFrameworkFileChanges();
85
+ console.log(`- .gitignore: framework build artifacts`);
86
+ console.log(`- README.md: install/dev/check commands`);
87
+ console.log(`- package manager config: ${packageManagerConfigFile()}`);
88
+ if (selectedPackageManager === 'bun') {
89
+ console.log(`- bun.lock: package name`);
90
+ }
91
+ updateFrameworkFiles();
59
92
  updateAppText();
93
+ updateGitIgnore();
60
94
  updatePackageManagerFiles();
61
95
  writeAppReadme();
62
96
 
63
97
  if (shouldInstallDependencies) {
98
+ console.log();
99
+ section(`Installing dependencies with ${selectedPackageManager}`);
64
100
  installDependencies();
65
101
  }
66
102
 
67
- const repoSetup = await maybeSetupRepo();
103
+ await applyRepoPlan(repoPlan);
68
104
 
69
- console.log(`\nCreated ${appName} in ${targetPath}\n`);
70
- if (repoSetup === 'github') {
71
- console.log(
72
- 'Created a local git repository and configured GitHub origin.'
73
- );
74
- } else if (repoSetup === 'local') {
75
- console.log('Initialized a local git repository.');
76
- }
77
- if (shouldInstallDependencies) {
78
- console.log(`Installed dependencies with ${selectedPackageManager}.`);
79
- }
80
- console.log('\nNext steps:');
81
- if (targetArg !== '.') {
82
- console.log(` cd ${targetArg}`);
83
- }
84
- if (!shouldInstallDependencies) {
85
- console.log(` ${installCommand()}`);
86
- }
87
- console.log(` ${devCommand()}`);
105
+ ready(appName, nextSteps());
88
106
  }
89
107
 
90
108
  function run(command, args, options = {}) {
@@ -116,8 +134,20 @@ function updatePackageJson() {
116
134
  delete packageJson.engines;
117
135
  delete packageJson.scripts.prepare;
118
136
  delete packageJson.scripts.release;
119
- packageJson.scripts.check =
120
- 'tsc -p tsconfig.json --noEmit && eslint . && prettier . --check && vite build';
137
+ if (selectedFramework === 'next') {
138
+ packageJson.scripts.dev = 'next dev';
139
+ packageJson.scripts.build = 'next build';
140
+ delete packageJson.scripts.preview;
141
+ packageJson.scripts.check =
142
+ 'tsc -p tsconfig.json --noEmit && eslint . && prettier . --check && next build';
143
+ packageJson.dependencies.next = nextVersion;
144
+ packageJson.devDependencies['@next/eslint-plugin-next'] = nextVersion;
145
+ delete packageJson.devDependencies['@vitejs/plugin-react'];
146
+ delete packageJson.devDependencies.vite;
147
+ } else {
148
+ packageJson.scripts.check =
149
+ 'tsc -p tsconfig.json --noEmit && eslint . && prettier . --check && vite build';
150
+ }
121
151
  packageJson.packageManager = packageManagerDeclaration();
122
152
 
123
153
  writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 4)}\n`);
@@ -144,20 +174,31 @@ function updateBunLock() {
144
174
  }
145
175
 
146
176
  function updateAppText() {
147
- replaceInFile(
148
- join(targetPath, 'index.html'),
149
- '<title>Frontend Template</title>',
150
- {
151
- with: `<title>${appName}</title>`,
152
- }
153
- );
154
- replaceInFile(
155
- join(targetPath, 'src/components/App.tsx'),
156
- '>Frontend Template<',
157
- {
158
- with: `>${appName}<`,
159
- }
160
- );
177
+ if (selectedFramework === 'vite') {
178
+ replaceInFile(
179
+ join(targetPath, 'index.html'),
180
+ '<title>Frontend Template</title>',
181
+ {
182
+ with: `<title>${appName}</title>`,
183
+ }
184
+ );
185
+ }
186
+
187
+ writeFileSync(join(targetPath, 'src/components/App.tsx'), appComponent());
188
+ }
189
+
190
+ function updateFrameworkFiles() {
191
+ if (selectedFramework === 'next') {
192
+ writeNextAppFiles();
193
+ }
194
+ }
195
+
196
+ function updateGitIgnore() {
197
+ if (selectedFramework !== 'next') {
198
+ return;
199
+ }
200
+
201
+ appendGitIgnoreEntries(['.next/', 'next-env.d.ts']);
161
202
  }
162
203
 
163
204
  function updatePackageManagerFiles() {
@@ -219,7 +260,7 @@ function writeAppReadme() {
219
260
  const securityNote = securityNoteForPackageManager();
220
261
  const readme = `# ${appName}
221
262
 
222
- Created from the frontend template.
263
+ Created from the ${frameworkDescription(selectedFramework)} frontend template.
223
264
 
224
265
  ## Install
225
266
 
@@ -245,58 +286,132 @@ ${securityNote}
245
286
  writeFileSync(join(targetPath, 'README.md'), readme);
246
287
  }
247
288
 
248
- async function maybeSetupRepo() {
289
+ async function planFramework() {
290
+ if (parsedArgs.framework || !isInteractive) {
291
+ return selectedFramework;
292
+ }
293
+
294
+ const framework = await select({
295
+ message: 'Framework',
296
+ options: [
297
+ { label: 'Vite', value: 'vite' },
298
+ { label: 'Next.js', value: 'next' },
299
+ ],
300
+ initialValue: 'vite',
301
+ });
302
+ gap();
303
+
304
+ return framework;
305
+ }
306
+
307
+ async function planInstallDependencies() {
308
+ if (!shouldInstallDependencies || !isInteractive) {
309
+ return shouldInstallDependencies;
310
+ }
311
+
312
+ const shouldInstall = await confirm({
313
+ message: `Should I run "${installCommand()}" for you?`,
314
+ initialValue: true,
315
+ });
316
+ gap();
317
+
318
+ return shouldInstall;
319
+ }
320
+
321
+ async function planRepoSetup() {
249
322
  if (shouldSkipRepoSetup || !isInteractive) {
250
323
  return null;
251
324
  }
252
325
 
253
- const rl = readline.createInterface({ input, output });
326
+ const shouldCreateRepo = await confirm({
327
+ message: 'Create a git repository?',
328
+ initialValue: true,
329
+ });
330
+ gap();
254
331
 
255
- try {
256
- const shouldCreateRepo = await promptYesNo(
257
- rl,
258
- 'Create a git repository?',
259
- true
332
+ if (!shouldCreateRepo) {
333
+ return null;
334
+ }
335
+
336
+ const repoPlan = {
337
+ git: true,
338
+ github: false,
339
+ };
340
+ const hasGitHubCli = canUseGitHubCli();
341
+
342
+ if (!hasGitHubCli) {
343
+ warn(
344
+ 'GitHub CLI is unavailable or not authenticated; keeping a local repository only.'
260
345
  );
346
+ gap();
347
+ return repoPlan;
348
+ }
261
349
 
262
- if (!shouldCreateRepo) {
263
- return null;
264
- }
350
+ const shouldCreateGitHubRepo = await confirm({
351
+ message: 'Create a GitHub repository too?',
352
+ initialValue: true,
353
+ });
354
+ gap();
355
+
356
+ if (!shouldCreateGitHubRepo) {
357
+ return repoPlan;
358
+ }
265
359
 
266
- initLocalRepo();
360
+ const defaultRepoName = basename(targetPath);
361
+ const repoName = await text({
362
+ message: 'Repository name',
363
+ defaultValue: defaultRepoName,
364
+ placeholder: defaultRepoName,
365
+ validate(value) {
366
+ return value.trim() ? undefined : 'Repository name is required.';
367
+ },
368
+ });
369
+ gap();
370
+ const visibility = await select({
371
+ message: 'Visibility',
372
+ options: [
373
+ { label: 'Private', value: 'private' },
374
+ { label: 'Public', value: 'public' },
375
+ ],
376
+ initialValue: 'private',
377
+ });
378
+ gap();
379
+
380
+ return {
381
+ ...repoPlan,
382
+ github: true,
383
+ repoName,
384
+ visibility,
385
+ };
386
+ }
267
387
 
268
- if (!canUseGitHubCli()) {
269
- return 'local';
270
- }
388
+ async function applyRepoPlan(repoPlan) {
389
+ if (!repoPlan) {
390
+ return;
391
+ }
271
392
 
272
- const defaultRepoName = basename(targetPath);
273
- const repoName = await promptWithDefault(
274
- rl,
275
- 'Repository name',
276
- defaultRepoName
277
- );
278
- const visibility = await promptChoice(rl, 'Visibility', [
279
- { label: 'private', value: 'private', default: true },
280
- { label: 'public', value: 'public' },
281
- ]);
282
-
283
- run(
284
- 'gh',
285
- [
286
- 'repo',
287
- 'create',
288
- repoName,
289
- `--${visibility}`,
290
- '--source=.',
291
- '--remote=origin',
292
- ],
293
- { cwd: targetPath }
294
- );
393
+ console.log();
394
+ section('Initializing local git repository');
395
+ initLocalRepo();
295
396
 
296
- return 'github';
297
- } finally {
298
- rl.close();
397
+ if (!repoPlan.github) {
398
+ return;
299
399
  }
400
+
401
+ console.log();
402
+ section('Creating GitHub repository');
403
+ run(
404
+ 'gh',
405
+ [
406
+ 'repo',
407
+ 'create',
408
+ repoPlan.repoName,
409
+ `--${repoPlan.visibility}`,
410
+ '--source=.',
411
+ '--remote=origin',
412
+ ],
413
+ { cwd: targetPath }
414
+ );
300
415
  }
301
416
 
302
417
  function initLocalRepo() {
@@ -305,76 +420,58 @@ function initLocalRepo() {
305
420
  }
306
421
 
307
422
  function canUseGitHubCli() {
308
- return Boolean(
423
+ return (
309
424
  run('gh', ['auth', 'status'], {
310
425
  cwd: targetPath,
311
426
  capture: true,
312
427
  allowFailure: true,
313
- })
428
+ }) !== null
314
429
  );
315
430
  }
316
431
 
317
- async function promptYesNo(rl, label, defaultValue) {
318
- const hint = defaultValue ? 'Y/n' : 'y/N';
432
+ function nextSteps() {
433
+ const steps = [];
319
434
 
320
- while (true) {
321
- const answer = (await rl.question(`${label} [${hint}] `))
322
- .trim()
323
- .toLowerCase();
435
+ if (targetArg !== '.') {
436
+ steps.push(`cd ${targetArg}`);
437
+ }
324
438
 
325
- if (!answer) {
326
- return defaultValue;
327
- }
439
+ if (!shouldInstallDependencies) {
440
+ steps.push(installCommand());
441
+ }
328
442
 
329
- if (['y', 'yes'].includes(answer)) {
330
- return true;
331
- }
443
+ steps.push(devCommand());
332
444
 
333
- if (['n', 'no'].includes(answer)) {
334
- return false;
335
- }
336
- }
445
+ return steps;
337
446
  }
338
447
 
339
- async function promptWithDefault(rl, label, defaultValue) {
340
- const answer = (await rl.question(`${label} (${defaultValue}): `)).trim();
341
-
342
- return answer || defaultValue;
448
+ function replaceInFile(filePath, searchValue, replacement) {
449
+ const source = readFileSync(filePath, 'utf8');
450
+ writeFileSync(filePath, source.replace(searchValue, replacement.with));
343
451
  }
344
452
 
345
- async function promptChoice(rl, label, choices) {
346
- const renderedChoices = choices
347
- .map((choice) =>
348
- choice.default ? `${choice.label.toUpperCase()}` : choice.label
349
- )
350
- .join('/');
453
+ function appendGitIgnoreEntries(entries) {
454
+ const gitIgnorePath = join(targetPath, '.gitignore');
351
455
 
352
- while (true) {
353
- const answer = (await rl.question(`${label} (${renderedChoices}): `))
354
- .trim()
355
- .toLowerCase();
456
+ if (!existsSync(gitIgnorePath)) {
457
+ writeFileSync(gitIgnorePath, `${entries.join('\n')}\n`);
458
+ return;
459
+ }
356
460
 
357
- if (!answer) {
358
- const defaultChoice = choices.find((choice) => choice.default);
461
+ const source = readFileSync(gitIgnorePath, 'utf8');
462
+ const lines = new Set(source.split('\n').filter(Boolean));
463
+ let nextSource = source;
359
464
 
360
- if (defaultChoice) {
361
- return defaultChoice.value;
362
- }
465
+ for (const entry of entries) {
466
+ if (lines.has(entry)) {
467
+ continue;
363
468
  }
364
469
 
365
- const matchingChoice = choices.find(
366
- (choice) => choice.label === answer || choice.value === answer
367
- );
368
-
369
- if (matchingChoice) {
370
- return matchingChoice.value;
371
- }
470
+ nextSource += nextSource.endsWith('\n') ? `${entry}\n` : `\n${entry}\n`;
471
+ lines.add(entry);
372
472
  }
373
- }
374
473
 
375
- function replaceInFile(filePath, searchValue, replacement) {
376
- const source = readFileSync(filePath, 'utf8');
377
- writeFileSync(filePath, source.replace(searchValue, replacement.with));
474
+ writeFileSync(gitIgnorePath, nextSource);
378
475
  }
379
476
 
380
477
  function toPackageName(value) {
@@ -391,6 +488,7 @@ function toPackageName(value) {
391
488
 
392
489
  function parseCliArgs(args) {
393
490
  const parsedArgs = {
491
+ framework: null,
394
492
  noInstall: false,
395
493
  noRepo: false,
396
494
  packageManager: null,
@@ -399,6 +497,12 @@ function parseCliArgs(args) {
399
497
 
400
498
  for (const arg of args) {
401
499
  switch (arg) {
500
+ case '--vite':
501
+ setFrameworkOverride(parsedArgs, 'vite');
502
+ continue;
503
+ case '--next':
504
+ setFrameworkOverride(parsedArgs, 'next');
505
+ continue;
402
506
  case '--bun':
403
507
  setPackageManagerOverride(parsedArgs, 'bun');
404
508
  continue;
@@ -433,6 +537,14 @@ function parseCliArgs(args) {
433
537
  return parsedArgs;
434
538
  }
435
539
 
540
+ function setFrameworkOverride(parsedArgs, framework) {
541
+ if (parsedArgs.framework && parsedArgs.framework !== framework) {
542
+ fail('Pass only one of --vite or --next.');
543
+ }
544
+
545
+ parsedArgs.framework = framework;
546
+ }
547
+
436
548
  function setPackageManagerOverride(parsedArgs, packageManager) {
437
549
  if (
438
550
  parsedArgs.packageManager &&
@@ -448,12 +560,339 @@ function resolvePackageManager(parsedArgs) {
448
560
  return parsedArgs.packageManager ?? 'bun';
449
561
  }
450
562
 
563
+ function resolveFramework(parsedArgs) {
564
+ return parsedArgs.framework ?? 'vite';
565
+ }
566
+
451
567
  function readNpmBooleanFlag(name) {
452
568
  const value = process.env[`npm_config_${name}`];
453
569
 
454
570
  return value === 'true' || value === '';
455
571
  }
456
572
 
573
+ function logFrameworkFileChanges() {
574
+ if (selectedFramework === 'next') {
575
+ console.log(
576
+ `- Next app router files: src/app/layout.tsx, src/app/[[...slug]]/*`
577
+ );
578
+ console.log(`- src/app/global.css: app styles and client bootstrap`);
579
+ console.log(`- Next config: next.config.mjs, next-env.d.ts`);
580
+ console.log(
581
+ `- Vite files removed: index.html, vite.config.mjs, src/main.tsx`
582
+ );
583
+ return;
584
+ }
585
+
586
+ console.log(`- index.html: title`);
587
+ console.log(`- src/components/App.tsx: app name`);
588
+ }
589
+
590
+ function writeNextAppFiles() {
591
+ rmSync(join(targetPath, 'index.html'), { force: true });
592
+ rmSync(join(targetPath, 'vite.config.mjs'), { force: true });
593
+ rmSync(join(targetPath, 'src/main.tsx'), { force: true });
594
+ rmSync(join(targetPath, 'src/vite-env.d.ts'), { force: true });
595
+ rmSync(join(targetPath, 'src/global.css'), { force: true });
596
+
597
+ const appPath = join(targetPath, 'src/app');
598
+ const catchAllPath = join(appPath, '[[...slug]]');
599
+ mkdirSync(appPath, { recursive: true });
600
+ mkdirSync(catchAllPath, { recursive: true });
601
+
602
+ writeFileSync(join(targetPath, 'next-env.d.ts'), nextEnvTypes());
603
+ writeFileSync(join(targetPath, 'next.config.mjs'), nextConfig());
604
+ writeFileSync(join(targetPath, 'eslint.config.mjs'), nextEslintConfig());
605
+ writeFileSync(join(targetPath, 'tsconfig.json'), nextTsconfig());
606
+ writeFileSync(join(appPath, 'layout.tsx'), nextLayout());
607
+ writeFileSync(join(appPath, 'global.css'), nextGlobalCss());
608
+ writeFileSync(join(catchAllPath, 'client.tsx'), nextClientPage());
609
+ writeFileSync(join(catchAllPath, 'page.tsx'), nextPage());
610
+ }
611
+
612
+ function frameworkLabel(framework) {
613
+ switch (framework) {
614
+ case 'vite':
615
+ return 'Vite';
616
+ case 'next':
617
+ return 'Next.js';
618
+ default:
619
+ fail(`Unsupported framework: ${framework}`);
620
+ }
621
+ }
622
+
623
+ function frameworkDescription(framework) {
624
+ switch (framework) {
625
+ case 'vite':
626
+ return 'Vite';
627
+ case 'next':
628
+ return 'Next.js App Router SPA';
629
+ default:
630
+ fail(`Unsupported framework: ${framework}`);
631
+ }
632
+ }
633
+
634
+ function frameworkTitle(framework) {
635
+ switch (framework) {
636
+ case 'vite':
637
+ return 'Vite, React, and TypeScript.';
638
+ case 'next':
639
+ return 'Next.js, React, and TypeScript.';
640
+ default:
641
+ fail(`Unsupported framework: ${framework}`);
642
+ }
643
+ }
644
+
645
+ function appComponent() {
646
+ return `import type { JSX } from 'react';
647
+
648
+ export function App(): JSX.Element {
649
+ return (
650
+ <main className='app'>
651
+ <section className='app__content'>
652
+ <p className='app__eyebrow'>${appName}</p>
653
+ <h1 className='app__title'>${frameworkTitle(selectedFramework)}</h1>
654
+ <p className='app__description'>
655
+ A clean baseline with strict tooling, useful tokens, and no
656
+ unnecessary UI noise.
657
+ </p>
658
+ </section>
659
+ </main>
660
+ );
661
+ }
662
+ `;
663
+ }
664
+
665
+ function nextEnvTypes() {
666
+ return `/// <reference types="next" />
667
+ /// <reference types="next/image-types/global" />
668
+
669
+ // This file should not be edited.
670
+ `;
671
+ }
672
+
673
+ function nextConfig() {
674
+ return `/** @type {import("next").NextConfig} */
675
+ const nextConfig = {
676
+ output: 'export',
677
+ distDir: './dist',
678
+ };
679
+
680
+ export default nextConfig;
681
+ `;
682
+ }
683
+
684
+ function nextEslintConfig() {
685
+ return `import nextPlugin from '@next/eslint-plugin-next';
686
+ import { completeConfigBase } from 'eslint-config-complete';
687
+
688
+ export default [
689
+ ...completeConfigBase,
690
+
691
+ {
692
+ ignores: ['.next/**', 'dist/**', 'node_modules/**'],
693
+ },
694
+
695
+ {
696
+ plugins: {
697
+ '@next/next': nextPlugin,
698
+ },
699
+ rules: {
700
+ ...nextPlugin.configs.recommended.rules,
701
+ ...nextPlugin.configs['core-web-vitals'].rules,
702
+ '@stylistic/quotes': [
703
+ 'error',
704
+ 'single',
705
+ {
706
+ avoidEscape: true,
707
+ },
708
+ ],
709
+ 'import-x/no-unassigned-import': [
710
+ 'error',
711
+ {
712
+ allow: ['**/*.css'],
713
+ },
714
+ ],
715
+ },
716
+ },
717
+
718
+ {
719
+ files: ['src/app/**/*.tsx'],
720
+ rules: {
721
+ 'complete/no-mutable-return': 'off',
722
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
723
+ 'n/file-extension-in-import': 'off',
724
+ 'import-x/no-default-export': 'off',
725
+ },
726
+ },
727
+ ];
728
+ `;
729
+ }
730
+
731
+ function nextTsconfig() {
732
+ return `{
733
+ "compilerOptions": {
734
+ "target": "ES2022",
735
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
736
+ "allowJs": false,
737
+ "skipLibCheck": true,
738
+ "strict": true,
739
+ "noEmit": true,
740
+ "esModuleInterop": true,
741
+ "module": "ESNext",
742
+ "moduleResolution": "Bundler",
743
+ "resolveJsonModule": true,
744
+ "isolatedModules": true,
745
+ "jsx": "react-jsx",
746
+ "incremental": true,
747
+ "noUnusedLocals": true,
748
+ "noUnusedParameters": true,
749
+ "noFallthroughCasesInSwitch": true,
750
+ "plugins": [
751
+ {
752
+ "name": "next"
753
+ }
754
+ ],
755
+ "paths": {
756
+ "@/*": ["./src/*"]
757
+ }
758
+ },
759
+ "include": [
760
+ "next-env.d.ts",
761
+ "src/**/*.ts",
762
+ "src/**/*.tsx",
763
+ ".next/dev/types/**/*.ts",
764
+ ".next/types/**/*.ts"
765
+ ],
766
+ "exclude": ["node_modules"]
767
+ }
768
+ `;
769
+ }
770
+
771
+ function nextLayout() {
772
+ return `import type { JSX, ReactNode } from 'react';
773
+ import type { Metadata } from 'next';
774
+
775
+ import './global.css';
776
+
777
+ export const metadata: Metadata = {
778
+ title: '${appName}',
779
+ description: 'Created from create-hsi-app.',
780
+ };
781
+
782
+ interface RootLayoutProps {
783
+ readonly children: ReactNode;
784
+ }
785
+
786
+ export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
787
+ return (
788
+ <html lang='en'>
789
+ <body>{children}</body>
790
+ </html>
791
+ );
792
+ }
793
+ `;
794
+ }
795
+
796
+ function nextClientPage() {
797
+ return `'use client';
798
+
799
+ import type { JSX } from 'react';
800
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
801
+
802
+ import { App } from '@/components/App';
803
+
804
+ const queryClient = new QueryClient();
805
+
806
+ export function ClientOnly(): JSX.Element {
807
+ return (
808
+ <QueryClientProvider client={queryClient}>
809
+ <App />
810
+ </QueryClientProvider>
811
+ );
812
+ }
813
+ `;
814
+ }
815
+
816
+ function nextPage() {
817
+ return `import type { JSX } from 'react';
818
+
819
+ import { ClientOnly } from './client';
820
+
821
+ export function generateStaticParams() {
822
+ return [{ slug: [''] }];
823
+ }
824
+
825
+ export default function HomePage(): JSX.Element {
826
+ return <ClientOnly />;
827
+ }
828
+ `;
829
+ }
830
+
831
+ function nextGlobalCss() {
832
+ return `@import '../constants/color.css';
833
+ @import '../constants/font.css';
834
+
835
+ * {
836
+ margin: 0;
837
+ padding: 0;
838
+ box-sizing: border-box;
839
+ }
840
+
841
+ html {
842
+ background-color: var(--clr-bg);
843
+ color: var(--clr-text);
844
+ }
845
+
846
+ body {
847
+ min-width: 320px;
848
+ min-height: 100vh;
849
+ font: var(--font-body-md);
850
+ line-height: 1.5;
851
+ background-color: var(--clr-bg);
852
+ }
853
+
854
+ a {
855
+ color: inherit;
856
+ }
857
+
858
+ :focus-visible {
859
+ outline: calc(var(--space-16) / 8) solid var(--clr-accent);
860
+ outline-offset: calc(var(--space-16) / 8);
861
+ }
862
+
863
+ .app {
864
+ min-height: 100vh;
865
+ display: grid;
866
+ place-items: center;
867
+ padding: var(--space-32) var(--space-24);
868
+ }
869
+
870
+ .app__content {
871
+ display: grid;
872
+ justify-items: center;
873
+ gap: var(--space-16);
874
+ width: fit-content;
875
+ max-width: 100%;
876
+ text-align: center;
877
+ }
878
+
879
+ .app__eyebrow {
880
+ color: var(--clr-text-muted);
881
+ font: var(--font-label);
882
+ letter-spacing: 0.08em;
883
+ text-transform: uppercase;
884
+ }
885
+
886
+ .app__title {
887
+ font: var(--font-display);
888
+ }
889
+
890
+ .app__description {
891
+ color: var(--clr-text-muted);
892
+ }
893
+ `;
894
+ }
895
+
457
896
  function packageManagerDeclaration() {
458
897
  switch (selectedPackageManager) {
459
898
  case 'bun':
@@ -525,7 +964,17 @@ function securityNoteForPackageManager() {
525
964
  }
526
965
  }
527
966
 
528
- function fail(message) {
529
- console.error(`create-hsi-app: ${message}`);
530
- process.exit(1);
967
+ function packageManagerConfigFile() {
968
+ switch (selectedPackageManager) {
969
+ case 'bun':
970
+ return 'bunfig.toml';
971
+ case 'npm':
972
+ return '.npmrc';
973
+ case 'pnpm':
974
+ return 'pnpm-workspace.yaml';
975
+ case 'yarn':
976
+ return '.yarnrc.yml';
977
+ default:
978
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
979
+ }
531
980
  }
package/bin/ui.mjs ADDED
@@ -0,0 +1,70 @@
1
+ import * as prompts from '@clack/prompts';
2
+ import color from 'picocolors';
3
+
4
+ const branch = color.dim('│');
5
+
6
+ export function intro(appName, targetPath) {
7
+ prompts.intro(color.inverse(' create-hsi-app '));
8
+ console.log(branch);
9
+ console.log(
10
+ `${color.cyan('◇')} Scaffolding ${color.bold(appName)} in ${targetPath}`
11
+ );
12
+ console.log(branch);
13
+ }
14
+
15
+ export function closePrompts() {
16
+ console.log('└─');
17
+ console.log();
18
+ }
19
+
20
+ export function section(title) {
21
+ console.log(color.bold(color.magentaBright(title)));
22
+ }
23
+
24
+ export function gap() {
25
+ console.log(branch);
26
+ }
27
+
28
+ export function warn(message) {
29
+ console.log(`${color.yellow('▲')} ${message}`);
30
+ }
31
+
32
+ export function fail(message) {
33
+ prompts.cancel(color.red(message));
34
+ process.exit(1);
35
+ }
36
+
37
+ export function ready(appName, lines) {
38
+ console.log();
39
+ console.log(color.green(`App scaffolded: ${appName}`));
40
+ console.log();
41
+ section('Next steps');
42
+
43
+ for (const line of lines) {
44
+ console.log(line);
45
+ }
46
+ }
47
+
48
+ export async function confirm(options) {
49
+ const value = await prompts.confirm(options);
50
+ return unwrapPrompt(value);
51
+ }
52
+
53
+ export async function select(options) {
54
+ const value = await prompts.select(options);
55
+ return unwrapPrompt(value);
56
+ }
57
+
58
+ export async function text(options) {
59
+ const value = await prompts.text(options);
60
+ return unwrapPrompt(value);
61
+ }
62
+
63
+ function unwrapPrompt(value) {
64
+ if (prompts.isCancel(value)) {
65
+ prompts.cancel('Cancelled.');
66
+ process.exit(1);
67
+ }
68
+
69
+ return value;
70
+ }
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "create-hsi-app",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "Create a new app from the frontend template.",
6
+ "dependencies": {
7
+ "@clack/prompts": "0.11.0",
8
+ "picocolors": "1.1.1"
9
+ },
6
10
  "bin": {
7
11
  "create-hsi-app": "bin/create-hsi-app.mjs"
8
12
  },