evolit 0.1.0-alpha.3 → 0.1.0-alpha.30

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,98 @@ 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, { scroll: false })` adds a browser-history entry. Pass
109
+ `scroll: false` to keep the current viewport position.
110
+ - `replace(target, { scroll: false })` updates the current entry, useful for
111
+ visual-only query state. It accepts the same scroll option.
112
+ - `refresh()` bypasses the client delta cache for the current URL.
113
+ - `createHref(pathname, searchParams)` creates a relative internal URL. It accepts standard
114
+ `URLSearchParams`, preserving repeated keys such as `facet=brand&facet=material`.
115
+
116
+ `createHref` can also be imported directly from `evolit/navigation`; it is browser-free and safe to
117
+ share with server-evaluated route code. `useNavigation()` itself is browser-only and must only run
118
+ from a connected client component.
119
+
120
+ Client components can also read the active route state with browser-only hooks:
121
+
122
+ ```jsx
123
+ import { useParams, useSearchParams } from "evolit/navigation";
124
+
125
+ const { slug = [] } = useParams();
126
+ const searchParams = useSearchParams();
127
+ const selectedFacets = searchParams.getAll("facet");
128
+ ```
129
+
130
+ Both hooks update after client navigation. `useParams()` returns a read-only snapshot; the
131
+ `URLSearchParams` from `useSearchParams()` is a local snapshot, so build a new href and navigate to
132
+ it to update the URL.
133
+
134
+ ### Development refresh
135
+
136
+ In development, each browser subscribes its active URL over the Evolit WebSocket. After source
137
+ invalidation, the server renders the fresh SSR representation once per request context and pushes
138
+ the resulting delta directly to its subscribers. No follow-up browser request is needed. Server-only
139
+ module and static-asset changes update only the affected route segment while preserving the document,
140
+ scroll position, and persistent layouts.
141
+ Changes to a hydrated client boundary rebuild its browser artifact and update the stable Evolit
142
+ development proxy registered for that tag. Existing instances receive the new implementation
143
+ without redefining the Custom Element or replacing the document. Invalid deltas and failed hot
144
+ updates still fall back to a document reload.
145
+
146
+ ### Progressive links and forms
147
+
148
+ Links work without JavaScript. With JavaScript, Evolit intercepts ordinary same-origin left-clicks.
149
+ Set `data-evolit-navigation="false"` on a link to keep native navigation.
150
+
151
+ Internal `<form method="get">` elements are treated the same way: their successful controls become
152
+ `URLSearchParams` and navigate through a delta. Without JavaScript the browser submits the exact
153
+ same GET form normally. Evolit intentionally does not intercept `POST`, file-upload, external,
154
+ targeted, or opted-out forms.
155
+
156
+ ### Navigation cache and document updates
157
+
158
+ The browser cache is scoped to browser-history entries, not a global URL map. Going back or forward
159
+ can reuse the delta for that exact entry; opening a new branch after going back discards its known
160
+ forward branch. This avoids an unbounded catalog cache in a long-lived tab.
161
+
162
+ - `dynamic` routes are never cached in the browser.
163
+ - `revalidate` entries remain reusable only until their route TTL expires.
164
+ - `static` entries remain reusable while their history entry exists in the current tab session.
165
+
166
+ Each delta also synchronizes route `<title>`, route-specific `<head>` markup, managed styles and
167
+ module preloads, `html`/`body` attributes, scroll position, hash targets, and focus. If a response
168
+ cannot be represented as an Evolit delta —for example a 404 from another adapter— navigation falls
169
+ back to a normal document load.
170
+
79
171
  ## Route Cache Policies
80
172
 
81
173
  Route modules can export a `routeConfig` object with a `cache` policy:
@@ -104,12 +196,18 @@ export const routeConfig = {
104
196
  - `static`: prerender in `build` and serve from the response cache in `start`
105
197
  - `revalidate`: cache the HTML response for `N` seconds and regenerate on expiry
106
198
 
199
+ Pages without `routeConfig.cache` default to `{ revalidate: 60 }`. This caches a normal SSR
200
+ render by pathname and query string while keeping content fresh without requiring a cache
201
+ declaration on every catalog or CMS page. Declare `cache: "static"` for fully static pages or
202
+ `cache: "dynamic"` when a page must always render per request.
203
+
107
204
  The same semantics work in local development and in production runtimes. Only the backing cache
108
205
  store changes.
109
206
 
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
207
+ The default cache key includes the pathname and query string, so `params` and `searchParams` are
208
+ cacheable by URL. Reading the `request` prop, request headers, cookies, or `requestUrl()` makes
209
+ the completed render dynamic; it is never stored in the HTML response cache. `routeConfig.cache`
210
+ is the sole authority for HTML caching; setting a
113
211
  `Cache-Control` response header does not alter that policy.
114
212
 
115
213
  ## Request APIs
@@ -231,6 +329,33 @@ export default {
231
329
  };
232
330
  ```
233
331
 
332
+ For monorepos, development watches only the application project by default. Additional source
333
+ trees can be opted into explicitly; generated output and dependency directories below every root
334
+ remain ignored:
335
+
336
+ ```js
337
+ export default {
338
+ development: {
339
+ managedSourceRoots: ["../packages/design-system/src"],
340
+ },
341
+ };
342
+ ```
343
+
344
+ Imports outside these roots remain unmanaged and produce the existing development warning.
345
+
346
+ Evolit normally discovers browser boundaries from the static module graph and reconciles that
347
+ inventory with the components actually emitted by SSR. A genuinely computed import cannot be
348
+ enumerated at build time, so production applications can declare only those exceptional modules:
349
+
350
+ ```js
351
+ export default {
352
+ clientBoundaries: ["./src/components/dynamic-card.litsx", "@acme/ui/product-card"],
353
+ };
354
+ ```
355
+
356
+ These modules are materialized as build artifacts, but are not automatically imported or
357
+ preloaded. They reach the browser only when an SSR hydration root references them.
358
+
234
359
  The framework also exports `ObjectStorageResponseCacheStore`, which is intended for object-store
235
360
  backends such as S3. A concrete app can wire it to the AWS SDK without pulling AWS dependencies
236
361
  into `evolit` itself.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolit",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.30",
4
4
  "description": "A convention-driven application framework for LitSX and web components.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.10.3",
@@ -14,22 +14,30 @@
14
14
  "homepage": "https://github.com/litsxdev/nextsx#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
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
+ "./internal/development-hot": "./src/development-hot-client.js"
27
34
  },
28
35
  "scripts": {
29
36
  "dev": "node ./src/cli.js dev",
30
37
  "build": "node ./src/cli.js build",
31
38
  "start": "node ./src/cli.js start",
32
- "test": "node --test",
39
+ "test": "node --test test/*.test.js",
40
+ "test:browser": "playwright test",
33
41
  "typecheck": "litsx-tsc -p jsconfig.json --noEmit",
34
42
  "release:check": "yarn test && yarn typecheck && yarn pack --dry-run"
35
43
  },
@@ -47,17 +55,29 @@
47
55
  },
48
56
  "dependencies": {
49
57
  "@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",
58
+ "@litsx/compiler": "0.10.0-canary-feat-ssr-20260801095541",
59
+ "@litsx/core": "0.17.0-canary-feat-ssr-20260801101748",
60
+ "@litsx/ssr": "0.2.0-canary-feat-ssr-20260801095541",
53
61
  "@litsx/typescript": "^0.9.0",
62
+ "@rollup/plugin-commonjs": "^29.0.0",
54
63
  "@rollup/plugin-node-resolve": "^16.0.3",
55
64
  "lit": "^3.3.3",
56
65
  "magic-string": "^1.1.0",
57
66
  "rollup": "^4.46.1",
58
- "typescript": "^6.0.0"
67
+ "typescript": "^6.0.0",
68
+ "ws": "^8.18.3"
69
+ },
70
+ "peerDependencies": {
71
+ "@litsx/urql": "^0.3.0"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "@litsx/urql": {
75
+ "optional": true
76
+ }
59
77
  },
60
78
  "devDependencies": {
79
+ "@playwright/test": "^1.62.0",
80
+ "@webcomponents/scoped-custom-element-registry": "^0.0.10",
61
81
  "vite": "^8.1.5"
62
82
  }
63
83
  }
package/src/build.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import {
4
5
  discoverAppRouteHandlers,
5
6
  discoverAppRoutes,
@@ -15,11 +16,20 @@ import {
15
16
  emitBundledClientAssets,
16
17
  emitHashedClientAssets,
17
18
  normalizeHydrationDataForClient,
19
+ resolveServerStyleUrls,
20
+ resolveHydrationRootClientImports,
18
21
  resolveSharedVendorModuleUrl,
19
22
  rewriteHydrationDataScript,
20
23
  rewriteServerAssetPlaceholders,
21
24
  } from "./client-assets.js";
22
- import { compileModuleGraph, importCompiledModule } from "./compiler.js";
25
+ import {
26
+ collectClientGraphInventory,
27
+ compileModuleGraph,
28
+ emitClientStaticAssets,
29
+ getClientStaticAssetModule,
30
+ importCompiledModule,
31
+ resolveProjectModuleSpecifier,
32
+ } from "./compiler.js";
23
33
  import { loadEvolitConfig } from "./config.js";
24
34
  import {
25
35
  BUILD_DIRECTORY,
@@ -38,6 +48,7 @@ import {
38
48
  import { serializeRouteCachePolicy } from "./route-config.js";
39
49
  import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
40
50
  import { ensureDirectory, writeJson } from "./fs-utils.js";
51
+ import { appendSsrUrqlData, runWithOptionalSsrUrqlScope } from "./urql-ssr.js";
41
52
 
42
53
  const CONTENT_TYPE_BY_EXTENSION = new Map([
43
54
  [".css", "text/css; charset=utf-8"],
@@ -122,7 +133,37 @@ export async function buildProject(projectRoot) {
122
133
  const routeHandlers = await discoverAppRouteHandlers(projectRoot);
123
134
  const buildRoot = path.join(projectRoot, INTERNAL_DIRECTORY, BUILD_DIRECTORY);
124
135
  const entryClientModules = new Set();
136
+ const serverAssetImportsByEntry = {};
137
+ const clientBoundariesByEntry = {};
125
138
  const deployHandlers = [];
139
+ const compiledClientBoundaries = new Map();
140
+ const inventoriesBySourceEntry = new Map();
141
+
142
+ function getEntryInventory(entryPath) {
143
+ let inventory = inventoriesBySourceEntry.get(entryPath);
144
+ if (!inventory) {
145
+ inventory = collectClientGraphInventory([entryPath], { projectRoot });
146
+ inventoriesBySourceEntry.set(entryPath, inventory);
147
+ }
148
+ return inventory;
149
+ }
150
+
151
+ async function compileProductionClientBoundary(sourcePath) {
152
+ let clientModule = compiledClientBoundaries.get(sourcePath);
153
+ if (clientModule) return clientModule;
154
+ const clientBuild = await compileModuleGraph(sourcePath, {
155
+ projectRoot,
156
+ mode: "production",
157
+ sourceMaps: true,
158
+ target: "client",
159
+ });
160
+ clientModule = path.relative(clientBuild.outputRoot, clientBuild.entrypoint)
161
+ .split(path.sep)
162
+ .join("/");
163
+ compiledClientBoundaries.set(sourcePath, clientModule);
164
+ entryClientModules.add(clientModule);
165
+ return clientModule;
166
+ }
126
167
 
127
168
  await ensureDirectory(buildRoot);
128
169
 
@@ -165,16 +206,6 @@ export async function buildProject(projectRoot) {
165
206
  target: "server",
166
207
  });
167
208
 
168
- const pageClientBuild = await compileModuleGraph(route.page, {
169
- projectRoot,
170
- mode: "production",
171
- sourceMaps: true,
172
- target: "client",
173
- });
174
- entryClientModules.add(
175
- path.relative(pageClientBuild.outputRoot, pageClientBuild.entrypoint).split(path.sep).join("/"),
176
- );
177
-
178
209
  for (const layoutPath of route.layouts) {
179
210
  await compileModuleGraph(layoutPath, {
180
211
  projectRoot,
@@ -184,15 +215,6 @@ export async function buildProject(projectRoot) {
184
215
  target: "server",
185
216
  });
186
217
 
187
- const layoutClientBuild = await compileModuleGraph(layoutPath, {
188
- projectRoot,
189
- mode: "production",
190
- sourceMaps: true,
191
- target: "client",
192
- });
193
- entryClientModules.add(
194
- path.relative(layoutClientBuild.outputRoot, layoutClientBuild.entrypoint).split(path.sep).join("/"),
195
- );
196
218
  }
197
219
 
198
220
  const boundaryModules = [
@@ -207,23 +229,70 @@ export async function buildProject(projectRoot) {
207
229
  ssr: true,
208
230
  target: "server",
209
231
  });
232
+ }
210
233
 
211
- const boundaryClientBuild = await compileModuleGraph(boundaryPath, {
234
+ const toProjectRelative = (filePath) => path.relative(projectRoot, filePath).split(path.sep).join("/");
235
+ const segmentEntries = [...new Set([route.page, ...route.layouts, ...boundaryModules])];
236
+ const inventoriesByEntry = new Map(await Promise.all(segmentEntries.map(async (entryPath) => [
237
+ entryPath,
238
+ await getEntryInventory(entryPath),
239
+ ])));
240
+ const allStyles = new Set();
241
+ const allAssets = new Set();
242
+ const allClientBoundaries = new Set();
243
+ for (const [entryPath, entryInventory] of inventoriesByEntry) {
244
+ serverAssetImportsByEntry[toProjectRelative(entryPath)] = {
245
+ styles: entryInventory.styles.map((filePath) => getClientStaticAssetModule(projectRoot, filePath)),
246
+ assets: entryInventory.assets.map((filePath) => getClientStaticAssetModule(projectRoot, filePath)),
247
+ };
248
+ entryInventory.styles.forEach((filePath) => allStyles.add(filePath));
249
+ entryInventory.assets.forEach((filePath) => allAssets.add(filePath));
250
+ entryInventory.clientBoundaries.forEach((filePath) => allClientBoundaries.add(filePath));
251
+ }
252
+ await emitClientStaticAssets([...allStyles, ...allAssets], {
253
+ projectRoot,
254
+ mode: "production",
255
+ });
256
+ const compiledBoundaryModules = new Map();
257
+ for (const clientBoundary of allClientBoundaries) {
258
+ compiledBoundaryModules.set(
259
+ clientBoundary,
260
+ await compileProductionClientBoundary(clientBoundary),
261
+ );
262
+ }
263
+ for (const [entryPath, entryInventory] of inventoriesByEntry) {
264
+ clientBoundariesByEntry[toProjectRelative(entryPath)] = entryInventory.clientBoundaries
265
+ .map((clientBoundary) => compiledBoundaryModules.get(clientBoundary))
266
+ .filter(Boolean)
267
+ .sort();
268
+ }
269
+ }
270
+
271
+ const configuredClientBoundaries = evolitConfig.clientBoundaries ?? [];
272
+ if (!Array.isArray(configuredClientBoundaries)) {
273
+ throw new Error("Expected clientBoundaries in evolit.config.js to be an array of module specifiers.");
274
+ }
275
+ for (const specifier of configuredClientBoundaries) {
276
+ if (typeof specifier !== "string" || specifier.length === 0) {
277
+ throw new Error("Expected every clientBoundaries entry to be a non-empty module specifier.");
278
+ }
279
+ const sourcePath = path.isAbsolute(specifier)
280
+ ? specifier
281
+ : await resolveProjectModuleSpecifier(
212
282
  projectRoot,
213
- mode: "production",
214
- sourceMaps: true,
215
- target: "client",
216
- });
217
- entryClientModules.add(
218
- path.relative(boundaryClientBuild.outputRoot, boundaryClientBuild.entrypoint)
219
- .split(path.sep)
220
- .join("/"),
283
+ path.join(projectRoot, "evolit.config.js"),
284
+ specifier,
221
285
  );
286
+ if (!sourcePath) {
287
+ throw new Error(`Unable to resolve configured client boundary ${JSON.stringify(specifier)}.`);
222
288
  }
289
+ await compileProductionClientBoundary(sourcePath);
223
290
  }
224
291
 
225
292
  const clientAssets = await emitBundledClientAssets(projectRoot, {
226
293
  entryClientModules,
294
+ serverAssetImportsByEntry,
295
+ clientBoundariesByEntry,
227
296
  });
228
297
  const staticAssetPublicUrls = createStaticAssetPublicUrlMap(clientAssets);
229
298
  await rewriteServerAssetPlaceholders(projectRoot, clientAssets);
@@ -241,27 +310,93 @@ export async function buildProject(projectRoot) {
241
310
  );
242
311
  const ssrAdapter = createSsrAdapter({
243
312
  assetResolver,
244
- async resolveAdditionalHead({ result }) {
245
- const clientImports = Array.isArray(result.clientImports) ? result.clientImports : [];
246
- const urls = collectTransitiveAssetPreloads(clientImports, clientAssets);
247
- const styleUrls = collectTransitiveStyleUrls(clientImports, clientAssets);
313
+ async onSsrResult({ routeResult, result }) {
314
+ const renderedModules = [...new Set([
315
+ ...(Array.isArray(result.clientImports) ? result.clientImports : []),
316
+ ...(Array.isArray(result.hydrationData?.roots)
317
+ ? result.hydrationData.roots.map((root) => root?.moduleId)
318
+ : []),
319
+ ].filter((moduleId) => typeof moduleId === "string" && moduleId.length > 0))];
320
+ const unresolved = [];
321
+ for (const moduleId of renderedModules) {
322
+ if (assetResolver(moduleId) || clientAssets.byPublicPath?.[moduleId]) continue;
323
+ const importerPath = routeResult.boundaryModule
324
+ ?? routeResult.route?.page
325
+ ?? path.join(projectRoot, "app", "page.litsx");
326
+ let sourcePath = null;
327
+ if (moduleId.startsWith("file:")) {
328
+ try { sourcePath = fileURLToPath(moduleId); } catch {}
329
+ } else if (path.isAbsolute(moduleId) && !moduleId.startsWith("/app/") && !moduleId.startsWith("/src/")) {
330
+ sourcePath = moduleId;
331
+ } else if (moduleId.startsWith("/")) {
332
+ sourcePath = await resolveProjectModuleSpecifier(projectRoot, importerPath, `.${moduleId}`);
333
+ } else {
334
+ sourcePath = await resolveProjectModuleSpecifier(projectRoot, importerPath, moduleId);
335
+ }
336
+ const publicUrl = sourcePath ? assetResolver(sourcePath) : null;
337
+ if (!publicUrl) {
338
+ unresolved.push(moduleId);
339
+ continue;
340
+ }
341
+ for (const root of result.hydrationData?.roots ?? []) {
342
+ if (root?.moduleId === moduleId) root.moduleId = sourcePath;
343
+ }
344
+ if (Array.isArray(result.clientImports)) {
345
+ result.clientImports = result.clientImports.map((value) => value === moduleId ? publicUrl : value);
346
+ }
347
+ if (Array.isArray(result.hydrationData?.clientImports)) {
348
+ result.hydrationData.clientImports = result.hydrationData.clientImports
349
+ .map((value) => value === moduleId ? publicUrl : value);
350
+ }
351
+ }
352
+ if (unresolved.length > 0) {
353
+ throw new Error(
354
+ `SSR rendered client boundaries without build artifacts for ${routeResult.route?.pathname ?? "route"}: `
355
+ + `${unresolved.join(", ")}.`,
356
+ );
357
+ }
358
+ },
359
+ async resolveAdditionalHead({ routeResult, result }) {
360
+ const clientImports = [...new Set([
361
+ ...(Array.isArray(result.clientImports) ? result.clientImports : []),
362
+ ...resolveHydrationRootClientImports(result.hydrationData, assetResolver),
363
+ ])];
364
+ const hydratedClientImports = clientImports;
365
+ const urls = hydratedClientImports.length > 0
366
+ ? [...new Set([
367
+ ...hydratedClientImports,
368
+ ...collectTransitiveAssetPreloads(hydratedClientImports, clientAssets),
369
+ ])]
370
+ : [];
371
+ const styleUrls = [...new Set([
372
+ ...collectTransitiveStyleUrls(clientImports, clientAssets),
373
+ ...resolveServerStyleUrls(routeResult, projectRoot, clientAssets),
374
+ ])];
248
375
 
249
376
  return [
250
- ...urls.map((href) => `<link rel="modulepreload" href="${href}">`),
251
- ...styleUrls.map((href) => `<link rel="stylesheet" href="${href}">`),
377
+ ...urls.map((href) => `<link rel="modulepreload" href="${href}" data-evolit-route-asset="preload">`),
378
+ ...styleUrls.map((href) => `<link rel="stylesheet" href="${href}" data-evolit-route-asset="style">`),
252
379
  ].join("\n");
253
380
  },
254
- resolveBootstrap({ result }) {
381
+ resolveBootstrap({ routeResult, result }) {
255
382
  return createHydrationBootstrap({
256
- hydrationData: normalizeHydrationDataForClient(result.hydrationData, projectRoot),
383
+ hydrationData: normalizeHydrationDataForClient(
384
+ result.hydrationData,
385
+ projectRoot,
386
+ resolveHydrationRootClientImports(result.hydrationData, assetResolver),
387
+ ),
257
388
  assetResolver,
258
389
  hydrationModuleUrl,
259
390
  });
260
391
  },
261
- transformDocument({ result, document }) {
392
+ transformDocument({ routeResult, result, document }) {
262
393
  return rewriteHydrationDataScript(
263
394
  document,
264
- normalizeHydrationDataForClient(result.hydrationData, projectRoot),
395
+ normalizeHydrationDataForClient(
396
+ result.hydrationData,
397
+ projectRoot,
398
+ resolveHydrationRootClientImports(result.hydrationData, assetResolver),
399
+ ),
265
400
  );
266
401
  },
267
402
  });
@@ -309,12 +444,23 @@ export async function buildProject(projectRoot) {
309
444
  seenPrerenderTargets.add(targetPathname);
310
445
 
311
446
  const targetRequest = new Request(`http://evolit.local${targetPathname}`);
312
- const routeResult = await routeResolver.resolveRequest(targetRequest);
313
- if (routeResult.type !== "route" || routeResult.cachePolicy.mode === "dynamic") {
447
+ const { routeResult, response } = await runWithOptionalSsrUrqlScope(async (urqlAdapter) => {
448
+ const resolvedRouteResult = await routeResolver.resolveRequest(targetRequest);
449
+ if (resolvedRouteResult.type !== "route" || resolvedRouteResult.cachePolicy.mode === "dynamic") {
450
+ return { routeResult: resolvedRouteResult, response: null };
451
+ }
452
+
453
+ const renderedResponse = await renderRouteTreeWithAdapter(resolvedRouteResult, ssrAdapter);
454
+ return {
455
+ routeResult: resolvedRouteResult,
456
+ response: urqlAdapter
457
+ ? appendSsrUrqlData(renderedResponse, await urqlAdapter.getUrqlSsrData())
458
+ : renderedResponse,
459
+ };
460
+ });
461
+ if (!response) {
314
462
  continue;
315
463
  }
316
-
317
- const response = await renderRouteTreeWithAdapter(routeResult, ssrAdapter);
318
464
  if (response.status !== 200) {
319
465
  continue;
320
466
  }
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
  });