toiljs 0.0.11 → 0.0.14
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 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/configure.js +10 -4
- package/build/cli/create.js +58 -30
- package/build/cli/diagnostics.d.ts +55 -0
- package/build/cli/diagnostics.js +333 -0
- package/build/cli/doctor.d.ts +6 -0
- package/build/cli/doctor.js +249 -0
- package/build/cli/index.js +26 -0
- package/build/cli/proc.d.ts +5 -0
- package/build/cli/proc.js +20 -0
- package/build/cli/ui.d.ts +1 -0
- package/build/cli/ui.js +1 -0
- package/build/cli/update.d.ts +7 -0
- package/build/cli/update.js +117 -0
- package/build/cli/updates.d.ts +10 -0
- package/build/cli/updates.js +45 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/head/metadata.js +3 -1
- package/build/client/index.d.ts +5 -1
- package/build/client/index.js +2 -0
- package/build/client/navigation/navigation.js +1 -1
- package/build/client/routing/Router.js +2 -2
- package/build/client/search/search.d.ts +26 -0
- package/build/client/search/search.js +101 -0
- package/build/client/search/use-page-search.d.ts +8 -0
- package/build/client/search/use-page-search.js +21 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +33 -24
- package/build/compiler/index.d.ts +2 -0
- package/build/compiler/index.js +1 -0
- package/build/compiler/pages.d.ts +8 -0
- package/build/compiler/pages.js +37 -0
- package/build/compiler/plugin.js +3 -1
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +11 -5
- package/build/compiler/seo.js +10 -3
- package/build/io/.tsbuildinfo +1 -1
- package/examples/basic/client/components/Header.tsx +43 -41
- package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
- package/examples/basic/client/public/index.html +18 -16
- package/examples/basic/client/routes/(legal)/privacy.tsx +18 -19
- package/examples/basic/client/routes/(legal)/terms.tsx +15 -16
- package/examples/basic/client/routes/about.tsx +21 -22
- package/examples/basic/client/routes/blog/[id].tsx +26 -18
- package/examples/basic/client/routes/features/actions.tsx +67 -67
- package/examples/basic/client/routes/features/error/index.tsx +27 -27
- package/examples/basic/client/routes/features/head.tsx +38 -38
- package/examples/basic/client/routes/features/index.tsx +83 -75
- package/examples/basic/client/routes/features/realtime.tsx +34 -32
- package/examples/basic/client/routes/features/script.tsx +31 -31
- package/examples/basic/client/routes/features/seo.tsx +39 -39
- package/examples/basic/client/routes/features/template/index.tsx +20 -20
- package/examples/basic/client/routes/features/template/template.tsx +16 -18
- package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -23
- package/examples/basic/client/routes/gallery/index.tsx +42 -42
- package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -18
- package/examples/basic/client/routes/get-started.tsx +157 -84
- package/examples/basic/client/routes/index.tsx +137 -96
- package/examples/basic/client/routes/loader-demo/index.tsx +59 -52
- package/examples/basic/client/routes/search.tsx +61 -0
- package/examples/basic/client/routes/test.tsx +7 -8
- package/examples/basic/client/styles/main.css +624 -552
- package/package.json +2 -2
- package/presets/eslint.js +10 -3
- package/src/cli/configure.ts +363 -353
- package/src/cli/create.ts +563 -530
- package/src/cli/diagnostics.ts +421 -0
- package/src/cli/doctor.ts +318 -0
- package/src/cli/features.ts +166 -160
- package/src/cli/index.ts +242 -211
- package/src/cli/proc.ts +30 -0
- package/src/cli/ui.ts +111 -103
- package/src/cli/update.ts +150 -0
- package/src/cli/updates.ts +69 -0
- package/src/client/components/Image.tsx +91 -89
- package/src/client/dev/error-overlay.tsx +193 -197
- package/src/client/head/metadata.ts +94 -92
- package/src/client/index.ts +79 -64
- package/src/client/navigation/Link.tsx +94 -100
- package/src/client/navigation/navigation.ts +215 -218
- package/src/client/routing/Router.tsx +210 -193
- package/src/client/routing/hooks.ts +110 -114
- package/src/client/routing/lazy.ts +77 -81
- package/src/client/search/search.ts +189 -0
- package/src/client/search/use-page-search.ts +73 -0
- package/src/compiler/config.ts +173 -171
- package/src/compiler/fonts.ts +89 -87
- package/src/compiler/generate.ts +45 -27
- package/src/compiler/image-report.ts +88 -85
- package/src/compiler/index.ts +2 -0
- package/src/compiler/pages.ts +70 -0
- package/src/compiler/plugin.ts +51 -47
- package/src/compiler/prerender.ts +152 -130
- package/src/compiler/routes.ts +132 -131
- package/src/compiler/seo.ts +381 -356
- package/src/compiler/vite.ts +155 -145
- package/src/io/FastSet.ts +99 -96
- package/test/configure.test.ts +94 -90
- package/test/doctor.test.ts +140 -0
- package/test/dom/Image.test.tsx +73 -46
- package/test/dom/Script.test.tsx +48 -45
- package/test/dom/action.test.tsx +146 -129
- package/test/dom/error-overlay.test.tsx +1 -1
- package/test/dom/loader.test.tsx +2 -2
- package/test/dom/revalidate.test.tsx +1 -1
- package/test/dom/route-head.test.tsx +1 -2
- package/test/dom/router-loading.test.tsx +1 -1
- package/test/dom/slot.test.tsx +131 -109
- package/test/dom/view-transitions.test.tsx +53 -51
- package/test/features.test.ts +149 -142
- package/test/fonts.test.ts +28 -26
- package/test/head.test.ts +45 -35
- package/test/metadata.test.ts +42 -41
- package/test/pages.test.ts +105 -0
- package/test/prerender.test.ts +54 -46
- package/test/search.test.ts +114 -0
- package/test/seo.test.ts +30 -8
- package/test/update.test.ts +44 -0
package/src/cli/create.ts
CHANGED
|
@@ -1,530 +1,563 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `toiljs create`, an interactive project scaffolder (Clack-powered) that wires a new
|
|
3
|
-
* app to the enforced toiljs presets (tsconfig / eslint / prettier) and file-based routing.
|
|
4
|
-
* Supports a non-interactive path via flags (`--yes`, `--template`, …) for scripting/CI.
|
|
5
|
-
*/
|
|
6
|
-
import fs from 'node:fs/promises';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
multiselect,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
spinner,
|
|
20
|
-
|
|
21
|
-
} from '@clack/prompts';
|
|
22
|
-
import {
|
|
23
|
-
import pc from 'picocolors';
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
PKG_VERSION,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} from './features.js';
|
|
37
|
-
import { run } from './proc.js';
|
|
38
|
-
import { accent, dim, version } from './ui.js';
|
|
39
|
-
import { isPackageManager, isValidName, resolveProjectDir } from './validate.js';
|
|
40
|
-
|
|
41
|
-
export type Template = 'app' | 'minimal';
|
|
42
|
-
|
|
43
|
-
/** Human label for each preprocessor in the styling picker. */
|
|
44
|
-
const PREPROCESSOR_LABEL: Record<Preprocessor, string> = {
|
|
45
|
-
css: 'Plain CSS',
|
|
46
|
-
sass: 'Sass (SCSS)',
|
|
47
|
-
less: 'Less',
|
|
48
|
-
stylus: 'Stylus',
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/** Default global stylesheet contents (palette base styles), shared by every preprocessor. */
|
|
52
|
-
const DEFAULT_STYLE_CONTENT =
|
|
53
|
-
':root {\n color-scheme: dark;\n}\n\n' +
|
|
54
|
-
'body {\n margin: 0;\n background: #080d11;\n color: #f5f6fa;\n' +
|
|
55
|
-
' font-family: system-ui, -apple-system, sans-serif;\n line-height: 1.6;\n}\n\n' +
|
|
56
|
-
'a {\n color: #2563ff;\n text-decoration: none;\n}\n\n' +
|
|
57
|
-
'a:hover {\n color: #22e3ab;\n}\n\n' +
|
|
58
|
-
'code {\n background: #11161f;\n color: #22e3ab;\n padding: 0.1rem 0.4rem;\n' +
|
|
59
|
-
' border-radius: 4px;\n font-size: 0.9em;\n}\n\n' +
|
|
60
|
-
'h1 {\n background: linear-gradient(90deg, #2563ff, #7c3aed, #22e3ab);\n' +
|
|
61
|
-
' -webkit-background-clip: text;\n background-clip: text;\n color: transparent;\n}\n';
|
|
62
|
-
|
|
63
|
-
/** A selectable template in the `create` wizard. */
|
|
64
|
-
interface TemplateOption {
|
|
65
|
-
readonly value: Template;
|
|
66
|
-
readonly label: string;
|
|
67
|
-
readonly hint: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface CreateOptions {
|
|
71
|
-
readonly name?: string;
|
|
72
|
-
readonly template?: Template;
|
|
73
|
-
readonly preprocessor?: Preprocessor;
|
|
74
|
-
readonly tailwind?: boolean;
|
|
75
|
-
/** AI assistant files to scaffold: `true` = all, `false` = none, omitted = ask. */
|
|
76
|
-
readonly ai?: boolean;
|
|
77
|
-
/** Enable build-time image optimization. Default `true`; omitted = ask. */
|
|
78
|
-
readonly images?: boolean;
|
|
79
|
-
readonly install?: boolean;
|
|
80
|
-
readonly git?: boolean;
|
|
81
|
-
readonly pm?: string;
|
|
82
|
-
readonly yes?: boolean;
|
|
83
|
-
readonly cwd: string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Aborts the wizard cleanly on Ctrl-C / cancel, narrowing the prompt result to its value type. */
|
|
87
|
-
function bail<T>(value: T | symbol): asserts value is T {
|
|
88
|
-
if (isCancel(value)) {
|
|
89
|
-
cancel('Scaffolding cancelled.');
|
|
90
|
-
process.exit(0);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function isEmptyDir(dir: string): Promise<boolean> {
|
|
95
|
-
try {
|
|
96
|
-
const entries = await fs.readdir(dir);
|
|
97
|
-
return entries.length === 0;
|
|
98
|
-
} catch {
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Builds the full file map (relative path → contents) for a scaffolded project. */
|
|
104
|
-
function scaffold(
|
|
105
|
-
name: string,
|
|
106
|
-
template: Template,
|
|
107
|
-
features: StyleFeatures,
|
|
108
|
-
aiTools: readonly string[],
|
|
109
|
-
images: boolean,
|
|
110
|
-
): Record<string, string> {
|
|
111
|
-
const toilVersion = version();
|
|
112
|
-
const devDependencies: Record<string, string> = {
|
|
113
|
-
'@types/react': '^19.2.15',
|
|
114
|
-
'@types/react-dom': '^19.2.3',
|
|
115
|
-
eslint: '^10.2.0',
|
|
116
|
-
prettier: '^3.8.1',
|
|
117
|
-
toilscript: '^0.1.2',
|
|
118
|
-
typescript: '^6.0.3',
|
|
119
|
-
};
|
|
120
|
-
for (const dep of requiredPackages(features).sort()) {
|
|
121
|
-
devDependencies[dep] = PKG_VERSION[dep] ?? 'latest';
|
|
122
|
-
}
|
|
123
|
-
const pkg = {
|
|
124
|
-
name: path.basename(name),
|
|
125
|
-
private: true,
|
|
126
|
-
type: 'module',
|
|
127
|
-
scripts: {
|
|
128
|
-
dev: 'toiljs dev',
|
|
129
|
-
build: 'toiljs build && toilscript --target release',
|
|
130
|
-
'build:client': 'toiljs build',
|
|
131
|
-
'build:server': 'toilscript --target release',
|
|
132
|
-
lint: 'eslint client',
|
|
133
|
-
typecheck: 'tsc --noEmit',
|
|
134
|
-
format: 'prettier --write "client/**/*.{ts,tsx,css,scss,less}" "client/public/**/*.html"',
|
|
135
|
-
},
|
|
136
|
-
dependencies: {
|
|
137
|
-
toiljs: `^${toilVersion}`,
|
|
138
|
-
react: '^19.2.6',
|
|
139
|
-
'react-dom': '^19.2.6',
|
|
140
|
-
},
|
|
141
|
-
devDependencies,
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const files: Record<string, string> = {
|
|
145
|
-
'package.json': JSON.stringify(pkg, null, 4) + '\n',
|
|
146
|
-
'toil.config.ts':
|
|
147
|
-
"import { defineConfig } from 'toiljs/compiler';\n\n" +
|
|
148
|
-
'export default defineConfig({\n' +
|
|
149
|
-
' client: {\n' +
|
|
150
|
-
' // Optimize images at build time (resize/compress imported images).\n' +
|
|
151
|
-
` images: ${String(images)},\n` +
|
|
152
|
-
' },\n' +
|
|
153
|
-
'});\n',
|
|
154
|
-
'tsconfig.json':
|
|
155
|
-
'{\n "extends": "toiljs/tsconfig",\n "include": ["client", "toil-env.d.ts", "toil-routes.d.ts"]\n}\n',
|
|
156
|
-
'eslint.config.js': "import toiljs from 'toiljs/eslint';\n\nexport default toiljs;\n",
|
|
157
|
-
'.prettierrc': '"toiljs/prettier"\n',
|
|
158
|
-
'.gitignore': 'node_modules\nbuild\n.toil\ntoil-env.d.ts\ntoil-routes.d.ts\n',
|
|
159
|
-
// Use the project's pinned TypeScript (node_modules) instead of VS Code's bundled version.
|
|
160
|
-
'.vscode/settings.json':
|
|
161
|
-
JSON.stringify({ 'typescript.tsdk': 'node_modules/typescript/lib' }, null, 4) + '\n',
|
|
162
|
-
'toil-env.d.ts': TOIL_ENV_DTS,
|
|
163
|
-
// Stub typed-routes augmentation (RoutePath = string until the first dev/build regenerates it).
|
|
164
|
-
'toil-routes.d.ts': '// AUTO-GENERATED by toil, do not edit.\nexport {};\n',
|
|
165
|
-
'toilconfig.json':
|
|
166
|
-
JSON.stringify(
|
|
167
|
-
{
|
|
168
|
-
entries: ['server/main.ts'],
|
|
169
|
-
targets: {
|
|
170
|
-
release: {
|
|
171
|
-
outFile: 'build/server/release.wasm',
|
|
172
|
-
textFile: 'build/server/release.wat',
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
options: {
|
|
176
|
-
sourceMap: false,
|
|
177
|
-
optimizeLevel: 3,
|
|
178
|
-
shrinkLevel: 1,
|
|
179
|
-
converge: true,
|
|
180
|
-
noAssert: false,
|
|
181
|
-
enable: [
|
|
182
|
-
'sign-extension',
|
|
183
|
-
'mutable-globals',
|
|
184
|
-
'nontrapping-f2i',
|
|
185
|
-
'bulk-memory',
|
|
186
|
-
'simd',
|
|
187
|
-
'reference-types',
|
|
188
|
-
'multi-value',
|
|
189
|
-
],
|
|
190
|
-
runtime: 'stub',
|
|
191
|
-
memoryBase: 0,
|
|
192
|
-
initialMemory: 1,
|
|
193
|
-
debug: false,
|
|
194
|
-
trapMode: 'allow',
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
null,
|
|
198
|
-
4,
|
|
199
|
-
) + '\n',
|
|
200
|
-
'server/tsconfig.json':
|
|
201
|
-
JSON.stringify(
|
|
202
|
-
{
|
|
203
|
-
extends: 'toilscript/std/assembly.json',
|
|
204
|
-
include: ['./**/*.ts'],
|
|
205
|
-
},
|
|
206
|
-
null,
|
|
207
|
-
4,
|
|
208
|
-
) + '\n',
|
|
209
|
-
'server/index.ts': 'export function add(a: i32, b: i32): i32 {\n return a + b;\n}\n',
|
|
210
|
-
'server/main.ts':
|
|
211
|
-
"import { add } from './index';\n\n" +
|
|
212
|
-
'@main\nfunction run(): i32 {\n return add(40, 2);\n}\n',
|
|
213
|
-
'README.md': [
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
'
|
|
249
|
-
'
|
|
250
|
-
'
|
|
251
|
-
'
|
|
252
|
-
'
|
|
253
|
-
'
|
|
254
|
-
'
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
'client/public/
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
function
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
'
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*/
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (!opts.yes) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
await
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `toiljs create`, an interactive project scaffolder (Clack-powered) that wires a new
|
|
3
|
+
* app to the enforced toiljs presets (tsconfig / eslint / prettier) and file-based routing.
|
|
4
|
+
* Supports a non-interactive path via flags (`--yes`, `--template`, …) for scripting/CI.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
cancel,
|
|
12
|
+
confirm,
|
|
13
|
+
intro,
|
|
14
|
+
isCancel,
|
|
15
|
+
multiselect,
|
|
16
|
+
note,
|
|
17
|
+
outro,
|
|
18
|
+
select,
|
|
19
|
+
spinner,
|
|
20
|
+
text,
|
|
21
|
+
} from '@clack/prompts';
|
|
22
|
+
import { AI_HELPER_IDS, AI_HELPERS, aiHelperFiles, TOIL_DOCS, TOIL_ENV_DTS } from 'toiljs/compiler';
|
|
23
|
+
import pc from 'picocolors';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
PKG_VERSION,
|
|
27
|
+
type Preprocessor,
|
|
28
|
+
PREPROCESSORS,
|
|
29
|
+
requiredPackages,
|
|
30
|
+
setStyleImports,
|
|
31
|
+
styleEntry,
|
|
32
|
+
type StyleFeatures,
|
|
33
|
+
styleImportLines,
|
|
34
|
+
TAILWIND_CSS,
|
|
35
|
+
TAILWIND_ENTRY,
|
|
36
|
+
} from './features.js';
|
|
37
|
+
import { run } from './proc.js';
|
|
38
|
+
import { accent, dim, version } from './ui.js';
|
|
39
|
+
import { isPackageManager, isValidName, resolveProjectDir } from './validate.js';
|
|
40
|
+
|
|
41
|
+
export type Template = 'app' | 'minimal';
|
|
42
|
+
|
|
43
|
+
/** Human label for each preprocessor in the styling picker. */
|
|
44
|
+
const PREPROCESSOR_LABEL: Record<Preprocessor, string> = {
|
|
45
|
+
css: 'Plain CSS',
|
|
46
|
+
sass: 'Sass (SCSS)',
|
|
47
|
+
less: 'Less',
|
|
48
|
+
stylus: 'Stylus',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Default global stylesheet contents (palette base styles), shared by every preprocessor. */
|
|
52
|
+
const DEFAULT_STYLE_CONTENT =
|
|
53
|
+
':root {\n color-scheme: dark;\n}\n\n' +
|
|
54
|
+
'body {\n margin: 0;\n background: #080d11;\n color: #f5f6fa;\n' +
|
|
55
|
+
' font-family: system-ui, -apple-system, sans-serif;\n line-height: 1.6;\n}\n\n' +
|
|
56
|
+
'a {\n color: #2563ff;\n text-decoration: none;\n}\n\n' +
|
|
57
|
+
'a:hover {\n color: #22e3ab;\n}\n\n' +
|
|
58
|
+
'code {\n background: #11161f;\n color: #22e3ab;\n padding: 0.1rem 0.4rem;\n' +
|
|
59
|
+
' border-radius: 4px;\n font-size: 0.9em;\n}\n\n' +
|
|
60
|
+
'h1 {\n background: linear-gradient(90deg, #2563ff, #7c3aed, #22e3ab);\n' +
|
|
61
|
+
' -webkit-background-clip: text;\n background-clip: text;\n color: transparent;\n}\n';
|
|
62
|
+
|
|
63
|
+
/** A selectable template in the `create` wizard. */
|
|
64
|
+
interface TemplateOption {
|
|
65
|
+
readonly value: Template;
|
|
66
|
+
readonly label: string;
|
|
67
|
+
readonly hint: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CreateOptions {
|
|
71
|
+
readonly name?: string;
|
|
72
|
+
readonly template?: Template;
|
|
73
|
+
readonly preprocessor?: Preprocessor;
|
|
74
|
+
readonly tailwind?: boolean;
|
|
75
|
+
/** AI assistant files to scaffold: `true` = all, `false` = none, omitted = ask. */
|
|
76
|
+
readonly ai?: boolean;
|
|
77
|
+
/** Enable build-time image optimization. Default `true`; omitted = ask. */
|
|
78
|
+
readonly images?: boolean;
|
|
79
|
+
readonly install?: boolean;
|
|
80
|
+
readonly git?: boolean;
|
|
81
|
+
readonly pm?: string;
|
|
82
|
+
readonly yes?: boolean;
|
|
83
|
+
readonly cwd: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Aborts the wizard cleanly on Ctrl-C / cancel, narrowing the prompt result to its value type. */
|
|
87
|
+
function bail<T>(value: T | symbol): asserts value is T {
|
|
88
|
+
if (isCancel(value)) {
|
|
89
|
+
cancel('Scaffolding cancelled.');
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function isEmptyDir(dir: string): Promise<boolean> {
|
|
95
|
+
try {
|
|
96
|
+
const entries = await fs.readdir(dir);
|
|
97
|
+
return entries.length === 0;
|
|
98
|
+
} catch {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Builds the full file map (relative path → contents) for a scaffolded project. */
|
|
104
|
+
function scaffold(
|
|
105
|
+
name: string,
|
|
106
|
+
template: Template,
|
|
107
|
+
features: StyleFeatures,
|
|
108
|
+
aiTools: readonly string[],
|
|
109
|
+
images: boolean,
|
|
110
|
+
): Record<string, string> {
|
|
111
|
+
const toilVersion = version();
|
|
112
|
+
const devDependencies: Record<string, string> = {
|
|
113
|
+
'@types/react': '^19.2.15',
|
|
114
|
+
'@types/react-dom': '^19.2.3',
|
|
115
|
+
eslint: '^10.2.0',
|
|
116
|
+
prettier: '^3.8.1',
|
|
117
|
+
toilscript: '^0.1.2',
|
|
118
|
+
typescript: '^6.0.3',
|
|
119
|
+
};
|
|
120
|
+
for (const dep of requiredPackages(features).sort()) {
|
|
121
|
+
devDependencies[dep] = PKG_VERSION[dep] ?? 'latest';
|
|
122
|
+
}
|
|
123
|
+
const pkg = {
|
|
124
|
+
name: path.basename(name),
|
|
125
|
+
private: true,
|
|
126
|
+
type: 'module',
|
|
127
|
+
scripts: {
|
|
128
|
+
dev: 'toiljs dev',
|
|
129
|
+
build: 'toiljs build && toilscript --target release',
|
|
130
|
+
'build:client': 'toiljs build',
|
|
131
|
+
'build:server': 'toilscript --target release',
|
|
132
|
+
lint: 'eslint client',
|
|
133
|
+
typecheck: 'tsc --noEmit',
|
|
134
|
+
format: 'prettier --write "client/**/*.{ts,tsx,css,scss,less}" "client/public/**/*.html"',
|
|
135
|
+
},
|
|
136
|
+
dependencies: {
|
|
137
|
+
toiljs: `^${toilVersion}`,
|
|
138
|
+
react: '^19.2.6',
|
|
139
|
+
'react-dom': '^19.2.6',
|
|
140
|
+
},
|
|
141
|
+
devDependencies,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const files: Record<string, string> = {
|
|
145
|
+
'package.json': JSON.stringify(pkg, null, 4) + '\n',
|
|
146
|
+
'toil.config.ts':
|
|
147
|
+
"import { defineConfig } from 'toiljs/compiler';\n\n" +
|
|
148
|
+
'export default defineConfig({\n' +
|
|
149
|
+
' client: {\n' +
|
|
150
|
+
' // Optimize images at build time (resize/compress imported images).\n' +
|
|
151
|
+
` images: ${String(images)},\n` +
|
|
152
|
+
' },\n' +
|
|
153
|
+
'});\n',
|
|
154
|
+
'tsconfig.json':
|
|
155
|
+
'{\n "extends": "toiljs/tsconfig",\n "include": ["client", "toil-env.d.ts", "toil-routes.d.ts"]\n}\n',
|
|
156
|
+
'eslint.config.js': "import toiljs from 'toiljs/eslint';\n\nexport default toiljs;\n",
|
|
157
|
+
'.prettierrc': '"toiljs/prettier"\n',
|
|
158
|
+
'.gitignore': 'node_modules\nbuild\n.toil\ntoil-env.d.ts\ntoil-routes.d.ts\n',
|
|
159
|
+
// Use the project's pinned TypeScript (node_modules) instead of VS Code's bundled version.
|
|
160
|
+
'.vscode/settings.json':
|
|
161
|
+
JSON.stringify({ 'typescript.tsdk': 'node_modules/typescript/lib' }, null, 4) + '\n',
|
|
162
|
+
'toil-env.d.ts': TOIL_ENV_DTS,
|
|
163
|
+
// Stub typed-routes augmentation (RoutePath = string until the first dev/build regenerates it).
|
|
164
|
+
'toil-routes.d.ts': '// AUTO-GENERATED by toil, do not edit.\nexport {};\n',
|
|
165
|
+
'toilconfig.json':
|
|
166
|
+
JSON.stringify(
|
|
167
|
+
{
|
|
168
|
+
entries: ['server/main.ts'],
|
|
169
|
+
targets: {
|
|
170
|
+
release: {
|
|
171
|
+
outFile: 'build/server/release.wasm',
|
|
172
|
+
textFile: 'build/server/release.wat',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
options: {
|
|
176
|
+
sourceMap: false,
|
|
177
|
+
optimizeLevel: 3,
|
|
178
|
+
shrinkLevel: 1,
|
|
179
|
+
converge: true,
|
|
180
|
+
noAssert: false,
|
|
181
|
+
enable: [
|
|
182
|
+
'sign-extension',
|
|
183
|
+
'mutable-globals',
|
|
184
|
+
'nontrapping-f2i',
|
|
185
|
+
'bulk-memory',
|
|
186
|
+
'simd',
|
|
187
|
+
'reference-types',
|
|
188
|
+
'multi-value',
|
|
189
|
+
],
|
|
190
|
+
runtime: 'stub',
|
|
191
|
+
memoryBase: 0,
|
|
192
|
+
initialMemory: 1,
|
|
193
|
+
debug: false,
|
|
194
|
+
trapMode: 'allow',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
null,
|
|
198
|
+
4,
|
|
199
|
+
) + '\n',
|
|
200
|
+
'server/tsconfig.json':
|
|
201
|
+
JSON.stringify(
|
|
202
|
+
{
|
|
203
|
+
extends: 'toilscript/std/assembly.json',
|
|
204
|
+
include: ['./**/*.ts'],
|
|
205
|
+
},
|
|
206
|
+
null,
|
|
207
|
+
4,
|
|
208
|
+
) + '\n',
|
|
209
|
+
'server/index.ts': 'export function add(a: i32, b: i32): i32 {\n return a + b;\n}\n',
|
|
210
|
+
'server/main.ts':
|
|
211
|
+
"import { add } from './index';\n\n" +
|
|
212
|
+
'@main\nfunction run(): i32 {\n return add(40, 2);\n}\n',
|
|
213
|
+
'README.md': [
|
|
214
|
+
'# ' + path.basename(name),
|
|
215
|
+
'',
|
|
216
|
+
'A [toiljs](https://toil.org) app.',
|
|
217
|
+
'',
|
|
218
|
+
'## Develop',
|
|
219
|
+
'',
|
|
220
|
+
' npm install',
|
|
221
|
+
' npm run dev',
|
|
222
|
+
'',
|
|
223
|
+
'## Build',
|
|
224
|
+
'',
|
|
225
|
+
' npm run build',
|
|
226
|
+
'',
|
|
227
|
+
].join('\n'),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// The `app` template's client UI is copied from examples/basic/client at runtime; `minimal` ships an
|
|
231
|
+
// inline client here.
|
|
232
|
+
if (template === 'minimal') Object.assign(files, minimalClient(name, features));
|
|
233
|
+
|
|
234
|
+
// Selected AI-assistant pointer files at the root (committed). The real docs are always seeded
|
|
235
|
+
// under .toil/docs (gitignored; regenerated by dev/build) since the framework manages them.
|
|
236
|
+
Object.assign(files, aiHelperFiles(aiTools));
|
|
237
|
+
for (const [docName, content] of Object.entries(TOIL_DOCS)) {
|
|
238
|
+
files[`.toil/docs/${docName}`] = content;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return files;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The inline client UI for the `minimal` template (the `app` template copies examples/basic/client). */
|
|
245
|
+
function minimalClient(name: string, features: StyleFeatures): Record<string, string> {
|
|
246
|
+
const files: Record<string, string> = {
|
|
247
|
+
'client/public/index.html':
|
|
248
|
+
'<!doctype html>\n<html lang="en">\n <head>\n' +
|
|
249
|
+
' <meta charset="utf-8" />\n' +
|
|
250
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1" />\n' +
|
|
251
|
+
' <meta name="theme-color" content="#080D11" />\n' +
|
|
252
|
+
' <meta name="description" content="" />\n' +
|
|
253
|
+
' <link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n' +
|
|
254
|
+
' <link rel="manifest" href="/manifest.webmanifest" />\n' +
|
|
255
|
+
` <title>${path.basename(name)}</title>\n` +
|
|
256
|
+
' </head>\n <body>\n <div id="root"></div>\n </body>\n</html>\n',
|
|
257
|
+
'client/public/favicon.svg':
|
|
258
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">\n' +
|
|
259
|
+
' <defs>\n' +
|
|
260
|
+
' <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">\n' +
|
|
261
|
+
' <stop offset="0" stop-color="#2563FF" />\n' +
|
|
262
|
+
' <stop offset="0.5" stop-color="#7C3AED" />\n' +
|
|
263
|
+
' <stop offset="1" stop-color="#22E3AB" />\n' +
|
|
264
|
+
' </linearGradient>\n' +
|
|
265
|
+
' </defs>\n' +
|
|
266
|
+
' <rect width="32" height="32" rx="7" fill="#080D11" />\n' +
|
|
267
|
+
' <path d="M9 10h14v3.2h-5.4V24h-3.2V13.2H9z" fill="url(#g)" />\n' +
|
|
268
|
+
'</svg>\n',
|
|
269
|
+
'client/public/robots.txt': 'User-agent: *\nAllow: /\n',
|
|
270
|
+
'client/public/manifest.webmanifest':
|
|
271
|
+
JSON.stringify(
|
|
272
|
+
{
|
|
273
|
+
name: path.basename(name),
|
|
274
|
+
short_name: path.basename(name),
|
|
275
|
+
start_url: '/',
|
|
276
|
+
display: 'standalone',
|
|
277
|
+
background_color: '#080D11',
|
|
278
|
+
theme_color: '#080D11',
|
|
279
|
+
icons: [{ src: '/favicon.svg', type: 'image/svg+xml', sizes: 'any' }],
|
|
280
|
+
},
|
|
281
|
+
null,
|
|
282
|
+
4,
|
|
283
|
+
) + '\n',
|
|
284
|
+
'client/public/images/.gitkeep':
|
|
285
|
+
'# Place images and other static assets here; served at /images/*.\n',
|
|
286
|
+
'client/toil.tsx':
|
|
287
|
+
"import { routes, layout, notFound, globalError, slots } from 'toiljs/routes';\n\n" +
|
|
288
|
+
styleImportLines(features).join('\n') +
|
|
289
|
+
'\n\n' +
|
|
290
|
+
'Toil.mount(routes, layout, notFound, globalError, slots);\n',
|
|
291
|
+
[`client/${styleEntry(features.preprocessor)}`]: DEFAULT_STYLE_CONTENT,
|
|
292
|
+
'client/components/.gitkeep': '# Place shared React components here.\n',
|
|
293
|
+
'client/layout.tsx': `import { type ReactNode } from 'react';
|
|
294
|
+
|
|
295
|
+
export default function Layout({ children }: { children?: ReactNode }) {
|
|
296
|
+
return (
|
|
297
|
+
<div style={{ maxWidth: 680, margin: '0 auto', padding: '3rem 1.5rem' }}>
|
|
298
|
+
<header
|
|
299
|
+
style={{
|
|
300
|
+
display: 'flex',
|
|
301
|
+
gap: '1rem',
|
|
302
|
+
alignItems: 'baseline',
|
|
303
|
+
borderBottom: '1px solid #1b2330',
|
|
304
|
+
paddingBottom: '0.75rem',
|
|
305
|
+
marginBottom: '1.5rem',
|
|
306
|
+
}}>
|
|
307
|
+
<strong style={{ color: '#2563FF', fontSize: '1.1rem' }}>${path.basename(name)}</strong>
|
|
308
|
+
<nav style={{ display: 'flex', gap: '1rem' }}>
|
|
309
|
+
<Toil.Link href="/">home</Toil.Link>
|
|
310
|
+
</nav>
|
|
311
|
+
</header>
|
|
312
|
+
{children}
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
`,
|
|
317
|
+
'client/routes/index.tsx':
|
|
318
|
+
'export default function Home() {\n' +
|
|
319
|
+
' return (\n <main>\n' +
|
|
320
|
+
' <h1>Welcome to toiljs</h1>\n' +
|
|
321
|
+
' <p>File-based routing, bundled by Vite, zero config.</p>\n' +
|
|
322
|
+
' </main>\n );\n}\n',
|
|
323
|
+
};
|
|
324
|
+
if (features.tailwind) files[`client/${TAILWIND_ENTRY}`] = TAILWIND_CSS;
|
|
325
|
+
return files;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Absolute path to the `app` starter client UI. There is a single source: `examples/basic/client`
|
|
330
|
+
* (shipped in the package), the runnable example IS the create template, so there's nothing to
|
|
331
|
+
* keep in sync.
|
|
332
|
+
*/
|
|
333
|
+
function appClientDir(): string {
|
|
334
|
+
return path.resolve(
|
|
335
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
336
|
+
'..',
|
|
337
|
+
'..',
|
|
338
|
+
'examples',
|
|
339
|
+
'basic',
|
|
340
|
+
'client',
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Applies the chosen styling to a copied template's client dir: renames the stylesheet to the
|
|
346
|
+
* preprocessor's extension, adds the Tailwind entry, and rewrites `toil.tsx`'s style imports.
|
|
347
|
+
*/
|
|
348
|
+
async function applyStyling(clientDir: string, features: StyleFeatures): Promise<void> {
|
|
349
|
+
// Plain CSS without Tailwind is exactly what the template ships, leave it byte-for-byte.
|
|
350
|
+
if (features.preprocessor === 'css' && !features.tailwind) return;
|
|
351
|
+
const entry = styleEntry(features.preprocessor);
|
|
352
|
+
if (entry !== 'styles/main.css') {
|
|
353
|
+
await fs.rename(path.join(clientDir, 'styles', 'main.css'), path.join(clientDir, entry));
|
|
354
|
+
}
|
|
355
|
+
if (features.tailwind) {
|
|
356
|
+
await fs.writeFile(path.join(clientDir, TAILWIND_ENTRY), TAILWIND_CSS, 'utf8');
|
|
357
|
+
}
|
|
358
|
+
const toilPath = path.join(clientDir, 'toil.tsx');
|
|
359
|
+
await fs.writeFile(
|
|
360
|
+
toilPath,
|
|
361
|
+
setStyleImports(await fs.readFile(toilPath, 'utf8'), features),
|
|
362
|
+
'utf8',
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function writeFiles(dir: string, files: Record<string, string>): Promise<void> {
|
|
367
|
+
for (const [rel, contents] of Object.entries(files)) {
|
|
368
|
+
const full = path.join(dir, rel);
|
|
369
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
370
|
+
await fs.writeFile(full, contents, 'utf8');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Runs the create flow (interactive unless `--yes`). */
|
|
375
|
+
export async function runCreate(opts: CreateOptions): Promise<void> {
|
|
376
|
+
intro(accent(' toiljs create '));
|
|
377
|
+
|
|
378
|
+
let name = opts.name;
|
|
379
|
+
if (!name) {
|
|
380
|
+
if (opts.yes) {
|
|
381
|
+
name = 'my-toil-app';
|
|
382
|
+
} else {
|
|
383
|
+
const answer = await text({
|
|
384
|
+
message: 'Project name',
|
|
385
|
+
placeholder: 'my-toil-app',
|
|
386
|
+
defaultValue: 'my-toil-app',
|
|
387
|
+
validate: (v) => {
|
|
388
|
+
const result = isValidName(v || 'my-toil-app');
|
|
389
|
+
return result === true ? undefined : result;
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
bail(answer);
|
|
393
|
+
name = answer.trim() || 'my-toil-app';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const valid = isValidName(name);
|
|
397
|
+
if (valid !== true) {
|
|
398
|
+
cancel(valid);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const targetDir = resolveProjectDir(opts.cwd, name);
|
|
403
|
+
if (targetDir === null) {
|
|
404
|
+
cancel('Project name must stay inside the current directory (no "..", no absolute paths).');
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
const rel = path.relative(opts.cwd, targetDir) || '.';
|
|
408
|
+
|
|
409
|
+
if (!(await isEmptyDir(targetDir))) {
|
|
410
|
+
if (opts.yes) {
|
|
411
|
+
cancel(`Directory ${pc.cyan(rel)} is not empty.`);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
const proceed = await confirm({
|
|
415
|
+
message: `Directory ${pc.cyan(rel)} is not empty. Scaffold into it anyway?`,
|
|
416
|
+
initialValue: false,
|
|
417
|
+
});
|
|
418
|
+
bail(proceed);
|
|
419
|
+
if (!proceed) {
|
|
420
|
+
cancel('Scaffolding cancelled.');
|
|
421
|
+
process.exit(0);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let template: Template = opts.template ?? 'app';
|
|
426
|
+
if (!opts.template && !opts.yes) {
|
|
427
|
+
const templateOptions: TemplateOption[] = [
|
|
428
|
+
{
|
|
429
|
+
value: 'app',
|
|
430
|
+
label: 'App',
|
|
431
|
+
hint: 'the full ToilJS starter, landing page, layout, styles, demo routes',
|
|
432
|
+
},
|
|
433
|
+
{ value: 'minimal', label: 'Minimal', hint: 'just a layout and a home route' },
|
|
434
|
+
];
|
|
435
|
+
const choice = await select({
|
|
436
|
+
message: 'Which template?',
|
|
437
|
+
options: templateOptions,
|
|
438
|
+
initialValue: 'app',
|
|
439
|
+
});
|
|
440
|
+
bail(choice);
|
|
441
|
+
template = choice === 'minimal' ? 'minimal' : 'app';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let preprocessor: Preprocessor = opts.preprocessor ?? 'css';
|
|
445
|
+
let tailwind = opts.tailwind ?? false;
|
|
446
|
+
if (!opts.yes) {
|
|
447
|
+
if (opts.preprocessor === undefined) {
|
|
448
|
+
const choice = await select<Preprocessor>({
|
|
449
|
+
message: 'Styling',
|
|
450
|
+
options: PREPROCESSORS.map((value) => ({
|
|
451
|
+
value,
|
|
452
|
+
label: PREPROCESSOR_LABEL[value],
|
|
453
|
+
})),
|
|
454
|
+
initialValue: 'css',
|
|
455
|
+
});
|
|
456
|
+
bail(choice);
|
|
457
|
+
preprocessor = choice;
|
|
458
|
+
}
|
|
459
|
+
if (opts.tailwind === undefined) {
|
|
460
|
+
const tw = await confirm({ message: 'Add Tailwind CSS?', initialValue: false });
|
|
461
|
+
bail(tw);
|
|
462
|
+
tailwind = tw;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const features: StyleFeatures = { preprocessor, tailwind };
|
|
466
|
+
|
|
467
|
+
// AI assistant files: --ai = all, --no-ai = none, otherwise ask (default: all selected).
|
|
468
|
+
let aiTools: string[] = opts.ai === false ? [] : [...AI_HELPER_IDS];
|
|
469
|
+
if (opts.ai === undefined && !opts.yes) {
|
|
470
|
+
const picked = await multiselect<string>({
|
|
471
|
+
message: 'AI assistant files (read by Claude, Cursor, Codex, Copilot)',
|
|
472
|
+
options: [
|
|
473
|
+
...AI_HELPERS.map((h) => ({ value: h.id, label: h.label })),
|
|
474
|
+
{ value: 'none', label: 'None' },
|
|
475
|
+
],
|
|
476
|
+
initialValues: [...AI_HELPER_IDS],
|
|
477
|
+
required: false,
|
|
478
|
+
});
|
|
479
|
+
bail(picked);
|
|
480
|
+
// Selecting "None" (or deselecting everything) scaffolds no AI helper files.
|
|
481
|
+
aiTools = picked.includes('none') ? [] : picked;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Build-time image optimization: on by default (just press enter to keep it).
|
|
485
|
+
let images = opts.images ?? true;
|
|
486
|
+
if (opts.images === undefined && !opts.yes) {
|
|
487
|
+
const im = await confirm({ message: 'Optimize images at build time?', initialValue: true });
|
|
488
|
+
bail(im);
|
|
489
|
+
images = im;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let initGit = opts.git ?? false;
|
|
493
|
+
let install = opts.install ?? false;
|
|
494
|
+
const pm = opts.pm ?? 'npm';
|
|
495
|
+
if (!isPackageManager(pm)) {
|
|
496
|
+
cancel(`Unsupported package manager: ${pm} (use npm, pnpm, yarn, or bun).`);
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
if (!opts.yes) {
|
|
500
|
+
if (opts.git === undefined) {
|
|
501
|
+
const g = await confirm({
|
|
502
|
+
message: 'Initialize a git repository?',
|
|
503
|
+
initialValue: true,
|
|
504
|
+
});
|
|
505
|
+
bail(g);
|
|
506
|
+
initGit = g;
|
|
507
|
+
}
|
|
508
|
+
if (opts.install === undefined) {
|
|
509
|
+
const i = await confirm({ message: 'Install dependencies now?', initialValue: false });
|
|
510
|
+
bail(i);
|
|
511
|
+
install = i;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const s = spinner();
|
|
516
|
+
s.start('Scaffolding project');
|
|
517
|
+
await writeFiles(targetDir, scaffold(name, template, features, aiTools, images));
|
|
518
|
+
if (template === 'app') {
|
|
519
|
+
// Copy the example client (the single starter source), set its <title>, then apply styling.
|
|
520
|
+
await fs.cp(appClientDir(), path.join(targetDir, 'client'), { recursive: true });
|
|
521
|
+
const indexHtml = path.join(targetDir, 'client', 'public', 'index.html');
|
|
522
|
+
const html = await fs.readFile(indexHtml, 'utf8');
|
|
523
|
+
await fs.writeFile(
|
|
524
|
+
indexHtml,
|
|
525
|
+
html.replace(/<title>[^<]*<\/title>/, `<title>${path.basename(name)}</title>`),
|
|
526
|
+
);
|
|
527
|
+
await applyStyling(path.join(targetDir, 'client'), features);
|
|
528
|
+
}
|
|
529
|
+
s.stop(`Scaffolded ${pc.cyan(rel)}`);
|
|
530
|
+
|
|
531
|
+
if (initGit) {
|
|
532
|
+
const g = spinner();
|
|
533
|
+
g.start('Initializing git repository');
|
|
534
|
+
try {
|
|
535
|
+
await run('git', ['init', '-q'], targetDir);
|
|
536
|
+
await run('git', ['add', '-A'], targetDir);
|
|
537
|
+
g.stop('Initialized git repository');
|
|
538
|
+
} catch {
|
|
539
|
+
g.stop(pc.yellow('Skipped git init (git not available)'));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (install) {
|
|
544
|
+
const i = spinner();
|
|
545
|
+
i.start(`Installing dependencies with ${pm}`);
|
|
546
|
+
try {
|
|
547
|
+
await run(pm, ['install'], targetDir);
|
|
548
|
+
i.stop('Installed dependencies');
|
|
549
|
+
} catch {
|
|
550
|
+
i.stop(pc.yellow(`Could not install with ${pm}, run it yourself later`));
|
|
551
|
+
install = false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const steps: string[] = [];
|
|
556
|
+
if (rel !== '.') steps.push(`cd ${rel}`);
|
|
557
|
+
if (!install) steps.push('npm install');
|
|
558
|
+
steps.push(`${accent('npm run dev')} ${dim('start the dev server')}`);
|
|
559
|
+
steps.push(`${accent('npm run build')} ${dim('build for production')}`);
|
|
560
|
+
note(steps.map((l) => dim(' ') + l).join('\n'), 'Next steps');
|
|
561
|
+
|
|
562
|
+
outro(`Created ${accent(path.basename(name))}, happy building! ${dim('· v' + version())}`);
|
|
563
|
+
}
|