toiljs 0.0.7 → 0.0.8

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.
Files changed (65) hide show
  1. package/build/cli/.tsbuildinfo +1 -1
  2. package/build/cli/configure.d.ts +1 -0
  3. package/build/cli/configure.js +83 -18
  4. package/build/cli/create.d.ts +1 -0
  5. package/build/cli/create.js +14 -3
  6. package/build/cli/features.d.ts +2 -0
  7. package/build/cli/features.js +22 -0
  8. package/build/cli/index.js +8 -0
  9. package/build/client/.tsbuildinfo +1 -1
  10. package/build/client/components/Form.d.ts +12 -0
  11. package/build/client/components/Form.js +23 -0
  12. package/build/client/components/Image.d.ts +13 -0
  13. package/build/client/components/Image.js +22 -0
  14. package/build/client/components/Script.d.ts +13 -0
  15. package/build/client/components/Script.js +68 -0
  16. package/build/client/index.d.ts +10 -2
  17. package/build/client/index.js +5 -1
  18. package/build/client/routing/Router.js +4 -4
  19. package/build/client/routing/action.d.ts +17 -0
  20. package/build/client/routing/action.js +55 -0
  21. package/build/client/routing/hooks.d.ts +1 -0
  22. package/build/client/routing/hooks.js +4 -1
  23. package/build/client/routing/loader.d.ts +8 -2
  24. package/build/client/routing/loader.js +75 -24
  25. package/build/compiler/.tsbuildinfo +1 -1
  26. package/build/compiler/config.d.ts +2 -0
  27. package/build/compiler/config.js +1 -0
  28. package/build/compiler/generate.js +2 -0
  29. package/build/compiler/image-report.d.ts +2 -0
  30. package/build/compiler/image-report.js +62 -0
  31. package/build/compiler/vite.js +8 -0
  32. package/examples/basic/client/components/Header.tsx +38 -0
  33. package/examples/basic/client/components/HoneycombBackground.tsx +86 -18
  34. package/examples/basic/client/global-error.tsx +2 -2
  35. package/examples/basic/client/layout.tsx +2 -33
  36. package/examples/basic/client/public/images/test_image.webp +0 -0
  37. package/examples/basic/client/routes/index.tsx +8 -1
  38. package/examples/basic/client/routes/loader-demo/index.tsx +24 -1
  39. package/examples/basic/client/routes/test.tsx +8 -0
  40. package/examples/basic/client/styles/main.css +48 -1
  41. package/package.json +8 -6
  42. package/presets/eslint.js +4 -4
  43. package/src/cli/configure.ts +98 -17
  44. package/src/cli/create.ts +18 -2
  45. package/src/cli/features.ts +32 -0
  46. package/src/cli/index.ts +9 -0
  47. package/src/client/components/Form.tsx +65 -0
  48. package/src/client/components/Image.tsx +89 -0
  49. package/src/client/components/Script.tsx +113 -0
  50. package/src/client/index.ts +15 -2
  51. package/src/client/routing/Router.tsx +17 -5
  52. package/src/client/routing/action.ts +122 -0
  53. package/src/client/routing/hooks.ts +18 -5
  54. package/src/client/routing/loader.ts +146 -35
  55. package/src/compiler/config.ts +9 -0
  56. package/src/compiler/generate.ts +3 -0
  57. package/src/compiler/image-report.ts +85 -0
  58. package/src/compiler/vite.ts +12 -0
  59. package/test/dom/Image.test.tsx +46 -0
  60. package/test/dom/Script.test.tsx +45 -0
  61. package/test/dom/action.test.tsx +129 -0
  62. package/test/dom/loader.test.tsx +121 -0
  63. package/test/dom/router-loading.test.tsx +44 -0
  64. package/test/features.test.ts +31 -0
  65. package/examples/basic/client/template.tsx +0 -7
@@ -4,6 +4,7 @@ const HEX_R = 34;
4
4
  const GAP = 3;
5
5
  const DRAW_R = HEX_R - GAP;
6
6
  const GLOW_DIST = 140;
7
+ const LOGO_SRC = '/images/logo.svg';
7
8
 
8
9
  function tracePath(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number) {
9
10
  ctx.beginPath();
@@ -43,6 +44,41 @@ function buildGrid(w: number, h: number): Array<{ x: number; y: number }> {
43
44
  return hexes;
44
45
  }
45
46
 
47
+ /** Samples logo colours per hex centre for use in border glow. */
48
+ function buildLogoColors(
49
+ img: HTMLImageElement,
50
+ hexes: Array<{ x: number; y: number }>,
51
+ w: number,
52
+ h: number,
53
+ ): Array<[number, number, number]> | null {
54
+ const lc = document.createElement('canvas');
55
+
56
+ lc.width = w;
57
+ lc.height = h;
58
+
59
+ const lctx = lc.getContext('2d');
60
+
61
+ if (!lctx) return null;
62
+
63
+ // Draw logo large + blurred, roughly where the hero logo sits in the viewport
64
+ const size = 700;
65
+ const cx = w / 2;
66
+ const cy = h * 0.42;
67
+
68
+ lctx.filter = 'blur(90px)';
69
+ lctx.drawImage(img, cx - size / 2, cy - size / 2, size, size);
70
+ lctx.filter = 'none';
71
+
72
+ // Sample one pixel per hex centre so we can use logo colours for border glow
73
+ return hexes.map(({ x, y }) => {
74
+ const px = Math.round(Math.max(0, Math.min(w - 1, x)));
75
+ const py = Math.round(Math.max(0, Math.min(h - 1, y)));
76
+ const d = lctx.getImageData(px, py, 1, 1).data;
77
+
78
+ return [d[0], d[1], d[2]] as [number, number, number];
79
+ });
80
+ }
81
+
46
82
  export default function HoneycombBackground() {
47
83
  const canvasRef = useRef<HTMLCanvasElement>(null);
48
84
  const mouse = useRef({ x: -9999, y: -9999 });
@@ -58,19 +94,42 @@ export default function HoneycombBackground() {
58
94
 
59
95
  const dpr = window.devicePixelRatio || 1;
60
96
  let hexes: Array<{ x: number; y: number }> = [];
97
+ let hexColors: Array<[number, number, number]> = [];
61
98
  let raf: number;
62
99
 
100
+ const img = new Image();
101
+
102
+ img.onload = () => {
103
+ const colors = buildLogoColors(img, hexes, window.innerWidth, window.innerHeight);
104
+
105
+ if (colors) {
106
+ hexColors = colors;
107
+ }
108
+ };
109
+
110
+ img.src = LOGO_SRC;
111
+
63
112
  function resize() {
64
113
  if (!canvas || !ctx) return;
65
114
 
66
115
  const w = window.innerWidth;
67
116
  const h = window.innerHeight;
117
+
68
118
  canvas.width = w * dpr;
69
119
  canvas.height = h * dpr;
70
120
  canvas.style.width = `${w}px`;
71
121
  canvas.style.height = `${h}px`;
72
122
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
73
123
  hexes = buildGrid(w, h);
124
+
125
+ // Rebuild logo colours if image is already loaded
126
+ if (img.complete && img.naturalWidth > 0) {
127
+ const colors = buildLogoColors(img, hexes, w, h);
128
+
129
+ if (colors) {
130
+ hexColors = colors;
131
+ }
132
+ }
74
133
  }
75
134
 
76
135
  function draw() {
@@ -78,36 +137,53 @@ export default function HoneycombBackground() {
78
137
 
79
138
  const w = window.innerWidth;
80
139
  const h = window.innerHeight;
140
+
81
141
  ctx.clearRect(0, 0, w, h);
82
142
 
83
143
  const mx = mouse.current.x;
84
144
  const my = mouse.current.y;
85
145
 
86
- for (const { x, y } of hexes) {
146
+ for (let i = 0; i < hexes.length; i++) {
147
+ const hex = hexes[i];
148
+
149
+ if (!hex) continue;
150
+
151
+ const { x, y } = hex;
87
152
  const dist = Math.hypot(x - mx, y - my);
88
153
  const t = Math.max(0, 1 - dist / GLOW_DIST);
89
154
  const ease = t * t * (3 - 2 * t);
90
155
 
156
+ // Base fill
91
157
  tracePath(ctx, x, y, DRAW_R);
92
-
93
158
  ctx.fillStyle = 'rgba(255,255,255,0.018)';
94
159
  ctx.fill();
95
160
 
96
- if (ease > 0) {
97
- ctx.fillStyle = `rgba(72,148,255,${ease * 0.025})`;
98
- ctx.fill();
99
- }
100
161
 
162
+
163
+ // Base border
164
+ tracePath(ctx, x, y, DRAW_R);
101
165
  ctx.strokeStyle = 'rgba(255,255,255,0.055)';
102
166
  ctx.lineWidth = 1;
103
167
  ctx.stroke();
104
168
 
169
+ // Glow border using logo-sampled colour
105
170
  if (ease > 0) {
171
+ const col = hexColors[i];
172
+ const r = col ? col[0] : 120;
173
+ const g = col ? col[1] : 180;
174
+ const b = col ? col[2] : 255;
175
+
176
+ // If the logo has colour here, use it; otherwise fall back to a soft white
177
+ const bright = r + g + b;
178
+ const fr = bright > 30 ? r : 120;
179
+ const fg = bright > 30 ? g : 180;
180
+ const fb = bright > 30 ? b : 255;
181
+
106
182
  ctx.save();
107
- ctx.shadowColor = `rgba(72,148,255,${ease * 0.2})`;
183
+ ctx.shadowColor = `rgba(${fr},${fg},${fb},${ease * 0.25})`;
108
184
  ctx.shadowBlur = 8 * ease;
109
- ctx.strokeStyle = `rgba(120,180,255,${ease * 0.2})`;
110
- ctx.lineWidth = 1 + ease * 0.4;
185
+ ctx.strokeStyle = `rgba(${fr},${fg},${fb},${ease * 0.18})`;
186
+ ctx.lineWidth = 1 + ease * 0.5;
111
187
  ctx.stroke();
112
188
  ctx.restore();
113
189
  }
@@ -148,15 +224,7 @@ export default function HoneycombBackground() {
148
224
  return (
149
225
  <canvas
150
226
  ref={canvasRef}
151
- style={{
152
- position: 'fixed',
153
- inset: 0,
154
- width: '100%',
155
- height: '100%',
156
- pointerEvents: 'none',
157
- zIndex: 0,
158
- }}
227
+ className="honeycomb-canvas"
159
228
  />
160
229
  );
161
230
  }
162
-
@@ -2,9 +2,9 @@
2
2
  // also catches errors thrown while rendering the layout itself — the last line of defense.
3
3
  export default function GlobalError({ error, reset }: Toil.RouteErrorProps) {
4
4
  return (
5
- <main style={{ padding: 48, fontFamily: 'system-ui', textAlign: 'center' }}>
5
+ <main className="global-error">
6
6
  <h1>Something went wrong</h1>
7
- <p style={{ opacity: 0.7 }}>{error.message}</p>
7
+ <p className="global-error-message">{error.message}</p>
8
8
  <button type="button" onClick={reset}>
9
9
  Try again
10
10
  </button>
@@ -1,13 +1,8 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import Footer from './components/Footer';
3
+ import Header from './components/Header';
3
4
  import HoneycombBackground from './components/HoneycombBackground';
4
5
 
5
- const GitHubIcon = () => (
6
- <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
7
- <path d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.604-3.369-1.341-3.369-1.341-.454-1.154-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0 1 12 6.836a9.59 9.59 0 0 1 2.504.337c1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.202 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.579.688.481C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z" />
8
- </svg>
9
- );
10
-
11
6
  export default function Layout({ children }: { children?: ReactNode }) {
12
7
  return (
13
8
  <div className="app">
@@ -17,33 +12,7 @@ export default function Layout({ children }: { children?: ReactNode }) {
17
12
  title="ToilJS"
18
13
  meta={[{ name: 'description', content: 'The most performant React framework.' }]}
19
14
  />
20
- <header className="nav">
21
- <Toil.Link href="/" className="nav-logo">
22
- <img src="images/logo.svg" alt="ToilJS" width={28} height={28} />
23
- <span>ToilJS</span>
24
- </Toil.Link>
25
-
26
- <nav className="nav-center">
27
- <Toil.NavLink href="/" end className="nav-center-link">
28
- Home
29
- </Toil.NavLink>
30
- <Toil.NavLink href="/get-started" className="nav-center-link">
31
- Get Started
32
- </Toil.NavLink>
33
- </nav>
34
-
35
- <nav className="nav-links">
36
- <Toil.Link href="https://toil.org/docs">Docs</Toil.Link>
37
- <a
38
- href="https://github.com/btc-vision/toiljs"
39
- target="_blank"
40
- rel="noopener noreferrer"
41
- className="nav-github">
42
- <GitHubIcon />
43
- GitHub
44
- </a>
45
- </nav>
46
- </header>
15
+ <Header />
47
16
 
48
17
  <main className="content">{children}</main>
49
18
 
@@ -43,7 +43,14 @@ export default function Home() {
43
43
  <section className="hero">
44
44
  <div className="hero-logo">
45
45
  <img src="images/logo.svg" className="hero-logo-glow" alt="" aria-hidden="true" width={96} height={96} />
46
- <img src="images/logo.svg" className="hero-logo-img" alt="ToilJS" width={96} height={96} />
46
+ <Toil.Image
47
+ src="images/logo.svg"
48
+ className="hero-logo-img"
49
+ alt="ToilJS"
50
+ width={96}
51
+ height={96}
52
+ priority
53
+ />
47
54
  </div>
48
55
 
49
56
  <h1 className="hero-title">ToilJS</h1>
@@ -7,8 +7,14 @@ export const loader = async ({ searchParams }: Toil.LoaderArgs) => {
7
7
  return { loadedAt: new Date().toISOString(), q: searchParams.get('q') };
8
8
  };
9
9
 
10
+ // Cache this route's data for 10s: revisiting within 10s is instant (no 2s wait); after that it
11
+ // refetches on navigation. Use `false` to cache forever, or omit for the default (refetch every nav).
12
+ export const revalidate: Toil.Revalidate = 10;
13
+
10
14
  export default function LoaderDemo() {
11
- const data = Toil.useLoaderData<{ loadedAt: string; q: string | null }>();
15
+ // Pass the loader to infer the data type from its return no generics, no restating the shape.
16
+ const data = Toil.useLoaderData(loader);
17
+ const router = Toil.useRouter();
12
18
  return (
13
19
  <main>
14
20
  <h1>Loader demo</h1>
@@ -16,6 +22,23 @@ export default function LoaderDemo() {
16
22
  Data loaded before render (no <code>useEffect</code>): <code>{data.loadedAt}</code>
17
23
  {data.q !== null ? ` · q=${data.q}` : ''}
18
24
  </p>
25
+ <p>
26
+ <button type="button" onClick={() => { router.revalidate(); }}>
27
+ Revalidate (refetch)
28
+ </button>
29
+ </p>
30
+ {/* The write half: an action runs on submit, then revalidates this route's loader so
31
+ `loadedAt` above updates — read → write → revalidate, no manual refetch. */}
32
+ <Toil.Form action={async (form) => { await wait(500); console.log('saved', form.get('note')); }}>
33
+ {({ pending }) => (
34
+ <>
35
+ <input name="note" placeholder="Leave a note" disabled={pending} />
36
+ <button type="submit" disabled={pending}>
37
+ {pending ? 'Saving…' : 'Save & revalidate'}
38
+ </button>
39
+ </>
40
+ )}
41
+ </Toil.Form>
19
42
  <Toil.Link href="/">Back home</Toil.Link>
20
43
  </main>
21
44
  );
@@ -0,0 +1,8 @@
1
+ export default function TestPage() {
2
+ return (
3
+ <div className="test-page">
4
+ <img src="/images/test_image.webp" alt="Test" className="test-page-image" />
5
+ </div>
6
+ );
7
+ }
8
+
@@ -291,13 +291,26 @@ a:hover { color: var(--accent3); }
291
291
  background: #131d2e;
292
292
  }
293
293
 
294
+ /* ── Global Error ── */
295
+ .global-error {
296
+ padding: 3rem;
297
+ font-family: system-ui;
298
+ text-align: center;
299
+ }
300
+
301
+ .global-error-message {
302
+ opacity: 0.7;
303
+ }
304
+
294
305
  /* ── Footer ── */
295
306
  .footer {
296
- text-align: center;
297
307
  padding: 1.25rem;
298
308
  font-size: 0.82rem;
299
309
  color: var(--muted);
300
310
  border-top: 1px solid var(--border);
311
+ background: var(--bg);
312
+ position: relative;
313
+ z-index: 1;
301
314
  }
302
315
 
303
316
  /* ── Misc ── */
@@ -503,3 +516,37 @@ code {
503
516
  .spinner { animation-duration: 0s; }
504
517
  }
505
518
 
519
+ /* ── Global Error ── */
520
+ .global-error {
521
+ padding: 3rem;
522
+ font-family: system-ui;
523
+ text-align: center;
524
+ }
525
+
526
+ .global-error-message {
527
+ opacity: 0.7;
528
+ }
529
+
530
+ /* ── Test page ── */
531
+ .test-page {
532
+ display: flex;
533
+ justify-content: center;
534
+ align-items: center;
535
+ min-height: 100%;
536
+ }
537
+
538
+ .test-page-image {
539
+ max-width: 100%;
540
+ max-height: 80vh;
541
+ border-radius: 8px;
542
+ }
543
+
544
+ /* ── Honeycomb background ── */
545
+ .honeycomb-canvas {
546
+ position: fixed;
547
+ inset: 0;
548
+ width: 100%;
549
+ height: 100%;
550
+ pointer-events: none;
551
+ z-index: 0;
552
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.7",
4
+ "version": "0.0.8",
5
5
  "author": "Dacely",
6
6
  "description": "todo",
7
7
  "engines": {
@@ -95,16 +95,18 @@
95
95
  "@btc-vision/as-loader": "^0.0.0",
96
96
  "@btc-vision/hyper-express": "^6.17.4",
97
97
  "@clack/prompts": "^1.5.0",
98
- "@eslint-react/eslint-plugin": "^4.2.3",
98
+ "@eslint-react/eslint-plugin": "^5.8.8",
99
99
  "@eslint/js": "^10.0.1",
100
100
  "@typescript-eslint/utils": "^8.60.0",
101
101
  "@vitejs/plugin-react": "^6.0.2",
102
- "eslint-plugin-react-hooks": "^7.1.0-canary-ab18f33d-20260220",
102
+ "eslint-plugin-react-hooks": "^7.1.1",
103
103
  "eslint-plugin-react-refresh": "^0.5.2",
104
104
  "picocolors": "^1.1.1",
105
+ "sharp": "^0.34.5",
105
106
  "toilscript": "^0.1.4",
106
107
  "typescript-eslint": "^8.60.0",
107
108
  "vite": "^8.0.14",
109
+ "vite-imagetools": "^10.0.0",
108
110
  "vite-plugin-node-polyfills": "^0.28.0"
109
111
  },
110
112
  "peerDependencies": {
@@ -131,10 +133,10 @@
131
133
  "@types/react-dom": "^19.2.3",
132
134
  "@vitest/coverage-v8": "^4.1.7",
133
135
  "@vitest/ui": "^4.1.7",
134
- "eslint": "^10.2.0",
135
- "jsdom": "^26.1.0",
136
+ "eslint": "^10.4.1",
137
+ "jsdom": "^29.1.1",
136
138
  "micromatch": "^4.0.8",
137
- "prettier": "^3.8.1",
139
+ "prettier": "^3.8.3",
138
140
  "react": "^19.2.6",
139
141
  "react-dom": "^19.2.6",
140
142
  "typedoc": "^0.28.19",
package/presets/eslint.js CHANGED
@@ -30,12 +30,12 @@ export default tseslint.config(
30
30
  },
31
31
  rules: {
32
32
  ...reactHooks.configs.recommended.rules,
33
- // Route files conventionally export a `loader` alongside the default component; the toil
34
- // compiler consumes it at runtime. Allow it (plus primitive constants) so Fast Refresh
35
- // doesn't flag the pattern.
33
+ // Route files conventionally export a `loader` / `revalidate` alongside the default
34
+ // component; the toil compiler consumes them at runtime. Allow them (plus primitive
35
+ // constants) so Fast Refresh doesn't flag the pattern.
36
36
  'react-refresh/only-export-components': [
37
37
  'warn',
38
- { allowConstantExport: true, allowExportNames: ['loader'] },
38
+ { allowConstantExport: true, allowExportNames: ['loader', 'revalidate'] },
39
39
  ],
40
40
  'no-undef': 'off',
41
41
  '@typescript-eslint/no-unused-vars': 'off',
@@ -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 {
@@ -224,13 +276,18 @@ export async function runConfigure(opts: ConfigureOptions): Promise<void> {
224
276
  tailwind: detectTailwind(deps),
225
277
  };
226
278
 
227
- const nonInteractive = opts.preprocessor !== undefined || opts.tailwind !== undefined;
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
- if (target.preprocessor === current.preprocessor && target.tailwind === current.tailwind) {
247
- outro('No changes your styling setup is already up to date.');
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
- s.stop('Updated stylesheets, entry imports, and package.json');
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
- const pm = await detectPackageManager(root);
257
- if (opts.install === false) {
258
- note(`${pc.cyan(`${pm} install`)} to sync the dependency changes.`, 'Next step');
259
- } else {
260
- const i = spinner();
261
- i.start(`Syncing dependencies with ${pm}`);
262
- try {
263
- await run(pm, ['install'], root);
264
- i.stop('Dependencies synced');
265
- } catch {
266
- i.stop(pc.yellow(`Could not run \`${pm} install\` — run it yourself to finish`));
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
- note(describe(current, target), 'Styling updated');
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');
271
352
  outro(`Reconfigured — restart \`${accent('toiljs dev')}\` to pick up the changes.`);
272
353
  }
package/src/cli/create.ts CHANGED
@@ -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({});\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',
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",
@@ -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 });