streetui 1.9.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- export { R as RenderToStringOptions, S as STATE_MARKER_ATTR, a as ServerDOMAdapter, r as readState, b as renderToString, s as serializeState } from './server-DnwqBTfE.cjs';
2
- export { V as VERSION } from './compile-DqsMQVJk.cjs';
1
+ export { c as RenderToStringOptions, S as STATE_MARKER_ATTR, d as ServerDOMAdapter, r as readState, f as renderHead, g as renderToString, s as serializeState } from './server-DnAOFpMP.cjs';
2
+ export { V as VERSION } from './hydration-diagnostics-CFYDXBlm.cjs';
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { R as RenderToStringOptions, S as STATE_MARKER_ATTR, a as ServerDOMAdapter, r as readState, b as renderToString, s as serializeState } from './server-BYnmb_hR.js';
2
- export { V as VERSION } from './compile-DqsMQVJk.js';
1
+ export { c as RenderToStringOptions, S as STATE_MARKER_ATTR, d as ServerDOMAdapter, r as readState, f as renderHead, g as renderToString, s as serializeState } from './server-C0PvFBzD.js';
2
+ export { V as VERSION } from './hydration-diagnostics-CFYDXBlm.js';
package/dist/server.js CHANGED
@@ -500,6 +500,9 @@ var ServerDOMAdapter = class {
500
500
  body() {
501
501
  return null;
502
502
  }
503
+ head() {
504
+ return null;
505
+ }
503
506
  activeElement() {
504
507
  return null;
505
508
  }
@@ -691,7 +694,13 @@ var TAG_MAP = {
691
694
  // its children are relocated to a document.body container on the browser
692
695
  // (see the portal branch in mount.ts). On the server (no body) it renders
693
696
  // inline, so the anchor tag is what SSR/hydration positionally match on.
694
- portal: "div"
697
+ portal: "div",
698
+ // A `head()` node renders as a neutral, empty inline anchor <div> at its
699
+ // declaration site (like a portal anchor). Its actual contribution — title/
700
+ // meta/link/etc. — is applied to `document.head` by `wireHeadBehavior` on the
701
+ // browser, and emitted separately by `renderHead()` on the server. Keeping a
702
+ // one-node/one-element anchor preserves positional hydration.
703
+ head: "div"
695
704
  };
696
705
  function resolveTag(type) {
697
706
  return TAG_MAP[type] ?? "div";
@@ -1136,6 +1145,184 @@ var TransitionController = class {
1136
1145
  }
1137
1146
  };
1138
1147
 
1148
+ // ../renderer/src/head.ts
1149
+ var HEAD_MARKER = "data-streetui-head";
1150
+ var HEAD_KEY = "data-streetui-head-key";
1151
+ function isSignalLike(v) {
1152
+ return v !== null && typeof v === "object" && typeof v["subscribe"] === "function" && typeof v["peek"] === "function";
1153
+ }
1154
+ function readValue(v) {
1155
+ if (isSignalLike(v)) return String(v.peek() ?? "");
1156
+ return String(v ?? "");
1157
+ }
1158
+ function resolveEntry(entry) {
1159
+ const attrs = {};
1160
+ for (const key of Object.keys(entry.attrs)) {
1161
+ attrs[key] = readValue(entry.attrs[key]);
1162
+ }
1163
+ const resolved = { tag: entry.tag, attrs };
1164
+ if (entry.tag === "title") {
1165
+ return { ...resolved, text: readValue(entry.text) };
1166
+ }
1167
+ return resolved;
1168
+ }
1169
+ var HeadManager = class {
1170
+ _dom;
1171
+ _head;
1172
+ _contributions = /* @__PURE__ */ new Map();
1173
+ _applied = /* @__PURE__ */ new Map();
1174
+ _order = 0;
1175
+ _adopted = false;
1176
+ constructor(dom, head) {
1177
+ this._dom = dom;
1178
+ this._head = head;
1179
+ }
1180
+ /** Register (or replace) a node's contribution and re-apply the merged result. */
1181
+ register(nodeId, entries) {
1182
+ this._contributions.set(nodeId, { order: this._order++, entries });
1183
+ this.apply();
1184
+ }
1185
+ /** Withdraw a node's contribution (unmount / route change) and re-apply. */
1186
+ unregister(nodeId) {
1187
+ if (this._contributions.delete(nodeId)) this.apply();
1188
+ }
1189
+ /** Recompute the merged head and patch `document.head` to match. */
1190
+ apply() {
1191
+ if (!this._adopted) {
1192
+ this._adoptServerTags();
1193
+ this._adopted = true;
1194
+ }
1195
+ const ordered = [...this._contributions.values()].sort((a, b) => a.order - b.order);
1196
+ const merged = /* @__PURE__ */ new Map();
1197
+ for (const contribution of ordered) {
1198
+ for (const entry of contribution.entries) {
1199
+ merged.set(entry.dedupKey, resolveEntry(entry));
1200
+ }
1201
+ }
1202
+ for (const [key, desired] of merged) {
1203
+ const existing = this._applied.get(key);
1204
+ if (existing !== void 0 && existing.tag === desired.tag) {
1205
+ this._reconcileAttrs(existing, desired);
1206
+ } else {
1207
+ if (existing !== void 0) {
1208
+ this._dom.removeChild(this._head, existing.el);
1209
+ this._applied.delete(key);
1210
+ }
1211
+ const el = this._createTag(key, desired);
1212
+ this._dom.appendChild(this._head, el);
1213
+ this._applied.set(key, { el, attrKeys: new Set(Object.keys(desired.attrs)), tag: desired.tag });
1214
+ }
1215
+ }
1216
+ for (const [key, record] of [...this._applied]) {
1217
+ if (!merged.has(key)) {
1218
+ this._dom.removeChild(this._head, record.el);
1219
+ this._applied.delete(key);
1220
+ }
1221
+ }
1222
+ }
1223
+ _createTag(key, desired) {
1224
+ const el = this._dom.createElement(desired.tag);
1225
+ this._dom.setAttribute(el, HEAD_MARKER, "");
1226
+ this._dom.setAttribute(el, HEAD_KEY, key);
1227
+ for (const attr of Object.keys(desired.attrs)) {
1228
+ this._dom.setAttribute(el, attr, desired.attrs[attr]);
1229
+ }
1230
+ if (desired.tag === "title") this._dom.setTextContent(el, desired.text ?? "");
1231
+ return el;
1232
+ }
1233
+ _reconcileAttrs(record, desired) {
1234
+ const nextKeys = new Set(Object.keys(desired.attrs));
1235
+ for (const attr of record.attrKeys) {
1236
+ if (!nextKeys.has(attr)) this._dom.removeAttribute(record.el, attr);
1237
+ }
1238
+ for (const attr of nextKeys) {
1239
+ this._dom.setAttribute(record.el, attr, desired.attrs[attr]);
1240
+ }
1241
+ if (desired.tag === "title") this._dom.setTextContent(record.el, desired.text ?? "");
1242
+ record.attrKeys = nextKeys;
1243
+ }
1244
+ /**
1245
+ * Seed `_applied` from server-emitted `[data-streetui-head-key]` tags already
1246
+ * in `document.head`. The subsequent diff reuses these elements when the
1247
+ * client desires the same key (no duplicate), rewrites them if the value
1248
+ * changed, or removes them if the client graph no longer wants them.
1249
+ */
1250
+ _adoptServerTags() {
1251
+ for (const child of this._dom.childNodes(this._head)) {
1252
+ if (!this._dom.isElement(child)) continue;
1253
+ const el = child;
1254
+ const key = this._dom.getAttribute(el, HEAD_KEY);
1255
+ if (key === null) continue;
1256
+ this._applied.set(key, {
1257
+ el,
1258
+ attrKeys: new Set(this._attrNames(el)),
1259
+ tag: this._dom.tagName(el)
1260
+ });
1261
+ }
1262
+ }
1263
+ /** The framework-managed attribute names currently on a server tag. */
1264
+ _attrNames(el) {
1265
+ const names = [];
1266
+ if (this._dom.getAttribute(el, HEAD_MARKER) !== null) names.push(HEAD_MARKER);
1267
+ if (this._dom.getAttribute(el, HEAD_KEY) !== null) names.push(HEAD_KEY);
1268
+ return names;
1269
+ }
1270
+ };
1271
+ function getHeadManager(ctx) {
1272
+ const head = ctx.dom.head();
1273
+ if (head === null) return null;
1274
+ const mutable = ctx;
1275
+ if (mutable.head === void 0) mutable.head = new HeadManager(ctx.dom, head);
1276
+ return mutable.head;
1277
+ }
1278
+ function wireHeadBehavior(ctx, graphNode, instance) {
1279
+ const manager = getHeadManager(ctx);
1280
+ if (manager === null) return;
1281
+ const descFn = ctx.graph.getHandler(`__head__${graphNode.id}`);
1282
+ if (descFn === void 0) return;
1283
+ const contribution = descFn();
1284
+ const nodeId = graphNode.id;
1285
+ manager.register(nodeId, contribution.entries);
1286
+ for (const entry of contribution.entries) {
1287
+ for (const attrKey of Object.keys(entry.attrs)) {
1288
+ const v = entry.attrs[attrKey];
1289
+ if (isSignalLike(v)) {
1290
+ instance.trackCleanup(v.subscribe(() => manager.apply()));
1291
+ }
1292
+ }
1293
+ if (isSignalLike(entry.text)) {
1294
+ instance.trackCleanup(entry.text.subscribe(() => manager.apply()));
1295
+ }
1296
+ }
1297
+ instance.trackCleanup(() => manager.unregister(nodeId));
1298
+ }
1299
+ function renderHead(compiled) {
1300
+ const graph = compiled.graph;
1301
+ const merged = /* @__PURE__ */ new Map();
1302
+ graph.walk((node) => {
1303
+ if (node.type !== "head") return;
1304
+ const descFn = graph.getHandler(`__head__${node.id}`);
1305
+ if (descFn === void 0) return;
1306
+ for (const entry of descFn().entries) {
1307
+ merged.set(entry.dedupKey, resolveEntry(entry));
1308
+ }
1309
+ });
1310
+ if (merged.size === 0) return "";
1311
+ const dom = new ServerDOMAdapter();
1312
+ let out = "";
1313
+ for (const [key, desired] of merged) {
1314
+ const el = dom.createElement(desired.tag);
1315
+ dom.setAttribute(el, HEAD_MARKER, "");
1316
+ dom.setAttribute(el, HEAD_KEY, key);
1317
+ for (const attr of Object.keys(desired.attrs)) {
1318
+ dom.setAttribute(el, attr, desired.attrs[attr]);
1319
+ }
1320
+ if (desired.tag === "title") dom.setTextContent(el, desired.text ?? "");
1321
+ out += dom.serializeOuter(el);
1322
+ }
1323
+ return out;
1324
+ }
1325
+
1139
1326
  // ../renderer/src/mount.ts
1140
1327
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
1141
1328
  "text",
@@ -1258,6 +1445,9 @@ function mountNode(ctx, graphNode, parentDom) {
1258
1445
  const instance2 = new NodeInstance(graphNode, el2);
1259
1446
  ctx.instances.set(graphNode.id, instance2);
1260
1447
  wireEvents(dom, graph, graphNode, el2, instance2);
1448
+ if (graphNode.stateRefs.length !== 0) {
1449
+ wireSignalBindings(ctx, graphNode, instance2, linkUpdate(dom, el2));
1450
+ }
1261
1451
  dom.appendChild(parentDom, el2);
1262
1452
  return instance2;
1263
1453
  }
@@ -1313,6 +1503,16 @@ function mountNode(ctx, graphNode, parentDom) {
1313
1503
  wireOverlayBehavior(ctx, graphNode, instance2, target);
1314
1504
  return instance2;
1315
1505
  }
1506
+ if (graphNode.type === "head") {
1507
+ const anchor = dom.createElement(resolveTag("head"));
1508
+ dom.setAttribute(anchor, "data-streetui-head-anchor", "");
1509
+ applyNodeProps(ctx, graphNode, anchor);
1510
+ const instance2 = new NodeInstance(graphNode, anchor);
1511
+ ctx.instances.set(graphNode.id, instance2);
1512
+ dom.appendChild(parentDom, anchor);
1513
+ wireHeadBehavior(ctx, graphNode, instance2);
1514
+ return instance2;
1515
+ }
1316
1516
  const tag = resolveTag(graphNode.type);
1317
1517
  const el = dom.createElement(tag);
1318
1518
  applyNodeProps(ctx, graphNode, el);
@@ -1377,6 +1577,17 @@ function buttonUpdate(dom, el) {
1377
1577
  }
1378
1578
  };
1379
1579
  }
1580
+ function linkUpdate(dom, el) {
1581
+ return (propKey, value) => {
1582
+ if (propKey === "label") {
1583
+ dom.setTextContent(el, String(value ?? ""));
1584
+ } else if (propKey === "href") {
1585
+ dom.setAttribute(el, "href", String(value ?? ""));
1586
+ } else {
1587
+ applyProp(dom, el, propKey, value);
1588
+ }
1589
+ };
1590
+ }
1380
1591
  function applyNodeProps(ctx, graphNode, el) {
1381
1592
  const props = graphNode.props;
1382
1593
  for (const key in props) {
@@ -1588,7 +1799,8 @@ function analyzeGraph(graph) {
1588
1799
  const isConditional = node.type === "conditional";
1589
1800
  const isPortal = node.type === "portal";
1590
1801
  const isComponent = node.type === "component";
1591
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
1802
+ const isHead = node.type === "head";
1803
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent && !isHead;
1592
1804
  const isStaticSubtree = isStatic && allChildrenStatic;
1593
1805
  nodes.set(node.id, {
1594
1806
  isStatic,
@@ -1675,12 +1887,13 @@ function renderToString(compiled, options = {}) {
1675
1887
  }
1676
1888
 
1677
1889
  // src/version.ts
1678
- var VERSION = "1.9.0";
1890
+ var VERSION = "2.0.0";
1679
1891
  export {
1680
1892
  STATE_MARKER_ATTR,
1681
1893
  ServerDOMAdapter,
1682
1894
  VERSION,
1683
1895
  readState,
1896
+ renderHead,
1684
1897
  renderToString,
1685
1898
  serializeState
1686
1899
  };