create-hsi-app 0.5.2 → 0.6.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.
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 + React + TypeScript app from the frontend
4
+ 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.0';
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,30 @@ 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(`- README.md: install/dev/check commands`);
86
+ console.log(`- package manager config: ${packageManagerConfigFile()}`);
87
+ if (selectedPackageManager === 'bun') {
88
+ console.log(`- bun.lock: package name`);
89
+ }
90
+ updateFrameworkFiles();
59
91
  updateAppText();
60
92
  updatePackageManagerFiles();
61
93
  writeAppReadme();
62
94
 
63
95
  if (shouldInstallDependencies) {
96
+ console.log();
97
+ section(`Installing dependencies with ${selectedPackageManager}`);
64
98
  installDependencies();
65
99
  }
66
100
 
67
- const repoSetup = await maybeSetupRepo();
101
+ await applyRepoPlan(repoPlan);
68
102
 
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()}`);
103
+ ready(appName, nextSteps());
88
104
  }
89
105
 
90
106
  function run(command, args, options = {}) {
@@ -116,8 +132,20 @@ function updatePackageJson() {
116
132
  delete packageJson.engines;
117
133
  delete packageJson.scripts.prepare;
118
134
  delete packageJson.scripts.release;
119
- packageJson.scripts.check =
120
- 'tsc -p tsconfig.json --noEmit && eslint . && prettier . --check && vite build';
135
+ if (selectedFramework === 'next') {
136
+ packageJson.scripts.dev = 'next dev';
137
+ packageJson.scripts.build = 'next build';
138
+ packageJson.scripts.preview = 'next start';
139
+ packageJson.scripts.check =
140
+ 'tsc -p tsconfig.json --noEmit && eslint . && prettier . --check && next build';
141
+ packageJson.dependencies.next = nextVersion;
142
+ packageJson.devDependencies['@next/eslint-plugin-next'] = nextVersion;
143
+ delete packageJson.devDependencies['@vitejs/plugin-react'];
144
+ delete packageJson.devDependencies.vite;
145
+ } else {
146
+ packageJson.scripts.check =
147
+ 'tsc -p tsconfig.json --noEmit && eslint . && prettier . --check && vite build';
148
+ }
121
149
  packageJson.packageManager = packageManagerDeclaration();
122
150
 
123
151
  writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 4)}\n`);
@@ -144,20 +172,23 @@ function updateBunLock() {
144
172
  }
145
173
 
146
174
  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
- );
175
+ if (selectedFramework === 'vite') {
176
+ replaceInFile(
177
+ join(targetPath, 'index.html'),
178
+ '<title>Frontend Template</title>',
179
+ {
180
+ with: `<title>${appName}</title>`,
181
+ }
182
+ );
183
+ }
184
+
185
+ writeFileSync(join(targetPath, 'src/components/App.tsx'), appComponent());
186
+ }
187
+
188
+ function updateFrameworkFiles() {
189
+ if (selectedFramework === 'next') {
190
+ writeNextAppFiles();
191
+ }
161
192
  }
162
193
 
163
194
  function updatePackageManagerFiles() {
@@ -219,7 +250,7 @@ function writeAppReadme() {
219
250
  const securityNote = securityNoteForPackageManager();
220
251
  const readme = `# ${appName}
221
252
 
222
- Created from the frontend template.
253
+ Created from the ${frameworkLabel(selectedFramework)} frontend template.
223
254
 
224
255
  ## Install
225
256
 
@@ -245,58 +276,132 @@ ${securityNote}
245
276
  writeFileSync(join(targetPath, 'README.md'), readme);
246
277
  }
247
278
 
248
- async function maybeSetupRepo() {
279
+ async function planFramework() {
280
+ if (parsedArgs.framework || !isInteractive) {
281
+ return selectedFramework;
282
+ }
283
+
284
+ const framework = await select({
285
+ message: 'Framework',
286
+ options: [
287
+ { label: 'Vite', value: 'vite' },
288
+ { label: 'Next.js', value: 'next' },
289
+ ],
290
+ initialValue: 'vite',
291
+ });
292
+ gap();
293
+
294
+ return framework;
295
+ }
296
+
297
+ async function planInstallDependencies() {
298
+ if (!shouldInstallDependencies || !isInteractive) {
299
+ return shouldInstallDependencies;
300
+ }
301
+
302
+ const shouldInstall = await confirm({
303
+ message: `Should I run "${installCommand()}" for you?`,
304
+ initialValue: true,
305
+ });
306
+ gap();
307
+
308
+ return shouldInstall;
309
+ }
310
+
311
+ async function planRepoSetup() {
249
312
  if (shouldSkipRepoSetup || !isInteractive) {
250
313
  return null;
251
314
  }
252
315
 
253
- const rl = readline.createInterface({ input, output });
316
+ const shouldCreateRepo = await confirm({
317
+ message: 'Create a git repository?',
318
+ initialValue: true,
319
+ });
320
+ gap();
254
321
 
255
- try {
256
- const shouldCreateRepo = await promptYesNo(
257
- rl,
258
- 'Create a git repository?',
259
- true
322
+ if (!shouldCreateRepo) {
323
+ return null;
324
+ }
325
+
326
+ const repoPlan = {
327
+ git: true,
328
+ github: false,
329
+ };
330
+ const hasGitHubCli = canUseGitHubCli();
331
+
332
+ if (!hasGitHubCli) {
333
+ warn(
334
+ 'GitHub CLI is unavailable or not authenticated; keeping a local repository only.'
260
335
  );
336
+ gap();
337
+ return repoPlan;
338
+ }
261
339
 
262
- if (!shouldCreateRepo) {
263
- return null;
264
- }
340
+ const shouldCreateGitHubRepo = await confirm({
341
+ message: 'Create a GitHub repository too?',
342
+ initialValue: true,
343
+ });
344
+ gap();
345
+
346
+ if (!shouldCreateGitHubRepo) {
347
+ return repoPlan;
348
+ }
265
349
 
266
- initLocalRepo();
350
+ const defaultRepoName = basename(targetPath);
351
+ const repoName = await text({
352
+ message: 'Repository name',
353
+ defaultValue: defaultRepoName,
354
+ placeholder: defaultRepoName,
355
+ validate(value) {
356
+ return value.trim() ? undefined : 'Repository name is required.';
357
+ },
358
+ });
359
+ gap();
360
+ const visibility = await select({
361
+ message: 'Visibility',
362
+ options: [
363
+ { label: 'Private', value: 'private' },
364
+ { label: 'Public', value: 'public' },
365
+ ],
366
+ initialValue: 'private',
367
+ });
368
+ gap();
369
+
370
+ return {
371
+ ...repoPlan,
372
+ github: true,
373
+ repoName,
374
+ visibility,
375
+ };
376
+ }
267
377
 
268
- if (!canUseGitHubCli()) {
269
- return 'local';
270
- }
378
+ async function applyRepoPlan(repoPlan) {
379
+ if (!repoPlan) {
380
+ return;
381
+ }
271
382
 
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
- );
383
+ console.log();
384
+ section('Initializing local git repository');
385
+ initLocalRepo();
295
386
 
296
- return 'github';
297
- } finally {
298
- rl.close();
387
+ if (!repoPlan.github) {
388
+ return;
299
389
  }
390
+
391
+ console.log();
392
+ section('Creating GitHub repository');
393
+ run(
394
+ 'gh',
395
+ [
396
+ 'repo',
397
+ 'create',
398
+ repoPlan.repoName,
399
+ `--${repoPlan.visibility}`,
400
+ '--source=.',
401
+ '--remote=origin',
402
+ ],
403
+ { cwd: targetPath }
404
+ );
300
405
  }
301
406
 
302
407
  function initLocalRepo() {
@@ -305,71 +410,29 @@ function initLocalRepo() {
305
410
  }
306
411
 
307
412
  function canUseGitHubCli() {
308
- return Boolean(
413
+ return (
309
414
  run('gh', ['auth', 'status'], {
310
415
  cwd: targetPath,
311
416
  capture: true,
312
417
  allowFailure: true,
313
- })
418
+ }) !== null
314
419
  );
315
420
  }
316
421
 
317
- async function promptYesNo(rl, label, defaultValue) {
318
- const hint = defaultValue ? 'Y/n' : 'y/N';
319
-
320
- while (true) {
321
- const answer = (await rl.question(`${label} [${hint}] `))
322
- .trim()
323
- .toLowerCase();
324
-
325
- if (!answer) {
326
- return defaultValue;
327
- }
328
-
329
- if (['y', 'yes'].includes(answer)) {
330
- return true;
331
- }
422
+ function nextSteps() {
423
+ const steps = [];
332
424
 
333
- if (['n', 'no'].includes(answer)) {
334
- return false;
335
- }
425
+ if (targetArg !== '.') {
426
+ steps.push(`cd ${targetArg}`);
336
427
  }
337
- }
338
-
339
- async function promptWithDefault(rl, label, defaultValue) {
340
- const answer = (await rl.question(`${label} (${defaultValue}): `)).trim();
341
-
342
- return answer || defaultValue;
343
- }
344
-
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('/');
351
-
352
- while (true) {
353
- const answer = (await rl.question(`${label} (${renderedChoices}): `))
354
- .trim()
355
- .toLowerCase();
356
428
 
357
- if (!answer) {
358
- const defaultChoice = choices.find((choice) => choice.default);
359
-
360
- if (defaultChoice) {
361
- return defaultChoice.value;
362
- }
363
- }
429
+ if (!shouldInstallDependencies) {
430
+ steps.push(installCommand());
431
+ }
364
432
 
365
- const matchingChoice = choices.find(
366
- (choice) => choice.label === answer || choice.value === answer
367
- );
433
+ steps.push(devCommand());
368
434
 
369
- if (matchingChoice) {
370
- return matchingChoice.value;
371
- }
372
- }
435
+ return steps;
373
436
  }
374
437
 
375
438
  function replaceInFile(filePath, searchValue, replacement) {
@@ -391,6 +454,7 @@ function toPackageName(value) {
391
454
 
392
455
  function parseCliArgs(args) {
393
456
  const parsedArgs = {
457
+ framework: null,
394
458
  noInstall: false,
395
459
  noRepo: false,
396
460
  packageManager: null,
@@ -399,6 +463,12 @@ function parseCliArgs(args) {
399
463
 
400
464
  for (const arg of args) {
401
465
  switch (arg) {
466
+ case '--vite':
467
+ setFrameworkOverride(parsedArgs, 'vite');
468
+ continue;
469
+ case '--next':
470
+ setFrameworkOverride(parsedArgs, 'next');
471
+ continue;
402
472
  case '--bun':
403
473
  setPackageManagerOverride(parsedArgs, 'bun');
404
474
  continue;
@@ -433,6 +503,14 @@ function parseCliArgs(args) {
433
503
  return parsedArgs;
434
504
  }
435
505
 
506
+ function setFrameworkOverride(parsedArgs, framework) {
507
+ if (parsedArgs.framework && parsedArgs.framework !== framework) {
508
+ fail('Pass only one of --vite or --next.');
509
+ }
510
+
511
+ parsedArgs.framework = framework;
512
+ }
513
+
436
514
  function setPackageManagerOverride(parsedArgs, packageManager) {
437
515
  if (
438
516
  parsedArgs.packageManager &&
@@ -448,12 +526,295 @@ function resolvePackageManager(parsedArgs) {
448
526
  return parsedArgs.packageManager ?? 'bun';
449
527
  }
450
528
 
529
+ function resolveFramework(parsedArgs) {
530
+ return parsedArgs.framework ?? 'vite';
531
+ }
532
+
451
533
  function readNpmBooleanFlag(name) {
452
534
  const value = process.env[`npm_config_${name}`];
453
535
 
454
536
  return value === 'true' || value === '';
455
537
  }
456
538
 
539
+ function logFrameworkFileChanges() {
540
+ if (selectedFramework === 'next') {
541
+ console.log(
542
+ `- Next app router files: src/app/layout.tsx, src/app/page.tsx`
543
+ );
544
+ console.log(`- src/app/global.css: app styles`);
545
+ console.log(`- Next config: next.config.mjs, next-env.d.ts`);
546
+ console.log(
547
+ `- Vite files removed: index.html, vite.config.mjs, src/main.tsx`
548
+ );
549
+ return;
550
+ }
551
+
552
+ console.log(`- index.html: title`);
553
+ console.log(`- src/components/App.tsx: app name`);
554
+ }
555
+
556
+ function writeNextAppFiles() {
557
+ rmSync(join(targetPath, 'index.html'), { force: true });
558
+ rmSync(join(targetPath, 'vite.config.mjs'), { force: true });
559
+ rmSync(join(targetPath, 'src/main.tsx'), { force: true });
560
+ rmSync(join(targetPath, 'src/vite-env.d.ts'), { force: true });
561
+ rmSync(join(targetPath, 'src/global.css'), { force: true });
562
+
563
+ const appPath = join(targetPath, 'src/app');
564
+ mkdirSync(appPath, { recursive: true });
565
+
566
+ writeFileSync(join(targetPath, 'next-env.d.ts'), nextEnvTypes());
567
+ writeFileSync(join(targetPath, 'next.config.mjs'), nextConfig());
568
+ writeFileSync(join(targetPath, 'eslint.config.mjs'), nextEslintConfig());
569
+ writeFileSync(join(targetPath, 'tsconfig.json'), nextTsconfig());
570
+ writeFileSync(join(appPath, 'layout.tsx'), nextLayout());
571
+ writeFileSync(join(appPath, 'page.tsx'), nextPage());
572
+ writeFileSync(join(appPath, 'global.css'), nextGlobalCss());
573
+ }
574
+
575
+ function frameworkLabel(framework) {
576
+ switch (framework) {
577
+ case 'vite':
578
+ return 'Vite';
579
+ case 'next':
580
+ return 'Next.js';
581
+ default:
582
+ fail(`Unsupported framework: ${framework}`);
583
+ }
584
+ }
585
+
586
+ function frameworkTitle(framework) {
587
+ switch (framework) {
588
+ case 'vite':
589
+ return 'Vite, React, and TypeScript.';
590
+ case 'next':
591
+ return 'Next.js, React, and TypeScript.';
592
+ default:
593
+ fail(`Unsupported framework: ${framework}`);
594
+ }
595
+ }
596
+
597
+ function appComponent() {
598
+ return `import type { JSX } from 'react';
599
+
600
+ export function App(): JSX.Element {
601
+ return (
602
+ <main className='app'>
603
+ <section className='app__content'>
604
+ <p className='app__eyebrow'>${appName}</p>
605
+ <h1 className='app__title'>${frameworkTitle(selectedFramework)}</h1>
606
+ <p className='app__description'>
607
+ A clean baseline with strict tooling, useful tokens, and no
608
+ unnecessary UI noise.
609
+ </p>
610
+ </section>
611
+ </main>
612
+ );
613
+ }
614
+ `;
615
+ }
616
+
617
+ function nextEnvTypes() {
618
+ return `/// <reference types="next" />
619
+ /// <reference types="next/image-types/global" />
620
+
621
+ // This file should not be edited.
622
+ `;
623
+ }
624
+
625
+ function nextConfig() {
626
+ return `/** @type {import("next").NextConfig} */
627
+ const nextConfig = {};
628
+
629
+ export default nextConfig;
630
+ `;
631
+ }
632
+
633
+ function nextEslintConfig() {
634
+ return `import nextPlugin from '@next/eslint-plugin-next';
635
+ import { completeConfigBase } from 'eslint-config-complete';
636
+
637
+ export default [
638
+ ...completeConfigBase,
639
+
640
+ {
641
+ ignores: ['.next/**', 'node_modules/**'],
642
+ },
643
+
644
+ {
645
+ plugins: {
646
+ '@next/next': nextPlugin,
647
+ },
648
+ rules: {
649
+ ...nextPlugin.configs.recommended.rules,
650
+ ...nextPlugin.configs['core-web-vitals'].rules,
651
+ '@stylistic/quotes': [
652
+ 'error',
653
+ 'single',
654
+ {
655
+ avoidEscape: true,
656
+ },
657
+ ],
658
+ 'import-x/no-unassigned-import': [
659
+ 'error',
660
+ {
661
+ allow: ['**/*.css'],
662
+ },
663
+ ],
664
+ },
665
+ },
666
+
667
+ {
668
+ files: ['src/app/**/*.tsx'],
669
+ rules: {
670
+ 'import-x/no-default-export': 'off',
671
+ },
672
+ },
673
+ ];
674
+ `;
675
+ }
676
+
677
+ function nextTsconfig() {
678
+ return `{
679
+ "compilerOptions": {
680
+ "target": "ES2022",
681
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
682
+ "allowJs": false,
683
+ "skipLibCheck": true,
684
+ "strict": true,
685
+ "noEmit": true,
686
+ "esModuleInterop": true,
687
+ "module": "ESNext",
688
+ "moduleResolution": "Bundler",
689
+ "resolveJsonModule": true,
690
+ "isolatedModules": true,
691
+ "jsx": "react-jsx",
692
+ "incremental": true,
693
+ "noUnusedLocals": true,
694
+ "noUnusedParameters": true,
695
+ "noFallthroughCasesInSwitch": true,
696
+ "plugins": [
697
+ {
698
+ "name": "next"
699
+ }
700
+ ],
701
+ "paths": {
702
+ "@/*": ["./src/*"]
703
+ }
704
+ },
705
+ "include": [
706
+ "next-env.d.ts",
707
+ "src/**/*.ts",
708
+ "src/**/*.tsx",
709
+ ".next/dev/types/**/*.ts",
710
+ ".next/types/**/*.ts"
711
+ ],
712
+ "exclude": ["node_modules"]
713
+ }
714
+ `;
715
+ }
716
+
717
+ function nextLayout() {
718
+ return `import type { JSX, ReactNode } from 'react';
719
+ import type { Metadata } from 'next';
720
+
721
+ import './global.css';
722
+
723
+ export const metadata: Metadata = {
724
+ title: '${appName}',
725
+ description: 'Created from create-hsi-app.',
726
+ };
727
+
728
+ interface RootLayoutProps {
729
+ readonly children: ReactNode;
730
+ }
731
+
732
+ export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
733
+ return (
734
+ <html lang='en'>
735
+ <body>{children}</body>
736
+ </html>
737
+ );
738
+ }
739
+ `;
740
+ }
741
+
742
+ function nextPage() {
743
+ return `import type { JSX } from 'react';
744
+
745
+ import { App } from '@/components/App';
746
+
747
+ export default function HomePage(): JSX.Element {
748
+ return <App />;
749
+ }
750
+ `;
751
+ }
752
+
753
+ function nextGlobalCss() {
754
+ return `@import '../constants/color.css';
755
+ @import '../constants/font.css';
756
+
757
+ * {
758
+ margin: 0;
759
+ padding: 0;
760
+ box-sizing: border-box;
761
+ }
762
+
763
+ html {
764
+ background-color: var(--clr-bg);
765
+ color: var(--clr-text);
766
+ }
767
+
768
+ body {
769
+ min-width: 320px;
770
+ min-height: 100vh;
771
+ font: var(--font-body-md);
772
+ line-height: 1.5;
773
+ background-color: var(--clr-bg);
774
+ }
775
+
776
+ a {
777
+ color: inherit;
778
+ }
779
+
780
+ :focus-visible {
781
+ outline: calc(var(--space-16) / 8) solid var(--clr-accent);
782
+ outline-offset: calc(var(--space-16) / 8);
783
+ }
784
+
785
+ .app {
786
+ min-height: 100vh;
787
+ display: grid;
788
+ place-items: center;
789
+ padding: var(--space-32) var(--space-24);
790
+ }
791
+
792
+ .app__content {
793
+ display: grid;
794
+ justify-items: center;
795
+ gap: var(--space-16);
796
+ width: fit-content;
797
+ max-width: 100%;
798
+ text-align: center;
799
+ }
800
+
801
+ .app__eyebrow {
802
+ color: var(--clr-text-muted);
803
+ font: var(--font-label);
804
+ letter-spacing: 0.08em;
805
+ text-transform: uppercase;
806
+ }
807
+
808
+ .app__title {
809
+ font: var(--font-display);
810
+ }
811
+
812
+ .app__description {
813
+ color: var(--clr-text-muted);
814
+ }
815
+ `;
816
+ }
817
+
457
818
  function packageManagerDeclaration() {
458
819
  switch (selectedPackageManager) {
459
820
  case 'bun':
@@ -525,7 +886,17 @@ function securityNoteForPackageManager() {
525
886
  }
526
887
  }
527
888
 
528
- function fail(message) {
529
- console.error(`create-hsi-app: ${message}`);
530
- process.exit(1);
889
+ function packageManagerConfigFile() {
890
+ switch (selectedPackageManager) {
891
+ case 'bun':
892
+ return 'bunfig.toml';
893
+ case 'npm':
894
+ return '.npmrc';
895
+ case 'pnpm':
896
+ return 'pnpm-workspace.yaml';
897
+ case 'yarn':
898
+ return '.yarnrc.yml';
899
+ default:
900
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
901
+ }
531
902
  }
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.0",
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
  },