@alpaca-headless/dynamic-renderings 1.0.4694
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 +8 -0
- package/README.md +147 -0
- package/dist/client/ClientIslandRuntime.d.ts +1 -0
- package/dist/client/ClientIslandRuntime.js +12 -0
- package/dist/client/DynamicIsland.d.ts +12 -0
- package/dist/client/DynamicIsland.js +55 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +4 -0
- package/dist/css-policy/css-policy.d.mts +1 -0
- package/dist/css-policy/css-policy.mjs +1 -0
- package/dist/handlers/build.d.ts +4 -0
- package/dist/handlers/build.js +99 -0
- package/dist/handlers/client.d.ts +6 -0
- package/dist/handlers/client.js +159 -0
- package/dist/handlers/index.d.ts +3 -0
- package/dist/handlers/index.js +6 -0
- package/dist/next/index.d.ts +5 -0
- package/dist/next/index.js +6 -0
- package/dist/next/prefix.d.ts +8 -0
- package/dist/next/prefix.js +25 -0
- package/dist/next/request.d.ts +31 -0
- package/dist/next/request.js +33 -0
- package/dist/next/routes.d.ts +16 -0
- package/dist/next/routes.js +15 -0
- package/dist/next/theme.d.ts +8 -0
- package/dist/next/theme.js +26 -0
- package/dist/protocol/index.d.ts +3 -0
- package/dist/protocol/index.js +2 -0
- package/dist/protocol/protocol.d.ts +2 -0
- package/dist/protocol/protocol.js +1 -0
- package/dist/protocol/types.d.ts +1 -0
- package/dist/protocol/types.js +1 -0
- package/dist/react/DynamicRendering.d.ts +32 -0
- package/dist/react/DynamicRendering.js +88 -0
- package/dist/react/DynamicTheme.d.ts +5 -0
- package/dist/react/DynamicTheme.js +13 -0
- package/dist/react/RichText.d.ts +7 -0
- package/dist/react/RichText.js +31 -0
- package/dist/react/index.d.ts +9 -0
- package/dist/react/index.js +8 -0
- package/dist/react/renderer.d.ts +45 -0
- package/dist/react/renderer.js +41 -0
- package/dist/react/to-react.d.ts +42 -0
- package/dist/react/to-react.js +112 -0
- package/dist/react/trusted.d.ts +3 -0
- package/dist/react/trusted.js +31 -0
- package/dist/rich-text/index.d.ts +1 -0
- package/dist/rich-text/index.js +1 -0
- package/dist/rich-text/sanitizeRichText.d.ts +4 -0
- package/dist/rich-text/sanitizeRichText.js +99 -0
- package/dist/server/backend.d.ts +25 -0
- package/dist/server/backend.js +36 -0
- package/dist/server/config.d.ts +98 -0
- package/dist/server/config.js +49 -0
- package/dist/server/engine.d.ts +83 -0
- package/dist/server/engine.js +207 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +8 -0
- package/dist/server/page.d.ts +92 -0
- package/dist/server/page.js +235 -0
- package/dist/server/sanitize-tree.d.ts +28 -0
- package/dist/server/sanitize-tree.js +343 -0
- package/dist/server/theme.d.ts +14 -0
- package/dist/server/theme.js +38 -0
- package/next-config/index.cjs +101 -0
- package/next-config/index.d.cts +26 -0
- package/next-config/rewrites.cjs +25 -0
- package/next-config/rewrites.d.cts +3 -0
- package/package.json +108 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { type BackendIdentity } from "./backend.js";
|
|
3
|
+
import { type DynamicRenderingsConfig } from "./config.js";
|
|
4
|
+
import type { ComponentDto, PreparedComponent } from "../protocol/types.js";
|
|
5
|
+
export type LayoutComponent = {
|
|
6
|
+
id: string;
|
|
7
|
+
templateId?: string;
|
|
8
|
+
language?: string;
|
|
9
|
+
visible?: boolean;
|
|
10
|
+
rendering?: {
|
|
11
|
+
id?: string;
|
|
12
|
+
revision?: string;
|
|
13
|
+
} | null;
|
|
14
|
+
fields?: Record<string, {
|
|
15
|
+
name?: string;
|
|
16
|
+
value?: unknown;
|
|
17
|
+
} | undefined>;
|
|
18
|
+
placeholders?: {
|
|
19
|
+
name: string;
|
|
20
|
+
components?: LayoutComponent[];
|
|
21
|
+
}[];
|
|
22
|
+
};
|
|
23
|
+
export type LayoutPage = {
|
|
24
|
+
id: string;
|
|
25
|
+
templateId?: string;
|
|
26
|
+
database?: string;
|
|
27
|
+
language?: string;
|
|
28
|
+
fields?: LayoutComponent["fields"];
|
|
29
|
+
placeholders?: {
|
|
30
|
+
name: string;
|
|
31
|
+
components?: LayoutComponent[];
|
|
32
|
+
}[];
|
|
33
|
+
};
|
|
34
|
+
/** The page tenant's theme rendering, checked with the stylesheet value rules. */
|
|
35
|
+
export type PreparedTheme = {
|
|
36
|
+
id: string;
|
|
37
|
+
css: string | null;
|
|
38
|
+
hash?: string;
|
|
39
|
+
rejected?: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Which client code loads in the browser: none in edit mode (first paints only), all of it in a
|
|
43
|
+
* sealed request, and otherwise only that of approved renderings.
|
|
44
|
+
*/
|
|
45
|
+
export type ClientMode = "edit" | "sealed" | "approved";
|
|
46
|
+
export type DynamicPageState = {
|
|
47
|
+
nonce: string;
|
|
48
|
+
mode: string;
|
|
49
|
+
editing: boolean;
|
|
50
|
+
preview: boolean;
|
|
51
|
+
/** The request came through an editor's isolated preview (the config's requestAuth recognised it). */
|
|
52
|
+
sealed: boolean;
|
|
53
|
+
clientMode: ClientMode;
|
|
54
|
+
/** Keyed by component id. */
|
|
55
|
+
components: Map<string, PreparedComponent>;
|
|
56
|
+
/** Proxy prefix this request arrived under. */
|
|
57
|
+
prefix: string;
|
|
58
|
+
pageId: string;
|
|
59
|
+
database?: string;
|
|
60
|
+
/** Some compile did not finish within the page budget: this response must not be cached. */
|
|
61
|
+
preparing: boolean;
|
|
62
|
+
/** The renderings on this page that have client code. */
|
|
63
|
+
clientCodeRenderings: string[];
|
|
64
|
+
/** The page tenant's theme tokens, when it has a theme rendering. */
|
|
65
|
+
theme?: PreparedTheme;
|
|
66
|
+
/** Set when the head's own theme could not be read: nothing is built or rendered against a missing theme. */
|
|
67
|
+
themeError?: string;
|
|
68
|
+
};
|
|
69
|
+
export declare function emptyDynamicPage(mode: string, prefix?: string, { sealed, pageId, database }?: {
|
|
70
|
+
sealed?: boolean;
|
|
71
|
+
pageId?: string;
|
|
72
|
+
database?: string;
|
|
73
|
+
}): DynamicPageState;
|
|
74
|
+
export declare function normalizeId(id: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* What rendering code may see of a component: field values, placeholder names, language and the
|
|
77
|
+
* editing flag. No descriptors, no _editor data, no GraphQL, no URLs of the CM. Reference
|
|
78
|
+
* targets outside the rendering's tenant are dropped for sandbox tenants.
|
|
79
|
+
*/
|
|
80
|
+
export declare function toDto(config: Pick<DynamicRenderingsConfig, "sandboxRoot">, component: LayoutComponent, tenant: string, editing: boolean): ComponentDto;
|
|
81
|
+
export declare function prepareDynamicPage(config: DynamicRenderingsConfig, { page, mode, identity, prefix, sealed, }: {
|
|
82
|
+
page: LayoutPage;
|
|
83
|
+
mode: string;
|
|
84
|
+
identity: BackendIdentity;
|
|
85
|
+
prefix: string;
|
|
86
|
+
sealed?: boolean;
|
|
87
|
+
}): Promise<DynamicPageState>;
|
|
88
|
+
/** Theme tokens become a :root rule only after passing the same checks the build applies. */
|
|
89
|
+
export declare function prepareTheme(theme: {
|
|
90
|
+
id: string;
|
|
91
|
+
tokens: string;
|
|
92
|
+
}): PreparedTheme;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { fetchSources } from "./backend.js";
|
|
4
|
+
import { compileRendering, moduleOf, rememberClientBundle, renderTenant } from "./engine.js";
|
|
5
|
+
import { parseThemeTokens } from "../css-policy/css-policy.mjs";
|
|
6
|
+
import { isSandboxTenant, ThemeUnavailableError } from "./config.js";
|
|
7
|
+
import { combineThemeHash, loadTheme } from "./theme.js";
|
|
8
|
+
/** Time kept back from the page budget for rendering once the compiles are done. */
|
|
9
|
+
const RENDER_RESERVE_MS = 1500;
|
|
10
|
+
const PREPARING = "This rendering is being prepared. Reload in a moment.";
|
|
11
|
+
export function emptyDynamicPage(mode, prefix = "", { sealed = false, pageId = "", database } = {}) {
|
|
12
|
+
const editing = mode === "edit";
|
|
13
|
+
return {
|
|
14
|
+
nonce: randomBytes(12).toString("base64url"),
|
|
15
|
+
mode,
|
|
16
|
+
editing,
|
|
17
|
+
preview: mode === "preview",
|
|
18
|
+
sealed,
|
|
19
|
+
clientMode: editing ? "edit" : sealed ? "sealed" : "approved",
|
|
20
|
+
components: new Map(),
|
|
21
|
+
prefix,
|
|
22
|
+
pageId,
|
|
23
|
+
database,
|
|
24
|
+
preparing: false,
|
|
25
|
+
clientCodeRenderings: [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function collect(placeholders, out) {
|
|
29
|
+
for (const placeholder of placeholders ?? []) {
|
|
30
|
+
for (const component of placeholder.components ?? []) {
|
|
31
|
+
if (component.visible === false)
|
|
32
|
+
continue;
|
|
33
|
+
if (component.rendering?.id)
|
|
34
|
+
out.push(component);
|
|
35
|
+
collect(component.placeholders, out);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function normalizeId(id) {
|
|
40
|
+
return id.replace(/[{}]/g, "").toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
function pathOf(value) {
|
|
43
|
+
return value && typeof value === "object" && typeof value.path === "string"
|
|
44
|
+
? value.path
|
|
45
|
+
: undefined;
|
|
46
|
+
}
|
|
47
|
+
function insideTenant(path, tenant) {
|
|
48
|
+
if (!path)
|
|
49
|
+
return true;
|
|
50
|
+
const p = path.toLowerCase().replace(/\/+$/, "");
|
|
51
|
+
const t = tenant.toLowerCase().replace(/\/+$/, "");
|
|
52
|
+
return p === t || p.startsWith(`${t}/`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* What rendering code may see of a component: field values, placeholder names, language and the
|
|
56
|
+
* editing flag. No descriptors, no _editor data, no GraphQL, no URLs of the CM. Reference
|
|
57
|
+
* targets outside the rendering's tenant are dropped for sandbox tenants.
|
|
58
|
+
*/
|
|
59
|
+
export function toDto(config, component, tenant, editing) {
|
|
60
|
+
const sandbox = isSandboxTenant(config, tenant);
|
|
61
|
+
const fields = {};
|
|
62
|
+
for (const [key, field] of Object.entries(component.fields ?? {})) {
|
|
63
|
+
if (!field)
|
|
64
|
+
continue;
|
|
65
|
+
// Keyed by the Sitecore field name, the name rendering code and the authoring tools use; the
|
|
66
|
+
// layout JSON may camel-case its keys (targetDate for TargetDate).
|
|
67
|
+
const name = typeof field.name === "string" && field.name ? field.name : key;
|
|
68
|
+
if (name === "Rendering" || name.startsWith("_"))
|
|
69
|
+
continue;
|
|
70
|
+
let value = field.value;
|
|
71
|
+
if (sandbox) {
|
|
72
|
+
if (Array.isArray(value))
|
|
73
|
+
value = value.filter((entry) => insideTenant(pathOf(entry), tenant));
|
|
74
|
+
else if (pathOf(value) !== undefined && !insideTenant(pathOf(value), tenant))
|
|
75
|
+
value = null;
|
|
76
|
+
}
|
|
77
|
+
fields[name] = { name, value: stripDescriptors(value) };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
id: normalizeId(component.id),
|
|
81
|
+
templateId: component.templateId,
|
|
82
|
+
language: component.language,
|
|
83
|
+
editing,
|
|
84
|
+
fields,
|
|
85
|
+
placeholders: (component.placeholders ?? []).map((p) => p.name),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function stripDescriptors(value, depth = 0) {
|
|
89
|
+
if (depth > 12 || value === null || typeof value !== "object")
|
|
90
|
+
return value;
|
|
91
|
+
if (Array.isArray(value))
|
|
92
|
+
return value.map((v) => stripDescriptors(v, depth + 1));
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const [k, v] of Object.entries(value)) {
|
|
95
|
+
if (k === "descriptor" || k === "_editor")
|
|
96
|
+
continue;
|
|
97
|
+
out[k] = stripDescriptors(v, depth + 1);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
function firstError(compiled) {
|
|
102
|
+
if (compiled.ok)
|
|
103
|
+
return undefined;
|
|
104
|
+
const error = compiled.diagnostics.find((d) => d.severity === "error") ?? compiled.diagnostics[0];
|
|
105
|
+
if (!error)
|
|
106
|
+
return "The build failed.";
|
|
107
|
+
const where = error.line ? ` (${error.file ?? "rendering.tsx"}:${error.line}${error.column ? `:${error.column}` : ""})` : "";
|
|
108
|
+
return `Build failed${where}: ${error.message}`;
|
|
109
|
+
}
|
|
110
|
+
/** Waits for a compile until `deadline`; "preparing" when it is still running then. */
|
|
111
|
+
function within(compile, deadline) {
|
|
112
|
+
const remaining = deadline - Date.now();
|
|
113
|
+
if (remaining <= 0)
|
|
114
|
+
return Promise.race([compile, Promise.resolve("preparing")]);
|
|
115
|
+
let timer;
|
|
116
|
+
const timeout = new Promise((resolve) => {
|
|
117
|
+
timer = setTimeout(() => resolve("preparing"), remaining);
|
|
118
|
+
});
|
|
119
|
+
return Promise.race([compile, timeout]).finally(() => clearTimeout(timer));
|
|
120
|
+
}
|
|
121
|
+
export async function prepareDynamicPage(config, { page, mode, identity, prefix, sealed = false, }) {
|
|
122
|
+
const started = Date.now();
|
|
123
|
+
const state = emptyDynamicPage(mode, prefix, { sealed, pageId: page.id, database: page.database });
|
|
124
|
+
const found = [];
|
|
125
|
+
collect(page.placeholders, found);
|
|
126
|
+
// A theme that cannot be read is an error, never empty CSS: nothing compiles against a missing
|
|
127
|
+
// theme, and every dynamic component explains why it is missing.
|
|
128
|
+
let theme;
|
|
129
|
+
try {
|
|
130
|
+
theme = await loadTheme(config);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const message = err instanceof ThemeUnavailableError ? err.message : `The site theme could not be loaded: ${String(err)}`;
|
|
134
|
+
console.error(`[dynamic-renderings] ${message}`);
|
|
135
|
+
state.themeError = message;
|
|
136
|
+
for (const component of found) {
|
|
137
|
+
state.components.set(normalizeId(component.id), { renderingId: normalizeId(component.rendering.id), error: message });
|
|
138
|
+
}
|
|
139
|
+
return state;
|
|
140
|
+
}
|
|
141
|
+
// Asked on every page, also without dynamic components: the tenant may have a theme.
|
|
142
|
+
const renderingIds = [...new Set(found.map((c) => normalizeId(c.rendering.id)))];
|
|
143
|
+
let response = { renderings: [] };
|
|
144
|
+
try {
|
|
145
|
+
response = await fetchSources(config, identity, { pageId: page.id, ids: renderingIds, database: page.database, mode });
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.warn("[dynamic-renderings] sources call failed", err);
|
|
149
|
+
}
|
|
150
|
+
if (response.theme)
|
|
151
|
+
state.theme = prepareTheme(response.theme);
|
|
152
|
+
const tokens = response.theme?.tokens ?? "";
|
|
153
|
+
const themeHash = combineThemeHash(theme.hash, tokens);
|
|
154
|
+
// Compile what this process has not compiled yet. A page waits for that only within its
|
|
155
|
+
// budget, keeping time back for rendering; compiles still running then continue in the
|
|
156
|
+
// background and their components show as being prepared.
|
|
157
|
+
const sources = new Map();
|
|
158
|
+
for (const source of response.renderings)
|
|
159
|
+
sources.set(normalizeId(source.id), source);
|
|
160
|
+
const compileDeadline = started + config.limits.pageMs - RENDER_RESERVE_MS;
|
|
161
|
+
const compiled = new Map();
|
|
162
|
+
await Promise.all([...sources.entries()].map(async ([id, source]) => {
|
|
163
|
+
const compile = compileRendering(config, {
|
|
164
|
+
renderingId: id,
|
|
165
|
+
kind: source.kind,
|
|
166
|
+
name: source.name,
|
|
167
|
+
code: source.code,
|
|
168
|
+
clientCode: source.clientCode,
|
|
169
|
+
themeCss: theme.css,
|
|
170
|
+
themeTokens: tokens,
|
|
171
|
+
themeHash,
|
|
172
|
+
});
|
|
173
|
+
compiled.set(id, await within(compile, compileDeadline));
|
|
174
|
+
}));
|
|
175
|
+
const byTenant = new Map();
|
|
176
|
+
const modules = {};
|
|
177
|
+
const prepare = (renderingId) => {
|
|
178
|
+
const result = compiled.get(renderingId);
|
|
179
|
+
if (result === undefined)
|
|
180
|
+
return { renderingId, error: "This rendering is not available on this page." };
|
|
181
|
+
if (result === "preparing") {
|
|
182
|
+
state.preparing = true;
|
|
183
|
+
return { renderingId, preparing: true, error: PREPARING };
|
|
184
|
+
}
|
|
185
|
+
if (!result.ok) {
|
|
186
|
+
if (result.transient)
|
|
187
|
+
state.preparing = true;
|
|
188
|
+
return { renderingId, error: result.transient ? `${PREPARING} (${result.diagnostics[0]?.message ?? "busy"})` : firstError(result) };
|
|
189
|
+
}
|
|
190
|
+
modules[result.buildId] = moduleOf(result);
|
|
191
|
+
const approved = sources.get(renderingId)?.approved === true;
|
|
192
|
+
// The bundle route serves the browser module from here, with the CM's current verdict.
|
|
193
|
+
if (result.client)
|
|
194
|
+
rememberClientBundle(renderingId, result.client, approved);
|
|
195
|
+
return { renderingId, buildId: result.buildId, css: result.css, cssRejected: result.cssRejected, clientHash: result.client?.hash, approved };
|
|
196
|
+
};
|
|
197
|
+
for (const component of found) {
|
|
198
|
+
const renderingId = normalizeId(component.rendering.id);
|
|
199
|
+
const componentId = normalizeId(component.id);
|
|
200
|
+
const prepared = prepare(renderingId);
|
|
201
|
+
state.components.set(componentId, prepared);
|
|
202
|
+
const source = sources.get(renderingId);
|
|
203
|
+
if (source?.clientCode?.trim() && !state.clientCodeRenderings.includes(renderingId))
|
|
204
|
+
state.clientCodeRenderings.push(renderingId);
|
|
205
|
+
if (!prepared.buildId || !source)
|
|
206
|
+
continue;
|
|
207
|
+
const list = byTenant.get(source.tenant) ?? [];
|
|
208
|
+
list.push({ key: componentId, buildId: prepared.buildId, dto: toDto(config, component, source.tenant, state.editing) });
|
|
209
|
+
byTenant.set(source.tenant, list);
|
|
210
|
+
}
|
|
211
|
+
const renderBudget = Math.max(RENDER_RESERVE_MS, config.limits.pageMs - (Date.now() - started));
|
|
212
|
+
await Promise.all([...byTenant.entries()].map(async ([tenant, list]) => {
|
|
213
|
+
const results = await renderTenant(config, tenant, list, modules, { deadlineMs: renderBudget });
|
|
214
|
+
for (const result of results) {
|
|
215
|
+
const prepared = state.components.get(result.key);
|
|
216
|
+
if (prepared)
|
|
217
|
+
prepared.result = result;
|
|
218
|
+
}
|
|
219
|
+
}));
|
|
220
|
+
return state;
|
|
221
|
+
}
|
|
222
|
+
/** Theme tokens become a :root rule only after passing the same checks the build applies. */
|
|
223
|
+
export function prepareTheme(theme) {
|
|
224
|
+
const parsed = parseThemeTokens(theme.tokens);
|
|
225
|
+
if (!parsed.ok) {
|
|
226
|
+
return { id: theme.id, css: null, rejected: parsed.errors.map((e) => e.message).join("; ") };
|
|
227
|
+
}
|
|
228
|
+
if (!parsed.css)
|
|
229
|
+
return { id: theme.id, css: null };
|
|
230
|
+
return {
|
|
231
|
+
id: theme.id,
|
|
232
|
+
css: `:root {\n${parsed.css}\n}`,
|
|
233
|
+
hash: createHash("sha256").update(parsed.css).digest("hex").slice(0, 16),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RenderNode } from "../protocol/types.js";
|
|
2
|
+
export declare const ALLOWED_TAGS: Set<string>;
|
|
3
|
+
export declare const TEXT_TAGS: Set<string>;
|
|
4
|
+
export declare const RICH_TEXT_TAGS: Set<string>;
|
|
5
|
+
/** data-* attributes the editor bridge (or the host itself) reads. */
|
|
6
|
+
export declare const DENIED_DATA_ATTRIBUTE: RegExp;
|
|
7
|
+
/** Class names the bridge or Sitecore chrome react to. */
|
|
8
|
+
export declare function isDeniedClass(name: string): boolean;
|
|
9
|
+
export type SanitizeOptions = {
|
|
10
|
+
/** Absolute media hosts allowed in `src` (https only). */
|
|
11
|
+
mediaHosts?: readonly string[];
|
|
12
|
+
limits?: {
|
|
13
|
+
treeNodes: number;
|
|
14
|
+
treeDepth: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type SanitizeContext = {
|
|
18
|
+
renderingId: string;
|
|
19
|
+
idPrefix: string;
|
|
20
|
+
mediaHosts: readonly string[];
|
|
21
|
+
limits: {
|
|
22
|
+
treeNodes: number;
|
|
23
|
+
treeDepth: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export declare function sanitizeProps(tag: string, props: unknown, ctx: SanitizeContext): Record<string, unknown>;
|
|
27
|
+
/** Sanitizes a whole component tree. Throws when it exceeds the node or depth limits. */
|
|
28
|
+
export declare function sanitizeTree(tree: unknown, renderingId: string, options?: SanitizeOptions): RenderNode[];
|