@aglyn/tenant-data-admin 1.0.0-beta.184 → 1.0.0-beta.186
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/tenant-data-admin",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.186",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-util-fbserver": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.186",
|
|
29
|
+
"@aglyn/shared-util-email": "1.0.0-beta.186",
|
|
30
|
+
"@aglyn/shared-util-fbserver": "1.0.0-beta.186",
|
|
31
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.186",
|
|
32
|
+
"@aglyn/shared-util-http": "1.0.0-beta.186",
|
|
33
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.186",
|
|
34
34
|
"@msgpack/msgpack": "^3.1.3",
|
|
35
35
|
"@swc/helpers": "0.5.23",
|
|
36
36
|
"sharp": "^0.35.3",
|
|
@@ -52,6 +52,11 @@ export interface UsageCandidate {
|
|
|
52
52
|
layoutId?: string;
|
|
53
53
|
/** Screens only: the screen they nest under, which is part of their path. */
|
|
54
54
|
parentId?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Screens only: `'email'` for a campaign email design (AGL-3287), which
|
|
57
|
+
* a scan reports as an `emailDesign` rather than a page.
|
|
58
|
+
*/
|
|
59
|
+
kind?: string;
|
|
55
60
|
/**
|
|
56
61
|
* Components only: the properties the definition declares (AGL-1247). A
|
|
57
62
|
* Link property's default renders as a link wherever an instance leaves the
|
|
@@ -63,11 +68,15 @@ export interface UsageCandidate {
|
|
|
63
68
|
export interface UsageDependent {
|
|
64
69
|
/**
|
|
65
70
|
* `emailTemplate` is one of the site's own transactional emails
|
|
66
|
-
* (`hosts/{h}/emailTemplates/{key}`), `id` its catalog key (AGL-3287).
|
|
67
|
-
*
|
|
68
|
-
* `
|
|
71
|
+
* (`hosts/{h}/emailTemplates/{key}`), `id` its catalog key (AGL-3287).
|
|
72
|
+
*
|
|
73
|
+
* `emailDesign` is an email designed for a campaign: a `kind: 'email'`
|
|
74
|
+
* screen document, `id` its screen id. It is its own type rather than a
|
|
75
|
+
* `screen` because it is never a page. A caller that took it for one sent
|
|
76
|
+
* the reader to the page settings — a slug, SEO and a Publish button that
|
|
77
|
+
* would serve the email as a route — and a cache drop has no page to drop.
|
|
69
78
|
*/
|
|
70
|
-
type: 'screen' | 'layout' | 'component' | 'collection' | 'emailTemplate';
|
|
79
|
+
type: 'screen' | 'layout' | 'component' | 'collection' | 'emailTemplate' | 'emailDesign';
|
|
71
80
|
id: string;
|
|
72
81
|
name: string;
|
|
73
82
|
via: Array<'id' | 'name'>;
|
|
@@ -59,7 +59,7 @@ import { getTenantEmail, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-emai
|
|
|
59
59
|
if (type === 'component' && candidate.id === componentId) continue;
|
|
60
60
|
if (!nodesReferenceComponent(candidate.nodes, componentId)) continue;
|
|
61
61
|
dependents.push(_extends({
|
|
62
|
-
type,
|
|
62
|
+
type: type === 'screen' && candidate.kind === 'email' ? 'emailDesign' : type,
|
|
63
63
|
id: candidate.id,
|
|
64
64
|
name: usageCandidateLabel(candidate),
|
|
65
65
|
// Instances reference by id, so a rename can never break them.
|
|
@@ -208,10 +208,11 @@ import { getTenantEmail, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-emai
|
|
|
208
208
|
seenComponents.add(dependent.id);
|
|
209
209
|
next.push(dependent.id);
|
|
210
210
|
}
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
211
|
+
// An email — transactional or designed for a campaign — is not a page
|
|
212
|
+
// and nothing caches it, so it contributes no screen and is not
|
|
213
|
+
// followed (AGL-3287). A transactional email's id is a catalog key,
|
|
214
|
+
// and walking it as a component would read a key as a definition that
|
|
215
|
+
// does not exist.
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
frontier = next;
|
|
@@ -273,6 +274,8 @@ import { getTenantEmail, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-emai
|
|
|
273
274
|
layoutId: String(docSnapshot.get('layoutId'))
|
|
274
275
|
} : {}, docSnapshot.get('parentId') ? {
|
|
275
276
|
parentId: String(docSnapshot.get('parentId'))
|
|
277
|
+
} : {}, collectionName === 'screens' && docSnapshot.get('kind') ? {
|
|
278
|
+
kind: String(docSnapshot.get('kind'))
|
|
276
279
|
} : {}, collectionName === 'components' && Array.isArray(docSnapshot.get('props')) ? {
|
|
277
280
|
props: docSnapshot.get('props')
|
|
278
281
|
} : {});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/live-page-usage.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 * Which live pages a change reaches, for a site's screens, layouts and\n * components.\n *\n * The corpus read and the two closures over it, held here rather than in the\n * console because the console is no longer the only side that needs them. A\n * dataset record is written from the tenant as well — a form submission, an\n * automation step — and those writes make the same pages stale as a console\n * edit does. Two implementations of \"which screens render this\" is the shape\n * that put AGL-1223 in the tree: one reader handled a node tree's second\n * storage form and the other did not, so half the corpus answered \"used\n * nowhere\" and a publish dropped no cache.\n *\n * Nothing here is dataset-specific, and nothing is console-specific. The\n * console's `scan-artifact-usage` re-exports these and keeps the parts that\n * are about the console's own \"what would I break\" copy.\n */\n\nimport {\n nodesReferenceComponent,\n decodeStoredNodes,\n type ReusableComponentProp,\n} from '@aglyn/aglyn/server'\nimport {\n getTenantEmail,\n TENANT_EMAIL_COLLECTION,\n} from '@aglyn/shared-util-email/tenant-email-catalog'\n\n/** A screen/layout/component reduced to what a usage scan needs. */\nexport interface UsageCandidate {\n id: string\n displayName?: string\n /** Legacy field some older documents used instead of `displayName`. */\n name?: string\n deletedAt?: unknown\n /**\n * Node tree to search. For screens and layouts this is the PUBLISHED\n * version's nodes (what visitors see); for components it is the definition\n * tree off the component document, which is what the runtime reads.\n */\n nodes?: Record<string, any> | null\n /** Published version, carried through so the caller can deep-link. */\n versionId?: string\n /** Screens only: the layout they render inside. */\n layoutId?: string\n /** Screens only: the screen they nest under, which is part of their path. */\n parentId?: string\n /**\n * Components only: the properties the definition declares (AGL-1247). A\n * Link property's default renders as a link wherever an instance leaves the\n * property unset, and it is stored here, not in `nodes` (AGL-2846).\n */\n props?: ReadonlyArray<ReusableComponentProp | null | undefined> | null\n}\n\n/** One document that depends on the artifact a scan was asked about. */\nexport interface UsageDependent {\n /**\n * `emailTemplate` is one of the site's own transactional emails\n * (`hosts/{h}/emailTemplates/{key}`), `id` its catalog key (AGL-3287). An\n * email designed for a campaign is a `kind: 'email'` SCREEN and reports as\n * `screen`.\n */\n type: 'screen' | 'layout' | 'component' | 'collection' | 'emailTemplate'\n id: string\n name: string\n via: Array<'id' | 'name'>\n versionId?: string\n /**\n * HOW the dependent references the artifact — screens and collection\n * listings (AGL-703, AGL-2806).\n *\n * A component or a layout has exactly one kind of dependent and the noun\n * says everything: an instance, or a binding. A screen has three, and they\n * break in three different ways — a link goes dead, a child moves, a\n * collection loses the page it renders through. Copy that could not tell\n * them apart would have to describe the worst case every time. A collection\n * listing's dependents are all links, and say so.\n */\n relation?: 'link' | 'child' | 'template'\n}\n\n/** The three corpora every closure below walks, and a fourth for components. */\nexport interface UsageSources {\n screens: UsageCandidate[]\n layouts: UsageCandidate[]\n components: UsageCandidate[]\n /**\n * The site's transactional email designs, each on its published version\n * (AGL-3287). Read only where a scan asks \"what places this component\" —\n * {@link scanComponentUsage} lists them — because an email is not a page:\n * nothing is cached for it, so no closure that decides which pages to drop\n * has any use for them, and none reads them.\n */\n emailTemplates?: UsageCandidate[]\n}\n\n/** `displayName`, falling back to a legacy `name`, then the raw id. */\nexport function usageCandidateLabel(candidate: UsageCandidate): string {\n return String(candidate.displayName ?? candidate.name ?? candidate.id)\n}\n\n/** A soft-deleted document renders nothing, so it depends on nothing. */\nexport const isLiveUsageCandidate = (candidate: UsageCandidate): boolean =>\n !candidate.deletedAt\n\n/**\n * Everything that references a reusable component (AGL-703).\n *\n * Every place the renderer expands an instance: published screen versions\n * (email designs among them — a campaign email is a screen), published layout\n * versions, OTHER component definitions — `composeReusableComponentNodes`\n * grafts nested instances, so a component used only inside another component\n * is genuinely used — and, when the caller read them, the site's transactional\n * emails, which graft a placed header or footer at send time (AGL-3287).\n * Omitting any of them would report \"used nowhere\" for something that renders\n * and invite a confident deletion, which is worse than showing nothing at all.\n */\nexport function scanComponentUsage(\n componentId: string,\n sources: UsageSources,\n): UsageDependent[] {\n if (!componentId) return []\n const dependents: UsageDependent[] = []\n const collect = (\n candidates: UsageCandidate[],\n type: UsageDependent['type'],\n ) => {\n for (const candidate of candidates) {\n if (!isLiveUsageCandidate(candidate)) continue\n // A component never counts as using itself, however it nests.\n if (type === 'component' && candidate.id === componentId) continue\n if (!nodesReferenceComponent(candidate.nodes, componentId)) continue\n dependents.push({\n type,\n id: candidate.id,\n name: usageCandidateLabel(candidate),\n // Instances reference by id, so a rename can never break them.\n via: ['id'],\n ...(candidate.versionId ? { versionId: candidate.versionId } : {}),\n })\n }\n }\n collect(sources.screens, 'screen')\n collect(sources.layouts, 'layout')\n collect(sources.components, 'component')\n collect(sources.emailTemplates ?? [], 'emailTemplate')\n return dependents\n}\n\n/**\n * Everything rendering inside a layout (AGL-703).\n *\n * Two kinds of dependent, both expressed by the same `layoutId` pointer:\n *\n * - **screens**, which name the layout they render inside;\n * - **other layouts**, since a layout can itself sit inside one. A nested\n * layout is a real dependent — deleting its parent unwraps every screen\n * underneath it — so leaving layouts out would report a parent layout as\n * used only by the screens that name it directly, and none of the ones\n * that reach it through a child.\n *\n * A layout never counts as its own dependent; `canNestLayout` refuses that,\n * and this refuses to report it even if stored data holds one.\n */\nexport function scanLayoutUsage(\n layoutId: string,\n screens: UsageCandidate[],\n layouts: UsageCandidate[] = [],\n): UsageDependent[] {\n if (!layoutId) return []\n const dependentsOf = (\n candidates: UsageCandidate[],\n type: 'screen' | 'layout',\n ) =>\n candidates\n .filter(\n (candidate) =>\n isLiveUsageCandidate(candidate) &&\n candidate.layoutId === layoutId &&\n candidate.id !== layoutId,\n )\n .map((candidate) => ({\n type,\n id: candidate.id,\n name: usageCandidateLabel(candidate),\n via: ['id' as const],\n ...(candidate.versionId ? { versionId: candidate.versionId } : {}),\n }))\n return [\n ...dependentsOf(screens, 'screen'),\n ...dependentsOf(layouts, 'layout'),\n ]\n}\n\n/**\n * Every live screen rendered inside `layoutId`, at ANY nesting depth\n * (AGL-1150).\n *\n * `scanLayoutUsage` answers one level. Layouts nest — a screen points at a\n * layout, which can point at a parent layout, and `compose-screen-nodes` walks\n * that whole chain when composing a page. So publishing a layout changes every\n * screen below it, not just the ones bound to it directly, and a cache drop\n * that only handles the direct level leaves the rest showing stale chrome for\n * the full revalidate window.\n *\n * Pure, and separate from the Firestore read, so the nesting behaviour is\n * testable without a database.\n *\n * Cycle-safe. `canNestLayout` refuses to create a cycle, but a document written\n * straight to Firestore is not bound by that, and a cycle here would hang a\n * publish request rather than surface anything.\n */\nexport function screenIdsUsingLayoutDeep(\n layoutId: string,\n screens: UsageCandidate[],\n layouts: UsageCandidate[] = [],\n): string[] {\n if (!layoutId) return []\n const screenIds = new Set<string>()\n const seenLayouts = new Set<string>([layoutId])\n let frontier = [layoutId]\n\n while (frontier.length) {\n const next: string[] = []\n for (const id of frontier) {\n for (const dependent of scanLayoutUsage(id, screens, layouts)) {\n if (dependent.type === 'screen') {\n screenIds.add(dependent.id)\n } else if (!seenLayouts.has(dependent.id)) {\n seenLayouts.add(dependent.id)\n next.push(dependent.id)\n }\n }\n }\n frontier = next\n }\n\n return [...screenIds]\n}\n\n/**\n * Every live screen whose rendered output contains `componentId`, however\n * indirectly (AGL-1161).\n *\n * `scanComponentUsage` answers one level and returns three kinds of dependent.\n * Only one of them is a screen, and the other two both reach screens by routes\n * a single-level scan cannot see:\n *\n * - a **component** dependent nests the target inside itself, and that outer\n * component may itself only be used inside a third — so component→component\n * edges have to be followed to a fixed point;\n * - a **layout** dependent puts the component in page chrome, which every\n * screen under that layout renders. Layouts nest, so that is\n * `screenIdsUsingLayoutDeep`, not a direct `layoutId` match.\n *\n * Miss either and a publish reports success while some pages keep serving the\n * old component for the full revalidate window — the failure this whole arc\n * exists to remove, and the one that is hardest to notice because the pages\n * that ARE dropped update instantly.\n *\n * Pure, and separate from the Firestore read, so the closure is testable\n * without a database — the same split `screenIdsUsingLayoutDeep` uses.\n *\n * Cycle-safe. `composeReusableComponentNodes` would not survive a cycle, but a\n * document written straight to Firestore is not bound by what the editor\n * allows, and a cycle here would hang a publish rather than surface anything.\n */\nexport function screenIdsUsingComponentDeep(\n componentId: string,\n sources: UsageSources,\n): string[] {\n if (!componentId) return []\n const screenIds = new Set<string>()\n const seenComponents = new Set<string>([componentId])\n // Layouts are resolved through their own deep walk, so remember which ones\n // have already been expanded: two components in the same layout would\n // otherwise re-walk the whole layout tree once each.\n const seenLayouts = new Set<string>()\n let frontier = [componentId]\n\n while (frontier.length) {\n const next: string[] = []\n for (const id of frontier) {\n for (const dependent of scanComponentUsage(id, sources)) {\n if (dependent.type === 'screen') {\n screenIds.add(dependent.id)\n } else if (dependent.type === 'layout') {\n if (seenLayouts.has(dependent.id)) continue\n seenLayouts.add(dependent.id)\n // The layout itself renders no URL; the screens beneath it do.\n for (const screenId of screenIdsUsingLayoutDeep(\n dependent.id,\n sources.screens,\n sources.layouts,\n )) {\n screenIds.add(screenId)\n }\n } else if (\n dependent.type === 'component' &&\n !seenComponents.has(dependent.id)\n ) {\n seenComponents.add(dependent.id)\n next.push(dependent.id)\n }\n // A transactional email is not a page and nothing caches it, so it\n // contributes no screen and is not followed (AGL-3287). Its id is a\n // catalog key, and walking it as a component would read a key as a\n // definition that does not exist.\n }\n }\n frontier = next\n }\n\n return [...screenIds]\n}\n\nexport interface UsageCandidateRead {\n candidates: UsageCandidate[]\n /**\n * The collection held more documents than `limit` allowed, so the scan below\n * it is INCOMPLETE.\n *\n * Returned rather than logged, because the two callers owe the user\n * different things: an advisory \"what would I break\" can show a partial\n * answer and say so, while a cache drop that silently scans a prefix reports\n * a successful publish and leaves real pages stale.\n */\n truncated: boolean\n}\n\n/**\n * One collection's documents, with published nodes attached when the scan\n * needs to search them.\n *\n * `limit` is a real bound, not a guess: it is fetched with one extra document\n * so exceeding it is DETECTED rather than assumed away. A caller that ignores\n * `truncated` is choosing to be wrong quietly.\n *\n * `emailTemplates` — the site's transactional email designs (AGL-3287) — has\n * the screens' shape: a parent holding the published `versionId`, the tree on\n * that version. The catalog is code and fixed, and a template document only\n * exists once somebody pressed Design, so most sites have none and the read\n * costs one empty query. Its documents carry no name, so each is labelled with\n * the catalog's.\n */\nexport async function readUsageCandidates(\n hostRef: FirebaseFirestore.DocumentReference,\n collectionName:\n | 'screens'\n | 'layouts'\n | 'components'\n | typeof TENANT_EMAIL_COLLECTION,\n options: { withNodes: boolean; limit: number },\n): Promise<UsageCandidateRead> {\n const { withNodes, limit } = options\n // One over the limit: if the extra document comes back, there was more than\n // we are about to look at. Cheaper than a count() and exact.\n const docs = await hostRef.collection(collectionName).limit(limit + 1).get()\n const truncated = docs.size > limit\n const inScope = truncated ? docs.docs.slice(0, limit) : docs.docs\n\n const candidates = await Promise.all(\n inScope.map(async (docSnapshot) => {\n const versionId = docSnapshot.get('versionId')\n // Components keep their tree on the document; screens and layouts keep\n // it on the published version.\n //\n // BOTH reads are decoded (AGL-1223). A component document is msgpack\n // for anything promoted since components were compressed and a plain\n // map for everything older, and `decodeStoredNodes` returns a map\n // unchanged — so the branch below cannot be simplified back into a raw\n // read on either side.\n //\n // What rides on this: every consumer walks the value with\n // `Object.values`, which over a `Buffer` yields byte NUMBERS and\n // matches nothing. The answer that produces is \"used nowhere\" — for\n // `/api/hosts/where-used` an invitation to delete something a live page\n // renders, and for `/api/screens/revalidate` a publish that drops no\n // cache and leaves the old page served.\n const nodes =\n collectionName === 'components'\n ? decodeStoredNodes(docSnapshot.get('nodes'))\n : withNodes && versionId\n ? await docSnapshot.ref\n .collection('versions')\n .doc(String(versionId))\n .get()\n .then((version) => decodeStoredNodes(version.get('nodes')))\n .catch(() => null)\n : null\n return {\n id: docSnapshot.id,\n displayName:\n collectionName === TENANT_EMAIL_COLLECTION\n ? (getTenantEmail(docSnapshot.id)?.name ?? docSnapshot.id)\n : docSnapshot.get('displayName'),\n name: docSnapshot.get('name'),\n deletedAt: docSnapshot.get('deletedAt'),\n nodes,\n ...(versionId ? { versionId: String(versionId) } : {}),\n ...(docSnapshot.get('layoutId')\n ? { layoutId: String(docSnapshot.get('layoutId')) }\n : {}),\n // Screens only, and only the screen scan reads it: a child screen is\n // a dependent of its parent, because its PATH is built from it\n // (AGL-703).\n ...(docSnapshot.get('parentId')\n ? { parentId: String(docSnapshot.get('parentId')) }\n : {}),\n // Components only: the declared properties, whose Link defaults render\n // as links wherever an instance leaves one unset (AGL-2846). The same\n // document the tree came from, so it costs no read.\n ...(collectionName === 'components' &&\n Array.isArray(docSnapshot.get('props'))\n ? { props: docSnapshot.get('props') }\n : {}),\n } satisfies UsageCandidate\n }),\n )\n\n return { candidates, truncated }\n}\n\n/**\n * Every screen, layout and component of a site, with their node trees — the\n * corpus every tree-searching closure above walks.\n *\n * Each collection ONCE, in memory: a query per level would multiply round\n * trips by the nesting depth of the graph. Shared so two scans cannot read\n * different corpora under different bounds, and a change to one cannot quietly\n * narrow the other.\n */\nexport async function readUsageSources(\n hostRef: FirebaseFirestore.DocumentReference,\n limit: number,\n): Promise<{ candidates: UsageSources; truncated: boolean }> {\n const [screens, layouts, components] = await Promise.all([\n readUsageCandidates(hostRef, 'screens', { withNodes: true, limit }),\n readUsageCandidates(hostRef, 'layouts', { withNodes: true, limit }),\n readUsageCandidates(hostRef, 'components', { withNodes: true, limit }),\n ])\n return {\n candidates: {\n screens: screens.candidates,\n layouts: layouts.candidates,\n components: components.candidates,\n },\n truncated: screens.truncated || layouts.truncated || components.truncated,\n }\n}\n"],"names":["nodesReferenceComponent","decodeStoredNodes","getTenantEmail","TENANT_EMAIL_COLLECTION","usageCandidateLabel","candidate","String","displayName","name","id","isLiveUsageCandidate","deletedAt","scanComponentUsage","componentId","sources","dependents","collect","candidates","type","nodes","push","via","versionId","screens","layouts","components","emailTemplates","scanLayoutUsage","layoutId","dependentsOf","filter","map","screenIdsUsingLayoutDeep","screenIds","Set","seenLayouts","frontier","length","next","dependent","add","has","screenIdsUsingComponentDeep","seenComponents","screenId","readUsageCandidates","hostRef","collectionName","options","withNodes","limit","docs","collection","get","truncated","size","inScope","slice","Promise","all","docSnapshot","ref","doc","then","version","catch","parentId","Array","isArray","props","readUsageSources"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/live-page-usage.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 * Which live pages a change reaches, for a site's screens, layouts and\n * components.\n *\n * The corpus read and the two closures over it, held here rather than in the\n * console because the console is no longer the only side that needs them. A\n * dataset record is written from the tenant as well — a form submission, an\n * automation step — and those writes make the same pages stale as a console\n * edit does. Two implementations of \"which screens render this\" is the shape\n * that put AGL-1223 in the tree: one reader handled a node tree's second\n * storage form and the other did not, so half the corpus answered \"used\n * nowhere\" and a publish dropped no cache.\n *\n * Nothing here is dataset-specific, and nothing is console-specific. The\n * console's `scan-artifact-usage` re-exports these and keeps the parts that\n * are about the console's own \"what would I break\" copy.\n */\n\nimport {\n nodesReferenceComponent,\n decodeStoredNodes,\n type ReusableComponentProp,\n} from '@aglyn/aglyn/server'\nimport {\n getTenantEmail,\n TENANT_EMAIL_COLLECTION,\n} from '@aglyn/shared-util-email/tenant-email-catalog'\n\n/** A screen/layout/component reduced to what a usage scan needs. */\nexport interface UsageCandidate {\n id: string\n displayName?: string\n /** Legacy field some older documents used instead of `displayName`. */\n name?: string\n deletedAt?: unknown\n /**\n * Node tree to search. For screens and layouts this is the PUBLISHED\n * version's nodes (what visitors see); for components it is the definition\n * tree off the component document, which is what the runtime reads.\n */\n nodes?: Record<string, any> | null\n /** Published version, carried through so the caller can deep-link. */\n versionId?: string\n /** Screens only: the layout they render inside. */\n layoutId?: string\n /** Screens only: the screen they nest under, which is part of their path. */\n parentId?: string\n /**\n * Screens only: `'email'` for a campaign email design (AGL-3287), which\n * a scan reports as an `emailDesign` rather than a page.\n */\n kind?: string\n /**\n * Components only: the properties the definition declares (AGL-1247). A\n * Link property's default renders as a link wherever an instance leaves the\n * property unset, and it is stored here, not in `nodes` (AGL-2846).\n */\n props?: ReadonlyArray<ReusableComponentProp | null | undefined> | null\n}\n\n/** One document that depends on the artifact a scan was asked about. */\nexport interface UsageDependent {\n /**\n * `emailTemplate` is one of the site's own transactional emails\n * (`hosts/{h}/emailTemplates/{key}`), `id` its catalog key (AGL-3287).\n *\n * `emailDesign` is an email designed for a campaign: a `kind: 'email'`\n * screen document, `id` its screen id. It is its own type rather than a\n * `screen` because it is never a page. A caller that took it for one sent\n * the reader to the page settings — a slug, SEO and a Publish button that\n * would serve the email as a route — and a cache drop has no page to drop.\n */\n type:\n | 'screen'\n | 'layout'\n | 'component'\n | 'collection'\n | 'emailTemplate'\n | 'emailDesign'\n id: string\n name: string\n via: Array<'id' | 'name'>\n versionId?: string\n /**\n * HOW the dependent references the artifact — screens and collection\n * listings (AGL-703, AGL-2806).\n *\n * A component or a layout has exactly one kind of dependent and the noun\n * says everything: an instance, or a binding. A screen has three, and they\n * break in three different ways — a link goes dead, a child moves, a\n * collection loses the page it renders through. Copy that could not tell\n * them apart would have to describe the worst case every time. A collection\n * listing's dependents are all links, and say so.\n */\n relation?: 'link' | 'child' | 'template'\n}\n\n/** The three corpora every closure below walks, and a fourth for components. */\nexport interface UsageSources {\n screens: UsageCandidate[]\n layouts: UsageCandidate[]\n components: UsageCandidate[]\n /**\n * The site's transactional email designs, each on its published version\n * (AGL-3287). Read only where a scan asks \"what places this component\" —\n * {@link scanComponentUsage} lists them — because an email is not a page:\n * nothing is cached for it, so no closure that decides which pages to drop\n * has any use for them, and none reads them.\n */\n emailTemplates?: UsageCandidate[]\n}\n\n/** `displayName`, falling back to a legacy `name`, then the raw id. */\nexport function usageCandidateLabel(candidate: UsageCandidate): string {\n return String(candidate.displayName ?? candidate.name ?? candidate.id)\n}\n\n/** A soft-deleted document renders nothing, so it depends on nothing. */\nexport const isLiveUsageCandidate = (candidate: UsageCandidate): boolean =>\n !candidate.deletedAt\n\n/**\n * Everything that references a reusable component (AGL-703).\n *\n * Every place the renderer expands an instance: published screen versions\n * (email designs among them — a campaign email is a screen), published layout\n * versions, OTHER component definitions — `composeReusableComponentNodes`\n * grafts nested instances, so a component used only inside another component\n * is genuinely used — and, when the caller read them, the site's transactional\n * emails, which graft a placed header or footer at send time (AGL-3287).\n * Omitting any of them would report \"used nowhere\" for something that renders\n * and invite a confident deletion, which is worse than showing nothing at all.\n */\nexport function scanComponentUsage(\n componentId: string,\n sources: UsageSources,\n): UsageDependent[] {\n if (!componentId) return []\n const dependents: UsageDependent[] = []\n const collect = (\n candidates: UsageCandidate[],\n type: UsageDependent['type'],\n ) => {\n for (const candidate of candidates) {\n if (!isLiveUsageCandidate(candidate)) continue\n // A component never counts as using itself, however it nests.\n if (type === 'component' && candidate.id === componentId) continue\n if (!nodesReferenceComponent(candidate.nodes, componentId)) continue\n dependents.push({\n type:\n type === 'screen' && candidate.kind === 'email'\n ? 'emailDesign'\n : type,\n id: candidate.id,\n name: usageCandidateLabel(candidate),\n // Instances reference by id, so a rename can never break them.\n via: ['id'],\n ...(candidate.versionId ? { versionId: candidate.versionId } : {}),\n })\n }\n }\n collect(sources.screens, 'screen')\n collect(sources.layouts, 'layout')\n collect(sources.components, 'component')\n collect(sources.emailTemplates ?? [], 'emailTemplate')\n return dependents\n}\n\n/**\n * Everything rendering inside a layout (AGL-703).\n *\n * Two kinds of dependent, both expressed by the same `layoutId` pointer:\n *\n * - **screens**, which name the layout they render inside;\n * - **other layouts**, since a layout can itself sit inside one. A nested\n * layout is a real dependent — deleting its parent unwraps every screen\n * underneath it — so leaving layouts out would report a parent layout as\n * used only by the screens that name it directly, and none of the ones\n * that reach it through a child.\n *\n * A layout never counts as its own dependent; `canNestLayout` refuses that,\n * and this refuses to report it even if stored data holds one.\n */\nexport function scanLayoutUsage(\n layoutId: string,\n screens: UsageCandidate[],\n layouts: UsageCandidate[] = [],\n): UsageDependent[] {\n if (!layoutId) return []\n const dependentsOf = (\n candidates: UsageCandidate[],\n type: 'screen' | 'layout',\n ) =>\n candidates\n .filter(\n (candidate) =>\n isLiveUsageCandidate(candidate) &&\n candidate.layoutId === layoutId &&\n candidate.id !== layoutId,\n )\n .map((candidate) => ({\n type,\n id: candidate.id,\n name: usageCandidateLabel(candidate),\n via: ['id' as const],\n ...(candidate.versionId ? { versionId: candidate.versionId } : {}),\n }))\n return [\n ...dependentsOf(screens, 'screen'),\n ...dependentsOf(layouts, 'layout'),\n ]\n}\n\n/**\n * Every live screen rendered inside `layoutId`, at ANY nesting depth\n * (AGL-1150).\n *\n * `scanLayoutUsage` answers one level. Layouts nest — a screen points at a\n * layout, which can point at a parent layout, and `compose-screen-nodes` walks\n * that whole chain when composing a page. So publishing a layout changes every\n * screen below it, not just the ones bound to it directly, and a cache drop\n * that only handles the direct level leaves the rest showing stale chrome for\n * the full revalidate window.\n *\n * Pure, and separate from the Firestore read, so the nesting behaviour is\n * testable without a database.\n *\n * Cycle-safe. `canNestLayout` refuses to create a cycle, but a document written\n * straight to Firestore is not bound by that, and a cycle here would hang a\n * publish request rather than surface anything.\n */\nexport function screenIdsUsingLayoutDeep(\n layoutId: string,\n screens: UsageCandidate[],\n layouts: UsageCandidate[] = [],\n): string[] {\n if (!layoutId) return []\n const screenIds = new Set<string>()\n const seenLayouts = new Set<string>([layoutId])\n let frontier = [layoutId]\n\n while (frontier.length) {\n const next: string[] = []\n for (const id of frontier) {\n for (const dependent of scanLayoutUsage(id, screens, layouts)) {\n if (dependent.type === 'screen') {\n screenIds.add(dependent.id)\n } else if (!seenLayouts.has(dependent.id)) {\n seenLayouts.add(dependent.id)\n next.push(dependent.id)\n }\n }\n }\n frontier = next\n }\n\n return [...screenIds]\n}\n\n/**\n * Every live screen whose rendered output contains `componentId`, however\n * indirectly (AGL-1161).\n *\n * `scanComponentUsage` answers one level and returns three kinds of dependent.\n * Only one of them is a screen, and the other two both reach screens by routes\n * a single-level scan cannot see:\n *\n * - a **component** dependent nests the target inside itself, and that outer\n * component may itself only be used inside a third — so component→component\n * edges have to be followed to a fixed point;\n * - a **layout** dependent puts the component in page chrome, which every\n * screen under that layout renders. Layouts nest, so that is\n * `screenIdsUsingLayoutDeep`, not a direct `layoutId` match.\n *\n * Miss either and a publish reports success while some pages keep serving the\n * old component for the full revalidate window — the failure this whole arc\n * exists to remove, and the one that is hardest to notice because the pages\n * that ARE dropped update instantly.\n *\n * Pure, and separate from the Firestore read, so the closure is testable\n * without a database — the same split `screenIdsUsingLayoutDeep` uses.\n *\n * Cycle-safe. `composeReusableComponentNodes` would not survive a cycle, but a\n * document written straight to Firestore is not bound by what the editor\n * allows, and a cycle here would hang a publish rather than surface anything.\n */\nexport function screenIdsUsingComponentDeep(\n componentId: string,\n sources: UsageSources,\n): string[] {\n if (!componentId) return []\n const screenIds = new Set<string>()\n const seenComponents = new Set<string>([componentId])\n // Layouts are resolved through their own deep walk, so remember which ones\n // have already been expanded: two components in the same layout would\n // otherwise re-walk the whole layout tree once each.\n const seenLayouts = new Set<string>()\n let frontier = [componentId]\n\n while (frontier.length) {\n const next: string[] = []\n for (const id of frontier) {\n for (const dependent of scanComponentUsage(id, sources)) {\n if (dependent.type === 'screen') {\n screenIds.add(dependent.id)\n } else if (dependent.type === 'layout') {\n if (seenLayouts.has(dependent.id)) continue\n seenLayouts.add(dependent.id)\n // The layout itself renders no URL; the screens beneath it do.\n for (const screenId of screenIdsUsingLayoutDeep(\n dependent.id,\n sources.screens,\n sources.layouts,\n )) {\n screenIds.add(screenId)\n }\n } else if (\n dependent.type === 'component' &&\n !seenComponents.has(dependent.id)\n ) {\n seenComponents.add(dependent.id)\n next.push(dependent.id)\n }\n // An email — transactional or designed for a campaign — is not a page\n // and nothing caches it, so it contributes no screen and is not\n // followed (AGL-3287). A transactional email's id is a catalog key,\n // and walking it as a component would read a key as a definition that\n // does not exist.\n }\n }\n frontier = next\n }\n\n return [...screenIds]\n}\n\nexport interface UsageCandidateRead {\n candidates: UsageCandidate[]\n /**\n * The collection held more documents than `limit` allowed, so the scan below\n * it is INCOMPLETE.\n *\n * Returned rather than logged, because the two callers owe the user\n * different things: an advisory \"what would I break\" can show a partial\n * answer and say so, while a cache drop that silently scans a prefix reports\n * a successful publish and leaves real pages stale.\n */\n truncated: boolean\n}\n\n/**\n * One collection's documents, with published nodes attached when the scan\n * needs to search them.\n *\n * `limit` is a real bound, not a guess: it is fetched with one extra document\n * so exceeding it is DETECTED rather than assumed away. A caller that ignores\n * `truncated` is choosing to be wrong quietly.\n *\n * `emailTemplates` — the site's transactional email designs (AGL-3287) — has\n * the screens' shape: a parent holding the published `versionId`, the tree on\n * that version. The catalog is code and fixed, and a template document only\n * exists once somebody pressed Design, so most sites have none and the read\n * costs one empty query. Its documents carry no name, so each is labelled with\n * the catalog's.\n */\nexport async function readUsageCandidates(\n hostRef: FirebaseFirestore.DocumentReference,\n collectionName:\n | 'screens'\n | 'layouts'\n | 'components'\n | typeof TENANT_EMAIL_COLLECTION,\n options: { withNodes: boolean; limit: number },\n): Promise<UsageCandidateRead> {\n const { withNodes, limit } = options\n // One over the limit: if the extra document comes back, there was more than\n // we are about to look at. Cheaper than a count() and exact.\n const docs = await hostRef.collection(collectionName).limit(limit + 1).get()\n const truncated = docs.size > limit\n const inScope = truncated ? docs.docs.slice(0, limit) : docs.docs\n\n const candidates = await Promise.all(\n inScope.map(async (docSnapshot) => {\n const versionId = docSnapshot.get('versionId')\n // Components keep their tree on the document; screens and layouts keep\n // it on the published version.\n //\n // BOTH reads are decoded (AGL-1223). A component document is msgpack\n // for anything promoted since components were compressed and a plain\n // map for everything older, and `decodeStoredNodes` returns a map\n // unchanged — so the branch below cannot be simplified back into a raw\n // read on either side.\n //\n // What rides on this: every consumer walks the value with\n // `Object.values`, which over a `Buffer` yields byte NUMBERS and\n // matches nothing. The answer that produces is \"used nowhere\" — for\n // `/api/hosts/where-used` an invitation to delete something a live page\n // renders, and for `/api/screens/revalidate` a publish that drops no\n // cache and leaves the old page served.\n const nodes =\n collectionName === 'components'\n ? decodeStoredNodes(docSnapshot.get('nodes'))\n : withNodes && versionId\n ? await docSnapshot.ref\n .collection('versions')\n .doc(String(versionId))\n .get()\n .then((version) => decodeStoredNodes(version.get('nodes')))\n .catch(() => null)\n : null\n return {\n id: docSnapshot.id,\n displayName:\n collectionName === TENANT_EMAIL_COLLECTION\n ? (getTenantEmail(docSnapshot.id)?.name ?? docSnapshot.id)\n : docSnapshot.get('displayName'),\n name: docSnapshot.get('name'),\n deletedAt: docSnapshot.get('deletedAt'),\n nodes,\n ...(versionId ? { versionId: String(versionId) } : {}),\n ...(docSnapshot.get('layoutId')\n ? { layoutId: String(docSnapshot.get('layoutId')) }\n : {}),\n // Screens only, and only the screen scan reads it: a child screen is\n // a dependent of its parent, because its PATH is built from it\n // (AGL-703).\n ...(docSnapshot.get('parentId')\n ? { parentId: String(docSnapshot.get('parentId')) }\n : {}),\n // Screens only: a campaign email design is a screen document that is\n // never a page (AGL-3287).\n ...(collectionName === 'screens' && docSnapshot.get('kind')\n ? { kind: String(docSnapshot.get('kind')) }\n : {}),\n // Components only: the declared properties, whose Link defaults render\n // as links wherever an instance leaves one unset (AGL-2846). The same\n // document the tree came from, so it costs no read.\n ...(collectionName === 'components' &&\n Array.isArray(docSnapshot.get('props'))\n ? { props: docSnapshot.get('props') }\n : {}),\n } satisfies UsageCandidate\n }),\n )\n\n return { candidates, truncated }\n}\n\n/**\n * Every screen, layout and component of a site, with their node trees — the\n * corpus every tree-searching closure above walks.\n *\n * Each collection ONCE, in memory: a query per level would multiply round\n * trips by the nesting depth of the graph. Shared so two scans cannot read\n * different corpora under different bounds, and a change to one cannot quietly\n * narrow the other.\n */\nexport async function readUsageSources(\n hostRef: FirebaseFirestore.DocumentReference,\n limit: number,\n): Promise<{ candidates: UsageSources; truncated: boolean }> {\n const [screens, layouts, components] = await Promise.all([\n readUsageCandidates(hostRef, 'screens', { withNodes: true, limit }),\n readUsageCandidates(hostRef, 'layouts', { withNodes: true, limit }),\n readUsageCandidates(hostRef, 'components', { withNodes: true, limit }),\n ])\n return {\n candidates: {\n screens: screens.candidates,\n layouts: layouts.candidates,\n components: components.candidates,\n },\n truncated: screens.truncated || layouts.truncated || components.truncated,\n }\n}\n"],"names":["nodesReferenceComponent","decodeStoredNodes","getTenantEmail","TENANT_EMAIL_COLLECTION","usageCandidateLabel","candidate","String","displayName","name","id","isLiveUsageCandidate","deletedAt","scanComponentUsage","componentId","sources","dependents","collect","candidates","type","nodes","push","kind","via","versionId","screens","layouts","components","emailTemplates","scanLayoutUsage","layoutId","dependentsOf","filter","map","screenIdsUsingLayoutDeep","screenIds","Set","seenLayouts","frontier","length","next","dependent","add","has","screenIdsUsingComponentDeep","seenComponents","screenId","readUsageCandidates","hostRef","collectionName","options","withNodes","limit","docs","collection","get","truncated","size","inScope","slice","Promise","all","docSnapshot","ref","doc","then","version","catch","parentId","Array","isArray","props","readUsageSources"],"mappings":"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"}
|