toiljs 0.0.115 → 0.0.117

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 (326) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +16 -7
  3. package/TYPESCRIPT_LAW.md +1946 -2073
  4. package/build/backend/.tsbuildinfo +1 -1
  5. package/build/cli/.tsbuildinfo +1 -1
  6. package/build/cli/index.js +2375 -258
  7. package/build/client/.tsbuildinfo +1 -1
  8. package/build/client/dev/devtools.js +1 -1
  9. package/build/client/dev/error-overlay.js +1 -1
  10. package/build/client/routing/mount.js +1 -1
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/auth-emails.js +7 -3
  13. package/build/compiler/emails.js +1 -1
  14. package/build/compiler/index.d.ts +1 -0
  15. package/build/compiler/index.js +25 -4
  16. package/build/compiler/pages.js +2 -3
  17. package/build/compiler/prerender.d.ts +3 -8
  18. package/build/compiler/prerender.js +88 -127
  19. package/build/compiler/ssg.js +3 -4
  20. package/build/compiler/template-build.js +3 -4
  21. package/build/compiler/toil-docs.generated.js +6 -5
  22. package/build/compiler/vite.js +3 -6
  23. package/build/devserver/.tsbuildinfo +1 -1
  24. package/build/devserver/db/database.js +7 -3
  25. package/build/devserver/db/types.js +1 -1
  26. package/build/devserver/production.js +4 -3
  27. package/build/devserver/runtime/crypto.d.ts +1 -1
  28. package/build/io/.tsbuildinfo +1 -1
  29. package/build/logger/.tsbuildinfo +1 -1
  30. package/build/shared/.tsbuildinfo +1 -1
  31. package/examples/basic/client/routes/analytics.tsx +7 -5
  32. package/examples/basic/server/tsconfig.json +1 -3
  33. package/package.json +45 -44
  34. package/presets/no-uint8array-tostring.js +120 -153
  35. package/presets/oxlint.d.ts +5 -0
  36. package/presets/oxlint.js +224 -0
  37. package/server/runtime/tsconfig.json +1 -3
  38. package/tsconfig.json +2 -11
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  40. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  41. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  42. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  43. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  44. package/.github/changelog-config.json +0 -45
  45. package/.github/dependabot.yml +0 -27
  46. package/.github/workflows/ci.yml +0 -222
  47. package/.vscode/settings.json +0 -10
  48. package/docs/README.md +0 -107
  49. package/docs/auth/README.md +0 -104
  50. package/docs/auth/configuration.md +0 -105
  51. package/docs/auth/emails.md +0 -92
  52. package/docs/auth/extending.md +0 -202
  53. package/docs/auth/how-it-works.md +0 -138
  54. package/docs/auth/usage.md +0 -188
  55. package/docs/backend/README.md +0 -143
  56. package/docs/backend/data.md +0 -351
  57. package/docs/backend/rest.md +0 -402
  58. package/docs/backend/rpc.md +0 -226
  59. package/docs/background/README.md +0 -114
  60. package/docs/background/daemons.md +0 -276
  61. package/docs/background/derive.md +0 -179
  62. package/docs/cli/README.md +0 -379
  63. package/docs/concepts/ai-guide.md +0 -132
  64. package/docs/concepts/config.md +0 -416
  65. package/docs/concepts/decorators.md +0 -127
  66. package/docs/concepts/security.md +0 -108
  67. package/docs/concepts/tiers.md +0 -166
  68. package/docs/concepts/types.md +0 -216
  69. package/docs/database/README.md +0 -143
  70. package/docs/database/capacity.md +0 -350
  71. package/docs/database/counters.md +0 -174
  72. package/docs/database/documents.md +0 -277
  73. package/docs/database/events.md +0 -307
  74. package/docs/database/membership.md +0 -246
  75. package/docs/database/setup.md +0 -155
  76. package/docs/database/unique.md +0 -216
  77. package/docs/database/views.md +0 -246
  78. package/docs/frontend/README.md +0 -104
  79. package/docs/frontend/components.md +0 -286
  80. package/docs/frontend/data-fetching.md +0 -260
  81. package/docs/frontend/images.md +0 -148
  82. package/docs/frontend/metadata.md +0 -344
  83. package/docs/frontend/navigation.md +0 -296
  84. package/docs/frontend/rendering.md +0 -236
  85. package/docs/frontend/routing.md +0 -346
  86. package/docs/frontend/scripts.md +0 -118
  87. package/docs/frontend/search.md +0 -191
  88. package/docs/frontend/styling.md +0 -147
  89. package/docs/frontend/toil-global.md +0 -216
  90. package/docs/getting-started/README.md +0 -81
  91. package/docs/getting-started/create-project.md +0 -131
  92. package/docs/getting-started/deploy.md +0 -101
  93. package/docs/getting-started/first-app.md +0 -215
  94. package/docs/getting-started/installation.md +0 -124
  95. package/docs/getting-started/migrating.md +0 -125
  96. package/docs/getting-started/project-structure.md +0 -163
  97. package/docs/introduction/README.md +0 -66
  98. package/docs/introduction/design-principles.md +0 -81
  99. package/docs/introduction/distributed.md +0 -91
  100. package/docs/introduction/how-it-works.md +0 -121
  101. package/docs/introduction/hyperscale.md +0 -70
  102. package/docs/introduction/modern-stack.md +0 -85
  103. package/docs/introduction/vs-other-frameworks.md +0 -64
  104. package/docs/introduction/why-toil.md +0 -99
  105. package/docs/llms.txt +0 -95
  106. package/docs/realtime/README.md +0 -102
  107. package/docs/realtime/channels.md +0 -211
  108. package/docs/realtime/streams.md +0 -369
  109. package/docs/services/README.md +0 -60
  110. package/docs/services/analytics.md +0 -279
  111. package/docs/services/caching.md +0 -175
  112. package/docs/services/cookies.md +0 -235
  113. package/docs/services/crypto.md +0 -209
  114. package/docs/services/email.md +0 -289
  115. package/docs/services/environment.md +0 -141
  116. package/docs/services/ratelimit.md +0 -174
  117. package/docs/services/time.md +0 -85
  118. package/eslint.config.js +0 -57
  119. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  120. package/presets/eslint.js +0 -88
  121. package/src/backend/index.ts +0 -204
  122. package/src/cli/configure.ts +0 -363
  123. package/src/cli/create.ts +0 -924
  124. package/src/cli/db.ts +0 -158
  125. package/src/cli/diagnostics.ts +0 -665
  126. package/src/cli/doctor.ts +0 -982
  127. package/src/cli/features.ts +0 -170
  128. package/src/cli/index.ts +0 -326
  129. package/src/cli/notify.ts +0 -120
  130. package/src/cli/proc.ts +0 -56
  131. package/src/cli/ui.ts +0 -169
  132. package/src/cli/update.ts +0 -248
  133. package/src/cli/updates.ts +0 -92
  134. package/src/cli/validate.ts +0 -31
  135. package/src/cli/version-check.ts +0 -125
  136. package/src/client/auth.ts +0 -869
  137. package/src/client/channel/channel.ts +0 -146
  138. package/src/client/components/Form.tsx +0 -65
  139. package/src/client/components/Image.tsx +0 -160
  140. package/src/client/components/Script.tsx +0 -113
  141. package/src/client/components/Slot.tsx +0 -21
  142. package/src/client/dev/devtools.tsx +0 -1089
  143. package/src/client/dev/error-overlay.tsx +0 -225
  144. package/src/client/errors.ts +0 -11
  145. package/src/client/head/head.ts +0 -182
  146. package/src/client/head/metadata.ts +0 -110
  147. package/src/client/index.ts +0 -130
  148. package/src/client/navigation/Link.tsx +0 -108
  149. package/src/client/navigation/NavLink.tsx +0 -86
  150. package/src/client/navigation/navigation.ts +0 -235
  151. package/src/client/navigation/prefetch.ts +0 -169
  152. package/src/client/navigation/scroll.ts +0 -53
  153. package/src/client/routing/Router.tsx +0 -216
  154. package/src/client/routing/action.ts +0 -122
  155. package/src/client/routing/error-boundary.tsx +0 -43
  156. package/src/client/routing/hooks.ts +0 -140
  157. package/src/client/routing/lazy.ts +0 -77
  158. package/src/client/routing/loader.ts +0 -381
  159. package/src/client/routing/match.ts +0 -47
  160. package/src/client/routing/mount.tsx +0 -90
  161. package/src/client/routing/params-context.ts +0 -10
  162. package/src/client/routing/slot-context.ts +0 -7
  163. package/src/client/rpc.ts +0 -97
  164. package/src/client/search/search.ts +0 -189
  165. package/src/client/search/use-page-search.ts +0 -73
  166. package/src/client/ssr/markers.tsx +0 -172
  167. package/src/client/stream/client.ts +0 -237
  168. package/src/client/types.ts +0 -73
  169. package/src/compiler/auth-emails.ts +0 -312
  170. package/src/compiler/config.ts +0 -369
  171. package/src/compiler/docs.ts +0 -127
  172. package/src/compiler/email-preview.ts +0 -314
  173. package/src/compiler/emails.ts +0 -404
  174. package/src/compiler/fonts.ts +0 -89
  175. package/src/compiler/generate.ts +0 -471
  176. package/src/compiler/image-blur.ts +0 -42
  177. package/src/compiler/image-report.ts +0 -88
  178. package/src/compiler/index.ts +0 -1362
  179. package/src/compiler/pages.ts +0 -49
  180. package/src/compiler/plugin.ts +0 -391
  181. package/src/compiler/prerender.ts +0 -263
  182. package/src/compiler/routes.ts +0 -170
  183. package/src/compiler/seo.ts +0 -415
  184. package/src/compiler/sri.ts +0 -144
  185. package/src/compiler/ssg.ts +0 -177
  186. package/src/compiler/ssr-codegen.ts +0 -85
  187. package/src/compiler/template-build.ts +0 -676
  188. package/src/compiler/template.ts +0 -262
  189. package/src/compiler/toil-docs.generated.ts +0 -77
  190. package/src/compiler/vite.ts +0 -245
  191. package/src/devserver/analytics/index.ts +0 -317
  192. package/src/devserver/config/dotenv.ts +0 -102
  193. package/src/devserver/config/env.ts +0 -30
  194. package/src/devserver/config/ratelimit.ts +0 -123
  195. package/src/devserver/daemon/catalog.ts +0 -120
  196. package/src/devserver/daemon/cron.ts +0 -87
  197. package/src/devserver/daemon/host.ts +0 -134
  198. package/src/devserver/daemon/index.ts +0 -369
  199. package/src/devserver/daemon/runtime.ts +0 -48
  200. package/src/devserver/db/catalog.ts +0 -109
  201. package/src/devserver/db/database.ts +0 -1834
  202. package/src/devserver/db/derives.ts +0 -121
  203. package/src/devserver/db/index.ts +0 -20
  204. package/src/devserver/db/routeKinds.ts +0 -191
  205. package/src/devserver/db/types.ts +0 -168
  206. package/src/devserver/email/caps.ts +0 -0
  207. package/src/devserver/email/config.ts +0 -141
  208. package/src/devserver/email/console.ts +0 -348
  209. package/src/devserver/email/index.ts +0 -155
  210. package/src/devserver/email/providers.ts +0 -131
  211. package/src/devserver/email/status.ts +0 -23
  212. package/src/devserver/email/validate.ts +0 -37
  213. package/src/devserver/email/wire.ts +0 -55
  214. package/src/devserver/http/cache.ts +0 -0
  215. package/src/devserver/http/envelope.ts +0 -157
  216. package/src/devserver/http/proxy.ts +0 -172
  217. package/src/devserver/http/runtime.ts +0 -387
  218. package/src/devserver/index.ts +0 -43
  219. package/src/devserver/production-ipc.ts +0 -63
  220. package/src/devserver/production-worker.ts +0 -83
  221. package/src/devserver/production.ts +0 -718
  222. package/src/devserver/runtime/crypto.ts +0 -587
  223. package/src/devserver/runtime/host.ts +0 -427
  224. package/src/devserver/runtime/module.ts +0 -442
  225. package/src/devserver/server.ts +0 -251
  226. package/src/devserver/ssr.ts +0 -208
  227. package/src/devserver/stream/catalog.ts +0 -106
  228. package/src/devserver/stream/host.ts +0 -42
  229. package/src/devserver/stream/index.ts +0 -301
  230. package/src/devserver/stream/manager.ts +0 -167
  231. package/src/devserver/stream/router.ts +0 -101
  232. package/src/devserver/stream/wire.ts +0 -58
  233. package/src/devserver/stream/ws.ts +0 -78
  234. package/src/devserver/wasm/sections.ts +0 -59
  235. package/src/devserver/wasm/surface.ts +0 -115
  236. package/src/io/FastMap.ts +0 -151
  237. package/src/io/FastSet.ts +0 -113
  238. package/src/io/codec.ts +0 -362
  239. package/src/io/index.ts +0 -10
  240. package/src/io/lengths.ts +0 -14
  241. package/src/io/types.ts +0 -19
  242. package/src/logger/index.ts +0 -22
  243. package/src/shared/index.ts +0 -46
  244. package/test/analytics-dev.test.ts +0 -113
  245. package/test/assembly/aspect-shim.ts +0 -33
  246. package/test/assembly/cookie.spec.ts +0 -303
  247. package/test/assembly/example.spec.ts +0 -22
  248. package/test/assembly/ssr.spec.ts +0 -95
  249. package/test/assembly.test.ts +0 -59
  250. package/test/auth-emails.test.ts +0 -118
  251. package/test/built-ssr.test.ts +0 -148
  252. package/test/channel.test.ts +0 -21
  253. package/test/config-host-port.test.ts +0 -59
  254. package/test/configure.test.ts +0 -94
  255. package/test/daemon-build.test.ts +0 -278
  256. package/test/daemon-catalog.test.ts +0 -349
  257. package/test/daemon-emulation.test.ts +0 -256
  258. package/test/db.test.ts +0 -0
  259. package/test/devserver-database.test.ts +0 -1102
  260. package/test/devserver-email.test.ts +0 -306
  261. package/test/devserver-pqauth.test.ts +0 -93
  262. package/test/devserver-secrets.test.ts +0 -63
  263. package/test/devserver.test.ts +0 -259
  264. package/test/doctor.test.ts +0 -298
  265. package/test/dom/Image.test.tsx +0 -144
  266. package/test/dom/Link.test.tsx +0 -67
  267. package/test/dom/NavLink.test.tsx +0 -37
  268. package/test/dom/Script.test.tsx +0 -48
  269. package/test/dom/action.test.tsx +0 -146
  270. package/test/dom/error-overlay.test.tsx +0 -44
  271. package/test/dom/loader.test.tsx +0 -121
  272. package/test/dom/navigation.test.ts +0 -59
  273. package/test/dom/revalidate.test.tsx +0 -38
  274. package/test/dom/route-head.test.tsx +0 -56
  275. package/test/dom/router-loading.test.tsx +0 -44
  276. package/test/dom/scroll.test.ts +0 -56
  277. package/test/dom/slot.test.tsx +0 -131
  278. package/test/dom/use-metadata.test.tsx +0 -58
  279. package/test/dom/view-transitions.test.tsx +0 -53
  280. package/test/dynamic-routing.test.ts +0 -132
  281. package/test/email-console.test.ts +0 -86
  282. package/test/email-preview.test.ts +0 -73
  283. package/test/emails.test.ts +0 -58
  284. package/test/errors.test.ts +0 -21
  285. package/test/example-guestbook.test.ts +0 -123
  286. package/test/features.test.ts +0 -149
  287. package/test/fixtures/bignum-wire/spec.ts +0 -25
  288. package/test/fixtures/daemon-app.ts +0 -65
  289. package/test/fixtures/stream-echo.ts +0 -26
  290. package/test/fixtures/stream-gate.ts +0 -28
  291. package/test/fixtures/stream-trap.ts +0 -18
  292. package/test/fixtures/stream-typed.ts +0 -41
  293. package/test/fonts.test.ts +0 -28
  294. package/test/global-setup.ts +0 -17
  295. package/test/head.test.ts +0 -42
  296. package/test/image-blur.test.ts +0 -50
  297. package/test/io.test.ts +0 -117
  298. package/test/metadata.test.ts +0 -40
  299. package/test/navlink.test.ts +0 -28
  300. package/test/pages.test.ts +0 -105
  301. package/test/placeholder.test.ts +0 -9
  302. package/test/pqauth-e2e.test.ts +0 -216
  303. package/test/pqauth-email.test.ts +0 -801
  304. package/test/prerender.test.ts +0 -116
  305. package/test/prettier-plugin.test.ts +0 -46
  306. package/test/routes.test.ts +0 -76
  307. package/test/rpc-bignum-wire.test.ts +0 -164
  308. package/test/rpc-dispatch.test.ts +0 -132
  309. package/test/rpc-kinds.test.ts +0 -18
  310. package/test/rpc.test.ts +0 -66
  311. package/test/search.test.ts +0 -114
  312. package/test/seo.test.ts +0 -175
  313. package/test/slot-layouts.test.ts +0 -69
  314. package/test/sri.test.ts +0 -129
  315. package/test/ssg.test.ts +0 -36
  316. package/test/ssr-hydration.test.tsx +0 -122
  317. package/test/ssr-render.test.ts +0 -197
  318. package/test/ssr-template.test.tsx +0 -482
  319. package/test/stream-emulation.test.ts +0 -479
  320. package/test/ui.test.ts +0 -40
  321. package/test/update.test.ts +0 -58
  322. package/test/validate.test.ts +0 -42
  323. package/test/version-check.test.ts +0 -130
  324. package/tests/data-parity/generated-parity.ts +0 -99
  325. package/tests/data-parity/parity.ts +0 -80
  326. package/tests/data-parity/spec.ts +0 -46
@@ -1,114 +0,0 @@
1
- import { beforeEach, describe, expect, it } from 'vitest';
2
-
3
- import {
4
- getPages,
5
- type PageMeta,
6
- pagePath,
7
- registerPages,
8
- searchPages,
9
- } from '../src/client/search/search';
10
-
11
- const PAGES: PageMeta[] = [
12
- {
13
- path: '/',
14
- dynamic: false,
15
- metadata: { title: 'Home', description: 'Welcome to the toil demo site' },
16
- },
17
- {
18
- path: '/about',
19
- dynamic: false,
20
- metadata: { title: 'About', description: 'Who we are', keywords: ['team', 'company'] },
21
- },
22
- {
23
- path: '/blog',
24
- dynamic: false,
25
- metadata: {
26
- title: 'Blog',
27
- description: 'Articles and updates',
28
- openGraph: { title: 'The Blog', siteName: 'toil' },
29
- },
30
- },
31
- { path: '/blog/:id', dynamic: true, metadata: { title: 'Blog post' } },
32
- { path: '/get-started', dynamic: false, metadata: {} },
33
- ];
34
-
35
- beforeEach(() => {
36
- registerPages(PAGES);
37
- });
38
-
39
- describe('searchPages', () => {
40
- it('returns no results for an empty / whitespace query', () => {
41
- expect(searchPages('')).toEqual([]);
42
- expect(searchPages(' ')).toEqual([]);
43
- });
44
-
45
- it('ranks a title match above a description-only match', () => {
46
- const results = searchPages('about');
47
- expect(results[0].page.path).toBe('/about');
48
- // Two pages mention "about"? Only /about does here; the home page does not.
49
- expect(results.map((r) => r.page.path)).toContain('/about');
50
- expect(results[0].matches).toContain('title');
51
- });
52
-
53
- it('matches keywords', () => {
54
- const results = searchPages('team');
55
- expect(results).toHaveLength(1);
56
- expect(results[0].page.path).toBe('/about');
57
- expect(results[0].matches).toContain('keywords');
58
- });
59
-
60
- it('matches OpenGraph fields', () => {
61
- const results = searchPages('blog', { fields: ['openGraph'] });
62
- expect(results.map((r) => r.page.path)).toEqual(['/blog']);
63
- expect(results[0].matches).toEqual(['openGraph']);
64
- });
65
-
66
- it('makes slugs word-searchable via the path field', () => {
67
- const results = searchPages('started');
68
- expect(results.map((r) => r.page.path)).toContain('/get-started');
69
- });
70
-
71
- it('excludes dynamic routes unless includeDynamic is set', () => {
72
- expect(searchPages('post').map((r) => r.page.path)).not.toContain('/blog/:id');
73
- expect(searchPages('post', { includeDynamic: true }).map((r) => r.page.path)).toContain(
74
- '/blog/:id',
75
- );
76
- });
77
-
78
- it('uses AND semantics: every term must match', () => {
79
- expect(searchPages('blog updates').map((r) => r.page.path)).toEqual(['/blog']);
80
- expect(searchPages('blog nonexistentword')).toEqual([]);
81
- });
82
-
83
- it('honors the limit option', () => {
84
- // "toil" appears in the home description and the blog's OpenGraph siteName.
85
- const all = searchPages('toil');
86
- expect(all.length).toBeGreaterThan(1);
87
- expect(searchPages('toil', { limit: 1 })).toHaveLength(1);
88
- });
89
-
90
- it('restricts matching to the requested fields', () => {
91
- // "welcome" only lives in the home description; excluding it yields nothing.
92
- expect(searchPages('welcome', { fields: ['title', 'keywords'] })).toEqual([]);
93
- expect(searchPages('welcome', { fields: ['description'] }).map((r) => r.page.path)).toEqual(
94
- ['/'],
95
- );
96
- });
97
-
98
- it('is case-insensitive', () => {
99
- expect(searchPages('ABOUT')[0].page.path).toBe('/about');
100
- });
101
- });
102
-
103
- describe('registry helpers', () => {
104
- it('getPages returns the registered index', () => {
105
- expect(getPages()).toBe(PAGES);
106
- });
107
-
108
- it('pagePath normalizes results, pages, and raw strings', () => {
109
- const result = searchPages('about')[0];
110
- expect(pagePath(result)).toBe('/about');
111
- expect(pagePath(result.page)).toBe('/about');
112
- expect(pagePath('/raw')).toBe('/raw');
113
- });
114
- });
package/test/seo.test.ts DELETED
@@ -1,175 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import type { ScannedRoute } from '../src/compiler/routes';
4
- import {
5
- injectSeoHtml,
6
- llmsTxt,
7
- robotsTxt,
8
- routeSeo,
9
- seoHeadTags,
10
- sitemapXml,
11
- } from '../src/compiler/seo';
12
-
13
- const routes: ScannedRoute[] = [
14
- { file: 'a', pattern: '/' },
15
- { file: 'b', pattern: '/about' },
16
- { file: 'c', pattern: '/blog/:id' }, // dynamic, excluded from sitemap
17
- { file: 'd', pattern: '/photo/:id', slot: 'modal', intercept: true }, // slot/intercept, excluded
18
- ];
19
-
20
- describe('seoHeadTags', () => {
21
- it('bakes description, OG, canonical, preconnect, and JSON-LD', () => {
22
- const html = seoHeadTags({
23
- url: 'https://x.test',
24
- title: 'Home',
25
- description: 'desc',
26
- openGraph: { type: 'website', image: 'https://x.test/og.png' },
27
- preconnect: ['https://cdn.test'],
28
- jsonLd: { '@context': 'https://schema.org', '@type': 'WebSite' },
29
- });
30
- expect(html).toContain('<meta name="description" content="desc" />');
31
- expect(html).toContain('<meta property="og:title" content="Home" />');
32
- expect(html).toContain('<meta property="og:image" content="https://x.test/og.png" />');
33
- expect(html).toContain('<link rel="canonical" href="https://x.test" />');
34
- expect(html).toContain('<link rel="preconnect" href="https://cdn.test" />');
35
- expect(html).toContain('application/ld+json');
36
- expect(html).toContain('"@type":"WebSite"');
37
- });
38
-
39
- it('escapes attribute values', () => {
40
- expect(seoHeadTags({ description: 'a "b" <c>' })).toContain(
41
- 'content="a &quot;b&quot; &lt;c&gt;"',
42
- );
43
- });
44
-
45
- it('renders a full Twitter card + OG image dimensions + fb:app_id', () => {
46
- const html = seoHeadTags({
47
- title: 'Home',
48
- description: 'd',
49
- openGraph: {
50
- image: 'https://x.test/og.png',
51
- imageAlt: 'alt',
52
- imageWidth: 1200,
53
- imageHeight: 630,
54
- },
55
- twitter: { site: '@x' },
56
- facebook: { appId: '123' },
57
- });
58
- expect(html).toContain('<meta name="twitter:card" content="summary_large_image" />');
59
- expect(html).toContain('<meta name="twitter:site" content="@x" />');
60
- expect(html).toContain('<meta name="twitter:title" content="Home" />');
61
- expect(html).toContain('<meta name="twitter:image" content="https://x.test/og.png" />');
62
- expect(html).toContain('<meta property="og:image:width" content="1200" />');
63
- expect(html).toContain('<meta property="og:image:alt" content="alt" />');
64
- expect(html).toContain('<meta property="fb:app_id" content="123" />');
65
- });
66
-
67
- it('neutralizes </script> in JSON-LD (no script breakout)', () => {
68
- const html = seoHeadTags({ jsonLd: { x: '</script><img src=x onerror=alert(1)>' } });
69
- expect(html).not.toContain('</script><img');
70
- expect(html).toContain('\\u003c/script');
71
- });
72
- });
73
-
74
- describe('routeSeo', () => {
75
- it("overlays a route's metadata over the site defaults and points URLs at the route", () => {
76
- const site = { url: 'https://x.test', title: 'Site', description: 'site desc' };
77
- const out = routeSeo(site, { title: 'About', description: 'about desc' }, '/about');
78
- expect(out.title).toBe('About');
79
- expect(out.description).toBe('about desc');
80
- expect(out.url).toBe('https://x.test/about');
81
- });
82
-
83
- it('falls back to the site defaults when a route has no metadata', () => {
84
- const site = { url: 'https://x.test', title: 'Site' };
85
- expect(routeSeo(site, null, '/x')).toMatchObject({
86
- title: 'Site',
87
- url: 'https://x.test/x',
88
- });
89
- });
90
- });
91
-
92
- describe('injectSeoHtml', () => {
93
- it('replaces the title + description and inserts the rest before </head>', () => {
94
- const shell =
95
- '<!doctype html><html><head><title>old</title><meta name="description" content="" /></head><body></body></html>';
96
- const out = injectSeoHtml(shell, {
97
- title: 'New',
98
- description: 'fresh',
99
- url: 'https://x.test',
100
- });
101
- expect(out).toContain('<title>New</title>');
102
- expect(out).not.toContain('<title>old</title>');
103
- expect(out.match(/name="description"/g)).toHaveLength(1);
104
- expect(out).toContain('content="fresh"');
105
- expect(out).toContain('<link rel="canonical" href="https://x.test" />');
106
- });
107
- });
108
-
109
- describe('robotsTxt', () => {
110
- it('allows all + lists AI crawlers + links the sitemap by default', () => {
111
- const txt = robotsTxt({ url: 'https://x.test' });
112
- expect(txt).toContain('User-agent: *');
113
- expect(txt).toContain('Allow: /');
114
- expect(txt).toContain('User-agent: GPTBot');
115
- expect(txt).toContain('User-agent: ClaudeBot');
116
- expect(txt).toContain('Sitemap: https://x.test/sitemap.xml');
117
- });
118
-
119
- it('disallows AI crawlers when ai: "disallow"', () => {
120
- const txt = robotsTxt({ url: 'https://x.test', robots: { ai: 'disallow' } });
121
- expect(txt).toMatch(/User-agent: GPTBot\nDisallow: \//);
122
- });
123
-
124
- it('is empty when robots: false', () => {
125
- expect(robotsTxt({ robots: false })).toBe('');
126
- });
127
- });
128
-
129
- describe('sitemapXml', () => {
130
- it('lists only static routes, absolute', () => {
131
- const xml = sitemapXml({ url: 'https://x.test' }, routes);
132
- expect(xml).toContain('<loc>https://x.test</loc>');
133
- expect(xml).toContain('<loc>https://x.test/about</loc>');
134
- expect(xml).not.toContain(':id');
135
- expect(xml).not.toContain('/photo');
136
- });
137
-
138
- it('is empty without a base url', () => {
139
- expect(sitemapXml({}, routes)).toBe('');
140
- });
141
- });
142
-
143
- describe('llmsTxt', () => {
144
- it('renders title, summary, instructions, and pages', () => {
145
- const txt = llmsTxt(
146
- {
147
- url: 'https://x.test',
148
- title: 'My Site',
149
- description: 'a site',
150
- llms: { instructions: 'Be nice.' },
151
- },
152
- routes,
153
- );
154
- expect(txt).toContain('# My Site');
155
- expect(txt).toContain('> a site');
156
- expect(txt).toContain('Be nice.');
157
- expect(txt).toContain('[Home](https://x.test)');
158
- expect(txt).toContain('[/about](https://x.test/about)');
159
- });
160
-
161
- it('is empty when llms: false', () => {
162
- expect(llmsTxt({ llms: false }, routes)).toBe('');
163
- });
164
-
165
- it('renders a supplied page list (titles + descriptions, e.g. SSG pages) over the static paths', () => {
166
- const txt = llmsTxt({ url: 'https://x.test', title: 'Docs' }, routes, [
167
- { title: 'useReducer | React Hooks', url: 'https://x.test/hooks/usereducer', description: 'A reducer.' },
168
- { title: 'About', url: 'https://x.test/about' },
169
- ]);
170
- expect(txt).toContain('[useReducer | React Hooks](https://x.test/hooks/usereducer): A reducer.');
171
- expect(txt).toContain('[About](https://x.test/about)');
172
- // the supplied list replaces the bare static-path fallback
173
- expect(txt).not.toContain('[Home](https://x.test)');
174
- });
175
- });
@@ -1,69 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- import { afterEach, describe, expect, it } from 'vitest';
6
-
7
- import { loadConfig } from '../src/compiler/config';
8
- import { generate } from '../src/compiler/generate';
9
-
10
- const roots: string[] = [];
11
- function project(files: Record<string, string>): string {
12
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'toil-gen-'));
13
- roots.push(root);
14
- for (const [rel, content] of Object.entries(files)) {
15
- const abs = path.join(root, rel);
16
- fs.mkdirSync(path.dirname(abs), { recursive: true });
17
- fs.writeFileSync(abs, content);
18
- }
19
- return root;
20
- }
21
- afterEach(() => {
22
- for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
23
- });
24
-
25
- const COMP = `export default function C() { return null; }\n`;
26
- const LAYOUT = `export default function L({ children }) { return children; }\n`;
27
- const HTML = `<!doctype html><html><head></head><body><div id="root"></div></body></html>\n`;
28
-
29
- describe('generate: parallel-slot layout chains', () => {
30
- it('keeps the parent layout on the full-page route but drops it from the @slot route', async () => {
31
- const root = project({
32
- 'client/public/index.html': HTML,
33
- 'client/routes/gallery/layout.tsx': LAYOUT,
34
- 'client/routes/gallery/index.tsx': COMP,
35
- 'client/routes/gallery/photo/[id].tsx': COMP,
36
- 'client/routes/gallery/@modal/(.)photo/[id].tsx': COMP,
37
- });
38
- const cfg = await loadConfig({ root });
39
- generate(cfg);
40
- const lines = fs.readFileSync(path.join(cfg.toilDir, 'routes.ts'), 'utf8').split('\n');
41
-
42
- // The normal full-page route is wrapped by gallery/layout.
43
- const mainLine = lines.find((l) => l.includes('photo/[id]') && !l.includes('@modal'));
44
- expect(mainLine).toMatch(/gallery\/layout/);
45
-
46
- // The intercepting @modal slot route is rendered INTO gallery/layout's <Slot>, so it must not
47
- // re-include that layout (doing so recurses, the slot rendering itself forever).
48
- const slotLine = lines.find((l) => l.includes('@modal'));
49
- expect(slotLine).toContain('intercept: true');
50
- expect(slotLine).toMatch(/layouts: \[\]/);
51
- expect(slotLine).not.toMatch(/gallery\/layout/);
52
- });
53
-
54
- it('still applies a layout placed inside the @slot subtree', async () => {
55
- const root = project({
56
- 'client/public/index.html': HTML,
57
- 'client/routes/gallery/layout.tsx': LAYOUT,
58
- 'client/routes/gallery/@modal/layout.tsx': LAYOUT,
59
- 'client/routes/gallery/@modal/(.)photo/[id].tsx': COMP,
60
- });
61
- const cfg = await loadConfig({ root });
62
- generate(cfg);
63
- const lines = fs.readFileSync(path.join(cfg.toilDir, 'routes.ts'), 'utf8').split('\n');
64
- const slotLine = lines.find((l) => l.includes('@modal/(.)photo'));
65
- // The slot's own layout (inside @modal) applies; the parent gallery layout does not.
66
- expect(slotLine).toMatch(/@modal\/layout/);
67
- expect(slotLine).not.toMatch(/gallery\/layout/);
68
- });
69
- });
package/test/sri.test.ts DELETED
@@ -1,129 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- import { afterEach, describe, expect, it } from 'vitest';
6
-
7
- import { buildImportMapIntegrity, injectImportMap, injectSri } from '../src/compiler/sri';
8
-
9
- describe('injectSri', () => {
10
- const dirs: string[] = [];
11
- function outDir(files: Record<string, string>): string {
12
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'toil-sri-'));
13
- dirs.push(root);
14
- for (const [rel, body] of Object.entries(files)) {
15
- const full = path.join(root, rel);
16
- fs.mkdirSync(path.dirname(full), { recursive: true });
17
- fs.writeFileSync(full, body);
18
- }
19
- return root;
20
- }
21
- afterEach(() => {
22
- for (const d of dirs.splice(0)) fs.rmSync(d, { recursive: true, force: true });
23
- });
24
-
25
- it('adds sha384 integrity + crossorigin to a local module script', () => {
26
- const root = outDir({ 'assets/index-abc.js': 'console.log(1)' });
27
- const html = injectSri(
28
- '<script type="module" crossorigin src="/assets/index-abc.js"></script>',
29
- root,
30
- '/',
31
- );
32
- expect(html).toMatch(/integrity="sha384-[A-Za-z0-9+/]+=*"/);
33
- // Vite already put crossorigin on the tag, so it isn't duplicated.
34
- expect(html.match(/crossorigin/g)?.length).toBe(1);
35
- });
36
-
37
- it('adds integrity to stylesheets and modulepreload, plus a missing crossorigin', () => {
38
- const root = outDir({
39
- 'assets/a.css': 'body{}',
40
- 'assets/chunk.js': 'export default 1',
41
- });
42
- const css = injectSri('<link rel="stylesheet" href="/assets/a.css">', root, '/');
43
- expect(css).toMatch(/integrity="sha384-/);
44
- expect(css).toContain('crossorigin="anonymous"');
45
- const pre = injectSri('<link rel="modulepreload" href="/assets/chunk.js" />', root, '/');
46
- expect(pre).toMatch(/integrity="sha384-/);
47
- expect(pre.trimEnd().endsWith('/>')).toBe(true); // self-closing preserved
48
- });
49
-
50
- it('leaves external URLs, inline scripts, icons, and missing files alone', () => {
51
- const root = outDir({ 'assets/x.js': 'x' });
52
- const external = '<script src="https://cdn.example.com/a.js"></script>';
53
- expect(injectSri(external, root, '/')).toBe(external);
54
- const inline = '<script type="application/json">{"a":1}</script>';
55
- expect(injectSri(inline, root, '/')).toBe(inline);
56
- const icon = '<link rel="icon" href="/favicon.ico">';
57
- expect(injectSri(icon, root, '/')).toBe(icon);
58
- const missing = '<script src="/assets/gone.js"></script>';
59
- expect(injectSri(missing, root, '/')).toBe(missing); // unreadable -> skipped, not crashed
60
- });
61
-
62
- it('does not double-tag a script that already has integrity', () => {
63
- const root = outDir({ 'assets/x.js': 'x' });
64
- const already = '<script src="/assets/x.js" integrity="sha384-existing"></script>';
65
- expect(injectSri(already, root, '/')).toBe(already);
66
- });
67
-
68
- it('honors a non-root base', () => {
69
- const root = outDir({ 'assets/x.js': 'x' });
70
- const html = injectSri('<script src="/sub/assets/x.js"></script>', root, '/sub/');
71
- expect(html).toMatch(/integrity="sha384-/);
72
- });
73
- });
74
-
75
- describe('buildImportMapIntegrity + injectImportMap (module-graph coverage)', () => {
76
- const dirs: string[] = [];
77
- function outDir(files: Record<string, string>): string {
78
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'toil-srimap-'));
79
- dirs.push(root);
80
- for (const [rel, body] of Object.entries(files)) {
81
- const full = path.join(root, rel);
82
- fs.mkdirSync(path.dirname(full), { recursive: true });
83
- fs.writeFileSync(full, body);
84
- }
85
- return root;
86
- }
87
- afterEach(() => {
88
- for (const d of dirs.splice(0)) fs.rmSync(d, { recursive: true, force: true });
89
- });
90
-
91
- it('maps every emitted chunk (incl. ones no HTML tag references) with sha384', () => {
92
- const root = outDir({
93
- 'assets/index-abc.js': 'entry',
94
- 'assets/layout-def.js': 'lazy route chunk', // loaded via import(), never in a tag
95
- 'assets/react-ghi.js': 'vendor',
96
- 'assets/style-x.css': 'body{}', // not a module: excluded
97
- });
98
- const tag = buildImportMapIntegrity(root, '/');
99
- expect(tag).not.toBeNull();
100
- const map = JSON.parse((tag as string).replace(/^<script type="importmap">|<\/script>$/g, ''));
101
- expect(Object.keys(map.integrity).sort()).toEqual([
102
- '/assets/index-abc.js',
103
- '/assets/layout-def.js',
104
- '/assets/react-ghi.js',
105
- ]);
106
- for (const v of Object.values(map.integrity)) expect(v).toMatch(/^sha384-/);
107
- });
108
-
109
- it('prefixes a non-root base and returns null with no chunks', () => {
110
- const root = outDir({ 'assets/a.js': 'a' });
111
- const tag = buildImportMapIntegrity(root, '/sub/') as string;
112
- expect(tag).toContain('"/sub/assets/a.js"');
113
- const empty = outDir({ 'index.html': '<html></html>' });
114
- expect(buildImportMapIntegrity(empty, '/')).toBeNull();
115
- });
116
-
117
- it('injects after <head>, before any module script, and is idempotent', () => {
118
- const html = '<html><head><meta x></head><body><script type="module" src="/a.js"></script></body></html>';
119
- const tag = '<script type="importmap">{"integrity":{"/assets/a.js":"sha384-zzz"}}</script>';
120
- const out = injectImportMap(html, tag);
121
- expect(out.indexOf('importmap')).toBeGreaterThan(out.indexOf('<head>'));
122
- expect(out.indexOf('importmap')).toBeLessThan(out.indexOf('type="module"'));
123
- // Idempotent: a second pass does not duplicate the map.
124
- expect(injectImportMap(out, tag)).toBe(out);
125
- // No <head>: falls back to before the first <script>.
126
- const headless = injectImportMap('<script src="/x.js"></script>', tag);
127
- expect(headless.indexOf('importmap')).toBeLessThan(headless.indexOf('src="/x.js"'));
128
- });
129
- });
package/test/ssg.test.ts DELETED
@@ -1,36 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { sitemapXml, type SeoConfig } from '../src/compiler/seo';
4
- import { fillPattern } from '../src/compiler/ssg';
5
- import { type ScannedRoute } from '../src/compiler/routes';
6
-
7
- describe('fillPattern', () => {
8
- it('substitutes :param and *catch-all segments', () => {
9
- expect(fillPattern('/:a/:b/:c', { a: 'x', b: 'y', c: 'z' })).toBe('/x/y/z');
10
- expect(fillPattern('/blog/:id', { id: '42' })).toBe('/blog/42');
11
- expect(fillPattern('/docs/*slug', { slug: ['a', 'b'] })).toBe('/docs/a/b');
12
- expect(fillPattern('/static', {})).toBe('/static');
13
- });
14
- });
15
-
16
- describe('sitemapXml with SSG paths', () => {
17
- const seo: SeoConfig = { url: 'https://x.dev' };
18
- const routes: ScannedRoute[] = [
19
- { file: 'a', pattern: '/' },
20
- { file: 'b', pattern: '/about' },
21
- { file: 'c', pattern: '/blog/:id' },
22
- ];
23
-
24
- it('lists static routes plus enumerated SSG URLs, deduped, never the bare pattern', () => {
25
- const xml = sitemapXml(seo, routes, ['/blog/1', '/blog/2', '/about']);
26
- expect(xml).toContain('https://x.dev/blog/1');
27
- expect(xml).toContain('https://x.dev/blog/2');
28
- expect(xml).toContain('https://x.dev/about');
29
- expect(xml).not.toContain('/blog/:id'); // dynamic pattern is never listed literally
30
- expect((xml.match(/<loc>[^<]*\/about<\/loc>/g) ?? []).length).toBe(1); // deduped
31
- });
32
-
33
- it('is empty without a base url', () => {
34
- expect(sitemapXml({}, routes, ['/blog/1'])).toBe('');
35
- });
36
- });
@@ -1,122 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * Real-hydration test: drive the actual build (`extractRouteTemplate`, which now
4
- * renders with `renderToString`) -> splice -> `hydrateRoot` path in jsdom and
5
- * assert there is NO hydration mismatch and the content is present. This is the
6
- * failure users hit ("server rendered HTML didn't match the client"), and it
7
- * covers the things that broke it:
8
- * - "text + hole + text" (`Hello, <Hole>{name}</Hole>!`): needs the `<!-- -->`
9
- * text-boundary markers `renderToString` emits.
10
- * - an `<img>`, whose React 19 auto-preload `<link>` must be kept OUT of `#root`.
11
- * - an `<Island>`, which must be empty on the first (hydration) render.
12
- * - the Suspense dehydration markers (`<!--$-->`): `assembleRouteElement` wraps the
13
- * route (and each layout) in `Suspense`, so we hydrate through the SAME `Suspense`
14
- * structure the client Router renders. Remove that wrapping and this test fails
15
- * with "server rendered HTML didn't match the client", the exact regression.
16
- */
17
- import { act, Suspense } from 'react';
18
- import { hydrateRoot } from 'react-dom/client';
19
- import { describe, expect, it, vi } from 'vitest';
20
-
21
- import { Hole, Island, RawHtml, __setSsrBuild } from '../src/client/ssr/markers';
22
- import { extractRouteTemplate } from '../src/compiler/template-build';
23
- import { reactEscapeHtml, spliceTemplate } from '../src/compiler/template';
24
-
25
- const NAME = 'world';
26
- const BLURB = 'Rendered at the <strong>edge</strong>.';
27
-
28
- function Page(): React.ReactElement {
29
- return (
30
- <main>
31
- <img src="/images/logo.svg" alt="logo" width={28} height={28} />
32
- <h1>
33
- Hello, <Hole id="name">{NAME}</Hole>!
34
- </h1>
35
- <p>
36
- <RawHtml id="blurb" html={BLURB} as="span" />
37
- </p>
38
- <Island>
39
- <span className="isle">island-only</span>
40
- </Island>
41
- </main>
42
- );
43
- }
44
-
45
- const SHELL =
46
- '<!doctype html><html><head><title>t</title></head><body><div id="root"></div></body></html>';
47
-
48
- /** Build the template (renderToString + strip), splice per-slot values, return #root inner HTML. */
49
- function serverRootHtml(): string {
50
- const art = extractRouteTemplate({
51
- name: 'hyd',
52
- Page,
53
- layouts: [],
54
- loaderData: null,
55
- loaderContext: null,
56
- setSsrBuild: __setSsrBuild,
57
- shell: SHELL,
58
- });
59
- const valueFor: Record<number, Buffer> = {
60
- 0: Buffer.from(reactEscapeHtml(NAME), 'utf8'), // name (text)
61
- 1: Buffer.from(BLURB, 'utf8'), // blurb (raw)
62
- };
63
- const nSlots = art.slotsBin.readUInt16LE(44);
64
- const inserts: { offset: number; value: Buffer }[] = [];
65
- let o = 46;
66
- for (let i = 0; i < nSlots; i++) {
67
- inserts.push({ offset: art.slotsBin.readUInt32LE(o), value: valueFor[art.slotsBin.readUInt16LE(o + 4)] });
68
- o += 8;
69
- }
70
- const full = spliceTemplate(art.tmpl, inserts).toString('utf8');
71
- const m = /<div id="root">([\s\S]*?)<\/div><template id="__toil_ssr">/.exec(full);
72
- if (!m) throw new Error('could not isolate #root');
73
- return m[1];
74
- }
75
-
76
- describe('ssr hydration (real hydrateRoot, no mismatch)', () => {
77
- it('hydrates the spliced server markup cleanly and reveals the island after mount', async () => {
78
- const rootInner = serverRootHtml();
79
- expect(rootInner).not.toContain('rel="preload"'); // preload hoisted to <head>, not #root
80
- expect(rootInner).not.toContain('island-only'); // island empty server-side
81
- expect(rootInner).toContain('Hello, '); // text hole filled
82
- expect(rootInner).toContain('<strong>edge</strong>'); // raw hole verbatim
83
-
84
- document.body.innerHTML = `<div id="root">${rootInner}</div>`;
85
- const rootEl = document.getElementById('root')!;
86
-
87
- const recoverable: string[] = [];
88
- const consoleErrors: string[] = [];
89
- const spy = vi.spyOn(console, 'error').mockImplementation((...a: unknown[]) => {
90
- consoleErrors.push(a.map(String).join(' '));
91
- });
92
- try {
93
- await act(async () => {
94
- // Hydrate through the route Suspense `assembleRouteElement` emits (layouts: []
95
- // here -> just the route boundary), so the client's Suspense markers line up
96
- // with the server's. This is what the real client Router does.
97
- hydrateRoot(
98
- rootEl,
99
- (
100
- <Suspense fallback={null}>
101
- <Page />
102
- </Suspense>
103
- ),
104
- {
105
- onRecoverableError: (e) => recoverable.push(String(e)),
106
- },
107
- );
108
- });
109
- } finally {
110
- spy.mockRestore();
111
- }
112
-
113
- const noise = /hydrat|did not match|server rendered|didn't match/i;
114
- expect(recoverable.filter((e) => noise.test(e))).toEqual([]);
115
- expect(consoleErrors.filter((e) => noise.test(e))).toEqual([]);
116
-
117
- // Content survived hydration (not regenerated/blanked); the island revealed after mount.
118
- expect(rootEl.querySelector('h1')?.textContent).toBe('Hello, world!');
119
- expect(rootEl.querySelector('.isle')?.textContent).toBe('island-only');
120
- expect(rootEl.querySelector('img')?.getAttribute('src')).toBe('/images/logo.svg');
121
- });
122
- });