toiljs 0.0.11 → 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.
Files changed (119) hide show
  1. package/README.md +2 -0
  2. package/build/cli/.tsbuildinfo +1 -1
  3. package/build/cli/configure.js +10 -4
  4. package/build/cli/create.js +58 -30
  5. package/build/cli/diagnostics.d.ts +55 -0
  6. package/build/cli/diagnostics.js +333 -0
  7. package/build/cli/doctor.d.ts +6 -0
  8. package/build/cli/doctor.js +249 -0
  9. package/build/cli/index.js +26 -0
  10. package/build/cli/proc.d.ts +5 -0
  11. package/build/cli/proc.js +20 -0
  12. package/build/cli/ui.d.ts +1 -0
  13. package/build/cli/ui.js +1 -0
  14. package/build/cli/update.d.ts +7 -0
  15. package/build/cli/update.js +117 -0
  16. package/build/cli/updates.d.ts +10 -0
  17. package/build/cli/updates.js +45 -0
  18. package/build/client/.tsbuildinfo +1 -1
  19. package/build/client/dev/error-overlay.js +1 -1
  20. package/build/client/head/metadata.js +3 -1
  21. package/build/client/index.d.ts +5 -1
  22. package/build/client/index.js +2 -0
  23. package/build/client/navigation/navigation.js +1 -1
  24. package/build/client/routing/Router.js +2 -2
  25. package/build/client/search/search.d.ts +26 -0
  26. package/build/client/search/search.js +101 -0
  27. package/build/client/search/use-page-search.d.ts +8 -0
  28. package/build/client/search/use-page-search.js +21 -0
  29. package/build/compiler/.tsbuildinfo +1 -1
  30. package/build/compiler/generate.js +26 -23
  31. package/build/compiler/index.d.ts +2 -0
  32. package/build/compiler/index.js +1 -0
  33. package/build/compiler/pages.d.ts +8 -0
  34. package/build/compiler/pages.js +37 -0
  35. package/build/compiler/plugin.js +3 -1
  36. package/build/compiler/prerender.d.ts +1 -0
  37. package/build/compiler/prerender.js +11 -5
  38. package/build/compiler/seo.js +10 -3
  39. package/build/io/.tsbuildinfo +1 -1
  40. package/examples/basic/client/components/Header.tsx +43 -41
  41. package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
  42. package/examples/basic/client/public/index.html +18 -16
  43. package/examples/basic/client/routes/(legal)/privacy.tsx +18 -19
  44. package/examples/basic/client/routes/(legal)/terms.tsx +15 -16
  45. package/examples/basic/client/routes/about.tsx +21 -22
  46. package/examples/basic/client/routes/blog/[id].tsx +26 -18
  47. package/examples/basic/client/routes/features/actions.tsx +67 -67
  48. package/examples/basic/client/routes/features/error/index.tsx +27 -27
  49. package/examples/basic/client/routes/features/head.tsx +38 -38
  50. package/examples/basic/client/routes/features/index.tsx +83 -75
  51. package/examples/basic/client/routes/features/realtime.tsx +34 -32
  52. package/examples/basic/client/routes/features/script.tsx +31 -31
  53. package/examples/basic/client/routes/features/seo.tsx +39 -39
  54. package/examples/basic/client/routes/features/template/index.tsx +20 -20
  55. package/examples/basic/client/routes/features/template/template.tsx +16 -18
  56. package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -23
  57. package/examples/basic/client/routes/gallery/index.tsx +42 -42
  58. package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -18
  59. package/examples/basic/client/routes/get-started.tsx +157 -84
  60. package/examples/basic/client/routes/index.tsx +137 -96
  61. package/examples/basic/client/routes/loader-demo/index.tsx +59 -52
  62. package/examples/basic/client/routes/search.tsx +61 -0
  63. package/examples/basic/client/routes/test.tsx +7 -8
  64. package/examples/basic/client/styles/main.css +624 -552
  65. package/package.json +2 -2
  66. package/presets/eslint.js +10 -3
  67. package/src/cli/configure.ts +363 -353
  68. package/src/cli/create.ts +563 -530
  69. package/src/cli/diagnostics.ts +421 -0
  70. package/src/cli/doctor.ts +318 -0
  71. package/src/cli/features.ts +166 -160
  72. package/src/cli/index.ts +242 -211
  73. package/src/cli/proc.ts +30 -0
  74. package/src/cli/ui.ts +111 -103
  75. package/src/cli/update.ts +150 -0
  76. package/src/cli/updates.ts +69 -0
  77. package/src/client/components/Image.tsx +91 -89
  78. package/src/client/dev/error-overlay.tsx +193 -197
  79. package/src/client/head/metadata.ts +94 -92
  80. package/src/client/index.ts +79 -64
  81. package/src/client/navigation/Link.tsx +94 -100
  82. package/src/client/navigation/navigation.ts +215 -218
  83. package/src/client/routing/Router.tsx +210 -193
  84. package/src/client/routing/hooks.ts +110 -114
  85. package/src/client/routing/lazy.ts +77 -81
  86. package/src/client/search/search.ts +189 -0
  87. package/src/client/search/use-page-search.ts +73 -0
  88. package/src/compiler/config.ts +173 -171
  89. package/src/compiler/fonts.ts +89 -87
  90. package/src/compiler/generate.ts +378 -373
  91. package/src/compiler/image-report.ts +88 -85
  92. package/src/compiler/index.ts +2 -0
  93. package/src/compiler/pages.ts +70 -0
  94. package/src/compiler/plugin.ts +51 -47
  95. package/src/compiler/prerender.ts +152 -130
  96. package/src/compiler/routes.ts +132 -131
  97. package/src/compiler/seo.ts +381 -356
  98. package/src/compiler/vite.ts +155 -145
  99. package/src/io/FastSet.ts +99 -96
  100. package/test/configure.test.ts +94 -90
  101. package/test/doctor.test.ts +140 -0
  102. package/test/dom/Image.test.tsx +73 -46
  103. package/test/dom/Script.test.tsx +48 -45
  104. package/test/dom/action.test.tsx +146 -129
  105. package/test/dom/error-overlay.test.tsx +44 -44
  106. package/test/dom/loader.test.tsx +2 -2
  107. package/test/dom/revalidate.test.tsx +1 -1
  108. package/test/dom/route-head.test.tsx +1 -2
  109. package/test/dom/slot.test.tsx +131 -109
  110. package/test/dom/view-transitions.test.tsx +53 -51
  111. package/test/features.test.ts +149 -142
  112. package/test/fonts.test.ts +28 -26
  113. package/test/head.test.ts +45 -35
  114. package/test/metadata.test.ts +42 -41
  115. package/test/pages.test.ts +105 -0
  116. package/test/prerender.test.ts +54 -46
  117. package/test/search.test.ts +114 -0
  118. package/test/seo.test.ts +164 -142
  119. package/test/update.test.ts +44 -0
@@ -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.fn().mockReturnValue({ matches }) as unknown as typeof window.matchMedia;
21
- }
22
-
23
- it('wraps navigation in startViewTransition when enabled and supported', () => {
24
- const vt = vi.fn((cb: () => void) => {
25
- cb();
26
- });
27
- doc.startViewTransition = vt;
28
- stubReducedMotion(false);
29
- setViewTransitions(true);
30
- navigate('/a');
31
- expect(vt).toHaveBeenCalledOnce();
32
- });
33
-
34
- it('skips the view transition under prefers-reduced-motion', () => {
35
- const vt = vi.fn();
36
- doc.startViewTransition = vt;
37
- stubReducedMotion(true);
38
- setViewTransitions(true);
39
- navigate('/b');
40
- expect(vt).not.toHaveBeenCalled();
41
- });
42
-
43
- it('does not use view transitions when disabled', () => {
44
- const vt = vi.fn();
45
- doc.startViewTransition = vt;
46
- stubReducedMotion(false);
47
- setViewTransitions(false);
48
- navigate('/c');
49
- expect(vt).not.toHaveBeenCalled();
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
+ });
@@ -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
- styleImportLines,
14
- type StyleFeatures,
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(packageDiff({ preprocessor: 'sass', tailwind: false }, { preprocessor: 'less', tailwind: false })).toEqual(
58
- { add: ['less'], remove: ['sass'] },
59
- );
60
- });
61
- });
62
-
63
- describe('styleImportLines / setStyleImports', () => {
64
- it('orders Tailwind before the main stylesheet', () => {
65
- expect(styleImportLines(CSS)).toEqual(["import './styles/main.css';"]);
66
- expect(styleImportLines(SASS_TW)).toEqual([
67
- "import './styles/tailwind.css';",
68
- "import './styles/main.scss';",
69
- ]);
70
- });
71
-
72
- it('rewrites the app entry imports, preserving the rest', () => {
73
- const src = [
74
- "import { routes, layout, notFound } from 'toiljs/routes';",
75
- '',
76
- "import './styles/main.css';",
77
- '',
78
- 'Toil.mount(routes, layout, notFound);',
79
- '',
80
- ].join('\n');
81
-
82
- const out = setStyleImports(src, SASS_TW);
83
- expect(out).toContain("import './styles/tailwind.css';");
84
- expect(out).toContain("import './styles/main.scss';");
85
- expect(out).not.toContain("import './styles/main.css';");
86
- expect(out).toContain("from 'toiljs/routes'");
87
- expect(out).toContain('Toil.mount(routes, layout, notFound);');
88
- });
89
-
90
- it('round-trips back to plain CSS (drops Tailwind import)', () => {
91
- const src = [
92
- "import { routes, layout, notFound } from 'toiljs/routes';",
93
- "import './styles/tailwind.css';",
94
- "import './styles/main.scss';",
95
- 'Toil.mount(routes, layout, notFound);',
96
- ].join('\n');
97
-
98
- const out = setStyleImports(src, CSS);
99
- expect(out).toContain("import './styles/main.css';");
100
- expect(out).not.toContain('tailwind.css');
101
- expect(out).not.toContain('main.scss');
102
- });
103
- });
104
-
105
- describe('detect from dependencies', () => {
106
- it('finds the active preprocessor and Tailwind', () => {
107
- expect(detectPreprocessor({ sass: '^1' })).toBe('sass');
108
- expect(detectPreprocessor({ less: '^4' })).toBe('less');
109
- expect(detectPreprocessor({})).toBe('css');
110
- expect(detectTailwind({ '@tailwindcss/vite': '^4' })).toBe(true);
111
- expect(detectTailwind({ react: '^19' })).toBe(false);
112
- });
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
+ 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
+ });
@@ -1,26 +1,28 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { fontPreloadTags } from '../src/compiler/fonts';
4
-
5
- describe('fontPreloadTags', () => {
6
- it('builds a crossorigin preload link per font, skipping non-fonts', () => {
7
- const tags = fontPreloadTags(['fonts/a-abc.woff2', 'assets/x-1.js', 'fonts/b.ttf'], '/');
8
- expect(tags).toHaveLength(2);
9
- expect(tags[0]).toEqual({
10
- tag: 'link',
11
- attrs: {
12
- rel: 'preload',
13
- as: 'font',
14
- type: 'font/woff2',
15
- href: '/fonts/a-abc.woff2',
16
- crossorigin: '',
17
- },
18
- injectTo: 'head',
19
- });
20
- expect(tags[1].attrs?.type).toBe('font/ttf');
21
- });
22
-
23
- it('respects a non-root base path', () => {
24
- expect(fontPreloadTags(['fonts/a.woff2'], '/app/')[0].attrs?.href).toBe('/app/fonts/a.woff2');
25
- });
26
- });
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { fontPreloadTags } from '../src/compiler/fonts';
4
+
5
+ describe('fontPreloadTags', () => {
6
+ it('builds a crossorigin preload link per font, skipping non-fonts', () => {
7
+ const tags = fontPreloadTags(['fonts/a-abc.woff2', 'assets/x-1.js', 'fonts/b.ttf'], '/');
8
+ expect(tags).toHaveLength(2);
9
+ expect(tags[0]).toEqual({
10
+ tag: 'link',
11
+ attrs: {
12
+ rel: 'preload',
13
+ as: 'font',
14
+ type: 'font/woff2',
15
+ href: '/fonts/a-abc.woff2',
16
+ crossorigin: '',
17
+ },
18
+ injectTo: 'head',
19
+ });
20
+ expect(tags[1].attrs?.type).toBe('font/ttf');
21
+ });
22
+
23
+ it('respects a non-root base path', () => {
24
+ expect(fontPreloadTags(['fonts/a.woff2'], '/app/')[0].attrs?.href).toBe(
25
+ '/app/fonts/a.woff2',
26
+ );
27
+ });
28
+ });
package/test/head.test.ts CHANGED
@@ -1,35 +1,45 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { mergeHead } from '../src/client/head/head';
4
-
5
- describe('mergeHead', () => {
6
- it('takes the last title and applies a titleTemplate', () => {
7
- expect(mergeHead([{ title: 'Home' }]).title).toBe('Home');
8
- expect(mergeHead([{ title: 'A' }, { title: 'B' }]).title).toBe('B');
9
- expect(mergeHead([{ titleTemplate: '%s · toiljs' }, { title: 'About' }]).title).toBe(
10
- 'About · toiljs',
11
- );
12
- });
13
-
14
- it('leaves title undefined when nothing sets it', () => {
15
- expect(mergeHead([{ meta: [{ name: 'x', content: 'y' }] }]).title).toBeUndefined();
16
- });
17
-
18
- it('dedupes meta by name/property, last wins', () => {
19
- const resolved = mergeHead([
20
- { meta: [{ name: 'description', content: 'old' }] },
21
- { meta: [{ name: 'description', content: 'new' }, { property: 'og:title', content: 'T' }] },
22
- ]);
23
- expect(resolved.meta).toHaveLength(2);
24
- expect(resolved.meta.find((m) => m.name === 'description')?.content).toBe('new');
25
- expect(resolved.meta.find((m) => m.property === 'og:title')?.content).toBe('T');
26
- });
27
-
28
- it('dedupes links by rel+href', () => {
29
- const resolved = mergeHead([
30
- { link: [{ rel: 'icon', href: '/a.svg' }] },
31
- { link: [{ rel: 'icon', href: '/a.svg' }, { rel: 'canonical', href: '/x' }] },
32
- ]);
33
- expect(resolved.link).toHaveLength(2);
34
- });
35
- });
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { mergeHead } from '../src/client/head/head';
4
+
5
+ describe('mergeHead', () => {
6
+ it('takes the last title and applies a titleTemplate', () => {
7
+ expect(mergeHead([{ title: 'Home' }]).title).toBe('Home');
8
+ expect(mergeHead([{ title: 'A' }, { title: 'B' }]).title).toBe('B');
9
+ expect(mergeHead([{ titleTemplate: '%s · toiljs' }, { title: 'About' }]).title).toBe(
10
+ 'About · toiljs',
11
+ );
12
+ });
13
+
14
+ it('leaves title undefined when nothing sets it', () => {
15
+ expect(mergeHead([{ meta: [{ name: 'x', content: 'y' }] }]).title).toBeUndefined();
16
+ });
17
+
18
+ it('dedupes meta by name/property, last wins', () => {
19
+ const resolved = mergeHead([
20
+ { meta: [{ name: 'description', content: 'old' }] },
21
+ {
22
+ meta: [
23
+ { name: 'description', content: 'new' },
24
+ { property: 'og:title', content: 'T' },
25
+ ],
26
+ },
27
+ ]);
28
+ expect(resolved.meta).toHaveLength(2);
29
+ expect(resolved.meta.find((m) => m.name === 'description')?.content).toBe('new');
30
+ expect(resolved.meta.find((m) => m.property === 'og:title')?.content).toBe('T');
31
+ });
32
+
33
+ it('dedupes links by rel+href', () => {
34
+ const resolved = mergeHead([
35
+ { link: [{ rel: 'icon', href: '/a.svg' }] },
36
+ {
37
+ link: [
38
+ { rel: 'icon', href: '/a.svg' },
39
+ { rel: 'canonical', href: '/x' },
40
+ ],
41
+ },
42
+ ]);
43
+ expect(resolved.link).toHaveLength(2);
44
+ });
45
+ });
@@ -1,41 +1,42 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { resolveMetadata } from '../src/client/head/metadata';
4
-
5
- describe('resolveMetadata', () => {
6
- it('expands convenience fields into meta/link tags', () => {
7
- const head = resolveMetadata({
8
- title: 'About',
9
- titleTemplate: '%s · toiljs',
10
- description: 'desc',
11
- keywords: ['a', 'b'],
12
- robots: 'noindex',
13
- themeColor: '#000',
14
- canonical: 'https://x.test/about',
15
- openGraph: { title: 'OG', type: 'website', image: 'https://x.test/og.png' },
16
- });
17
-
18
- expect(head.title).toBe('About');
19
- expect(head.titleTemplate).toBe('%s · toiljs');
20
- const byName = (name: string) => head.meta?.find((m) => m.name === name)?.content;
21
- const byProp = (property: string) => head.meta?.find((m) => m.property === property)?.content;
22
- expect(byName('description')).toBe('desc');
23
- expect(byName('keywords')).toBe('a, b');
24
- expect(byName('robots')).toBe('noindex');
25
- expect(byName('theme-color')).toBe('#000');
26
- expect(byProp('og:title')).toBe('OG');
27
- expect(byProp('og:type')).toBe('website');
28
- expect(byProp('og:image')).toBe('https://x.test/og.png');
29
- expect(head.link?.find((l) => l.rel === 'canonical')?.href).toBe('https://x.test/about');
30
- });
31
-
32
- it('passes through raw meta/link and omits unset fields', () => {
33
- const head = resolveMetadata({
34
- title: 'X',
35
- meta: [{ name: 'author', content: 'me' }],
36
- link: [{ rel: 'alternate', href: '/rss' }],
37
- });
38
- expect(head.meta).toEqual([{ name: 'author', content: 'me' }]);
39
- expect(head.link).toEqual([{ rel: 'alternate', href: '/rss' }]);
40
- });
41
- });
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { resolveMetadata } from '../src/client/head/metadata';
4
+
5
+ describe('resolveMetadata', () => {
6
+ it('expands convenience fields into meta/link tags', () => {
7
+ const head = resolveMetadata({
8
+ title: 'About',
9
+ titleTemplate: '%s · toiljs',
10
+ description: 'desc',
11
+ keywords: ['a', 'b'],
12
+ robots: 'noindex',
13
+ themeColor: '#000',
14
+ canonical: 'https://x.test/about',
15
+ openGraph: { title: 'OG', type: 'website', image: 'https://x.test/og.png' },
16
+ });
17
+
18
+ expect(head.title).toBe('About');
19
+ expect(head.titleTemplate).toBe('%s · toiljs');
20
+ const byName = (name: string) => head.meta?.find((m) => m.name === name)?.content;
21
+ const byProp = (property: string) =>
22
+ head.meta?.find((m) => m.property === property)?.content;
23
+ expect(byName('description')).toBe('desc');
24
+ expect(byName('keywords')).toBe('a, b');
25
+ expect(byName('robots')).toBe('noindex');
26
+ expect(byName('theme-color')).toBe('#000');
27
+ expect(byProp('og:title')).toBe('OG');
28
+ expect(byProp('og:type')).toBe('website');
29
+ expect(byProp('og:image')).toBe('https://x.test/og.png');
30
+ expect(head.link?.find((l) => l.rel === 'canonical')?.href).toBe('https://x.test/about');
31
+ });
32
+
33
+ it('passes through raw meta/link and omits unset fields', () => {
34
+ const head = resolveMetadata({
35
+ title: 'X',
36
+ meta: [{ name: 'author', content: 'me' }],
37
+ link: [{ rel: 'alternate', href: '/rss' }],
38
+ });
39
+ expect(head.meta).toEqual([{ name: 'author', content: 'me' }]);
40
+ expect(head.link).toEqual([{ rel: 'alternate', href: '/rss' }]);
41
+ });
42
+ });