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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
clearLoaderData,
|
|
6
|
+
invalidateLoaderData,
|
|
7
|
+
loaderKey,
|
|
8
|
+
readRouteData,
|
|
9
|
+
type LoaderData,
|
|
10
|
+
} from '../../src/client/routing/loader';
|
|
11
|
+
import type { Revalidate } from '../../src/client/routing/loader';
|
|
12
|
+
import type { RouteDef } from '../../src/client/types';
|
|
13
|
+
|
|
14
|
+
/** Reads route data, awaiting the suspending promise once if it's pending. */
|
|
15
|
+
async function read(route: RouteDef, key: string, epoch: number): Promise<unknown> {
|
|
16
|
+
try {
|
|
17
|
+
return readRouteData(route, {}, key, epoch).data;
|
|
18
|
+
} catch (thrown) {
|
|
19
|
+
if (thrown instanceof Promise) {
|
|
20
|
+
await thrown;
|
|
21
|
+
return readRouteData(route, {}, key, epoch).data;
|
|
22
|
+
}
|
|
23
|
+
throw thrown;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A route whose `load()` count tells us how many times the loader actually ran. */
|
|
28
|
+
function makeRoute(revalidate?: Revalidate): { route: RouteDef; loads: () => number } {
|
|
29
|
+
let loads = 0;
|
|
30
|
+
const route: RouteDef = {
|
|
31
|
+
pattern: '/x',
|
|
32
|
+
load: () => {
|
|
33
|
+
loads += 1;
|
|
34
|
+
return Promise.resolve({
|
|
35
|
+
default: () => null,
|
|
36
|
+
loader: () => ({ n: loads }),
|
|
37
|
+
revalidate,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return { route, loads: () => loads };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
clearLoaderData();
|
|
46
|
+
});
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
vi.useRealTimers();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('loader caching', () => {
|
|
52
|
+
it('reuses cached data on re-read within the same navigation (loader runs once)', async () => {
|
|
53
|
+
const { route, loads } = makeRoute();
|
|
54
|
+
const key = loaderKey('/x', '');
|
|
55
|
+
await read(route, key, 1);
|
|
56
|
+
await read(route, key, 1);
|
|
57
|
+
expect(loads()).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('a route with no loader stays cached across navigations (never re-suspends)', async () => {
|
|
61
|
+
let loads = 0;
|
|
62
|
+
const route: RouteDef = {
|
|
63
|
+
pattern: '/p',
|
|
64
|
+
load: () => {
|
|
65
|
+
loads += 1;
|
|
66
|
+
return Promise.resolve({ default: () => null });
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const key = loaderKey('/p', '');
|
|
70
|
+
await read(route, key, 1);
|
|
71
|
+
await read(route, key, 2);
|
|
72
|
+
await read(route, key, 3);
|
|
73
|
+
expect(loads).toBe(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('refetches on a new navigation under the default policy', async () => {
|
|
77
|
+
const { route, loads } = makeRoute();
|
|
78
|
+
const key = loaderKey('/x', '');
|
|
79
|
+
await read(route, key, 1);
|
|
80
|
+
await read(route, key, 2);
|
|
81
|
+
expect(loads()).toBe(2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('revalidate=false caches across navigations', async () => {
|
|
85
|
+
const { route, loads } = makeRoute(false);
|
|
86
|
+
const key = loaderKey('/x', '');
|
|
87
|
+
await read(route, key, 1);
|
|
88
|
+
await read(route, key, 2);
|
|
89
|
+
expect(loads()).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('numeric revalidate caches until the staleTime elapses', async () => {
|
|
93
|
+
vi.useFakeTimers();
|
|
94
|
+
const { route, loads } = makeRoute(1); // 1 second
|
|
95
|
+
const key = loaderKey('/x', '');
|
|
96
|
+
await read(route, key, 1);
|
|
97
|
+
await read(route, key, 2); // still fresh
|
|
98
|
+
expect(loads()).toBe(1);
|
|
99
|
+
vi.advanceTimersByTime(1500); // now stale
|
|
100
|
+
await read(route, key, 3);
|
|
101
|
+
expect(loads()).toBe(2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('invalidateLoaderData(href) forces a refetch of that route', async () => {
|
|
105
|
+
const { route, loads } = makeRoute(false);
|
|
106
|
+
const key = loaderKey('/x', '');
|
|
107
|
+
await read(route, key, 1);
|
|
108
|
+
invalidateLoaderData('/x');
|
|
109
|
+
await read(route, key, 1);
|
|
110
|
+
expect(loads()).toBe(2);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('useLoaderData type inference', () => {
|
|
115
|
+
it('LoaderData<typeof loader> resolves to the loader return type', () => {
|
|
116
|
+
const loader = async () => Promise.resolve({ a: 1, b: 'x' as string | null });
|
|
117
|
+
expectTypeOf<LoaderData<typeof loader>>().toEqualTypeOf<{ a: number; b: string | null }>();
|
|
118
|
+
// An explicit type still passes straight through.
|
|
119
|
+
expectTypeOf<LoaderData<{ id: number }>>().toEqualTypeOf<{ id: number }>();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, cleanup, render, waitFor } from '@testing-library/react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { navigate } from '../../src/client/navigation/navigation';
|
|
6
|
+
import { Router } from '../../src/client/routing/Router';
|
|
7
|
+
import type { RouteDef } from '../../src/client/types';
|
|
8
|
+
|
|
9
|
+
afterEach(cleanup);
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
window.history.replaceState({}, '', '/');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const routes: RouteDef[] = [
|
|
15
|
+
{
|
|
16
|
+
pattern: '/',
|
|
17
|
+
load: () => Promise.resolve({ default: () => <div>HOME</div> }),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
// Page chunk never resolves, so the route stays suspended — exercising the fallback path.
|
|
21
|
+
pattern: '/slow',
|
|
22
|
+
load: () => new Promise<{ default: () => null }>(() => undefined),
|
|
23
|
+
loading: () => Promise.resolve({ default: () => <div>LOADING</div> }),
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
describe('Router loading fallback', () => {
|
|
28
|
+
it("shows the route's loading.tsx immediately when navigating to a suspending route", async () => {
|
|
29
|
+
const { findByText, queryByText } = render(<Router routes={routes} />);
|
|
30
|
+
await findByText('HOME');
|
|
31
|
+
|
|
32
|
+
// A route with a `loading.tsx` keys its Suspense boundary per URL, so even though navigation
|
|
33
|
+
// runs in a transition the fallback appears immediately (it isn't suppressed / frozen).
|
|
34
|
+
act(() => {
|
|
35
|
+
navigate('/slow');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await waitFor(() => {
|
|
39
|
+
expect(queryByText('LOADING')).not.toBeNull();
|
|
40
|
+
});
|
|
41
|
+
// The keyed boundary remounts for the new route, so the previous page is gone (not frozen).
|
|
42
|
+
expect(queryByText('HOME')).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
package/test/features.test.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
defaultConfigSource,
|
|
4
5
|
detectPreprocessor,
|
|
5
6
|
detectTailwind,
|
|
6
7
|
packageDiff,
|
|
7
8
|
preprocessorForExt,
|
|
8
9
|
requiredPackages,
|
|
10
|
+
setConfigImages,
|
|
9
11
|
setStyleImports,
|
|
10
12
|
styleEntry,
|
|
11
13
|
styleImportLines,
|
|
@@ -109,3 +111,32 @@ describe('detect from dependencies', () => {
|
|
|
109
111
|
expect(detectTailwind({ react: '^19' })).toBe(false);
|
|
110
112
|
});
|
|
111
113
|
});
|
|
114
|
+
|
|
115
|
+
describe('setConfigImages / defaultConfigSource', () => {
|
|
116
|
+
it('flips an existing images flag', () => {
|
|
117
|
+
const src = 'export default defineConfig({\n client: {\n images: true,\n },\n});\n';
|
|
118
|
+
expect(setConfigImages(src, false)).toContain('images: false');
|
|
119
|
+
expect(setConfigImages(src, false)).not.toContain('images: true');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('adds images to an existing client block', () => {
|
|
123
|
+
const out = setConfigImages('export default defineConfig({ client: { base: "/" } });', false);
|
|
124
|
+
expect(out).toContain('images: false');
|
|
125
|
+
expect(out).toContain('base: "/"');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('adds a client block to a bare config', () => {
|
|
129
|
+
const out = setConfigImages('export default defineConfig({});', false);
|
|
130
|
+
expect(out).toContain('client: { images: false }');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('returns null when the shape is unrecognized', () => {
|
|
134
|
+
expect(setConfigImages('const x = 1;', false)).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('round-trips through defaultConfigSource', () => {
|
|
138
|
+
const src = defaultConfigSource(false);
|
|
139
|
+
expect(src).toContain('images: false');
|
|
140
|
+
expect(setConfigImages(src, true)).toContain('images: true');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
// Like `layout.tsx`, but re-mounted on every navigation (keyed by pathname) instead of persisting.
|
|
4
|
-
// Use it for per-navigation effects — enter animations, resetting state, replaying transitions.
|
|
5
|
-
export default function Template({ children }: { children?: ReactNode }) {
|
|
6
|
-
return <div className="route-transition">{children}</div>;
|
|
7
|
-
}
|