evolit 0.2.0 → 0.2.1
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 +4 -3
- package/package.json +1 -1
- package/src/navigation-client.js +99 -10
- package/src/render.js +118 -37
- package/src/request-context.js +4 -1
package/README.md
CHANGED
|
@@ -162,9 +162,10 @@ from a connected client component.
|
|
|
162
162
|
|
|
163
163
|
Navigation context is intended for compact UI state that must survive back/forward navigation but
|
|
164
164
|
does not belong in the public URL. It must be a JSON-safe object and is limited to 8 KiB after UTF-8
|
|
165
|
-
serialization. Evolit includes it in browser
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
serialization. Evolit includes it in browser and response-cache identity. Segment caching and delta
|
|
166
|
+
identity track only the top-level context keys each layout or page reads, so unrelated context
|
|
167
|
+
changes preserve mounted layouts while consumers still update. Invalid context is rejected at the
|
|
168
|
+
client boundary; malformed context received by the server is ignored.
|
|
168
169
|
|
|
169
170
|
Client components can also read the active route state with browser-only hooks:
|
|
170
171
|
|
package/package.json
CHANGED
package/src/navigation-client.js
CHANGED
|
@@ -194,6 +194,79 @@ function syncDocumentState(nextState, documentRef) {
|
|
|
194
194
|
if (script) script.textContent = JSON.stringify(nextState);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
function readElementAttributes(element) {
|
|
198
|
+
return Object.fromEntries([...element.attributes].map((attribute) => [
|
|
199
|
+
attribute.name,
|
|
200
|
+
attribute.value,
|
|
201
|
+
]));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isRootBodyProjection(segment, projection) {
|
|
205
|
+
return segment?.kind === "layout"
|
|
206
|
+
&& segment?.depth === 0
|
|
207
|
+
&& typeof projection?.html === "string"
|
|
208
|
+
&& /<body(?:\s|>)/iu.test(projection.html);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseRootBodyProjection(projectionHtml, documentRef) {
|
|
212
|
+
const DOMParserConstructor = documentRef.defaultView?.DOMParser ?? globalThis.DOMParser;
|
|
213
|
+
if (typeof DOMParserConstructor !== "function") return null;
|
|
214
|
+
return new DOMParserConstructor().parseFromString(projectionHtml, "text/html").body;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function collectRouteRuntimeScripts(root) {
|
|
218
|
+
const scripts = [...root.querySelectorAll("script")];
|
|
219
|
+
return scripts.filter((script) =>
|
|
220
|
+
script.id === "__EVOLIT_ROUTE__"
|
|
221
|
+
|| script.id === "__EVOLIT_DOCUMENT__"
|
|
222
|
+
|| script.id === "__LITSX_HYDRATION__"
|
|
223
|
+
|| script.hasAttribute("data-evolit-live-reload")
|
|
224
|
+
|| (script.type === "module" && (
|
|
225
|
+
script.textContent?.includes("getNavigation")
|
|
226
|
+
|| script.textContent?.includes("hydratePage")
|
|
227
|
+
)),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function replaceRootBodyProjection({
|
|
232
|
+
bodyProjection,
|
|
233
|
+
delta,
|
|
234
|
+
documentRef,
|
|
235
|
+
fragment,
|
|
236
|
+
target,
|
|
237
|
+
}) {
|
|
238
|
+
const body = documentRef.body;
|
|
239
|
+
if (!body || !bodyProjection) return false;
|
|
240
|
+
|
|
241
|
+
const authoredAttributes = readElementAttributes(bodyProjection);
|
|
242
|
+
const managedAttributes = normalizeManagedAttributes(delta.document?.bodyAttributes);
|
|
243
|
+
syncElementAttributes(
|
|
244
|
+
body,
|
|
245
|
+
readElementAttributes(body),
|
|
246
|
+
{ ...authoredAttributes, ...managedAttributes },
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const runtimeScripts = collectRouteRuntimeScripts(documentRef);
|
|
250
|
+
body.replaceChildren(target.start, fragment, target.end, ...runtimeScripts);
|
|
251
|
+
|
|
252
|
+
// HTML parsing can leave nodes from an authored root <body> beside the live
|
|
253
|
+
// body. Once the stable body owns the committed projection and runtime
|
|
254
|
+
// scripts, discard only those invalid element siblings.
|
|
255
|
+
for (const element of [...documentRef.documentElement.children]) {
|
|
256
|
+
if (element !== documentRef.head && element !== body) element.remove();
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function syncRouteStateScripts(delta, documentRef) {
|
|
262
|
+
const routeScript = documentRef.getElementById("__EVOLIT_ROUTE__");
|
|
263
|
+
if (routeScript) routeScript.textContent = JSON.stringify(delta.route);
|
|
264
|
+
const hydrationScript = documentRef.getElementById("__LITSX_HYDRATION__");
|
|
265
|
+
if (hydrationScript && delta.hydrationData) {
|
|
266
|
+
hydrationScript.textContent = JSON.stringify(delta.hydrationData);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
197
270
|
function versionClientImport(specifier, version) {
|
|
198
271
|
if (!version || typeof specifier !== "string") return specifier;
|
|
199
272
|
const url = new URL(specifier, globalThis.location?.href ?? "http://evolit.local/");
|
|
@@ -260,9 +333,18 @@ async function applyRouteDelta(delta, documentRef = document, signal, options =
|
|
|
260
333
|
throwIfAborted(signal);
|
|
261
334
|
const projection = next.projections[offset] ?? next.projections[0];
|
|
262
335
|
if (!projection) continue;
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
336
|
+
const bodyProjection = isRootBodyProjection(next, projection)
|
|
337
|
+
? parseRootBodyProjection(projection.html, documentRef)
|
|
338
|
+
: null;
|
|
339
|
+
const fragment = documentRef.createDocumentFragment();
|
|
340
|
+
if (bodyProjection) {
|
|
341
|
+
for (const script of collectRouteRuntimeScripts(bodyProjection)) script.remove();
|
|
342
|
+
fragment.append(...bodyProjection.childNodes);
|
|
343
|
+
} else {
|
|
344
|
+
const template = documentRef.createElement("template");
|
|
345
|
+
template.innerHTML = projection.html;
|
|
346
|
+
fragment.append(template.content);
|
|
347
|
+
}
|
|
266
348
|
materializeDeclarativeShadowDom(fragment);
|
|
267
349
|
// Restore opaque SSR forwarded refs while the fragment is detached, before
|
|
268
350
|
// its custom elements upgrade on insertion.
|
|
@@ -279,11 +361,19 @@ async function applyRouteDelta(delta, documentRef = document, signal, options =
|
|
|
279
361
|
roots.map(({ root, element }) => ({ ...root, element })),
|
|
280
362
|
delta.hydrationData,
|
|
281
363
|
);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
364
|
+
if (!replaceRootBodyProjection({
|
|
365
|
+
bodyProjection,
|
|
366
|
+
delta,
|
|
367
|
+
documentRef,
|
|
368
|
+
fragment,
|
|
369
|
+
target,
|
|
370
|
+
})) {
|
|
371
|
+
const range = documentRef.createRange();
|
|
372
|
+
range.setStartAfter(target.start);
|
|
373
|
+
range.setEndBefore(target.end);
|
|
374
|
+
range.deleteContents();
|
|
375
|
+
range.insertNode(fragment);
|
|
376
|
+
}
|
|
287
377
|
insertedRoots.push(...roots);
|
|
288
378
|
target.start.data = `evolit:segment:start:${next.id}`;
|
|
289
379
|
target.end.data = `evolit:segment:end:${next.id}`;
|
|
@@ -292,8 +382,7 @@ async function applyRouteDelta(delta, documentRef = document, signal, options =
|
|
|
292
382
|
// document to point persistent layout refs at the new target, or clear them
|
|
293
383
|
// when the replacement page no longer exposes one.
|
|
294
384
|
prepareForwardedRefs(documentRef);
|
|
295
|
-
|
|
296
|
-
if (script) script.textContent = JSON.stringify(delta.route);
|
|
385
|
+
syncRouteStateScripts(delta, documentRef);
|
|
297
386
|
if (delta.title) documentRef.title = delta.title;
|
|
298
387
|
for (const { root, element } of insertedRoots) {
|
|
299
388
|
throwIfAborted(signal);
|
package/src/render.js
CHANGED
|
@@ -295,6 +295,67 @@ function createTrackedRouteValues(values) {
|
|
|
295
295
|
};
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
function createTrackedNavigationContext(values) {
|
|
299
|
+
const tracked = createTrackedRouteValues(values ?? {});
|
|
300
|
+
let accessed = false;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
value: values == null ? null : tracked.value,
|
|
304
|
+
read() {
|
|
305
|
+
accessed = true;
|
|
306
|
+
return values == null ? null : tracked.value;
|
|
307
|
+
},
|
|
308
|
+
profile() {
|
|
309
|
+
return { accessed, ...tracked.profile() };
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function createTrackedSegmentProps(
|
|
315
|
+
trackedParams,
|
|
316
|
+
trackedSearchParams,
|
|
317
|
+
trackedNavigationContext,
|
|
318
|
+
props,
|
|
319
|
+
) {
|
|
320
|
+
const result = {
|
|
321
|
+
params: trackedParams.value,
|
|
322
|
+
searchParams: trackedSearchParams.value,
|
|
323
|
+
...props,
|
|
324
|
+
};
|
|
325
|
+
Object.defineProperty(result, "navigationContext", {
|
|
326
|
+
enumerable: true,
|
|
327
|
+
get: () => trackedNavigationContext.read(),
|
|
328
|
+
});
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function createSegmentDependencyProfile(
|
|
333
|
+
trackedParams,
|
|
334
|
+
trackedSearchParams,
|
|
335
|
+
trackedNavigationContext,
|
|
336
|
+
) {
|
|
337
|
+
return {
|
|
338
|
+
params: trackedParams.profile(),
|
|
339
|
+
searchParams: trackedSearchParams.profile(),
|
|
340
|
+
navigationContext: trackedNavigationContext.profile(),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function selectTrackedValues(profile, values) {
|
|
345
|
+
const normalized = profile ?? { all: false, keys: [] };
|
|
346
|
+
return Object.fromEntries(
|
|
347
|
+
(normalized.all ? Object.keys(values ?? {}) : normalized.keys ?? [])
|
|
348
|
+
.sort()
|
|
349
|
+
.map((key) => [key, values?.[key]]),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function selectTrackedNavigationContext(profile, navigationContext) {
|
|
354
|
+
if (!profile?.accessed) return undefined;
|
|
355
|
+
if (navigationContext == null) return null;
|
|
356
|
+
return selectTrackedValues(profile, navigationContext);
|
|
357
|
+
}
|
|
358
|
+
|
|
298
359
|
function createSegmentCacheKey(
|
|
299
360
|
segment,
|
|
300
361
|
idPrefix,
|
|
@@ -303,30 +364,26 @@ function createSegmentCacheKey(
|
|
|
303
364
|
searchParams,
|
|
304
365
|
navigationContext,
|
|
305
366
|
) {
|
|
306
|
-
const select = (values) => Object.fromEntries(
|
|
307
|
-
(profile.all ? Object.keys(values) : profile.keys)
|
|
308
|
-
.sort()
|
|
309
|
-
.map((key) => [key, values[key]]),
|
|
310
|
-
);
|
|
311
367
|
return JSON.stringify({
|
|
312
368
|
modulePath: segment.modulePath,
|
|
313
369
|
idPrefix,
|
|
314
|
-
params:
|
|
315
|
-
searchParams:
|
|
316
|
-
navigationContext:
|
|
370
|
+
params: selectTrackedValues(profile.params, params),
|
|
371
|
+
searchParams: selectTrackedValues(profile.searchParams, searchParams),
|
|
372
|
+
navigationContext: selectTrackedNavigationContext(
|
|
373
|
+
profile.navigationContext,
|
|
374
|
+
navigationContext,
|
|
375
|
+
),
|
|
317
376
|
});
|
|
318
377
|
}
|
|
319
378
|
|
|
320
379
|
function createSegmentInputKey(profile, params, searchParams, navigationContext) {
|
|
321
|
-
const select = (values) => Object.fromEntries(
|
|
322
|
-
(profile.all ? Object.keys(values) : profile.keys)
|
|
323
|
-
.sort()
|
|
324
|
-
.map((key) => [key, values[key]]),
|
|
325
|
-
);
|
|
326
380
|
return JSON.stringify({
|
|
327
|
-
params:
|
|
328
|
-
searchParams:
|
|
329
|
-
navigationContext:
|
|
381
|
+
params: selectTrackedValues(profile.params, params),
|
|
382
|
+
searchParams: selectTrackedValues(profile.searchParams, searchParams),
|
|
383
|
+
navigationContext: selectTrackedNavigationContext(
|
|
384
|
+
profile.navigationContext,
|
|
385
|
+
navigationContext,
|
|
386
|
+
),
|
|
330
387
|
});
|
|
331
388
|
}
|
|
332
389
|
|
|
@@ -457,18 +514,37 @@ async function renderSegmentedComponentTree(
|
|
|
457
514
|
const idPrefix = `${segment.id}-p${projectionPath.join("-") || "0"}`;
|
|
458
515
|
|
|
459
516
|
if (index === layoutComponents.length) {
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
517
|
+
const trackedParams = createTrackedRouteValues(requestContext.params);
|
|
518
|
+
const trackedSearchParams = createTrackedRouteValues(requestContext.searchParams);
|
|
519
|
+
const trackedNavigationContext = createTrackedNavigationContext(
|
|
520
|
+
requestContext.navigationContext,
|
|
521
|
+
);
|
|
522
|
+
const pageValue = await runWithRouteState(requestContext, {
|
|
523
|
+
params: trackedParams.value,
|
|
524
|
+
searchParams: trackedSearchParams.value,
|
|
525
|
+
navigationContext: trackedNavigationContext.value,
|
|
526
|
+
trackNavigationContext: () => trackedNavigationContext.read(),
|
|
527
|
+
}, () => component(createTrackedSegmentProps(
|
|
528
|
+
trackedParams,
|
|
529
|
+
trackedSearchParams,
|
|
530
|
+
trackedNavigationContext,
|
|
531
|
+
{ request, ...extraProps },
|
|
532
|
+
), incomingRef));
|
|
467
533
|
const pageResult = await renderToString(wrapRouteSegment(segment, pageValue), {
|
|
468
534
|
assetResolver: options.assetResolver,
|
|
469
535
|
context: { idPrefix },
|
|
470
536
|
});
|
|
471
537
|
pageResult.segmentModulePath = segment.modulePath;
|
|
538
|
+
segment.inputKey = createSegmentInputKey(
|
|
539
|
+
createSegmentDependencyProfile(
|
|
540
|
+
trackedParams,
|
|
541
|
+
trackedSearchParams,
|
|
542
|
+
trackedNavigationContext,
|
|
543
|
+
),
|
|
544
|
+
requestContext.params,
|
|
545
|
+
requestContext.searchParams,
|
|
546
|
+
requestContext.navigationContext,
|
|
547
|
+
);
|
|
472
548
|
results.push(pageResult);
|
|
473
549
|
return pageResult.html;
|
|
474
550
|
}
|
|
@@ -486,31 +562,36 @@ async function renderSegmentedComponentTree(
|
|
|
486
562
|
if (!layoutResult) {
|
|
487
563
|
const trackedParams = createTrackedRouteValues(requestContext.params);
|
|
488
564
|
const trackedSearchParams = createTrackedRouteValues(requestContext.searchParams);
|
|
565
|
+
const trackedNavigationContext = createTrackedNavigationContext(
|
|
566
|
+
requestContext.navigationContext,
|
|
567
|
+
);
|
|
489
568
|
const didUseDynamicRequestData = requestContext.didUseDynamicRequestData;
|
|
490
569
|
layoutResult = await runWithRouteState(requestContext, {
|
|
491
570
|
params: trackedParams.value,
|
|
492
571
|
searchParams: trackedSearchParams.value,
|
|
572
|
+
navigationContext: trackedNavigationContext.value,
|
|
573
|
+
trackNavigationContext: () => trackedNavigationContext.read(),
|
|
493
574
|
}, async () => {
|
|
494
|
-
const layoutValue = await layoutComponents[index](
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
575
|
+
const layoutValue = await layoutComponents[index](createTrackedSegmentProps(
|
|
576
|
+
trackedParams,
|
|
577
|
+
trackedSearchParams,
|
|
578
|
+
trackedNavigationContext,
|
|
579
|
+
{
|
|
580
|
+
request,
|
|
581
|
+
children: withForwardedChildRef(html`${unsafeHTML(childrenMarker)}`, childRef),
|
|
582
|
+
},
|
|
583
|
+
), incomingRef);
|
|
501
584
|
return renderToString(wrapRouteSegment(segment, layoutValue), {
|
|
502
585
|
assetResolver: options.assetResolver,
|
|
503
586
|
context: { idPrefix },
|
|
504
587
|
});
|
|
505
588
|
});
|
|
506
589
|
layoutResult.segmentModulePath = segment.modulePath;
|
|
507
|
-
profile =
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
])].sort(),
|
|
513
|
-
};
|
|
590
|
+
profile = createSegmentDependencyProfile(
|
|
591
|
+
trackedParams,
|
|
592
|
+
trackedSearchParams,
|
|
593
|
+
trackedNavigationContext,
|
|
594
|
+
);
|
|
514
595
|
if (!didUseDynamicRequestData && !requestContext.didUseDynamicRequestData) {
|
|
515
596
|
options.segmentCache?.set(
|
|
516
597
|
segment,
|
package/src/request-context.js
CHANGED
|
@@ -175,7 +175,10 @@ export function getRouteState() {
|
|
|
175
175
|
},
|
|
176
176
|
navigationContext: {
|
|
177
177
|
enumerable: true,
|
|
178
|
-
|
|
178
|
+
get() {
|
|
179
|
+
trackedState?.trackNavigationContext?.();
|
|
180
|
+
return routeState.navigationContext;
|
|
181
|
+
},
|
|
179
182
|
},
|
|
180
183
|
});
|
|
181
184
|
|