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 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. 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
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.2",
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-20260726130435",
51
- "@litsx/core": "0.17.0-canary-feat-ssr-20260726130435",
52
- "@litsx/ssr": "0.2.0-canary-feat-ssr-20260726130435",
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 = Array.isArray(result.clientImports) ? result.clientImports : [];
246
- const urls = collectTransitiveAssetPreloads(clientImports, clientAssets);
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(result.hydrationData, projectRoot),
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(result.hydrationData, projectRoot),
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
- console.log("Usage: evolit <directory>");
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
- console.log(`Created evolit site: ${createdDirectory}`);
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
- console.log(`Built evolit app: ${path.relative(projectRoot, manifestPath)}`);
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
- console.log(`evolit start listening on http://localhost:${server.port}`);
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, options);
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
- console.error(error instanceof Error ? error.stack ?? error.message : String(error));
89
+ reportEvent({ type: "fatal-error", error });
83
90
  process.exitCode = 1;
84
91
  });