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 +128 -3
- package/package.json +28 -8
- package/src/build.js +189 -43
- package/src/cli.js +16 -9
- package/src/client-assets.js +487 -93
- package/src/compiler.js +928 -17
- package/src/config.js +25 -0
- package/src/deployment-runtime.js +566 -81
- package/src/development-events.js +101 -0
- package/src/development-hot-client.js +65 -0
- package/src/navigation-client.js +670 -0
- package/src/navigation-server.js +5 -0
- package/src/navigation-url.js +28 -0
- package/src/render.js +363 -19
- package/src/route-config.js +3 -1
- package/src/route-segments.js +276 -0
- package/src/server.js +250 -18
- package/src/ssr-adapter.js +133 -12
- package/src/terminal.js +32 -0
- package/src/urql-ssr.js +60 -0
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
|
|
@@ -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
|
+
"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.
|
|
51
|
-
"@litsx/core": "0.17.0-canary-feat-ssr-
|
|
52
|
-
"@litsx/ssr": "0.2.0-canary-feat-ssr-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
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(
|
|
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(
|
|
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
|
|
313
|
-
|
|
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
|
-
|
|
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
|
});
|