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 +3 -2
- package/bin/create-hsi-app.mjs +509 -138
- 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,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
|
-
|
|
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
32
|
const parsedArgs = parseCliArgs(rawArgs);
|
|
20
33
|
const selectedPackageManager = resolvePackageManager(parsedArgs);
|
|
21
|
-
|
|
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 =
|
|
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
|
-
|
|
101
|
+
await applyRepoPlan(repoPlan);
|
|
68
102
|
|
|
69
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
|
316
|
+
const shouldCreateRepo = await confirm({
|
|
317
|
+
message: 'Create a git repository?',
|
|
318
|
+
initialValue: true,
|
|
319
|
+
});
|
|
320
|
+
gap();
|
|
254
321
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
378
|
+
async function applyRepoPlan(repoPlan) {
|
|
379
|
+
if (!repoPlan) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
271
382
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
|
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
|
-
|
|
318
|
-
const
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (defaultChoice) {
|
|
361
|
-
return defaultChoice.value;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
429
|
+
if (!shouldInstallDependencies) {
|
|
430
|
+
steps.push(installCommand());
|
|
431
|
+
}
|
|
364
432
|
|
|
365
|
-
|
|
366
|
-
(choice) => choice.label === answer || choice.value === answer
|
|
367
|
-
);
|
|
433
|
+
steps.push(devCommand());
|
|
368
434
|
|
|
369
|
-
|
|
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
|
|
529
|
-
|
|
530
|
-
|
|
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.
|
|
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
|
},
|