toiljs 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -1
- package/assets/logo.svg +37 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/configure.js +10 -4
- package/build/cli/create.js +60 -32
- package/build/cli/diagnostics.d.ts +55 -0
- package/build/cli/diagnostics.js +333 -0
- package/build/cli/doctor.d.ts +6 -0
- package/build/cli/doctor.js +249 -0
- package/build/cli/index.js +26 -0
- package/build/cli/proc.d.ts +5 -0
- package/build/cli/proc.js +20 -0
- package/build/cli/ui.d.ts +1 -0
- package/build/cli/ui.js +1 -0
- package/build/cli/update.d.ts +7 -0
- package/build/cli/update.js +117 -0
- package/build/cli/updates.d.ts +10 -0
- package/build/cli/updates.js +45 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/head/metadata.js +3 -1
- package/build/client/index.d.ts +5 -1
- package/build/client/index.js +2 -0
- package/build/client/navigation/navigation.js +1 -1
- package/build/client/routing/Router.js +2 -2
- package/build/client/search/search.d.ts +26 -0
- package/build/client/search/search.js +101 -0
- package/build/client/search/use-page-search.d.ts +8 -0
- package/build/client/search/use-page-search.js +21 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +35 -26
- package/build/compiler/index.d.ts +2 -0
- package/build/compiler/index.js +1 -0
- package/build/compiler/pages.d.ts +8 -0
- package/build/compiler/pages.js +37 -0
- package/build/compiler/plugin.js +3 -1
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +11 -5
- package/build/compiler/seo.js +10 -3
- package/build/compiler/vite.js +7 -0
- package/build/io/.tsbuildinfo +1 -1
- package/examples/basic/client/components/Header.tsx +43 -38
- package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
- package/examples/basic/client/layout.tsx +4 -1
- package/examples/basic/client/public/index.html +18 -16
- package/examples/basic/client/routes/(legal)/privacy.tsx +18 -0
- package/examples/basic/client/routes/(legal)/terms.tsx +15 -0
- package/examples/basic/client/routes/about.tsx +21 -19
- package/examples/basic/client/routes/blog/[id].tsx +26 -12
- package/examples/basic/client/routes/features/actions.tsx +67 -0
- package/examples/basic/client/routes/features/error/error.tsx +16 -0
- package/examples/basic/client/routes/features/error/index.tsx +27 -0
- package/examples/basic/client/routes/features/head.tsx +38 -0
- package/examples/basic/client/routes/features/index.tsx +83 -0
- package/examples/basic/client/routes/features/realtime.tsx +34 -0
- package/examples/basic/client/routes/features/script.tsx +31 -0
- package/examples/basic/client/routes/features/seo.tsx +39 -0
- package/examples/basic/client/routes/features/template/b.tsx +14 -0
- package/examples/basic/client/routes/features/template/index.tsx +20 -0
- package/examples/basic/client/routes/features/template/template.tsx +16 -0
- package/examples/basic/client/routes/files/[[...slug]].tsx +21 -0
- package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -0
- package/examples/basic/client/routes/gallery/index.tsx +42 -0
- package/examples/basic/client/routes/gallery/layout.tsx +13 -0
- package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -0
- package/examples/basic/client/routes/get-started.tsx +157 -84
- package/examples/basic/client/routes/index.tsx +137 -87
- package/examples/basic/client/routes/loader-demo/index.tsx +59 -50
- package/examples/basic/client/routes/search.tsx +61 -0
- package/examples/basic/client/routes/test.tsx +7 -8
- package/examples/basic/client/styles/main.css +624 -552
- package/examples/basic/client/toil.tsx +2 -4
- package/package.json +3 -2
- package/presets/eslint.js +10 -3
- package/src/cli/configure.ts +363 -353
- package/src/cli/create.ts +563 -530
- package/src/cli/diagnostics.ts +421 -0
- package/src/cli/doctor.ts +318 -0
- package/src/cli/features.ts +166 -160
- package/src/cli/index.ts +242 -211
- package/src/cli/proc.ts +30 -0
- package/src/cli/ui.ts +111 -103
- package/src/cli/update.ts +150 -0
- package/src/cli/updates.ts +69 -0
- package/src/client/components/Image.tsx +91 -89
- package/src/client/dev/error-overlay.tsx +193 -197
- package/src/client/head/metadata.ts +94 -92
- package/src/client/index.ts +79 -64
- package/src/client/navigation/Link.tsx +94 -100
- package/src/client/navigation/navigation.ts +215 -218
- package/src/client/routing/Router.tsx +210 -193
- package/src/client/routing/hooks.ts +110 -114
- package/src/client/routing/lazy.ts +77 -81
- package/src/client/search/search.ts +189 -0
- package/src/client/search/use-page-search.ts +73 -0
- package/src/compiler/config.ts +173 -171
- package/src/compiler/fonts.ts +89 -87
- package/src/compiler/generate.ts +378 -364
- package/src/compiler/image-report.ts +88 -85
- package/src/compiler/index.ts +2 -0
- package/src/compiler/pages.ts +70 -0
- package/src/compiler/plugin.ts +51 -47
- package/src/compiler/prerender.ts +152 -130
- package/src/compiler/routes.ts +132 -131
- package/src/compiler/seo.ts +381 -356
- package/src/compiler/vite.ts +155 -130
- package/src/io/FastSet.ts +99 -96
- package/test/configure.test.ts +94 -90
- package/test/doctor.test.ts +140 -0
- package/test/dom/Image.test.tsx +73 -46
- package/test/dom/Script.test.tsx +48 -45
- package/test/dom/action.test.tsx +146 -129
- package/test/dom/error-overlay.test.tsx +44 -44
- package/test/dom/loader.test.tsx +2 -2
- package/test/dom/revalidate.test.tsx +1 -1
- package/test/dom/route-head.test.tsx +35 -2
- package/test/dom/slot.test.tsx +131 -109
- package/test/dom/view-transitions.test.tsx +53 -51
- package/test/features.test.ts +149 -142
- package/test/fonts.test.ts +28 -26
- package/test/head.test.ts +45 -35
- package/test/metadata.test.ts +42 -41
- package/test/pages.test.ts +105 -0
- package/test/prerender.test.ts +54 -46
- package/test/search.test.ts +114 -0
- package/test/seo.test.ts +164 -142
- package/test/slot-layouts.test.ts +69 -0
- package/test/update.test.ts +44 -0
package/test/dom/slot.test.tsx
CHANGED
|
@@ -1,109 +1,131 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { act, cleanup, render } from '@testing-library/react';
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
-
import type { ReactNode } from 'react';
|
|
5
|
-
|
|
6
|
-
import { Slot } from '../../src/client/components/Slot';
|
|
7
|
-
import { navigate } from '../../src/client/navigation/navigation';
|
|
8
|
-
import { Router } from '../../src/client/routing/Router';
|
|
9
|
-
import { clearLoaderData } from '../../src/client/routing/loader';
|
|
10
|
-
import { SlotContext } from '../../src/client/routing/slot-context';
|
|
11
|
-
import type { RouteDef } from '../../src/client/types';
|
|
12
|
-
|
|
13
|
-
const layoutWithModal = () =>
|
|
14
|
-
Promise.resolve({
|
|
15
|
-
default: ({ children }: { children?: ReactNode }) => (
|
|
16
|
-
<div>
|
|
17
|
-
{children}
|
|
18
|
-
<Slot name="modal" />
|
|
19
|
-
</div>
|
|
20
|
-
),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(cleanup);
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
clearLoaderData();
|
|
26
|
-
window.history.replaceState({}, '', '/');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('Slot', () => {
|
|
30
|
-
it('renders the named slot element from context, or the fallback', () => {
|
|
31
|
-
const { getByText, queryByText } = render(
|
|
32
|
-
<SlotContext.Provider value={{ modal: <span>MODAL</span> }}>
|
|
33
|
-
<Slot name="modal" />
|
|
34
|
-
<Slot
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, cleanup, render } from '@testing-library/react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import { Slot } from '../../src/client/components/Slot';
|
|
7
|
+
import { navigate } from '../../src/client/navigation/navigation';
|
|
8
|
+
import { Router } from '../../src/client/routing/Router';
|
|
9
|
+
import { clearLoaderData } from '../../src/client/routing/loader';
|
|
10
|
+
import { SlotContext } from '../../src/client/routing/slot-context';
|
|
11
|
+
import type { RouteDef } from '../../src/client/types';
|
|
12
|
+
|
|
13
|
+
const layoutWithModal = () =>
|
|
14
|
+
Promise.resolve({
|
|
15
|
+
default: ({ children }: { children?: ReactNode }) => (
|
|
16
|
+
<div>
|
|
17
|
+
{children}
|
|
18
|
+
<Slot name="modal" />
|
|
19
|
+
</div>
|
|
20
|
+
),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(cleanup);
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
clearLoaderData();
|
|
26
|
+
window.history.replaceState({}, '', '/');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('Slot', () => {
|
|
30
|
+
it('renders the named slot element from context, or the fallback', () => {
|
|
31
|
+
const { getByText, queryByText } = render(
|
|
32
|
+
<SlotContext.Provider value={{ modal: <span>MODAL</span> }}>
|
|
33
|
+
<Slot name="modal" />
|
|
34
|
+
<Slot
|
|
35
|
+
name="missing"
|
|
36
|
+
fallback={<span>FB</span>}
|
|
37
|
+
/>
|
|
38
|
+
</SlotContext.Provider>,
|
|
39
|
+
);
|
|
40
|
+
expect(getByText('MODAL')).toBeTruthy();
|
|
41
|
+
expect(getByText('FB')).toBeTruthy();
|
|
42
|
+
expect(queryByText('missing')).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('Router parallel slots', () => {
|
|
47
|
+
const routes: RouteDef[] = [
|
|
48
|
+
{ pattern: '/', load: () => Promise.resolve({ default: () => <main>PAGE</main> }) },
|
|
49
|
+
];
|
|
50
|
+
// A layout that renders the page plus the "modal" slot.
|
|
51
|
+
const layout = () =>
|
|
52
|
+
Promise.resolve({
|
|
53
|
+
default: ({ children }: { children?: ReactNode }) => (
|
|
54
|
+
<div>
|
|
55
|
+
{children}
|
|
56
|
+
<Slot name="modal" />
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
});
|
|
60
|
+
const slots: Record<string, RouteDef[]> = {
|
|
61
|
+
modal: [
|
|
62
|
+
{ pattern: '/', load: () => Promise.resolve({ default: () => <aside>SLOT</aside> }) },
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
it('renders the main route and a matching slot together', async () => {
|
|
67
|
+
const { findByText } = render(
|
|
68
|
+
<Router
|
|
69
|
+
routes={routes}
|
|
70
|
+
layout={layout}
|
|
71
|
+
slots={slots}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
// Both the page and the parallel slot render for the same URL.
|
|
75
|
+
await findByText('PAGE');
|
|
76
|
+
await findByText('SLOT');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('intercepting routes', () => {
|
|
81
|
+
const routes: RouteDef[] = [
|
|
82
|
+
{
|
|
83
|
+
pattern: '/photo/:id',
|
|
84
|
+
load: () => Promise.resolve({ default: () => <main>PHOTO PAGE</main> }),
|
|
85
|
+
},
|
|
86
|
+
{ pattern: '/', load: () => Promise.resolve({ default: () => <main>FEED</main> }) },
|
|
87
|
+
];
|
|
88
|
+
const slots: Record<string, RouteDef[]> = {
|
|
89
|
+
modal: [
|
|
90
|
+
{
|
|
91
|
+
pattern: '/photo/:id',
|
|
92
|
+
intercept: true,
|
|
93
|
+
load: () => Promise.resolve({ default: () => <aside>PHOTO MODAL</aside> }),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// This test must run before any navigation so it observes a "hard" load (soft-nav state is false).
|
|
99
|
+
it('shows the full page on a hard load (no interception)', async () => {
|
|
100
|
+
window.history.replaceState({}, '', '/photo/1');
|
|
101
|
+
const { findByText, queryByText } = render(
|
|
102
|
+
<Router
|
|
103
|
+
routes={routes}
|
|
104
|
+
layout={layoutWithModal}
|
|
105
|
+
slots={slots}
|
|
106
|
+
/>,
|
|
107
|
+
);
|
|
108
|
+
await findByText('PHOTO PAGE');
|
|
109
|
+
expect(queryByText('PHOTO MODAL')).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('intercepts on soft navigation: modal overlays, previous page stays', async () => {
|
|
113
|
+
window.history.replaceState({}, '', '/');
|
|
114
|
+
const { findByText, queryByText } = render(
|
|
115
|
+
<Router
|
|
116
|
+
routes={routes}
|
|
117
|
+
layout={layoutWithModal}
|
|
118
|
+
slots={slots}
|
|
119
|
+
/>,
|
|
120
|
+
);
|
|
121
|
+
await findByText('FEED');
|
|
122
|
+
act(() => {
|
|
123
|
+
navigate('/photo/1');
|
|
124
|
+
});
|
|
125
|
+
// The intercepting slot route shows the modal…
|
|
126
|
+
await findByText('PHOTO MODAL');
|
|
127
|
+
// …while the main view keeps the previous page (the backdrop), not the full photo page.
|
|
128
|
+
expect(queryByText('FEED')).not.toBeNull();
|
|
129
|
+
expect(queryByText('PHOTO PAGE')).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -1,51 +1,53 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { navigate, setViewTransitions } from '../../src/client/navigation/navigation';
|
|
5
|
-
|
|
6
|
-
interface VTDoc {
|
|
7
|
-
startViewTransition?: (cb: () => void) => unknown;
|
|
8
|
-
}
|
|
9
|
-
const doc = document as Document & VTDoc;
|
|
10
|
-
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
setViewTransitions(false);
|
|
13
|
-
delete doc.startViewTransition;
|
|
14
|
-
vi.restoreAllMocks();
|
|
15
|
-
window.history.replaceState({}, '', '/');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('view transitions', () => {
|
|
19
|
-
function stubReducedMotion(matches: boolean): void {
|
|
20
|
-
window.matchMedia = vi
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { navigate, setViewTransitions } from '../../src/client/navigation/navigation';
|
|
5
|
+
|
|
6
|
+
interface VTDoc {
|
|
7
|
+
startViewTransition?: (cb: () => void) => unknown;
|
|
8
|
+
}
|
|
9
|
+
const doc = document as Document & VTDoc;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
setViewTransitions(false);
|
|
13
|
+
delete doc.startViewTransition;
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
window.history.replaceState({}, '', '/');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('view transitions', () => {
|
|
19
|
+
function stubReducedMotion(matches: boolean): void {
|
|
20
|
+
window.matchMedia = vi
|
|
21
|
+
.fn()
|
|
22
|
+
.mockReturnValue({ matches }) as unknown as typeof window.matchMedia;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
it('wraps navigation in startViewTransition when enabled and supported', () => {
|
|
26
|
+
const vt = vi.fn((cb: () => void) => {
|
|
27
|
+
cb();
|
|
28
|
+
});
|
|
29
|
+
doc.startViewTransition = vt;
|
|
30
|
+
stubReducedMotion(false);
|
|
31
|
+
setViewTransitions(true);
|
|
32
|
+
navigate('/a');
|
|
33
|
+
expect(vt).toHaveBeenCalledOnce();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('skips the view transition under prefers-reduced-motion', () => {
|
|
37
|
+
const vt = vi.fn();
|
|
38
|
+
doc.startViewTransition = vt;
|
|
39
|
+
stubReducedMotion(true);
|
|
40
|
+
setViewTransitions(true);
|
|
41
|
+
navigate('/b');
|
|
42
|
+
expect(vt).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not use view transitions when disabled', () => {
|
|
46
|
+
const vt = vi.fn();
|
|
47
|
+
doc.startViewTransition = vt;
|
|
48
|
+
stubReducedMotion(false);
|
|
49
|
+
setViewTransitions(false);
|
|
50
|
+
navigate('/c');
|
|
51
|
+
expect(vt).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
});
|
package/test/features.test.ts
CHANGED
|
@@ -1,142 +1,149 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
defaultConfigSource,
|
|
5
|
-
detectPreprocessor,
|
|
6
|
-
detectTailwind,
|
|
7
|
-
packageDiff,
|
|
8
|
-
preprocessorForExt,
|
|
9
|
-
requiredPackages,
|
|
10
|
-
setConfigImages,
|
|
11
|
-
setStyleImports,
|
|
12
|
-
styleEntry,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from '../src/cli/features';
|
|
16
|
-
|
|
17
|
-
const CSS: StyleFeatures = { preprocessor: 'css', tailwind: false };
|
|
18
|
-
const SASS_TW: StyleFeatures = { preprocessor: 'sass', tailwind: true };
|
|
19
|
-
|
|
20
|
-
describe('styleEntry / preprocessorForExt', () => {
|
|
21
|
-
it('maps preprocessors to stylesheet paths', () => {
|
|
22
|
-
expect(styleEntry('css')).toBe('styles/main.css');
|
|
23
|
-
expect(styleEntry('sass')).toBe('styles/main.scss');
|
|
24
|
-
expect(styleEntry('less')).toBe('styles/main.less');
|
|
25
|
-
expect(styleEntry('stylus')).toBe('styles/main.styl');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('reverses extensions back to preprocessors', () => {
|
|
29
|
-
expect(preprocessorForExt('scss')).toBe('sass');
|
|
30
|
-
expect(preprocessorForExt('.sass')).toBe('sass');
|
|
31
|
-
expect(preprocessorForExt('less')).toBe('less');
|
|
32
|
-
expect(preprocessorForExt('styl')).toBe('stylus');
|
|
33
|
-
expect(preprocessorForExt('css')).toBe('css');
|
|
34
|
-
expect(preprocessorForExt('txt')).toBeNull();
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('requiredPackages / packageDiff', () => {
|
|
39
|
-
it('lists packages for a feature set', () => {
|
|
40
|
-
expect(requiredPackages(CSS)).toEqual([]);
|
|
41
|
-
expect(requiredPackages({ preprocessor: 'sass', tailwind: false })).toEqual(['sass']);
|
|
42
|
-
expect(requiredPackages({ preprocessor: 'css', tailwind: true })).toEqual([
|
|
43
|
-
'tailwindcss',
|
|
44
|
-
'@tailwindcss/vite',
|
|
45
|
-
]);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('diffs add/remove between two setups', () => {
|
|
49
|
-
expect(packageDiff(CSS, SASS_TW)).toEqual({
|
|
50
|
-
add: ['sass', 'tailwindcss', '@tailwindcss/vite'],
|
|
51
|
-
remove: [],
|
|
52
|
-
});
|
|
53
|
-
expect(packageDiff(SASS_TW, CSS)).toEqual({
|
|
54
|
-
add: [],
|
|
55
|
-
remove: ['sass', 'tailwindcss', '@tailwindcss/vite'],
|
|
56
|
-
});
|
|
57
|
-
expect(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
'',
|
|
78
|
-
'
|
|
79
|
-
'',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
expect(out).toContain("
|
|
87
|
-
expect(out).toContain('
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
expect(
|
|
111
|
-
expect(
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
defaultConfigSource,
|
|
5
|
+
detectPreprocessor,
|
|
6
|
+
detectTailwind,
|
|
7
|
+
packageDiff,
|
|
8
|
+
preprocessorForExt,
|
|
9
|
+
requiredPackages,
|
|
10
|
+
setConfigImages,
|
|
11
|
+
setStyleImports,
|
|
12
|
+
styleEntry,
|
|
13
|
+
type StyleFeatures,
|
|
14
|
+
styleImportLines,
|
|
15
|
+
} from '../src/cli/features';
|
|
16
|
+
|
|
17
|
+
const CSS: StyleFeatures = { preprocessor: 'css', tailwind: false };
|
|
18
|
+
const SASS_TW: StyleFeatures = { preprocessor: 'sass', tailwind: true };
|
|
19
|
+
|
|
20
|
+
describe('styleEntry / preprocessorForExt', () => {
|
|
21
|
+
it('maps preprocessors to stylesheet paths', () => {
|
|
22
|
+
expect(styleEntry('css')).toBe('styles/main.css');
|
|
23
|
+
expect(styleEntry('sass')).toBe('styles/main.scss');
|
|
24
|
+
expect(styleEntry('less')).toBe('styles/main.less');
|
|
25
|
+
expect(styleEntry('stylus')).toBe('styles/main.styl');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('reverses extensions back to preprocessors', () => {
|
|
29
|
+
expect(preprocessorForExt('scss')).toBe('sass');
|
|
30
|
+
expect(preprocessorForExt('.sass')).toBe('sass');
|
|
31
|
+
expect(preprocessorForExt('less')).toBe('less');
|
|
32
|
+
expect(preprocessorForExt('styl')).toBe('stylus');
|
|
33
|
+
expect(preprocessorForExt('css')).toBe('css');
|
|
34
|
+
expect(preprocessorForExt('txt')).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('requiredPackages / packageDiff', () => {
|
|
39
|
+
it('lists packages for a feature set', () => {
|
|
40
|
+
expect(requiredPackages(CSS)).toEqual([]);
|
|
41
|
+
expect(requiredPackages({ preprocessor: 'sass', tailwind: false })).toEqual(['sass']);
|
|
42
|
+
expect(requiredPackages({ preprocessor: 'css', tailwind: true })).toEqual([
|
|
43
|
+
'tailwindcss',
|
|
44
|
+
'@tailwindcss/vite',
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('diffs add/remove between two setups', () => {
|
|
49
|
+
expect(packageDiff(CSS, SASS_TW)).toEqual({
|
|
50
|
+
add: ['sass', 'tailwindcss', '@tailwindcss/vite'],
|
|
51
|
+
remove: [],
|
|
52
|
+
});
|
|
53
|
+
expect(packageDiff(SASS_TW, CSS)).toEqual({
|
|
54
|
+
add: [],
|
|
55
|
+
remove: ['sass', 'tailwindcss', '@tailwindcss/vite'],
|
|
56
|
+
});
|
|
57
|
+
expect(
|
|
58
|
+
packageDiff(
|
|
59
|
+
{ preprocessor: 'sass', tailwind: false },
|
|
60
|
+
{ preprocessor: 'less', tailwind: false },
|
|
61
|
+
),
|
|
62
|
+
).toEqual({ add: ['less'], remove: ['sass'] });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('styleImportLines / setStyleImports', () => {
|
|
67
|
+
it('orders Tailwind before the main stylesheet', () => {
|
|
68
|
+
expect(styleImportLines(CSS)).toEqual(["import './styles/main.css';"]);
|
|
69
|
+
expect(styleImportLines(SASS_TW)).toEqual([
|
|
70
|
+
"import './styles/tailwind.css';",
|
|
71
|
+
"import './styles/main.scss';",
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('rewrites the app entry imports, preserving the rest', () => {
|
|
76
|
+
const src = [
|
|
77
|
+
"import { routes, layout, notFound } from 'toiljs/routes';",
|
|
78
|
+
'',
|
|
79
|
+
"import './styles/main.css';",
|
|
80
|
+
'',
|
|
81
|
+
'Toil.mount(routes, layout, notFound);',
|
|
82
|
+
'',
|
|
83
|
+
].join('\n');
|
|
84
|
+
|
|
85
|
+
const out = setStyleImports(src, SASS_TW);
|
|
86
|
+
expect(out).toContain("import './styles/tailwind.css';");
|
|
87
|
+
expect(out).toContain("import './styles/main.scss';");
|
|
88
|
+
expect(out).not.toContain("import './styles/main.css';");
|
|
89
|
+
expect(out).toContain("from 'toiljs/routes'");
|
|
90
|
+
expect(out).toContain('Toil.mount(routes, layout, notFound);');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('round-trips back to plain CSS (drops Tailwind import)', () => {
|
|
94
|
+
const src = [
|
|
95
|
+
"import { routes, layout, notFound } from 'toiljs/routes';",
|
|
96
|
+
"import './styles/tailwind.css';",
|
|
97
|
+
"import './styles/main.scss';",
|
|
98
|
+
'Toil.mount(routes, layout, notFound);',
|
|
99
|
+
].join('\n');
|
|
100
|
+
|
|
101
|
+
const out = setStyleImports(src, CSS);
|
|
102
|
+
expect(out).toContain("import './styles/main.css';");
|
|
103
|
+
expect(out).not.toContain('tailwind.css');
|
|
104
|
+
expect(out).not.toContain('main.scss');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('detect from dependencies', () => {
|
|
109
|
+
it('finds the active preprocessor and Tailwind', () => {
|
|
110
|
+
expect(detectPreprocessor({ sass: '^1' })).toBe('sass');
|
|
111
|
+
expect(detectPreprocessor({ less: '^4' })).toBe('less');
|
|
112
|
+
expect(detectPreprocessor({})).toBe('css');
|
|
113
|
+
expect(detectTailwind({ '@tailwindcss/vite': '^4' })).toBe(true);
|
|
114
|
+
expect(detectTailwind({ react: '^19' })).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('setConfigImages / defaultConfigSource', () => {
|
|
119
|
+
it('flips an existing images flag', () => {
|
|
120
|
+
const src =
|
|
121
|
+
'export default defineConfig({\n client: {\n images: true,\n },\n});\n';
|
|
122
|
+
expect(setConfigImages(src, false)).toContain('images: false');
|
|
123
|
+
expect(setConfigImages(src, false)).not.toContain('images: true');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('adds images to an existing client block', () => {
|
|
127
|
+
const out = setConfigImages(
|
|
128
|
+
'export default defineConfig({ client: { base: "/" } });',
|
|
129
|
+
false,
|
|
130
|
+
);
|
|
131
|
+
expect(out).toContain('images: false');
|
|
132
|
+
expect(out).toContain('base: "/"');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('adds a client block to a bare config', () => {
|
|
136
|
+
const out = setConfigImages('export default defineConfig({});', false);
|
|
137
|
+
expect(out).toContain('client: { images: false }');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns null when the shape is unrecognized', () => {
|
|
141
|
+
expect(setConfigImages('const x = 1;', false)).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('round-trips through defaultConfigSource', () => {
|
|
145
|
+
const src = defaultConfigSource(false);
|
|
146
|
+
expect(src).toContain('images: false');
|
|
147
|
+
expect(setConfigImages(src, true)).toContain('images: true');
|
|
148
|
+
});
|
|
149
|
+
});
|