toiljs 0.0.7 → 0.0.9
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/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/configure.d.ts +1 -0
- package/build/cli/configure.js +85 -20
- package/build/cli/create.d.ts +1 -0
- package/build/cli/create.js +18 -7
- package/build/cli/features.d.ts +2 -0
- package/build/cli/features.js +22 -0
- package/build/cli/index.js +8 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/components/Form.d.ts +12 -0
- package/build/client/components/Form.js +23 -0
- package/build/client/components/Image.d.ts +13 -0
- package/build/client/components/Image.js +22 -0
- package/build/client/components/Script.d.ts +13 -0
- package/build/client/components/Script.js +68 -0
- package/build/client/components/Slot.d.ts +6 -0
- package/build/client/components/Slot.js +6 -0
- package/build/client/dev/error-overlay.d.ts +20 -0
- package/build/client/dev/error-overlay.js +123 -0
- package/build/client/head/head.d.ts +2 -0
- package/build/client/head/head.js +17 -2
- package/build/client/head/metadata.d.ts +29 -0
- package/build/client/head/metadata.js +38 -0
- package/build/client/index.d.ts +15 -3
- package/build/client/index.js +8 -2
- package/build/client/navigation/navigation.d.ts +3 -0
- package/build/client/navigation/navigation.js +42 -1
- package/build/client/routing/Router.d.ts +1 -0
- package/build/client/routing/Router.js +56 -34
- package/build/client/routing/action.d.ts +17 -0
- package/build/client/routing/action.js +55 -0
- package/build/client/routing/hooks.d.ts +1 -0
- package/build/client/routing/hooks.js +6 -7
- package/build/client/routing/loader.d.ts +10 -2
- package/build/client/routing/loader.js +83 -24
- package/build/client/routing/mount.d.ts +1 -1
- package/build/client/routing/mount.js +12 -4
- package/build/client/routing/slot-context.d.ts +2 -0
- package/build/client/routing/slot-context.js +2 -0
- package/build/client/types.d.ts +1 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +10 -0
- package/build/compiler/config.js +5 -1
- package/build/compiler/docs.js +26 -26
- package/build/compiler/fonts.d.ts +4 -0
- package/build/compiler/fonts.js +64 -0
- package/build/compiler/generate.js +67 -32
- package/build/compiler/image-report.d.ts +2 -0
- package/build/compiler/image-report.js +62 -0
- package/build/compiler/plugin.js +1 -1
- package/build/compiler/prerender.d.ts +7 -0
- package/build/compiler/prerender.js +111 -0
- package/build/compiler/routes.d.ts +3 -0
- package/build/compiler/routes.js +50 -5
- package/build/compiler/seo.d.ts +70 -0
- package/build/compiler/seo.js +221 -0
- package/build/compiler/vite.js +13 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/examples/basic/client/404.tsx +1 -1
- package/examples/basic/client/components/Header.tsx +38 -0
- package/examples/basic/client/components/HoneycombBackground.tsx +86 -18
- package/examples/basic/client/global-error.tsx +3 -3
- package/examples/basic/client/layout.tsx +2 -33
- package/examples/basic/client/public/images/test_image.webp +0 -0
- package/examples/basic/client/routes/about.tsx +8 -0
- package/examples/basic/client/routes/get-started.tsx +1 -1
- package/examples/basic/client/routes/index.tsx +8 -1
- package/examples/basic/client/routes/io.tsx +1 -1
- package/examples/basic/client/routes/loader-demo/index.tsx +29 -1
- package/examples/basic/client/routes/test.tsx +8 -0
- package/examples/basic/client/styles/main.css +48 -1
- package/package.json +8 -6
- package/presets/eslint.js +7 -4
- package/presets/tsconfig.json +1 -1
- package/src/backend/index.ts +1 -1
- package/src/cli/configure.ts +102 -21
- package/src/cli/create.ts +25 -9
- package/src/cli/features.ts +33 -1
- package/src/cli/index.ts +10 -1
- package/src/cli/ui.ts +1 -1
- package/src/cli/validate.ts +1 -1
- package/src/client/components/Form.tsx +65 -0
- package/src/client/components/Image.tsx +89 -0
- package/src/client/components/Script.tsx +113 -0
- package/src/client/components/Slot.tsx +21 -0
- package/src/client/dev/error-overlay.tsx +197 -0
- package/src/client/head/head.ts +28 -3
- package/src/client/head/metadata.ts +92 -0
- package/src/client/index.ts +20 -3
- package/src/client/navigation/Link.tsx +1 -1
- package/src/client/navigation/navigation.ts +74 -4
- package/src/client/navigation/prefetch.ts +2 -2
- package/src/client/routing/Router.tsx +128 -62
- package/src/client/routing/action.ts +122 -0
- package/src/client/routing/error-boundary.tsx +1 -1
- package/src/client/routing/hooks.ts +17 -23
- package/src/client/routing/loader.ts +158 -35
- package/src/client/routing/mount.tsx +25 -3
- package/src/client/routing/slot-context.ts +7 -0
- package/src/client/types.ts +6 -4
- package/src/compiler/config.ts +40 -3
- package/src/compiler/docs.ts +26 -26
- package/src/compiler/fonts.ts +87 -0
- package/src/compiler/generate.ts +69 -31
- package/src/compiler/image-report.ts +85 -0
- package/src/compiler/plugin.ts +2 -2
- package/src/compiler/prerender.ts +130 -0
- package/src/compiler/routes.ts +62 -7
- package/src/compiler/seo.ts +356 -0
- package/src/compiler/vite.ts +21 -4
- package/src/io/FastSet.ts +1 -1
- package/src/io/index.ts +1 -1
- package/src/io/types.ts +1 -1
- package/src/server/index.ts +1 -1
- package/src/server/main.ts +1 -1
- package/src/shared/index.ts +1 -1
- package/test/dom/Image.test.tsx +46 -0
- package/test/dom/Script.test.tsx +45 -0
- package/test/dom/action.test.tsx +129 -0
- package/test/dom/error-overlay.test.tsx +44 -0
- package/test/dom/loader.test.tsx +121 -0
- package/test/dom/revalidate.test.tsx +38 -0
- package/test/dom/route-head.test.tsx +34 -0
- package/test/dom/router-loading.test.tsx +44 -0
- package/test/dom/slot.test.tsx +109 -0
- package/test/dom/view-transitions.test.tsx +51 -0
- package/test/features.test.ts +31 -0
- package/test/fonts.test.ts +26 -0
- package/test/metadata.test.ts +41 -0
- package/test/prerender.test.ts +46 -0
- package/test/routes.test.ts +20 -1
- package/test/seo.test.ts +142 -0
- package/examples/basic/client/template.tsx +0 -7
package/src/cli/configure.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `toiljs configure
|
|
2
|
+
* `toiljs configure`, toggle a project's client styling features (CSS preprocessor + Tailwind) on
|
|
3
3
|
* an existing app. Detects the current setup, prompts for the desired one, then rewrites the
|
|
4
4
|
* stylesheet(s) + the `client/toil.tsx` imports, edits `package.json`, and syncs node_modules with
|
|
5
5
|
* the project's package manager (so removed features are fully cleaned, not just disabled).
|
|
@@ -16,10 +16,12 @@ import {
|
|
|
16
16
|
PREPROCESSORS,
|
|
17
17
|
TAILWIND_CSS,
|
|
18
18
|
TAILWIND_ENTRY,
|
|
19
|
+
defaultConfigSource,
|
|
19
20
|
detectPreprocessor,
|
|
20
21
|
detectTailwind,
|
|
21
22
|
packageDiff,
|
|
22
23
|
preprocessorForExt,
|
|
24
|
+
setConfigImages,
|
|
23
25
|
setStyleImports,
|
|
24
26
|
styleEntry,
|
|
25
27
|
type Preprocessor,
|
|
@@ -34,10 +36,60 @@ export interface ConfigureOptions {
|
|
|
34
36
|
/** When set, the corresponding prompt is skipped (non-interactive). */
|
|
35
37
|
readonly preprocessor?: Preprocessor;
|
|
36
38
|
readonly tailwind?: boolean;
|
|
39
|
+
/** Toggle build-time image optimization. When set, the prompt is skipped. */
|
|
40
|
+
readonly images?: boolean;
|
|
37
41
|
/** Run the package manager to sync deps. Default `true`; `false` edits files only. */
|
|
38
42
|
readonly install?: boolean;
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
const CONFIG_FILES = [
|
|
46
|
+
'toil.config.ts',
|
|
47
|
+
'toil.config.mts',
|
|
48
|
+
'toil.config.js',
|
|
49
|
+
'toil.config.mjs',
|
|
50
|
+
'toiljs.config.ts',
|
|
51
|
+
'toiljs.config.mts',
|
|
52
|
+
'toiljs.config.js',
|
|
53
|
+
'toiljs.config.mjs',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** Reads the project's `toil.config.*` (path + source), or null if none exists. */
|
|
57
|
+
async function readConfigFile(root: string): Promise<{ path: string; source: string } | null> {
|
|
58
|
+
for (const name of CONFIG_FILES) {
|
|
59
|
+
const p = path.join(root, name);
|
|
60
|
+
try {
|
|
61
|
+
return { path: p, source: await fs.readFile(p, 'utf8') };
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Persists `client.images` to the project's `toil.config`. Edits an existing config in place (or
|
|
69
|
+
* creates `toil.config.ts` if none); returns `false` if the existing file's shape couldn't be
|
|
70
|
+
* edited, so the caller can tell the user to set it by hand.
|
|
71
|
+
*/
|
|
72
|
+
async function writeImagesFlag(root: string, enabled: boolean): Promise<boolean> {
|
|
73
|
+
const existing = await readConfigFile(root);
|
|
74
|
+
if (!existing) {
|
|
75
|
+
await fs.writeFile(path.join(root, 'toil.config.ts'), defaultConfigSource(enabled), 'utf8');
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
const next = setConfigImages(existing.source, enabled);
|
|
79
|
+
if (next === null) return false;
|
|
80
|
+
await fs.writeFile(existing.path, next, 'utf8');
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Current `client.images` setting (defaults to `true` when the config can't be loaded). */
|
|
85
|
+
async function resolveImages(root: string): Promise<boolean> {
|
|
86
|
+
try {
|
|
87
|
+
return (await loadConfig({ root })).images;
|
|
88
|
+
} catch {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
41
93
|
/** Resolves the client source dir, falling back to `<root>/client` if the config can't be loaded. */
|
|
42
94
|
async function resolveClientDir(root: string): Promise<string> {
|
|
43
95
|
try {
|
|
@@ -121,7 +173,7 @@ async function applyStyleFiles(
|
|
|
121
173
|
const newPath = path.join(clientDir, styleEntry(to.preprocessor));
|
|
122
174
|
await fs.mkdir(path.dirname(newPath), { recursive: true });
|
|
123
175
|
// Rename whatever main stylesheet actually exists (preserving its content), not an assumed
|
|
124
|
-
// name
|
|
176
|
+
// name, so we never blow away the user's styles when the on-disk extension differs.
|
|
125
177
|
const existing = await findMainStylesheet(clientDir);
|
|
126
178
|
if (existing && path.resolve(existing) !== path.resolve(newPath)) {
|
|
127
179
|
await fs.rename(existing, newPath);
|
|
@@ -213,7 +265,7 @@ export async function runConfigure(opts: ConfigureOptions): Promise<void> {
|
|
|
213
265
|
try {
|
|
214
266
|
pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8')) as PackageJson;
|
|
215
267
|
} catch {
|
|
216
|
-
cancel(`No package.json in ${pc.cyan(root)}
|
|
268
|
+
cancel(`No package.json in ${pc.cyan(root)}, run this inside a toiljs project.`);
|
|
217
269
|
process.exit(1);
|
|
218
270
|
}
|
|
219
271
|
|
|
@@ -224,13 +276,18 @@ export async function runConfigure(opts: ConfigureOptions): Promise<void> {
|
|
|
224
276
|
tailwind: detectTailwind(deps),
|
|
225
277
|
};
|
|
226
278
|
|
|
227
|
-
const
|
|
279
|
+
const currentImages = await resolveImages(root);
|
|
280
|
+
|
|
281
|
+
const nonInteractive =
|
|
282
|
+
opts.preprocessor !== undefined || opts.tailwind !== undefined || opts.images !== undefined;
|
|
228
283
|
let target: StyleFeatures;
|
|
284
|
+
let targetImages: boolean;
|
|
229
285
|
if (nonInteractive) {
|
|
230
286
|
target = {
|
|
231
287
|
preprocessor: opts.preprocessor ?? current.preprocessor,
|
|
232
288
|
tailwind: opts.tailwind ?? current.tailwind,
|
|
233
289
|
};
|
|
290
|
+
targetImages = opts.images ?? currentImages;
|
|
234
291
|
} else {
|
|
235
292
|
const ppChoice = await select<Preprocessor>({
|
|
236
293
|
message: 'CSS preprocessor',
|
|
@@ -240,33 +297,57 @@ export async function runConfigure(opts: ConfigureOptions): Promise<void> {
|
|
|
240
297
|
bail(ppChoice);
|
|
241
298
|
const twChoice = await confirm({ message: 'Use Tailwind CSS?', initialValue: current.tailwind });
|
|
242
299
|
bail(twChoice);
|
|
300
|
+
const imChoice = await confirm({
|
|
301
|
+
message: 'Optimize images at build time?',
|
|
302
|
+
initialValue: currentImages,
|
|
303
|
+
});
|
|
304
|
+
bail(imChoice);
|
|
243
305
|
target = { preprocessor: ppChoice, tailwind: twChoice };
|
|
306
|
+
targetImages = imChoice;
|
|
244
307
|
}
|
|
245
308
|
|
|
246
|
-
|
|
247
|
-
|
|
309
|
+
const styleChanged =
|
|
310
|
+
target.preprocessor !== current.preprocessor || target.tailwind !== current.tailwind;
|
|
311
|
+
const imagesChanged = targetImages !== currentImages;
|
|
312
|
+
if (!styleChanged && !imagesChanged) {
|
|
313
|
+
outro('No changes, your setup is already up to date.');
|
|
248
314
|
return;
|
|
249
315
|
}
|
|
250
316
|
|
|
251
317
|
const s = spinner();
|
|
252
318
|
s.start('Updating project files');
|
|
253
|
-
await applyConfigure(clientAbsDir, pkgPath, pkg, current, target);
|
|
254
|
-
|
|
319
|
+
if (styleChanged) await applyConfigure(clientAbsDir, pkgPath, pkg, current, target);
|
|
320
|
+
let imagesWarning = '';
|
|
321
|
+
if (imagesChanged && !(await writeImagesFlag(root, targetImages))) {
|
|
322
|
+
imagesWarning = pc.yellow(
|
|
323
|
+
' Could not edit toil.config automatically, set `client.images` by hand.',
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
s.stop('Updated project files');
|
|
255
327
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
328
|
+
if (styleChanged) {
|
|
329
|
+
const pm = await detectPackageManager(root);
|
|
330
|
+
if (opts.install === false) {
|
|
331
|
+
note(`${pc.cyan(`${pm} install`)} to sync the dependency changes.`, 'Next step');
|
|
332
|
+
} else {
|
|
333
|
+
const i = spinner();
|
|
334
|
+
i.start(`Syncing dependencies with ${pm}`);
|
|
335
|
+
try {
|
|
336
|
+
await run(pm, ['install'], root);
|
|
337
|
+
i.stop('Dependencies synced');
|
|
338
|
+
} catch {
|
|
339
|
+
i.stop(pc.yellow(`Could not run \`${pm} install\`, run it yourself to finish`));
|
|
340
|
+
}
|
|
267
341
|
}
|
|
268
342
|
}
|
|
269
343
|
|
|
270
|
-
|
|
271
|
-
|
|
344
|
+
const summary = [
|
|
345
|
+
styleChanged ? describe(current, target) : '',
|
|
346
|
+
imagesChanged ? dim(' ') + `image optimization: ${currentImages ? 'on' : 'off'} → ${targetImages ? 'on' : 'off'}` : '',
|
|
347
|
+
imagesWarning,
|
|
348
|
+
]
|
|
349
|
+
.filter(Boolean)
|
|
350
|
+
.join('\n');
|
|
351
|
+
note(summary, 'Updated');
|
|
352
|
+
outro(`Reconfigured, restart \`${accent('toiljs dev')}\` to pick up the changes.`);
|
|
272
353
|
}
|
package/src/cli/create.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `toiljs create
|
|
2
|
+
* `toiljs create`, an interactive project scaffolder (Clack-powered) that wires a new
|
|
3
3
|
* app to the enforced toiljs presets (tsconfig / eslint / prettier) and file-based routing.
|
|
4
4
|
* Supports a non-interactive path via flags (`--yes`, `--template`, …) for scripting/CI.
|
|
5
5
|
*/
|
|
@@ -74,6 +74,8 @@ export interface CreateOptions {
|
|
|
74
74
|
readonly tailwind?: boolean;
|
|
75
75
|
/** AI assistant files to scaffold: `true` = all, `false` = none, omitted = ask. */
|
|
76
76
|
readonly ai?: boolean;
|
|
77
|
+
/** Enable build-time image optimization. Default `true`; omitted = ask. */
|
|
78
|
+
readonly images?: boolean;
|
|
77
79
|
readonly install?: boolean;
|
|
78
80
|
readonly git?: boolean;
|
|
79
81
|
readonly pm?: string;
|
|
@@ -104,6 +106,7 @@ function scaffold(
|
|
|
104
106
|
template: Template,
|
|
105
107
|
features: StyleFeatures,
|
|
106
108
|
aiTools: readonly string[],
|
|
109
|
+
images: boolean,
|
|
107
110
|
): Record<string, string> {
|
|
108
111
|
const toilVersion = version();
|
|
109
112
|
const devDependencies: Record<string, string> = {
|
|
@@ -142,7 +145,12 @@ function scaffold(
|
|
|
142
145
|
'package.json': JSON.stringify(pkg, null, 4) + '\n',
|
|
143
146
|
'toil.config.ts':
|
|
144
147
|
"import { defineConfig } from 'toiljs/compiler';\n\n" +
|
|
145
|
-
'export default defineConfig({
|
|
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',
|
|
146
154
|
'tsconfig.json':
|
|
147
155
|
'{\n "extends": "toiljs/tsconfig",\n "include": ["client", "toil-env.d.ts", "toil-routes.d.ts"]\n}\n',
|
|
148
156
|
'eslint.config.js': "import toiljs from 'toiljs/eslint';\n\nexport default toiljs;\n",
|
|
@@ -153,7 +161,7 @@ function scaffold(
|
|
|
153
161
|
JSON.stringify({ 'typescript.tsdk': 'node_modules/typescript/lib' }, null, 4) + '\n',
|
|
154
162
|
'toil-env.d.ts': TOIL_ENV_DTS,
|
|
155
163
|
// Stub typed-routes augmentation (RoutePath = string until the first dev/build regenerates it).
|
|
156
|
-
'toil-routes.d.ts': '// AUTO-GENERATED by toil
|
|
164
|
+
'toil-routes.d.ts': '// AUTO-GENERATED by toil, do not edit.\nexport {};\n',
|
|
157
165
|
'toilconfig.json':
|
|
158
166
|
JSON.stringify(
|
|
159
167
|
{
|
|
@@ -304,7 +312,7 @@ export default function Layout({ children }: { children?: ReactNode }) {
|
|
|
304
312
|
|
|
305
313
|
/**
|
|
306
314
|
* Absolute path to the `app` starter client UI. There is a single source: `examples/basic/client`
|
|
307
|
-
* (shipped in the package)
|
|
315
|
+
* (shipped in the package), the runnable example IS the create template, so there's nothing to
|
|
308
316
|
* keep in sync.
|
|
309
317
|
*/
|
|
310
318
|
function appClientDir(): string {
|
|
@@ -323,7 +331,7 @@ function appClientDir(): string {
|
|
|
323
331
|
* preprocessor's extension, adds the Tailwind entry, and rewrites `toil.tsx`'s style imports.
|
|
324
332
|
*/
|
|
325
333
|
async function applyStyling(clientDir: string, features: StyleFeatures): Promise<void> {
|
|
326
|
-
// Plain CSS without Tailwind is exactly what the template ships
|
|
334
|
+
// Plain CSS without Tailwind is exactly what the template ships, leave it byte-for-byte.
|
|
327
335
|
if (features.preprocessor === 'css' && !features.tailwind) return;
|
|
328
336
|
const entry = styleEntry(features.preprocessor);
|
|
329
337
|
if (entry !== 'styles/main.css') {
|
|
@@ -398,7 +406,7 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
|
|
|
398
406
|
let template: Template = opts.template ?? 'app';
|
|
399
407
|
if (!opts.template && !opts.yes) {
|
|
400
408
|
const templateOptions: TemplateOption[] = [
|
|
401
|
-
{ value: 'app', label: 'App', hint: 'the full ToilJS starter
|
|
409
|
+
{ value: 'app', label: 'App', hint: 'the full ToilJS starter, landing page, layout, styles, demo routes' },
|
|
402
410
|
{ value: 'minimal', label: 'Minimal', hint: 'just a layout and a home route' },
|
|
403
411
|
];
|
|
404
412
|
const choice = await select({ message: 'Which template?', options: templateOptions, initialValue: 'app' });
|
|
@@ -443,6 +451,14 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
|
|
|
443
451
|
aiTools = picked.includes('none') ? [] : picked;
|
|
444
452
|
}
|
|
445
453
|
|
|
454
|
+
// Build-time image optimization: on by default (just press enter to keep it).
|
|
455
|
+
let images = opts.images ?? true;
|
|
456
|
+
if (opts.images === undefined && !opts.yes) {
|
|
457
|
+
const im = await confirm({ message: 'Optimize images at build time?', initialValue: true });
|
|
458
|
+
bail(im);
|
|
459
|
+
images = im;
|
|
460
|
+
}
|
|
461
|
+
|
|
446
462
|
let initGit = opts.git ?? false;
|
|
447
463
|
let install = opts.install ?? false;
|
|
448
464
|
const pm = opts.pm ?? 'npm';
|
|
@@ -465,7 +481,7 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
|
|
|
465
481
|
|
|
466
482
|
const s = spinner();
|
|
467
483
|
s.start('Scaffolding project');
|
|
468
|
-
await writeFiles(targetDir, scaffold(name, template, features, aiTools));
|
|
484
|
+
await writeFiles(targetDir, scaffold(name, template, features, aiTools, images));
|
|
469
485
|
if (template === 'app') {
|
|
470
486
|
// Copy the example client (the single starter source), set its <title>, then apply styling.
|
|
471
487
|
await fs.cp(appClientDir(), path.join(targetDir, 'client'), { recursive: true });
|
|
@@ -498,7 +514,7 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
|
|
|
498
514
|
await run(pm, ['install'], targetDir);
|
|
499
515
|
i.stop('Installed dependencies');
|
|
500
516
|
} catch {
|
|
501
|
-
i.stop(pc.yellow(`Could not install with ${pm}
|
|
517
|
+
i.stop(pc.yellow(`Could not install with ${pm}, run it yourself later`));
|
|
502
518
|
install = false;
|
|
503
519
|
}
|
|
504
520
|
}
|
|
@@ -510,5 +526,5 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
|
|
|
510
526
|
steps.push(`${accent('npm run build')} ${dim('build for production')}`);
|
|
511
527
|
note(steps.map((l) => dim(' ') + l).join('\n'), 'Next steps');
|
|
512
528
|
|
|
513
|
-
outro(`Created ${accent(path.basename(name))}
|
|
529
|
+
outro(`Created ${accent(path.basename(name))}, happy building! ${dim('· v' + version())}`);
|
|
514
530
|
}
|
package/src/cli/features.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure description of toiljs's optional client styling features
|
|
2
|
+
* Pure description of toiljs's optional client styling features, a CSS preprocessor and Tailwind ,
|
|
3
3
|
* shared by `create` (scaffold) and `configure` (toggle on existing projects). Dependency-light
|
|
4
4
|
* (no node IO) so it can be unit-tested; the file writes and package-manager calls live in the
|
|
5
5
|
* commands. Preprocessor and Tailwind are independent: Tailwind lives in its own `.css` entry so
|
|
@@ -114,6 +114,38 @@ export function setStyleImports(source: string, f: StyleFeatures): string {
|
|
|
114
114
|
return `${head}\n\n${block}\n${tail}`.replace(/\n{3,}/g, '\n\n');
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** A `toil.config` source containing `client: { images: <bool> }` (for scaffolding when none exists). */
|
|
118
|
+
export function defaultConfigSource(images: boolean): string {
|
|
119
|
+
return (
|
|
120
|
+
"import { defineConfig } from 'toiljs/compiler';\n\n" +
|
|
121
|
+
'export default defineConfig({\n' +
|
|
122
|
+
' client: {\n' +
|
|
123
|
+
' // Optimize images at build time (resize/compress imported images).\n' +
|
|
124
|
+
` images: ${String(images)},\n` +
|
|
125
|
+
' },\n' +
|
|
126
|
+
'});\n'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Sets the `client.images` flag in a `toil.config` source, returning the updated source, or `null`
|
|
132
|
+
* if the file's shape isn't recognized (the caller should then fall back to a manual note). Handles
|
|
133
|
+
* an existing `images:` value, an existing `client: {` block, or a bare `defineConfig({ … })`.
|
|
134
|
+
*/
|
|
135
|
+
export function setConfigImages(source: string, enabled: boolean): string | null {
|
|
136
|
+
const value = String(enabled);
|
|
137
|
+
if (/\bimages\s*:\s*(?:true|false)/.test(source)) {
|
|
138
|
+
return source.replace(/\bimages\s*:\s*(?:true|false)/, `images: ${value}`);
|
|
139
|
+
}
|
|
140
|
+
if (/\bclient\s*:\s*\{/.test(source)) {
|
|
141
|
+
return source.replace(/\bclient\s*:\s*\{/, `client: {\n images: ${value},`);
|
|
142
|
+
}
|
|
143
|
+
if (/defineConfig\(\s*\{/.test(source)) {
|
|
144
|
+
return source.replace(/defineConfig\(\s*\{/, `defineConfig({\n client: { images: ${value} },`);
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
117
149
|
/** Detects the active preprocessor from a project's combined dependency map. */
|
|
118
150
|
export function detectPreprocessor(deps: Record<string, string>): Preprocessor {
|
|
119
151
|
if ('sass' in deps) return 'sass';
|
package/src/cli/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* toiljs CLI. Routes `create` / `dev` / `build` and wraps them in the toiljs brand banner.
|
|
4
4
|
* The compiler stays presentation-free (imported via the package's own `toiljs/compiler`
|
|
5
|
-
* export); the epic bits
|
|
5
|
+
* export); the epic bits, banner, the Clack scaffolding wizard, live here.
|
|
6
6
|
*/
|
|
7
7
|
import { build, dev, start } from 'toiljs/compiler';
|
|
8
8
|
|
|
@@ -19,6 +19,7 @@ interface Flags {
|
|
|
19
19
|
preprocessor?: Preprocessor;
|
|
20
20
|
tailwind?: boolean;
|
|
21
21
|
ai?: boolean;
|
|
22
|
+
images?: boolean;
|
|
22
23
|
install?: boolean;
|
|
23
24
|
git?: boolean;
|
|
24
25
|
pm?: string;
|
|
@@ -64,6 +65,12 @@ function parseArgs(argv: string[]): Flags {
|
|
|
64
65
|
case '--no-ai':
|
|
65
66
|
flags.ai = false;
|
|
66
67
|
break;
|
|
68
|
+
case '--images':
|
|
69
|
+
flags.images = true;
|
|
70
|
+
break;
|
|
71
|
+
case '--no-images':
|
|
72
|
+
flags.images = false;
|
|
73
|
+
break;
|
|
67
74
|
case '--install':
|
|
68
75
|
flags.install = true;
|
|
69
76
|
break;
|
|
@@ -134,6 +141,7 @@ async function main(): Promise<void> {
|
|
|
134
141
|
preprocessor: flags.preprocessor,
|
|
135
142
|
tailwind: flags.tailwind,
|
|
136
143
|
ai: flags.ai,
|
|
144
|
+
images: flags.images,
|
|
137
145
|
install: flags.install,
|
|
138
146
|
git: flags.git,
|
|
139
147
|
pm: flags.pm,
|
|
@@ -148,6 +156,7 @@ async function main(): Promise<void> {
|
|
|
148
156
|
root: flags.root,
|
|
149
157
|
preprocessor: flags.preprocessor,
|
|
150
158
|
tailwind: flags.tailwind,
|
|
159
|
+
images: flags.images,
|
|
151
160
|
install: flags.install,
|
|
152
161
|
cwd: process.cwd(),
|
|
153
162
|
});
|
package/src/cli/ui.ts
CHANGED
|
@@ -54,7 +54,7 @@ export function success(s: string): string {
|
|
|
54
54
|
return rgb(ACCENT, s);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/** Error accent (red
|
|
57
|
+
/** Error accent (red, kept outside the brand palette since errors should read as errors). */
|
|
58
58
|
export const danger = pc.red;
|
|
59
59
|
|
|
60
60
|
function lerp(a: number, b: number, t: number): number {
|
package/src/cli/validate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure input validation for `toiljs create
|
|
2
|
+
* Pure input validation for `toiljs create`, kept dependency-light (only node:path) so it can be
|
|
3
3
|
* unit-tested without pulling in the rest of the CLI.
|
|
4
4
|
*/
|
|
5
5
|
import path from 'node:path';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useRef, type ReactNode, type SyntheticEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useAction, type ActionState, type RevalidateTarget } from '../routing/action.js';
|
|
4
|
+
|
|
5
|
+
/** Props for {@link Form}. */
|
|
6
|
+
export interface FormProps {
|
|
7
|
+
/** Handles the submission, receiving the form's `FormData`. May be async. */
|
|
8
|
+
action: (data: FormData) => void | Promise<void>;
|
|
9
|
+
/** Loader data to revalidate after a successful submit. Default `true` (the current route). */
|
|
10
|
+
revalidate?: RevalidateTarget;
|
|
11
|
+
/** Called after a successful submit. */
|
|
12
|
+
onSuccess?: () => void;
|
|
13
|
+
/** Called when the action throws. */
|
|
14
|
+
onError?: (error: unknown) => void;
|
|
15
|
+
/** Reset the form fields after a successful submit. Default `false`. */
|
|
16
|
+
resetOnSuccess?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Form contents. Pass a render function to receive live submit state, e.g. to disable the
|
|
20
|
+
* button while pending: `{({ pending }) => <button disabled={pending}>Save</button>}`.
|
|
21
|
+
*/
|
|
22
|
+
children?: ReactNode | ((state: ActionState<void>) => ReactNode);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A `<form>` that runs an {@link useAction} on submit (no page reload) and revalidates loader data
|
|
27
|
+
* on success, the write half of the loader/action data loop. Tracks pending/error state, which a
|
|
28
|
+
* render-function child can read.
|
|
29
|
+
*/
|
|
30
|
+
export function Form({
|
|
31
|
+
action,
|
|
32
|
+
revalidate,
|
|
33
|
+
onSuccess,
|
|
34
|
+
onError,
|
|
35
|
+
resetOnSuccess = false,
|
|
36
|
+
className,
|
|
37
|
+
children,
|
|
38
|
+
}: FormProps): ReactNode {
|
|
39
|
+
const formRef = useRef<HTMLFormElement | null>(null);
|
|
40
|
+
const handle = useAction((data: FormData) => action(data), {
|
|
41
|
+
revalidate,
|
|
42
|
+
onError,
|
|
43
|
+
onSuccess: () => {
|
|
44
|
+
if (resetOnSuccess) formRef.current?.reset();
|
|
45
|
+
onSuccess?.();
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const onSubmit = (event: SyntheticEvent<HTMLFormElement>): void => {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
formRef.current = event.currentTarget;
|
|
52
|
+
void handle.run(new FormData(event.currentTarget));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<form
|
|
57
|
+
ref={formRef}
|
|
58
|
+
className={className}
|
|
59
|
+
onSubmit={onSubmit}>
|
|
60
|
+
{typeof children === 'function'
|
|
61
|
+
? children({ pending: handle.pending, error: handle.error, data: handle.data })
|
|
62
|
+
: children}
|
|
63
|
+
</form>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState, type CSSProperties, type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for {@link Image}: every standard `<img>` attribute, plus toil's layout/loading controls.
|
|
5
|
+
* `src` and `alt` are required (`alt` is enforced for accessibility, pass `alt=""` for decorative
|
|
6
|
+
* images). `width`/`height` (or `fill`) reserve space to prevent layout shift.
|
|
7
|
+
*/
|
|
8
|
+
export interface ImageProps
|
|
9
|
+
extends Omit<ComponentPropsWithRef<'img'>, 'loading' | 'placeholder' | 'width' | 'height'> {
|
|
10
|
+
src: string;
|
|
11
|
+
alt: string;
|
|
12
|
+
/** Intrinsic width in px. Set together with `height` to reserve space (avoids layout shift). */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** Intrinsic height in px. Set together with `width` to reserve space (avoids layout shift). */
|
|
15
|
+
height?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Fill the nearest positioned ancestor (the parent must be `position: relative|absolute|fixed`).
|
|
18
|
+
* The image is absolutely positioned at 100% × 100%; `width`/`height` are ignored. Pair with
|
|
19
|
+
* `objectFit` to control cropping.
|
|
20
|
+
*/
|
|
21
|
+
fill?: boolean;
|
|
22
|
+
/** `object-fit` for the rendered image (handy with `fill`). */
|
|
23
|
+
objectFit?: CSSProperties['objectFit'];
|
|
24
|
+
/**
|
|
25
|
+
* Mark this as a high-priority (LCP) image: eager load + `fetchpriority="high"` and no lazy
|
|
26
|
+
* loading. Use for above-the-fold hero images; everything else stays lazy. Default `false`.
|
|
27
|
+
*/
|
|
28
|
+
priority?: boolean;
|
|
29
|
+
/** Placeholder shown until the image loads: `'empty'` (default) or `'blur'` (needs `blurDataURL`). */
|
|
30
|
+
placeholder?: 'empty' | 'blur';
|
|
31
|
+
/** A tiny (base64) image shown blurred behind the image while it loads, when `placeholder="blur"`. */
|
|
32
|
+
blurDataURL?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A drop-in `<img>` replacement that prevents layout shift and lazy-loads by default. It reserves
|
|
37
|
+
* space from `width`/`height` (or fills its container with `fill`), decodes async, lazy-loads unless
|
|
38
|
+
* `priority`, and can fade in from a `blur` placeholder. This is a client-only component, there is
|
|
39
|
+
* no server-side resizing; pass an already-optimized `src` (Vite hashes imported assets for you).
|
|
40
|
+
*/
|
|
41
|
+
export function Image(props: ImageProps): ReactNode {
|
|
42
|
+
const {
|
|
43
|
+
src,
|
|
44
|
+
alt,
|
|
45
|
+
width,
|
|
46
|
+
height,
|
|
47
|
+
fill = false,
|
|
48
|
+
objectFit,
|
|
49
|
+
priority = false,
|
|
50
|
+
placeholder = 'empty',
|
|
51
|
+
blurDataURL,
|
|
52
|
+
style,
|
|
53
|
+
onLoad,
|
|
54
|
+
...rest
|
|
55
|
+
} = props;
|
|
56
|
+
|
|
57
|
+
const [loaded, setLoaded] = useState(false);
|
|
58
|
+
const showBlur = placeholder === 'blur' && blurDataURL !== undefined && !loaded;
|
|
59
|
+
|
|
60
|
+
const layoutStyle: CSSProperties = fill
|
|
61
|
+
? { position: 'absolute', inset: 0, width: '100%', height: '100%' }
|
|
62
|
+
: {};
|
|
63
|
+
const blurStyle: CSSProperties = showBlur
|
|
64
|
+
? {
|
|
65
|
+
backgroundImage: `url(${blurDataURL})`,
|
|
66
|
+
backgroundSize: 'cover',
|
|
67
|
+
backgroundPosition: 'center',
|
|
68
|
+
filter: 'blur(20px)',
|
|
69
|
+
}
|
|
70
|
+
: {};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<img
|
|
74
|
+
{...rest}
|
|
75
|
+
src={src}
|
|
76
|
+
alt={alt}
|
|
77
|
+
width={fill ? undefined : width}
|
|
78
|
+
height={fill ? undefined : height}
|
|
79
|
+
loading={priority ? 'eager' : 'lazy'}
|
|
80
|
+
decoding="async"
|
|
81
|
+
fetchPriority={priority ? 'high' : 'auto'}
|
|
82
|
+
onLoad={(event) => {
|
|
83
|
+
setLoaded(true);
|
|
84
|
+
onLoad?.(event);
|
|
85
|
+
}}
|
|
86
|
+
style={{ ...layoutStyle, objectFit, ...blurStyle, ...style }}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|