@vertz/ui-server 0.2.47 → 0.2.49
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/dist/bun-dev-server.d.ts +35 -0
- package/dist/bun-dev-server.js +58 -0
- package/dist/bun-plugin/state-inspector.d.ts +59 -0
- package/dist/bun-plugin/state-inspector.js +270 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +23 -7
- package/dist/node-handler.d.ts +22 -0
- package/dist/node-handler.js +2 -2
- package/dist/shared/{chunk-kzycr5v0.js → chunk-1hjzk64k.js} +31 -10
- package/dist/shared/{chunk-ck3n699k.js → chunk-91eg6dps.js} +34 -7
- package/dist/shared/{chunk-tm5aeq94.js → chunk-j9z9r179.js} +38 -4
- package/dist/ssr/index.d.ts +22 -0
- package/dist/ssr/index.js +2 -2
- package/package.json +9 -5
package/dist/bun-dev-server.d.ts
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
interface QuerySnapshot {
|
|
2
|
+
data: SerializedValue;
|
|
3
|
+
loading: boolean;
|
|
4
|
+
revalidating: boolean;
|
|
5
|
+
error: SerializedValue;
|
|
6
|
+
idle: boolean;
|
|
7
|
+
key?: string;
|
|
8
|
+
}
|
|
9
|
+
interface InstanceSnapshot {
|
|
10
|
+
index: number;
|
|
11
|
+
key?: string;
|
|
12
|
+
signals: Record<string, SerializedValue>;
|
|
13
|
+
queries: Record<string, QuerySnapshot>;
|
|
14
|
+
}
|
|
15
|
+
interface ComponentSnapshot {
|
|
16
|
+
name: string;
|
|
17
|
+
moduleId: string;
|
|
18
|
+
instanceCount: number;
|
|
19
|
+
instances: InstanceSnapshot[];
|
|
20
|
+
}
|
|
21
|
+
interface StateSnapshot {
|
|
22
|
+
components: ComponentSnapshot[];
|
|
23
|
+
totalInstances: number;
|
|
24
|
+
connectedClients: number;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
message?: string;
|
|
27
|
+
truncated?: boolean;
|
|
28
|
+
}
|
|
29
|
+
type SerializedValue = string | number | boolean | null | object;
|
|
1
30
|
import { AccessSet } from "@vertz/ui/auth";
|
|
2
31
|
interface SessionData {
|
|
3
32
|
user: {
|
|
@@ -198,6 +227,12 @@ interface BunDevServer {
|
|
|
198
227
|
clearErrorForFileChange(): void;
|
|
199
228
|
/** Set the last changed file path (for testing). */
|
|
200
229
|
setLastChangedFile(file: string): void;
|
|
230
|
+
/**
|
|
231
|
+
* Request a state inspection from connected browser clients.
|
|
232
|
+
* Broadcasts an `inspect-state` command and waits for the first
|
|
233
|
+
* `state-snapshot` response. Returns the snapshot JSON or an error.
|
|
234
|
+
*/
|
|
235
|
+
inspectState(filter?: string): Promise<StateSnapshot>;
|
|
201
236
|
}
|
|
202
237
|
interface HMRAssets {
|
|
203
238
|
/** Discovered `/_bun/client/<hash>.js` URL, or null if not found */
|
package/dist/bun-dev-server.js
CHANGED
|
@@ -3632,6 +3632,7 @@ function createBunDevServer(options) {
|
|
|
3632
3632
|
let ssrFallback = false;
|
|
3633
3633
|
const wsClients = new Set;
|
|
3634
3634
|
let currentError = null;
|
|
3635
|
+
const pendingInspections = new Map;
|
|
3635
3636
|
const sourceMapResolver = createSourceMapResolver(projectRoot);
|
|
3636
3637
|
let clearGraceUntil = 0;
|
|
3637
3638
|
let runtimeDebounceTimer = null;
|
|
@@ -4098,6 +4099,10 @@ function createBunDevServer(options) {
|
|
|
4098
4099
|
const frInitPath = resolve(devDir, "fast-refresh-init.ts");
|
|
4099
4100
|
writeFileSync3(frInitPath, `import '@vertz/ui-server/fast-refresh-runtime';
|
|
4100
4101
|
if (import.meta.hot) import.meta.hot.accept();
|
|
4102
|
+
`);
|
|
4103
|
+
const siInitPath = resolve(devDir, "state-inspector-init.ts");
|
|
4104
|
+
writeFileSync3(siInitPath, `import '@vertz/ui-server/state-inspector';
|
|
4105
|
+
if (import.meta.hot) import.meta.hot.accept();
|
|
4101
4106
|
`);
|
|
4102
4107
|
const hmrShellHtml = `<!doctype html>
|
|
4103
4108
|
<html lang="en"><head>
|
|
@@ -4105,6 +4110,7 @@ if (import.meta.hot) import.meta.hot.accept();
|
|
|
4105
4110
|
<title>HMR Shell</title>
|
|
4106
4111
|
</head><body>
|
|
4107
4112
|
<script type="module" src="./fast-refresh-init.ts"></script>
|
|
4113
|
+
<script type="module" src="./state-inspector-init.ts"></script>
|
|
4108
4114
|
<script type="module" src="${clientSrc}"></script>
|
|
4109
4115
|
</body></html>`;
|
|
4110
4116
|
const hmrShellPath = resolve(devDir, "hmr-shell.html");
|
|
@@ -4431,6 +4437,13 @@ data: {}
|
|
|
4431
4437
|
devServer.restart();
|
|
4432
4438
|
} else if (data.type === "ping") {
|
|
4433
4439
|
ws.sendText(JSON.stringify({ type: "pong" }));
|
|
4440
|
+
} else if (data.type === "state-snapshot" && data.requestId) {
|
|
4441
|
+
const pending = pendingInspections.get(data.requestId);
|
|
4442
|
+
if (pending) {
|
|
4443
|
+
clearTimeout(pending.timer);
|
|
4444
|
+
pendingInspections.delete(data.requestId);
|
|
4445
|
+
pending.resolve(data.snapshot);
|
|
4446
|
+
}
|
|
4434
4447
|
} else if (data.type === "resolve-stack" && data.stack) {
|
|
4435
4448
|
const selfFetch = async (url) => {
|
|
4436
4449
|
const absUrl = url.startsWith("http") ? url : `http://${host}:${server?.port}${url}`;
|
|
@@ -4803,8 +4816,53 @@ data: {}
|
|
|
4803
4816
|
setLastChangedFile(file) {
|
|
4804
4817
|
lastChangedFile = file;
|
|
4805
4818
|
},
|
|
4819
|
+
async inspectState(filter) {
|
|
4820
|
+
if (wsClients.size === 0) {
|
|
4821
|
+
return {
|
|
4822
|
+
components: [],
|
|
4823
|
+
totalInstances: 0,
|
|
4824
|
+
connectedClients: 0,
|
|
4825
|
+
timestamp: new Date().toISOString(),
|
|
4826
|
+
message: "No browser clients connected. Open the app in a browser first."
|
|
4827
|
+
};
|
|
4828
|
+
}
|
|
4829
|
+
const requestId = crypto.randomUUID();
|
|
4830
|
+
const TIMEOUT_MS = 5000;
|
|
4831
|
+
return new Promise((resolve2) => {
|
|
4832
|
+
const timer = setTimeout(() => {
|
|
4833
|
+
pendingInspections.delete(requestId);
|
|
4834
|
+
resolve2({
|
|
4835
|
+
components: [],
|
|
4836
|
+
totalInstances: 0,
|
|
4837
|
+
connectedClients: wsClients.size,
|
|
4838
|
+
timestamp: new Date().toISOString(),
|
|
4839
|
+
message: "State inspection timed out after 5 seconds."
|
|
4840
|
+
});
|
|
4841
|
+
}, TIMEOUT_MS);
|
|
4842
|
+
pendingInspections.set(requestId, { resolve: resolve2, timer });
|
|
4843
|
+
const cmd = JSON.stringify({
|
|
4844
|
+
type: "inspect-state",
|
|
4845
|
+
requestId,
|
|
4846
|
+
...filter ? { filter } : {}
|
|
4847
|
+
});
|
|
4848
|
+
for (const client of wsClients) {
|
|
4849
|
+
client.sendText(cmd);
|
|
4850
|
+
}
|
|
4851
|
+
});
|
|
4852
|
+
},
|
|
4806
4853
|
async stop() {
|
|
4807
4854
|
stopped = true;
|
|
4855
|
+
for (const [, { resolve: res, timer }] of pendingInspections) {
|
|
4856
|
+
clearTimeout(timer);
|
|
4857
|
+
res({
|
|
4858
|
+
components: [],
|
|
4859
|
+
totalInstances: 0,
|
|
4860
|
+
connectedClients: 0,
|
|
4861
|
+
timestamp: new Date().toISOString(),
|
|
4862
|
+
message: "Server stopped."
|
|
4863
|
+
});
|
|
4864
|
+
}
|
|
4865
|
+
pendingInspections.clear();
|
|
4808
4866
|
if (refreshTimeout) {
|
|
4809
4867
|
clearTimeout(refreshTimeout);
|
|
4810
4868
|
refreshTimeout = null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
interface QuerySnapshot {
|
|
2
|
+
data: SerializedValue;
|
|
3
|
+
loading: boolean;
|
|
4
|
+
revalidating: boolean;
|
|
5
|
+
error: SerializedValue;
|
|
6
|
+
idle: boolean;
|
|
7
|
+
key?: string;
|
|
8
|
+
}
|
|
9
|
+
interface InstanceSnapshot {
|
|
10
|
+
index: number;
|
|
11
|
+
key?: string;
|
|
12
|
+
signals: Record<string, SerializedValue>;
|
|
13
|
+
queries: Record<string, QuerySnapshot>;
|
|
14
|
+
}
|
|
15
|
+
interface ComponentSnapshot {
|
|
16
|
+
name: string;
|
|
17
|
+
moduleId: string;
|
|
18
|
+
instanceCount: number;
|
|
19
|
+
instances: InstanceSnapshot[];
|
|
20
|
+
}
|
|
21
|
+
interface StateSnapshot {
|
|
22
|
+
components: ComponentSnapshot[];
|
|
23
|
+
totalInstances: number;
|
|
24
|
+
connectedClients: number;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
message?: string;
|
|
27
|
+
truncated?: boolean;
|
|
28
|
+
}
|
|
29
|
+
type SerializedValue = string | number | boolean | null | object;
|
|
30
|
+
/**
|
|
31
|
+
* Serialize any JavaScript value to a JSON-safe representation.
|
|
32
|
+
* Handles functions, DOM nodes, circular references, Date, Map, Set,
|
|
33
|
+
* Error, Promise, Symbol, WeakRef, ArrayBuffer, and depth limiting.
|
|
34
|
+
*/
|
|
35
|
+
declare function safeSerialize(value: unknown, maxDepth?: number, seen?: WeakSet<object>): SerializedValue;
|
|
36
|
+
/**
|
|
37
|
+
* Walk the Fast Refresh registry and collect a state snapshot.
|
|
38
|
+
* Optionally filter by component function name (case-sensitive).
|
|
39
|
+
*/
|
|
40
|
+
declare function collectStateSnapshot(filter?: string): StateSnapshot;
|
|
41
|
+
/**
|
|
42
|
+
* Handle incoming `inspect-state` WebSocket messages.
|
|
43
|
+
* Collects a state snapshot and sends it back with the matching requestId.
|
|
44
|
+
*/
|
|
45
|
+
declare function handleInspectMessage(event: MessageEvent, ws: WebSocket): void;
|
|
46
|
+
/**
|
|
47
|
+
* Set up the state inspector's WebSocket listener.
|
|
48
|
+
*
|
|
49
|
+
* Uses `addEventListener` instead of replacing `onmessage` to coexist with
|
|
50
|
+
* the error overlay handler. Polls for `__vertz_overlay._ws` reference changes
|
|
51
|
+
* so that reconnections (which create a new WebSocket instance) are re-hooked.
|
|
52
|
+
*
|
|
53
|
+
* NOTE: When multiple browser tabs are connected, the server broadcasts
|
|
54
|
+
* inspect-state to all. The first response wins — other tabs' responses are
|
|
55
|
+
* dropped. This is acceptable for v0.1.x; a future improvement could merge
|
|
56
|
+
* responses from multiple tabs.
|
|
57
|
+
*/
|
|
58
|
+
declare function setupStateInspector(): void;
|
|
59
|
+
export { setupStateInspector, safeSerialize, handleInspectMessage, collectStateSnapshot, StateSnapshot, QuerySnapshot, InstanceSnapshot, ComponentSnapshot };
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import"../shared/chunk-pshsm7ck.js";
|
|
3
|
+
|
|
4
|
+
// src/bun-plugin/state-inspector.ts
|
|
5
|
+
var REGISTRY_KEY = Symbol.for("vertz:fast-refresh:registry");
|
|
6
|
+
var MAX_RESPONSE_SIZE = 2 * 1024 * 1024;
|
|
7
|
+
var DEFAULT_MAX_DEPTH = 4;
|
|
8
|
+
var QUERY_SIGNAL_NAMES = ["data", "loading", "revalidating", "error", "idle"];
|
|
9
|
+
function safeSerialize(value, maxDepth = DEFAULT_MAX_DEPTH, seen = new WeakSet) {
|
|
10
|
+
if (value === null)
|
|
11
|
+
return null;
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return null;
|
|
14
|
+
if (typeof value === "boolean")
|
|
15
|
+
return value;
|
|
16
|
+
if (typeof value === "number") {
|
|
17
|
+
if (Number.isNaN(value))
|
|
18
|
+
return "[NaN]";
|
|
19
|
+
if (!Number.isFinite(value))
|
|
20
|
+
return value > 0 ? "[Infinity]" : "[-Infinity]";
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "string")
|
|
24
|
+
return value;
|
|
25
|
+
if (typeof value === "bigint")
|
|
26
|
+
return value.toString();
|
|
27
|
+
if (typeof value === "symbol") {
|
|
28
|
+
const desc = value.description;
|
|
29
|
+
return desc ? `[Symbol: ${desc}]` : "[Symbol]";
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "function") {
|
|
32
|
+
const name = value.name;
|
|
33
|
+
return name && name !== "anonymous" ? `[Function: ${name}]` : "[Function]";
|
|
34
|
+
}
|
|
35
|
+
const obj = value;
|
|
36
|
+
if (seen.has(obj))
|
|
37
|
+
return "[Circular]";
|
|
38
|
+
if (obj instanceof Date)
|
|
39
|
+
return obj.toISOString();
|
|
40
|
+
if (obj instanceof Error) {
|
|
41
|
+
return { name: obj.name, message: obj.message };
|
|
42
|
+
}
|
|
43
|
+
if (obj instanceof Promise)
|
|
44
|
+
return "[Promise]";
|
|
45
|
+
if (obj instanceof Map)
|
|
46
|
+
return `[Map: ${obj.size} entries]`;
|
|
47
|
+
if (obj instanceof Set)
|
|
48
|
+
return `[Set: ${obj.size} items]`;
|
|
49
|
+
if (obj instanceof WeakMap)
|
|
50
|
+
return "[WeakMap]";
|
|
51
|
+
if (obj instanceof WeakSet)
|
|
52
|
+
return "[WeakSet]";
|
|
53
|
+
if (typeof WeakRef !== "undefined" && obj instanceof WeakRef)
|
|
54
|
+
return "[WeakRef]";
|
|
55
|
+
if (obj instanceof ArrayBuffer)
|
|
56
|
+
return `[ArrayBuffer: ${obj.byteLength} bytes]`;
|
|
57
|
+
if (ArrayBuffer.isView(obj) && "byteLength" in obj) {
|
|
58
|
+
return `[ArrayBuffer: ${obj.byteLength} bytes]`;
|
|
59
|
+
}
|
|
60
|
+
if (typeof HTMLElement !== "undefined" && obj instanceof HTMLElement) {
|
|
61
|
+
return `[HTMLElement: ${obj.tagName}]`;
|
|
62
|
+
}
|
|
63
|
+
if (typeof Node !== "undefined" && obj instanceof Node) {
|
|
64
|
+
return `[Node: ${obj.nodeName}]`;
|
|
65
|
+
}
|
|
66
|
+
if (maxDepth <= 0) {
|
|
67
|
+
if (Array.isArray(obj))
|
|
68
|
+
return `[Array: ${obj.length} items]`;
|
|
69
|
+
return `[Object: ${Object.keys(obj).length} keys]`;
|
|
70
|
+
}
|
|
71
|
+
seen.add(obj);
|
|
72
|
+
if (Array.isArray(obj)) {
|
|
73
|
+
const result2 = obj.map((item) => safeSerialize(item, maxDepth - 1, seen));
|
|
74
|
+
seen.delete(obj);
|
|
75
|
+
return result2;
|
|
76
|
+
}
|
|
77
|
+
const result = {};
|
|
78
|
+
for (const key of Object.keys(obj)) {
|
|
79
|
+
result[key] = safeSerialize(obj[key], maxDepth - 1, seen);
|
|
80
|
+
}
|
|
81
|
+
seen.delete(obj);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
function peekSafe(sig) {
|
|
85
|
+
try {
|
|
86
|
+
return sig.peek();
|
|
87
|
+
} catch (e) {
|
|
88
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
89
|
+
return `[Error: ${msg}]`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function collectStateSnapshot(filter) {
|
|
93
|
+
const registry = globalThis[REGISTRY_KEY];
|
|
94
|
+
if (!registry || registry.size === 0) {
|
|
95
|
+
return emptySnapshot(filter ? `${filter} is not in the component registry. Check the name spelling or ensure the file has been loaded.` : undefined);
|
|
96
|
+
}
|
|
97
|
+
const components = [];
|
|
98
|
+
let totalInstances = 0;
|
|
99
|
+
let foundInRegistry = false;
|
|
100
|
+
for (const [moduleId, moduleMap] of registry) {
|
|
101
|
+
for (const [name, record] of moduleMap) {
|
|
102
|
+
if (filter && name !== filter)
|
|
103
|
+
continue;
|
|
104
|
+
if (filter)
|
|
105
|
+
foundInRegistry = true;
|
|
106
|
+
const instances = [];
|
|
107
|
+
for (let i = 0;i < record.instances.length; i++) {
|
|
108
|
+
const inst = record.instances[i];
|
|
109
|
+
if (!inst.element?.isConnected)
|
|
110
|
+
continue;
|
|
111
|
+
const signals = {};
|
|
112
|
+
const queries = {};
|
|
113
|
+
const queryGroups = new Map;
|
|
114
|
+
const standaloneSignals = [];
|
|
115
|
+
for (const sig of inst.signals) {
|
|
116
|
+
const group = sig._queryGroup;
|
|
117
|
+
if (group) {
|
|
118
|
+
if (!queryGroups.has(group))
|
|
119
|
+
queryGroups.set(group, []);
|
|
120
|
+
queryGroups.get(group).push(sig);
|
|
121
|
+
} else {
|
|
122
|
+
standaloneSignals.push(sig);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let positionalIdx = 0;
|
|
126
|
+
for (const sig of standaloneSignals) {
|
|
127
|
+
const key = sig._hmrKey ?? `signal_${positionalIdx++}`;
|
|
128
|
+
signals[key] = safeSerialize(peekSafe(sig));
|
|
129
|
+
}
|
|
130
|
+
for (const [groupKey, groupSignals] of queryGroups) {
|
|
131
|
+
queries[groupKey] = buildQuerySnapshot(groupSignals, groupKey);
|
|
132
|
+
}
|
|
133
|
+
instances.push({ index: i, signals, queries });
|
|
134
|
+
totalInstances++;
|
|
135
|
+
}
|
|
136
|
+
if (instances.length > 0) {
|
|
137
|
+
components.push({ name, moduleId, instanceCount: instances.length, instances });
|
|
138
|
+
} else if (filter && foundInRegistry) {}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let message;
|
|
142
|
+
if (filter && components.length === 0) {
|
|
143
|
+
if (foundInRegistry) {
|
|
144
|
+
let moduleId = "";
|
|
145
|
+
for (const [mid, moduleMap] of registry) {
|
|
146
|
+
if (moduleMap.has(filter)) {
|
|
147
|
+
moduleId = mid;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
message = `${filter} is registered (in ${moduleId}) but has 0 mounted instances on the current page. Navigate to a page that renders it.`;
|
|
152
|
+
} else {
|
|
153
|
+
message = `${filter} is not in the component registry. Check the name spelling or ensure the file has been loaded.`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const snapshot = {
|
|
157
|
+
components,
|
|
158
|
+
totalInstances,
|
|
159
|
+
connectedClients: 0,
|
|
160
|
+
timestamp: new Date().toISOString(),
|
|
161
|
+
...message ? { message } : {}
|
|
162
|
+
};
|
|
163
|
+
const jsonStr = JSON.stringify(snapshot);
|
|
164
|
+
if (jsonStr.length > MAX_RESPONSE_SIZE) {
|
|
165
|
+
return truncateSnapshot(snapshot);
|
|
166
|
+
}
|
|
167
|
+
return snapshot;
|
|
168
|
+
}
|
|
169
|
+
function emptySnapshot(message) {
|
|
170
|
+
return {
|
|
171
|
+
components: [],
|
|
172
|
+
totalInstances: 0,
|
|
173
|
+
connectedClients: 0,
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
...message ? { message } : {}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function buildQuerySnapshot(signals, groupKey) {
|
|
179
|
+
const named = new Map;
|
|
180
|
+
const unnamed = [];
|
|
181
|
+
for (const sig of signals) {
|
|
182
|
+
const val = peekSafe(sig);
|
|
183
|
+
if (sig._hmrKey && QUERY_SIGNAL_NAMES.includes(sig._hmrKey)) {
|
|
184
|
+
named.set(sig._hmrKey, val);
|
|
185
|
+
} else {
|
|
186
|
+
unnamed.push(val);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
data: safeSerialize(named.get("data") ?? unnamed[0] ?? null),
|
|
191
|
+
loading: Boolean(named.get("loading") ?? unnamed[1] ?? false),
|
|
192
|
+
revalidating: Boolean(named.get("revalidating") ?? unnamed[2] ?? false),
|
|
193
|
+
error: safeSerialize(named.get("error") ?? unnamed[3] ?? null),
|
|
194
|
+
idle: Boolean(named.get("idle") ?? unnamed[4] ?? false),
|
|
195
|
+
key: groupKey
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function handleInspectMessage(event, ws) {
|
|
199
|
+
if (typeof event.data !== "string")
|
|
200
|
+
return;
|
|
201
|
+
try {
|
|
202
|
+
const msg = JSON.parse(event.data);
|
|
203
|
+
if (msg.type === "inspect-state") {
|
|
204
|
+
const snapshot = collectStateSnapshot(msg.filter ?? undefined);
|
|
205
|
+
ws.send(JSON.stringify({
|
|
206
|
+
type: "state-snapshot",
|
|
207
|
+
requestId: msg.requestId,
|
|
208
|
+
snapshot
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
function setupStateInspector() {
|
|
214
|
+
if (typeof window === "undefined")
|
|
215
|
+
return;
|
|
216
|
+
let currentWs = null;
|
|
217
|
+
const MAX_INIT_RETRIES = 10;
|
|
218
|
+
let initRetries = 0;
|
|
219
|
+
function hookWs(ws) {
|
|
220
|
+
if (ws === currentWs)
|
|
221
|
+
return;
|
|
222
|
+
currentWs = ws;
|
|
223
|
+
ws.addEventListener("message", (event) => {
|
|
224
|
+
handleInspectMessage(event, ws);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function poll() {
|
|
228
|
+
const overlay = window.__vertz_overlay;
|
|
229
|
+
if (!overlay) {
|
|
230
|
+
if (initRetries++ < MAX_INIT_RETRIES) {
|
|
231
|
+
setTimeout(poll, 500);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const checkWs = () => {
|
|
236
|
+
if (overlay._ws && overlay._ws !== currentWs) {
|
|
237
|
+
hookWs(overlay._ws);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
checkWs();
|
|
241
|
+
setInterval(checkWs, 2000);
|
|
242
|
+
}
|
|
243
|
+
poll();
|
|
244
|
+
}
|
|
245
|
+
if (typeof document !== "undefined") {
|
|
246
|
+
if (document.readyState === "loading") {
|
|
247
|
+
document.addEventListener("DOMContentLoaded", setupStateInspector);
|
|
248
|
+
} else {
|
|
249
|
+
setupStateInspector();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function truncateSnapshot(snapshot) {
|
|
253
|
+
const truncated = {
|
|
254
|
+
...snapshot,
|
|
255
|
+
truncated: true,
|
|
256
|
+
components: snapshot.components.map((comp) => ({
|
|
257
|
+
...comp,
|
|
258
|
+
instances: comp.instances.slice(0, 3),
|
|
259
|
+
instanceCount: comp.instanceCount
|
|
260
|
+
}))
|
|
261
|
+
};
|
|
262
|
+
truncated.totalInstances = truncated.components.reduce((sum, c) => sum + c.instances.length, 0);
|
|
263
|
+
return truncated;
|
|
264
|
+
}
|
|
265
|
+
export {
|
|
266
|
+
setupStateInspector,
|
|
267
|
+
safeSerialize,
|
|
268
|
+
handleInspectMessage,
|
|
269
|
+
collectStateSnapshot
|
|
270
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -536,6 +536,28 @@ interface SSRHandlerOptions {
|
|
|
536
536
|
* (JSON files, third-party APIs, custom DB clients).
|
|
537
537
|
*/
|
|
538
538
|
aotDataResolver?: AotDataResolver;
|
|
539
|
+
/**
|
|
540
|
+
* Derive attributes to set on the `<html>` tag from the incoming request.
|
|
541
|
+
*
|
|
542
|
+
* Useful for setting `data-theme`, `dir`, `lang`, or other attributes that
|
|
543
|
+
* must be on `<html>` to avoid FOUC. The callback runs before SSR rendering
|
|
544
|
+
* so the attributes are available in the first byte of the response.
|
|
545
|
+
*
|
|
546
|
+
* If the template already has an attribute with the same name, the callback's
|
|
547
|
+
* value overrides it. Values are HTML-escaped automatically. Keys must be
|
|
548
|
+
* valid HTML attribute names (`/^[a-zA-Z][a-zA-Z0-9-]*$/`).
|
|
549
|
+
*
|
|
550
|
+
* Return `undefined`, `null`, or `{}` to skip injection.
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```ts
|
|
554
|
+
* htmlAttributes: (request) => ({
|
|
555
|
+
* 'data-theme': getThemeCookie(request) ?? 'dark',
|
|
556
|
+
* dir: getDirection(request),
|
|
557
|
+
* })
|
|
558
|
+
* ```
|
|
559
|
+
*/
|
|
560
|
+
htmlAttributes?: (request: Request) => Record<string, string> | null | undefined;
|
|
539
561
|
}
|
|
540
562
|
declare function createSSRHandler(options: SSRHandlerOptions): (request: Request) => Promise<Response>;
|
|
541
563
|
type NodeHandlerOptions = SSRHandlerOptions;
|
|
@@ -741,6 +763,8 @@ interface AotBarrelResult {
|
|
|
741
763
|
* Write each entry as `<tempDir>/<filename>.ts` alongside the barrel.
|
|
742
764
|
*/
|
|
743
765
|
files: Record<string, string>;
|
|
766
|
+
/** Function names skipped due to residual JSX in compiled output. */
|
|
767
|
+
skippedFns: string[];
|
|
744
768
|
}
|
|
745
769
|
/**
|
|
746
770
|
* Generate a barrel module that re-exports __ssr_* functions from compiled files.
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSSRHandler,
|
|
3
3
|
loadAotManifest
|
|
4
|
-
} from "./shared/chunk-
|
|
4
|
+
} from "./shared/chunk-91eg6dps.js";
|
|
5
5
|
import {
|
|
6
6
|
createNodeHandler
|
|
7
|
-
} from "./shared/chunk-
|
|
7
|
+
} from "./shared/chunk-1hjzk64k.js";
|
|
8
8
|
import {
|
|
9
9
|
clearRouteCssCache,
|
|
10
10
|
collectStreamChunks,
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
ssrStreamNavQueries,
|
|
35
35
|
streamToString,
|
|
36
36
|
toPrefetchSession
|
|
37
|
-
} from "./shared/chunk-
|
|
37
|
+
} from "./shared/chunk-j9z9r179.js";
|
|
38
38
|
import {
|
|
39
39
|
clearGlobalSSRTimeout,
|
|
40
40
|
createSSRAdapter,
|
|
@@ -171578,26 +171578,39 @@ function generateAotBarrel(compiledFiles, routeMap, appEntry) {
|
|
|
171578
171578
|
"import { __esc, __esc_attr, __ssr_spread, __ssr_style_object } from '@vertz/ui-server';"
|
|
171579
171579
|
];
|
|
171580
171580
|
const files = {};
|
|
171581
|
+
const skippedFns = [];
|
|
171581
171582
|
let fileIndex = 0;
|
|
171582
171583
|
for (const [filePath, fns] of fileToFns) {
|
|
171583
171584
|
const moduleName = basename(filePath, ".tsx").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
171584
171585
|
const tempFileName = `__aot_${fileIndex}_${moduleName}`;
|
|
171585
171586
|
const moduleRef = `./${tempFileName}.ts`;
|
|
171586
|
-
lines.push(`export { ${fns.sort().join(", ")} } from '${moduleRef}';`);
|
|
171587
171587
|
const compiled = compiledFiles[filePath];
|
|
171588
171588
|
if (compiled) {
|
|
171589
171589
|
const helperImport = `import { __esc, __esc_attr, __ssr_spread, __ssr_style_object } from '@vertz/ui-server';
|
|
171590
171590
|
` + `import type { SSRAotContext } from '@vertz/ui-server';
|
|
171591
171591
|
`;
|
|
171592
|
-
const
|
|
171593
|
-
|
|
171592
|
+
const cleanFns = [];
|
|
171593
|
+
for (const fn of fns) {
|
|
171594
|
+
const fnCode = extractSsrFunctions(compiled.code, [fn]);
|
|
171595
|
+
if (hasResidualJsx(fnCode)) {
|
|
171596
|
+
skippedFns.push(fn);
|
|
171597
|
+
} else {
|
|
171598
|
+
cleanFns.push(fn);
|
|
171599
|
+
}
|
|
171600
|
+
}
|
|
171601
|
+
if (cleanFns.length > 0) {
|
|
171602
|
+
const extracted = extractSsrFunctions(compiled.code, cleanFns);
|
|
171603
|
+
lines.push(`export { ${cleanFns.sort().join(", ")} } from '${moduleRef}';`);
|
|
171604
|
+
files[`${tempFileName}.ts`] = helperImport + extracted;
|
|
171605
|
+
}
|
|
171594
171606
|
}
|
|
171595
171607
|
fileIndex++;
|
|
171596
171608
|
}
|
|
171597
171609
|
return {
|
|
171598
171610
|
barrelSource: lines.join(`
|
|
171599
171611
|
`),
|
|
171600
|
-
files
|
|
171612
|
+
files,
|
|
171613
|
+
skippedFns
|
|
171601
171614
|
};
|
|
171602
171615
|
}
|
|
171603
171616
|
function extractSsrFunctions(code, fnNames) {
|
|
@@ -171624,6 +171637,9 @@ function extractSsrFunctions(code, fnNames) {
|
|
|
171624
171637
|
return extracted.join(`
|
|
171625
171638
|
`);
|
|
171626
171639
|
}
|
|
171640
|
+
function hasResidualJsx(code) {
|
|
171641
|
+
return /<[A-Za-z]\w*\s+[\w-]+=\{/.test(code);
|
|
171642
|
+
}
|
|
171627
171643
|
function findAppComponent(components) {
|
|
171628
171644
|
for (const [name, comp] of Object.entries(components)) {
|
|
171629
171645
|
if (comp.tier === "runtime-fallback")
|
package/dist/node-handler.d.ts
CHANGED
|
@@ -261,6 +261,28 @@ interface SSRHandlerOptions {
|
|
|
261
261
|
* (JSON files, third-party APIs, custom DB clients).
|
|
262
262
|
*/
|
|
263
263
|
aotDataResolver?: AotDataResolver;
|
|
264
|
+
/**
|
|
265
|
+
* Derive attributes to set on the `<html>` tag from the incoming request.
|
|
266
|
+
*
|
|
267
|
+
* Useful for setting `data-theme`, `dir`, `lang`, or other attributes that
|
|
268
|
+
* must be on `<html>` to avoid FOUC. The callback runs before SSR rendering
|
|
269
|
+
* so the attributes are available in the first byte of the response.
|
|
270
|
+
*
|
|
271
|
+
* If the template already has an attribute with the same name, the callback's
|
|
272
|
+
* value overrides it. Values are HTML-escaped automatically. Keys must be
|
|
273
|
+
* valid HTML attribute names (`/^[a-zA-Z][a-zA-Z0-9-]*$/`).
|
|
274
|
+
*
|
|
275
|
+
* Return `undefined`, `null`, or `{}` to skip injection.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```ts
|
|
279
|
+
* htmlAttributes: (request) => ({
|
|
280
|
+
* 'data-theme': getThemeCookie(request) ?? 'dark',
|
|
281
|
+
* dir: getDirection(request),
|
|
282
|
+
* })
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
htmlAttributes?: (request: Request) => Record<string, string> | null | undefined;
|
|
264
286
|
}
|
|
265
287
|
type NodeHandlerOptions = SSRHandlerOptions;
|
|
266
288
|
declare function createNodeHandler(options: NodeHandlerOptions): (req: IncomingMessage, res: ServerResponse) => void;
|
package/dist/node-handler.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createNodeHandler
|
|
3
|
-
} from "./shared/chunk-
|
|
4
|
-
import"./shared/chunk-
|
|
3
|
+
} from "./shared/chunk-1hjzk64k.js";
|
|
4
|
+
import"./shared/chunk-j9z9r179.js";
|
|
5
5
|
import"./shared/chunk-szvdd1qq.js";
|
|
6
6
|
import"./shared/chunk-bt1px3c4.js";
|
|
7
7
|
export {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
escapeAttr,
|
|
3
|
+
injectHtmlAttributes,
|
|
3
4
|
injectIntoTemplate,
|
|
4
5
|
precomputeHandlerState,
|
|
5
6
|
resolveRouteModulepreload,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
ssrRenderSinglePass,
|
|
11
12
|
ssrStreamNavQueries,
|
|
12
13
|
toPrefetchSession
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-j9z9r179.js";
|
|
14
15
|
|
|
15
16
|
// src/node-handler.ts
|
|
16
17
|
function createNodeHandler(options) {
|
|
@@ -25,7 +26,8 @@ function createNodeHandler(options) {
|
|
|
25
26
|
manifest,
|
|
26
27
|
progressiveHTML,
|
|
27
28
|
aotManifest,
|
|
28
|
-
aotDataResolver
|
|
29
|
+
aotDataResolver,
|
|
30
|
+
htmlAttributes
|
|
29
31
|
} = options;
|
|
30
32
|
const { template, linkHeader, modulepreloadTags, splitResult } = precomputeHandlerState(options);
|
|
31
33
|
const useProgressive = progressiveHTML && splitResult && !(manifest?.routeEntries && Object.keys(manifest.routeEntries).length > 0);
|
|
@@ -38,20 +40,37 @@ function createNodeHandler(options) {
|
|
|
38
40
|
await handleNavRequest(req, res, module, pathname, ssrTimeout);
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
|
-
let
|
|
42
|
-
|
|
43
|
-
if (sessionResolver) {
|
|
43
|
+
let webRequest;
|
|
44
|
+
if (sessionResolver || htmlAttributes) {
|
|
44
45
|
const fullUrl = `http://${req.headers.host ?? "localhost"}${url}`;
|
|
45
|
-
|
|
46
|
+
webRequest = new Request(fullUrl, {
|
|
46
47
|
method: req.method ?? "GET",
|
|
47
48
|
headers: req.headers
|
|
48
49
|
});
|
|
50
|
+
}
|
|
51
|
+
let sessionScript = "";
|
|
52
|
+
let ssrAuth;
|
|
53
|
+
if (sessionResolver && webRequest) {
|
|
49
54
|
const result2 = await resolveSession(webRequest, sessionResolver, nonce);
|
|
50
55
|
sessionScript = result2.sessionScript;
|
|
51
56
|
ssrAuth = result2.ssrAuth;
|
|
52
57
|
}
|
|
58
|
+
let requestTemplate = template;
|
|
59
|
+
let requestHeadTemplate = splitResult?.headTemplate;
|
|
60
|
+
if (htmlAttributes && webRequest) {
|
|
61
|
+
const attrs = htmlAttributes(webRequest);
|
|
62
|
+
if (attrs && Object.keys(attrs).length > 0) {
|
|
63
|
+
requestTemplate = injectHtmlAttributes(template, attrs);
|
|
64
|
+
if (requestHeadTemplate) {
|
|
65
|
+
requestHeadTemplate = injectHtmlAttributes(requestHeadTemplate, attrs);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
53
69
|
if (useProgressive) {
|
|
54
|
-
await handleProgressiveRequest(req, res, module,
|
|
70
|
+
await handleProgressiveRequest(req, res, module, {
|
|
71
|
+
headTemplate: requestHeadTemplate ?? splitResult.headTemplate,
|
|
72
|
+
tailTemplate: splitResult.tailTemplate
|
|
73
|
+
}, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest);
|
|
55
74
|
return;
|
|
56
75
|
}
|
|
57
76
|
const prefetchSession = ssrAuth ? toPrefetchSession(ssrAuth) : undefined;
|
|
@@ -79,7 +98,7 @@ function createNodeHandler(options) {
|
|
|
79
98
|
const allHeadTags = [result.headTags, resolvedModulepreloadTags].filter(Boolean).join(`
|
|
80
99
|
`);
|
|
81
100
|
const html = injectIntoTemplate({
|
|
82
|
-
template,
|
|
101
|
+
template: requestTemplate,
|
|
83
102
|
appHtml: result.html,
|
|
84
103
|
appCss: result.css,
|
|
85
104
|
ssrData: result.ssrData,
|
|
@@ -94,7 +113,8 @@ function createNodeHandler(options) {
|
|
|
94
113
|
headers.Link = linkHeader;
|
|
95
114
|
if (cacheControl)
|
|
96
115
|
headers["Cache-Control"] = cacheControl;
|
|
97
|
-
|
|
116
|
+
const status = result.matchedRoutePatterns?.length ? 200 : 404;
|
|
117
|
+
res.writeHead(status, headers);
|
|
98
118
|
res.end(html);
|
|
99
119
|
} catch (err) {
|
|
100
120
|
console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
|
|
@@ -153,7 +173,8 @@ async function handleProgressiveRequest(req, res, module, split, url, ssrTimeout
|
|
|
153
173
|
headers.Link = linkHeader;
|
|
154
174
|
if (cacheControl)
|
|
155
175
|
headers["Cache-Control"] = cacheControl;
|
|
156
|
-
|
|
176
|
+
const status = result.matchedRoutePatterns?.length ? 200 : 404;
|
|
177
|
+
res.writeHead(status, headers);
|
|
157
178
|
res.write(headChunk);
|
|
158
179
|
let clientDisconnected = false;
|
|
159
180
|
req.on("close", () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
encodeChunk,
|
|
3
3
|
escapeAttr,
|
|
4
|
+
injectHtmlAttributes,
|
|
4
5
|
injectIntoTemplate,
|
|
5
6
|
precomputeHandlerState,
|
|
6
7
|
resolveRouteModulepreload,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
ssrRenderSinglePass,
|
|
12
13
|
ssrStreamNavQueries,
|
|
13
14
|
toPrefetchSession
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-j9z9r179.js";
|
|
15
16
|
|
|
16
17
|
// src/aot-manifest-loader.ts
|
|
17
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -108,7 +109,7 @@ function buildProgressiveResponse(options) {
|
|
|
108
109
|
"Content-Type": "text/html; charset=utf-8",
|
|
109
110
|
...headers
|
|
110
111
|
};
|
|
111
|
-
return new Response(stream, { status: 200, headers: responseHeaders });
|
|
112
|
+
return new Response(stream, { status: options.status ?? 200, headers: responseHeaders });
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
// src/ssr-handler.ts
|
|
@@ -124,7 +125,8 @@ function createSSRHandler(options) {
|
|
|
124
125
|
manifest,
|
|
125
126
|
progressiveHTML,
|
|
126
127
|
aotManifest,
|
|
127
|
-
aotDataResolver
|
|
128
|
+
aotDataResolver,
|
|
129
|
+
htmlAttributes
|
|
128
130
|
} = options;
|
|
129
131
|
const { template, linkHeader, modulepreloadTags, splitResult } = precomputeHandlerState(options);
|
|
130
132
|
return async (request) => {
|
|
@@ -141,11 +143,33 @@ function createSSRHandler(options) {
|
|
|
141
143
|
ssrAuth = result.ssrAuth;
|
|
142
144
|
}
|
|
143
145
|
const cookies = request.headers.get("Cookie") ?? undefined;
|
|
146
|
+
let requestTemplate = template;
|
|
147
|
+
let requestHeadTemplate = splitResult?.headTemplate;
|
|
148
|
+
if (htmlAttributes) {
|
|
149
|
+
try {
|
|
150
|
+
const attrs = htmlAttributes(request);
|
|
151
|
+
if (attrs && Object.keys(attrs).length > 0) {
|
|
152
|
+
requestTemplate = injectHtmlAttributes(template, attrs);
|
|
153
|
+
if (requestHeadTemplate) {
|
|
154
|
+
requestHeadTemplate = injectHtmlAttributes(requestHeadTemplate, attrs);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error("[SSR] htmlAttributes failed:", err instanceof Error ? err.message : err);
|
|
159
|
+
return new Response("Internal Server Error", {
|
|
160
|
+
status: 500,
|
|
161
|
+
headers: { "Content-Type": "text/plain" }
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
144
165
|
const useProgressive = progressiveHTML && splitResult && !(manifest?.routeEntries && Object.keys(manifest.routeEntries).length > 0);
|
|
145
166
|
if (useProgressive) {
|
|
146
|
-
return handleProgressiveHTMLRequest(module,
|
|
167
|
+
return handleProgressiveHTMLRequest(module, {
|
|
168
|
+
headTemplate: requestHeadTemplate ?? splitResult.headTemplate,
|
|
169
|
+
tailTemplate: splitResult.tailTemplate
|
|
170
|
+
}, pathname + url.search, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest, cookies);
|
|
147
171
|
}
|
|
148
|
-
return handleHTMLRequest(module,
|
|
172
|
+
return handleHTMLRequest(module, requestTemplate, pathname + url.search, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest, aotManifest, aotDataResolver, cookies);
|
|
149
173
|
};
|
|
150
174
|
}
|
|
151
175
|
async function handleNavRequest(module, url, ssrTimeout) {
|
|
@@ -219,13 +243,15 @@ async function handleProgressiveHTMLRequest(module, split, url, ssrTimeout, nonc
|
|
|
219
243
|
headers.Link = linkHeader;
|
|
220
244
|
if (cacheControl)
|
|
221
245
|
headers["Cache-Control"] = cacheControl;
|
|
246
|
+
const status = result.matchedRoutePatterns?.length ? 200 : 404;
|
|
222
247
|
return buildProgressiveResponse({
|
|
223
248
|
headChunk,
|
|
224
249
|
renderStream: result.renderStream,
|
|
225
250
|
tailChunk: split.tailTemplate,
|
|
226
251
|
ssrData: result.ssrData,
|
|
227
252
|
nonce,
|
|
228
|
-
headers
|
|
253
|
+
headers,
|
|
254
|
+
status
|
|
229
255
|
});
|
|
230
256
|
} catch (err) {
|
|
231
257
|
console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
|
|
@@ -278,7 +304,8 @@ async function handleHTMLRequest(module, template, url, ssrTimeout, nonce, fallb
|
|
|
278
304
|
headers.Link = linkHeader;
|
|
279
305
|
if (cacheControl)
|
|
280
306
|
headers["Cache-Control"] = cacheControl;
|
|
281
|
-
|
|
307
|
+
const status = result.matchedRoutePatterns?.length ? 200 : 404;
|
|
308
|
+
return new Response(html, { status, headers });
|
|
282
309
|
} catch (err) {
|
|
283
310
|
console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
|
|
284
311
|
return new Response("Internal Server Error", {
|
|
@@ -1050,9 +1050,10 @@ async function ssrRenderAot(module, url, options) {
|
|
|
1050
1050
|
try {
|
|
1051
1051
|
setGlobalSSRTimeout(ssrTimeout);
|
|
1052
1052
|
const holes = createHoles(aotEntry.holes, module, normalizedUrl, queryCache, options.ssrAuth);
|
|
1053
|
+
const resolveKey = (key) => key.includes("${") ? resolveParamQueryKeys([key], match.params, searchParams)[0] : key;
|
|
1053
1054
|
const ctx = {
|
|
1054
1055
|
holes,
|
|
1055
|
-
getData: (key) => queryCache.get(key),
|
|
1056
|
+
getData: (key) => queryCache.get(resolveKey(key)),
|
|
1056
1057
|
session: options.prefetchSession,
|
|
1057
1058
|
params: match.params,
|
|
1058
1059
|
searchParams
|
|
@@ -1075,7 +1076,7 @@ async function ssrRenderAot(module, url, options) {
|
|
|
1075
1076
|
appHoles.RouterView = () => html;
|
|
1076
1077
|
const appCtx = {
|
|
1077
1078
|
holes: appHoles,
|
|
1078
|
-
getData: (key) => queryCache.get(key),
|
|
1079
|
+
getData: (key) => queryCache.get(resolveKey(key)),
|
|
1079
1080
|
session: options.prefetchSession,
|
|
1080
1081
|
params: match.params,
|
|
1081
1082
|
searchParams
|
|
@@ -1158,7 +1159,7 @@ function resolveParamQueryKeys(queryKeys, params, searchParams) {
|
|
|
1158
1159
|
return value || defaultVal;
|
|
1159
1160
|
}
|
|
1160
1161
|
return value ?? "";
|
|
1161
|
-
}).replace(/\$\{(\w+)\}/g, (_, paramName) => params[paramName] ?? ""));
|
|
1162
|
+
}).replace(/\$\{(\w+)\}/g, (_, paramName) => params[paramName] ?? searchParams?.get(paramName) ?? ""));
|
|
1162
1163
|
}
|
|
1163
1164
|
function extractSearchParams(url) {
|
|
1164
1165
|
const qIdx = url.indexOf("?");
|
|
@@ -1461,6 +1462,39 @@ async function resolveSession(request, sessionResolver, nonce) {
|
|
|
1461
1462
|
}
|
|
1462
1463
|
|
|
1463
1464
|
// src/template-inject.ts
|
|
1465
|
+
var VALID_ATTR_KEY = /^[a-zA-Z][a-zA-Z0-9-]*$/;
|
|
1466
|
+
function injectHtmlAttributes(template, attrs) {
|
|
1467
|
+
const entries = Object.entries(attrs);
|
|
1468
|
+
if (entries.length === 0)
|
|
1469
|
+
return template;
|
|
1470
|
+
for (const [key] of entries) {
|
|
1471
|
+
if (!VALID_ATTR_KEY.test(key)) {
|
|
1472
|
+
throw new Error(`Invalid HTML attribute key: "${key}"`);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
const htmlTagMatch = template.match(/<html(\s[^>]*)?>|<html>/i);
|
|
1476
|
+
if (!htmlTagMatch || htmlTagMatch.index == null)
|
|
1477
|
+
return template;
|
|
1478
|
+
const existingAttrsStr = htmlTagMatch[1] ?? "";
|
|
1479
|
+
const existingAttrs = parseHtmlTagAttrs(existingAttrsStr);
|
|
1480
|
+
const merged = { ...existingAttrs };
|
|
1481
|
+
for (const [key, value] of entries) {
|
|
1482
|
+
merged[key] = escapeAttr(value);
|
|
1483
|
+
}
|
|
1484
|
+
const attrStr = Object.entries(merged).map(([k, v]) => v === "" && !(k in attrs) ? ` ${k}` : ` ${k}="${v}"`).join("");
|
|
1485
|
+
const originalTag = htmlTagMatch[0].match(/^<([a-zA-Z]+)/i)[1];
|
|
1486
|
+
const tagEnd = htmlTagMatch.index + htmlTagMatch[0].length;
|
|
1487
|
+
return template.slice(0, htmlTagMatch.index) + `<${originalTag}${attrStr}>` + template.slice(tagEnd);
|
|
1488
|
+
}
|
|
1489
|
+
function parseHtmlTagAttrs(attrStr) {
|
|
1490
|
+
const attrs = {};
|
|
1491
|
+
const re = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*"([^"]*)"|(?:\s*=\s*'([^']*)'))?/g;
|
|
1492
|
+
let m;
|
|
1493
|
+
while ((m = re.exec(attrStr)) !== null) {
|
|
1494
|
+
attrs[m[1]] = m[2] ?? m[3] ?? "";
|
|
1495
|
+
}
|
|
1496
|
+
return attrs;
|
|
1497
|
+
}
|
|
1464
1498
|
function injectIntoTemplate(options) {
|
|
1465
1499
|
const { template, appHtml, appCss, ssrData, nonce, headTags, sessionScript } = options;
|
|
1466
1500
|
let html;
|
|
@@ -1523,4 +1557,4 @@ function replaceAppDivContent(template, appHtml) {
|
|
|
1523
1557
|
return template.slice(0, contentStart) + appHtml + template.slice(i);
|
|
1524
1558
|
}
|
|
1525
1559
|
|
|
1526
|
-
export { escapeHtml, escapeAttr, serializeToHtml, toPrefetchSession, evaluateAccessRule, reconstructDescriptors, compileThemeCached, createRequestContext, matchUrlToPatterns, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, ssrRenderSinglePass, ssrRenderProgressive, ssrStreamNavQueries, createHoles, ssrRenderAot, isAotDebugEnabled, clearRouteCssCache, getAccessSetForSSR, createAccessSetScript, createSessionScript, resolveRouteModulepreload, precomputeHandlerState, resolveSession, injectIntoTemplate };
|
|
1560
|
+
export { escapeHtml, escapeAttr, serializeToHtml, toPrefetchSession, evaluateAccessRule, reconstructDescriptors, compileThemeCached, createRequestContext, matchUrlToPatterns, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, ssrRenderSinglePass, ssrRenderProgressive, ssrStreamNavQueries, createHoles, ssrRenderAot, isAotDebugEnabled, clearRouteCssCache, getAccessSetForSSR, createAccessSetScript, createSessionScript, resolveRouteModulepreload, precomputeHandlerState, resolveSession, injectHtmlAttributes, injectIntoTemplate };
|
package/dist/ssr/index.d.ts
CHANGED
|
@@ -395,6 +395,28 @@ interface SSRHandlerOptions {
|
|
|
395
395
|
* (JSON files, third-party APIs, custom DB clients).
|
|
396
396
|
*/
|
|
397
397
|
aotDataResolver?: AotDataResolver;
|
|
398
|
+
/**
|
|
399
|
+
* Derive attributes to set on the `<html>` tag from the incoming request.
|
|
400
|
+
*
|
|
401
|
+
* Useful for setting `data-theme`, `dir`, `lang`, or other attributes that
|
|
402
|
+
* must be on `<html>` to avoid FOUC. The callback runs before SSR rendering
|
|
403
|
+
* so the attributes are available in the first byte of the response.
|
|
404
|
+
*
|
|
405
|
+
* If the template already has an attribute with the same name, the callback's
|
|
406
|
+
* value overrides it. Values are HTML-escaped automatically. Keys must be
|
|
407
|
+
* valid HTML attribute names (`/^[a-zA-Z][a-zA-Z0-9-]*$/`).
|
|
408
|
+
*
|
|
409
|
+
* Return `undefined`, `null`, or `{}` to skip injection.
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* ```ts
|
|
413
|
+
* htmlAttributes: (request) => ({
|
|
414
|
+
* 'data-theme': getThemeCookie(request) ?? 'dark',
|
|
415
|
+
* dir: getDirection(request),
|
|
416
|
+
* })
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
htmlAttributes?: (request: Request) => Record<string, string> | null | undefined;
|
|
398
420
|
}
|
|
399
421
|
declare function createSSRHandler(options: SSRHandlerOptions): (request: Request) => Promise<Response>;
|
|
400
422
|
interface InjectIntoTemplateOptions {
|
package/dist/ssr/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSSRHandler,
|
|
3
3
|
loadAotManifest
|
|
4
|
-
} from "../shared/chunk-
|
|
4
|
+
} from "../shared/chunk-91eg6dps.js";
|
|
5
5
|
import {
|
|
6
6
|
injectIntoTemplate,
|
|
7
7
|
ssrRenderSinglePass,
|
|
8
8
|
ssrStreamNavQueries
|
|
9
|
-
} from "../shared/chunk-
|
|
9
|
+
} from "../shared/chunk-j9z9r179.js";
|
|
10
10
|
import"../shared/chunk-szvdd1qq.js";
|
|
11
11
|
import"../shared/chunk-bt1px3c4.js";
|
|
12
12
|
// src/prerender.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/ui-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.49",
|
|
4
4
|
"description": "Vertz UI server-side rendering runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
"import": "./dist/bun-plugin/fast-refresh-runtime.js",
|
|
40
40
|
"types": "./dist/bun-plugin/fast-refresh-runtime.d.ts"
|
|
41
41
|
},
|
|
42
|
+
"./state-inspector": {
|
|
43
|
+
"import": "./dist/bun-plugin/state-inspector.js",
|
|
44
|
+
"types": "./dist/bun-plugin/state-inspector.d.ts"
|
|
45
|
+
},
|
|
42
46
|
"./bun-dev-server": {
|
|
43
47
|
"import": "./dist/bun-dev-server.js",
|
|
44
48
|
"types": "./dist/bun-dev-server.d.ts"
|
|
@@ -57,7 +61,7 @@
|
|
|
57
61
|
"provenance": true
|
|
58
62
|
},
|
|
59
63
|
"scripts": {
|
|
60
|
-
"build": "bunup",
|
|
64
|
+
"build": "vtzx bunup",
|
|
61
65
|
"test": "bun test --timeout 60000 src/",
|
|
62
66
|
"test:integration": "bun test src/__tests__/bun-dev-server.integration.local.ts",
|
|
63
67
|
"test:e2e": "bunx playwright test",
|
|
@@ -67,15 +71,15 @@
|
|
|
67
71
|
"@ampproject/remapping": "^2.3.0",
|
|
68
72
|
"@capsizecss/unpack": "^4.0.0",
|
|
69
73
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
70
|
-
"@vertz/core": "^0.2.
|
|
71
|
-
"@vertz/ui": "^0.2.
|
|
74
|
+
"@vertz/core": "^0.2.48",
|
|
75
|
+
"@vertz/ui": "^0.2.48",
|
|
72
76
|
"magic-string": "^0.30.0",
|
|
73
77
|
"sharp": "^0.34.5"
|
|
74
78
|
},
|
|
75
79
|
"devDependencies": {
|
|
76
80
|
"@happy-dom/global-registrator": "^20.8.3",
|
|
77
81
|
"@playwright/test": "^1.58.2",
|
|
78
|
-
"@vertz/codegen": "^0.2.
|
|
82
|
+
"@vertz/codegen": "^0.2.48",
|
|
79
83
|
"bun-types": "^1.3.10",
|
|
80
84
|
"bunup": "^0.16.31",
|
|
81
85
|
"happy-dom": "^20.8.3",
|