evolit 0.1.0-alpha.8 → 0.1.0
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 +193 -3
- package/package.json +31 -8
- package/src/build.js +211 -45
- package/src/cli.js +16 -9
- package/src/client-assets.js +479 -97
- package/src/compiler.js +928 -17
- package/src/config.js +25 -0
- 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 +9 -0
- 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 +415 -25
- package/src/request-context-browser.js +1 -0
- package/src/request-context.js +18 -2
- package/src/route-config.js +3 -1
- package/src/route-segments.js +276 -0
- package/src/server-api.js +3 -0
- 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/LICENSE +0 -201
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
|
|
111
|
-
headers, cookies, or `requestUrl()` makes
|
|
112
|
-
|
|
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
|
|
@@ -141,6 +239,71 @@ headers. `redirect()` and `permanentRedirect()` end rendering with `307` and `30
|
|
|
141
239
|
`notFound()` renders a `404`. Reading headers, cookies, or the request URL makes the completed
|
|
142
240
|
render dynamic, so it is not stored by the route response cache.
|
|
143
241
|
|
|
242
|
+
## Extensions
|
|
243
|
+
|
|
244
|
+
Optional integrations are configured explicitly in `evolit.config.js`. Core only coordinates their
|
|
245
|
+
request and browser-navigation lifecycles: it has no knowledge of tenants, locales, catalogues, or
|
|
246
|
+
message formats. Plugins run in declaration order. Each request hook receives the URL produced by
|
|
247
|
+
the preceding hook; a `rewrite` continues the chain, while a `redirect` or `Response` stops it.
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// evolit.config.js
|
|
251
|
+
import { defineEvolitPlugin } from "evolit/extensions";
|
|
252
|
+
|
|
253
|
+
export default {
|
|
254
|
+
plugins: [defineEvolitPlugin({
|
|
255
|
+
name: "tenant-prefix",
|
|
256
|
+
onRequest({ pathname, headers, set }) {
|
|
257
|
+
const tenant = headers().get("x-tenant") ?? "public";
|
|
258
|
+
set("tenant", tenant);
|
|
259
|
+
if (pathname === "/shop") return { rewrite: `/tenants/${tenant}/shop` };
|
|
260
|
+
},
|
|
261
|
+
client: {
|
|
262
|
+
module: "@example/evolit-tenant/client",
|
|
263
|
+
options: { prefix: "/tenants" },
|
|
264
|
+
},
|
|
265
|
+
})],
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`onRequest` is server-only. It can read the `Request`, headers, cookies and URL, set JSON-serializable
|
|
270
|
+
request values, rewrite internally, or return `{ redirect, status? }` / a Web `Response`. Reading
|
|
271
|
+
request-bound data marks the request dynamic. Server components and handlers read the values without
|
|
272
|
+
prop drilling:
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
import { getRequestContext } from "evolit/server";
|
|
276
|
+
|
|
277
|
+
export default async function TenantPage() {
|
|
278
|
+
const { tenant } = getRequestContext();
|
|
279
|
+
return `<main>Tenant: ${tenant}</main>`;
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The `client.module` is a bare package specifier and is the only extension code included in the
|
|
284
|
+
browser build. It must export a named `navigation` object (or `default`) with optional hooks:
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
// @example/evolit-tenant/client
|
|
288
|
+
export const navigation = {
|
|
289
|
+
// Synchronous and idempotent: it may run for generated links and intercepted links.
|
|
290
|
+
transformUrl({ url, options }) {
|
|
291
|
+
return url.startsWith(options.prefix) ? url : `${options.prefix}${url}`;
|
|
292
|
+
},
|
|
293
|
+
async beforeNavigate({ url }) {
|
|
294
|
+
if (url.endsWith("/blocked")) return false;
|
|
295
|
+
},
|
|
296
|
+
afterNavigate({ from, url }) {
|
|
297
|
+
// Browser-only analytics or state synchronization.
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`transformUrl` runs for `createHref()` and before a SPA navigation; `beforeNavigate` can transform
|
|
303
|
+
or cancel a navigation, and `afterNavigate` runs after its delta is applied. Hooks are sequential
|
|
304
|
+
and later hooks receive the URL returned by earlier hooks. Browser modules must not import server
|
|
305
|
+
code. Request values are isolated with async request context and are discarded when rendering ends.
|
|
306
|
+
|
|
144
307
|
## Route Boundaries
|
|
145
308
|
|
|
146
309
|
`not-found.litsx` and `error.litsx` are resolved from the current route directory up to `app/`.
|
|
@@ -231,6 +394,33 @@ export default {
|
|
|
231
394
|
};
|
|
232
395
|
```
|
|
233
396
|
|
|
397
|
+
For monorepos, development watches only the application project by default. Additional source
|
|
398
|
+
trees can be opted into explicitly; generated output and dependency directories below every root
|
|
399
|
+
remain ignored:
|
|
400
|
+
|
|
401
|
+
```js
|
|
402
|
+
export default {
|
|
403
|
+
development: {
|
|
404
|
+
managedSourceRoots: ["../packages/design-system/src"],
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Imports outside these roots remain unmanaged and produce the existing development warning.
|
|
410
|
+
|
|
411
|
+
Evolit normally discovers browser boundaries from the static module graph and reconciles that
|
|
412
|
+
inventory with the components actually emitted by SSR. A genuinely computed import cannot be
|
|
413
|
+
enumerated at build time, so production applications can declare only those exceptional modules:
|
|
414
|
+
|
|
415
|
+
```js
|
|
416
|
+
export default {
|
|
417
|
+
clientBoundaries: ["./src/components/dynamic-card.litsx", "@acme/ui/product-card"],
|
|
418
|
+
};
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
These modules are materialized as build artifacts, but are not automatically imported or
|
|
422
|
+
preloaded. They reach the browser only when an SSR hydration root references them.
|
|
423
|
+
|
|
234
424
|
The framework also exports `ObjectStorageResponseCacheStore`, which is intended for object-store
|
|
235
425
|
backends such as S3. A concrete app can wire it to the AWS SDK without pulling AWS dependencies
|
|
236
426
|
into `evolit` itself.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolit",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
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,34 @@
|
|
|
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
|
+
"./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
|
},
|
|
@@ -47,10 +59,11 @@
|
|
|
47
59
|
},
|
|
48
60
|
"dependencies": {
|
|
49
61
|
"@jridgewell/remapping": "^2.3.5",
|
|
50
|
-
"@litsx/compiler": "0.
|
|
51
|
-
"@litsx/core": "0.17.0-canary-feat-ssr-
|
|
52
|
-
"@litsx/ssr": "0.2.0-canary-feat-ssr-
|
|
62
|
+
"@litsx/compiler": "0.10.0-canary-feat-ssr-20260802205539",
|
|
63
|
+
"@litsx/core": "0.17.0-canary-feat-ssr-20260802205539",
|
|
64
|
+
"@litsx/ssr": "0.2.0-canary-feat-ssr-20260802205539",
|
|
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
|
+
}
|
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,13 +16,22 @@ import {
|
|
|
15
16
|
emitBundledClientAssets,
|
|
16
17
|
emitHashedClientAssets,
|
|
17
18
|
normalizeHydrationDataForClient,
|
|
18
|
-
|
|
19
|
+
resolveServerStyleUrls,
|
|
20
|
+
resolveHydrationRootClientImports,
|
|
19
21
|
resolveSharedVendorModuleUrl,
|
|
20
22
|
rewriteHydrationDataScript,
|
|
21
23
|
rewriteServerAssetPlaceholders,
|
|
22
24
|
} from "./client-assets.js";
|
|
23
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
collectClientGraphInventory,
|
|
27
|
+
compileModuleGraph,
|
|
28
|
+
emitClientStaticAssets,
|
|
29
|
+
getClientStaticAssetModule,
|
|
30
|
+
importCompiledModule,
|
|
31
|
+
resolveProjectModuleSpecifier,
|
|
32
|
+
} from "./compiler.js";
|
|
24
33
|
import { loadEvolitConfig } from "./config.js";
|
|
34
|
+
import { getExtensionClientDescriptors, resolveEvolitExtensions } from "./extensions.js";
|
|
25
35
|
import {
|
|
26
36
|
BUILD_DIRECTORY,
|
|
27
37
|
DEPLOY_ASSETS_MANIFEST_FILENAME,
|
|
@@ -39,6 +49,7 @@ import {
|
|
|
39
49
|
import { serializeRouteCachePolicy } from "./route-config.js";
|
|
40
50
|
import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
|
|
41
51
|
import { ensureDirectory, writeJson } from "./fs-utils.js";
|
|
52
|
+
import { appendSsrUrqlData, runWithOptionalSsrUrqlScope } from "./urql-ssr.js";
|
|
42
53
|
|
|
43
54
|
const CONTENT_TYPE_BY_EXTENSION = new Map([
|
|
44
55
|
[".css", "text/css; charset=utf-8"],
|
|
@@ -119,11 +130,46 @@ async function writeDeploymentRuntimeEntry(buildRoot) {
|
|
|
119
130
|
|
|
120
131
|
export async function buildProject(projectRoot) {
|
|
121
132
|
const evolitConfig = await loadEvolitConfig(projectRoot);
|
|
133
|
+
const extensions = resolveEvolitExtensions(evolitConfig);
|
|
134
|
+
const extensionClientDescriptors = getExtensionClientDescriptors(extensions);
|
|
135
|
+
const sharedVendorOptions = {
|
|
136
|
+
additionalEntrySpecifiers: extensionClientDescriptors.map((descriptor) => descriptor.module),
|
|
137
|
+
};
|
|
122
138
|
const routes = await discoverAppRoutes(projectRoot);
|
|
123
139
|
const routeHandlers = await discoverAppRouteHandlers(projectRoot);
|
|
124
140
|
const buildRoot = path.join(projectRoot, INTERNAL_DIRECTORY, BUILD_DIRECTORY);
|
|
125
141
|
const entryClientModules = new Set();
|
|
142
|
+
const serverAssetImportsByEntry = {};
|
|
143
|
+
const clientBoundariesByEntry = {};
|
|
126
144
|
const deployHandlers = [];
|
|
145
|
+
const compiledClientBoundaries = new Map();
|
|
146
|
+
const inventoriesBySourceEntry = new Map();
|
|
147
|
+
|
|
148
|
+
function getEntryInventory(entryPath) {
|
|
149
|
+
let inventory = inventoriesBySourceEntry.get(entryPath);
|
|
150
|
+
if (!inventory) {
|
|
151
|
+
inventory = collectClientGraphInventory([entryPath], { projectRoot });
|
|
152
|
+
inventoriesBySourceEntry.set(entryPath, inventory);
|
|
153
|
+
}
|
|
154
|
+
return inventory;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function compileProductionClientBoundary(sourcePath) {
|
|
158
|
+
let clientModule = compiledClientBoundaries.get(sourcePath);
|
|
159
|
+
if (clientModule) return clientModule;
|
|
160
|
+
const clientBuild = await compileModuleGraph(sourcePath, {
|
|
161
|
+
projectRoot,
|
|
162
|
+
mode: "production",
|
|
163
|
+
sourceMaps: true,
|
|
164
|
+
target: "client",
|
|
165
|
+
});
|
|
166
|
+
clientModule = path.relative(clientBuild.outputRoot, clientBuild.entrypoint)
|
|
167
|
+
.split(path.sep)
|
|
168
|
+
.join("/");
|
|
169
|
+
compiledClientBoundaries.set(sourcePath, clientModule);
|
|
170
|
+
entryClientModules.add(clientModule);
|
|
171
|
+
return clientModule;
|
|
172
|
+
}
|
|
127
173
|
|
|
128
174
|
await ensureDirectory(buildRoot);
|
|
129
175
|
|
|
@@ -166,16 +212,6 @@ export async function buildProject(projectRoot) {
|
|
|
166
212
|
target: "server",
|
|
167
213
|
});
|
|
168
214
|
|
|
169
|
-
const pageClientBuild = await compileModuleGraph(route.page, {
|
|
170
|
-
projectRoot,
|
|
171
|
-
mode: "production",
|
|
172
|
-
sourceMaps: true,
|
|
173
|
-
target: "client",
|
|
174
|
-
});
|
|
175
|
-
entryClientModules.add(
|
|
176
|
-
path.relative(pageClientBuild.outputRoot, pageClientBuild.entrypoint).split(path.sep).join("/"),
|
|
177
|
-
);
|
|
178
|
-
|
|
179
215
|
for (const layoutPath of route.layouts) {
|
|
180
216
|
await compileModuleGraph(layoutPath, {
|
|
181
217
|
projectRoot,
|
|
@@ -185,15 +221,6 @@ export async function buildProject(projectRoot) {
|
|
|
185
221
|
target: "server",
|
|
186
222
|
});
|
|
187
223
|
|
|
188
|
-
const layoutClientBuild = await compileModuleGraph(layoutPath, {
|
|
189
|
-
projectRoot,
|
|
190
|
-
mode: "production",
|
|
191
|
-
sourceMaps: true,
|
|
192
|
-
target: "client",
|
|
193
|
-
});
|
|
194
|
-
entryClientModules.add(
|
|
195
|
-
path.relative(layoutClientBuild.outputRoot, layoutClientBuild.entrypoint).split(path.sep).join("/"),
|
|
196
|
-
);
|
|
197
224
|
}
|
|
198
225
|
|
|
199
226
|
const boundaryModules = [
|
|
@@ -208,23 +235,70 @@ export async function buildProject(projectRoot) {
|
|
|
208
235
|
ssr: true,
|
|
209
236
|
target: "server",
|
|
210
237
|
});
|
|
238
|
+
}
|
|
211
239
|
|
|
212
|
-
|
|
240
|
+
const toProjectRelative = (filePath) => path.relative(projectRoot, filePath).split(path.sep).join("/");
|
|
241
|
+
const segmentEntries = [...new Set([route.page, ...route.layouts, ...boundaryModules])];
|
|
242
|
+
const inventoriesByEntry = new Map(await Promise.all(segmentEntries.map(async (entryPath) => [
|
|
243
|
+
entryPath,
|
|
244
|
+
await getEntryInventory(entryPath),
|
|
245
|
+
])));
|
|
246
|
+
const allStyles = new Set();
|
|
247
|
+
const allAssets = new Set();
|
|
248
|
+
const allClientBoundaries = new Set();
|
|
249
|
+
for (const [entryPath, entryInventory] of inventoriesByEntry) {
|
|
250
|
+
serverAssetImportsByEntry[toProjectRelative(entryPath)] = {
|
|
251
|
+
styles: entryInventory.styles.map((filePath) => getClientStaticAssetModule(projectRoot, filePath)),
|
|
252
|
+
assets: entryInventory.assets.map((filePath) => getClientStaticAssetModule(projectRoot, filePath)),
|
|
253
|
+
};
|
|
254
|
+
entryInventory.styles.forEach((filePath) => allStyles.add(filePath));
|
|
255
|
+
entryInventory.assets.forEach((filePath) => allAssets.add(filePath));
|
|
256
|
+
entryInventory.clientBoundaries.forEach((filePath) => allClientBoundaries.add(filePath));
|
|
257
|
+
}
|
|
258
|
+
await emitClientStaticAssets([...allStyles, ...allAssets], {
|
|
259
|
+
projectRoot,
|
|
260
|
+
mode: "production",
|
|
261
|
+
});
|
|
262
|
+
const compiledBoundaryModules = new Map();
|
|
263
|
+
for (const clientBoundary of allClientBoundaries) {
|
|
264
|
+
compiledBoundaryModules.set(
|
|
265
|
+
clientBoundary,
|
|
266
|
+
await compileProductionClientBoundary(clientBoundary),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
for (const [entryPath, entryInventory] of inventoriesByEntry) {
|
|
270
|
+
clientBoundariesByEntry[toProjectRelative(entryPath)] = entryInventory.clientBoundaries
|
|
271
|
+
.map((clientBoundary) => compiledBoundaryModules.get(clientBoundary))
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
.sort();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const configuredClientBoundaries = evolitConfig.clientBoundaries ?? [];
|
|
278
|
+
if (!Array.isArray(configuredClientBoundaries)) {
|
|
279
|
+
throw new Error("Expected clientBoundaries in evolit.config.js to be an array of module specifiers.");
|
|
280
|
+
}
|
|
281
|
+
for (const specifier of configuredClientBoundaries) {
|
|
282
|
+
if (typeof specifier !== "string" || specifier.length === 0) {
|
|
283
|
+
throw new Error("Expected every clientBoundaries entry to be a non-empty module specifier.");
|
|
284
|
+
}
|
|
285
|
+
const sourcePath = path.isAbsolute(specifier)
|
|
286
|
+
? specifier
|
|
287
|
+
: await resolveProjectModuleSpecifier(
|
|
213
288
|
projectRoot,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
target: "client",
|
|
217
|
-
});
|
|
218
|
-
entryClientModules.add(
|
|
219
|
-
path.relative(boundaryClientBuild.outputRoot, boundaryClientBuild.entrypoint)
|
|
220
|
-
.split(path.sep)
|
|
221
|
-
.join("/"),
|
|
289
|
+
path.join(projectRoot, "evolit.config.js"),
|
|
290
|
+
specifier,
|
|
222
291
|
);
|
|
292
|
+
if (!sourcePath) {
|
|
293
|
+
throw new Error(`Unable to resolve configured client boundary ${JSON.stringify(specifier)}.`);
|
|
223
294
|
}
|
|
295
|
+
await compileProductionClientBoundary(sourcePath);
|
|
224
296
|
}
|
|
225
297
|
|
|
226
298
|
const clientAssets = await emitBundledClientAssets(projectRoot, {
|
|
227
299
|
entryClientModules,
|
|
300
|
+
serverAssetImportsByEntry,
|
|
301
|
+
clientBoundariesByEntry,
|
|
228
302
|
});
|
|
229
303
|
const staticAssetPublicUrls = createStaticAssetPublicUrlMap(clientAssets);
|
|
230
304
|
await rewriteServerAssetPlaceholders(projectRoot, clientAssets);
|
|
@@ -239,33 +313,114 @@ export async function buildProject(projectRoot) {
|
|
|
239
313
|
projectRoot,
|
|
240
314
|
"production",
|
|
241
315
|
"@litsx/ssr/hydration",
|
|
316
|
+
sharedVendorOptions,
|
|
242
317
|
);
|
|
318
|
+
const navigationModuleUrl = await resolveSharedVendorModuleUrl(
|
|
319
|
+
projectRoot,
|
|
320
|
+
"production",
|
|
321
|
+
"evolit/navigation",
|
|
322
|
+
sharedVendorOptions,
|
|
323
|
+
);
|
|
324
|
+
const navigationExtensions = await Promise.all(extensionClientDescriptors.map(async (descriptor) => ({
|
|
325
|
+
...descriptor,
|
|
326
|
+
module: await resolveSharedVendorModuleUrl(
|
|
327
|
+
projectRoot,
|
|
328
|
+
"production",
|
|
329
|
+
descriptor.module,
|
|
330
|
+
sharedVendorOptions,
|
|
331
|
+
),
|
|
332
|
+
})));
|
|
243
333
|
const ssrAdapter = createSsrAdapter({
|
|
244
334
|
assetResolver,
|
|
335
|
+
async onSsrResult({ routeResult, result }) {
|
|
336
|
+
const renderedModules = [...new Set([
|
|
337
|
+
...(Array.isArray(result.clientImports) ? result.clientImports : []),
|
|
338
|
+
...(Array.isArray(result.hydrationData?.roots)
|
|
339
|
+
? result.hydrationData.roots.map((root) => root?.moduleId)
|
|
340
|
+
: []),
|
|
341
|
+
].filter((moduleId) => typeof moduleId === "string" && moduleId.length > 0))];
|
|
342
|
+
const unresolved = [];
|
|
343
|
+
for (const moduleId of renderedModules) {
|
|
344
|
+
if (assetResolver(moduleId) || clientAssets.byPublicPath?.[moduleId]) continue;
|
|
345
|
+
const importerPath = routeResult.boundaryModule
|
|
346
|
+
?? routeResult.route?.page
|
|
347
|
+
?? path.join(projectRoot, "app", "page.litsx");
|
|
348
|
+
let sourcePath = null;
|
|
349
|
+
if (moduleId.startsWith("file:")) {
|
|
350
|
+
try { sourcePath = fileURLToPath(moduleId); } catch {}
|
|
351
|
+
} else if (path.isAbsolute(moduleId) && !moduleId.startsWith("/app/") && !moduleId.startsWith("/src/")) {
|
|
352
|
+
sourcePath = moduleId;
|
|
353
|
+
} else if (moduleId.startsWith("/")) {
|
|
354
|
+
sourcePath = await resolveProjectModuleSpecifier(projectRoot, importerPath, `.${moduleId}`);
|
|
355
|
+
} else {
|
|
356
|
+
sourcePath = await resolveProjectModuleSpecifier(projectRoot, importerPath, moduleId);
|
|
357
|
+
}
|
|
358
|
+
const publicUrl = sourcePath ? assetResolver(sourcePath) : null;
|
|
359
|
+
if (!publicUrl) {
|
|
360
|
+
unresolved.push(moduleId);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
for (const root of result.hydrationData?.roots ?? []) {
|
|
364
|
+
if (root?.moduleId === moduleId) root.moduleId = sourcePath;
|
|
365
|
+
}
|
|
366
|
+
if (Array.isArray(result.clientImports)) {
|
|
367
|
+
result.clientImports = result.clientImports.map((value) => value === moduleId ? publicUrl : value);
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(result.hydrationData?.clientImports)) {
|
|
370
|
+
result.hydrationData.clientImports = result.hydrationData.clientImports
|
|
371
|
+
.map((value) => value === moduleId ? publicUrl : value);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (unresolved.length > 0) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`SSR rendered client boundaries without build artifacts for ${routeResult.route?.pathname ?? "route"}: `
|
|
377
|
+
+ `${unresolved.join(", ")}.`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
},
|
|
245
381
|
async resolveAdditionalHead({ routeResult, result }) {
|
|
246
|
-
const clientImports = [
|
|
382
|
+
const clientImports = [...new Set([
|
|
247
383
|
...(Array.isArray(result.clientImports) ? result.clientImports : []),
|
|
248
|
-
...
|
|
249
|
-
];
|
|
250
|
-
const
|
|
251
|
-
const
|
|
384
|
+
...resolveHydrationRootClientImports(result.hydrationData, assetResolver),
|
|
385
|
+
])];
|
|
386
|
+
const hydratedClientImports = clientImports;
|
|
387
|
+
const urls = hydratedClientImports.length > 0
|
|
388
|
+
? [...new Set([
|
|
389
|
+
...hydratedClientImports,
|
|
390
|
+
...collectTransitiveAssetPreloads(hydratedClientImports, clientAssets),
|
|
391
|
+
])]
|
|
392
|
+
: [];
|
|
393
|
+
const styleUrls = [...new Set([
|
|
394
|
+
...collectTransitiveStyleUrls(clientImports, clientAssets),
|
|
395
|
+
...resolveServerStyleUrls(routeResult, projectRoot, clientAssets),
|
|
396
|
+
])];
|
|
252
397
|
|
|
253
398
|
return [
|
|
254
|
-
...urls.map((href) => `<link rel="modulepreload" href="${href}">`),
|
|
255
|
-
...styleUrls.map((href) => `<link rel="stylesheet" href="${href}">`),
|
|
399
|
+
...urls.map((href) => `<link rel="modulepreload" href="${href}" data-evolit-route-asset="preload">`),
|
|
400
|
+
...styleUrls.map((href) => `<link rel="stylesheet" href="${href}" data-evolit-route-asset="style">`),
|
|
256
401
|
].join("\n");
|
|
257
402
|
},
|
|
258
|
-
resolveBootstrap({ result }) {
|
|
403
|
+
resolveBootstrap({ routeResult, result }) {
|
|
259
404
|
return createHydrationBootstrap({
|
|
260
|
-
hydrationData: normalizeHydrationDataForClient(
|
|
405
|
+
hydrationData: normalizeHydrationDataForClient(
|
|
406
|
+
result.hydrationData,
|
|
407
|
+
projectRoot,
|
|
408
|
+
resolveHydrationRootClientImports(result.hydrationData, assetResolver),
|
|
409
|
+
),
|
|
261
410
|
assetResolver,
|
|
262
411
|
hydrationModuleUrl,
|
|
412
|
+
navigationModuleUrl,
|
|
413
|
+
navigationExtensions,
|
|
263
414
|
});
|
|
264
415
|
},
|
|
265
|
-
transformDocument({ result, document }) {
|
|
416
|
+
transformDocument({ routeResult, result, document }) {
|
|
266
417
|
return rewriteHydrationDataScript(
|
|
267
418
|
document,
|
|
268
|
-
normalizeHydrationDataForClient(
|
|
419
|
+
normalizeHydrationDataForClient(
|
|
420
|
+
result.hydrationData,
|
|
421
|
+
projectRoot,
|
|
422
|
+
resolveHydrationRootClientImports(result.hydrationData, assetResolver),
|
|
423
|
+
),
|
|
269
424
|
);
|
|
270
425
|
},
|
|
271
426
|
});
|
|
@@ -313,12 +468,23 @@ export async function buildProject(projectRoot) {
|
|
|
313
468
|
seenPrerenderTargets.add(targetPathname);
|
|
314
469
|
|
|
315
470
|
const targetRequest = new Request(`http://evolit.local${targetPathname}`);
|
|
316
|
-
const routeResult = await
|
|
317
|
-
|
|
471
|
+
const { routeResult, response } = await runWithOptionalSsrUrqlScope(async (urqlAdapter) => {
|
|
472
|
+
const resolvedRouteResult = await routeResolver.resolveRequest(targetRequest);
|
|
473
|
+
if (resolvedRouteResult.type !== "route" || resolvedRouteResult.cachePolicy.mode === "dynamic") {
|
|
474
|
+
return { routeResult: resolvedRouteResult, response: null };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const renderedResponse = await renderRouteTreeWithAdapter(resolvedRouteResult, ssrAdapter);
|
|
478
|
+
return {
|
|
479
|
+
routeResult: resolvedRouteResult,
|
|
480
|
+
response: urqlAdapter
|
|
481
|
+
? appendSsrUrqlData(renderedResponse, await urqlAdapter.getUrqlSsrData())
|
|
482
|
+
: renderedResponse,
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
if (!response) {
|
|
318
486
|
continue;
|
|
319
487
|
}
|
|
320
|
-
|
|
321
|
-
const response = await renderRouteTreeWithAdapter(routeResult, ssrAdapter);
|
|
322
488
|
if (response.status !== 200) {
|
|
323
489
|
continue;
|
|
324
490
|
}
|