evolit 0.1.0-alpha.2 → 0.1.0-alpha.21
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 +73 -3
- package/package.json +13 -6
- package/src/build.js +37 -9
- package/src/cli.js +16 -9
- package/src/client-assets.js +326 -70
- package/src/compiler.js +176 -9
- package/src/deployment-runtime.js +246 -43
- package/src/development-events.js +96 -0
- package/src/navigation-client.js +519 -0
- package/src/navigation-server.js +3 -0
- package/src/navigation-url.js +28 -0
- package/src/render.js +328 -19
- package/src/route-config.js +3 -1
- package/src/route-segments.js +272 -0
- package/src/scaffold.js +4 -0
- package/src/server.js +156 -13
- package/src/ssr-adapter.js +127 -12
- package/src/terminal.js +32 -0
- package/templates/default/yarnrc.yml.template +1 -0
package/README.md
CHANGED
|
@@ -76,6 +76,70 @@ The current runtime is split into a few small layers:
|
|
|
76
76
|
- `src/scaffold.js`: creates new site projects from templates
|
|
77
77
|
- `src/cli.js`: framework entrypoint
|
|
78
78
|
|
|
79
|
+
## Client Navigation
|
|
80
|
+
|
|
81
|
+
Evolit hydrates a small browser router automatically for SSR page documents. It requests a route
|
|
82
|
+
delta, replaces only the changed route segment, and keeps parent layouts mounted when possible.
|
|
83
|
+
Plain `<a>` elements remain valid SSR HTML; same-origin links are progressively intercepted after
|
|
84
|
+
hydration.
|
|
85
|
+
|
|
86
|
+
Use `useNavigation()` inside a LitSX browser component for imperative navigation and pending UI:
|
|
87
|
+
|
|
88
|
+
```jsx
|
|
89
|
+
import { useNavigation } from "evolit/navigation";
|
|
90
|
+
|
|
91
|
+
export default function CollectionControls() {
|
|
92
|
+
const navigation = useNavigation();
|
|
93
|
+
|
|
94
|
+
function changeSort(event) {
|
|
95
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
96
|
+
searchParams.delete("page");
|
|
97
|
+
searchParams.delete("skip");
|
|
98
|
+
searchParams.set("sort", event.target.value);
|
|
99
|
+
navigation.push(navigation.createHref("/explore/home-garden", searchParams));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return <select onChange={changeSort} disabled={navigation.status === "pending"}>…</select>;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The hook returns `{ status, url, pendingUrl, error, push, replace, refresh, createHref }`:
|
|
107
|
+
|
|
108
|
+
- `push(target)` adds a browser-history entry.
|
|
109
|
+
- `replace(target)` updates the current entry, useful for visual-only query state.
|
|
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
|
+
### Progressive links and forms
|
|
119
|
+
|
|
120
|
+
Links work without JavaScript. With JavaScript, Evolit intercepts ordinary same-origin left-clicks.
|
|
121
|
+
Set `data-evolit-navigation="false"` on a link to keep native navigation.
|
|
122
|
+
|
|
123
|
+
Internal `<form method="get">` elements are treated the same way: their successful controls become
|
|
124
|
+
`URLSearchParams` and navigate through a delta. Without JavaScript the browser submits the exact
|
|
125
|
+
same GET form normally. Evolit intentionally does not intercept `POST`, file-upload, external,
|
|
126
|
+
targeted, or opted-out forms.
|
|
127
|
+
|
|
128
|
+
### Navigation cache and document updates
|
|
129
|
+
|
|
130
|
+
The browser cache is scoped to browser-history entries, not a global URL map. Going back or forward
|
|
131
|
+
can reuse the delta for that exact entry; opening a new branch after going back discards its known
|
|
132
|
+
forward branch. This avoids an unbounded catalog cache in a long-lived tab.
|
|
133
|
+
|
|
134
|
+
- `dynamic` routes are never cached in the browser.
|
|
135
|
+
- `revalidate` entries remain reusable only until their route TTL expires.
|
|
136
|
+
- `static` entries remain reusable while their history entry exists in the current tab session.
|
|
137
|
+
|
|
138
|
+
Each delta also synchronizes route `<title>`, route-specific `<head>` markup, managed styles and
|
|
139
|
+
module preloads, `html`/`body` attributes, scroll position, hash targets, and focus. If a response
|
|
140
|
+
cannot be represented as an Evolit delta —for example a 404 from another adapter— navigation falls
|
|
141
|
+
back to a normal document load.
|
|
142
|
+
|
|
79
143
|
## Route Cache Policies
|
|
80
144
|
|
|
81
145
|
Route modules can export a `routeConfig` object with a `cache` policy:
|
|
@@ -104,12 +168,18 @@ export const routeConfig = {
|
|
|
104
168
|
- `static`: prerender in `build` and serve from the response cache in `start`
|
|
105
169
|
- `revalidate`: cache the HTML response for `N` seconds and regenerate on expiry
|
|
106
170
|
|
|
171
|
+
Pages without `routeConfig.cache` default to `{ revalidate: 60 }`. This caches a normal SSR
|
|
172
|
+
render by pathname and query string while keeping content fresh without requiring a cache
|
|
173
|
+
declaration on every catalog or CMS page. Declare `cache: "static"` for fully static pages or
|
|
174
|
+
`cache: "dynamic"` when a page must always render per request.
|
|
175
|
+
|
|
107
176
|
The same semantics work in local development and in production runtimes. Only the backing cache
|
|
108
177
|
store changes.
|
|
109
178
|
|
|
110
|
-
The default cache key includes the pathname and query string
|
|
111
|
-
headers, cookies, or `requestUrl()` makes
|
|
112
|
-
|
|
179
|
+
The default cache key includes the pathname and query string, so `params` and `searchParams` are
|
|
180
|
+
cacheable by URL. Reading the `request` prop, request headers, cookies, or `requestUrl()` makes
|
|
181
|
+
the completed render dynamic; it is never stored in the HTML response cache. `routeConfig.cache`
|
|
182
|
+
is the sole authority for HTML caching; setting a
|
|
113
183
|
`Cache-Control` response header does not alter that policy.
|
|
114
184
|
|
|
115
185
|
## Request APIs
|
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.21",
|
|
4
4
|
"description": "A convention-driven application framework for LitSX and web components.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "yarn@4.10.3",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"main": "./src/index.js",
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./src/index.js",
|
|
23
|
+
"./navigation": {
|
|
24
|
+
"browser": "./src/navigation-client.js",
|
|
25
|
+
"default": "./src/navigation-server.js"
|
|
26
|
+
},
|
|
23
27
|
"./server": {
|
|
24
28
|
"browser": "./src/request-context-browser.js",
|
|
25
29
|
"default": "./src/server-api.js"
|
|
@@ -29,7 +33,8 @@
|
|
|
29
33
|
"dev": "node ./src/cli.js dev",
|
|
30
34
|
"build": "node ./src/cli.js build",
|
|
31
35
|
"start": "node ./src/cli.js start",
|
|
32
|
-
"test": "node --test",
|
|
36
|
+
"test": "node --test test/*.test.js",
|
|
37
|
+
"test:browser": "playwright test",
|
|
33
38
|
"typecheck": "litsx-tsc -p jsconfig.json --noEmit",
|
|
34
39
|
"release:check": "yarn test && yarn typecheck && yarn pack --dry-run"
|
|
35
40
|
},
|
|
@@ -47,17 +52,19 @@
|
|
|
47
52
|
},
|
|
48
53
|
"dependencies": {
|
|
49
54
|
"@jridgewell/remapping": "^2.3.5",
|
|
50
|
-
"@litsx/compiler": "0.9.5-canary-feat-ssr-
|
|
51
|
-
"@litsx/core": "0.17.0-canary-feat-ssr-
|
|
52
|
-
"@litsx/ssr": "0.2.0-canary-feat-ssr-
|
|
55
|
+
"@litsx/compiler": "0.9.5-canary-feat-ssr-20260726184312",
|
|
56
|
+
"@litsx/core": "0.17.0-canary-feat-ssr-20260726184312",
|
|
57
|
+
"@litsx/ssr": "0.2.0-canary-feat-ssr-20260726184312",
|
|
53
58
|
"@litsx/typescript": "^0.9.0",
|
|
54
59
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
55
60
|
"lit": "^3.3.3",
|
|
56
61
|
"magic-string": "^1.1.0",
|
|
57
62
|
"rollup": "^4.46.1",
|
|
58
|
-
"typescript": "^6.0.0"
|
|
63
|
+
"typescript": "^6.0.0",
|
|
64
|
+
"ws": "^8.18.3"
|
|
59
65
|
},
|
|
60
66
|
"devDependencies": {
|
|
67
|
+
"@playwright/test": "^1.62.0",
|
|
61
68
|
"vite": "^8.1.5"
|
|
62
69
|
}
|
|
63
70
|
}
|
package/src/build.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
emitBundledClientAssets,
|
|
16
16
|
emitHashedClientAssets,
|
|
17
17
|
normalizeHydrationDataForClient,
|
|
18
|
+
resolveRouteClientImports,
|
|
18
19
|
resolveSharedVendorModuleUrl,
|
|
19
20
|
rewriteHydrationDataScript,
|
|
20
21
|
rewriteServerAssetPlaceholders,
|
|
@@ -241,27 +242,54 @@ export async function buildProject(projectRoot) {
|
|
|
241
242
|
);
|
|
242
243
|
const ssrAdapter = createSsrAdapter({
|
|
243
244
|
assetResolver,
|
|
244
|
-
async resolveAdditionalHead({ result }) {
|
|
245
|
-
const clientImports =
|
|
246
|
-
|
|
245
|
+
async resolveAdditionalHead({ routeResult, result }) {
|
|
246
|
+
const clientImports = [
|
|
247
|
+
...(Array.isArray(result.clientImports) ? result.clientImports : []),
|
|
248
|
+
...resolveRouteClientImports(routeResult, projectRoot, clientAssets),
|
|
249
|
+
];
|
|
250
|
+
const hydratedClientImports = Array.isArray(result.clientImports) ? result.clientImports : [];
|
|
251
|
+
const urls = hydratedClientImports.length > 0
|
|
252
|
+
? [...new Set([
|
|
253
|
+
...hydratedClientImports,
|
|
254
|
+
...collectTransitiveAssetPreloads(hydratedClientImports, clientAssets),
|
|
255
|
+
])]
|
|
256
|
+
: [];
|
|
247
257
|
const styleUrls = collectTransitiveStyleUrls(clientImports, clientAssets);
|
|
248
258
|
|
|
249
259
|
return [
|
|
250
|
-
...urls.map((href) => `<link rel="modulepreload" href="${href}">`),
|
|
251
|
-
...styleUrls.map((href) => `<link rel="stylesheet" href="${href}">`),
|
|
260
|
+
...urls.map((href) => `<link rel="modulepreload" href="${href}" data-evolit-route-asset="preload">`),
|
|
261
|
+
...styleUrls.map((href) => `<link rel="stylesheet" href="${href}" data-evolit-route-asset="style">`),
|
|
252
262
|
].join("\n");
|
|
253
263
|
},
|
|
254
|
-
resolveBootstrap({ result }) {
|
|
264
|
+
resolveBootstrap({ routeResult, result }) {
|
|
265
|
+
const routeClientImports = resolveRouteClientImports(
|
|
266
|
+
routeResult,
|
|
267
|
+
projectRoot,
|
|
268
|
+
clientAssets,
|
|
269
|
+
);
|
|
255
270
|
return createHydrationBootstrap({
|
|
256
|
-
hydrationData: normalizeHydrationDataForClient(
|
|
271
|
+
hydrationData: normalizeHydrationDataForClient(
|
|
272
|
+
result.hydrationData,
|
|
273
|
+
projectRoot,
|
|
274
|
+
routeClientImports,
|
|
275
|
+
),
|
|
257
276
|
assetResolver,
|
|
258
277
|
hydrationModuleUrl,
|
|
259
278
|
});
|
|
260
279
|
},
|
|
261
|
-
transformDocument({ result, document }) {
|
|
280
|
+
transformDocument({ routeResult, result, document }) {
|
|
281
|
+
const routeClientImports = resolveRouteClientImports(
|
|
282
|
+
routeResult,
|
|
283
|
+
projectRoot,
|
|
284
|
+
clientAssets,
|
|
285
|
+
);
|
|
262
286
|
return rewriteHydrationDataScript(
|
|
263
287
|
document,
|
|
264
|
-
normalizeHydrationDataForClient(
|
|
288
|
+
normalizeHydrationDataForClient(
|
|
289
|
+
result.hydrationData,
|
|
290
|
+
projectRoot,
|
|
291
|
+
routeClientImports,
|
|
292
|
+
),
|
|
265
293
|
);
|
|
266
294
|
},
|
|
267
295
|
});
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { scaffoldSite } from "./scaffold.js";
|
|
6
|
+
import { createDevelopmentEventReporter } from "./development-events.js";
|
|
7
|
+
|
|
8
|
+
const reportEvent = createDevelopmentEventReporter();
|
|
6
9
|
|
|
7
10
|
function parseArguments(argv) {
|
|
8
11
|
const [command, ...rest] = argv;
|
|
@@ -24,9 +27,7 @@ function parseArguments(argv) {
|
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
function printUsage() {
|
|
27
|
-
|
|
28
|
-
console.log(" evolit init <directory>");
|
|
29
|
-
console.log(" evolit <dev|build|start> [--port 3000]");
|
|
30
|
+
reportEvent({ type: "usage" });
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
async function createSite(projectRoot, targetDirectory) {
|
|
@@ -35,7 +36,7 @@ async function createSite(projectRoot, targetDirectory) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const createdDirectory = await scaffoldSite(path.resolve(projectRoot, targetDirectory));
|
|
38
|
-
|
|
39
|
+
reportEvent({ type: "site-created", directory: createdDirectory });
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
async function run() {
|
|
@@ -55,23 +56,29 @@ async function run() {
|
|
|
55
56
|
if (command === "build") {
|
|
56
57
|
const { buildProject } = await import("./build.js");
|
|
57
58
|
const manifestPath = await buildProject(projectRoot);
|
|
58
|
-
|
|
59
|
+
reportEvent({
|
|
60
|
+
type: "build-ready",
|
|
61
|
+
manifestPath: path.relative(projectRoot, manifestPath),
|
|
62
|
+
});
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
if (command === "start") {
|
|
63
67
|
const { createStartServer } = await import("./server.js");
|
|
68
|
+
reportEvent({ type: "server-starting", mode: "start" });
|
|
64
69
|
const server = await createStartServer(projectRoot, options);
|
|
65
70
|
await server.listen();
|
|
66
|
-
|
|
71
|
+
reportEvent({ type: "server-ready", port: server.port });
|
|
67
72
|
return;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
if (command === "dev") {
|
|
71
76
|
const { createDevServer } = await import("./server.js");
|
|
72
|
-
const server = await createDevServer(projectRoot,
|
|
77
|
+
const server = await createDevServer(projectRoot, {
|
|
78
|
+
...options,
|
|
79
|
+
onDevelopmentEvent: reportEvent,
|
|
80
|
+
});
|
|
73
81
|
await server.listen();
|
|
74
|
-
console.log(`evolit dev listening on http://localhost:${server.port}`);
|
|
75
82
|
return;
|
|
76
83
|
}
|
|
77
84
|
|
|
@@ -79,6 +86,6 @@ async function run() {
|
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
run().catch((error) => {
|
|
82
|
-
|
|
89
|
+
reportEvent({ type: "fatal-error", error });
|
|
83
90
|
process.exitCode = 1;
|
|
84
91
|
});
|