ui/server/src/pagesEditor
pagesEditor.mjs's implementation is pure logic (JSX tree edits, Palette insertion, enforcement checks) with no Cockpit-server dependency beyond a path-containment helper, so it now lives in packages/engine where const...
Cockpit server, next. Source: ui/server/src/pagesEditor.mjs.
Classes#
PagesEditorError#
Defined in: packages/engine/pagesEditor.mjs:40
An HTTP-facing error from one of the Pages Editor's own operations (the scope guard, the
content-hash guard, a missing node/file, an enforcement rejection, ...): carries the
response status to send, an optional violations list and an
optional machine-readable code (e.g. HASH_REQUIRED, CHANGED_ON_DISK) callers branch on.
Extends#
Error
Constructors#
Constructor
new PagesEditorError(
message,options?):PagesEditorError
Defined in: packages/engine/pagesEditor.mjs:47
Parameters
message
string
Human-readable error message.
options?
status defaults to
400; violations is the enforcement violation list when this rejects a save; code is a
short machine-readable reason (e.g. HASH_REQUIRED, CHANGED_ON_DISK) for client branching.
code?
string
status?
number = 400
violations?
any[]
Returns
Overrides
Error.constructor
Properties#
code
code:
string
Defined in: packages/engine/pagesEditor.mjs:51
status
status:
number
Defined in: packages/engine/pagesEditor.mjs:49
violations
violations:
any[]
Defined in: packages/engine/pagesEditor.mjs:50
Functions#
addChildInSnippet()#
addChildInSnippet(
snippetSource,parentId): {ok:true;snippet:string; } | {error:string;ok:false; }
Defined in: packages/engine/pagesEditor.mjs:566
Ticket F.3 — append a new (fixed, deliberately minimal
— <div />) child right before a node's closing tag, reusing the
start/end offsets parsePageTree already computed (no second AST walk).
Only supported when the node already has an opening/closing tag pair or
is a fragment (<Tag>...</Tag> / <>...</>) — a self-closing element
(<Tag />) is rejected rather than the tool guessing how to split />
into an open/close pair on the caller's behalf. Never throws.
Parameters#
snippetSource
string
The snippet's own current source text.
parentId
string
Id of the node to append a new child into.
Returns#
{ ok: true; snippet: string; } | { error: string; ok: false; }
The snippet with a new <div />
child appended, or why it couldn't be added.
applyAutoMap()#
applyAutoMap(
source,nodeId,propNames):string
Defined in: packages/engine/pagesEditor.mjs:774
's write side: apply a batch of accepted auto-map suggestions to one node, wiring each
propName as the shorthand {propName} identifier attribute, one patchNode splice at a time (the
content hash is re-taken after each patch so the next splice targets the just-patched text).
Parameters#
source
string
Page file's full source text.
nodeId
string
Id of the node to add the props to.
propNames
string[]
The in-scope names to wire as same-named {name} attributes.
Returns#
string
The patched full page source with every prop applied.
applyWrapConfirm()#
applyWrapConfirm(
root,absPath,source,nodeId,name?):object
Defined in: packages/engine/pagesEditor.mjs:1000
-- the real write, only ever reached from "Approve": re-derives the same flagged hit from
nodeId (never trusting a client-sent range) and calls the real, non-dry-run
extractExpression(). Touches multiple files (the page, a new Expression, and optionally one or
more new Components) -- the caller (the server route) commits/records all of them as one save.
Parameters#
root
string
Project root.
absPath
string
Absolute path of the page/component file being wrapped.
source
string
That file's full current source text.
nodeId
string
Id of the selected node the flagged hit is re-derived from.
name?
string
Caller-supplied Expression name override.
Returns#
object
The written files, as extractExpression's own non-dry-run result reports them.
component
component:
object
component.file
file:
string
component.name
name:
string
components
components:
object[]
expression
expression:
object
expression.file
file:
string
expression.name
name:
string
page
page:
object
page.file
file:
string
Throws#
When nothing is flagged (PAGE-008) at the selection.
Throws#
Any usage error extractExpression itself throws.
assertContentHash()#
assertContentHash(
source,contentHash,options?):void
Defined in: packages/engine/pagesEditor.mjs:179
— the one hash guard every Pages-editor write that lands on disk goes through, the same
contract componentSave (componentsApi.mjs) already enforces for the Components screen: a hash
that's missing, empty or not a string is a distinct 400 HASH_REQUIRED (the client never fetched,
or forgot to resend, the hash it loaded the file against — a client bug, never routine) from a
present-but-wrong hash's 409 CHANGED_ON_DISK (someone else's edit landed on disk first — routine,
"reload and try again"). Never optional: a save endpoint that skips this on a falsy hash is exactly
's bug (a client that omits the hash silently wins over whatever is on disk).
Parameters#
source
string
Current on-disk file content to hash and compare against.
contentHash
string
The hash the caller loaded the file against (from a prior read).
options?
mismatchMessage overrides the 409 message used
when a present hash doesn't match the current content.
mismatchMessage?
string = 'The file changed on disk since this was loaded — reload the tree and try again.'
Returns#
void
Throws#
400 HASH_REQUIRED when contentHash is missing/empty; 409
CHANGED_ON_DISK when it doesn't match source's current hash.
buildAttributeSnippet()#
buildAttributeSnippet(
source,nodeId,propName,kind,value,index?):string
Defined in: packages/engine/pagesEditor.mjs:385
Update a single JSXAttribute's value on a node by textually replacing just that attribute (or appending a new one if it doesn't exist yet) inside the node's opening tag, then delegating to the same splice-based patchNode used by so both go through one mechanism (and one enforcement gate).
follow-up to — a kind === 'spread' edit is a different shape
({...expr}, no attribute name, no =) and has no name to look it up
by, so it takes its own branch keyed on index (its position in the
opening tag's attribute list, from the prop record's index field) instead of
going through the name lookup below. Only edits an
existing spread — this doesn't support inserting a brand new one.
Parameters#
source
string
Page file's full source text.
nodeId
string
Id of the node whose opening tag is edited.
propName
string
The attribute's name, or null for a kind === 'spread' edit.
kind
"string" | "number" | "boolean" | "spread" | "identifier" | "expression"
The prop's value
kind, as recorded on the node's props entries; 'spread' takes the index branch below,
every other kind is passed straight through to setAttributeText.
value
string | number | boolean
The new attribute value (rendered by setAttributeText/setSpreadText).
index?
number
Position of the spread among the tag's attributes (only for kind === 'spread').
Returns#
string
The node's whole new opening-tag-through-node text, ready for patchNode to splice in.
Throws#
409 when nodeId/the targeted spread no longer exists, or the node is a fragment.
buildComponentUsageJsx()#
buildComponentUsageJsx(
name,described):string
Defined in: packages/engine/pagesEditor.mjs:828
A self-closing JSX usage for a Component entry, its required props stubbed from described
(describeComponent's real react-docgen result) -- optional props are left out entirely rather than
guessed. described may be null/failed (a component that couldn't be documented): the usage then
has no props at all, same as any component with none.
Parameters#
name
string
The component's exported name.
described
any
describeComponent's result for the file it's exported from, or null.
Returns#
string
A self-closing JSX usage, e.g. <Foo bar="" /> or <Foo />.
buildPaletteInsertion()#
buildPaletteInsertion(
root,pageAbsPath,source,entry,describe?):Promise<{ok:true;source:string; } | {error:string;ok:false; }>
Defined in: packages/engine/pagesEditor.mjs:880
(Slice 2 of 's design) -- insert one Palette entry's real usage into a page: the import
(skipped if already present; rejected if the name is already imported from somewhere ELSE) plus
either a self-closing JSX usage (Component, appended as the last child of the page's own root JSX
element/fragment) or a hook-call statement (Provider, inserted as the first statement of that
root's enclosing component function). No selection UI needed (block-palette.md section 4's Slice 2
note): unlike "Wrap with..." (Slice 3, ), a brand-new usage doesn't need to target existing
JSX -- there is exactly one sensible place to add it. entry must be one of buildPalette's own
providers/components entries (the caller re-derives it from a fresh buildPalette call rather
than trusting whatever the client sent, the same "never resolve a client-named path directly"
discipline componentsApi.mjs's pick() uses) plus a kind.
Parameters#
root
string
Project root.
pageAbsPath
string
Absolute path of the page file to insert into.
source
string
The page file's full current source text.
entry
The Palette entry to insert (one of buildPalette's own components/providers entries).
feature?
string
kind
"provider" | "component"
name
string
path?
string
via?
string
describe?
Function = describeComponent
describeComponent, injectable for tests.
Returns#
Promise<{ ok: true; source: string; } | { error: string; ok: false; }>
The page source with the usage inserted, or why it was rejected.
buildProviderUsageStatement()#
buildProviderUsageStatement(
name):string
Defined in: packages/engine/pagesEditor.mjs:843
A Provider hook-call statement for a Provider entry. Assigns the WHOLE result to a local variable
rather than destructuring named fields: useProvider()'s return shape (Value) isn't statically
known here (no type-checker run for this), so guessing field names would risk inserting code that
doesn't compile -- calling the hook and letting the developer destructure by hand is the honest
"real, working reference" this slice promises, not a fabricated shape.
Parameters#
name
string
The Provider hook's exported name (e.g. useCartProvider).
Returns#
string
A statement calling it, e.g. const cart = useCartProvider();.
buildWrapSuggestion()#
buildWrapSuggestion(
root,absPath,source,nodeId,palette,name?,options?):object
Defined in: packages/engine/pagesEditor.mjs:958
(Slice 3 of 's design, docs/design/block-palette.md section 3) -- everything the Palette
tab's "Wrap with..." flow needs for one JSX selection: which flagged PAGE-008 conditional/loop (if
any) encloses it, every Expression in scope annotated with its structural fit
(packages/engine/palette.mjs's buildWrapSuggestions), the name that would be used (derived, or
name once given), and -- only when includeFiles is true -- a real dry-run preview of exactly
what construct refactor extract-expression (packages/core/extractExpression.mjs, ) would
write. dryRun: true throughout: nothing here ever touches disk. includeFiles defaults to
false so the automatic "you selected something" suggestion (fired the moment a selection lands,
per design section 3 step 1-2) never itself renders a diff -- only the explicit "Wrap with..."
click (design section 3 step 4) asks for one, keeping "Suggest" and "Confirm" two distinct,
separately-observable steps exactly as the design's own step numbering has them.
Parameters#
root
string
Project root.
absPath
string
Absolute path of the page/component file the selection is in.
source
string
That file's full current source text.
nodeId
string
Id of the selected node.
palette
A fresh buildPalette(...) result for this feature
(only its expressions entries are used here).
expressions
any[]
name?
string
Caller-supplied Expression name override.
options?
includeFiles also computes the real dry-run diff preview.
includeFiles?
boolean = false
Returns#
object
files
files:
object[]
hit
hit:
any
name
name:
string
nameError
nameError:
string
nameRequired
nameRequired:
boolean
ok
ok:
true
suggestions
suggestions:
any[]
checkEnforcement()#
checkEnforcement(
root,relPath,patchedSource):object
Defined in: packages/engine/pagesEditor.mjs:1023
— run the existing PAGE-* / COMPONENT-* architecture rules and the
separation-of-concerns rules against a prospective file, scoped to just
that one file, and surface any error-severity violation before it
lands (the caller writes patched to disk only if this returns
ok: true, and reverts otherwise — see server route below).
Parameters#
root
string
Project root.
relPath
string
The file's path relative to root (as classified by the layer graph).
patchedSource
string
The prospective new content to check, written to disk temporarily
(and restored in a finally, whether this returns or throws).
Returns#
object
Every architecture/SoC violation
attributed to this file, errors filtered to error-severity ones, and whether any exist.
errors
errors:
any[]
ok
ok:
boolean
violations
violations:
any[]
collectComponentScopeNames()#
collectComponentScopeNames(
ast):string[]
Defined in: packages/ast/jsxScope.mjs:52
In-scope names of a page: every function's destructured/plain parameter names plus every
const [value, setValue] = useState(...) binding, in source order, de-duplicated.
Parameters#
ast
any
A parsed Program.
Returns#
string[]
The in-scope names, in source order, de-duplicated.
featuresRootOf()#
featuresRootOf(
root):string
Defined in: packages/engine/pagesEditor.mjs:61
The configured features-root directory name for a project (e.g. "features"), from
construct.config's features.root, defaulting to "features" when unset.
Parameters#
root
string
Project root.
Returns#
string
The features-root directory name, relative to root.
findUnmappedProps()#
findUnmappedProps(
source,nodeId,root?,pageAbsPath?):object
Defined in: packages/engine/pagesEditor.mjs:675
— for a selected custom-component node, find scope names (parent
page's own props/state) that are NOT currently passed down to it as a
same-named attribute, and propose the shorthand {name} wiring for each.
follow-up: when the child component's own declared props can be
resolved across files (see resolveDeclaredPropNames above), candidates
are further filtered to names the child actually declares — e.g. a
state setter like setCount is no longer offered for a child that
doesn't destructure a same-named prop. root/pageAbsPath are optional
so in-memory single-string callers (existing tests) keep working; omit
them to keep the old, fully permissive behavior.
Parameters#
source
string
Page file's full source text.
nodeId
string
Id of the selected custom-component node.
root?
string
Project root, so the child's declared props can be resolved across files.
pageAbsPath?
string
Absolute path of source's own file (paired with root).
Returns#
object
Unmapped in-scope names to offer, and whether they were filtered to the child's own declared props.
candidates
candidates:
string[]
childPropsResolved
childPropsResolved:
boolean
nodeId
nodeId:
string
Throws#
409 when nodeId doesn't exist in source.
getNodeProps()#
getNodeProps(
source,nodeId):object
Defined in: packages/engine/pagesEditor.mjs:354
read-only helper: same shape as a tree node's props, for a single
node, plus the enclosing page component's own in-scope prop/state names
(used by 's auto-mapper).
Parameters#
source
string
Page file's full source text.
nodeId
string
Id of the node to read.
Returns#
object
The node's tag/props plus the enclosing component's in-scope prop/state names.
nodeId
nodeId:
string
props
props:
any[]
scopeNames
scopeNames:
string[]
tag
tag:
string
Throws#
409 when nodeId doesn't exist in source.
getNodeSnippet()#
getNodeSnippet(
source,nodeId):object
Defined in: packages/engine/pagesEditor.mjs:282
The exact source snippet for one node plus its parent chain's tag names for breadcrumb display.
Parameters#
source
string
Page file's full source text.
nodeId
string
Id of the node to read (from a prior parsePageTree/serializeTree call).
Returns#
object
The node's own source text and
source's content hash.
contentHash
contentHash:
string
nodeId
nodeId:
string
snippet
snippet:
string
Throws#
409 when nodeId doesn't exist in source.
getScopeLinks()#
getScopeLinks(
source,nodeId,root?,pageAbsPath?):any
Defined in: packages/engine/pagesEditor.mjs:745
Scope/binding link graph for one element: thin glue over the core buildScopeLinks block --
the path-scoped cross-file lookup of the child component's source (bare/package imports and
anything outside root resolve to null, leaving childProps unknown) plus, independently of which
element is selected, every reachable Provider hook's own source so the 'provider' scope kind is populated for real Cockpit pages.
Parameters#
source
string
Page file's full source text.
nodeId
string
Id of the selected node.
root?
string
Project root, so child/provider sources can be resolved across files.
pageAbsPath?
string
Absolute path of source's own file (paired with root).
Returns#
any
The scope/binding link graph (see buildScopeLinks).
Throws#
409 when nodeId doesn't exist in source.
hashOf()#
hashOf(
source):string
Defined in: packages/engine/pagesEditor.mjs:241
The sha256 content hash of a file's source text, used as the Pages Editor's optimistic-concurrency token: the client resends the hash it last loaded, and every mutating endpoint rejects a save whose hash no longer matches the current on-disk content.
Parameters#
source
string
File content to hash.
Returns#
string
The hex-encoded sha256 digest.
listAllPages()#
listAllPages(
root):object[]
Defined in: packages/engine/pagesEditor.mjs:104
Every page of the project, [{ feature, file }]: what the Browser lists on the Pages screen. Only names
the client may later send back to the per-feature routes, each of which re-checks it with resolvePageFile.
Parameters#
root
string
Project root.
Returns#
object[]
Every page, across every feature.
listFeatures()#
listFeatures(
root):string[]
Defined in: packages/engine/pagesEditor.mjs:70
Every feature directory under the configured features root.
Parameters#
root
string
Project root.
Returns#
string[]
Feature directory names, sorted; [] when the features root doesn't exist.
listPages()#
listPages(
root,feature):string[]
Defined in: packages/engine/pagesEditor.mjs:88
Every file under features//pages/**, relative to that pages/ folder (e.g. "Home.jsx", "billing/Detail.tsx").
Parameters#
root
string
Project root.
feature
string
Feature directory name.
Returns#
string[]
Page file paths relative to that feature's pages/ folder, sorted; [] when
the feature has no pages/ folder.
moveNodeInSnippet()#
moveNodeInSnippet(
snippetSource,nodeId,direction): {ok:true;snippet:string; } | {error:string;ok:false; }
Defined in: packages/engine/pagesEditor.mjs:528
Ticket F.3 — move a node one position up/down among its own siblings, expressed as swapping its text with the adjacent sibling's (whatever sits between the two — other text/elements — stays exactly where it is; only the two elements' own text spans trade places). Rejects a root (no siblings to move among) or a node already at the first/last position for the requested direction. Never throws.
Parameters#
snippetSource
string
The snippet's own current source text.
nodeId
string
Id of the node to move.
direction
"up" | "down"
Which way to move it among its siblings.
Returns#
{ ok: true; snippet: string; } | { error: string; ok: false; }
The snippet with the node moved, or why the move was rejected.
parsePageTree()#
parsePageTree(
source):object
Defined in: packages/engine/pagesEditor.mjs:204
Parse a page file's JSX into a navigable, serializable element tree.
Node ids are assigned in source (document) order during one traversal —
n0, n1, ... — which is what every later save/props/auto-map lookup
uses to re-find "the same" node on a fresh re-parse. Ids are only valid
for the exact source text they were computed from, which is why every
mutating endpoint re-parses fresh and includes contentHash (sha256 of
the file's current text) so a stale client can't silently patch the
wrong node after the file changed underneath it.
Parameters#
source
string
Page file's full JSX/TSX source text.
Returns#
object
The navigable node tree.
ast
ast:
any
byId
byId:
Map<string,any>
roots
roots:
any[]
parseSnippetToTree()#
parseSnippetToTree(
snippetSource):object
Defined in: packages/engine/pagesEditor.mjs:264
Ticket F.1 — parse a bare snippet's own text (not a
whole page file) into the same node tree shape serializeTree produces, so
the visual composer can derive its graph live from exactly the text
currently sitting in the snippet editor, with zero persisted metadata: no
.flyde-style side file, no cached JSON layout, just a fresh parse of
whatever text is passed in. Reuses parsePageTree/serializeTree completely
unchanged — a standalone JSX snippet (e.g. <div><Foo/></div>) is already
valid module source on its own (it parses as an ExpressionStatement
wrapping the JSXElement/JSXFragment), so no wrapping hack is needed.
Parse failures are expected transiently while the user is mid-edit (an
unclosed tag, etc.) — reported as {roots: [], error} rather than thrown,
so the canvas can just keep showing its last-good graph instead of
crashing on every keystroke.
Parameters#
snippetSource
string
The snippet's own current source text (not a whole page file).
Returns#
object
The parsed tree (same shape
as serializeTree) plus a null error, or {roots: [], error} when it doesn't currently parse
or is blank.
contentHash?
optionalcontentHash?:string
error
error:
string
roots
roots:
any[]
patchNode()#
patchNode(
source,nodeId,newSnippet,expectedHash):string
Defined in: packages/engine/pagesEditor.mjs:339
Patch one node's JSX back into the file. We re-parse the current disk content, require the
caller's contentHash to match it, then delegate the actual splice to
spliceNodeText.
Parameters#
source
string
Current disk content of the page file.
nodeId
string
Id of the node to replace.
newSnippet
string
Replacement JSX text for that node (one element or fragment).
expectedHash
string
The content hash the caller loaded source against.
Returns#
string
The patched full page source.
Throws#
400/409 from the hash guard, 409 if nodeId no longer exists, or 400
if newSnippet isn't valid JSX / leaves the file unparseable.
removeAttributeSnippet()#
removeAttributeSnippet(
source,nodeId,propName):string
Defined in: packages/engine/pagesEditor.mjs:418
Ticket F.2 — remove one named attribute from a node's opening tag, returning that node's whole replacement text (same "whole node's new text" contract buildAttributeSnippet above already uses, so patchNode splices it back the same way). Also consumes one immediately-preceding whitespace run so the removal doesn't leave a double space behind in the tag. Only for a named (non-spread) attribute that currently exists.
Parameters#
source
string
Page file's full source text.
nodeId
string
Id of the node whose opening tag is edited.
propName
string
Name of the attribute to remove.
Returns#
string
The node's whole new text with that attribute removed.
Throws#
409 when nodeId doesn't exist or the node is a fragment; a plain
error when propName isn't currently on the node.
removeNodeInSnippet()#
removeNodeInSnippet(
snippetSource,nodeId): {ok:true;snippet:string; } | {error:string;ok:false; }
Defined in: packages/engine/pagesEditor.mjs:497
Ticket F.3 — remove a node (and its whole subtree) from
the snippet, expressed as a plain text-range deletion plus a small
whitespace cleanup (one run of leading indentation, one trailing newline)
so it doesn't leave a blank line behind. Rejects removing the snippet's
own single root — a snippet is always exactly one root element/fragment
(the same invariant patchNode's own save-time validation enforces), so
there is nothing left to save if the root itself were removed. Never
throws — {ok:false, error} on any rejection.
Parameters#
snippetSource
string
The snippet's own current source text.
nodeId
string
Id of the node (and subtree) to remove.
Returns#
{ ok: true; snippet: string; } | { error: string; ok: false; }
The snippet with that node removed, or why the removal was rejected.
resolveDeclaredPropNames()#
resolveDeclaredPropNames(
root,pageAbsPath,pageAst,tagName):any
Defined in: packages/engine/pagesEditor.mjs:640
follow-up to — resolve the child component actually rendered by
JSX tag tagName (via the page file's own import of it) and return its
declared prop names, so findUnmappedProps can filter candidates to ones
the child can actually use instead of every in-scope name. Returns null
when the child can't be resolved (bare/package import, dynamic tag, no
matching export) or its param shape isn't recognized — callers treat
null as "unknown, don't filter" (today's permissive behavior). The AST
work (import lookup, finding the component function, reading its declared
props) is src/ast's; this function only does the file resolution, scoped to root.
Parameters#
root
string
Project root.
pageAbsPath
string
Absolute path of the page file tagName is rendered in.
pageAst
any
The page's already-parsed AST (from parsePageTree).
tagName
string
The JSX tag name to resolve (the rendered custom component).
Returns#
any
{closed, names, types} (see declaredPropNames), or null when the child
can't be resolved or its prop shape isn't recognized.
resolvePageFile()#
resolvePageFile(
root,feature,file):object
Defined in: packages/engine/pagesEditor.mjs:118
Resolve + validate a (feature, file) pair to an absolute path strictly
inside that feature's pages/ folder. Throws PagesEditorError on any attempt to escape it — a .. segment, an absolute file
path, a symlink-free resolution that lands outside pages/, or a
feature/file that simply doesn't exist.
Parameters#
root
string
Project root.
feature
string
Feature directory name.
file
string
Page file path relative to that feature's pages/ folder, as sent by the client.
Returns#
object
The resolved absolute path and its path relative to root.
absPath
absPath:
string
relPath
relPath:
string
rewireWireInSnippet()#
rewireWireInSnippet(
snippetSource,wire): {ok:true;snippet:string; } | {error:string;ok:false; }
Defined in: packages/engine/pagesEditor.mjs:448
Ticket F.2 — the visual composer's wire-rewrite: moves
an existing prop from one child to a different sibling under the same
parent, expressed as an unambiguous source-text edit (remove the
attribute from the old child, add the identical attribute — same kind
and value — to the new one), reusing removeAttributeSnippet/
buildAttributeSnippet/patchNode unchanged rather than a second splicing
mechanism. Operates purely on the snippet's own text (never a file) so
the result can be handed straight to the existing save-back-to-source +
diff-preview flow. Never throws — an invalid drag (stale ids, a
cross-parent target, a name collision on the target) comes back as
{ok: false, error} so the canvas can show a clear inline message and
leave the snippet untouched, per 's "reject rather than write broken
code" instruction.
Parameters#
snippetSource
string
The snippet's own current source text.
wire
The common parent, the prop being moved, and the sibling ids it moves from/to.
fromChildId
string
parentId
string
propName
string
toChildId
string
Returns#
{ ok: true; snippet: string; } | { error: string; ok: false; }
The rewired snippet text, or why the rewire was rejected.
serializeTree()#
serializeTree(
source):object
Defined in: packages/engine/pagesEditor.mjs:216
Lightweight JSON-safe projection of parsePageTree's node records (drops
start/end/internal fields the frontend doesn't need, but keeps a
content hash so the client can detect the tree going stale).
Parameters#
source
string
Page file's full JSX/TSX source text.
Returns#
object
The stripped-down tree the client renders, plus
source's content hash.
contentHash
contentHash:
string
roots
roots:
any[]