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.
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/configure.d.ts +1 -0
- package/build/cli/configure.js +83 -18
- package/build/cli/create.d.ts +1 -0
- package/build/cli/create.js +14 -3
- 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/index.d.ts +10 -2
- package/build/client/index.js +5 -1
- package/build/client/routing/Router.js +4 -4
- 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 +4 -1
- package/build/client/routing/loader.d.ts +8 -2
- package/build/client/routing/loader.js +75 -24
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +2 -0
- package/build/compiler/config.js +1 -0
- package/build/compiler/generate.js +2 -0
- package/build/compiler/image-report.d.ts +2 -0
- package/build/compiler/image-report.js +62 -0
- package/build/compiler/vite.js +8 -0
- 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 +2 -2
- package/examples/basic/client/layout.tsx +2 -33
- package/examples/basic/client/public/images/test_image.webp +0 -0
- package/examples/basic/client/routes/index.tsx +8 -1
- package/examples/basic/client/routes/loader-demo/index.tsx +24 -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 +4 -4
- package/src/cli/configure.ts +98 -17
- package/src/cli/create.ts +18 -2
- package/src/cli/features.ts +32 -0
- package/src/cli/index.ts +9 -0
- 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/index.ts +15 -2
- package/src/client/routing/Router.tsx +17 -5
- package/src/client/routing/action.ts +122 -0
- package/src/client/routing/hooks.ts +18 -5
- package/src/client/routing/loader.ts +146 -35
- package/src/compiler/config.ts +9 -0
- package/src/compiler/generate.ts +3 -0
- package/src/compiler/image-report.ts +85 -0
- package/src/compiler/vite.ts +12 -0
- 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/loader.test.tsx +121 -0
- package/test/dom/router-loading.test.tsx +44 -0
- package/test/features.test.ts +31 -0
- 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 (
|
|
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(
|
|
183
|
+
ctx.shadowColor = `rgba(${fr},${fg},${fb},${ease * 0.25})`;
|
|
108
184
|
ctx.shadowBlur = 8 * ease;
|
|
109
|
-
ctx.strokeStyle = `rgba(
|
|
110
|
-
ctx.lineWidth = 1 + ease * 0.
|
|
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
|
-
|
|
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
|
|
5
|
+
<main className="global-error">
|
|
6
6
|
<h1>Something went wrong</h1>
|
|
7
|
-
<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
|
-
<
|
|
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
|
|
|
Binary file
|
|
@@ -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
|
-
<
|
|
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
|
-
|
|
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
|
);
|
|
@@ -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.
|
|
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": "^
|
|
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.
|
|
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.
|
|
135
|
-
"jsdom": "^
|
|
136
|
+
"eslint": "^10.4.1",
|
|
137
|
+
"jsdom": "^29.1.1",
|
|
136
138
|
"micromatch": "^4.0.8",
|
|
137
|
-
"prettier": "^3.8.
|
|
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
|
|
34
|
-
// compiler consumes
|
|
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',
|
package/src/cli/configure.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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({
|
|
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 });
|