@vidact/start 0.2.0-beta.6 → 0.2.0-beta.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @vidact/start
2
2
 
3
+ ## 0.2.0-beta.8
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [e480658]
8
+ - @vidact/runtime@0.2.0-beta.8
9
+ - @vidact/vite@0.2.0-beta.8
10
+
11
+ ## 0.2.0-beta.7
12
+
13
+ ### Patch Changes
14
+
15
+ - 5d2dd5d: Preserve destination fragments through successful client navigation and redirects.
16
+ - 977b90f: Retain unchanged Start route owners and update their loader, parameter, URL, and child props during client navigation.
17
+ - d35d98f: Stream the Start document shell before pending application resources and support streaming custom document shells.
18
+ - 8ce4ca8: Separate cached document and client-navigation responses with a Vary header.
19
+ - Updated dependencies [ed60a1c]
20
+ - Updated dependencies [977b90f]
21
+ - Updated dependencies [9581420]
22
+ - @vidact/runtime@0.2.0-beta.7
23
+ - @vidact/vite@0.2.0-beta.7
24
+
3
25
  ## 0.2.0-beta.6
4
26
 
5
27
  ### Patch Changes
package/README.md CHANGED
@@ -45,8 +45,8 @@ application. Route components must be named functions so the Vidact compiler
45
45
  can identify and lower them.
46
46
 
47
47
  `Link` renders an ordinary anchor during SSR. After hydration, unmodified clicks
48
- on same-origin Start routes request a server loader snapshot, replace the owned
49
- route root, and update browser history without loading another document.
48
+ on same-origin Start routes request a server loader snapshot, update the retained
49
+ route chain, and update browser history without loading another document.
50
50
  External URLs, downloads, modified clicks, `target` links, hash-only links, and
51
51
  links with `reloadDocument` retain native browser behavior. Use `replace` to
52
52
  replace the current history entry.
@@ -60,6 +60,21 @@ replace the current history entry.
60
60
  `hydrateStart()` returns a `StartClient` whose `navigate()` method provides the
61
61
  same behavior for programmatic navigation.
62
62
 
63
- This first navigation contract replaces the owned route root atomically, so
64
- component-local state in shared layouts resets. Retaining unchanged layout
65
- owners is a follow-up routing contract.
63
+ Document HTML and client-navigation snapshots share the same route URL. Start
64
+ marks both successful representations with `Vary: x-vidact-start-navigation`,
65
+ so HTTP caches that honor `Vary` keep them separate. Deployments must preserve
66
+ that response header when adding their own caching rules.
67
+
68
+ The built-in document streams its opening shell as soon as loaders settle. The
69
+ marker-complete application follows after its async resources settle, then the
70
+ snapshot and client script close the document. `renderDocumentShell` customizes
71
+ the strings before and after the application while preserving this behavior.
72
+ The older `renderDocument` callback still receives one `applicationHtml` string
73
+ and therefore buffers the application before returning a response.
74
+
75
+ Start retains the longest common prefix of component-bearing route entries.
76
+ Those components keep their DOM, local state, refs, and effects while their
77
+ `loaderData`, `params`, `requestUrl`, and `children` props update. The first
78
+ changed route entry and all descendants are staged as a new owned suffix, then
79
+ the previous suffix is disposed. Routes without a component do not establish a
80
+ DOM owner.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;UA0BiB;WACN,eAAe,WAAW;WAC1B,UAAU;;;;;;WAMV,sBAAsB;WACtB,OAAO;WACP;WACA;WACA;;UAGM;WACN;WACA;;UAGM,oBAAoB;WAC1B,WAAW,aAAa,KAAK,UAAU,yBAAyB;;iBAKrD,aAAa,SAAS,sBAAsB,QAAQ"}
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;UAgCiB;WACN,eAAe,WAAW;WAC1B,UAAU;;;;;;WAMV,sBAAsB;WACtB,OAAO;WACP;WACA;WACA;;UAGM;WACN;WACA;;UAGM,oBAAoB;WAC1B,WAAW,aAAa,KAAK,UAAU,yBAAyB;;iBAyBrD,aAAa,SAAS,sBAAsB,QAAQ"}
package/dist/client.js CHANGED
@@ -1,10 +1,11 @@
1
- import { composeRouteMatches, loadRouteMatches, matchRoutes } from "./router.js";
1
+ import { loadRouteMatches, matchRoutes } from "./router.js";
2
2
  import { VIDACT_START_NAVIGATION_HEADER, decodeStartSnapshot } from "./snapshot.js";
3
- import { cloneRenderable, compiledRoot, createCompiledScope } from "@vidact/runtime";
4
- import { createElement, hydrateRoot } from "@vidact/runtime/hydrate";
3
+ import { binding, cloneRenderable, compiledRoot, createCompiledScope, createCompiledState, createComponentRenderable, source } from "@vidact/runtime";
4
+ import { hydrateRoot } from "@vidact/runtime/hydrate";
5
5
  //#region src/client.ts
6
6
  const DEFAULT_ROOT_ID = "vidact-start-root";
7
7
  const DEFAULT_SNAPSHOT_ID = "vidact-start-snapshot";
8
+ const routeComponentIdentities = /* @__PURE__ */ new WeakMap();
8
9
  async function hydrateStart(options) {
9
10
  const snapshot = decodeStartSnapshot(options.snapshot ?? readSnapshot(options.snapshotId ?? DEFAULT_SNAPSHOT_ID));
10
11
  const initialUrl = new URL(snapshot.pathname, window.location.href);
@@ -14,7 +15,8 @@ async function hydrateStart(options) {
14
15
  const loaded = await loadRouteMatches(matches, request, snapshot.loaderData);
15
16
  const host = options.root ?? document.querySelector(`#${CSS.escape(options.rootId ?? DEFAULT_ROOT_ID)}`);
16
17
  if (host === null) throw new Error("Vidact Start hydration root is missing");
17
- const root = hydrateRoot(host, createClientApplication(loaded, request.url), { onRecoverableError: options.onRecoverableError ?? ((error) => console.warn("[vidact/start] hydration recovered by re-rendering:", error)) });
18
+ let application = createClientApplication(loaded, request.url);
19
+ const root = hydrateRoot(host, application.application, { onRecoverableError: options.onRecoverableError ?? ((error) => console.warn("[vidact/start] hydration recovered by re-rendering:", error)) });
18
20
  const fetchNavigation = options.fetch ?? window.fetch.bind(window);
19
21
  let navigation = 0;
20
22
  let navigationController;
@@ -50,6 +52,7 @@ async function hydrateStart(options) {
50
52
  }
51
53
  const nextSnapshot = decodeStartSnapshot(await response.text());
52
54
  const nextUrl = new URL(nextSnapshot.pathname, target.origin);
55
+ if (nextUrl.hash === "") nextUrl.hash = target.hash;
53
56
  const snapshotMatches = matchRoutes(options.manifest, nextUrl.pathname);
54
57
  if (snapshotMatches.length === 0) {
55
58
  navigateDocument(target, historyMode === "replace");
@@ -58,7 +61,12 @@ async function hydrateStart(options) {
58
61
  const nextRequest = new Request(nextUrl);
59
62
  const nextLoaded = await loadRouteMatches(snapshotMatches, nextRequest, nextSnapshot.loaderData);
60
63
  if (attempt !== navigation) return false;
61
- root.replace(createClientApplication(nextLoaded, nextRequest.url));
64
+ const nextState = createClientRouteState(nextLoaded, nextRequest.url);
65
+ if (sharedRouteComponentCount(application.state, nextState) === 0) {
66
+ const replacement = createClientApplication(nextLoaded, nextRequest.url);
67
+ root.replace(replacement.application);
68
+ application = replacement;
69
+ } else application.update(nextState);
62
70
  updateHistory(nextUrl, historyMode);
63
71
  if (scroll) scrollToLocation(nextUrl);
64
72
  return true;
@@ -97,12 +105,99 @@ async function hydrateStart(options) {
97
105
  };
98
106
  }
99
107
  function createClientApplication(matches, requestUrl) {
100
- return () => {
101
- const scope = createCompiledScope();
102
- const application = composeRouteMatches(matches, (component, props) => createElement(component, props), requestUrl);
103
- return compiledRoot(scope, () => cloneRenderable(application));
108
+ let currentState = createClientRouteState(matches, requestUrl);
109
+ let updateState;
110
+ return {
111
+ application: () => {
112
+ const scope = createCompiledScope();
113
+ const routeSource = source(0);
114
+ const state = createCompiledState(scope, routeSource, currentState);
115
+ updateState = (nextState) => {
116
+ const previousState = currentState;
117
+ try {
118
+ state.set(nextState);
119
+ currentState = nextState;
120
+ } catch (error) {
121
+ try {
122
+ state.set(previousState);
123
+ } catch {}
124
+ currentState = previousState;
125
+ throw error;
126
+ }
127
+ };
128
+ const route = createRetainedRouteRenderable(scope, routeSource, state.get, 0);
129
+ return compiledRoot(scope, () => cloneRenderable(route));
130
+ },
131
+ get state() {
132
+ return currentState;
133
+ },
134
+ update(nextState) {
135
+ if (updateState === void 0) throw new Error("cannot update a Vidact Start application before it mounts");
136
+ updateState(nextState);
137
+ }
138
+ };
139
+ }
140
+ function createClientRouteState(matches, requestUrl) {
141
+ return {
142
+ components: matches.flatMap((match) => {
143
+ const component = match.definition.options.component;
144
+ return component === void 0 ? [] : [{
145
+ component,
146
+ entry: match.entry,
147
+ loaderData: match.loaderData,
148
+ params: match.params
149
+ }];
150
+ }),
151
+ requestUrl
104
152
  };
105
153
  }
154
+ function createRetainedRouteRenderable(scope, routeSource, readState, index) {
155
+ const initial = readState().components[index];
156
+ if (initial === void 0) return void 0;
157
+ const identity = routeComponentIdentity(initial.entry, initial.component);
158
+ let lastProps = routeComponentProps(scope, routeSource, readState, index);
159
+ const input = binding(scope, routeSource, () => {
160
+ const current = readState().components[index];
161
+ if (current === void 0 || routeComponentIdentity(current.entry, current.component) !== identity) return lastProps;
162
+ lastProps = routeComponentProps(scope, routeSource, readState, index);
163
+ return lastProps;
164
+ });
165
+ return createComponentRenderable(initial.component, input, identity);
166
+ }
167
+ function routeComponentProps(scope, routeSource, readState, index) {
168
+ const state = readState();
169
+ const match = state.components[index];
170
+ return {
171
+ children: createRetainedRouteRenderable(scope, routeSource, readState, index + 1),
172
+ loaderData: match.loaderData,
173
+ params: match.params,
174
+ requestUrl: state.requestUrl
175
+ };
176
+ }
177
+ function sharedRouteComponentCount(left, right) {
178
+ const length = Math.min(left.components.length, right.components.length);
179
+ let index = 0;
180
+ while (index < length) {
181
+ const previous = left.components[index];
182
+ const next = right.components[index];
183
+ if (previous.entry !== next.entry || previous.component !== next.component) break;
184
+ index += 1;
185
+ }
186
+ return index;
187
+ }
188
+ function routeComponentIdentity(entry, component) {
189
+ let componentIdentities = routeComponentIdentities.get(entry);
190
+ if (componentIdentities === void 0) {
191
+ componentIdentities = /* @__PURE__ */ new WeakMap();
192
+ routeComponentIdentities.set(entry, componentIdentities);
193
+ }
194
+ let identity = componentIdentities.get(component);
195
+ if (identity === void 0) {
196
+ identity = {};
197
+ componentIdentities.set(component, identity);
198
+ }
199
+ return identity;
200
+ }
106
201
  function linkForEvent(event) {
107
202
  if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
108
203
  const target = event.target;
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"mappings":"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"}
@@ -1,8 +1,10 @@
1
1
  import { anchorProps } from "./link-contract.js";
2
+ import { compiledRoot, createNarrowCompiledScope } from "@vidact/runtime";
2
3
  import { createElement } from "@vidact/runtime/hydrate";
3
4
  //#region src/link-client.ts
4
5
  function Link(props) {
5
- return createElement("a", anchorProps(props));
6
+ const scope = createNarrowCompiledScope();
7
+ return compiledRoot(scope, () => createElement("a", anchorProps(props)));
6
8
  }
7
9
  //#endregion
8
10
  export { Link };
@@ -1 +1 @@
1
- {"version":3,"file":"link-client.js","names":[],"sources":["../src/link-client.ts"],"mappings":";;;AAKA,SAAgB,KAAK,OAAuC;CAC1D,OAAO,cAAc,KAAK,YAAY,KAAK,CAAC;AAC9C"}
1
+ {"version":3,"file":"link-client.js","names":[],"sources":["../src/link-client.ts"],"mappings":";;;;AAKA,SAAgB,KAAK,OAAuC;CAC1D,MAAM,QAAQ,0BAA0B;CACxC,OAAO,aAAa,aAAa,cAAc,KAAK,YAAY,KAAK,CAAC,CAAC;AACzE"}
package/dist/server.d.ts CHANGED
@@ -7,15 +7,26 @@ interface StartDocumentContext {
7
7
  readonly snapshot: string;
8
8
  readonly snapshotId: string;
9
9
  }
10
+ interface StartDocumentShellContext {
11
+ readonly clientEntry: string | undefined;
12
+ readonly rootId: string;
13
+ readonly snapshot: string;
14
+ readonly snapshotId: string;
15
+ }
16
+ interface StartDocumentShell {
17
+ readonly afterApplication: string;
18
+ readonly beforeApplication: string;
19
+ }
10
20
  interface StartHandlerOptions {
11
21
  readonly clientEntry?: string;
12
22
  readonly manifest: RouteManifest;
13
23
  readonly notFound?: (request: Request) => Response | Promise<Response>;
14
24
  readonly renderDocument?: (context: StartDocumentContext) => string | Promise<string>;
25
+ readonly renderDocumentShell?: (context: StartDocumentShellContext) => StartDocumentShell | Promise<StartDocumentShell>;
15
26
  readonly rootId?: string;
16
27
  readonly snapshotId?: string;
17
28
  }
18
29
  declare function createStartHandler(options: StartHandlerOptions): (request: Request) => Promise<Response>;
19
30
  //#endregion
20
- export { type StartComponent, StartDocumentContext, StartHandlerOptions, createStartHandler };
31
+ export { type StartComponent, StartDocumentContext, StartDocumentShell, StartDocumentShellContext, StartHandlerOptions, createStartHandler };
21
32
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;UAkCiB;WACN;WACA;WACA;WACA;WACA;;UAGM;WACN;WACA,UAAU;WACV,YAAY,SAAS,YAAY,WAAW,QAAQ;WACpD,kBAAkB,SAAS,kCAAkC;WAC7D;WACA;;iBAGK,mBACd,SAAS,uBACP,SAAS,YAAY,QAAQ"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;UAkCiB;WACN;WACA;WACA;WACA;WACA;;UAGM;WACN;WACA;WACA;WACA;;UAGM;WACN;WACA;;UAGM;WACN;WACA,UAAU;WACV,YAAY,SAAS,YAAY,WAAW,QAAQ;WACpD,kBAAkB,SAAS,kCAAkC;WAC7D,uBACP,SAAS,8BACN,qBAAqB,QAAQ;WACzB;WACA;;iBAGK,mBACd,SAAS,uBACP,SAAS,YAAY,QAAQ"}
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { composeRouteMatches, matchRoutes, resolveRouteMatches, routeLoaderData, runRouteLoaders } from "./router.js";
2
- import { VIDACT_START_PROTOCOL, VIDACT_START_SNAPSHOT_MEDIA_TYPE, encodeStartSnapshot } from "./snapshot.js";
2
+ import { VIDACT_START_NAVIGATION_HEADER, VIDACT_START_PROTOCOL, VIDACT_START_SNAPSHOT_MEDIA_TYPE, encodeStartSnapshot } from "./snapshot.js";
3
3
  import { createElement } from "@vidact/runtime/server";
4
4
  import { renderToReadableStream } from "@vidact/runtime/framework/server";
5
5
  //#region src/server.ts
@@ -15,6 +15,7 @@ function StartApplication({ children }) {
15
15
  const DEFAULT_ROOT_ID = "vidact-start-root";
16
16
  const DEFAULT_SNAPSHOT_ID = "vidact-start-snapshot";
17
17
  function createStartHandler(options) {
18
+ if (options.renderDocument !== void 0 && options.renderDocumentShell !== void 0) throw new TypeError("renderDocument and renderDocumentShell cannot be used together");
18
19
  const rootId = options.rootId ?? DEFAULT_ROOT_ID;
19
20
  const snapshotId = options.snapshotId ?? DEFAULT_SNAPSHOT_ID;
20
21
  return async (request) => {
@@ -48,20 +49,38 @@ function createStartHandler(options) {
48
49
  pathname: `${url.pathname}${url.search}`,
49
50
  loaderData: routeLoaderData(loaded)
50
51
  }));
51
- if (request.headers.get("x-vidact-start-navigation") === "1") return new Response(request.method === "HEAD" ? null : snapshot, { headers: { "content-type": `${VIDACT_START_SNAPSHOT_MEDIA_TYPE}; charset=utf-8` } });
52
+ if (request.headers.get("x-vidact-start-navigation") === "1") return new Response(request.method === "HEAD" ? null : snapshot, { headers: representationHeaders(`${VIDACT_START_SNAPSHOT_MEDIA_TYPE}; charset=utf-8`) });
53
+ const headers = representationHeaders("text/html; charset=utf-8");
54
+ if (request.method === "HEAD") return new Response(null, { headers });
52
55
  const application = composeRouteMatches(loaded, (component, props) => createElement(component, props), request.url);
53
- const stream = await renderToReadableStream(() => createElement(StartApplication, { children: application }), { identifierPrefix: "start-" });
54
- const documentContext = {
55
- applicationHtml: await new Response(stream).text(),
56
+ const stream = await renderToReadableStream(() => createElement(StartApplication, { children: application }), {
57
+ identifierPrefix: "start-",
58
+ signal: request.signal
59
+ });
60
+ stream.allReady.catch(() => void 0);
61
+ const shellContext = {
56
62
  clientEntry: options.clientEntry,
57
63
  rootId,
58
64
  snapshot,
59
65
  snapshotId
60
66
  };
61
- const html = options.renderDocument === void 0 ? defaultDocument(documentContext) : await options.renderDocument(documentContext);
62
- return new Response(request.method === "HEAD" ? null : html, { headers: { "content-type": "text/html; charset=utf-8" } });
67
+ if (options.renderDocument !== void 0) {
68
+ const applicationHtml = await new Response(stream).text();
69
+ const html = await options.renderDocument({
70
+ ...shellContext,
71
+ applicationHtml
72
+ });
73
+ return new Response(html, { headers });
74
+ }
75
+ const shell = options.renderDocumentShell === void 0 ? defaultDocumentShell(shellContext) : validateDocumentShell(await options.renderDocumentShell(shellContext));
76
+ return new Response(streamDocument(shell, stream), { headers });
63
77
  };
64
78
  }
79
+ function representationHeaders(contentType) {
80
+ const headers = new Headers({ "content-type": contentType });
81
+ headers.append("vary", VIDACT_START_NAVIGATION_HEADER);
82
+ return headers;
83
+ }
65
84
  function responseForRequest(request, response) {
66
85
  return request.method === "HEAD" ? new Response(null, response) : response;
67
86
  }
@@ -93,9 +112,53 @@ function allowedMethods(matches, canRender) {
93
112
  function methodRank(method) {
94
113
  return method === "GET" ? 0 : method === "HEAD" ? 1 : 2;
95
114
  }
96
- function defaultDocument(context) {
115
+ function defaultDocumentShell(context) {
97
116
  const clientScript = context.clientEntry === void 0 ? "" : `<script type="module" src="${escapeAttribute(context.clientEntry)}"><\/script>`;
98
- return `<!doctype html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head><body><div id="${escapeAttribute(context.rootId)}">${context.applicationHtml}</div><script id="${escapeAttribute(context.snapshotId)}" type="application/json">${context.snapshot}<\/script>${clientScript}</body></html>`;
117
+ return {
118
+ beforeApplication: `<!doctype html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head><body><div id="${escapeAttribute(context.rootId)}">`,
119
+ afterApplication: `</div><script id="${escapeAttribute(context.snapshotId)}" type="application/json">${context.snapshot}<\/script>${clientScript}</body></html>`
120
+ };
121
+ }
122
+ function validateDocumentShell(value) {
123
+ if (typeof value !== "object" || value === null || typeof value.beforeApplication !== "string" || typeof value.afterApplication !== "string") throw new TypeError("renderDocumentShell must return beforeApplication and afterApplication");
124
+ return value;
125
+ }
126
+ function streamDocument(shell, application) {
127
+ const encoder = new TextEncoder();
128
+ const before = encoder.encode(shell.beforeApplication);
129
+ const after = encoder.encode(shell.afterApplication);
130
+ const reader = application.getReader();
131
+ let phase = "before";
132
+ return new ReadableStream({
133
+ async pull(controller) {
134
+ try {
135
+ if (phase === "before") {
136
+ phase = "application";
137
+ if (before.length > 0) controller.enqueue(before);
138
+ return;
139
+ }
140
+ if (phase === "application") {
141
+ const result = await reader.read();
142
+ if (!result.done) {
143
+ controller.enqueue(result.value);
144
+ return;
145
+ }
146
+ phase = "after";
147
+ }
148
+ if (phase === "after") {
149
+ phase = "done";
150
+ if (after.length > 0) controller.enqueue(after);
151
+ return;
152
+ }
153
+ controller.close();
154
+ } catch (error) {
155
+ controller.error(error);
156
+ }
157
+ },
158
+ cancel(reason) {
159
+ return reader.cancel(reason);
160
+ }
161
+ });
99
162
  }
100
163
  function escapeAttribute(value) {
101
164
  return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidact/start",
3
- "version": "0.2.0-beta.6",
3
+ "version": "0.2.0-beta.8",
4
4
  "description": "Full-stack application framework primitives for Vidact",
5
5
  "keywords": [
6
6
  "full-stack",
@@ -53,8 +53,8 @@
53
53
  "access": "public"
54
54
  },
55
55
  "dependencies": {
56
- "@vidact/runtime": "0.2.0-beta.6",
57
- "@vidact/vite": "0.2.0-beta.6"
56
+ "@vidact/vite": "0.2.0-beta.8",
57
+ "@vidact/runtime": "0.2.0-beta.8"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "24.13.3",
package/src/client.ts CHANGED
@@ -1,18 +1,24 @@
1
1
  import {
2
+ binding,
2
3
  cloneRenderable,
3
4
  compiledRoot,
5
+ createComponentRenderable,
4
6
  createCompiledScope,
7
+ createCompiledState,
8
+ source,
5
9
  type CompiledComponentResult,
10
+ type CompiledRenderValue,
6
11
  type CompiledRoot,
7
12
  } from '@vidact/runtime'
8
- import { createElement, hydrateRoot } from '@vidact/runtime/hydrate'
13
+ import { hydrateRoot } from '@vidact/runtime/hydrate'
9
14
 
10
15
  import {
11
- composeRouteMatches,
12
16
  loadRouteMatches,
13
17
  matchRoutes,
14
18
  type LoadedRouteMatch,
19
+ type RouteComponentProps,
15
20
  type RouteManifest,
21
+ type RouteManifestEntry,
16
22
  type StartComponent,
17
23
  } from './router.ts'
18
24
  import {
@@ -50,6 +56,26 @@ export interface StartClient extends CompiledRoot {
50
56
 
51
57
  type HistoryMode = 'none' | 'push' | 'replace'
52
58
 
59
+ interface ClientRouteState {
60
+ readonly components: readonly ClientRouteComponent[]
61
+ readonly requestUrl: string
62
+ }
63
+
64
+ interface ClientRouteComponent {
65
+ readonly component: StartComponent
66
+ readonly entry: RouteManifestEntry
67
+ readonly loaderData: LoadedRouteMatch['loaderData']
68
+ readonly params: LoadedRouteMatch['params']
69
+ }
70
+
71
+ interface RetainedClientApplication {
72
+ readonly application: () => CompiledComponentResult
73
+ readonly state: ClientRouteState
74
+ readonly update: (state: ClientRouteState) => void
75
+ }
76
+
77
+ const routeComponentIdentities = new WeakMap<RouteManifestEntry, WeakMap<StartComponent, object>>()
78
+
53
79
  export async function hydrateStart(options: HydrateStartOptions): Promise<StartClient> {
54
80
  const snapshot = decodeStartSnapshot(
55
81
  options.snapshot ?? readSnapshot(options.snapshotId ?? DEFAULT_SNAPSHOT_ID),
@@ -63,7 +89,8 @@ export async function hydrateStart(options: HydrateStartOptions): Promise<StartC
63
89
  options.root ?? document.querySelector(`#${CSS.escape(options.rootId ?? DEFAULT_ROOT_ID)}`)
64
90
  if (host === null) throw new Error('Vidact Start hydration root is missing')
65
91
 
66
- const root = hydrateRoot(host, createClientApplication(loaded, request.url), {
92
+ let application = createClientApplication(loaded, request.url)
93
+ const root = hydrateRoot(host, application.application, {
67
94
  onRecoverableError:
68
95
  options.onRecoverableError ??
69
96
  ((error) => console.warn('[vidact/start] hydration recovered by re-rendering:', error)),
@@ -123,6 +150,9 @@ export async function hydrateStart(options: HydrateStartOptions): Promise<StartC
123
150
 
124
151
  const nextSnapshot = decodeStartSnapshot(await response.text())
125
152
  const nextUrl = new URL(nextSnapshot.pathname, target.origin)
153
+ // Fragments never reach the server request. Preserve the caller's fragment unless
154
+ // the snapshot explicitly supplies one, including when the server redirects the path.
155
+ if (nextUrl.hash === '') nextUrl.hash = target.hash
126
156
  const snapshotMatches = matchRoutes(options.manifest, nextUrl.pathname)
127
157
  if (snapshotMatches.length === 0) {
128
158
  navigateDocument(target, historyMode === 'replace')
@@ -136,7 +166,14 @@ export async function hydrateStart(options: HydrateStartOptions): Promise<StartC
136
166
  )
137
167
  if (attempt !== navigation) return false
138
168
 
139
- root.replace(createClientApplication(nextLoaded, nextRequest.url))
169
+ const nextState = createClientRouteState(nextLoaded, nextRequest.url)
170
+ if (sharedRouteComponentCount(application.state, nextState) === 0) {
171
+ const replacement = createClientApplication(nextLoaded, nextRequest.url)
172
+ root.replace(replacement.application)
173
+ application = replacement
174
+ } else {
175
+ application.update(nextState)
176
+ }
140
177
  updateHistory(nextUrl, historyMode)
141
178
  if (scroll) scrollToLocation(nextUrl)
142
179
  return true
@@ -192,16 +229,124 @@ export async function hydrateStart(options: HydrateStartOptions): Promise<StartC
192
229
  function createClientApplication(
193
230
  matches: readonly LoadedRouteMatch[],
194
231
  requestUrl: string,
195
- ): () => CompiledComponentResult {
196
- return () => {
197
- const scope = createCompiledScope()
198
- const application = composeRouteMatches(
199
- matches,
200
- (component, props) => createElement(component as never, props),
201
- requestUrl,
202
- )
203
- return compiledRoot(scope, () => cloneRenderable(application))
232
+ ): RetainedClientApplication {
233
+ const initialState = createClientRouteState(matches, requestUrl)
234
+ let currentState = initialState
235
+ let updateState: ((state: ClientRouteState) => void) | undefined
236
+ return {
237
+ application: () => {
238
+ const scope = createCompiledScope()
239
+ const routeSource = source(0)
240
+ const state = createCompiledState(scope, routeSource, currentState)
241
+ updateState = (nextState) => {
242
+ const previousState = currentState
243
+ try {
244
+ state.set(nextState)
245
+ currentState = nextState
246
+ } catch (error) {
247
+ try {
248
+ state.set(previousState)
249
+ } catch {
250
+ // Preserve the navigation failure that triggered the rollback.
251
+ }
252
+ currentState = previousState
253
+ throw error
254
+ }
255
+ }
256
+ const route = createRetainedRouteRenderable(scope, routeSource, state.get, 0)
257
+ return compiledRoot(scope, () => cloneRenderable(route))
258
+ },
259
+ get state() {
260
+ return currentState
261
+ },
262
+ update(nextState) {
263
+ if (updateState === undefined) {
264
+ throw new Error('cannot update a Vidact Start application before it mounts')
265
+ }
266
+ updateState(nextState)
267
+ },
268
+ }
269
+ }
270
+
271
+ function createClientRouteState(
272
+ matches: readonly LoadedRouteMatch[],
273
+ requestUrl: string,
274
+ ): ClientRouteState {
275
+ return {
276
+ components: matches.flatMap((match) => {
277
+ const component = match.definition.options.component
278
+ return component === undefined
279
+ ? []
280
+ : [{ component, entry: match.entry, loaderData: match.loaderData, params: match.params }]
281
+ }),
282
+ requestUrl,
283
+ }
284
+ }
285
+
286
+ function createRetainedRouteRenderable(
287
+ scope: ReturnType<typeof createCompiledScope>,
288
+ routeSource: ReturnType<typeof source>,
289
+ readState: () => ClientRouteState,
290
+ index: number,
291
+ ): CompiledRenderValue {
292
+ const initial = readState().components[index]
293
+ if (initial === undefined) return undefined
294
+ const identity = routeComponentIdentity(initial.entry, initial.component)
295
+ let lastProps = routeComponentProps(scope, routeSource, readState, index)
296
+ const input = binding(scope, routeSource, () => {
297
+ const current = readState().components[index]
298
+ if (
299
+ current === undefined ||
300
+ routeComponentIdentity(current.entry, current.component) !== identity
301
+ ) {
302
+ return lastProps
303
+ }
304
+ lastProps = routeComponentProps(scope, routeSource, readState, index)
305
+ return lastProps
306
+ })
307
+ return createComponentRenderable(initial.component as never, input, identity)
308
+ }
309
+
310
+ function routeComponentProps(
311
+ scope: ReturnType<typeof createCompiledScope>,
312
+ routeSource: ReturnType<typeof source>,
313
+ readState: () => ClientRouteState,
314
+ index: number,
315
+ ): RouteComponentProps {
316
+ const state = readState()
317
+ const match = state.components[index]!
318
+ return {
319
+ children: createRetainedRouteRenderable(scope, routeSource, readState, index + 1),
320
+ loaderData: match.loaderData,
321
+ params: match.params,
322
+ requestUrl: state.requestUrl,
323
+ }
324
+ }
325
+
326
+ function sharedRouteComponentCount(left: ClientRouteState, right: ClientRouteState): number {
327
+ const length = Math.min(left.components.length, right.components.length)
328
+ let index = 0
329
+ while (index < length) {
330
+ const previous = left.components[index]!
331
+ const next = right.components[index]!
332
+ if (previous.entry !== next.entry || previous.component !== next.component) break
333
+ index += 1
334
+ }
335
+ return index
336
+ }
337
+
338
+ function routeComponentIdentity(entry: RouteManifestEntry, component: StartComponent): object {
339
+ let componentIdentities = routeComponentIdentities.get(entry)
340
+ if (componentIdentities === undefined) {
341
+ componentIdentities = new WeakMap()
342
+ routeComponentIdentities.set(entry, componentIdentities)
343
+ }
344
+ let identity = componentIdentities.get(component)
345
+ if (identity === undefined) {
346
+ identity = {}
347
+ componentIdentities.set(component, identity)
204
348
  }
349
+ return identity
205
350
  }
206
351
 
207
352
  function linkForEvent(event: MouseEvent): HTMLAnchorElement | undefined {
@@ -1,10 +1,11 @@
1
- import type { CompiledRenderValue } from '@vidact/runtime'
1
+ import { compiledRoot, createNarrowCompiledScope, type CompiledRenderValue } from '@vidact/runtime'
2
2
  import { createElement } from '@vidact/runtime/hydrate'
3
3
 
4
4
  import { anchorProps, type LinkProps } from './link-contract.ts'
5
5
 
6
6
  export function Link(props: LinkProps): CompiledRenderValue {
7
- return createElement('a', anchorProps(props)) as unknown as CompiledRenderValue
7
+ const scope = createNarrowCompiledScope()
8
+ return compiledRoot(scope, () => createElement('a', anchorProps(props)))
8
9
  }
9
10
 
10
11
  export type { LinkProps }
package/src/server.ts CHANGED
@@ -40,11 +40,26 @@ export interface StartDocumentContext {
40
40
  readonly snapshotId: string
41
41
  }
42
42
 
43
+ export interface StartDocumentShellContext {
44
+ readonly clientEntry: string | undefined
45
+ readonly rootId: string
46
+ readonly snapshot: string
47
+ readonly snapshotId: string
48
+ }
49
+
50
+ export interface StartDocumentShell {
51
+ readonly afterApplication: string
52
+ readonly beforeApplication: string
53
+ }
54
+
43
55
  export interface StartHandlerOptions {
44
56
  readonly clientEntry?: string
45
57
  readonly manifest: RouteManifest
46
58
  readonly notFound?: (request: Request) => Response | Promise<Response>
47
59
  readonly renderDocument?: (context: StartDocumentContext) => string | Promise<string>
60
+ readonly renderDocumentShell?: (
61
+ context: StartDocumentShellContext,
62
+ ) => StartDocumentShell | Promise<StartDocumentShell>
48
63
  readonly rootId?: string
49
64
  readonly snapshotId?: string
50
65
  }
@@ -52,6 +67,9 @@ export interface StartHandlerOptions {
52
67
  export function createStartHandler(
53
68
  options: StartHandlerOptions,
54
69
  ): (request: Request) => Promise<Response> {
70
+ if (options.renderDocument !== undefined && options.renderDocumentShell !== undefined) {
71
+ throw new TypeError('renderDocument and renderDocumentShell cannot be used together')
72
+ }
55
73
  const rootId = options.rootId ?? DEFAULT_ROOT_ID
56
74
  const snapshotId = options.snapshotId ?? DEFAULT_SNAPSHOT_ID
57
75
  return async (request) => {
@@ -100,9 +118,11 @@ export function createStartHandler(
100
118
  )
101
119
  if (request.headers.get(VIDACT_START_NAVIGATION_HEADER) === '1') {
102
120
  return new Response(request.method === 'HEAD' ? null : snapshot, {
103
- headers: { 'content-type': `${VIDACT_START_SNAPSHOT_MEDIA_TYPE}; charset=utf-8` },
121
+ headers: representationHeaders(`${VIDACT_START_SNAPSHOT_MEDIA_TYPE}; charset=utf-8`),
104
122
  })
105
123
  }
124
+ const headers = representationHeaders('text/html; charset=utf-8')
125
+ if (request.method === 'HEAD') return new Response(null, { headers })
106
126
 
107
127
  const application = composeRouteMatches(
108
128
  loaded,
@@ -112,26 +132,34 @@ export function createStartHandler(
112
132
  ) as ServerChild
113
133
  const stream = await renderToReadableStream(
114
134
  () => createElement(StartApplication, { children: application }) as ServerChild,
115
- { identifierPrefix: 'start-' },
135
+ { identifierPrefix: 'start-', signal: request.signal },
116
136
  )
117
- const applicationHtml = await new Response(stream).text()
118
- const documentContext = {
119
- applicationHtml,
137
+ void stream.allReady.catch(() => undefined)
138
+ const shellContext = {
120
139
  clientEntry: options.clientEntry,
121
140
  rootId,
122
141
  snapshot,
123
142
  snapshotId,
124
143
  }
125
- const html =
126
- options.renderDocument === undefined
127
- ? defaultDocument(documentContext)
128
- : await options.renderDocument(documentContext)
129
- return new Response(request.method === 'HEAD' ? null : html, {
130
- headers: { 'content-type': 'text/html; charset=utf-8' },
131
- })
144
+ if (options.renderDocument !== undefined) {
145
+ const applicationHtml = await new Response(stream).text()
146
+ const html = await options.renderDocument({ ...shellContext, applicationHtml })
147
+ return new Response(html, { headers })
148
+ }
149
+ const shell =
150
+ options.renderDocumentShell === undefined
151
+ ? defaultDocumentShell(shellContext)
152
+ : validateDocumentShell(await options.renderDocumentShell(shellContext))
153
+ return new Response(streamDocument(shell, stream), { headers })
132
154
  }
133
155
  }
134
156
 
157
+ function representationHeaders(contentType: string): Headers {
158
+ const headers = new Headers({ 'content-type': contentType })
159
+ headers.append('vary', VIDACT_START_NAVIGATION_HEADER)
160
+ return headers
161
+ }
162
+
135
163
  function responseForRequest(request: Request, response: Response): Response {
136
164
  return request.method === 'HEAD' ? new Response(null, response) : response
137
165
  }
@@ -188,12 +216,68 @@ function methodRank(method: string): number {
188
216
  return method === 'GET' ? 0 : method === 'HEAD' ? 1 : 2
189
217
  }
190
218
 
191
- function defaultDocument(context: StartDocumentContext): string {
219
+ function defaultDocumentShell(context: StartDocumentShellContext): StartDocumentShell {
192
220
  const clientScript =
193
221
  context.clientEntry === undefined
194
222
  ? ''
195
223
  : `<script type="module" src="${escapeAttribute(context.clientEntry)}"></script>`
196
- return `<!doctype html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head><body><div id="${escapeAttribute(context.rootId)}">${context.applicationHtml}</div><script id="${escapeAttribute(context.snapshotId)}" type="application/json">${context.snapshot}</script>${clientScript}</body></html>`
224
+ return {
225
+ beforeApplication: `<!doctype html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head><body><div id="${escapeAttribute(context.rootId)}">`,
226
+ afterApplication: `</div><script id="${escapeAttribute(context.snapshotId)}" type="application/json">${context.snapshot}</script>${clientScript}</body></html>`,
227
+ }
228
+ }
229
+
230
+ function validateDocumentShell(value: StartDocumentShell): StartDocumentShell {
231
+ if (
232
+ typeof value !== 'object' ||
233
+ value === null ||
234
+ typeof value.beforeApplication !== 'string' ||
235
+ typeof value.afterApplication !== 'string'
236
+ ) {
237
+ throw new TypeError('renderDocumentShell must return beforeApplication and afterApplication')
238
+ }
239
+ return value
240
+ }
241
+
242
+ function streamDocument(
243
+ shell: StartDocumentShell,
244
+ application: ReadableStream<Uint8Array>,
245
+ ): ReadableStream<Uint8Array> {
246
+ const encoder = new TextEncoder()
247
+ const before = encoder.encode(shell.beforeApplication)
248
+ const after = encoder.encode(shell.afterApplication)
249
+ const reader = application.getReader()
250
+ let phase: 'after' | 'application' | 'before' | 'done' = 'before'
251
+ return new ReadableStream<Uint8Array>({
252
+ async pull(controller) {
253
+ try {
254
+ if (phase === 'before') {
255
+ phase = 'application'
256
+ if (before.length > 0) controller.enqueue(before)
257
+ return
258
+ }
259
+ if (phase === 'application') {
260
+ const result = await reader.read()
261
+ if (!result.done) {
262
+ controller.enqueue(result.value)
263
+ return
264
+ }
265
+ phase = 'after'
266
+ }
267
+ if (phase === 'after') {
268
+ phase = 'done'
269
+ if (after.length > 0) controller.enqueue(after)
270
+ return
271
+ }
272
+ controller.close()
273
+ } catch (error) {
274
+ controller.error(error)
275
+ }
276
+ },
277
+ cancel(reason) {
278
+ return reader.cancel(reason)
279
+ },
280
+ })
197
281
  }
198
282
 
199
283
  function escapeAttribute(value: string): string {