create-hsi-app 0.5.0 → 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 +3 -2
- package/bin/create-hsi-app.mjs +573 -157
- package/bin/ui.mjs +70 -0
- package/package.json +5 -1
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
|
|
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/
|
|
33
|
+
[docs/CLI.md](https://github.com/Hsiii/frontend-template/blob/main/docs/CLI.md)
|
package/bin/create-hsi-app.mjs
CHANGED
|
@@ -2,25 +2,42 @@
|
|
|
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
|
-
|
|
12
|
-
import
|
|
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.
|
|
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
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
const parsedArgs = parseCliArgs(rawArgs);
|
|
33
|
+
const selectedPackageManager = resolvePackageManager(parsedArgs);
|
|
34
|
+
let selectedFramework = resolveFramework(parsedArgs);
|
|
35
|
+
let shouldInstallDependencies = !(
|
|
36
|
+
parsedArgs.noInstall || readNpmBooleanFlag('noinstall')
|
|
37
|
+
);
|
|
38
|
+
const shouldSkipRepoSetup = parsedArgs.noRepo || readNpmBooleanFlag('norepo');
|
|
39
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
40
|
+
const targetArg = parsedArgs.targetArg ?? '.';
|
|
24
41
|
const targetPath = resolve(targetArg);
|
|
25
42
|
const appName = toPackageName(basename(targetPath));
|
|
26
43
|
|
|
@@ -33,6 +50,13 @@ async function main() {
|
|
|
33
50
|
fail(`Target directory is not empty: ${targetPath}`);
|
|
34
51
|
}
|
|
35
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`);
|
|
36
60
|
run('git', [
|
|
37
61
|
'-c',
|
|
38
62
|
'advice.detachedHead=false',
|
|
@@ -53,35 +77,30 @@ async function main() {
|
|
|
53
77
|
|
|
54
78
|
updatePackageJson();
|
|
55
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();
|
|
56
91
|
updateAppText();
|
|
57
92
|
updatePackageManagerFiles();
|
|
58
93
|
writeAppReadme();
|
|
59
94
|
|
|
60
95
|
if (shouldInstallDependencies) {
|
|
96
|
+
console.log();
|
|
97
|
+
section(`Installing dependencies with ${selectedPackageManager}`);
|
|
61
98
|
installDependencies();
|
|
62
99
|
}
|
|
63
100
|
|
|
64
|
-
|
|
101
|
+
await applyRepoPlan(repoPlan);
|
|
65
102
|
|
|
66
|
-
|
|
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()}`);
|
|
103
|
+
ready(appName, nextSteps());
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
function run(command, args, options = {}) {
|
|
@@ -111,9 +130,22 @@ function updatePackageJson() {
|
|
|
111
130
|
delete packageJson.publishConfig;
|
|
112
131
|
delete packageJson.packageManager;
|
|
113
132
|
delete packageJson.engines;
|
|
133
|
+
delete packageJson.scripts.prepare;
|
|
114
134
|
delete packageJson.scripts.release;
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
149
|
packageJson.packageManager = packageManagerDeclaration();
|
|
118
150
|
|
|
119
151
|
writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 4)}\n`);
|
|
@@ -140,20 +172,23 @@ function updateBunLock() {
|
|
|
140
172
|
}
|
|
141
173
|
|
|
142
174
|
function updateAppText() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|
|
157
192
|
}
|
|
158
193
|
|
|
159
194
|
function updatePackageManagerFiles() {
|
|
@@ -215,7 +250,7 @@ function writeAppReadme() {
|
|
|
215
250
|
const securityNote = securityNoteForPackageManager();
|
|
216
251
|
const readme = `# ${appName}
|
|
217
252
|
|
|
218
|
-
Created from the frontend template.
|
|
253
|
+
Created from the ${frameworkLabel(selectedFramework)} frontend template.
|
|
219
254
|
|
|
220
255
|
## Install
|
|
221
256
|
|
|
@@ -241,58 +276,132 @@ ${securityNote}
|
|
|
241
276
|
writeFileSync(join(targetPath, 'README.md'), readme);
|
|
242
277
|
}
|
|
243
278
|
|
|
244
|
-
async function
|
|
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() {
|
|
245
312
|
if (shouldSkipRepoSetup || !isInteractive) {
|
|
246
313
|
return null;
|
|
247
314
|
}
|
|
248
315
|
|
|
249
|
-
const
|
|
316
|
+
const shouldCreateRepo = await confirm({
|
|
317
|
+
message: 'Create a git repository?',
|
|
318
|
+
initialValue: true,
|
|
319
|
+
});
|
|
320
|
+
gap();
|
|
250
321
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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.'
|
|
256
335
|
);
|
|
336
|
+
gap();
|
|
337
|
+
return repoPlan;
|
|
338
|
+
}
|
|
257
339
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
340
|
+
const shouldCreateGitHubRepo = await confirm({
|
|
341
|
+
message: 'Create a GitHub repository too?',
|
|
342
|
+
initialValue: true,
|
|
343
|
+
});
|
|
344
|
+
gap();
|
|
261
345
|
|
|
262
|
-
|
|
346
|
+
if (!shouldCreateGitHubRepo) {
|
|
347
|
+
return repoPlan;
|
|
348
|
+
}
|
|
263
349
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
);
|
|
378
|
+
async function applyRepoPlan(repoPlan) {
|
|
379
|
+
if (!repoPlan) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
console.log();
|
|
384
|
+
section('Initializing local git repository');
|
|
385
|
+
initLocalRepo();
|
|
291
386
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
rl.close();
|
|
387
|
+
if (!repoPlan.github) {
|
|
388
|
+
return;
|
|
295
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
|
+
);
|
|
296
405
|
}
|
|
297
406
|
|
|
298
407
|
function initLocalRepo() {
|
|
@@ -301,71 +410,29 @@ function initLocalRepo() {
|
|
|
301
410
|
}
|
|
302
411
|
|
|
303
412
|
function canUseGitHubCli() {
|
|
304
|
-
return
|
|
413
|
+
return (
|
|
305
414
|
run('gh', ['auth', 'status'], {
|
|
306
415
|
cwd: targetPath,
|
|
307
416
|
capture: true,
|
|
308
417
|
allowFailure: true,
|
|
309
|
-
})
|
|
418
|
+
}) !== null
|
|
310
419
|
);
|
|
311
420
|
}
|
|
312
421
|
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
while (true) {
|
|
317
|
-
const answer = (await rl.question(`${label} [${hint}] `))
|
|
318
|
-
.trim()
|
|
319
|
-
.toLowerCase();
|
|
320
|
-
|
|
321
|
-
if (!answer) {
|
|
322
|
-
return defaultValue;
|
|
323
|
-
}
|
|
422
|
+
function nextSteps() {
|
|
423
|
+
const steps = [];
|
|
324
424
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (['n', 'no'].includes(answer)) {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
425
|
+
if (targetArg !== '.') {
|
|
426
|
+
steps.push(`cd ${targetArg}`);
|
|
332
427
|
}
|
|
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
428
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
429
|
+
if (!shouldInstallDependencies) {
|
|
430
|
+
steps.push(installCommand());
|
|
431
|
+
}
|
|
360
432
|
|
|
361
|
-
|
|
362
|
-
(choice) => choice.label === answer || choice.value === answer
|
|
363
|
-
);
|
|
433
|
+
steps.push(devCommand());
|
|
364
434
|
|
|
365
|
-
|
|
366
|
-
return matchingChoice.value;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
435
|
+
return steps;
|
|
369
436
|
}
|
|
370
437
|
|
|
371
438
|
function replaceInFile(filePath, searchValue, replacement) {
|
|
@@ -385,30 +452,369 @@ function toPackageName(value) {
|
|
|
385
452
|
return name || defaultAppName;
|
|
386
453
|
}
|
|
387
454
|
|
|
388
|
-
function
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
455
|
+
function parseCliArgs(args) {
|
|
456
|
+
const parsedArgs = {
|
|
457
|
+
framework: null,
|
|
458
|
+
noInstall: false,
|
|
459
|
+
noRepo: false,
|
|
460
|
+
packageManager: null,
|
|
461
|
+
targetArg: null,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
for (const arg of args) {
|
|
465
|
+
switch (arg) {
|
|
466
|
+
case '--vite':
|
|
467
|
+
setFrameworkOverride(parsedArgs, 'vite');
|
|
468
|
+
continue;
|
|
469
|
+
case '--next':
|
|
470
|
+
setFrameworkOverride(parsedArgs, 'next');
|
|
471
|
+
continue;
|
|
472
|
+
case '--bun':
|
|
473
|
+
setPackageManagerOverride(parsedArgs, 'bun');
|
|
474
|
+
continue;
|
|
475
|
+
case '--npm':
|
|
476
|
+
setPackageManagerOverride(parsedArgs, 'npm');
|
|
477
|
+
continue;
|
|
478
|
+
case '--pnpm':
|
|
479
|
+
setPackageManagerOverride(parsedArgs, 'pnpm');
|
|
480
|
+
continue;
|
|
481
|
+
case '--yarn':
|
|
482
|
+
setPackageManagerOverride(parsedArgs, 'yarn');
|
|
483
|
+
continue;
|
|
484
|
+
case '--noInstall':
|
|
485
|
+
parsedArgs.noInstall = true;
|
|
486
|
+
continue;
|
|
487
|
+
case '--noRepo':
|
|
488
|
+
parsedArgs.noRepo = true;
|
|
489
|
+
continue;
|
|
490
|
+
default:
|
|
491
|
+
if (arg.startsWith('--')) {
|
|
492
|
+
fail(`Unsupported option: ${arg}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (parsedArgs.targetArg) {
|
|
496
|
+
fail(`Unexpected argument: ${arg}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
parsedArgs.targetArg = arg;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return parsedArgs;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function setFrameworkOverride(parsedArgs, framework) {
|
|
507
|
+
if (parsedArgs.framework && parsedArgs.framework !== framework) {
|
|
508
|
+
fail('Pass only one of --vite or --next.');
|
|
509
|
+
}
|
|
392
510
|
|
|
393
|
-
|
|
511
|
+
parsedArgs.framework = framework;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function setPackageManagerOverride(parsedArgs, packageManager) {
|
|
515
|
+
if (
|
|
516
|
+
parsedArgs.packageManager &&
|
|
517
|
+
parsedArgs.packageManager !== packageManager
|
|
518
|
+
) {
|
|
394
519
|
fail('Pass only one of --bun, --npm, --pnpm, or --yarn.');
|
|
395
520
|
}
|
|
396
521
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
522
|
+
parsedArgs.packageManager = packageManager;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function resolvePackageManager(parsedArgs) {
|
|
526
|
+
return parsedArgs.packageManager ?? 'bun';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function resolveFramework(parsedArgs) {
|
|
530
|
+
return parsedArgs.framework ?? 'vite';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function readNpmBooleanFlag(name) {
|
|
534
|
+
const value = process.env[`npm_config_${name}`];
|
|
535
|
+
|
|
536
|
+
return value === 'true' || value === '';
|
|
537
|
+
}
|
|
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.';
|
|
407
592
|
default:
|
|
408
|
-
fail(`Unsupported
|
|
593
|
+
fail(`Unsupported framework: ${framework}`);
|
|
409
594
|
}
|
|
410
595
|
}
|
|
411
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
|
+
|
|
412
818
|
function packageManagerDeclaration() {
|
|
413
819
|
switch (selectedPackageManager) {
|
|
414
820
|
case 'bun':
|
|
@@ -480,7 +886,17 @@ function securityNoteForPackageManager() {
|
|
|
480
886
|
}
|
|
481
887
|
}
|
|
482
888
|
|
|
483
|
-
function
|
|
484
|
-
|
|
485
|
-
|
|
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
|
+
}
|
|
486
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.
|
|
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
|
},
|