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.
- package/CHANGELOG.md +10 -0
- package/README.md +16 -7
- package/TYPESCRIPT_LAW.md +1946 -2073
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2375 -258
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.js +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/auth-emails.js +7 -3
- package/build/compiler/emails.js +1 -1
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +25 -4
- package/build/compiler/pages.js +2 -3
- package/build/compiler/prerender.d.ts +3 -8
- package/build/compiler/prerender.js +88 -127
- package/build/compiler/ssg.js +3 -4
- package/build/compiler/template-build.js +3 -4
- package/build/compiler/toil-docs.generated.js +6 -5
- package/build/compiler/vite.js +3 -6
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/database.js +7 -3
- package/build/devserver/db/types.js +1 -1
- package/build/devserver/production.js +4 -3
- package/build/devserver/runtime/crypto.d.ts +1 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/examples/basic/client/routes/analytics.tsx +7 -5
- package/examples/basic/server/tsconfig.json +1 -3
- package/package.json +45 -44
- package/presets/no-uint8array-tostring.js +120 -153
- package/presets/oxlint.d.ts +5 -0
- package/presets/oxlint.js +224 -0
- package/server/runtime/tsconfig.json +1 -3
- package/tsconfig.json +2 -11
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
- package/.github/changelog-config.json +0 -45
- package/.github/dependabot.yml +0 -27
- package/.github/workflows/ci.yml +0 -222
- package/.vscode/settings.json +0 -10
- package/docs/README.md +0 -107
- package/docs/auth/README.md +0 -104
- package/docs/auth/configuration.md +0 -105
- package/docs/auth/emails.md +0 -92
- package/docs/auth/extending.md +0 -202
- package/docs/auth/how-it-works.md +0 -138
- package/docs/auth/usage.md +0 -188
- package/docs/backend/README.md +0 -143
- package/docs/backend/data.md +0 -351
- package/docs/backend/rest.md +0 -402
- package/docs/backend/rpc.md +0 -226
- package/docs/background/README.md +0 -114
- package/docs/background/daemons.md +0 -276
- package/docs/background/derive.md +0 -179
- package/docs/cli/README.md +0 -379
- package/docs/concepts/ai-guide.md +0 -132
- package/docs/concepts/config.md +0 -416
- package/docs/concepts/decorators.md +0 -127
- package/docs/concepts/security.md +0 -108
- package/docs/concepts/tiers.md +0 -166
- package/docs/concepts/types.md +0 -216
- package/docs/database/README.md +0 -143
- package/docs/database/capacity.md +0 -350
- package/docs/database/counters.md +0 -174
- package/docs/database/documents.md +0 -277
- package/docs/database/events.md +0 -307
- package/docs/database/membership.md +0 -246
- package/docs/database/setup.md +0 -155
- package/docs/database/unique.md +0 -216
- package/docs/database/views.md +0 -246
- package/docs/frontend/README.md +0 -104
- package/docs/frontend/components.md +0 -286
- package/docs/frontend/data-fetching.md +0 -260
- package/docs/frontend/images.md +0 -148
- package/docs/frontend/metadata.md +0 -344
- package/docs/frontend/navigation.md +0 -296
- package/docs/frontend/rendering.md +0 -236
- package/docs/frontend/routing.md +0 -346
- package/docs/frontend/scripts.md +0 -118
- package/docs/frontend/search.md +0 -191
- package/docs/frontend/styling.md +0 -147
- package/docs/frontend/toil-global.md +0 -216
- package/docs/getting-started/README.md +0 -81
- package/docs/getting-started/create-project.md +0 -131
- package/docs/getting-started/deploy.md +0 -101
- package/docs/getting-started/first-app.md +0 -215
- package/docs/getting-started/installation.md +0 -124
- package/docs/getting-started/migrating.md +0 -125
- package/docs/getting-started/project-structure.md +0 -163
- package/docs/introduction/README.md +0 -66
- package/docs/introduction/design-principles.md +0 -81
- package/docs/introduction/distributed.md +0 -91
- package/docs/introduction/how-it-works.md +0 -121
- package/docs/introduction/hyperscale.md +0 -70
- package/docs/introduction/modern-stack.md +0 -85
- package/docs/introduction/vs-other-frameworks.md +0 -64
- package/docs/introduction/why-toil.md +0 -99
- package/docs/llms.txt +0 -95
- package/docs/realtime/README.md +0 -102
- package/docs/realtime/channels.md +0 -211
- package/docs/realtime/streams.md +0 -369
- package/docs/services/README.md +0 -60
- package/docs/services/analytics.md +0 -279
- package/docs/services/caching.md +0 -175
- package/docs/services/cookies.md +0 -235
- package/docs/services/crypto.md +0 -209
- package/docs/services/email.md +0 -289
- package/docs/services/environment.md +0 -141
- package/docs/services/ratelimit.md +0 -174
- package/docs/services/time.md +0 -85
- package/eslint.config.js +0 -57
- package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
- package/presets/eslint.js +0 -88
- package/src/backend/index.ts +0 -204
- package/src/cli/configure.ts +0 -363
- package/src/cli/create.ts +0 -924
- package/src/cli/db.ts +0 -158
- package/src/cli/diagnostics.ts +0 -665
- package/src/cli/doctor.ts +0 -982
- package/src/cli/features.ts +0 -170
- package/src/cli/index.ts +0 -326
- package/src/cli/notify.ts +0 -120
- package/src/cli/proc.ts +0 -56
- package/src/cli/ui.ts +0 -169
- package/src/cli/update.ts +0 -248
- package/src/cli/updates.ts +0 -92
- package/src/cli/validate.ts +0 -31
- package/src/cli/version-check.ts +0 -125
- package/src/client/auth.ts +0 -869
- package/src/client/channel/channel.ts +0 -146
- package/src/client/components/Form.tsx +0 -65
- package/src/client/components/Image.tsx +0 -160
- package/src/client/components/Script.tsx +0 -113
- package/src/client/components/Slot.tsx +0 -21
- package/src/client/dev/devtools.tsx +0 -1089
- package/src/client/dev/error-overlay.tsx +0 -225
- package/src/client/errors.ts +0 -11
- package/src/client/head/head.ts +0 -182
- package/src/client/head/metadata.ts +0 -110
- package/src/client/index.ts +0 -130
- package/src/client/navigation/Link.tsx +0 -108
- package/src/client/navigation/NavLink.tsx +0 -86
- package/src/client/navigation/navigation.ts +0 -235
- package/src/client/navigation/prefetch.ts +0 -169
- package/src/client/navigation/scroll.ts +0 -53
- package/src/client/routing/Router.tsx +0 -216
- package/src/client/routing/action.ts +0 -122
- package/src/client/routing/error-boundary.tsx +0 -43
- package/src/client/routing/hooks.ts +0 -140
- package/src/client/routing/lazy.ts +0 -77
- package/src/client/routing/loader.ts +0 -381
- package/src/client/routing/match.ts +0 -47
- package/src/client/routing/mount.tsx +0 -90
- package/src/client/routing/params-context.ts +0 -10
- package/src/client/routing/slot-context.ts +0 -7
- package/src/client/rpc.ts +0 -97
- package/src/client/search/search.ts +0 -189
- package/src/client/search/use-page-search.ts +0 -73
- package/src/client/ssr/markers.tsx +0 -172
- package/src/client/stream/client.ts +0 -237
- package/src/client/types.ts +0 -73
- package/src/compiler/auth-emails.ts +0 -312
- package/src/compiler/config.ts +0 -369
- package/src/compiler/docs.ts +0 -127
- package/src/compiler/email-preview.ts +0 -314
- package/src/compiler/emails.ts +0 -404
- package/src/compiler/fonts.ts +0 -89
- package/src/compiler/generate.ts +0 -471
- package/src/compiler/image-blur.ts +0 -42
- package/src/compiler/image-report.ts +0 -88
- package/src/compiler/index.ts +0 -1362
- package/src/compiler/pages.ts +0 -49
- package/src/compiler/plugin.ts +0 -391
- package/src/compiler/prerender.ts +0 -263
- package/src/compiler/routes.ts +0 -170
- package/src/compiler/seo.ts +0 -415
- package/src/compiler/sri.ts +0 -144
- package/src/compiler/ssg.ts +0 -177
- package/src/compiler/ssr-codegen.ts +0 -85
- package/src/compiler/template-build.ts +0 -676
- package/src/compiler/template.ts +0 -262
- package/src/compiler/toil-docs.generated.ts +0 -77
- package/src/compiler/vite.ts +0 -245
- package/src/devserver/analytics/index.ts +0 -317
- package/src/devserver/config/dotenv.ts +0 -102
- package/src/devserver/config/env.ts +0 -30
- package/src/devserver/config/ratelimit.ts +0 -123
- package/src/devserver/daemon/catalog.ts +0 -120
- package/src/devserver/daemon/cron.ts +0 -87
- package/src/devserver/daemon/host.ts +0 -134
- package/src/devserver/daemon/index.ts +0 -369
- package/src/devserver/daemon/runtime.ts +0 -48
- package/src/devserver/db/catalog.ts +0 -109
- package/src/devserver/db/database.ts +0 -1834
- package/src/devserver/db/derives.ts +0 -121
- package/src/devserver/db/index.ts +0 -20
- package/src/devserver/db/routeKinds.ts +0 -191
- package/src/devserver/db/types.ts +0 -168
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +0 -141
- package/src/devserver/email/console.ts +0 -348
- package/src/devserver/email/index.ts +0 -155
- package/src/devserver/email/providers.ts +0 -131
- package/src/devserver/email/status.ts +0 -23
- package/src/devserver/email/validate.ts +0 -37
- package/src/devserver/email/wire.ts +0 -55
- package/src/devserver/http/cache.ts +0 -0
- package/src/devserver/http/envelope.ts +0 -157
- package/src/devserver/http/proxy.ts +0 -172
- package/src/devserver/http/runtime.ts +0 -387
- package/src/devserver/index.ts +0 -43
- package/src/devserver/production-ipc.ts +0 -63
- package/src/devserver/production-worker.ts +0 -83
- package/src/devserver/production.ts +0 -718
- package/src/devserver/runtime/crypto.ts +0 -587
- package/src/devserver/runtime/host.ts +0 -427
- package/src/devserver/runtime/module.ts +0 -442
- package/src/devserver/server.ts +0 -251
- package/src/devserver/ssr.ts +0 -208
- package/src/devserver/stream/catalog.ts +0 -106
- package/src/devserver/stream/host.ts +0 -42
- package/src/devserver/stream/index.ts +0 -301
- package/src/devserver/stream/manager.ts +0 -167
- package/src/devserver/stream/router.ts +0 -101
- package/src/devserver/stream/wire.ts +0 -58
- package/src/devserver/stream/ws.ts +0 -78
- package/src/devserver/wasm/sections.ts +0 -59
- package/src/devserver/wasm/surface.ts +0 -115
- package/src/io/FastMap.ts +0 -151
- package/src/io/FastSet.ts +0 -113
- package/src/io/codec.ts +0 -362
- package/src/io/index.ts +0 -10
- package/src/io/lengths.ts +0 -14
- package/src/io/types.ts +0 -19
- package/src/logger/index.ts +0 -22
- package/src/shared/index.ts +0 -46
- package/test/analytics-dev.test.ts +0 -113
- package/test/assembly/aspect-shim.ts +0 -33
- package/test/assembly/cookie.spec.ts +0 -303
- package/test/assembly/example.spec.ts +0 -22
- package/test/assembly/ssr.spec.ts +0 -95
- package/test/assembly.test.ts +0 -59
- package/test/auth-emails.test.ts +0 -118
- package/test/built-ssr.test.ts +0 -148
- package/test/channel.test.ts +0 -21
- package/test/config-host-port.test.ts +0 -59
- package/test/configure.test.ts +0 -94
- package/test/daemon-build.test.ts +0 -278
- package/test/daemon-catalog.test.ts +0 -349
- package/test/daemon-emulation.test.ts +0 -256
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +0 -1102
- package/test/devserver-email.test.ts +0 -306
- package/test/devserver-pqauth.test.ts +0 -93
- package/test/devserver-secrets.test.ts +0 -63
- package/test/devserver.test.ts +0 -259
- package/test/doctor.test.ts +0 -298
- package/test/dom/Image.test.tsx +0 -144
- package/test/dom/Link.test.tsx +0 -67
- package/test/dom/NavLink.test.tsx +0 -37
- package/test/dom/Script.test.tsx +0 -48
- package/test/dom/action.test.tsx +0 -146
- package/test/dom/error-overlay.test.tsx +0 -44
- package/test/dom/loader.test.tsx +0 -121
- package/test/dom/navigation.test.ts +0 -59
- package/test/dom/revalidate.test.tsx +0 -38
- package/test/dom/route-head.test.tsx +0 -56
- package/test/dom/router-loading.test.tsx +0 -44
- package/test/dom/scroll.test.ts +0 -56
- package/test/dom/slot.test.tsx +0 -131
- package/test/dom/use-metadata.test.tsx +0 -58
- package/test/dom/view-transitions.test.tsx +0 -53
- package/test/dynamic-routing.test.ts +0 -132
- package/test/email-console.test.ts +0 -86
- package/test/email-preview.test.ts +0 -73
- package/test/emails.test.ts +0 -58
- package/test/errors.test.ts +0 -21
- package/test/example-guestbook.test.ts +0 -123
- package/test/features.test.ts +0 -149
- package/test/fixtures/bignum-wire/spec.ts +0 -25
- package/test/fixtures/daemon-app.ts +0 -65
- package/test/fixtures/stream-echo.ts +0 -26
- package/test/fixtures/stream-gate.ts +0 -28
- package/test/fixtures/stream-trap.ts +0 -18
- package/test/fixtures/stream-typed.ts +0 -41
- package/test/fonts.test.ts +0 -28
- package/test/global-setup.ts +0 -17
- package/test/head.test.ts +0 -42
- package/test/image-blur.test.ts +0 -50
- package/test/io.test.ts +0 -117
- package/test/metadata.test.ts +0 -40
- package/test/navlink.test.ts +0 -28
- package/test/pages.test.ts +0 -105
- package/test/placeholder.test.ts +0 -9
- package/test/pqauth-e2e.test.ts +0 -216
- package/test/pqauth-email.test.ts +0 -801
- package/test/prerender.test.ts +0 -116
- package/test/prettier-plugin.test.ts +0 -46
- package/test/routes.test.ts +0 -76
- package/test/rpc-bignum-wire.test.ts +0 -164
- package/test/rpc-dispatch.test.ts +0 -132
- package/test/rpc-kinds.test.ts +0 -18
- package/test/rpc.test.ts +0 -66
- package/test/search.test.ts +0 -114
- package/test/seo.test.ts +0 -175
- package/test/slot-layouts.test.ts +0 -69
- package/test/sri.test.ts +0 -129
- package/test/ssg.test.ts +0 -36
- package/test/ssr-hydration.test.tsx +0 -122
- package/test/ssr-render.test.ts +0 -197
- package/test/ssr-template.test.tsx +0 -482
- package/test/stream-emulation.test.ts +0 -479
- package/test/ui.test.ts +0 -40
- package/test/update.test.ts +0 -58
- package/test/validate.test.ts +0 -42
- package/test/version-check.test.ts +0 -130
- package/tests/data-parity/generated-parity.ts +0 -99
- package/tests/data-parity/parity.ts +0 -80
- package/tests/data-parity/spec.ts +0 -46
package/test/search.test.ts
DELETED
|
@@ -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 "b" <c>"',
|
|
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
|
-
});
|