@aglyn/aglyn 1.0.0-beta.182 → 1.0.0-beta.183

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 (28) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/compose-reusable-components.d.ts +10 -0
  3. package/src/lib/app-utils/compose-reusable-components.js +6 -1
  4. package/src/lib/app-utils/compose-reusable-components.js.map +1 -1
  5. package/src/lib/app-utils/definition-canvas-tree.d.ts +24 -0
  6. package/src/lib/app-utils/definition-canvas-tree.js +38 -0
  7. package/src/lib/app-utils/definition-canvas-tree.js.map +1 -1
  8. package/src/lib/app-utils/docs-index.generated.js +9 -2
  9. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  10. package/src/lib/app-utils/load-referenced-components.d.ts +166 -0
  11. package/src/lib/app-utils/load-referenced-components.js +219 -0
  12. package/src/lib/app-utils/load-referenced-components.js.map +1 -0
  13. package/src/lib/app-utils/reusable-component-kind.d.ts +119 -0
  14. package/src/lib/app-utils/reusable-component-kind.js +119 -0
  15. package/src/lib/app-utils/reusable-component-kind.js.map +1 -0
  16. package/src/lib/app-utils/server.d.ts +1 -0
  17. package/src/lib/app-utils/server.js +2 -0
  18. package/src/lib/app-utils/server.js.map +1 -1
  19. package/src/lib/foundation/constants/components.d.ts +7 -0
  20. package/src/lib/foundation/constants/components.js +6 -0
  21. package/src/lib/foundation/constants/components.js.map +1 -1
  22. package/src/lib/foundation/definitions/platform.types.d.ts +29 -0
  23. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  24. package/src/lib/foundation/index.d.ts +1 -1
  25. package/src/lib/foundation/index.js.map +1 -1
  26. package/src/lib/plugin-manager/plugin-resource-drafts.d.ts +17 -3
  27. package/src/lib/plugin-manager/plugin-resource-drafts.js +2 -2
  28. package/src/lib/plugin-manager/plugin-resource-drafts.js.map +1 -1
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type ReusableComponentTree } from './compose-reusable-components';
18
+ /**
19
+ * The fields of `hosts/{hostId}/components/{id}` a render reads: the PUBLISHED
20
+ * snapshot. `rootId`, `nodes` and the declared `props` live on the component
21
+ * document itself, never on its versions (AGL-679), so one read per component
22
+ * is the whole cost.
23
+ */
24
+ export interface StoredComponentDocument {
25
+ rootId?: unknown;
26
+ nodes?: unknown;
27
+ props?: unknown;
28
+ deletedAt?: unknown;
29
+ }
30
+ /** Why a referenced component gave no tree to graft. */
31
+ export type ComponentSkipReason =
32
+ /** No such document. */
33
+ 'missing'
34
+ /** Soft-deleted: it renders nothing, wherever it was placed. */
35
+ | 'deleted'
36
+ /** Created but never published — no `rootId`/`nodes` on the document. */
37
+ | 'unpublished'
38
+ /** Its `nodes` could not be decoded; `decodeStoredNodes` logs why. */
39
+ | 'undecodable'
40
+ /** Not usable as one document id, so it was never read (AGL-1771). */
41
+ | 'invalid'
42
+ /** Past {@link REFERENCED_COMPONENTS_READ_LIMIT}; not read. */
43
+ | 'limit';
44
+ export type StoredComponentTreeRead = {
45
+ ok: true;
46
+ tree: ReusableComponentTree;
47
+ } | {
48
+ ok: false;
49
+ reason: Exclude<ComponentSkipReason, 'invalid' | 'limit'>;
50
+ };
51
+ /**
52
+ * A stored component document as the tree `composeReusableComponentNodes`
53
+ * grafts, or why it is not one.
54
+ *
55
+ * BOTH STORED FORMS (AGL-1151). A published definition is msgpack for anything
56
+ * promoted since components were compressed and a plain map for everything
57
+ * older, and nothing migrates them — `decodeStoredNodes` returns a map
58
+ * unchanged, so one call serves both forever. Reading the field raw is a
59
+ * SILENT failure: the graft looks up `nodes[rootId]` on a `Buffer`, finds
60
+ * nothing, and every placement of the component renders empty.
61
+ *
62
+ * An undecodable definition is skipped rather than grafted empty. The page or
63
+ * the mail comes out the same either way, but `decodeStoredNodes` logs the
64
+ * reason, and a definition that silently became `{}` would not say why the
65
+ * component vanished.
66
+ */
67
+ export declare function readStoredComponentTree(stored: StoredComponentDocument | null | undefined): StoredComponentTreeRead;
68
+ /**
69
+ * One component document by id — `null` or `undefined` when there is none. A
70
+ * rejection is a failed READ and fails the whole load: an email must not go out
71
+ * missing its footer because a request timed out, and every caller has a
72
+ * better answer to an error than a quietly partial design.
73
+ */
74
+ export type ReadComponentDocument = (componentId: string) => Promise<StoredComponentDocument | null | undefined>;
75
+ /** A referenced component that gave no tree, and why. */
76
+ export interface SkippedComponent {
77
+ id: string;
78
+ reason: ComponentSkipReason;
79
+ }
80
+ /**
81
+ * Documents one load may read — the same bound `getComponents` reads a whole
82
+ * site under, so an on-demand render can never cost more than a page does.
83
+ */
84
+ export declare const REFERENCED_COMPONENTS_READ_LIMIT = 200;
85
+ export interface LoadReferencedComponentsOptions {
86
+ /** Levels of nesting to follow; the graft's own bound by default. */
87
+ maxDepth?: number;
88
+ /** Documents one load may read; {@link REFERENCED_COMPONENTS_READ_LIMIT} by default. */
89
+ maxDocuments?: number;
90
+ /**
91
+ * Told once, after the load, about every referenced id that gave no tree.
92
+ * The default logs a warning: a placement of a deleted component renders
93
+ * nothing, and nothing else would say why.
94
+ */
95
+ onSkipped?: (skipped: readonly SkippedComponent[]) => void;
96
+ }
97
+ /**
98
+ * The published definitions `nodes` places — directly, or through another
99
+ * definition — keyed by id, in the shape `composeReusableComponentNodes`
100
+ * takes.
101
+ *
102
+ * Reads a LEVEL at a time: the ids the document places, then the ids those
103
+ * definitions place, and so on to {@link MAX_COMPONENT_DEPTH}. Each level's
104
+ * reads run in parallel, and an id is never read twice, so a cycle costs one
105
+ * read per component in it. Deleted, never-published, undecodable and missing
106
+ * definitions are left out — a placement of one renders nothing, exactly as it
107
+ * does on a page — and reported through `onSkipped`.
108
+ */
109
+ export declare function loadReferencedComponents(nodes: Readonly<Record<string, unknown>> | null | undefined, read: ReadComponentDocument, options?: LoadReferencedComponentsOptions): Promise<Record<string, ReusableComponentTree>>;
110
+ /**
111
+ * `nodes` with every reusable-component placement expanded from the published
112
+ * definitions it references — {@link loadReferencedComponents} then
113
+ * `composeReusableComponentNodes`.
114
+ *
115
+ * After it, each placement IS its component's root, carrying the placement's
116
+ * id (AGL-2521): a renderer sees the component's own blocks, with the
117
+ * placement's property values (AGL-1247) and attribute overrides (AGL-1899)
118
+ * applied, and knows nothing about components. Tokens the graft does not own
119
+ * — merge tokens, host tokens, site variables — pass through untouched for the
120
+ * renderer that does.
121
+ *
122
+ * A document that places nothing comes back AS GIVEN, the same reference, with
123
+ * no read made. A placement whose component did not load is left exactly as
124
+ * authored, which is how a deleted definition never takes a document down.
125
+ */
126
+ export declare function composeReferencedComponents<M extends Readonly<Record<string, unknown>>>(nodes: M, read: ReadComponentDocument, options?: LoadReferencedComponentsOptions): Promise<M>;
127
+ /** A document snapshot, as far as {@link hostComponentReader} reads one. */
128
+ interface ComponentSnapshotLike {
129
+ exists: boolean;
130
+ get(field: string): unknown;
131
+ }
132
+ interface ComponentDocumentRefLike {
133
+ get(): Promise<ComponentSnapshotLike>;
134
+ collection(path: string): ComponentCollectionRefLike;
135
+ }
136
+ interface ComponentCollectionRefLike {
137
+ doc(id: string): ComponentDocumentRefLike;
138
+ }
139
+ /**
140
+ * The slice of the Admin Firestore chain {@link hostComponentReader} walks.
141
+ * Structural, so a caller holding any Admin handle — or a spec's fake — passes
142
+ * it without a cast, and so this core module takes no `firebase-admin`
143
+ * dependency. `@aglyn/shared-util-email`'s `AdminFirestoreLike` satisfies it.
144
+ */
145
+ export interface ComponentStoreLike {
146
+ collection(path: string): ComponentCollectionRefLike;
147
+ }
148
+ /**
149
+ * A {@link ReadComponentDocument} over `hosts/{hostId}/components` through the
150
+ * Admin SDK — one document get per component, the four fields a render needs.
151
+ */
152
+ export declare function hostComponentReader(firestore: ComponentStoreLike, hostId: string): ReadComponentDocument;
153
+ /**
154
+ * {@link composeReferencedComponents} for a site's own document, read through
155
+ * the Admin SDK: what every server path that sends, previews or publishes an
156
+ * email composes with.
157
+ *
158
+ * Shaped `(nodes, { firestore, hostId })` so it is passed as it stands wherever
159
+ * a composer is asked for — `loadHostEmail`'s `compose` is one — rather than
160
+ * wrapped at each call site. Skips are logged with the site they belong to.
161
+ */
162
+ export declare function composeHostComponentNodes<M extends Readonly<Record<string, unknown>>>(nodes: M, context: {
163
+ firestore: ComponentStoreLike;
164
+ hostId: string;
165
+ }): Promise<M>;
166
+ export {};
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * THE COMPONENTS ONE DOCUMENT PLACES, READ ON DEMAND (AGL-3287).
18
+ *
19
+ * A published page composes against every component the site has, read in one
20
+ * collection query and held in the render cache — `getComponents` in the tenant
21
+ * runtime. That is the right trade for a page, which is rendered constantly and
22
+ * served from cache. It is the wrong one for a document rendered on demand: an
23
+ * email sent, test-sent or previewed, a design being published elsewhere. Those
24
+ * are not cached pages, a cache would serve a header the site has since
25
+ * republished, and most of them place no component at all.
26
+ *
27
+ * So this reads ONLY what a node map references, following nesting to the same
28
+ * depth the graft expands ({@link MAX_COMPONENT_DEPTH}), a level at a time, each
29
+ * level in parallel. A document that places nothing costs no read at all.
30
+ *
31
+ * It knows nothing about Firestore. A caller hands it a {@link
32
+ * ReadComponentDocument} — the Admin SDK through {@link hostComponentReader},
33
+ * the client SDK in a console preview — so the same walk serves a send, a
34
+ * browser and a spec's fake. Pure otherwise, and client-safe: no Node builtin,
35
+ * no cache, no Next.
36
+ *
37
+ * What a stored document MEANS as a tree is {@link readStoredComponentTree},
38
+ * which `getComponents` reads its collection through too, so the page and the
39
+ * mail cannot come to disagree about a component's published design.
40
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
41
+ import { collectReferencedComponentIds, composeReusableComponentNodes, MAX_COMPONENT_DEPTH } from "./compose-reusable-components.js";
42
+ import { decodeStoredNodes } from "./stored-nodes.js";
43
+ /**
44
+ * A stored component document as the tree `composeReusableComponentNodes`
45
+ * grafts, or why it is not one.
46
+ *
47
+ * BOTH STORED FORMS (AGL-1151). A published definition is msgpack for anything
48
+ * promoted since components were compressed and a plain map for everything
49
+ * older, and nothing migrates them — `decodeStoredNodes` returns a map
50
+ * unchanged, so one call serves both forever. Reading the field raw is a
51
+ * SILENT failure: the graft looks up `nodes[rootId]` on a `Buffer`, finds
52
+ * nothing, and every placement of the component renders empty.
53
+ *
54
+ * An undecodable definition is skipped rather than grafted empty. The page or
55
+ * the mail comes out the same either way, but `decodeStoredNodes` logs the
56
+ * reason, and a definition that silently became `{}` would not say why the
57
+ * component vanished.
58
+ */ export function readStoredComponentTree(stored) {
59
+ if (!stored) return {
60
+ ok: false,
61
+ reason: 'missing'
62
+ };
63
+ if (stored.deletedAt) return {
64
+ ok: false,
65
+ reason: 'deleted'
66
+ };
67
+ if (!stored.nodes || !stored.rootId) return {
68
+ ok: false,
69
+ reason: 'unpublished'
70
+ };
71
+ const nodes = decodeStoredNodes(stored.nodes);
72
+ if (!nodes) return {
73
+ ok: false,
74
+ reason: 'undecodable'
75
+ };
76
+ return {
77
+ ok: true,
78
+ tree: _extends({
79
+ rootId: stored.rootId,
80
+ nodes
81
+ }, Array.isArray(stored.props) && stored.props.length && {
82
+ props: stored.props
83
+ })
84
+ };
85
+ }
86
+ /**
87
+ * Documents one load may read — the same bound `getComponents` reads a whole
88
+ * site under, so an on-demand render can never cost more than a page does.
89
+ */ export const REFERENCED_COMPONENTS_READ_LIMIT = 200;
90
+ /**
91
+ * Whether a referenced id may be handed to a reader as ONE document id.
92
+ *
93
+ * `refId` is author-controlled, and `doc()` appends a slash-separated PATH:
94
+ * `half/path` throws where the reference is built and `a/b/c` reads beneath a
95
+ * document that does not exist (AGL-1771, `isDocumentId` in
96
+ * `@aglyn/tenant-data-admin/server/document-id`). That predicate is
97
+ * server-only — it measures with `Buffer` — and this walk also runs in a
98
+ * browser, so the same clauses are restated with `TextEncoder`.
99
+ */ function isReadableComponentId(id) {
100
+ return id.length > 0 && !id.includes('/') && id !== '.' && id !== '..' && !/^__.*__$/.test(id) && new TextEncoder().encode(id).length <= 1500;
101
+ }
102
+ function warnSkipped(skipped) {
103
+ console.warn('reusable components skipped', skipped);
104
+ }
105
+ /**
106
+ * The published definitions `nodes` places — directly, or through another
107
+ * definition — keyed by id, in the shape `composeReusableComponentNodes`
108
+ * takes.
109
+ *
110
+ * Reads a LEVEL at a time: the ids the document places, then the ids those
111
+ * definitions place, and so on to {@link MAX_COMPONENT_DEPTH}. Each level's
112
+ * reads run in parallel, and an id is never read twice, so a cycle costs one
113
+ * read per component in it. Deleted, never-published, undecodable and missing
114
+ * definitions are left out — a placement of one renders nothing, exactly as it
115
+ * does on a page — and reported through `onSkipped`.
116
+ */ export async function loadReferencedComponents(nodes, read, options = {}) {
117
+ var _options_maxDepth, _options_maxDocuments, _options_onSkipped;
118
+ const maxDepth = (_options_maxDepth = options.maxDepth) != null ? _options_maxDepth : MAX_COMPONENT_DEPTH;
119
+ const maxDocuments = (_options_maxDocuments = options.maxDocuments) != null ? _options_maxDocuments : REFERENCED_COMPONENTS_READ_LIMIT;
120
+ const source = nodes;
121
+ const definitions = {};
122
+ /** Every id already read, refused or skipped — never asked about again. */ const settled = new Set();
123
+ const skipped = [];
124
+ let reads = 0;
125
+ for(let depth = 0; depth < maxDepth; depth++){
126
+ // The whole reference set through what has loaded so far, minus what is
127
+ // settled — which is exactly the next level down.
128
+ const wanted = [
129
+ ...collectReferencedComponentIds(source, definitions)
130
+ ].filter((id)=>!settled.has(id));
131
+ if (!wanted.length) break;
132
+ const batch = [];
133
+ for (const id of wanted){
134
+ settled.add(id);
135
+ if (!isReadableComponentId(id)) {
136
+ skipped.push({
137
+ id,
138
+ reason: 'invalid'
139
+ });
140
+ } else if (reads + batch.length >= maxDocuments) {
141
+ skipped.push({
142
+ id,
143
+ reason: 'limit'
144
+ });
145
+ } else {
146
+ batch.push(id);
147
+ }
148
+ }
149
+ reads += batch.length;
150
+ const results = await Promise.all(batch.map(async (id)=>[
151
+ id,
152
+ readStoredComponentTree(await read(id))
153
+ ]));
154
+ for (const [id, outcome] of results){
155
+ if (outcome.ok === true) definitions[id] = outcome.tree;
156
+ else skipped.push({
157
+ id,
158
+ reason: outcome.reason
159
+ });
160
+ }
161
+ }
162
+ if (skipped.length) ((_options_onSkipped = options.onSkipped) != null ? _options_onSkipped : warnSkipped)(skipped);
163
+ return definitions;
164
+ }
165
+ /**
166
+ * `nodes` with every reusable-component placement expanded from the published
167
+ * definitions it references — {@link loadReferencedComponents} then
168
+ * `composeReusableComponentNodes`.
169
+ *
170
+ * After it, each placement IS its component's root, carrying the placement's
171
+ * id (AGL-2521): a renderer sees the component's own blocks, with the
172
+ * placement's property values (AGL-1247) and attribute overrides (AGL-1899)
173
+ * applied, and knows nothing about components. Tokens the graft does not own
174
+ * — merge tokens, host tokens, site variables — pass through untouched for the
175
+ * renderer that does.
176
+ *
177
+ * A document that places nothing comes back AS GIVEN, the same reference, with
178
+ * no read made. A placement whose component did not load is left exactly as
179
+ * authored, which is how a deleted definition never takes a document down.
180
+ */ export async function composeReferencedComponents(nodes, read, options) {
181
+ if (!collectReferencedComponentIds(nodes).size) return nodes;
182
+ const definitions = await loadReferencedComponents(nodes, read, options);
183
+ return composeReusableComponentNodes(nodes, definitions);
184
+ }
185
+ /**
186
+ * A {@link ReadComponentDocument} over `hosts/{hostId}/components` through the
187
+ * Admin SDK — one document get per component, the four fields a render needs.
188
+ */ export function hostComponentReader(firestore, hostId) {
189
+ return async (componentId)=>{
190
+ const snapshot = await firestore.collection('hosts').doc(hostId).collection('components').doc(componentId).get();
191
+ if (!snapshot.exists) return null;
192
+ return {
193
+ rootId: snapshot.get('rootId'),
194
+ nodes: snapshot.get('nodes'),
195
+ props: snapshot.get('props'),
196
+ deletedAt: snapshot.get('deletedAt')
197
+ };
198
+ };
199
+ }
200
+ /**
201
+ * {@link composeReferencedComponents} for a site's own document, read through
202
+ * the Admin SDK: what every server path that sends, previews or publishes an
203
+ * email composes with.
204
+ *
205
+ * Shaped `(nodes, { firestore, hostId })` so it is passed as it stands wherever
206
+ * a composer is asked for — `loadHostEmail`'s `compose` is one — rather than
207
+ * wrapped at each call site. Skips are logged with the site they belong to.
208
+ */ export function composeHostComponentNodes(nodes, context) {
209
+ const { firestore, hostId } = context;
210
+ return composeReferencedComponents(nodes, hostComponentReader(firestore, hostId), {
211
+ onSkipped: (skipped)=>console.warn(JSON.stringify({
212
+ tag: 'AGL-3287:components-skipped',
213
+ hostId,
214
+ skipped
215
+ }))
216
+ });
217
+ }
218
+
219
+ //# sourceMappingURL=load-referenced-components.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/load-referenced-components.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE COMPONENTS ONE DOCUMENT PLACES, READ ON DEMAND (AGL-3287).\n *\n * A published page composes against every component the site has, read in one\n * collection query and held in the render cache — `getComponents` in the tenant\n * runtime. That is the right trade for a page, which is rendered constantly and\n * served from cache. It is the wrong one for a document rendered on demand: an\n * email sent, test-sent or previewed, a design being published elsewhere. Those\n * are not cached pages, a cache would serve a header the site has since\n * republished, and most of them place no component at all.\n *\n * So this reads ONLY what a node map references, following nesting to the same\n * depth the graft expands ({@link MAX_COMPONENT_DEPTH}), a level at a time, each\n * level in parallel. A document that places nothing costs no read at all.\n *\n * It knows nothing about Firestore. A caller hands it a {@link\n * ReadComponentDocument} — the Admin SDK through {@link hostComponentReader},\n * the client SDK in a console preview — so the same walk serves a send, a\n * browser and a spec's fake. Pure otherwise, and client-safe: no Node builtin,\n * no cache, no Next.\n *\n * What a stored document MEANS as a tree is {@link readStoredComponentTree},\n * which `getComponents` reads its collection through too, so the page and the\n * mail cannot come to disagree about a component's published design.\n */\n\nimport type {\n AglynNodeSchema,\n NodeId,\n ReusableComponentProp,\n} from '../foundation'\nimport {\n collectReferencedComponentIds,\n composeReusableComponentNodes,\n MAX_COMPONENT_DEPTH,\n type ReusableComponentTree,\n} from './compose-reusable-components'\nimport { decodeStoredNodes } from './stored-nodes'\n\n/**\n * The fields of `hosts/{hostId}/components/{id}` a render reads: the PUBLISHED\n * snapshot. `rootId`, `nodes` and the declared `props` live on the component\n * document itself, never on its versions (AGL-679), so one read per component\n * is the whole cost.\n */\nexport interface StoredComponentDocument {\n rootId?: unknown\n nodes?: unknown\n props?: unknown\n deletedAt?: unknown\n}\n\n/** Why a referenced component gave no tree to graft. */\nexport type ComponentSkipReason =\n /** No such document. */\n | 'missing'\n /** Soft-deleted: it renders nothing, wherever it was placed. */\n | 'deleted'\n /** Created but never published — no `rootId`/`nodes` on the document. */\n | 'unpublished'\n /** Its `nodes` could not be decoded; `decodeStoredNodes` logs why. */\n | 'undecodable'\n /** Not usable as one document id, so it was never read (AGL-1771). */\n | 'invalid'\n /** Past {@link REFERENCED_COMPONENTS_READ_LIMIT}; not read. */\n | 'limit'\n\nexport type StoredComponentTreeRead =\n | { ok: true; tree: ReusableComponentTree }\n | { ok: false; reason: Exclude<ComponentSkipReason, 'invalid' | 'limit'> }\n\n/**\n * A stored component document as the tree `composeReusableComponentNodes`\n * grafts, or why it is not one.\n *\n * BOTH STORED FORMS (AGL-1151). A published definition is msgpack for anything\n * promoted since components were compressed and a plain map for everything\n * older, and nothing migrates them — `decodeStoredNodes` returns a map\n * unchanged, so one call serves both forever. Reading the field raw is a\n * SILENT failure: the graft looks up `nodes[rootId]` on a `Buffer`, finds\n * nothing, and every placement of the component renders empty.\n *\n * An undecodable definition is skipped rather than grafted empty. The page or\n * the mail comes out the same either way, but `decodeStoredNodes` logs the\n * reason, and a definition that silently became `{}` would not say why the\n * component vanished.\n */\nexport function readStoredComponentTree(\n stored: StoredComponentDocument | null | undefined,\n): StoredComponentTreeRead {\n if (!stored) return { ok: false, reason: 'missing' }\n if (stored.deletedAt) return { ok: false, reason: 'deleted' }\n if (!stored.nodes || !stored.rootId)\n return { ok: false, reason: 'unpublished' }\n const nodes = decodeStoredNodes<ReusableComponentTree['nodes']>(stored.nodes)\n if (!nodes) return { ok: false, reason: 'undecodable' }\n return {\n ok: true,\n tree: {\n rootId: stored.rootId as NodeId,\n nodes,\n // Declared props (AGL-1247): without these the graft leaves every\n // `{{prop.*}}` token unresolved wherever the component is placed.\n ...(Array.isArray(stored.props) &&\n stored.props.length && {\n props: stored.props as ReusableComponentProp[],\n }),\n },\n }\n}\n\n/**\n * One component document by id — `null` or `undefined` when there is none. A\n * rejection is a failed READ and fails the whole load: an email must not go out\n * missing its footer because a request timed out, and every caller has a\n * better answer to an error than a quietly partial design.\n */\nexport type ReadComponentDocument = (\n componentId: string,\n) => Promise<StoredComponentDocument | null | undefined>\n\n/** A referenced component that gave no tree, and why. */\nexport interface SkippedComponent {\n id: string\n reason: ComponentSkipReason\n}\n\n/**\n * Documents one load may read — the same bound `getComponents` reads a whole\n * site under, so an on-demand render can never cost more than a page does.\n */\nexport const REFERENCED_COMPONENTS_READ_LIMIT = 200\n\nexport interface LoadReferencedComponentsOptions {\n /** Levels of nesting to follow; the graft's own bound by default. */\n maxDepth?: number\n /** Documents one load may read; {@link REFERENCED_COMPONENTS_READ_LIMIT} by default. */\n maxDocuments?: number\n /**\n * Told once, after the load, about every referenced id that gave no tree.\n * The default logs a warning: a placement of a deleted component renders\n * nothing, and nothing else would say why.\n */\n onSkipped?: (skipped: readonly SkippedComponent[]) => void\n}\n\n/**\n * Whether a referenced id may be handed to a reader as ONE document id.\n *\n * `refId` is author-controlled, and `doc()` appends a slash-separated PATH:\n * `half/path` throws where the reference is built and `a/b/c` reads beneath a\n * document that does not exist (AGL-1771, `isDocumentId` in\n * `@aglyn/tenant-data-admin/server/document-id`). That predicate is\n * server-only — it measures with `Buffer` — and this walk also runs in a\n * browser, so the same clauses are restated with `TextEncoder`.\n */\nfunction isReadableComponentId(id: string): boolean {\n return (\n id.length > 0 &&\n !id.includes('/') &&\n id !== '.' &&\n id !== '..' &&\n !/^__.*__$/.test(id) &&\n new TextEncoder().encode(id).length <= 1500\n )\n}\n\nfunction warnSkipped(skipped: readonly SkippedComponent[]): void {\n console.warn('reusable components skipped', skipped)\n}\n\n/**\n * The published definitions `nodes` places — directly, or through another\n * definition — keyed by id, in the shape `composeReusableComponentNodes`\n * takes.\n *\n * Reads a LEVEL at a time: the ids the document places, then the ids those\n * definitions place, and so on to {@link MAX_COMPONENT_DEPTH}. Each level's\n * reads run in parallel, and an id is never read twice, so a cycle costs one\n * read per component in it. Deleted, never-published, undecodable and missing\n * definitions are left out — a placement of one renders nothing, exactly as it\n * does on a page — and reported through `onSkipped`.\n */\nexport async function loadReferencedComponents(\n nodes: Readonly<Record<string, unknown>> | null | undefined,\n read: ReadComponentDocument,\n options: LoadReferencedComponentsOptions = {},\n): Promise<Record<string, ReusableComponentTree>> {\n const maxDepth = options.maxDepth ?? MAX_COMPONENT_DEPTH\n const maxDocuments = options.maxDocuments ?? REFERENCED_COMPONENTS_READ_LIMIT\n const source = nodes as Record<string, AglynNodeSchema | undefined> | null\n const definitions: Record<string, ReusableComponentTree> = {}\n /** Every id already read, refused or skipped — never asked about again. */\n const settled = new Set<string>()\n const skipped: SkippedComponent[] = []\n let reads = 0\n\n for (let depth = 0; depth < maxDepth; depth++) {\n // The whole reference set through what has loaded so far, minus what is\n // settled — which is exactly the next level down.\n const wanted = [\n ...collectReferencedComponentIds(source, definitions),\n ].filter((id) => !settled.has(id))\n if (!wanted.length) break\n const batch: string[] = []\n for (const id of wanted) {\n settled.add(id)\n if (!isReadableComponentId(id)) {\n skipped.push({ id, reason: 'invalid' })\n } else if (reads + batch.length >= maxDocuments) {\n skipped.push({ id, reason: 'limit' })\n } else {\n batch.push(id)\n }\n }\n reads += batch.length\n const results = await Promise.all(\n batch.map(\n async (id) => [id, readStoredComponentTree(await read(id))] as const,\n ),\n )\n for (const [id, outcome] of results) {\n if (outcome.ok === true) definitions[id] = outcome.tree\n else skipped.push({ id, reason: outcome.reason })\n }\n }\n\n if (skipped.length) (options.onSkipped ?? warnSkipped)(skipped)\n return definitions\n}\n\n/**\n * `nodes` with every reusable-component placement expanded from the published\n * definitions it references — {@link loadReferencedComponents} then\n * `composeReusableComponentNodes`.\n *\n * After it, each placement IS its component's root, carrying the placement's\n * id (AGL-2521): a renderer sees the component's own blocks, with the\n * placement's property values (AGL-1247) and attribute overrides (AGL-1899)\n * applied, and knows nothing about components. Tokens the graft does not own\n * — merge tokens, host tokens, site variables — pass through untouched for the\n * renderer that does.\n *\n * A document that places nothing comes back AS GIVEN, the same reference, with\n * no read made. A placement whose component did not load is left exactly as\n * authored, which is how a deleted definition never takes a document down.\n */\nexport async function composeReferencedComponents<\n M extends Readonly<Record<string, unknown>>,\n>(\n nodes: M,\n read: ReadComponentDocument,\n options?: LoadReferencedComponentsOptions,\n): Promise<M> {\n if (!collectReferencedComponentIds(nodes as never).size) return nodes\n const definitions = await loadReferencedComponents(nodes, read, options)\n return composeReusableComponentNodes(\n nodes as unknown as Record<NodeId, AglynNodeSchema>,\n definitions,\n ) as unknown as M\n}\n\n/** A document snapshot, as far as {@link hostComponentReader} reads one. */\ninterface ComponentSnapshotLike {\n exists: boolean\n get(field: string): unknown\n}\ninterface ComponentDocumentRefLike {\n get(): Promise<ComponentSnapshotLike>\n collection(path: string): ComponentCollectionRefLike\n}\ninterface ComponentCollectionRefLike {\n doc(id: string): ComponentDocumentRefLike\n}\n\n/**\n * The slice of the Admin Firestore chain {@link hostComponentReader} walks.\n * Structural, so a caller holding any Admin handle — or a spec's fake — passes\n * it without a cast, and so this core module takes no `firebase-admin`\n * dependency. `@aglyn/shared-util-email`'s `AdminFirestoreLike` satisfies it.\n */\nexport interface ComponentStoreLike {\n collection(path: string): ComponentCollectionRefLike\n}\n\n/**\n * A {@link ReadComponentDocument} over `hosts/{hostId}/components` through the\n * Admin SDK — one document get per component, the four fields a render needs.\n */\nexport function hostComponentReader(\n firestore: ComponentStoreLike,\n hostId: string,\n): ReadComponentDocument {\n return async (componentId) => {\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('components')\n .doc(componentId)\n .get()\n if (!snapshot.exists) return null\n return {\n rootId: snapshot.get('rootId'),\n nodes: snapshot.get('nodes'),\n props: snapshot.get('props'),\n deletedAt: snapshot.get('deletedAt'),\n }\n }\n}\n\n/**\n * {@link composeReferencedComponents} for a site's own document, read through\n * the Admin SDK: what every server path that sends, previews or publishes an\n * email composes with.\n *\n * Shaped `(nodes, { firestore, hostId })` so it is passed as it stands wherever\n * a composer is asked for — `loadHostEmail`'s `compose` is one — rather than\n * wrapped at each call site. Skips are logged with the site they belong to.\n */\nexport function composeHostComponentNodes<\n M extends Readonly<Record<string, unknown>>,\n>(\n nodes: M,\n context: { firestore: ComponentStoreLike; hostId: string },\n): Promise<M> {\n const { firestore, hostId } = context\n return composeReferencedComponents(\n nodes,\n hostComponentReader(firestore, hostId),\n {\n onSkipped: (skipped) =>\n console.warn(\n JSON.stringify({\n tag: 'AGL-3287:components-skipped',\n hostId,\n skipped,\n }),\n ),\n },\n )\n}\n"],"names":["collectReferencedComponentIds","composeReusableComponentNodes","MAX_COMPONENT_DEPTH","decodeStoredNodes","readStoredComponentTree","stored","ok","reason","deletedAt","nodes","rootId","tree","Array","isArray","props","length","REFERENCED_COMPONENTS_READ_LIMIT","isReadableComponentId","id","includes","test","TextEncoder","encode","warnSkipped","skipped","console","warn","loadReferencedComponents","read","options","maxDepth","maxDocuments","source","definitions","settled","Set","reads","depth","wanted","filter","has","batch","add","push","results","Promise","all","map","outcome","onSkipped","composeReferencedComponents","size","hostComponentReader","firestore","hostId","componentId","snapshot","collection","doc","get","exists","composeHostComponentNodes","context","JSON","stringify","tag"],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { HostViewType, type ReusableComponentKind } from '../foundation/definitions/platform.types';
18
+ /**
19
+ * Where a reusable component is placed, and every answer that follows from it
20
+ * (AGL-3287) — see {@link ReusableComponentKind}.
21
+ *
22
+ * One module so the questions are answered from one place: which kind a
23
+ * promotion makes, which view a component's own editor opens in, which drawer
24
+ * group offers it, and which starters a new email block can begin from. The
25
+ * console, the designer and the plugins each ask one of them, and a second
26
+ * answer to any would let a component be saved as one kind and offered as the
27
+ * other.
28
+ */
29
+ /** Placed on pages: what every component made before AGL-3287 is. */
30
+ export declare const REUSABLE_COMPONENT_KIND_SITE: ReusableComponentKind;
31
+ /** Placed in emails: a reusable email block, such as a header or a footer. */
32
+ export declare const REUSABLE_COMPONENT_KIND_EMAIL: ReusableComponentKind;
33
+ /** Every value a component may be stored with. */
34
+ export declare const REUSABLE_COMPONENT_KINDS: readonly ReusableComponentKind[];
35
+ /**
36
+ * The bundle whose blocks an email's drawer offers (AGL-395).
37
+ *
38
+ * The EMAIL view keeps a drawer entry only when the entry's own `pluginId`
39
+ * names this bundle, and every other view drops such an entry. A reusable
40
+ * email block is filed under it for exactly that reason — it is the whole of
41
+ * how one reaches an email's drawer and never a page's.
42
+ */
43
+ export declare const EMAIL_VIEW_BUNDLE_ID = "email";
44
+ /** Whether a value is one a component may be stored with. */
45
+ export declare function isReusableComponentKind(value: unknown): value is ReusableComponentKind;
46
+ /**
47
+ * The kind a component document says it is.
48
+ *
49
+ * Anything but `email` — absent, `site`, or a value no reader knows — is a
50
+ * page component, which is what every component was before the field
51
+ * existed. Reading it any other way would move components nobody touched out
52
+ * of the drawer their pages use.
53
+ */
54
+ export declare function reusableComponentKindOf(component: {
55
+ kind?: unknown;
56
+ } | null | undefined): ReusableComponentKind;
57
+ /**
58
+ * The kind a component saved from an editor showing this view is: an email
59
+ * block when it was promoted out of an email, a page component otherwise.
60
+ *
61
+ * The view decides because the view decides what the subtree can be made of.
62
+ * An email's drawer offers email blocks alone, so whatever is promoted there
63
+ * is built from them — and renders in an email and nowhere else.
64
+ */
65
+ export declare function reusableComponentKindForView(viewType: unknown): ReusableComponentKind;
66
+ /**
67
+ * The kind a promoted subtree makes, read from the element being promoted:
68
+ * an email block when that element is one of the email bundle's.
69
+ *
70
+ * For a surface that reads the canvas but cannot see the designer's view — a
71
+ * plugin's own save action. It agrees with {@link reusableComponentKindForView}
72
+ * wherever both can be asked, because an email offers email blocks alone and
73
+ * a page never offers one.
74
+ */
75
+ export declare function reusableComponentKindForRoot(root: {
76
+ pluginId?: unknown;
77
+ } | null | undefined): ReusableComponentKind;
78
+ /**
79
+ * The view a component's own editor opens in.
80
+ *
81
+ * An email block edits in the EMAIL view, so its drawer offers what an
82
+ * email's does and its canvas draws what an email's does. A page component
83
+ * sets none, which leaves the editor in the screen view it has always used —
84
+ * deliberately not LAYOUT, whose slot outlet has nowhere to graft inside a
85
+ * component (AGL-680).
86
+ */
87
+ export declare function reusableComponentEditorView(kind: ReusableComponentKind): HostViewType | undefined;
88
+ /** Where a component's entry sits in the element drawer. */
89
+ export interface ReusableComponentPaletteSlot {
90
+ /** The drawer group the entry is listed under. */
91
+ category: string;
92
+ /** The bundle the entry is filed under, which the view filter reads. */
93
+ pluginId?: string;
94
+ }
95
+ /**
96
+ * The drawer group and bundle a component's entry is registered with.
97
+ *
98
+ * An email block is filed under {@link EMAIL_VIEW_BUNDLE_ID}, so the EMAIL
99
+ * view keeps it and every other view drops it — the rule the email plugin's
100
+ * own blocks already follow. A page component names no bundle, as it never
101
+ * has: it belongs to no plugin, so the email view drops it and no plugin
102
+ * switch on the site can hide it.
103
+ *
104
+ * Only the ENTRY is filed this way. The node it inserts is an ordinary
105
+ * instance, the same on a page and in an email, which is what lets the
106
+ * canvas draw either through one graft.
107
+ */
108
+ export declare function reusableComponentPaletteSlot(kind: ReusableComponentKind): ReusableComponentPaletteSlot;
109
+ /** What a new email block can start as on the Components page. */
110
+ export type ReusableEmailBlockStarter = 'header' | 'footer' | 'blank';
111
+ /**
112
+ * The element presets a Header or a Footer email block starts from, by id.
113
+ *
114
+ * Named here and registered by the email plugin, which owns the trees: the
115
+ * Components page builds its starters from the same presets an author drops
116
+ * into an email, so each starter is one tree wherever it is started from.
117
+ * `blank` has no preset — it is the empty canvas every new component opens on.
118
+ */
119
+ export declare const REUSABLE_EMAIL_BLOCK_STARTER_PRESET_IDS: Readonly<Record<Exclude<ReusableEmailBlockStarter, 'blank'>, string>>;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { REUSABLE_COMPONENT_CATEGORY, REUSABLE_EMAIL_BLOCK_CATEGORY } from "../foundation/constants/components.js";
17
+ import { HostViewType } from "../foundation/definitions/platform.types.js";
18
+ /**
19
+ * Where a reusable component is placed, and every answer that follows from it
20
+ * (AGL-3287) — see {@link ReusableComponentKind}.
21
+ *
22
+ * One module so the questions are answered from one place: which kind a
23
+ * promotion makes, which view a component's own editor opens in, which drawer
24
+ * group offers it, and which starters a new email block can begin from. The
25
+ * console, the designer and the plugins each ask one of them, and a second
26
+ * answer to any would let a component be saved as one kind and offered as the
27
+ * other.
28
+ */ /** Placed on pages: what every component made before AGL-3287 is. */ export const REUSABLE_COMPONENT_KIND_SITE = 'site';
29
+ /** Placed in emails: a reusable email block, such as a header or a footer. */ export const REUSABLE_COMPONENT_KIND_EMAIL = 'email';
30
+ /** Every value a component may be stored with. */ export const REUSABLE_COMPONENT_KINDS = [
31
+ REUSABLE_COMPONENT_KIND_SITE,
32
+ REUSABLE_COMPONENT_KIND_EMAIL
33
+ ];
34
+ /**
35
+ * The bundle whose blocks an email's drawer offers (AGL-395).
36
+ *
37
+ * The EMAIL view keeps a drawer entry only when the entry's own `pluginId`
38
+ * names this bundle, and every other view drops such an entry. A reusable
39
+ * email block is filed under it for exactly that reason — it is the whole of
40
+ * how one reaches an email's drawer and never a page's.
41
+ */ export const EMAIL_VIEW_BUNDLE_ID = 'email';
42
+ /** Whether a value is one a component may be stored with. */ export function isReusableComponentKind(value) {
43
+ return REUSABLE_COMPONENT_KINDS.includes(value);
44
+ }
45
+ /**
46
+ * The kind a component document says it is.
47
+ *
48
+ * Anything but `email` — absent, `site`, or a value no reader knows — is a
49
+ * page component, which is what every component was before the field
50
+ * existed. Reading it any other way would move components nobody touched out
51
+ * of the drawer their pages use.
52
+ */ export function reusableComponentKindOf(component) {
53
+ return (component == null ? void 0 : component.kind) === REUSABLE_COMPONENT_KIND_EMAIL ? REUSABLE_COMPONENT_KIND_EMAIL : REUSABLE_COMPONENT_KIND_SITE;
54
+ }
55
+ /**
56
+ * The kind a component saved from an editor showing this view is: an email
57
+ * block when it was promoted out of an email, a page component otherwise.
58
+ *
59
+ * The view decides because the view decides what the subtree can be made of.
60
+ * An email's drawer offers email blocks alone, so whatever is promoted there
61
+ * is built from them — and renders in an email and nowhere else.
62
+ */ export function reusableComponentKindForView(viewType) {
63
+ return viewType === HostViewType.EMAIL ? REUSABLE_COMPONENT_KIND_EMAIL : REUSABLE_COMPONENT_KIND_SITE;
64
+ }
65
+ /**
66
+ * The kind a promoted subtree makes, read from the element being promoted:
67
+ * an email block when that element is one of the email bundle's.
68
+ *
69
+ * For a surface that reads the canvas but cannot see the designer's view — a
70
+ * plugin's own save action. It agrees with {@link reusableComponentKindForView}
71
+ * wherever both can be asked, because an email offers email blocks alone and
72
+ * a page never offers one.
73
+ */ export function reusableComponentKindForRoot(root) {
74
+ return (root == null ? void 0 : root.pluginId) === EMAIL_VIEW_BUNDLE_ID ? REUSABLE_COMPONENT_KIND_EMAIL : REUSABLE_COMPONENT_KIND_SITE;
75
+ }
76
+ /**
77
+ * The view a component's own editor opens in.
78
+ *
79
+ * An email block edits in the EMAIL view, so its drawer offers what an
80
+ * email's does and its canvas draws what an email's does. A page component
81
+ * sets none, which leaves the editor in the screen view it has always used —
82
+ * deliberately not LAYOUT, whose slot outlet has nowhere to graft inside a
83
+ * component (AGL-680).
84
+ */ export function reusableComponentEditorView(kind) {
85
+ return kind === REUSABLE_COMPONENT_KIND_EMAIL ? HostViewType.EMAIL : undefined;
86
+ }
87
+ /**
88
+ * The drawer group and bundle a component's entry is registered with.
89
+ *
90
+ * An email block is filed under {@link EMAIL_VIEW_BUNDLE_ID}, so the EMAIL
91
+ * view keeps it and every other view drops it — the rule the email plugin's
92
+ * own blocks already follow. A page component names no bundle, as it never
93
+ * has: it belongs to no plugin, so the email view drops it and no plugin
94
+ * switch on the site can hide it.
95
+ *
96
+ * Only the ENTRY is filed this way. The node it inserts is an ordinary
97
+ * instance, the same on a page and in an email, which is what lets the
98
+ * canvas draw either through one graft.
99
+ */ export function reusableComponentPaletteSlot(kind) {
100
+ return kind === REUSABLE_COMPONENT_KIND_EMAIL ? {
101
+ category: REUSABLE_EMAIL_BLOCK_CATEGORY,
102
+ pluginId: EMAIL_VIEW_BUNDLE_ID
103
+ } : {
104
+ category: REUSABLE_COMPONENT_CATEGORY
105
+ };
106
+ }
107
+ /**
108
+ * The element presets a Header or a Footer email block starts from, by id.
109
+ *
110
+ * Named here and registered by the email plugin, which owns the trees: the
111
+ * Components page builds its starters from the same presets an author drops
112
+ * into an email, so each starter is one tree wherever it is started from.
113
+ * `blank` has no preset — it is the empty canvas every new component opens on.
114
+ */ export const REUSABLE_EMAIL_BLOCK_STARTER_PRESET_IDS = {
115
+ header: 'email:emailSection.header',
116
+ footer: 'email:emailSection.footer'
117
+ };
118
+
119
+ //# sourceMappingURL=reusable-component-kind.js.map