@aglyn/tenant-runtime 1.0.0-beta.143

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.
Files changed (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +26 -0
  3. package/package.json +39 -0
  4. package/src/index.d.ts +21 -0
  5. package/src/index.js +22 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/apply-publish-schedule.d.ts +34 -0
  8. package/src/lib/apply-publish-schedule.js +285 -0
  9. package/src/lib/apply-publish-schedule.js.map +1 -0
  10. package/src/lib/assign-contact-owner.d.ts +162 -0
  11. package/src/lib/assign-contact-owner.js +322 -0
  12. package/src/lib/assign-contact-owner.js.map +1 -0
  13. package/src/lib/associate-company-by-domain.d.ts +70 -0
  14. package/src/lib/associate-company-by-domain.js +150 -0
  15. package/src/lib/associate-company-by-domain.js.map +1 -0
  16. package/src/lib/author-page-nodes.d.ts +36 -0
  17. package/src/lib/author-page-nodes.js +296 -0
  18. package/src/lib/author-page-nodes.js.map +1 -0
  19. package/src/lib/built-in-page-layout.d.ts +48 -0
  20. package/src/lib/built-in-page-layout.js +107 -0
  21. package/src/lib/built-in-page-layout.js.map +1 -0
  22. package/src/lib/capture-host-contact.d.ts +95 -0
  23. package/src/lib/capture-host-contact.js +137 -0
  24. package/src/lib/capture-host-contact.js.map +1 -0
  25. package/src/lib/collection-fallback-nodes.d.ts +90 -0
  26. package/src/lib/collection-fallback-nodes.js +602 -0
  27. package/src/lib/collection-fallback-nodes.js.map +1 -0
  28. package/src/lib/compose-author-page.d.ts +109 -0
  29. package/src/lib/compose-author-page.js +225 -0
  30. package/src/lib/compose-author-page.js.map +1 -0
  31. package/src/lib/compose-collection-page.d.ts +94 -0
  32. package/src/lib/compose-collection-page.js +256 -0
  33. package/src/lib/compose-collection-page.js.map +1 -0
  34. package/src/lib/compose-screen-nodes.d.ts +156 -0
  35. package/src/lib/compose-screen-nodes.js +547 -0
  36. package/src/lib/compose-screen-nodes.js.map +1 -0
  37. package/src/lib/compose-search-page.d.ts +39 -0
  38. package/src/lib/compose-search-page.js +57 -0
  39. package/src/lib/compose-search-page.js.map +1 -0
  40. package/src/lib/convert-host-lead.d.ts +189 -0
  41. package/src/lib/convert-host-lead.js +443 -0
  42. package/src/lib/convert-host-lead.js.map +1 -0
  43. package/src/lib/defer-lazy-panels.d.ts +58 -0
  44. package/src/lib/defer-lazy-panels.js +171 -0
  45. package/src/lib/defer-lazy-panels.js.map +1 -0
  46. package/src/lib/element-animation-assets.d.ts +62 -0
  47. package/src/lib/element-animation-assets.js +290 -0
  48. package/src/lib/element-animation-assets.js.map +1 -0
  49. package/src/lib/emit-host-event.d.ts +29 -0
  50. package/src/lib/emit-host-event.js +30 -0
  51. package/src/lib/emit-host-event.js.map +1 -0
  52. package/src/lib/enrich-gated-page.d.ts +67 -0
  53. package/src/lib/enrich-gated-page.js +134 -0
  54. package/src/lib/enrich-gated-page.js.map +1 -0
  55. package/src/lib/entry-link-routes.d.ts +61 -0
  56. package/src/lib/entry-link-routes.js +169 -0
  57. package/src/lib/entry-link-routes.js.map +1 -0
  58. package/src/lib/get-author-content.d.ts +119 -0
  59. package/src/lib/get-author-content.js +288 -0
  60. package/src/lib/get-author-content.js.map +1 -0
  61. package/src/lib/get-collection-content.d.ts +290 -0
  62. package/src/lib/get-collection-content.js +565 -0
  63. package/src/lib/get-collection-content.js.map +1 -0
  64. package/src/lib/get-components.d.ts +24 -0
  65. package/src/lib/get-components.js +102 -0
  66. package/src/lib/get-components.js.map +1 -0
  67. package/src/lib/get-datasets.d.ts +45 -0
  68. package/src/lib/get-datasets.js +169 -0
  69. package/src/lib/get-datasets.js.map +1 -0
  70. package/src/lib/get-forms.d.ts +24 -0
  71. package/src/lib/get-forms.js +116 -0
  72. package/src/lib/get-forms.js.map +1 -0
  73. package/src/lib/get-layout-version.d.ts +26 -0
  74. package/src/lib/get-layout-version.js +99 -0
  75. package/src/lib/get-layout-version.js.map +1 -0
  76. package/src/lib/get-media-asset-facts.d.ts +74 -0
  77. package/src/lib/get-media-asset-facts.js +116 -0
  78. package/src/lib/get-media-asset-facts.js.map +1 -0
  79. package/src/lib/get-plugin-installs.d.ts +29 -0
  80. package/src/lib/get-plugin-installs.js +94 -0
  81. package/src/lib/get-plugin-installs.js.map +1 -0
  82. package/src/lib/get-screen-version.d.ts +27 -0
  83. package/src/lib/get-screen-version.js +66 -0
  84. package/src/lib/get-screen-version.js.map +1 -0
  85. package/src/lib/get-screen.d.ts +39 -0
  86. package/src/lib/get-screen.js +126 -0
  87. package/src/lib/get-screen.js.map +1 -0
  88. package/src/lib/get-variables.d.ts +41 -0
  89. package/src/lib/get-variables.js +128 -0
  90. package/src/lib/get-variables.js.map +1 -0
  91. package/src/lib/get-video-asset-facts.d.ts +22 -0
  92. package/src/lib/get-video-asset-facts.js +22 -0
  93. package/src/lib/get-video-asset-facts.js.map +1 -0
  94. package/src/lib/host-event-listeners.d.ts +83 -0
  95. package/src/lib/host-event-listeners.js +91 -0
  96. package/src/lib/host-event-listeners.js.map +1 -0
  97. package/src/lib/org-permissions.d.ts +63 -0
  98. package/src/lib/org-permissions.js +187 -0
  99. package/src/lib/org-permissions.js.map +1 -0
  100. package/src/lib/repeat-record-pages.d.ts +48 -0
  101. package/src/lib/repeat-record-pages.js +64 -0
  102. package/src/lib/repeat-record-pages.js.map +1 -0
  103. package/src/lib/required-site-plugins.d.ts +114 -0
  104. package/src/lib/required-site-plugins.js +121 -0
  105. package/src/lib/required-site-plugins.js.map +1 -0
  106. package/src/lib/resolve-dataset.d.ts +40 -0
  107. package/src/lib/resolve-dataset.js +64 -0
  108. package/src/lib/resolve-dataset.js.map +1 -0
  109. package/src/lib/search-results-nodes.d.ts +48 -0
  110. package/src/lib/search-results-nodes.js +302 -0
  111. package/src/lib/search-results-nodes.js.map +1 -0
  112. package/src/lib/social-image-facts.d.ts +107 -0
  113. package/src/lib/social-image-facts.js +133 -0
  114. package/src/lib/social-image-facts.js.map +1 -0
  115. package/src/lib/stamp-form-dataset-bindings.d.ts +32 -0
  116. package/src/lib/stamp-form-dataset-bindings.js +59 -0
  117. package/src/lib/stamp-form-dataset-bindings.js.map +1 -0
  118. package/src/lib/template-screens.d.ts +227 -0
  119. package/src/lib/template-screens.js +364 -0
  120. package/src/lib/template-screens.js.map +1 -0
@@ -0,0 +1,547 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import * as Aglyn from "@aglyn/aglyn/server";
18
+ // By path: the overlay is server-only, and every `@aglyn/aglyn` barrel
19
+ // re-exports `app-utils/server` into published pages.
20
+ import { applyMediaAssetFacts, mediaAssetRefs } from "@aglyn/aglyn/app-utils/media-asset-facts";
21
+ import applyDuePublishSchedule from "./apply-publish-schedule.js";
22
+ import getComponents from "./get-components.js";
23
+ import getDatasets from "./get-datasets.js";
24
+ import getForms from "./get-forms.js";
25
+ import getMediaAssetFacts from "./get-media-asset-facts.js";
26
+ import { getPublishedCollectionSource } from "./get-collection-content.js";
27
+ import getPluginInstalls from "./get-plugin-installs.js";
28
+ import getVariables, { getFunctions, getWorkflows } from "./get-variables.js";
29
+ import getPublishedLayoutVersion from "./get-layout-version.js";
30
+ import getScreenVersion from "./get-screen-version.js";
31
+ import { socialImageAssetFacts, socialImageRefs } from "./social-image-facts.js";
32
+ import { stampFormDatasetBindings } from "./stamp-form-dataset-bindings.js";
33
+ /**
34
+ * Which collections does this tree ask for (AGL-1152)?
35
+ *
36
+ * Extracted so the PREFETCH below and the expansion that consumes it read the
37
+ * tree through one function rather than two copies of the same predicate. A
38
+ * divergence between them is not a type error — it is a slug prefetched and
39
+ * never awaited, or (worse) a slug the prefetch missed that then pays the full
40
+ * serial read anyway — so there is deliberately no second implementation to
41
+ * drift.
42
+ */ function scanCollectionBlocks(nodes, collection) {
43
+ const slugs = new Set();
44
+ let hasRelated = false;
45
+ let hasCategories = false;
46
+ let hasSearch = false;
47
+ for (const node of Object.values(nodes)){
48
+ if ((node == null ? void 0 : node.componentId) === Aglyn.COLLECTION_ENTRIES_COMPONENT_ID) {
49
+ var _ref;
50
+ var _node_props;
51
+ const slug = String((_ref = node == null ? void 0 : (_node_props = node.props) == null ? void 0 : _node_props.collectionSlug) != null ? _ref : '').trim() || (collection == null ? void 0 : collection.slug);
52
+ if (slug) slugs.add(slug);
53
+ }
54
+ // Category pills (AGL-1321) need only the taxonomy, but it rides on the
55
+ // same source, so they count as a reason to resolve one.
56
+ if ((node == null ? void 0 : node.componentId) === Aglyn.COLLECTION_CATEGORIES_COMPONENT_ID) {
57
+ var _ref1;
58
+ var _node_props1;
59
+ const slug = String((_ref1 = node == null ? void 0 : (_node_props1 = node.props) == null ? void 0 : _node_props1.collectionSlug) != null ? _ref1 : '').trim() || (collection == null ? void 0 : collection.slug);
60
+ if (slug) {
61
+ hasCategories = true;
62
+ slugs.add(slug);
63
+ }
64
+ }
65
+ // The standalone toolbar search box (AGL-1516, Figma 494:1220) needs the
66
+ // collection's entries to index, and rides the same source the listing
67
+ // beside it was built from — one read, one answer.
68
+ if ((node == null ? void 0 : node.componentId) === Aglyn.COLLECTION_SEARCH_COMPONENT_ID) {
69
+ var _ref2;
70
+ var _node_props2;
71
+ const slug = String((_ref2 = node == null ? void 0 : (_node_props2 = node.props) == null ? void 0 : _node_props2.collectionSlug) != null ? _ref2 : '').trim() || (collection == null ? void 0 : collection.slug);
72
+ if (slug) {
73
+ hasSearch = true;
74
+ slugs.add(slug);
75
+ }
76
+ }
77
+ // Related posts (AGL-582) always resolve against the ROUTED collection
78
+ // — they only mean something with a current entry in context.
79
+ if ((node == null ? void 0 : node.componentId) === Aglyn.COLLECTION_RELATED_COMPONENT_ID && (collection == null ? void 0 : collection.slug) && collection.entry) {
80
+ hasRelated = true;
81
+ slugs.add(collection.slug);
82
+ }
83
+ }
84
+ return {
85
+ slugs,
86
+ hasRelated,
87
+ hasCategories,
88
+ hasSearch
89
+ };
90
+ }
91
+ /**
92
+ * Issue the collection reads AS SOON AS THE SCREEN NODES EXIST (AGL-1152).
93
+ *
94
+ * `getPublishedCollectionSource` is three SEQUENTIAL round trips —
95
+ * `findContentCollection`, then `listLiveEntries`, then `attachEntryAuthors`
96
+ * — and until this existed it ran after the chrome bundle had already been
97
+ * awaited, so a page carrying a Collection entries block paid the whole thing
98
+ * as a serial tail on the compose phase. Measured on the tenant: a page with
99
+ * no collection block composes in ~20 ms, one with a block in ~572 ms, and the
100
+ * tree work itself accounts for under 2 ms of that at 50 entries. The gap is
101
+ * this read, waiting for reads it shares nothing with.
102
+ *
103
+ * Same shape as `screenDatasetsPromise` directly below, and the same caveat
104
+ * applies: the SCREEN's own nodes are a fast path, NOT the correctness gate. A
105
+ * collection block can arrive from a layout or a grafted reusable component,
106
+ * neither of which exists yet at this point, so the real scan still runs
107
+ * against the composed tree and still fetches anything this missed.
108
+ *
109
+ * The ROUTED collection is deliberately excluded when its entries are already
110
+ * in hand: the expansion below answers that slug from `collection.entries`
111
+ * without reading at all, so prefetching it would buy a read nobody awaits.
112
+ */ function prefetchCollectionSources(hostId, screenNodes, collection) {
113
+ const prefetched = {};
114
+ for (const slug of scanCollectionBlocks(screenNodes, collection).slugs){
115
+ if (slug === (collection == null ? void 0 : collection.slug) && collection.entries) continue;
116
+ const pending = getPublishedCollectionSource({
117
+ hostId,
118
+ collectionSlug: slug
119
+ });
120
+ // Marked handled the moment it exists, for the reason `composed` is in
121
+ // `composeScreenNodes`: a tree that turns out not to need this slug never
122
+ // awaits it, and an unawaited rejection takes the process down rather than
123
+ // failing this one read. The real await below still sees the rejection.
124
+ void pending.catch(()=>undefined);
125
+ prefetched[slug] = pending;
126
+ }
127
+ return prefetched;
128
+ }
129
+ /**
130
+ * Expands Collection entries blocks (AGL-551) against their collections'
131
+ * published entries, and Related posts blocks (AGL-582) against the routed
132
+ * entry. Fetches lazily — screens without the blocks cost nothing — and
133
+ * fails open on lookup errors like every other compose stage.
134
+ */ async function expandCollectionEntryBlocks(hostId, nodes, collection, prefetched) {
135
+ const { slugs, hasRelated, hasCategories, hasSearch } = scanCollectionBlocks(nodes, collection);
136
+ if (!slugs.size) return nodes;
137
+ const sources = {};
138
+ await Promise.all([
139
+ ...slugs
140
+ ].map(async (slug)=>{
141
+ var _ref;
142
+ // The routed collection rides its already-fetched entries +
143
+ // categories (AGL-582); other collections fetch both on demand.
144
+ if (slug === (collection == null ? void 0 : collection.slug) && collection.entries) {
145
+ sources[slug] = _extends({
146
+ slug,
147
+ entries: collection.entries,
148
+ categories: collection.categories
149
+ }, collection.page ? {
150
+ page: collection.page
151
+ } : {}, collection.entriesReachedBound ? {
152
+ reachedBound: true
153
+ } : {});
154
+ return;
155
+ }
156
+ // The prefetch when this slug was visible on the screen's own nodes;
157
+ // a live read when it only appeared after layout/component grafting.
158
+ const fetched = await ((_ref = prefetched == null ? void 0 : prefetched[slug]) != null ? _ref : getPublishedCollectionSource({
159
+ hostId,
160
+ collectionSlug: slug
161
+ }));
162
+ sources[slug] = _extends({
163
+ slug,
164
+ entries: fetched.entries,
165
+ categories: slug === (collection == null ? void 0 : collection.slug) && collection.categories ? collection.categories : fetched.categories
166
+ }, fetched.reachedBound ? {
167
+ reachedBound: true
168
+ } : {});
169
+ }));
170
+ const expanded = Aglyn.expandCollectionEntries(nodes, sources, collection == null ? void 0 : collection.slug);
171
+ const withCategories = hasCategories ? Aglyn.expandCollectionCategories(expanded, sources, /*
172
+ No DEFAULT collection for the pills on a routeless page (AGL-2524).
173
+
174
+ Category pills are the one block that builds its links from the
175
+ SOURCE's slug — `/{slug}/category/{x}` — because a category is a
176
+ filter on one collection's own listing. On the author page that slug
177
+ is synthetic, so an unbound pills block stamped links to a route that
178
+ does not exist.
179
+
180
+ Rendering nothing is the honest answer rather than a fallback. The
181
+ page spans every collection, and there is no
182
+ `/author/{slug}/category/{x}` for a pill to lead to; the only real
183
+ destination would be some collection's listing, which drops the
184
+ author the reader is looking at. A pills block that NAMES a
185
+ collection is unaffected and still resolves that collection's own
186
+ taxonomy — "browse the blog by category", which is a sensible thing
187
+ to put beside an archive.
188
+ */ (collection == null ? void 0 : collection.routeless) ? undefined : collection == null ? void 0 : collection.slug, collection == null ? void 0 : collection.categorySlug) : expanded;
189
+ const withSearch = hasSearch ? Aglyn.expandCollectionSearch(withCategories, sources, collection == null ? void 0 : collection.slug) : withCategories;
190
+ if (!hasRelated || !(collection == null ? void 0 : collection.entry)) return withSearch;
191
+ return Aglyn.expandCollectionRelated(withSearch, sources[collection.slug], collection.entry);
192
+ }
193
+ /**
194
+ * Shared post-version composition (AGL-551, extracted from
195
+ * `composeScreenNodes`): layout chrome, reusable components, repeatables,
196
+ * collection entries, bindings, function definitions, plugin installs,
197
+ * named tokens, denormalize, and last the current facts of each placed image
198
+ * and film, and of the social card the page is shared as. The screen path and
199
+ * the collection-fallback
200
+ * path (which has no screen doc) build identical trees through this one
201
+ * pipeline.
202
+ */ export async function composeNodesWithChrome(options) {
203
+ var _ref;
204
+ var _this, _options_collection, _options_collection1, _options_collection2;
205
+ const { hostId, layoutId } = options;
206
+ /**
207
+ * The layout chain, innermost first (AGL-703).
208
+ *
209
+ * A layout may itself render inside another layout, so this walks the
210
+ * `layoutId` pointers rather than reading one. Fetching is sequential
211
+ * because each step's parent is only known once the previous layout
212
+ * document is in hand — but the walk is short by construction
213
+ * (MAX_LAYOUT_CHAIN_DEPTH) and every layout is already a cached read.
214
+ *
215
+ * `seen` stops a cycle from looping forever. Stored data can hold one
216
+ * even though both the console and `canNestLayout` refuse to create it:
217
+ * the API, a script, or a restored backup can all write a layout
218
+ * document directly, and a render must degrade rather than hang.
219
+ */ const walkLayoutChain = async ()=>{
220
+ const chain = [];
221
+ const seen = new Set();
222
+ const boundLayoutId = await layoutId;
223
+ let currentLayoutId = boundLayoutId ? String(boundLayoutId) : undefined;
224
+ while(currentLayoutId && !seen.has(currentLayoutId) && chain.length < Aglyn.MAX_LAYOUT_CHAIN_DEPTH){
225
+ var _layoutRes_version, _layoutRes_version1, _layoutRes_layout;
226
+ seen.add(currentLayoutId);
227
+ const layoutRes = await getPublishedLayoutVersion({
228
+ hostId,
229
+ layoutId: currentLayoutId
230
+ });
231
+ // The version's declared properties travel with its nodes (AGL-2893):
232
+ // they are applied to this layout alone, with the screen's values for
233
+ // this layout, before the screen is grafted into its slot.
234
+ chain.push({
235
+ layoutId: currentLayoutId,
236
+ nodes: layoutRes == null ? void 0 : (_layoutRes_version = layoutRes.version) == null ? void 0 : _layoutRes_version.nodes,
237
+ props: layoutRes == null ? void 0 : (_layoutRes_version1 = layoutRes.version) == null ? void 0 : _layoutRes_version1.props
238
+ });
239
+ const parentId = layoutRes == null ? void 0 : (_layoutRes_layout = layoutRes.layout) == null ? void 0 : _layoutRes_layout.layoutId;
240
+ currentLayoutId = parentId ? String(parentId) : undefined;
241
+ }
242
+ return chain;
243
+ };
244
+ /**
245
+ * ONE round-trip stage instead of three (AGL-1225).
246
+ *
247
+ * This used to be `await layout walk` → `await getComponents` → `await
248
+ * Promise.all([five reads])`: three sequential waits, where only the first
249
+ * has any reason to be sequential. Every one of the other six reads takes
250
+ * `hostId` and nothing else — none of them consumes the layout chain — so
251
+ * they were waiting on a walk whose result they never look at.
252
+ *
253
+ * The walk stays internally sequential because it genuinely is: each step's
254
+ * parent id is only known once the previous layout document is in hand. It
255
+ * just no longer gates anything else. The critical path becomes the walk
256
+ * alone rather than walk + components + bulk.
257
+ *
258
+ * Measured budget that motivated this (production, `/product/besigner`):
259
+ * `composeScreenNodes` was the largest single phase at 1577 ms cold and
260
+ * consistently ~1.4-1.6 s warm too, so this is not cold-start cost. The
261
+ * existing `AGL-1152:render` timing line reports `composeScreenNodes` as a
262
+ * phase, so the effect of this shows up there directly — no new
263
+ * instrumentation, and a regression would be visible in the same place.
264
+ */ /**
265
+ * Does the SCREEN itself repeat over a dataset (AGL-1440)?
266
+ *
267
+ * Asked as soon as the nodes are in hand — which since AGL-1428 is after
268
+ * the fan-out has been ISSUED rather than before — so the overwhelmingly
269
+ * common case still keeps the AGL-1225 shape: a page that repeats almost
270
+ * always says so on its own document, and its datasets read goes out
271
+ * alongside the remaining chrome reads instead of after them.
272
+ *
273
+ * It is deliberately NOT the correctness gate — a repeatable can arrive from
274
+ * a layout or a grafted reusable component, neither of which exists yet. The
275
+ * gate is re-asked against the composed tree after grafting, which is the
276
+ * exact input `expandRepeatables` reads.
277
+ */ /*
278
+ * ISSUE THE HOST-SCOPED READS FIRST, THEN AWAIT THE NODES (AGL-1428).
279
+ *
280
+ * Every read in this bundle is keyed by `hostId` (and `layoutId`) alone —
281
+ * none of them reads `screenNodes` — so starting them before the nodes are
282
+ * in hand lets `composeScreenNodes`' version read overlap them rather than
283
+ * run ahead of them. `Promise.all` is created BEFORE the `await` below on
284
+ * purpose: that is the line that makes the two independent, and moving the
285
+ * await above it silently gives the whole saving back.
286
+ *
287
+ * `getDatasets` is the one read that genuinely depends on the nodes, so it
288
+ * cannot join the bundle. It does not have to wait for the bundle either —
289
+ * it is issued the moment the nodes resolve and awaited after, so it still
290
+ * overlaps whatever is left of the chrome reads instead of costing the
291
+ * extra serial round trip that dropping it from the batch would imply.
292
+ */ const chromeBundle = Promise.all([
293
+ walkLayoutChain(),
294
+ getComponents({
295
+ hostId
296
+ }),
297
+ // Host variable + function bindings (AGL-91/93): {{name}} and
298
+ // {{fn:name(args)}} in string props resolve to values; unknown tokens
299
+ // and failed runs stay literal.
300
+ Promise.all([
301
+ getVariables({
302
+ hostId
303
+ }),
304
+ getFunctions({
305
+ hostId
306
+ }),
307
+ getWorkflows({
308
+ hostId
309
+ }),
310
+ getPluginInstalls({
311
+ hostId
312
+ })
313
+ ])
314
+ ]);
315
+ const screenNodes = await options.screenNodes;
316
+ const screenDatasetKeys = Aglyn.repeatDatasetKeys(screenNodes);
317
+ const screenDatasetsPromise = screenDatasetKeys.length ? getDatasets({
318
+ hostId,
319
+ keys: screenDatasetKeys
320
+ }) : undefined;
321
+ // Does the SCREEN itself place a form entity? Gated and re-asked exactly
322
+ // like the datasets read beside it (AGL-1440): most pages carry no form, and
323
+ // the ones that do usually say so on their own document, so the read goes
324
+ // out here alongside the chrome reads instead of as a serial tail. It is not
325
+ // the correctness gate — a placed form can arrive from a layout or a grafted
326
+ // component — so the composed tree is asked again below.
327
+ const screenFormsPromise = Aglyn.placesFormEntity(screenNodes) ? getForms({
328
+ hostId
329
+ }) : undefined;
330
+ // Issued HERE, beside the datasets read and before the chrome bundle is
331
+ // awaited, so the collection read overlaps it instead of trailing it.
332
+ const prefetchedSources = prefetchCollectionSources(hostId, screenNodes, options.collection);
333
+ const [layoutChain, componentsRes, bulk] = await chromeBundle;
334
+ const [rawVariables, functions, workflows, pluginInstalls] = bulk;
335
+ const screenDatasets = await screenDatasetsPromise;
336
+ // Settled by now: it is read off the same version document the layout
337
+ // binding the walk above waited on came from.
338
+ const layoutPropValues = await options.layoutPropValues;
339
+ const composedNodes = Aglyn.composeLayoutChainWithProps(layoutChain, screenNodes, layoutPropValues);
340
+ const graftedComponents = Aglyn.composeReusableComponentNodes(composedNodes, componentsRes.definitions);
341
+ /*
342
+ * PLACED FORMS RESOLVE AGAINST THEIR ENTITY (`docs/specs/reusable-forms.md`).
343
+ *
344
+ * A form node bound to `hosts/{hostId}/forms/{formId}` renders that entity's
345
+ * published design, so a form is edited once and every page placing it
346
+ * follows. Without this the entity's tree was written on every publish and
347
+ * read by nothing: the fields had to be redrawn per page, and the two copies
348
+ * diverged the moment either was touched.
349
+ *
350
+ * The gate is the COMPONENT-grafted tree, not the screen's own nodes, for
351
+ * the reason the repeatables gate below states: a form placed inside a
352
+ * layout or a shared component does not exist in `screenNodes`, and a page
353
+ * that renders one would silently keep its stale inline copy.
354
+ *
355
+ * The second graft re-runs the component expansion deliberately. Instances
356
+ * already expanded are skipped by their own prefix, so the repeat costs a
357
+ * scan, and passing BOTH placement kinds is what expands a reusable
358
+ * component nested inside a form's design — which the first pass could not
359
+ * have seen, because that subtree was not in the tree yet.
360
+ */ const forms = (_ref = (_this = await screenFormsPromise) == null ? void 0 : _this.forms) != null ? _ref : Aglyn.placesFormEntity(graftedComponents) ? (await getForms({
361
+ hostId
362
+ })).forms : undefined;
363
+ const grafted = forms ? Aglyn.composeReusableComponentNodes(graftedComponents, componentsRes.definitions, [
364
+ Aglyn.placedFormPlacement(forms)
365
+ ]) : graftedComponents;
366
+ // Computed variables (AGL-129): workflow-backed values resolve once per
367
+ // compose; failures keep each variable's stored fallback.
368
+ const variables = Aglyn.resolveComputedVariables(rawVariables, functions, workflows);
369
+ // Repeatables (AGL-103) expand after grafting (so they work inside
370
+ // reusable components) and before bindings (so {{name}} tokens inside
371
+ // cloned items still resolve).
372
+ //
373
+ // Only the datasets this tree repeats over are read (AGL-1440), and the tree
374
+ // asked is the composed one — after grafting — because that is the map the
375
+ // expansion reads: a repeatable living in a layout or a reusable component
376
+ // is invisible in `screenNodes`, and reading only the screen's keys would
377
+ // silently render one template row where the author put a list. The
378
+ // screen's own keys were issued beside the chrome reads above; a key only a
379
+ // layout or a component adds is read here, for that key alone.
380
+ const datasetKeys = Aglyn.repeatDatasetKeys(grafted);
381
+ const unreadDatasetKeys = datasetKeys.filter((key)=>!screenDatasetKeys.includes(key));
382
+ const datasets = unreadDatasetKeys.length ? _extends({}, screenDatasets, await getDatasets({
383
+ hostId,
384
+ keys: unreadDatasetKeys
385
+ })) : screenDatasets;
386
+ const repeated = Aglyn.expandRepeatables(grafted, datasets);
387
+ // Collection entries blocks (AGL-551) expand alongside repeatables:
388
+ // per-entry {{entry.*}} tokens substitute inside the clones here, while
389
+ // page-level tokens wait for resolveNamedTokens below.
390
+ const withEntries = await expandCollectionEntryBlocks(hostId, repeated, options.collection, prefetchedSources);
391
+ // Entry Meta blocks (AGL-1385): fill in the routed entry's date/category/
392
+ // tags. Needs no source fetch — the routed entry and its taxonomy are
393
+ // already in hand — so it sits outside `expandCollectionEntryBlocks`, which
394
+ // returns early when no block asks for a collection read. AFTER it, so the
395
+ // per-entry clones it just produced already carry their own resolved values
396
+ // and are skipped.
397
+ const withEntryMeta = Aglyn.expandCollectionEntryMeta(withEntries, (_options_collection = options.collection) == null ? void 0 : _options_collection.entry, (_options_collection1 = options.collection) == null ? void 0 : _options_collection1.categories);
398
+ // Entry Author cards (AGL-2486): the same fill, one block over. Its values
399
+ // come off the author RECORD the routed entry resolved to, which the
400
+ // collection read has already attached, so this costs nothing either.
401
+ const withEntryAuthor = Aglyn.expandCollectionEntryAuthor(withEntryMeta, (_options_collection2 = options.collection) == null ? void 0 : _options_collection2.entry);
402
+ const bound = Aglyn.resolveNodesBindings(withEntryAuthor, variables, functions);
403
+ // Host variables (AGL-1022): `{{host.*}}` resolves from the INSTALLING
404
+ // site, late — at render, never at install — so a rebrand propagates to
405
+ // every artifact that names the host instead of hard-coding it. Same
406
+ // registry the email path uses, so a token means one thing in both.
407
+ const withHostTokens = Aglyn.resolveNodesHostTokens(bound, options.host);
408
+ // Function widgets run client-side: embed their definitions (AGL-93), and
409
+ // beside each the site variables that function reads and no others
410
+ // (AGL-3202).
411
+ const withFunctions = Aglyn.attachFunctionDefinitions(withHostTokens, functions, variables);
412
+ // Marketplace plugins (AGL-45): stamp each marketplacePlugin node with its
413
+ // pinned install (version/sha256/capabilities) + kill-switch state.
414
+ const nodes = Aglyn.attachPluginInstalls(withFunctions, pluginInstalls);
415
+ // Entry-template tokens (AGL-105): {{entry.*}} from the rendered entry.
416
+ const finalNodes = Aglyn.resolveNamedTokens(nodes, options.tokens);
417
+ // The document's one `main` landmark (AGL-2486). LAST, so it reads the tree
418
+ // the page actually ships — a slot grafted from a layout chain, an element
419
+ // an author chose — rather than the screen as stored.
420
+ const withLandmark = Aglyn.stampDocumentLandmark(finalNodes);
421
+ // Each form's dataset binding, signed so the submit route can trust it
422
+ // (AGL-2773). Read off THIS tree, the one the page ships, so a form grafted
423
+ // from a layout, a component or a form entity is signed as it renders; and
424
+ // after every stage that rewrites props, so nothing changes what the
425
+ // signature covers.
426
+ const withFormBindings = stampFormDatasetBindings(withLandmark, hostId);
427
+ const denormalized = Aglyn.canvas.processNodesToDenormalized(withFormBindings);
428
+ /*
429
+ * WHAT EACH PLACED IMAGE AND FILM IS NOW (AGL-2807, AGL-2833).
430
+ *
431
+ * An image's pixel pair and a film's length, shape and poster come from the
432
+ * asset's DAM document as it is now, not as it was when it was picked. LAST,
433
+ * on the tree the page ships, because an asset can arrive from any stage
434
+ * above: a layout, a component, a form, a repeated row, a collection entry,
435
+ * a binding.
436
+ *
437
+ * ONE read for all of them, issued once the tree is final rather than beside
438
+ * the chrome reads. A read issued there could only cover the screen's own
439
+ * placements, and nearly every layout places an image the screen does not
440
+ * (a logo, a footer mark), so it would buy a second read on almost every
441
+ * page. The late read costs its own round trip after the chrome reads, paid
442
+ * each time a page is composed, which for a published page is its ISR
443
+ * regeneration. A tree with no library asset, on a page whose social card
444
+ * names none, issues none.
445
+ *
446
+ * THE SOCIAL CARD'S ASSETS JOIN THE SAME READ (AGL-2850), ahead of the
447
+ * placements. A placement the cap leaves out keeps its pick-time pair, which
448
+ * for an image is a reservation the decoded picture corrects. Nothing
449
+ * corrects a card's pair, because a crawler lays the card out from it before
450
+ * fetching the image, and a card names at most three documents.
451
+ */ const socialImages = options.socialImages;
452
+ const cardRefs = socialImages ? socialImageRefs(socialImages.images) : [];
453
+ const refs = mediaAssetRefs(denormalized);
454
+ if (!refs.length && !cardRefs.length) return denormalized;
455
+ const facts = await getMediaAssetFacts({
456
+ hostId,
457
+ refs: [
458
+ ...cardRefs,
459
+ ...refs
460
+ ]
461
+ });
462
+ if (socialImages) {
463
+ const cardFacts = socialImageAssetFacts(socialImages.images, facts);
464
+ if (cardFacts) socialImages.onFacts(cardFacts);
465
+ }
466
+ return applyMediaAssetFacts(denormalized, facts);
467
+ }
468
+ /**
469
+ * Full published-render composition for one screen (extracted for AGL-87 so
470
+ * the SSG path and the password-unlock API build identical trees): applies
471
+ * a due publish schedule, loads the version, composes the shared layout
472
+ * chrome, grafts reusable components, and denormalizes.
473
+ */ export async function composeScreenNodes(options) {
474
+ var _ref, _options_versionId;
475
+ const { hostId, screenId, screen } = options;
476
+ const effectiveVersionId = options.versionId ? null : await applyDuePublishSchedule({
477
+ hostId,
478
+ collectionName: 'screens',
479
+ docId: screenId,
480
+ parent: screen
481
+ });
482
+ const versionId = (_ref = (_options_versionId = options.versionId) != null ? _options_versionId : effectiveVersionId) != null ? _ref : screen.versionId;
483
+ /*
484
+ * OVERLAP THE VERSION READ WITH THE CHROME BUNDLE (AGL-1428).
485
+ *
486
+ * `composeNodesWithChrome`'s reads are keyed by `hostId`/`layoutId`, both
487
+ * of which are in hand here, so the version read no longer has to finish
488
+ * before they start. Handing the promise over instead of the resolved
489
+ * value is the whole change: it is `composeNodesWithChrome` that decides
490
+ * when it actually needs the nodes.
491
+ *
492
+ * The `versionId`-less screen still exits BEFORE anything is issued, so a
493
+ * screen that has never been published costs no reads. What remains is the
494
+ * narrow case of a `versionId` that points at a missing or unreadable
495
+ * version document — a data-integrity fault rather than a routing outcome
496
+ * — and that one now pays for a chrome bundle it discards. That is the
497
+ * deliberate trade: one wasted bundle on a broken screen, against the
498
+ * version read hiding under the chrome reads on every render that works.
499
+ */ if (!versionId) return null;
500
+ const versionPromise = getScreenVersion({
501
+ hostId,
502
+ screenId,
503
+ versionId
504
+ });
505
+ /*
506
+ * NEITHER DERIVED PROMISE MAY REJECT ON ITS OWN.
507
+ *
508
+ * `composed` is discarded on every failure path below, and a rejected
509
+ * promise nobody awaited is an unhandled rejection — which in Node takes
510
+ * the whole render process down instead of letting this 404. So the nodes
511
+ * handed to the compose absorb the failure (an empty tree it will never be
512
+ * asked for), and `composed` gets a rejection handler attached the moment
513
+ * it exists rather than at the point we decide to drop it. The real error
514
+ * still propagates, from the `await versionPromise` below, exactly where it
515
+ * did when this function awaited the version directly.
516
+ */ const composed = composeNodesWithChrome({
517
+ hostId,
518
+ // Version-first (key-present wins, null = explicitly no layout), screen
519
+ // fallback — resolved as a promise so only the layout-chain walk waits on
520
+ // the version read; the rest of the chrome bundle keeps the AGL-1428
521
+ // overlap. A failed version read falls back to the screen binding; the
522
+ // whole compose is discarded on that path anyway.
523
+ layoutId: versionPromise.then((res)=>res.version && 'layoutId' in res.version ? res.version.layoutId : screen.layoutId, ()=>screen.layoutId),
524
+ // The screen's values for its layouts' properties (AGL-2893), beside the
525
+ // binding on the same document; a failed read renders the defaults.
526
+ layoutPropValues: versionPromise.then((res)=>{
527
+ var _res_version;
528
+ return (_res_version = res.version) == null ? void 0 : _res_version.layoutPropValues;
529
+ }, ()=>undefined),
530
+ screenNodes: versionPromise.then((res)=>{
531
+ var _ref;
532
+ var _res_version;
533
+ return (_ref = (_res_version = res.version) == null ? void 0 : _res_version.nodes) != null ? _ref : {};
534
+ }, ()=>({})),
535
+ tokens: options.tokens,
536
+ collection: options.collection,
537
+ host: options.host,
538
+ socialImages: options.socialImages
539
+ });
540
+ void composed.catch(()=>undefined);
541
+ const versionRes = await versionPromise;
542
+ if (versionRes.error || !versionRes.version) return null;
543
+ return composed;
544
+ }
545
+ export default composeScreenNodes;
546
+
547
+ //# sourceMappingURL=compose-screen-nodes.js.map