evolit 0.1.0-alpha.21 → 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/package.json +10 -1
- package/src/build.js +21 -5
- package/src/client-assets.js +19 -0
- package/src/deployment-runtime.js +29 -16
- package/src/navigation-client.js +35 -8
- package/src/render.js +31 -7
- package/src/route-segments.js +2 -1
- package/src/urql-ssr.js +60 -0
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",
|
|
@@ -63,8 +63,17 @@
|
|
|
63
63
|
"typescript": "^6.0.0",
|
|
64
64
|
"ws": "^8.18.3"
|
|
65
65
|
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@litsx/urql": "^0.3.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@litsx/urql": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
66
74
|
"devDependencies": {
|
|
67
75
|
"@playwright/test": "^1.62.0",
|
|
76
|
+
"@webcomponents/scoped-custom-element-registry": "^0.0.10",
|
|
68
77
|
"vite": "^8.1.5"
|
|
69
78
|
}
|
|
70
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"],
|
|
@@ -288,7 +290,10 @@ export async function buildProject(projectRoot) {
|
|
|
288
290
|
normalizeHydrationDataForClient(
|
|
289
291
|
result.hydrationData,
|
|
290
292
|
projectRoot,
|
|
291
|
-
|
|
293
|
+
[
|
|
294
|
+
...routeClientImports,
|
|
295
|
+
...resolveHydrationRootClientImports(result.hydrationData, assetResolver),
|
|
296
|
+
],
|
|
292
297
|
),
|
|
293
298
|
);
|
|
294
299
|
},
|
|
@@ -337,12 +342,23 @@ export async function buildProject(projectRoot) {
|
|
|
337
342
|
seenPrerenderTargets.add(targetPathname);
|
|
338
343
|
|
|
339
344
|
const targetRequest = new Request(`http://evolit.local${targetPathname}`);
|
|
340
|
-
const routeResult = await
|
|
341
|
-
|
|
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) {
|
|
342
360
|
continue;
|
|
343
361
|
}
|
|
344
|
-
|
|
345
|
-
const response = await renderRouteTreeWithAdapter(routeResult, ssrAdapter);
|
|
346
362
|
if (response.status !== 200) {
|
|
347
363
|
continue;
|
|
348
364
|
}
|
package/src/client-assets.js
CHANGED
|
@@ -2032,6 +2032,25 @@ export function resolveRouteClientImports(routeResult, projectRoot, assetManifes
|
|
|
2032
2032
|
}).filter((publicUrl) => typeof publicUrl === "string"))];
|
|
2033
2033
|
}
|
|
2034
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
|
+
|
|
2035
2054
|
export function collectTransitiveStyleUrls(publicUrls, assetManifest) {
|
|
2036
2055
|
const normalizedManifest = normalizeClientAssetManifest(assetManifest);
|
|
2037
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,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
} from "./response-cache.js";
|
|
34
35
|
import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
|
|
35
36
|
import { createNavigationResponseFromDocument } from "./route-segments.js";
|
|
37
|
+
import { appendSsrUrqlData, runWithOptionalSsrUrqlScope } from "./urql-ssr.js";
|
|
36
38
|
const CONTENT_TYPE_BY_EXTENSION = new Map([
|
|
37
39
|
[".css", "text/css; charset=utf-8"],
|
|
38
40
|
[".svg", "image/svg+xml"],
|
|
@@ -340,7 +342,13 @@ export async function createRequestRenderer({
|
|
|
340
342
|
normalizeHydrationDataForClient(
|
|
341
343
|
result.hydrationData,
|
|
342
344
|
projectRoot,
|
|
343
|
-
|
|
345
|
+
[
|
|
346
|
+
...routeClientImports,
|
|
347
|
+
...resolveHydrationRootClientImports(
|
|
348
|
+
result.hydrationData,
|
|
349
|
+
currentAssetResolver,
|
|
350
|
+
),
|
|
351
|
+
],
|
|
344
352
|
),
|
|
345
353
|
);
|
|
346
354
|
},
|
|
@@ -495,22 +503,27 @@ export async function createRequestRenderer({
|
|
|
495
503
|
return effectiveRouteResolver.resolveRoutePolicy(request);
|
|
496
504
|
},
|
|
497
505
|
async renderRoute(request, routePolicyResult = null) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
+
}
|
|
504
513
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
+
});
|
|
514
527
|
},
|
|
515
528
|
get assetManifest() {
|
|
516
529
|
return currentAssetManifest;
|
package/src/navigation-client.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { useHost, useOnConnect, useState } from "@litsx/core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
applyHydrationPayload,
|
|
4
|
+
hydrateRoot,
|
|
5
|
+
registerHydrationModules,
|
|
6
|
+
} from "@litsx/ssr/hydration";
|
|
3
7
|
import { createHref } from "./navigation-url.js";
|
|
4
8
|
|
|
5
9
|
export { createHref } from "./navigation-url.js";
|
|
@@ -162,11 +166,21 @@ async function applyRouteDelta(delta, documentRef = document, signal) {
|
|
|
162
166
|
const nextSegments = delta.route?.segments ?? [];
|
|
163
167
|
const currentSegments = current?.segments ?? [];
|
|
164
168
|
let index = nextSegments.findIndex((segment, offset) => segment.id !== currentSegments[offset]?.id);
|
|
169
|
+
if (index < 0) {
|
|
170
|
+
index = nextSegments.findIndex(
|
|
171
|
+
(segment, offset) => segment.inputKey !== currentSegments[offset]?.inputKey,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
165
174
|
if (index < 0) index = Math.max(0, nextSegments.length - 1);
|
|
166
175
|
const next = nextSegments[index];
|
|
167
176
|
const previous = currentSegments[index] ?? currentSegments.at(-1);
|
|
168
177
|
if (!next || !previous) return;
|
|
169
178
|
const targets = findMarkers(documentRef, previous.id);
|
|
179
|
+
// A projection is positional: reusing projection zero for a second target
|
|
180
|
+
// duplicates DOM (and hydration roots) when a layout changes its children
|
|
181
|
+
// cardinality. A full document navigation is the only safe recovery when
|
|
182
|
+
// the live tree and delta disagree.
|
|
183
|
+
if (targets.length !== next.projections.length) return false;
|
|
170
184
|
await syncRouteHeadAssets(delta.headAssets, documentRef);
|
|
171
185
|
throwIfAborted(signal);
|
|
172
186
|
syncRouteHeadMarkup(delta.head, documentRef);
|
|
@@ -184,6 +198,14 @@ async function applyRouteDelta(delta, documentRef = document, signal) {
|
|
|
184
198
|
root,
|
|
185
199
|
element: findHydrationElement(fragment, root.id),
|
|
186
200
|
})).filter((entry) => entry.element);
|
|
201
|
+
// A custom element already defined by an earlier route is upgraded as
|
|
202
|
+
// soon as this fragment is connected. Apply its SSR data while it is
|
|
203
|
+
// still detached, otherwise Lit starts an update against its declarative
|
|
204
|
+
// shadow root without the properties used to render it on the server.
|
|
205
|
+
applyHydrationPayload(
|
|
206
|
+
roots.map(({ root, element }) => ({ ...root, element })),
|
|
207
|
+
delta.hydrationData,
|
|
208
|
+
);
|
|
187
209
|
const range = documentRef.createRange();
|
|
188
210
|
range.setStartAfter(target.start);
|
|
189
211
|
range.setEndBefore(target.end);
|
|
@@ -201,13 +223,14 @@ async function applyRouteDelta(delta, documentRef = document, signal) {
|
|
|
201
223
|
await Promise.all((delta.hydrationData?.clientImports ?? []).map((specifier) => import(specifier))),
|
|
202
224
|
);
|
|
203
225
|
for (const { root, element } of insertedRoots) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
226
|
+
throwIfAborted(signal);
|
|
227
|
+
await hydrateRoot(element, {
|
|
228
|
+
rootId: root.id,
|
|
229
|
+
hydrationData: delta.hydrationData,
|
|
230
|
+
clientImports: delta.hydrationData.clientImports,
|
|
231
|
+
});
|
|
210
232
|
}
|
|
233
|
+
return true;
|
|
211
234
|
}
|
|
212
235
|
|
|
213
236
|
function toHref(target, location) {
|
|
@@ -440,7 +463,11 @@ export function createBrowserNavigation(options = {}) {
|
|
|
440
463
|
})();
|
|
441
464
|
if (!delta || !isCurrent()) return null;
|
|
442
465
|
if (delta.type === "redirect") return navigate(delta.location, "replace", false);
|
|
443
|
-
await applyDelta(delta, { signal: navigationController.signal });
|
|
466
|
+
const applied = await applyDelta(delta, { signal: navigationController.signal });
|
|
467
|
+
if (applied === false) {
|
|
468
|
+
navigateDocument(href, mode);
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
444
471
|
if (!isCurrent()) return null;
|
|
445
472
|
const canonicalHref = toHref(delta.url ?? href, windowRef.location);
|
|
446
473
|
if (!fromPopState) {
|
package/src/render.js
CHANGED
|
@@ -263,6 +263,18 @@ function createSegmentCacheKey(segment, idPrefix, profile, params, searchParams)
|
|
|
263
263
|
});
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
function createSegmentInputKey(profile, params, searchParams) {
|
|
267
|
+
const select = (values) => Object.fromEntries(
|
|
268
|
+
(profile.all ? Object.keys(values) : profile.keys)
|
|
269
|
+
.sort()
|
|
270
|
+
.map((key) => [key, values[key]]),
|
|
271
|
+
);
|
|
272
|
+
return JSON.stringify({
|
|
273
|
+
params: select(params),
|
|
274
|
+
searchParams: select(searchParams),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
266
278
|
function getSegmentCacheExpiry(cachePolicy) {
|
|
267
279
|
if (cachePolicy?.mode === "static") return Number.POSITIVE_INFINITY;
|
|
268
280
|
if (cachePolicy?.mode === "revalidate" && Number.isFinite(cachePolicy.ttlSeconds)) {
|
|
@@ -296,6 +308,9 @@ function createSegmentRenderCache(projectRoot, onDevelopmentEvent) {
|
|
|
296
308
|
onDevelopmentEvent?.({ type: "segment-cache-hit", modulePath: segment.modulePath });
|
|
297
309
|
return entry.result;
|
|
298
310
|
},
|
|
311
|
+
getProfile(segment) {
|
|
312
|
+
return profiles.get(segment.modulePath) ?? null;
|
|
313
|
+
},
|
|
299
314
|
set(segment, idPrefix, params, searchParams, profile, result, cachePolicy) {
|
|
300
315
|
const expiresAt = getSegmentCacheExpiry(cachePolicy);
|
|
301
316
|
if (expiresAt == null) return;
|
|
@@ -379,6 +394,7 @@ async function renderSegmentedComponentTree(
|
|
|
379
394
|
}
|
|
380
395
|
|
|
381
396
|
const childrenMarker = createSegmentChildrenMarker(segment);
|
|
397
|
+
let profile = options.segmentCache?.getProfile(segment) ?? null;
|
|
382
398
|
let layoutResult = options.segmentCache?.get(
|
|
383
399
|
segment,
|
|
384
400
|
idPrefix,
|
|
@@ -399,24 +415,32 @@ async function renderSegmentedComponentTree(
|
|
|
399
415
|
assetResolver: options.assetResolver,
|
|
400
416
|
context: { idPrefix },
|
|
401
417
|
});
|
|
418
|
+
profile = {
|
|
419
|
+
all: trackedParams.profile().all || trackedSearchParams.profile().all,
|
|
420
|
+
keys: [...new Set([
|
|
421
|
+
...trackedParams.profile().keys,
|
|
422
|
+
...trackedSearchParams.profile().keys,
|
|
423
|
+
])].sort(),
|
|
424
|
+
};
|
|
402
425
|
if (!didUseDynamicRequestData && !requestContext.didUseDynamicRequestData) {
|
|
403
426
|
options.segmentCache?.set(
|
|
404
427
|
segment,
|
|
405
428
|
idPrefix,
|
|
406
429
|
requestContext.params,
|
|
407
430
|
requestContext.searchParams,
|
|
408
|
-
|
|
409
|
-
all: trackedParams.profile().all || trackedSearchParams.profile().all,
|
|
410
|
-
keys: [...new Set([
|
|
411
|
-
...trackedParams.profile().keys,
|
|
412
|
-
...trackedSearchParams.profile().keys,
|
|
413
|
-
])].sort(),
|
|
414
|
-
},
|
|
431
|
+
profile,
|
|
415
432
|
layoutResult,
|
|
416
433
|
options.cachePolicy,
|
|
417
434
|
);
|
|
418
435
|
}
|
|
419
436
|
}
|
|
437
|
+
if (profile) {
|
|
438
|
+
segment.inputKey = createSegmentInputKey(
|
|
439
|
+
profile,
|
|
440
|
+
requestContext.params,
|
|
441
|
+
requestContext.searchParams,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
420
444
|
results.push(layoutResult);
|
|
421
445
|
const projectionCount = layoutResult.html.split(childrenMarker).length - 1;
|
|
422
446
|
if (projectionCount === 0) return null;
|
package/src/route-segments.js
CHANGED
|
@@ -81,11 +81,12 @@ export function createRouteSegmentPayload(routeResult) {
|
|
|
81
81
|
: {}),
|
|
82
82
|
}
|
|
83
83
|
: null,
|
|
84
|
-
segments: segments.map(({ id, kind, depth, modulePath }) => ({
|
|
84
|
+
segments: segments.map(({ id, kind, depth, modulePath, inputKey }) => ({
|
|
85
85
|
id,
|
|
86
86
|
kind,
|
|
87
87
|
depth,
|
|
88
88
|
modulePath,
|
|
89
|
+
...(typeof inputKey === "string" ? { inputKey } : {}),
|
|
89
90
|
})),
|
|
90
91
|
};
|
|
91
92
|
}
|
package/src/urql-ssr.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
let adapterPromise = null;
|
|
2
|
+
|
|
3
|
+
function isMissingUrqlAdapter(error) {
|
|
4
|
+
return error?.code === "ERR_MODULE_NOT_FOUND"
|
|
5
|
+
&& String(error.message).includes("@litsx/urql");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function loadSsrUrqlAdapter() {
|
|
9
|
+
try {
|
|
10
|
+
return await import("@litsx/urql");
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (isMissingUrqlAdapter(error)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getSsrUrqlAdapter() {
|
|
20
|
+
adapterPromise ??= loadSsrUrqlAdapter();
|
|
21
|
+
return adapterPromise;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Opens the optional @litsx/urql request scope around one complete SSR render.
|
|
26
|
+
* Evolit owns only lifecycle here; client creation and URQL configuration stay
|
|
27
|
+
* entirely in the application and @litsx/urql.
|
|
28
|
+
*/
|
|
29
|
+
export async function runWithOptionalSsrUrqlScope(callback, options = {}) {
|
|
30
|
+
const adapter = Object.hasOwn(options, "adapter")
|
|
31
|
+
? options.adapter
|
|
32
|
+
: await getSsrUrqlAdapter();
|
|
33
|
+
if (!adapter) {
|
|
34
|
+
return callback(null);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return adapter.runWithUrqlScope(() => callback(adapter));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function escapeJsonForHtml(value) {
|
|
41
|
+
return JSON.stringify(value)
|
|
42
|
+
.replaceAll("<", "\\u003C")
|
|
43
|
+
.replaceAll(">", "\\u003E")
|
|
44
|
+
.replaceAll("&", "\\u0026");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Appends optional, application-defined URQL SSR data to an HTML response. */
|
|
48
|
+
export function appendSsrUrqlData(response, data) {
|
|
49
|
+
if (data === undefined || typeof response?.body !== "string") {
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const script = `<script type="application/json" id="__LITSX_URQL_DATA__">${escapeJsonForHtml(data)}</script>`;
|
|
54
|
+
return {
|
|
55
|
+
...response,
|
|
56
|
+
body: response.body.includes("</body>")
|
|
57
|
+
? response.body.replace("</body>", `${script}\n</body>`)
|
|
58
|
+
: `${response.body}${script}`,
|
|
59
|
+
};
|
|
60
|
+
}
|