@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.
- package/LICENSE +201 -0
- package/README.md +26 -0
- package/package.json +39 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +22 -0
- package/src/index.js.map +1 -0
- package/src/lib/apply-publish-schedule.d.ts +34 -0
- package/src/lib/apply-publish-schedule.js +285 -0
- package/src/lib/apply-publish-schedule.js.map +1 -0
- package/src/lib/assign-contact-owner.d.ts +162 -0
- package/src/lib/assign-contact-owner.js +322 -0
- package/src/lib/assign-contact-owner.js.map +1 -0
- package/src/lib/associate-company-by-domain.d.ts +70 -0
- package/src/lib/associate-company-by-domain.js +150 -0
- package/src/lib/associate-company-by-domain.js.map +1 -0
- package/src/lib/author-page-nodes.d.ts +36 -0
- package/src/lib/author-page-nodes.js +296 -0
- package/src/lib/author-page-nodes.js.map +1 -0
- package/src/lib/built-in-page-layout.d.ts +48 -0
- package/src/lib/built-in-page-layout.js +107 -0
- package/src/lib/built-in-page-layout.js.map +1 -0
- package/src/lib/capture-host-contact.d.ts +95 -0
- package/src/lib/capture-host-contact.js +137 -0
- package/src/lib/capture-host-contact.js.map +1 -0
- package/src/lib/collection-fallback-nodes.d.ts +90 -0
- package/src/lib/collection-fallback-nodes.js +602 -0
- package/src/lib/collection-fallback-nodes.js.map +1 -0
- package/src/lib/compose-author-page.d.ts +109 -0
- package/src/lib/compose-author-page.js +225 -0
- package/src/lib/compose-author-page.js.map +1 -0
- package/src/lib/compose-collection-page.d.ts +94 -0
- package/src/lib/compose-collection-page.js +256 -0
- package/src/lib/compose-collection-page.js.map +1 -0
- package/src/lib/compose-screen-nodes.d.ts +156 -0
- package/src/lib/compose-screen-nodes.js +547 -0
- package/src/lib/compose-screen-nodes.js.map +1 -0
- package/src/lib/compose-search-page.d.ts +39 -0
- package/src/lib/compose-search-page.js +57 -0
- package/src/lib/compose-search-page.js.map +1 -0
- package/src/lib/convert-host-lead.d.ts +189 -0
- package/src/lib/convert-host-lead.js +443 -0
- package/src/lib/convert-host-lead.js.map +1 -0
- package/src/lib/defer-lazy-panels.d.ts +58 -0
- package/src/lib/defer-lazy-panels.js +171 -0
- package/src/lib/defer-lazy-panels.js.map +1 -0
- package/src/lib/element-animation-assets.d.ts +62 -0
- package/src/lib/element-animation-assets.js +290 -0
- package/src/lib/element-animation-assets.js.map +1 -0
- package/src/lib/emit-host-event.d.ts +29 -0
- package/src/lib/emit-host-event.js +30 -0
- package/src/lib/emit-host-event.js.map +1 -0
- package/src/lib/enrich-gated-page.d.ts +67 -0
- package/src/lib/enrich-gated-page.js +134 -0
- package/src/lib/enrich-gated-page.js.map +1 -0
- package/src/lib/entry-link-routes.d.ts +61 -0
- package/src/lib/entry-link-routes.js +169 -0
- package/src/lib/entry-link-routes.js.map +1 -0
- package/src/lib/get-author-content.d.ts +119 -0
- package/src/lib/get-author-content.js +288 -0
- package/src/lib/get-author-content.js.map +1 -0
- package/src/lib/get-collection-content.d.ts +290 -0
- package/src/lib/get-collection-content.js +565 -0
- package/src/lib/get-collection-content.js.map +1 -0
- package/src/lib/get-components.d.ts +24 -0
- package/src/lib/get-components.js +102 -0
- package/src/lib/get-components.js.map +1 -0
- package/src/lib/get-datasets.d.ts +45 -0
- package/src/lib/get-datasets.js +169 -0
- package/src/lib/get-datasets.js.map +1 -0
- package/src/lib/get-forms.d.ts +24 -0
- package/src/lib/get-forms.js +116 -0
- package/src/lib/get-forms.js.map +1 -0
- package/src/lib/get-layout-version.d.ts +26 -0
- package/src/lib/get-layout-version.js +99 -0
- package/src/lib/get-layout-version.js.map +1 -0
- package/src/lib/get-media-asset-facts.d.ts +74 -0
- package/src/lib/get-media-asset-facts.js +116 -0
- package/src/lib/get-media-asset-facts.js.map +1 -0
- package/src/lib/get-plugin-installs.d.ts +29 -0
- package/src/lib/get-plugin-installs.js +94 -0
- package/src/lib/get-plugin-installs.js.map +1 -0
- package/src/lib/get-screen-version.d.ts +27 -0
- package/src/lib/get-screen-version.js +66 -0
- package/src/lib/get-screen-version.js.map +1 -0
- package/src/lib/get-screen.d.ts +39 -0
- package/src/lib/get-screen.js +126 -0
- package/src/lib/get-screen.js.map +1 -0
- package/src/lib/get-variables.d.ts +41 -0
- package/src/lib/get-variables.js +128 -0
- package/src/lib/get-variables.js.map +1 -0
- package/src/lib/get-video-asset-facts.d.ts +22 -0
- package/src/lib/get-video-asset-facts.js +22 -0
- package/src/lib/get-video-asset-facts.js.map +1 -0
- package/src/lib/host-event-listeners.d.ts +83 -0
- package/src/lib/host-event-listeners.js +91 -0
- package/src/lib/host-event-listeners.js.map +1 -0
- package/src/lib/org-permissions.d.ts +63 -0
- package/src/lib/org-permissions.js +187 -0
- package/src/lib/org-permissions.js.map +1 -0
- package/src/lib/repeat-record-pages.d.ts +48 -0
- package/src/lib/repeat-record-pages.js +64 -0
- package/src/lib/repeat-record-pages.js.map +1 -0
- package/src/lib/required-site-plugins.d.ts +114 -0
- package/src/lib/required-site-plugins.js +121 -0
- package/src/lib/required-site-plugins.js.map +1 -0
- package/src/lib/resolve-dataset.d.ts +40 -0
- package/src/lib/resolve-dataset.js +64 -0
- package/src/lib/resolve-dataset.js.map +1 -0
- package/src/lib/search-results-nodes.d.ts +48 -0
- package/src/lib/search-results-nodes.js +302 -0
- package/src/lib/search-results-nodes.js.map +1 -0
- package/src/lib/social-image-facts.d.ts +107 -0
- package/src/lib/social-image-facts.js +133 -0
- package/src/lib/social-image-facts.js.map +1 -0
- package/src/lib/stamp-form-dataset-bindings.d.ts +32 -0
- package/src/lib/stamp-form-dataset-bindings.js +59 -0
- package/src/lib/stamp-form-dataset-bindings.js.map +1 -0
- package/src/lib/template-screens.d.ts +227 -0
- package/src/lib/template-screens.js +364 -0
- 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
|