evolit 0.1.0-alpha.20 → 0.1.0-alpha.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/package.json +17 -2
- package/src/build.js +30 -8
- package/src/client-assets.js +34 -0
- package/src/deployment-runtime.js +83 -24
- package/src/development-events.js +9 -0
- package/src/navigation-client.js +546 -0
- package/src/navigation-server.js +3 -0
- package/src/navigation-url.js +28 -0
- package/src/render.js +333 -14
- package/src/route-segments.js +273 -0
- package/src/server.js +6 -2
- package/src/ssr-adapter.js +127 -12
- package/src/urql-ssr.js +60 -0
package/README.md
CHANGED
|
@@ -76,6 +76,70 @@ The current runtime is split into a few small layers:
|
|
|
76
76
|
- `src/scaffold.js`: creates new site projects from templates
|
|
77
77
|
- `src/cli.js`: framework entrypoint
|
|
78
78
|
|
|
79
|
+
## Client Navigation
|
|
80
|
+
|
|
81
|
+
Evolit hydrates a small browser router automatically for SSR page documents. It requests a route
|
|
82
|
+
delta, replaces only the changed route segment, and keeps parent layouts mounted when possible.
|
|
83
|
+
Plain `<a>` elements remain valid SSR HTML; same-origin links are progressively intercepted after
|
|
84
|
+
hydration.
|
|
85
|
+
|
|
86
|
+
Use `useNavigation()` inside a LitSX browser component for imperative navigation and pending UI:
|
|
87
|
+
|
|
88
|
+
```jsx
|
|
89
|
+
import { useNavigation } from "evolit/navigation";
|
|
90
|
+
|
|
91
|
+
export default function CollectionControls() {
|
|
92
|
+
const navigation = useNavigation();
|
|
93
|
+
|
|
94
|
+
function changeSort(event) {
|
|
95
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
96
|
+
searchParams.delete("page");
|
|
97
|
+
searchParams.delete("skip");
|
|
98
|
+
searchParams.set("sort", event.target.value);
|
|
99
|
+
navigation.push(navigation.createHref("/explore/home-garden", searchParams));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return <select onChange={changeSort} disabled={navigation.status === "pending"}>…</select>;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The hook returns `{ status, url, pendingUrl, error, push, replace, refresh, createHref }`:
|
|
107
|
+
|
|
108
|
+
- `push(target)` adds a browser-history entry.
|
|
109
|
+
- `replace(target)` updates the current entry, useful for visual-only query state.
|
|
110
|
+
- `refresh()` bypasses the client delta cache for the current URL.
|
|
111
|
+
- `createHref(pathname, searchParams)` creates a relative internal URL. It accepts standard
|
|
112
|
+
`URLSearchParams`, preserving repeated keys such as `facet=brand&facet=material`.
|
|
113
|
+
|
|
114
|
+
`createHref` can also be imported directly from `evolit/navigation`; it is browser-free and safe to
|
|
115
|
+
share with server-evaluated route code. `useNavigation()` itself is browser-only and must only run
|
|
116
|
+
from a connected client component.
|
|
117
|
+
|
|
118
|
+
### Progressive links and forms
|
|
119
|
+
|
|
120
|
+
Links work without JavaScript. With JavaScript, Evolit intercepts ordinary same-origin left-clicks.
|
|
121
|
+
Set `data-evolit-navigation="false"` on a link to keep native navigation.
|
|
122
|
+
|
|
123
|
+
Internal `<form method="get">` elements are treated the same way: their successful controls become
|
|
124
|
+
`URLSearchParams` and navigate through a delta. Without JavaScript the browser submits the exact
|
|
125
|
+
same GET form normally. Evolit intentionally does not intercept `POST`, file-upload, external,
|
|
126
|
+
targeted, or opted-out forms.
|
|
127
|
+
|
|
128
|
+
### Navigation cache and document updates
|
|
129
|
+
|
|
130
|
+
The browser cache is scoped to browser-history entries, not a global URL map. Going back or forward
|
|
131
|
+
can reuse the delta for that exact entry; opening a new branch after going back discards its known
|
|
132
|
+
forward branch. This avoids an unbounded catalog cache in a long-lived tab.
|
|
133
|
+
|
|
134
|
+
- `dynamic` routes are never cached in the browser.
|
|
135
|
+
- `revalidate` entries remain reusable only until their route TTL expires.
|
|
136
|
+
- `static` entries remain reusable while their history entry exists in the current tab session.
|
|
137
|
+
|
|
138
|
+
Each delta also synchronizes route `<title>`, route-specific `<head>` markup, managed styles and
|
|
139
|
+
module preloads, `html`/`body` attributes, scroll position, hash targets, and focus. If a response
|
|
140
|
+
cannot be represented as an Evolit delta —for example a 404 from another adapter— navigation falls
|
|
141
|
+
back to a normal document load.
|
|
142
|
+
|
|
79
143
|
## Route Cache Policies
|
|
80
144
|
|
|
81
145
|
Route modules can export a `routeConfig` object with a `cache` policy:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolit",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.22",
|
|
4
4
|
"description": "A convention-driven application framework for LitSX and web components.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "yarn@4.10.3",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"main": "./src/index.js",
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./src/index.js",
|
|
23
|
+
"./navigation": {
|
|
24
|
+
"browser": "./src/navigation-client.js",
|
|
25
|
+
"default": "./src/navigation-server.js"
|
|
26
|
+
},
|
|
23
27
|
"./server": {
|
|
24
28
|
"browser": "./src/request-context-browser.js",
|
|
25
29
|
"default": "./src/server-api.js"
|
|
@@ -29,7 +33,8 @@
|
|
|
29
33
|
"dev": "node ./src/cli.js dev",
|
|
30
34
|
"build": "node ./src/cli.js build",
|
|
31
35
|
"start": "node ./src/cli.js start",
|
|
32
|
-
"test": "node --test",
|
|
36
|
+
"test": "node --test test/*.test.js",
|
|
37
|
+
"test:browser": "playwright test",
|
|
33
38
|
"typecheck": "litsx-tsc -p jsconfig.json --noEmit",
|
|
34
39
|
"release:check": "yarn test && yarn typecheck && yarn pack --dry-run"
|
|
35
40
|
},
|
|
@@ -58,7 +63,17 @@
|
|
|
58
63
|
"typescript": "^6.0.0",
|
|
59
64
|
"ws": "^8.18.3"
|
|
60
65
|
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@litsx/urql": "^0.3.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@litsx/urql": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
61
74
|
"devDependencies": {
|
|
75
|
+
"@playwright/test": "^1.62.0",
|
|
76
|
+
"@webcomponents/scoped-custom-element-registry": "^0.0.10",
|
|
62
77
|
"vite": "^8.1.5"
|
|
63
78
|
}
|
|
64
79
|
}
|
package/src/build.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
emitBundledClientAssets,
|
|
16
16
|
emitHashedClientAssets,
|
|
17
17
|
normalizeHydrationDataForClient,
|
|
18
|
+
resolveHydrationRootClientImports,
|
|
18
19
|
resolveRouteClientImports,
|
|
19
20
|
resolveSharedVendorModuleUrl,
|
|
20
21
|
rewriteHydrationDataScript,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
import { serializeRouteCachePolicy } from "./route-config.js";
|
|
40
41
|
import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
|
|
41
42
|
import { ensureDirectory, writeJson } from "./fs-utils.js";
|
|
43
|
+
import { appendSsrUrqlData, runWithOptionalSsrUrqlScope } from "./urql-ssr.js";
|
|
42
44
|
|
|
43
45
|
const CONTENT_TYPE_BY_EXTENSION = new Map([
|
|
44
46
|
[".css", "text/css; charset=utf-8"],
|
|
@@ -247,12 +249,18 @@ export async function buildProject(projectRoot) {
|
|
|
247
249
|
...(Array.isArray(result.clientImports) ? result.clientImports : []),
|
|
248
250
|
...resolveRouteClientImports(routeResult, projectRoot, clientAssets),
|
|
249
251
|
];
|
|
250
|
-
const
|
|
252
|
+
const hydratedClientImports = Array.isArray(result.clientImports) ? result.clientImports : [];
|
|
253
|
+
const urls = hydratedClientImports.length > 0
|
|
254
|
+
? [...new Set([
|
|
255
|
+
...hydratedClientImports,
|
|
256
|
+
...collectTransitiveAssetPreloads(hydratedClientImports, clientAssets),
|
|
257
|
+
])]
|
|
258
|
+
: [];
|
|
251
259
|
const styleUrls = collectTransitiveStyleUrls(clientImports, clientAssets);
|
|
252
260
|
|
|
253
261
|
return [
|
|
254
|
-
...urls.map((href) => `<link rel="modulepreload" href="${href}">`),
|
|
255
|
-
...styleUrls.map((href) => `<link rel="stylesheet" href="${href}">`),
|
|
262
|
+
...urls.map((href) => `<link rel="modulepreload" href="${href}" data-evolit-route-asset="preload">`),
|
|
263
|
+
...styleUrls.map((href) => `<link rel="stylesheet" href="${href}" data-evolit-route-asset="style">`),
|
|
256
264
|
].join("\n");
|
|
257
265
|
},
|
|
258
266
|
resolveBootstrap({ routeResult, result }) {
|
|
@@ -282,7 +290,10 @@ export async function buildProject(projectRoot) {
|
|
|
282
290
|
normalizeHydrationDataForClient(
|
|
283
291
|
result.hydrationData,
|
|
284
292
|
projectRoot,
|
|
285
|
-
|
|
293
|
+
[
|
|
294
|
+
...routeClientImports,
|
|
295
|
+
...resolveHydrationRootClientImports(result.hydrationData, assetResolver),
|
|
296
|
+
],
|
|
286
297
|
),
|
|
287
298
|
);
|
|
288
299
|
},
|
|
@@ -331,12 +342,23 @@ export async function buildProject(projectRoot) {
|
|
|
331
342
|
seenPrerenderTargets.add(targetPathname);
|
|
332
343
|
|
|
333
344
|
const targetRequest = new Request(`http://evolit.local${targetPathname}`);
|
|
334
|
-
const routeResult = await
|
|
335
|
-
|
|
345
|
+
const { routeResult, response } = await runWithOptionalSsrUrqlScope(async (urqlAdapter) => {
|
|
346
|
+
const resolvedRouteResult = await routeResolver.resolveRequest(targetRequest);
|
|
347
|
+
if (resolvedRouteResult.type !== "route" || resolvedRouteResult.cachePolicy.mode === "dynamic") {
|
|
348
|
+
return { routeResult: resolvedRouteResult, response: null };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const renderedResponse = await renderRouteTreeWithAdapter(resolvedRouteResult, ssrAdapter);
|
|
352
|
+
return {
|
|
353
|
+
routeResult: resolvedRouteResult,
|
|
354
|
+
response: urqlAdapter
|
|
355
|
+
? appendSsrUrqlData(renderedResponse, await urqlAdapter.getUrqlSsrData())
|
|
356
|
+
: renderedResponse,
|
|
357
|
+
};
|
|
358
|
+
});
|
|
359
|
+
if (!response) {
|
|
336
360
|
continue;
|
|
337
361
|
}
|
|
338
|
-
|
|
339
|
-
const response = await renderRouteTreeWithAdapter(routeResult, ssrAdapter);
|
|
340
362
|
if (response.status !== 200) {
|
|
341
363
|
continue;
|
|
342
364
|
}
|
package/src/client-assets.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, realpathSync } from "node:fs";
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import MagicString from "magic-string";
|
|
7
8
|
import { rollup } from "rollup";
|
|
8
9
|
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
|
@@ -24,10 +25,12 @@ const MODULE_SPECIFIER_PATTERN =
|
|
|
24
25
|
/\b(?:import|export)\s*[^"']*?from\s*["']([^"']+)["']|\bimport\s*["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
25
26
|
|
|
26
27
|
const requireFromHere = createRequire(import.meta.url);
|
|
28
|
+
const EVOLIT_NAVIGATION_BROWSER_ENTRY = fileURLToPath(new URL("./navigation-client.js", import.meta.url));
|
|
27
29
|
const SHARED_VENDOR_SPECIFIERS = [
|
|
28
30
|
"@litsx/core",
|
|
29
31
|
"@litsx/core/elements",
|
|
30
32
|
"@litsx/ssr/hydration",
|
|
33
|
+
"evolit/navigation",
|
|
31
34
|
"lit",
|
|
32
35
|
];
|
|
33
36
|
const browserSpecifierFilePathCache = new Map();
|
|
@@ -516,6 +519,15 @@ export async function buildSharedVendorRuntime(
|
|
|
516
519
|
const bundle = await rollup({
|
|
517
520
|
input: inputEntries,
|
|
518
521
|
plugins: [
|
|
522
|
+
{
|
|
523
|
+
name: "evolit-browser-exports",
|
|
524
|
+
resolveId(source) {
|
|
525
|
+
// A consuming application does not normally have an `evolit`
|
|
526
|
+
// package nested in its node_modules. Resolve Evolit's own
|
|
527
|
+
// browser-only public entry before node-resolve marks it external.
|
|
528
|
+
return source === "evolit/navigation" ? EVOLIT_NAVIGATION_BROWSER_ENTRY : null;
|
|
529
|
+
},
|
|
530
|
+
},
|
|
519
531
|
nodeResolve({
|
|
520
532
|
browser: true,
|
|
521
533
|
exportConditions: ["browser", "import", "default"],
|
|
@@ -1084,6 +1096,7 @@ export function createHydrationBootstrap({
|
|
|
1084
1096
|
hydrationData,
|
|
1085
1097
|
assetResolver,
|
|
1086
1098
|
hydrationModuleUrl = "@litsx/ssr/hydration",
|
|
1099
|
+
navigationModuleUrl = null,
|
|
1087
1100
|
}) {
|
|
1088
1101
|
const normalizedHydrationData = normalizeHydrationDataForClient(hydrationData);
|
|
1089
1102
|
const rootModuleUrls = [...new Set([
|
|
@@ -1113,6 +1126,7 @@ export function createHydrationBootstrap({
|
|
|
1113
1126
|
|
|
1114
1127
|
const source = `
|
|
1115
1128
|
import { hydratePage, registerHydrationModules } from ${JSON.stringify(hydrationModuleUrl)};
|
|
1129
|
+
${navigationModuleUrl ? `import { getNavigation } from ${JSON.stringify(navigationModuleUrl)};` : ""}
|
|
1116
1130
|
|
|
1117
1131
|
await hydratePage({
|
|
1118
1132
|
clientImports: ${JSON.stringify(clientImports)},
|
|
@@ -1120,6 +1134,7 @@ await hydratePage({
|
|
|
1120
1134
|
${moduleLoaders}
|
|
1121
1135
|
])
|
|
1122
1136
|
});
|
|
1137
|
+
${navigationModuleUrl ? "getNavigation();" : ""}
|
|
1123
1138
|
`;
|
|
1124
1139
|
|
|
1125
1140
|
return escapeInlineScriptText(source);
|
|
@@ -2017,6 +2032,25 @@ export function resolveRouteClientImports(routeResult, projectRoot, assetManifes
|
|
|
2017
2032
|
}).filter((publicUrl) => typeof publicUrl === "string"))];
|
|
2018
2033
|
}
|
|
2019
2034
|
|
|
2035
|
+
/**
|
|
2036
|
+
* Resolve the client modules which define the hydrated custom-element roots.
|
|
2037
|
+
*
|
|
2038
|
+
* Route entry modules are not required to re-export their LitSX components.
|
|
2039
|
+
* The document bootstrap has always loaded these root modules independently;
|
|
2040
|
+
* navigation deltas need the same information in their hydration payload.
|
|
2041
|
+
*/
|
|
2042
|
+
export function resolveHydrationRootClientImports(hydrationData, assetResolver) {
|
|
2043
|
+
if (!hydrationData || typeof hydrationData !== "object" || typeof assetResolver !== "function") {
|
|
2044
|
+
return [];
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
return [...new Set(
|
|
2048
|
+
(Array.isArray(hydrationData.roots) ? hydrationData.roots : [])
|
|
2049
|
+
.map((root) => assetResolver(root?.moduleId))
|
|
2050
|
+
.filter((publicUrl) => typeof publicUrl === "string" && publicUrl.length > 0),
|
|
2051
|
+
)];
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2020
2054
|
export function collectTransitiveStyleUrls(publicUrls, assetManifest) {
|
|
2021
2055
|
const normalizedManifest = normalizeClientAssetManifest(assetManifest);
|
|
2022
2056
|
if (!normalizedManifest) {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
normalizeHydrationDataForClient,
|
|
18
18
|
normalizeClientAssetManifest,
|
|
19
19
|
resetDevelopmentAssetCaches,
|
|
20
|
+
resolveHydrationRootClientImports,
|
|
20
21
|
resolveRouteClientImports,
|
|
21
22
|
resolveSharedVendorModuleUrl,
|
|
22
23
|
resolveBrowserPackageAssetFilePath,
|
|
@@ -32,6 +33,8 @@ import {
|
|
|
32
33
|
resolveResponseCacheRuntime,
|
|
33
34
|
} from "./response-cache.js";
|
|
34
35
|
import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
|
|
36
|
+
import { createNavigationResponseFromDocument } from "./route-segments.js";
|
|
37
|
+
import { appendSsrUrqlData, runWithOptionalSsrUrqlScope } from "./urql-ssr.js";
|
|
35
38
|
const CONTENT_TYPE_BY_EXTENSION = new Map([
|
|
36
39
|
[".css", "text/css; charset=utf-8"],
|
|
37
40
|
[".svg", "image/svg+xml"],
|
|
@@ -232,6 +235,11 @@ export async function createRequestRenderer({
|
|
|
232
235
|
mode,
|
|
233
236
|
"@litsx/ssr/hydration",
|
|
234
237
|
);
|
|
238
|
+
let currentNavigationModuleUrl = await resolveSharedVendorModuleUrl(
|
|
239
|
+
projectRoot,
|
|
240
|
+
mode,
|
|
241
|
+
"evolit/navigation",
|
|
242
|
+
);
|
|
235
243
|
const devBundledEntries = new Set();
|
|
236
244
|
const devPreparedClientModules = new Set();
|
|
237
245
|
const devClientModuleDependencies = new Map();
|
|
@@ -244,6 +252,7 @@ export async function createRequestRenderer({
|
|
|
244
252
|
selectivelyInvalidatedClientEntries: 0,
|
|
245
253
|
};
|
|
246
254
|
const usesFrameworkRouteResolver = !routeResolver;
|
|
255
|
+
let sharedSegmentRenderCache = null;
|
|
247
256
|
|
|
248
257
|
function enqueueDevClientAssetWork(work) {
|
|
249
258
|
const nextWork = devClientAssetWork.then(work, work);
|
|
@@ -261,12 +270,18 @@ export async function createRequestRenderer({
|
|
|
261
270
|
return path.resolve(projectRoot, clientModule);
|
|
262
271
|
}
|
|
263
272
|
async function createFrameworkRouteResolver() {
|
|
264
|
-
|
|
273
|
+
const resolver = await createRouteResolver(projectRoot, mode, {
|
|
265
274
|
onDevelopmentEvent,
|
|
275
|
+
segmentRenderCache: sharedSegmentRenderCache,
|
|
266
276
|
getStaticAssetPublicUrls() {
|
|
267
277
|
return createStaticAssetPublicUrlMap(currentAssetManifest);
|
|
268
278
|
},
|
|
279
|
+
assetResolver(moduleId) {
|
|
280
|
+
return currentAssetResolver(moduleId);
|
|
281
|
+
},
|
|
269
282
|
});
|
|
283
|
+
sharedSegmentRenderCache ??= resolver.segmentRenderCache;
|
|
284
|
+
return resolver;
|
|
270
285
|
}
|
|
271
286
|
let effectiveRouteResolver = routeResolver ?? await createFrameworkRouteResolver();
|
|
272
287
|
const ssrAdapter = createSsrAdapter({
|
|
@@ -278,8 +293,12 @@ export async function createRequestRenderer({
|
|
|
278
293
|
...(Array.isArray(result.clientImports) ? result.clientImports : []),
|
|
279
294
|
...resolveRouteClientImports(routeResult, projectRoot, currentAssetManifest),
|
|
280
295
|
];
|
|
281
|
-
const
|
|
282
|
-
|
|
296
|
+
const hydratedClientImports = Array.isArray(result.clientImports) ? result.clientImports : [];
|
|
297
|
+
const urls = currentAssetManifest && hydratedClientImports.length > 0
|
|
298
|
+
? [...new Set([
|
|
299
|
+
...hydratedClientImports,
|
|
300
|
+
...collectTransitiveAssetPreloads(hydratedClientImports, currentAssetManifest),
|
|
301
|
+
])]
|
|
283
302
|
: [];
|
|
284
303
|
const styleUrls = currentAssetManifest
|
|
285
304
|
? collectTransitiveStyleUrls(clientImports, currentAssetManifest)
|
|
@@ -289,8 +308,8 @@ export async function createRequestRenderer({
|
|
|
289
308
|
}
|
|
290
309
|
|
|
291
310
|
return [
|
|
292
|
-
...urls.map((href) => `<link rel="modulepreload" href="${href}">`),
|
|
293
|
-
...styleUrls.map((href) => `<link rel="stylesheet" href="${href}">`),
|
|
311
|
+
...urls.map((href) => `<link rel="modulepreload" href="${href}" data-evolit-route-asset="preload">`),
|
|
312
|
+
...styleUrls.map((href) => `<link rel="stylesheet" href="${href}" data-evolit-route-asset="style">`),
|
|
294
313
|
].join("\n");
|
|
295
314
|
},
|
|
296
315
|
resolveBootstrap({ routeResult, result }) {
|
|
@@ -309,6 +328,7 @@ export async function createRequestRenderer({
|
|
|
309
328
|
return currentAssetResolver(moduleId);
|
|
310
329
|
},
|
|
311
330
|
hydrationModuleUrl: currentHydrationModuleUrl,
|
|
331
|
+
navigationModuleUrl: currentNavigationModuleUrl,
|
|
312
332
|
});
|
|
313
333
|
},
|
|
314
334
|
transformDocument({ routeResult, result, document }) {
|
|
@@ -322,7 +342,13 @@ export async function createRequestRenderer({
|
|
|
322
342
|
normalizeHydrationDataForClient(
|
|
323
343
|
result.hydrationData,
|
|
324
344
|
projectRoot,
|
|
325
|
-
|
|
345
|
+
[
|
|
346
|
+
...routeClientImports,
|
|
347
|
+
...resolveHydrationRootClientImports(
|
|
348
|
+
result.hydrationData,
|
|
349
|
+
currentAssetResolver,
|
|
350
|
+
),
|
|
351
|
+
],
|
|
326
352
|
),
|
|
327
353
|
);
|
|
328
354
|
},
|
|
@@ -380,6 +406,8 @@ export async function createRequestRenderer({
|
|
|
380
406
|
});
|
|
381
407
|
|
|
382
408
|
if (usesFrameworkRouteResolver) {
|
|
409
|
+
const segmentEntryCount = effectiveRouteResolver.invalidateSegmentCache?.(changedPaths) ?? 0;
|
|
410
|
+
reportDevelopmentEvent({ type: "segment-cache-invalidated", entryCount: segmentEntryCount });
|
|
383
411
|
effectiveRouteResolver = await createFrameworkRouteResolver();
|
|
384
412
|
}
|
|
385
413
|
});
|
|
@@ -475,22 +503,27 @@ export async function createRequestRenderer({
|
|
|
475
503
|
return effectiveRouteResolver.resolveRoutePolicy(request);
|
|
476
504
|
},
|
|
477
505
|
async renderRoute(request, routePolicyResult = null) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
506
|
+
return runWithOptionalSsrUrqlScope(async (urqlAdapter) => {
|
|
507
|
+
const shouldPrepareBeforeResolve = mode === "development" && !currentAssetManifest;
|
|
508
|
+
let resolvedRoutePolicyResult = routePolicyResult;
|
|
509
|
+
if (shouldPrepareBeforeResolve) {
|
|
510
|
+
resolvedRoutePolicyResult ??= await effectiveRouteResolver.resolveRoutePolicy(request);
|
|
511
|
+
await this.prepareRouteClientArtifacts(resolvedRoutePolicyResult);
|
|
512
|
+
}
|
|
484
513
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
514
|
+
const routeResult = await effectiveRouteResolver.resolveRequest(request, resolvedRoutePolicyResult);
|
|
515
|
+
if (!shouldPrepareBeforeResolve || routeResult.boundaryModule) {
|
|
516
|
+
await this.prepareRouteClientArtifacts(routeResult);
|
|
517
|
+
}
|
|
518
|
+
const renderedResponse = await renderRouteTreeWithAdapter(routeResult, ssrAdapter);
|
|
519
|
+
const response = urqlAdapter
|
|
520
|
+
? appendSsrUrqlData(renderedResponse, await urqlAdapter.getUrqlSsrData())
|
|
521
|
+
: renderedResponse;
|
|
522
|
+
return {
|
|
523
|
+
routeResult,
|
|
524
|
+
response,
|
|
525
|
+
};
|
|
526
|
+
});
|
|
494
527
|
},
|
|
495
528
|
get assetManifest() {
|
|
496
529
|
return currentAssetManifest;
|
|
@@ -536,6 +569,16 @@ export async function createDeploymentRuntime({
|
|
|
536
569
|
const runtimeState = {
|
|
537
570
|
assetManifest: mode === "development" ? null : normalizeClientAssetManifest(assetManifest),
|
|
538
571
|
};
|
|
572
|
+
|
|
573
|
+
function formatResponseForRequest(request, response) {
|
|
574
|
+
const representation = request.headers.get("accept")?.includes("application/vnd.evolit.navigation+json")
|
|
575
|
+
? createNavigationResponseFromDocument(response)
|
|
576
|
+
: response;
|
|
577
|
+
// The same URL has an HTML document representation and a navigation
|
|
578
|
+
// delta representation. Tell intermediary HTTP caches that Accept selects
|
|
579
|
+
// between them.
|
|
580
|
+
return addNavigationVaryHeader(representation);
|
|
581
|
+
}
|
|
539
582
|
const assets = createPublicAssetOrigin({
|
|
540
583
|
projectRoot,
|
|
541
584
|
mode,
|
|
@@ -579,7 +622,7 @@ export async function createDeploymentRuntime({
|
|
|
579
622
|
const cachedResponse = await renderer.responseCacheController.read(request, routePolicyResult);
|
|
580
623
|
if (cachedResponse) {
|
|
581
624
|
if (cachedResponse.fresh) {
|
|
582
|
-
return cachedResponse.response;
|
|
625
|
+
return formatResponseForRequest(request, cachedResponse.response);
|
|
583
626
|
}
|
|
584
627
|
|
|
585
628
|
if (
|
|
@@ -603,12 +646,15 @@ export async function createDeploymentRuntime({
|
|
|
603
646
|
revalidationTasks.set(cachedResponse.cacheKey, revalidationTask);
|
|
604
647
|
}
|
|
605
648
|
|
|
606
|
-
return cachedResponse.response;
|
|
649
|
+
return formatResponseForRequest(request, cachedResponse.response);
|
|
607
650
|
}
|
|
608
651
|
|
|
609
652
|
const { routeResult, response } = await renderer.renderRoute(request, routePolicyResult);
|
|
610
653
|
runtimeState.assetManifest = normalizeClientAssetManifest(renderer.assetManifest);
|
|
611
|
-
return
|
|
654
|
+
return formatResponseForRequest(
|
|
655
|
+
request,
|
|
656
|
+
await renderer.responseCacheController.write(request, routeResult, response),
|
|
657
|
+
);
|
|
612
658
|
},
|
|
613
659
|
async close() {
|
|
614
660
|
const revalidationTasks = this.revalidationTasks;
|
|
@@ -618,3 +664,16 @@ export async function createDeploymentRuntime({
|
|
|
618
664
|
},
|
|
619
665
|
};
|
|
620
666
|
}
|
|
667
|
+
|
|
668
|
+
function addNavigationVaryHeader(response) {
|
|
669
|
+
if (!response?.headers) return response;
|
|
670
|
+
const fields = String(response.headers.vary ?? "")
|
|
671
|
+
.split(",")
|
|
672
|
+
.map((field) => field.trim())
|
|
673
|
+
.filter(Boolean);
|
|
674
|
+
if (!fields.some((field) => field.toLowerCase() === "accept")) fields.push("accept");
|
|
675
|
+
return {
|
|
676
|
+
...response,
|
|
677
|
+
headers: { ...response.headers, vary: fields.join(", ") },
|
|
678
|
+
};
|
|
679
|
+
}
|
|
@@ -37,6 +37,15 @@ export function createDevelopmentEventReporter() {
|
|
|
37
37
|
case "client-assets-cache-hit":
|
|
38
38
|
console.log(`${terminal.cyan("[evolit]")} ${terminal.blue("●")} Client assets served from cache.`);
|
|
39
39
|
break;
|
|
40
|
+
case "segment-cache-hit":
|
|
41
|
+
console.log(`${terminal.cyan("[evolit]")} ${terminal.blue("●")} Reused route segment ${terminal.dim(event.modulePath)}.`);
|
|
42
|
+
break;
|
|
43
|
+
case "segment-cache-miss":
|
|
44
|
+
console.log(`${terminal.cyan("[evolit]")} ${terminal.magenta("○")} Rendering route segment ${terminal.dim(event.modulePath)} ${terminal.dim(`(${event.reason})`)}.`);
|
|
45
|
+
break;
|
|
46
|
+
case "segment-cache-invalidated":
|
|
47
|
+
console.log(`${terminal.cyan("[evolit]")} ${terminal.yellow("△")} Invalidated ${event.entryCount} cached route segments.`);
|
|
48
|
+
break;
|
|
40
49
|
case "invalidated":
|
|
41
50
|
console.log(
|
|
42
51
|
`${terminal.cyan("[evolit]")} ${terminal.yellow("△")} Source change detected${event.changedPathCount == null ? "" : ` (${event.changedPathCount} files)`}; invalidated ${event.affectedClientEntryCount} client entries.`,
|