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