create-hsi-app 0.1.5 → 0.5.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 ADDED
@@ -0,0 +1,32 @@
1
+ # create-hsi-app
2
+
3
+ Scaffold a new Vite + React + TypeScript app from the frontend template.
4
+
5
+ ## Usage
6
+
7
+ ### npm
8
+
9
+ ```bash
10
+ npm create hsi-app@latest
11
+ ```
12
+
13
+ ### yarn
14
+
15
+ ```bash
16
+ yarn create hsi-app
17
+ ```
18
+
19
+ ### pnpm
20
+
21
+ ```bash
22
+ pnpm create hsi-app@latest
23
+ ```
24
+
25
+ ### bun
26
+
27
+ ```bash
28
+ bun create hsi-app@latest
29
+ ```
30
+
31
+ 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)
@@ -8,50 +8,96 @@ import {
8
8
  writeFileSync,
9
9
  } from 'node:fs';
10
10
  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';
11
13
 
12
14
  const templateRepo = 'https://github.com/Hsiii/frontend-template.git';
13
- const templateTag = 'v0.1.5';
15
+ const templateTag = 'v0.5.0';
14
16
  const defaultAppName = 'my-app';
15
- const targetArg = process.argv[2] ?? defaultAppName;
17
+ const packageManagers = ['bun', 'npm', 'pnpm', 'yarn'];
18
+ const rawArgs = process.argv.slice(2);
19
+ const selectedPackageManager = resolvePackageManager(rawArgs);
20
+ const shouldInstallDependencies = !rawArgs.includes('--noInstall');
21
+ const shouldSkipRepoSetup = rawArgs.includes('--noRepo');
22
+ const isInteractive = input.isTTY && output.isTTY;
23
+ const targetArg = rawArgs.find((arg) => !arg.startsWith('--')) ?? '.';
16
24
  const targetPath = resolve(targetArg);
17
25
  const appName = toPackageName(basename(targetPath));
18
26
 
19
- if (existsSync(targetPath) && readdirSync(targetPath).length > 0) {
20
- fail(`Target directory is not empty: ${targetPath}`);
21
- }
22
-
23
- run('git', [
24
- '-c',
25
- 'advice.detachedHead=false',
26
- 'clone',
27
- '--branch',
28
- templateTag,
29
- '--depth',
30
- '1',
31
- templateRepo,
32
- targetPath,
33
- ]);
34
-
35
- rmSync(join(targetPath, '.git'), { force: true, recursive: true });
36
- rmSync(join(targetPath, '.github'), { force: true, recursive: true });
37
- rmSync(join(targetPath, 'packages'), { force: true, recursive: true });
38
-
39
- updatePackageJson();
40
- updateBunLock();
41
- updateAppText();
42
- writeAppReadme();
43
-
44
- console.log(`\nCreated ${appName} in ${targetPath}\n`);
45
- console.log('Next steps:');
46
- console.log(` cd ${targetArg}`);
47
- console.log(' bun i');
48
- console.log(' bun run dev');
49
-
50
- function run(command, args) {
27
+ main().catch((error) => {
28
+ fail(error.message);
29
+ });
30
+
31
+ async function main() {
32
+ if (existsSync(targetPath) && readdirSync(targetPath).length > 0) {
33
+ fail(`Target directory is not empty: ${targetPath}`);
34
+ }
35
+
36
+ run('git', [
37
+ '-c',
38
+ 'advice.detachedHead=false',
39
+ 'clone',
40
+ '--branch',
41
+ templateTag,
42
+ '--depth',
43
+ '1',
44
+ templateRepo,
45
+ targetPath,
46
+ ]);
47
+
48
+ rmSync(join(targetPath, '.git'), { force: true, recursive: true });
49
+ rmSync(join(targetPath, '.github'), { force: true, recursive: true });
50
+ rmSync(join(targetPath, 'docs'), { force: true, recursive: true });
51
+ rmSync(join(targetPath, 'packages'), { force: true, recursive: true });
52
+ rmSync(join(targetPath, 'scripts'), { force: true, recursive: true });
53
+
54
+ updatePackageJson();
55
+ updateBunLock();
56
+ updateAppText();
57
+ updatePackageManagerFiles();
58
+ writeAppReadme();
59
+
60
+ if (shouldInstallDependencies) {
61
+ installDependencies();
62
+ }
63
+
64
+ const repoSetup = await maybeSetupRepo();
65
+
66
+ console.log(`\nCreated ${appName} in ${targetPath}\n`);
67
+ if (repoSetup === 'github') {
68
+ console.log(
69
+ 'Created a local git repository and configured GitHub origin.'
70
+ );
71
+ } else if (repoSetup === 'local') {
72
+ console.log('Initialized a local git repository.');
73
+ }
74
+ if (shouldInstallDependencies) {
75
+ console.log(`Installed dependencies with ${selectedPackageManager}.`);
76
+ }
77
+ console.log('\nNext steps:');
78
+ if (targetArg !== '.') {
79
+ console.log(` cd ${targetArg}`);
80
+ }
81
+ if (!shouldInstallDependencies) {
82
+ console.log(` ${installCommand()}`);
83
+ }
84
+ console.log(` ${devCommand()}`);
85
+ }
86
+
87
+ function run(command, args, options = {}) {
51
88
  try {
52
- execFileSync(command, args, { stdio: 'inherit' });
53
- } catch {
54
- fail(`Failed to run: ${command} ${args.join(' ')}`);
89
+ return execFileSync(command, args, {
90
+ cwd: options.cwd,
91
+ encoding: options.capture ? 'utf8' : undefined,
92
+ stdio: options.capture ? 'pipe' : 'inherit',
93
+ });
94
+ } catch (error) {
95
+ if (options.allowFailure) {
96
+ return null;
97
+ }
98
+
99
+ const details = error.stderr?.toString().trim() || error.message;
100
+ fail(`Failed to run: ${command} ${args.join(' ')}\n${details}`);
55
101
  }
56
102
  }
57
103
 
@@ -63,9 +109,12 @@ function updatePackageJson() {
63
109
  packageJson.version = '0.1.0';
64
110
  delete packageJson.repository;
65
111
  delete packageJson.publishConfig;
66
- delete packageJson.scripts['check:create'];
112
+ delete packageJson.packageManager;
113
+ delete packageJson.engines;
114
+ delete packageJson.scripts.release;
67
115
  packageJson.scripts.check =
68
- 'bun run typecheck && bun run lint && bun run format:check && bun run build';
116
+ 'tsc -p tsconfig.json --noEmit && eslint . && prettier . --check && vite build';
117
+ packageJson.packageManager = packageManagerDeclaration();
69
118
 
70
119
  writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 4)}\n`);
71
120
  }
@@ -77,6 +126,11 @@ function updateBunLock() {
77
126
  return;
78
127
  }
79
128
 
129
+ if (selectedPackageManager !== 'bun') {
130
+ rmSync(lockPath, { force: true });
131
+ return;
132
+ }
133
+
80
134
  const lock = readFileSync(lockPath, 'utf8').replace(
81
135
  '"name": "frontend-template"',
82
136
  `"name": "${appName}"`
@@ -102,7 +156,63 @@ function updateAppText() {
102
156
  );
103
157
  }
104
158
 
159
+ function updatePackageManagerFiles() {
160
+ rmSync(join(targetPath, 'bunfig.toml'), { force: true });
161
+ rmSync(join(targetPath, '.npmrc'), { force: true });
162
+ rmSync(join(targetPath, 'pnpm-workspace.yaml'), { force: true });
163
+ rmSync(join(targetPath, '.yarnrc.yml'), { force: true });
164
+
165
+ switch (selectedPackageManager) {
166
+ case 'bun':
167
+ writeFileSync(
168
+ join(targetPath, 'bunfig.toml'),
169
+ '[install]\nminimumReleaseAge = 604800\n'
170
+ );
171
+ return;
172
+ case 'npm':
173
+ writeFileSync(join(targetPath, '.npmrc'), 'min-release-age=7\n');
174
+ return;
175
+ case 'pnpm':
176
+ writeFileSync(
177
+ join(targetPath, 'pnpm-workspace.yaml'),
178
+ 'minimumReleaseAge: 10080\n'
179
+ );
180
+ return;
181
+ case 'yarn':
182
+ writeFileSync(
183
+ join(targetPath, '.yarnrc.yml'),
184
+ 'npmMinimalAgeGate: 7d\n'
185
+ );
186
+ return;
187
+ default:
188
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
189
+ }
190
+ }
191
+
192
+ function installDependencies() {
193
+ switch (selectedPackageManager) {
194
+ case 'bun':
195
+ run('bun', ['install'], { cwd: targetPath });
196
+ return;
197
+ case 'npm':
198
+ run('npm', ['install'], { cwd: targetPath });
199
+ return;
200
+ case 'pnpm':
201
+ run('pnpm', ['install'], { cwd: targetPath });
202
+ return;
203
+ case 'yarn':
204
+ run('yarn', ['install'], { cwd: targetPath });
205
+ return;
206
+ default:
207
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
208
+ }
209
+ }
210
+
105
211
  function writeAppReadme() {
212
+ const installLine = installCommand();
213
+ const devLine = devCommand();
214
+ const checkLine = checkCommand();
215
+ const securityNote = securityNoteForPackageManager();
106
216
  const readme = `# ${appName}
107
217
 
108
218
  Created from the frontend template.
@@ -110,25 +220,154 @@ Created from the frontend template.
110
220
  ## Install
111
221
 
112
222
  \`\`\`bash
113
- bun i
223
+ ${installLine}
114
224
  \`\`\`
115
225
 
116
226
  ## Develop
117
227
 
118
228
  \`\`\`bash
119
- bun run dev
229
+ ${devLine}
120
230
  \`\`\`
121
231
 
122
232
  ## Check
123
233
 
124
234
  \`\`\`bash
125
- bun run check
235
+ ${checkLine}
126
236
  \`\`\`
237
+
238
+ ${securityNote}
127
239
  `;
128
240
 
129
241
  writeFileSync(join(targetPath, 'README.md'), readme);
130
242
  }
131
243
 
244
+ async function maybeSetupRepo() {
245
+ if (shouldSkipRepoSetup || !isInteractive) {
246
+ return null;
247
+ }
248
+
249
+ const rl = readline.createInterface({ input, output });
250
+
251
+ try {
252
+ const shouldCreateRepo = await promptYesNo(
253
+ rl,
254
+ 'Create a git repository?',
255
+ true
256
+ );
257
+
258
+ if (!shouldCreateRepo) {
259
+ return null;
260
+ }
261
+
262
+ initLocalRepo();
263
+
264
+ if (!canUseGitHubCli()) {
265
+ return 'local';
266
+ }
267
+
268
+ const defaultRepoName = basename(targetPath);
269
+ const repoName = await promptWithDefault(
270
+ rl,
271
+ 'Repository name',
272
+ defaultRepoName
273
+ );
274
+ const visibility = await promptChoice(rl, 'Visibility', [
275
+ { label: 'private', value: 'private', default: true },
276
+ { label: 'public', value: 'public' },
277
+ ]);
278
+
279
+ run(
280
+ 'gh',
281
+ [
282
+ 'repo',
283
+ 'create',
284
+ repoName,
285
+ `--${visibility}`,
286
+ '--source=.',
287
+ '--remote=origin',
288
+ ],
289
+ { cwd: targetPath }
290
+ );
291
+
292
+ return 'github';
293
+ } finally {
294
+ rl.close();
295
+ }
296
+ }
297
+
298
+ function initLocalRepo() {
299
+ run('git', ['init', '-b', 'main'], { cwd: targetPath });
300
+ run('git', ['config', 'core.hooksPath', '.githooks'], { cwd: targetPath });
301
+ }
302
+
303
+ function canUseGitHubCli() {
304
+ return Boolean(
305
+ run('gh', ['auth', 'status'], {
306
+ cwd: targetPath,
307
+ capture: true,
308
+ allowFailure: true,
309
+ })
310
+ );
311
+ }
312
+
313
+ async function promptYesNo(rl, label, defaultValue) {
314
+ const hint = defaultValue ? 'Y/n' : 'y/N';
315
+
316
+ while (true) {
317
+ const answer = (await rl.question(`${label} [${hint}] `))
318
+ .trim()
319
+ .toLowerCase();
320
+
321
+ if (!answer) {
322
+ return defaultValue;
323
+ }
324
+
325
+ if (['y', 'yes'].includes(answer)) {
326
+ return true;
327
+ }
328
+
329
+ if (['n', 'no'].includes(answer)) {
330
+ return false;
331
+ }
332
+ }
333
+ }
334
+
335
+ async function promptWithDefault(rl, label, defaultValue) {
336
+ const answer = (await rl.question(`${label} (${defaultValue}): `)).trim();
337
+
338
+ return answer || defaultValue;
339
+ }
340
+
341
+ async function promptChoice(rl, label, choices) {
342
+ const renderedChoices = choices
343
+ .map((choice) =>
344
+ choice.default ? `${choice.label.toUpperCase()}` : choice.label
345
+ )
346
+ .join('/');
347
+
348
+ while (true) {
349
+ const answer = (await rl.question(`${label} (${renderedChoices}): `))
350
+ .trim()
351
+ .toLowerCase();
352
+
353
+ if (!answer) {
354
+ const defaultChoice = choices.find((choice) => choice.default);
355
+
356
+ if (defaultChoice) {
357
+ return defaultChoice.value;
358
+ }
359
+ }
360
+
361
+ const matchingChoice = choices.find(
362
+ (choice) => choice.label === answer || choice.value === answer
363
+ );
364
+
365
+ if (matchingChoice) {
366
+ return matchingChoice.value;
367
+ }
368
+ }
369
+ }
370
+
132
371
  function replaceInFile(filePath, searchValue, replacement) {
133
372
  const source = readFileSync(filePath, 'utf8');
134
373
  writeFileSync(filePath, source.replace(searchValue, replacement.with));
@@ -146,6 +385,101 @@ function toPackageName(value) {
146
385
  return name || defaultAppName;
147
386
  }
148
387
 
388
+ function resolvePackageManager(args) {
389
+ const selectedFlags = args.filter((arg) =>
390
+ ['--bun', '--npm', '--pnpm', '--yarn'].includes(arg)
391
+ );
392
+
393
+ if (selectedFlags.length > 1) {
394
+ fail('Pass only one of --bun, --npm, --pnpm, or --yarn.');
395
+ }
396
+
397
+ switch (selectedFlags[0]) {
398
+ case '--npm':
399
+ return 'npm';
400
+ case '--pnpm':
401
+ return 'pnpm';
402
+ case '--yarn':
403
+ return 'yarn';
404
+ case '--bun':
405
+ case undefined:
406
+ return 'bun';
407
+ default:
408
+ fail(`Unsupported package manager flag: ${selectedFlags[0]}`);
409
+ }
410
+ }
411
+
412
+ function packageManagerDeclaration() {
413
+ switch (selectedPackageManager) {
414
+ case 'bun':
415
+ return 'bun@1.3.9';
416
+ case 'npm':
417
+ return 'npm@11';
418
+ case 'pnpm':
419
+ return 'pnpm@10';
420
+ case 'yarn':
421
+ return 'yarn@4';
422
+ default:
423
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
424
+ }
425
+ }
426
+
427
+ function installCommand() {
428
+ switch (selectedPackageManager) {
429
+ case 'bun':
430
+ return 'bun install';
431
+ case 'npm':
432
+ return 'npm install';
433
+ case 'pnpm':
434
+ return 'pnpm install';
435
+ case 'yarn':
436
+ return 'yarn install';
437
+ default:
438
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
439
+ }
440
+ }
441
+
442
+ function devCommand() {
443
+ switch (selectedPackageManager) {
444
+ case 'yarn':
445
+ return 'yarn dev';
446
+ case 'bun':
447
+ case 'npm':
448
+ case 'pnpm':
449
+ return `${selectedPackageManager} run dev`;
450
+ default:
451
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
452
+ }
453
+ }
454
+
455
+ function checkCommand() {
456
+ switch (selectedPackageManager) {
457
+ case 'yarn':
458
+ return 'yarn check';
459
+ case 'bun':
460
+ case 'npm':
461
+ case 'pnpm':
462
+ return `${selectedPackageManager} run check`;
463
+ default:
464
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
465
+ }
466
+ }
467
+
468
+ function securityNoteForPackageManager() {
469
+ switch (selectedPackageManager) {
470
+ case 'bun':
471
+ return 'This project includes `bunfig.toml` with `minimumReleaseAge = 604800`.';
472
+ case 'npm':
473
+ return 'This project includes `.npmrc` with `min-release-age=7`.';
474
+ case 'pnpm':
475
+ return 'This project includes `pnpm-workspace.yaml` with `minimumReleaseAge: 10080`.';
476
+ case 'yarn':
477
+ return 'This project includes `.yarnrc.yml` with `npmMinimalAgeGate: 7d`.';
478
+ default:
479
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
480
+ }
481
+ }
482
+
149
483
  function fail(message) {
150
484
  console.error(`create-hsi-app: ${message}`);
151
485
  process.exit(1);
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "create-hsi-app",
3
- "version": "0.1.5",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Create a new app from the frontend template.",
6
6
  "bin": {
7
7
  "create-hsi-app": "bin/create-hsi-app.mjs"
8
8
  },
9
9
  "files": [
10
- "bin"
10
+ "bin",
11
+ "README.md"
11
12
  ],
12
13
  "engines": {
13
14
  "node": ">=18"