evolit 0.1.8 → 0.2.0

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 CHANGED
@@ -20,8 +20,8 @@ It focuses on the core contract that matters first:
20
20
  - route modules live in `app/**/page.*`
21
21
  - layout modules live in `app/**/layout.*`
22
22
  - page and layout modules export a default async function
23
- - pages receive `{ params, searchParams, request }`
24
- - layouts receive `{ children, params, searchParams, request }`
23
+ - pages receive `{ params, searchParams, navigationContext, request }`
24
+ - layouts receive `{ children, params, searchParams, navigationContext, request }`
25
25
  - SSR document rendering is delegated to `@litsx/ssr`
26
26
 
27
27
  Supported authored module extensions:
@@ -266,20 +266,24 @@ Server pages and layouts can access the active Web Request context through `evol
266
266
  ```js
267
267
  import {
268
268
  cookies,
269
+ getRouteState,
269
270
  headers,
270
271
  notFound,
272
+ permanentRedirect,
271
273
  redirect,
272
274
  requestUrl,
273
275
  responseHeaders,
274
276
  } from "evolit/server";
275
277
 
276
278
  export default async function AccountPage() {
279
+ const { params, searchParams, navigationContext } = getRouteState();
280
+
277
281
  if (!cookies().has("session")) {
278
282
  redirect("/sign-in");
279
283
  }
280
284
 
281
285
  responseHeaders().set("x-account-page", "1");
282
- return `<p>${headers().get("user-agent")} ${requestUrl().pathname}</p>`;
286
+ return `<p>${headers().get("user-agent")} ${requestUrl().pathname} ${params.account ?? ""}</p>`;
283
287
  }
284
288
  ```
285
289
 
@@ -288,6 +292,14 @@ headers. `redirect()` and `permanentRedirect()` end rendering with `307` and `30
288
292
  `notFound()` renders a `404`. Reading headers, cookies, or the request URL makes the completed
289
293
  render dynamic, so it is not stored by the route response cache.
290
294
 
295
+ `getRouteState()` is the request-scoped server counterpart for reading the active route from nested
296
+ server components and helpers that do not receive route props directly. It returns the current
297
+ `{ url, params, searchParams, navigationContext }` as a read-only snapshot. It deliberately has no
298
+ `push`, `replace`, `refresh`, pending state, or history mutation methods: those belong to the browser
299
+ `useNavigation()` API. Redirects and `notFound()` remain separate server control-flow functions.
300
+ Reading `getRouteState().url` has the same dynamic-rendering semantics as `requestUrl()`; reading
301
+ `params` and `searchParams` participates in the normal segment-cache key tracking.
302
+
291
303
  ## Extensions
292
304
 
293
305
  Optional integrations are configured explicitly in `evolit.config.js`. Core only coordinates their
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolit",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "A convention-driven application framework for LitSX and web components.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.10.3",
@@ -1,4 +1,3 @@
1
- export function getNavigation() { throw new Error("Navigation is only available in a browser context."); }
2
1
  export { createHref } from "./navigation-url.js";
3
2
  export function useNavigation() { throw new Error("useNavigation() is only available in a browser component."); }
4
3
  export function useParams() { throw new Error("useParams() is only available in a browser component."); }
package/src/render.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  getRequestContextResponse,
15
15
  isEvolitHttpSignal,
16
16
  runWithRequestContext,
17
+ runWithRouteState,
17
18
  } from "./request-context.js";
18
19
  import { createDevelopmentEventReporter } from "./development-events.js";
19
20
  import {
@@ -486,16 +487,21 @@ async function renderSegmentedComponentTree(
486
487
  const trackedParams = createTrackedRouteValues(requestContext.params);
487
488
  const trackedSearchParams = createTrackedRouteValues(requestContext.searchParams);
488
489
  const didUseDynamicRequestData = requestContext.didUseDynamicRequestData;
489
- const layoutValue = await layoutComponents[index]({
490
+ layoutResult = await runWithRouteState(requestContext, {
490
491
  params: trackedParams.value,
491
492
  searchParams: trackedSearchParams.value,
492
- navigationContext: requestContext.navigationContext,
493
- request,
494
- children: withForwardedChildRef(html`${unsafeHTML(childrenMarker)}`, childRef),
495
- }, incomingRef);
496
- layoutResult = await renderToString(wrapRouteSegment(segment, layoutValue), {
497
- assetResolver: options.assetResolver,
498
- context: { idPrefix },
493
+ }, async () => {
494
+ const layoutValue = await layoutComponents[index]({
495
+ params: trackedParams.value,
496
+ searchParams: trackedSearchParams.value,
497
+ navigationContext: requestContext.navigationContext,
498
+ request,
499
+ children: withForwardedChildRef(html`${unsafeHTML(childrenMarker)}`, childRef),
500
+ }, incomingRef);
501
+ return renderToString(wrapRouteSegment(segment, layoutValue), {
502
+ assetResolver: options.assetResolver,
503
+ context: { idPrefix },
504
+ });
499
505
  });
500
506
  layoutResult.segmentModulePath = segment.modulePath;
501
507
  profile = {
@@ -10,3 +10,4 @@ export function redirect() { return throwServerOnlyApi("redirect"); }
10
10
  export function requestUrl() { return throwServerOnlyApi("requestUrl"); }
11
11
  export function responseHeaders() { return throwServerOnlyApi("responseHeaders"); }
12
12
  export function getRequestContext() { return throwServerOnlyApi("getRequestContext"); }
13
+ export function getRouteState() { return throwServerOnlyApi("getRouteState"); }
@@ -2,9 +2,11 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { readNavigationContext } from "./navigation-context.js";
3
3
 
4
4
  const REQUEST_CONTEXT_STORAGE = Symbol.for("evolit.request-context.storage");
5
+ const ROUTE_STATE_STORAGE = Symbol.for("evolit.route-state.storage");
5
6
  const HTTP_SIGNAL_CLASS = Symbol.for("evolit.request-context.http-signal");
6
7
 
7
8
  const requestContextStorage = globalThis[REQUEST_CONTEXT_STORAGE] ??= new AsyncLocalStorage();
9
+ const routeStateStorage = globalThis[ROUTE_STATE_STORAGE] ??= new AsyncLocalStorage();
8
10
  const EvolitHttpSignal = globalThis[HTTP_SIGNAL_CLASS] ??= class EvolitHttpSignal extends Error {
9
11
  constructor(type, options = {}) {
10
12
  super(type);
@@ -111,11 +113,14 @@ export function createRequestContext({
111
113
  extensionValues = {},
112
114
  didUseDynamicRequestData = false,
113
115
  }) {
116
+ const routeNavigationContext = navigationContext == null
117
+ ? null
118
+ : Object.freeze({ ...navigationContext });
114
119
  const context = {
115
120
  request,
116
121
  params: Object.freeze({ ...params }),
117
122
  searchParams: Object.freeze({ ...searchParams }),
118
- navigationContext,
123
+ navigationContext: routeNavigationContext,
119
124
  responseHeaders: new Headers(),
120
125
  responseCookies: [],
121
126
  didUseDynamicRequestData: didUseDynamicRequestData === true,
@@ -130,6 +135,53 @@ export function runWithRequestContext(context, callback) {
130
135
  return requestContextStorage.run(context, callback);
131
136
  }
132
137
 
138
+ export function runWithRouteState(context, routeState, callback) {
139
+ return routeStateStorage.run({ context, ...routeState }, callback);
140
+ }
141
+
142
+ /**
143
+ * Returns a read-only snapshot of the active SSR route. Unlike the browser
144
+ * navigation controller, this state has no history or mutation methods.
145
+ * Reading `url` follows the same dynamic-rendering semantics as requestUrl().
146
+ *
147
+ * @returns {{
148
+ * url: URL,
149
+ * params: Readonly<Record<string, string | string[] | undefined>>,
150
+ * searchParams: Readonly<Record<string, string | string[] | undefined>>,
151
+ * navigationContext: Readonly<Record<string, unknown>> | null,
152
+ * }}
153
+ */
154
+ export function getRouteState() {
155
+ const context = getActiveContext();
156
+ const trackedState = routeStateStorage.getStore();
157
+ const routeState = trackedState?.context === context ? trackedState : context;
158
+ const state = {};
159
+
160
+ Object.defineProperties(state, {
161
+ url: {
162
+ enumerable: true,
163
+ get() {
164
+ context.didUseDynamicRequestData = true;
165
+ return new URL(context.request.url);
166
+ },
167
+ },
168
+ params: {
169
+ enumerable: true,
170
+ value: routeState.params,
171
+ },
172
+ searchParams: {
173
+ enumerable: true,
174
+ value: routeState.searchParams,
175
+ },
176
+ navigationContext: {
177
+ enumerable: true,
178
+ value: context.navigationContext,
179
+ },
180
+ });
181
+
182
+ return Object.freeze(state);
183
+ }
184
+
133
185
  /**
134
186
  * Returns serializable values supplied by configured server extensions for
135
187
  * the active request. The object is isolated through AsyncLocalStorage and is
package/src/server-api.js CHANGED
@@ -52,3 +52,10 @@ export { responseHeaders } from "./request-context.js";
52
52
 
53
53
  /** Returns the active request's extension-provided serializable values. */
54
54
  export { getRequestContext } from "./request-context.js";
55
+
56
+ /**
57
+ * Returns a read-only snapshot of the active SSR route. This is the server
58
+ * counterpart for reading navigation state; redirects remain separate
59
+ * control-flow functions and browser history methods are intentionally absent.
60
+ */
61
+ export { getRouteState } from "./request-context.js";