evolit 0.1.0-alpha.9 → 0.1.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 CHANGED
@@ -8,7 +8,7 @@ This repository now contains the first framework MVP:
8
8
  - nested `layout` composition
9
9
  - server rendering through `@litsx/ssr`
10
10
  - a small `evolit` CLI with `init`, `dev`, `build`, and `start`
11
- - on-demand compilation of authored `.litsx` modules through `@litsx/compiler`
11
+ - on-demand compilation of authored `.jsx` modules through `@litsx/compiler`
12
12
  - a starter template for generating new sites
13
13
 
14
14
  ## MVP Scope
@@ -26,8 +26,6 @@ It focuses on the core contract that matters first:
26
26
 
27
27
  Supported authored module extensions:
28
28
 
29
- - `.litsx`
30
- - `.litsx.jsx`
31
29
  - `.js`
32
30
  - `.jsx`
33
31
  - `.ts`
@@ -76,6 +74,98 @@ The current runtime is split into a few small layers:
76
74
  - `src/scaffold.js`: creates new site projects from templates
77
75
  - `src/cli.js`: framework entrypoint
78
76
 
77
+ ## Client Navigation
78
+
79
+ Evolit hydrates a small browser router automatically for SSR page documents. It requests a route
80
+ delta, replaces only the changed route segment, and keeps parent layouts mounted when possible.
81
+ Plain `<a>` elements remain valid SSR HTML; same-origin links are progressively intercepted after
82
+ hydration.
83
+
84
+ Use `useNavigation()` inside a LitSX browser component for imperative navigation and pending UI:
85
+
86
+ ```jsx
87
+ import { useNavigation } from "evolit/navigation";
88
+
89
+ export default function CollectionControls() {
90
+ const navigation = useNavigation();
91
+
92
+ function changeSort(event) {
93
+ const searchParams = new URLSearchParams(window.location.search);
94
+ searchParams.delete("page");
95
+ searchParams.delete("skip");
96
+ searchParams.set("sort", event.target.value);
97
+ navigation.push(navigation.createHref("/explore/home-garden", searchParams));
98
+ }
99
+
100
+ return <select onChange={changeSort} disabled={navigation.status === "pending"}>…</select>;
101
+ }
102
+ ```
103
+
104
+ The hook returns `{ status, url, pendingUrl, error, push, replace, refresh, createHref }`:
105
+
106
+ - `push(target, { scroll: false })` adds a browser-history entry. Pass
107
+ `scroll: false` to keep the current viewport position.
108
+ - `replace(target, { scroll: false })` updates the current entry, useful for
109
+ visual-only query state. It accepts the same scroll option.
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
+ Client components can also read the active route state with browser-only hooks:
119
+
120
+ ```jsx
121
+ import { useParams, useSearchParams } from "evolit/navigation";
122
+
123
+ const { slug = [] } = useParams();
124
+ const searchParams = useSearchParams();
125
+ const selectedFacets = searchParams.getAll("facet");
126
+ ```
127
+
128
+ Both hooks update after client navigation. `useParams()` returns a read-only snapshot; the
129
+ `URLSearchParams` from `useSearchParams()` is a local snapshot, so build a new href and navigate to
130
+ it to update the URL.
131
+
132
+ ### Development refresh
133
+
134
+ In development, each browser subscribes its active URL over the Evolit WebSocket. After source
135
+ invalidation, the server renders the fresh SSR representation once per request context and pushes
136
+ the resulting delta directly to its subscribers. No follow-up browser request is needed. Server-only
137
+ module and static-asset changes update only the affected route segment while preserving the document,
138
+ scroll position, and persistent layouts.
139
+ Changes to a hydrated client boundary rebuild its browser artifact and update the stable Evolit
140
+ development proxy registered for that tag. Existing instances receive the new implementation
141
+ without redefining the Custom Element or replacing the document. Invalid deltas and failed hot
142
+ updates still fall back to a document reload.
143
+
144
+ ### Progressive links and forms
145
+
146
+ Links work without JavaScript. With JavaScript, Evolit intercepts ordinary same-origin left-clicks.
147
+ Set `data-evolit-navigation="false"` on a link to keep native navigation.
148
+
149
+ Internal `<form method="get">` elements are treated the same way: their successful controls become
150
+ `URLSearchParams` and navigate through a delta. Without JavaScript the browser submits the exact
151
+ same GET form normally. Evolit intentionally does not intercept `POST`, file-upload, external,
152
+ targeted, or opted-out forms.
153
+
154
+ ### Navigation cache and document updates
155
+
156
+ The browser cache is scoped to browser-history entries, not a global URL map. Going back or forward
157
+ can reuse the delta for that exact entry; opening a new branch after going back discards its known
158
+ forward branch. This avoids an unbounded catalog cache in a long-lived tab.
159
+
160
+ - `dynamic` routes are never cached in the browser.
161
+ - `revalidate` entries remain reusable only until their route TTL expires.
162
+ - `static` entries remain reusable while their history entry exists in the current tab session.
163
+
164
+ Each delta also synchronizes route `<title>`, route-specific `<head>` markup, managed styles and
165
+ module preloads, `html`/`body` attributes, scroll position, hash targets, and focus. If a response
166
+ cannot be represented as an Evolit delta —for example a 404 from another adapter— navigation falls
167
+ back to a normal document load.
168
+
79
169
  ## Route Cache Policies
80
170
 
81
171
  Route modules can export a `routeConfig` object with a `cache` policy:
@@ -104,12 +194,18 @@ export const routeConfig = {
104
194
  - `static`: prerender in `build` and serve from the response cache in `start`
105
195
  - `revalidate`: cache the HTML response for `N` seconds and regenerate on expiry
106
196
 
197
+ Pages without `routeConfig.cache` default to `{ revalidate: 60 }`. This caches a normal SSR
198
+ render by pathname and query string while keeping content fresh without requiring a cache
199
+ declaration on every catalog or CMS page. Declare `cache: "static"` for fully static pages or
200
+ `cache: "dynamic"` when a page must always render per request.
201
+
107
202
  The same semantics work in local development and in production runtimes. Only the backing cache
108
203
  store changes.
109
204
 
110
- The default cache key includes the pathname and query string. Reading the `request` prop, request
111
- headers, cookies, or `requestUrl()` makes the completed render dynamic; it is never stored in the
112
- HTML response cache. `routeConfig.cache` is the sole authority for HTML caching; setting a
205
+ The default cache key includes the pathname and query string, so `params` and `searchParams` are
206
+ cacheable by URL. Reading the `request` prop, request headers, cookies, or `requestUrl()` makes
207
+ the completed render dynamic; it is never stored in the HTML response cache. `routeConfig.cache`
208
+ is the sole authority for HTML caching; setting a
113
209
  `Cache-Control` response header does not alter that policy.
114
210
 
115
211
  ## Request APIs
@@ -141,22 +237,87 @@ headers. `redirect()` and `permanentRedirect()` end rendering with `307` and `30
141
237
  `notFound()` renders a `404`. Reading headers, cookies, or the request URL makes the completed
142
238
  render dynamic, so it is not stored by the route response cache.
143
239
 
240
+ ## Extensions
241
+
242
+ Optional integrations are configured explicitly in `evolit.config.js`. Core only coordinates their
243
+ request and browser-navigation lifecycles: it has no knowledge of tenants, locales, catalogues, or
244
+ message formats. Plugins run in declaration order. Each request hook receives the URL produced by
245
+ the preceding hook; a `rewrite` continues the chain, while a `redirect` or `Response` stops it.
246
+
247
+ ```js
248
+ // evolit.config.js
249
+ import { defineEvolitPlugin } from "evolit/extensions";
250
+
251
+ export default {
252
+ plugins: [defineEvolitPlugin({
253
+ name: "tenant-prefix",
254
+ onRequest({ pathname, headers, set }) {
255
+ const tenant = headers().get("x-tenant") ?? "public";
256
+ set("tenant", tenant);
257
+ if (pathname === "/shop") return { rewrite: `/tenants/${tenant}/shop` };
258
+ },
259
+ client: {
260
+ module: "@example/evolit-tenant/client",
261
+ options: { prefix: "/tenants" },
262
+ },
263
+ })],
264
+ };
265
+ ```
266
+
267
+ `onRequest` is server-only. It can read the `Request`, headers, cookies and URL, set JSON-serializable
268
+ request values, rewrite internally, or return `{ redirect, status? }` / a Web `Response`. Reading
269
+ request-bound data marks the request dynamic. Server components and handlers read the values without
270
+ prop drilling:
271
+
272
+ ```js
273
+ import { getRequestContext } from "evolit/server";
274
+
275
+ export default async function TenantPage() {
276
+ const { tenant } = getRequestContext();
277
+ return `<main>Tenant: ${tenant}</main>`;
278
+ }
279
+ ```
280
+
281
+ The `client.module` is a bare package specifier and is the only extension code included in the
282
+ browser build. It must export a named `navigation` object (or `default`) with optional hooks:
283
+
284
+ ```js
285
+ // @example/evolit-tenant/client
286
+ export const navigation = {
287
+ // Synchronous and idempotent: it may run for generated links and intercepted links.
288
+ transformUrl({ url, options }) {
289
+ return url.startsWith(options.prefix) ? url : `${options.prefix}${url}`;
290
+ },
291
+ async beforeNavigate({ url }) {
292
+ if (url.endsWith("/blocked")) return false;
293
+ },
294
+ afterNavigate({ from, url }) {
295
+ // Browser-only analytics or state synchronization.
296
+ },
297
+ };
298
+ ```
299
+
300
+ `transformUrl` runs for `createHref()` and before a SPA navigation; `beforeNavigate` can transform
301
+ or cancel a navigation, and `afterNavigate` runs after its delta is applied. Hooks are sequential
302
+ and later hooks receive the URL returned by earlier hooks. Browser modules must not import server
303
+ code. Request values are isolated with async request context and are discarded when rendering ends.
304
+
144
305
  ## Route Boundaries
145
306
 
146
- `not-found.litsx` and `error.litsx` are resolved from the current route directory up to `app/`.
147
- The nearest file wins and its output is wrapped by the route layouts. A root `app/not-found.litsx`
307
+ `not-found.jsx` and `error.jsx` are resolved from the current route directory up to `app/`.
308
+ The nearest file wins and its output is wrapped by the route layouts. A root `app/not-found.jsx`
148
309
  also handles unmatched URLs; without one, evolit returns its minimal built-in 404 document.
149
310
 
150
311
  ```js
151
- // app/blog/error.litsx
312
+ // app/blog/error.jsx
152
313
  export default async function BlogError({ error }) {
153
314
  return `<p>Could not load this post: ${error.message}</p>`;
154
315
  }
155
316
  ```
156
317
 
157
- Boundaries always bypass the route response cache. `loading.litsx` is intentionally not supported
318
+ Boundaries always bypass the route response cache. `loading.jsx` is intentionally not supported
158
319
  yet: it requires an end-to-end streaming document transport rather than an HTML-string fallback.
159
- In production, `error.litsx` receives a generic error with an opaque `digest`; the original error
320
+ In production, `error.jsx` receives a generic error with an opaque `digest`; the original error
160
321
  is reported only on the server.
161
322
 
162
323
  ## Route Handlers
@@ -231,6 +392,33 @@ export default {
231
392
  };
232
393
  ```
233
394
 
395
+ For monorepos, development watches only the application project by default. Additional source
396
+ trees can be opted into explicitly; generated output and dependency directories below every root
397
+ remain ignored:
398
+
399
+ ```js
400
+ export default {
401
+ development: {
402
+ managedSourceRoots: ["../packages/design-system/src"],
403
+ },
404
+ };
405
+ ```
406
+
407
+ Imports outside these roots remain unmanaged and produce the existing development warning.
408
+
409
+ Evolit normally discovers browser boundaries from the static module graph and reconciles that
410
+ inventory with the components actually emitted by SSR. A genuinely computed import cannot be
411
+ enumerated at build time, so production applications can declare only those exceptional modules:
412
+
413
+ ```js
414
+ export default {
415
+ clientBoundaries: ["./src/components/dynamic-card.jsx", "@acme/ui/product-card"],
416
+ };
417
+ ```
418
+
419
+ These modules are materialized as build artifacts, but are not automatically imported or
420
+ preloaded. They reach the browser only when an SSR hydration root references them.
421
+
234
422
  The framework also exports `ObjectStorageResponseCacheStore`, which is intended for object-store
235
423
  backends such as S3. A concrete app can wire it to the AWS SDK without pulling AWS dependencies
236
424
  into `evolit` itself.
package/package.json CHANGED
@@ -1,35 +1,47 @@
1
1
  {
2
2
  "name": "evolit",
3
- "version": "0.1.0-alpha.9",
3
+ "version": "0.1.1",
4
4
  "description": "A convention-driven application framework for LitSX and web components.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.10.3",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/litsxdev/nextsx.git"
9
+ "url": "git+https://github.com/litsxdev/evolit.git"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/litsxdev/nextsx/issues"
12
+ "url": "https://github.com/litsxdev/evolit/issues"
13
13
  },
14
- "homepage": "https://github.com/litsxdev/nextsx#readme",
14
+ "homepage": "https://github.com/litsxdev/evolit#readme",
15
15
  "files": [
16
16
  "src",
17
- "templates"
17
+ "templates/default/app/**",
18
+ "templates/default/jsconfig.json",
19
+ "templates/default/yarnrc.yml.template"
18
20
  ],
19
- "bin": "./src/cli.js",
21
+ "bin": "src/cli.js",
20
22
  "main": "./src/index.js",
21
23
  "exports": {
22
24
  ".": "./src/index.js",
25
+ "./navigation": {
26
+ "browser": "./src/navigation-client.js",
27
+ "default": "./src/navigation-server.js"
28
+ },
23
29
  "./server": {
24
30
  "browser": "./src/request-context-browser.js",
25
31
  "default": "./src/server-api.js"
26
- }
32
+ },
33
+ "./extensions": {
34
+ "browser": "./src/extensions-client.js",
35
+ "default": "./src/extensions.js"
36
+ },
37
+ "./internal/development-hot": "./src/development-hot-client.js"
27
38
  },
28
39
  "scripts": {
29
40
  "dev": "node ./src/cli.js dev",
30
41
  "build": "node ./src/cli.js build",
31
42
  "start": "node ./src/cli.js start",
32
- "test": "node --test",
43
+ "test": "node --test test/*.test.js",
44
+ "test:browser": "playwright test",
33
45
  "typecheck": "litsx-tsc -p jsconfig.json --noEmit",
34
46
  "release:check": "yarn test && yarn typecheck && yarn pack --dry-run"
35
47
  },
@@ -43,14 +55,15 @@
43
55
  "author": "LitSX Team",
44
56
  "license": "Apache-2.0",
45
57
  "engines": {
46
- "node": ">=20.19.5"
58
+ "node": "^22.18.0 || >=24.11.0"
47
59
  },
48
60
  "dependencies": {
49
61
  "@jridgewell/remapping": "^2.3.5",
50
- "@litsx/compiler": "0.9.5-canary-feat-ssr-20260726184312",
51
- "@litsx/core": "0.17.0-canary-feat-ssr-20260726184312",
52
- "@litsx/ssr": "0.2.0-canary-feat-ssr-20260726184312",
62
+ "@litsx/compiler": "1.0.0-next.8",
63
+ "@litsx/core": "1.0.0-next.5",
64
+ "@litsx/ssr": "1.0.0-next.3",
53
65
  "@litsx/typescript": "^0.9.0",
66
+ "@rollup/plugin-commonjs": "^29.0.0",
54
67
  "@rollup/plugin-node-resolve": "^16.0.3",
55
68
  "lit": "^3.3.3",
56
69
  "magic-string": "^1.1.0",
@@ -58,7 +71,17 @@
58
71
  "typescript": "^6.0.0",
59
72
  "ws": "^8.18.3"
60
73
  },
74
+ "peerDependencies": {
75
+ "@litsx/urql": "^0.3.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "@litsx/urql": {
79
+ "optional": true
80
+ }
81
+ },
61
82
  "devDependencies": {
83
+ "@playwright/test": "^1.62.0",
84
+ "@webcomponents/scoped-custom-element-registry": "^0.0.10",
62
85
  "vite": "^8.1.5"
63
86
  }
64
- }
87
+ }