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 +199 -11
- package/package.json +36 -13
- package/src/build.js +211 -45
- package/src/cli.js +16 -9
- package/src/client-assets.js +479 -97
- package/src/compiler.js +1104 -19
- package/src/config.js +25 -0
- package/src/constants.js +0 -2
- package/src/deployment-runtime.js +633 -91
- package/src/development-events.js +101 -0
- package/src/development-hot-client.js +65 -0
- package/src/extensions-client.js +101 -0
- package/src/extensions.js +218 -0
- package/src/index.js +10 -1
- package/src/navigation-client.js +711 -0
- package/src/navigation-server.js +5 -0
- package/src/navigation-url.js +28 -0
- package/src/render.js +414 -25
- package/src/request-context-browser.js +1 -0
- package/src/request-context.js +19 -3
- package/src/route-config.js +3 -1
- package/src/route-segments.js +276 -0
- package/src/scaffold.js +4 -1
- package/src/server-api.js +4 -1
- package/src/server.js +191 -33
- package/src/ssr-adapter.js +133 -12
- package/src/terminal.js +32 -0
- package/src/urql-ssr.js +60 -0
- package/templates/default/app/components/feature-card.jsx +37 -0
- package/templates/default/app/{page.litsx → page.jsx} +3 -3
- package/templates/default/jsconfig.json +1 -1
- package/LICENSE +0 -201
- package/templates/default/app/components/feature-card.litsx +0 -35
- /package/templates/default/app/about/{page.litsx → page.jsx} +0 -0
- /package/templates/default/app/{layout.litsx → layout.jsx} +0 -0
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 `.
|
|
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
|
|
111
|
-
headers, cookies, or `requestUrl()` makes
|
|
112
|
-
|
|
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.
|
|
147
|
-
The nearest file wins and its output is wrapped by the route layouts. A root `app/not-found.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
9
|
+
"url": "git+https://github.com/litsxdev/evolit.git"
|
|
10
10
|
},
|
|
11
11
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/litsxdev/
|
|
12
|
+
"url": "https://github.com/litsxdev/evolit/issues"
|
|
13
13
|
},
|
|
14
|
-
"homepage": "https://github.com/litsxdev/
|
|
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": "
|
|
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": ">=
|
|
58
|
+
"node": "^22.18.0 || >=24.11.0"
|
|
47
59
|
},
|
|
48
60
|
"dependencies": {
|
|
49
61
|
"@jridgewell/remapping": "^2.3.5",
|
|
50
|
-
"@litsx/compiler": "0.
|
|
51
|
-
"@litsx/core": "0.
|
|
52
|
-
"@litsx/ssr": "0.
|
|
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
|
+
}
|