toiljs 0.0.11 → 0.0.14

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 (120) hide show
  1. package/README.md +3 -1
  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 +33 -24
  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 +45 -27
  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 +1 -1
  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/router-loading.test.tsx +1 -1
  110. package/test/dom/slot.test.tsx +131 -109
  111. package/test/dom/view-transitions.test.tsx +53 -51
  112. package/test/features.test.ts +149 -142
  113. package/test/fonts.test.ts +28 -26
  114. package/test/head.test.ts +45 -35
  115. package/test/metadata.test.ts +42 -41
  116. package/test/pages.test.ts +105 -0
  117. package/test/prerender.test.ts +54 -46
  118. package/test/search.test.ts +114 -0
  119. package/test/seo.test.ts +30 -8
  120. package/test/update.test.ts +44 -0
@@ -1,171 +1,173 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath, pathToFileURL } from 'node:url';
4
-
5
- import { type InlineConfig } from 'vite';
6
-
7
- import { type SeoConfig } from './seo.js';
8
-
9
- export type { SeoConfig } from './seo.js';
10
-
11
- /**
12
- * Client-side (TSX/React/Vite) configuration. All fields optional; sensible defaults applied.
13
- */
14
- export interface ClientConfig {
15
- /** Client source directory, relative to root. Default `client`. */
16
- readonly srcDir?: string;
17
- /** Routes directory, relative to `srcDir`. Default `routes`. */
18
- readonly routesDir?: string;
19
- /**
20
- * Static assets directory, relative to root. Default `<srcDir>/public` (e.g. `client/public`).
21
- * Holds the `index.html` template (owned and edited by you) plus any files served as-is at the
22
- * base path (favicons, images, …).
23
- */
24
- readonly publicDir?: string;
25
- /** Production output directory, relative to root. Default `build/client`. */
26
- readonly outDir?: string;
27
- /** Public base path. Default `/`. */
28
- readonly base?: string;
29
- /** Dev server port. Default `3000`. */
30
- readonly port?: number;
31
- /**
32
- * Optimize imported images at build time (resize/convert via `vite-imagetools` + sharp): an
33
- * import like `logo.png?w=400;800&format=webp&as=srcset` emits resized, compressed variants.
34
- * Default `true`. Set `false` to disable the pipeline (images are then served as-is).
35
- */
36
- readonly images?: boolean;
37
- /**
38
- * Preload bundled fonts at build time: injects `<link rel="preload" as="font">` for each
39
- * `@font-face` font so it loads in parallel with the CSS (faster text paint). Default `true`.
40
- */
41
- readonly fonts?: boolean;
42
- /**
43
- * Animate cross-page navigations with the browser View Transitions API (a crossfade by default;
44
- * add `view-transition-name` in CSS for shared-element transitions). Respects
45
- * `prefers-reduced-motion`. Default `false`.
46
- */
47
- readonly viewTransitions?: boolean;
48
- /**
49
- * Build-time SEO: bakes site-level metadata into the HTML `<head>` (so JS-less crawlers and AI
50
- * bots see real tags) and generates `robots.txt`, `sitemap.xml`, and `llms.txt`. Omit to skip.
51
- */
52
- readonly seo?: SeoConfig;
53
- /**
54
- * Raw Vite escape hatch, deep-merged over the framework's opinionated config.
55
- * This is NOT the client config itself, toil owns the Vite setup; use this only
56
- * to override specific Vite options.
57
- */
58
- readonly vite?: InlineConfig;
59
- }
60
-
61
- /**
62
- * Server-side (toilscript → WASM) configuration. Reserved: the compiler does not yet
63
- * build the server target via `toil build`; today it is compiled by `toilscript` directly.
64
- */
65
- export interface ServerConfig {
66
- /** Server source directory, relative to root. Default `server`. */
67
- readonly srcDir?: string;
68
- /** Server build output directory, relative to root. Default `build/server`. */
69
- readonly outDir?: string;
70
- }
71
-
72
- /**
73
- * The `toil.config` schema. All fields optional; sensible defaults applied.
74
- * Client and server are configured in separate sections.
75
- */
76
- export interface ToilConfig {
77
- /** Project root. Defaults to the current working directory. */
78
- readonly root?: string;
79
- /** Client (TSX/React/Vite) configuration. */
80
- readonly client?: ClientConfig;
81
- /** Server (toilscript/WASM) configuration. */
82
- readonly server?: ServerConfig;
83
- }
84
-
85
- /** Fully-resolved config with absolute paths, used internally by the compiler. */
86
- export interface ResolvedToilConfig {
87
- readonly root: string;
88
- readonly srcDir: string;
89
- readonly clientAbsDir: string;
90
- readonly routesAbsDir: string;
91
- /** Absolute path to the static-assets dir (holds the `index.html` template). */
92
- readonly publicDir: string;
93
- readonly toilDir: string;
94
- readonly outDir: string;
95
- readonly base: string;
96
- readonly port: number;
97
- /** Whether build-time image optimization (`vite-imagetools`) is enabled. */
98
- readonly images: boolean;
99
- /** Whether build-time font preloading is enabled. */
100
- readonly fonts: boolean;
101
- /** Whether animated View Transitions are enabled for navigation. */
102
- readonly viewTransitions: boolean;
103
- /** Build-time SEO config, or `null` when not configured. */
104
- readonly seo: SeoConfig | null;
105
- /** Absolute path to the framework client runtime (`toiljs/client`). */
106
- readonly runtimePath: string;
107
- readonly vite: InlineConfig;
108
- }
109
-
110
- /** Identity helper for typed config files: `export default defineConfig({ ... })`. */
111
- export function defineConfig(config: ToilConfig): ToilConfig {
112
- return config;
113
- }
114
-
115
- const CONFIG_NAMES = [
116
- 'toil.config.ts',
117
- 'toil.config.mts',
118
- 'toil.config.js',
119
- 'toil.config.mjs',
120
- 'toiljs.config.ts',
121
- 'toiljs.config.mts',
122
- 'toiljs.config.js',
123
- 'toiljs.config.mjs',
124
- ];
125
-
126
- /** Path to the built client runtime (`build/client/index.js`), sibling to `build/compiler`. */
127
- function resolveRuntimePath(): string {
128
- return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../client/index.js');
129
- }
130
-
131
- /** Finds and loads `toil.config.*` or `toiljs.config.*` from `root`, then resolves defaults. */
132
- export async function loadConfig(
133
- opts: { root?: string; port?: number } = {},
134
- ): Promise<ResolvedToilConfig> {
135
- const root = path.resolve(opts.root ?? process.cwd());
136
-
137
- let user: ToilConfig = {};
138
- for (const name of CONFIG_NAMES) {
139
- const candidate = path.join(root, name);
140
- if (fs.existsSync(candidate)) {
141
- const loaded = (await import(pathToFileURL(candidate).href)) as { default?: ToilConfig };
142
- if (loaded.default) user = loaded.default;
143
- break;
144
- }
145
- }
146
-
147
- const client = user.client ?? {};
148
- const srcDir = client.srcDir ?? 'client';
149
- const routesDir = client.routesDir ?? 'routes';
150
- const clientAbsDir = path.join(root, srcDir);
151
-
152
- return {
153
- root,
154
- srcDir,
155
- clientAbsDir,
156
- routesAbsDir: path.join(clientAbsDir, routesDir),
157
- publicDir: client.publicDir
158
- ? path.resolve(root, client.publicDir)
159
- : path.join(clientAbsDir, 'public'),
160
- toilDir: path.join(root, '.toil'),
161
- outDir: client.outDir ?? 'build/client',
162
- base: client.base ?? '/',
163
- port: opts.port ?? client.port ?? 3000,
164
- images: client.images ?? true,
165
- fonts: client.fonts ?? true,
166
- viewTransitions: client.viewTransitions ?? false,
167
- seo: client.seo ?? null,
168
- runtimePath: resolveRuntimePath(),
169
- vite: client.vite ?? {},
170
- };
171
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+
5
+ import { type InlineConfig } from 'vite';
6
+
7
+ import { type SeoConfig } from './seo.js';
8
+
9
+ export type { SeoConfig } from './seo.js';
10
+
11
+ /**
12
+ * Client-side (TSX/React/Vite) configuration. All fields optional; sensible defaults applied.
13
+ */
14
+ export interface ClientConfig {
15
+ /** Client source directory, relative to root. Default `client`. */
16
+ readonly srcDir?: string;
17
+ /** Routes directory, relative to `srcDir`. Default `routes`. */
18
+ readonly routesDir?: string;
19
+ /**
20
+ * Static assets directory, relative to root. Default `<srcDir>/public` (e.g. `client/public`).
21
+ * Holds the `index.html` template (owned and edited by you) plus any files served as-is at the
22
+ * base path (favicons, images, …).
23
+ */
24
+ readonly publicDir?: string;
25
+ /** Production output directory, relative to root. Default `build/client`. */
26
+ readonly outDir?: string;
27
+ /** Public base path. Default `/`. */
28
+ readonly base?: string;
29
+ /** Dev server port. Default `3000`. */
30
+ readonly port?: number;
31
+ /**
32
+ * Optimize imported images at build time (resize/convert via `vite-imagetools` + sharp): an
33
+ * import like `logo.png?w=400;800&format=webp&as=srcset` emits resized, compressed variants.
34
+ * Default `true`. Set `false` to disable the pipeline (images are then served as-is).
35
+ */
36
+ readonly images?: boolean;
37
+ /**
38
+ * Preload bundled fonts at build time: injects `<link rel="preload" as="font">` for each
39
+ * `@font-face` font so it loads in parallel with the CSS (faster text paint). Default `true`.
40
+ */
41
+ readonly fonts?: boolean;
42
+ /**
43
+ * Animate cross-page navigations with the browser View Transitions API (a crossfade by default;
44
+ * add `view-transition-name` in CSS for shared-element transitions). Respects
45
+ * `prefers-reduced-motion`. Default `false`.
46
+ */
47
+ readonly viewTransitions?: boolean;
48
+ /**
49
+ * Build-time SEO: bakes site-level metadata into the HTML `<head>` (so JS-less crawlers and AI
50
+ * bots see real tags) and generates `robots.txt`, `sitemap.xml`, and `llms.txt`. Omit to skip.
51
+ */
52
+ readonly seo?: SeoConfig;
53
+ /**
54
+ * Raw Vite escape hatch, deep-merged over the framework's opinionated config.
55
+ * This is NOT the client config itself, toil owns the Vite setup; use this only
56
+ * to override specific Vite options.
57
+ */
58
+ readonly vite?: InlineConfig;
59
+ }
60
+
61
+ /**
62
+ * Server-side (toilscript → WASM) configuration. Reserved: the compiler does not yet
63
+ * build the server target via `toil build`; today it is compiled by `toilscript` directly.
64
+ */
65
+ export interface ServerConfig {
66
+ /** Server source directory, relative to root. Default `server`. */
67
+ readonly srcDir?: string;
68
+ /** Server build output directory, relative to root. Default `build/server`. */
69
+ readonly outDir?: string;
70
+ }
71
+
72
+ /**
73
+ * The `toil.config` schema. All fields optional; sensible defaults applied.
74
+ * Client and server are configured in separate sections.
75
+ */
76
+ export interface ToilConfig {
77
+ /** Project root. Defaults to the current working directory. */
78
+ readonly root?: string;
79
+ /** Client (TSX/React/Vite) configuration. */
80
+ readonly client?: ClientConfig;
81
+ /** Server (toilscript/WASM) configuration. */
82
+ readonly server?: ServerConfig;
83
+ }
84
+
85
+ /** Fully-resolved config with absolute paths, used internally by the compiler. */
86
+ export interface ResolvedToilConfig {
87
+ readonly root: string;
88
+ readonly srcDir: string;
89
+ readonly clientAbsDir: string;
90
+ readonly routesAbsDir: string;
91
+ /** Absolute path to the static-assets dir (holds the `index.html` template). */
92
+ readonly publicDir: string;
93
+ readonly toilDir: string;
94
+ readonly outDir: string;
95
+ readonly base: string;
96
+ readonly port: number;
97
+ /** Whether build-time image optimization (`vite-imagetools`) is enabled. */
98
+ readonly images: boolean;
99
+ /** Whether build-time font preloading is enabled. */
100
+ readonly fonts: boolean;
101
+ /** Whether animated View Transitions are enabled for navigation. */
102
+ readonly viewTransitions: boolean;
103
+ /** Build-time SEO config, or `null` when not configured. */
104
+ readonly seo: SeoConfig | null;
105
+ /** Absolute path to the framework client runtime (`toiljs/client`). */
106
+ readonly runtimePath: string;
107
+ readonly vite: InlineConfig;
108
+ }
109
+
110
+ /** Identity helper for typed config files: `export default defineConfig({ ... })`. */
111
+ export function defineConfig(config: ToilConfig): ToilConfig {
112
+ return config;
113
+ }
114
+
115
+ const CONFIG_NAMES = [
116
+ 'toil.config.ts',
117
+ 'toil.config.mts',
118
+ 'toil.config.js',
119
+ 'toil.config.mjs',
120
+ 'toiljs.config.ts',
121
+ 'toiljs.config.mts',
122
+ 'toiljs.config.js',
123
+ 'toiljs.config.mjs',
124
+ ];
125
+
126
+ /** Path to the built client runtime (`build/client/index.js`), sibling to `build/compiler`. */
127
+ function resolveRuntimePath(): string {
128
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../client/index.js');
129
+ }
130
+
131
+ /** Finds and loads `toil.config.*` or `toiljs.config.*` from `root`, then resolves defaults. */
132
+ export async function loadConfig(
133
+ opts: { root?: string; port?: number } = {},
134
+ ): Promise<ResolvedToilConfig> {
135
+ const root = path.resolve(opts.root ?? process.cwd());
136
+
137
+ let user: ToilConfig = {};
138
+ for (const name of CONFIG_NAMES) {
139
+ const candidate = path.join(root, name);
140
+ if (fs.existsSync(candidate)) {
141
+ const loaded = (await import(pathToFileURL(candidate).href)) as {
142
+ default?: ToilConfig;
143
+ };
144
+ if (loaded.default) user = loaded.default;
145
+ break;
146
+ }
147
+ }
148
+
149
+ const client = user.client ?? {};
150
+ const srcDir = client.srcDir ?? 'client';
151
+ const routesDir = client.routesDir ?? 'routes';
152
+ const clientAbsDir = path.join(root, srcDir);
153
+
154
+ return {
155
+ root,
156
+ srcDir,
157
+ clientAbsDir,
158
+ routesAbsDir: path.join(clientAbsDir, routesDir),
159
+ publicDir: client.publicDir
160
+ ? path.resolve(root, client.publicDir)
161
+ : path.join(clientAbsDir, 'public'),
162
+ toilDir: path.join(root, '.toil'),
163
+ outDir: client.outDir ?? 'build/client',
164
+ base: client.base ?? '/',
165
+ port: opts.port ?? client.port ?? 3000,
166
+ images: client.images ?? true,
167
+ fonts: client.fonts ?? true,
168
+ viewTransitions: client.viewTransitions ?? false,
169
+ seo: client.seo ?? null,
170
+ runtimePath: resolveRuntimePath(),
171
+ vite: client.vite ?? {},
172
+ };
173
+ }
@@ -1,87 +1,89 @@
1
- import type { HtmlTagDescriptor, Logger, Plugin } from 'vite';
2
-
3
- import { type ResolvedToilConfig } from './config.js';
4
-
5
- /**
6
- * Build-time font optimization. Bundled font files (`@font-face` `url(...)` imports) are emitted +
7
- * hashed by Vite, but without a hint the browser only discovers them after parsing CSS, delaying
8
- * text paint. This injects a `<link rel="preload" as="font" crossorigin>` for each bundled font so
9
- * it loads in parallel with the CSS, and logs what it preloaded (mirrors the image-optimization log).
10
- */
11
- const FONT_RE = /\.(woff2|woff|ttf|otf)$/i;
12
- const FONT_TYPE: Record<string, string> = {
13
- woff2: 'font/woff2',
14
- woff: 'font/woff',
15
- ttf: 'font/ttf',
16
- otf: 'font/otf',
17
- };
18
-
19
- function kb(bytes: number): string {
20
- return `${(bytes / 1000).toFixed(2)} kB`;
21
- }
22
-
23
- /** Builds the `<link rel="preload">` head tags for a set of bundled font file names. */
24
- export function fontPreloadTags(fileNames: readonly string[], base: string): HtmlTagDescriptor[] {
25
- const prefix = base.endsWith('/') ? base : `${base}/`;
26
- return fileNames
27
- .filter((name) => FONT_RE.test(name))
28
- .map((name) => {
29
- const ext = name.split('.').pop()?.toLowerCase() ?? '';
30
- return {
31
- tag: 'link',
32
- attrs: {
33
- rel: 'preload',
34
- as: 'font',
35
- type: FONT_TYPE[ext] ?? `font/${ext}`,
36
- href: `${prefix}${name}`,
37
- crossorigin: '',
38
- },
39
- injectTo: 'head',
40
- };
41
- });
42
- }
43
-
44
- /** Build-only plugin that preloads bundled fonts and logs them. Disabled by `client.fonts: false`. */
45
- export function fontPreloadPlugin(cfg: ResolvedToilConfig): Plugin {
46
- let logger: Logger | undefined;
47
- let logged = false;
48
- return {
49
- name: 'toil:font-preload',
50
- apply: 'build',
51
- configResolved(config) {
52
- logger = config.logger;
53
- },
54
- transformIndexHtml: {
55
- order: 'post',
56
- handler(html, ctx) {
57
- const bundle = ctx.bundle ?? {};
58
- const fonts = Object.values(bundle).filter(
59
- (file) => file.type === 'asset' && FONT_RE.test(file.fileName),
60
- );
61
- if (fonts.length === 0) return html;
62
-
63
- // Log once (the same template's HTML is transformed per emitted page).
64
- if (!logged && logger) {
65
- logged = true;
66
- logger.info('');
67
- logger.info(` ✓ preloaded ${String(fonts.length)} font${fonts.length === 1 ? '' : 's'}`);
68
- for (const file of fonts) {
69
- const size =
70
- file.type === 'asset' && typeof file.source !== 'string'
71
- ? kb(file.source.byteLength)
72
- : '';
73
- logger.info(` → ${file.fileName} ${size}`);
74
- }
75
- }
76
-
77
- return {
78
- html,
79
- tags: fontPreloadTags(
80
- fonts.map((f) => f.fileName),
81
- cfg.base,
82
- ),
83
- };
84
- },
85
- },
86
- };
87
- }
1
+ import type { HtmlTagDescriptor, Logger, Plugin } from 'vite';
2
+
3
+ import { type ResolvedToilConfig } from './config.js';
4
+
5
+ /**
6
+ * Build-time font optimization. Bundled font files (`@font-face` `url(...)` imports) are emitted +
7
+ * hashed by Vite, but without a hint the browser only discovers them after parsing CSS, delaying
8
+ * text paint. This injects a `<link rel="preload" as="font" crossorigin>` for each bundled font so
9
+ * it loads in parallel with the CSS, and logs what it preloaded (mirrors the image-optimization log).
10
+ */
11
+ const FONT_RE = /\.(woff2|woff|ttf|otf)$/i;
12
+ const FONT_TYPE: Record<string, string> = {
13
+ woff2: 'font/woff2',
14
+ woff: 'font/woff',
15
+ ttf: 'font/ttf',
16
+ otf: 'font/otf',
17
+ };
18
+
19
+ function kb(bytes: number): string {
20
+ return `${(bytes / 1000).toFixed(2)} kB`;
21
+ }
22
+
23
+ /** Builds the `<link rel="preload">` head tags for a set of bundled font file names. */
24
+ export function fontPreloadTags(fileNames: readonly string[], base: string): HtmlTagDescriptor[] {
25
+ const prefix = base.endsWith('/') ? base : `${base}/`;
26
+ return fileNames
27
+ .filter((name) => FONT_RE.test(name))
28
+ .map((name) => {
29
+ const ext = name.split('.').pop()?.toLowerCase() ?? '';
30
+ return {
31
+ tag: 'link',
32
+ attrs: {
33
+ rel: 'preload',
34
+ as: 'font',
35
+ type: FONT_TYPE[ext] ?? `font/${ext}`,
36
+ href: `${prefix}${name}`,
37
+ crossorigin: '',
38
+ },
39
+ injectTo: 'head',
40
+ };
41
+ });
42
+ }
43
+
44
+ /** Build-only plugin that preloads bundled fonts and logs them. Disabled by `client.fonts: false`. */
45
+ export function fontPreloadPlugin(cfg: ResolvedToilConfig): Plugin {
46
+ let logger: Logger | undefined;
47
+ let logged = false;
48
+ return {
49
+ name: 'toil:font-preload',
50
+ apply: 'build',
51
+ configResolved(config) {
52
+ logger = config.logger;
53
+ },
54
+ transformIndexHtml: {
55
+ order: 'post',
56
+ handler(html, ctx) {
57
+ const bundle = ctx.bundle ?? {};
58
+ const fonts = Object.values(bundle).filter(
59
+ (file) => file.type === 'asset' && FONT_RE.test(file.fileName),
60
+ );
61
+ if (fonts.length === 0) return html;
62
+
63
+ // Log once (the same template's HTML is transformed per emitted page).
64
+ if (!logged && logger) {
65
+ logged = true;
66
+ logger.info('');
67
+ logger.info(
68
+ ` ✓ preloaded ${String(fonts.length)} font${fonts.length === 1 ? '' : 's'}`,
69
+ );
70
+ for (const file of fonts) {
71
+ const size =
72
+ file.type === 'asset' && typeof file.source !== 'string'
73
+ ? kb(file.source.byteLength)
74
+ : '';
75
+ logger.info(` → ${file.fileName} ${size}`);
76
+ }
77
+ }
78
+
79
+ return {
80
+ html,
81
+ tags: fontPreloadTags(
82
+ fonts.map((f) => f.fileName),
83
+ cfg.base,
84
+ ),
85
+ };
86
+ },
87
+ },
88
+ };
89
+ }