ui/server/src/pagesEditor
Backend for epic #48 (visual page/JSX tree editor) — #49 pages browser, #50 JSX tree parse, #52 snippet save-back, #53 props inspector, #54 auto-map, #56 enforcement guard.
Cockpit server, v0.9. Source: ui/server/src/pagesEditor.mjs.
Classes#
PagesEditorError#
Defined in: ui/server/src/pagesEditor.mjs:37
Extends#
Error
Constructors#
Constructor
new PagesEditorError(
message,__namedParameters?):PagesEditorError
Defined in: ui/server/src/pagesEditor.mjs:38
Parameters
message
any
__namedParameters?
status?
number = 400
Returns
Overrides
Error.constructor
Properties#
code
code:
any
Defined in: ui/server/src/pagesEditor.mjs:42
status
status:
number
Defined in: ui/server/src/pagesEditor.mjs:40
violations
violations:
any
Defined in: ui/server/src/pagesEditor.mjs:41
Functions#
addChildInSnippet()#
addChildInSnippet(
snippetSource,parentId): {error:string;ok:boolean;snippet?:undefined; } | {error?:undefined;ok:boolean;snippet:string; }
Defined in: ui/server/src/pagesEditor.mjs:448
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
any
parentId
any
Returns#
{ error: string; ok: boolean; snippet?: undefined; } | { error?: undefined; ok: boolean; snippet: string; }
applyAutoMap()#
applyAutoMap(
source,nodeId,propNames):any
Defined in: ui/server/src/pagesEditor.mjs:620
Parameters#
source
any
nodeId
any
propNames
any
Returns#
any
applyWrapConfirm()#
applyWrapConfirm(
root,absPath,source,nodeId,name):object
Defined in: ui/server/src/pagesEditor.mjs:805
#533 -- 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
any
absPath
any
source
any
nodeId
any
name
any
Returns#
object
component
component:
object
component.file
file:
string
component.name
name:
string
dryRun?
optionaldryRun?:true
expression
expression:
object
expression.file
file:
string
expression.name
name:
string
page
page:
object
page.file
file:
string
assertContentHash()#
assertContentHash(
source,contentHash,__namedParameters?):void
Defined in: ui/server/src/pagesEditor.mjs:139
#590 — 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
#590's bug (a client that omits the hash silently wins over whatever is on disk).
Parameters#
source
any
contentHash
any
__namedParameters?
mismatchMessage?
string = 'The file changed on disk since this was loaded — reload the tree and try again.'
Returns#
void
buildAttributeSnippet()#
buildAttributeSnippet(
source,nodeId,propName,kind,value,index):string
Defined in: ui/server/src/pagesEditor.mjs:293
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 #52 so both go through one mechanism (and one #56 enforcement gate).
#77 follow-up to #53 — 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
any
nodeId
any
propName
any
kind
any
value
any
index
any
Returns#
string
buildComponentUsageJsx()#
buildComponentUsageJsx(
name,described):string
Defined in: ui/server/src/pagesEditor.mjs:669
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
any
described
any
Returns#
string
buildPaletteInsertion()#
buildPaletteInsertion(
root,pageAbsPath,source,entry,describe?):Promise<{error?:undefined;ok:boolean;source:any; } | {error:string;ok:boolean; }>
Defined in: ui/server/src/pagesEditor.mjs:704
#532 (Slice 2 of #518'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, #517), 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
any
pageAbsPath
any
source
any
entry
any
describe?
(root, relPath, opts?) => Promise<any>
Returns#
Promise<{ error?: undefined; ok: boolean; source: any; } | { error: string; ok: boolean; }>
buildProviderUsageStatement()#
buildProviderUsageStatement(
name):string
Defined in: ui/server/src/pagesEditor.mjs:680
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
any
Returns#
string
buildWrapSuggestion()#
buildWrapSuggestion(
root,absPath,source,nodeId,palette,name,__namedParameters?):object
Defined in: ui/server/src/pagesEditor.mjs:772
#533 (Slice 3 of #518'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, #517) 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
any
absPath
any
source
any
nodeId
any
palette
any
name
any
__namedParameters?
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: ui/server/src/pagesEditor.mjs:822
#56 — 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
any
relPath
any
patchedSource
any
Returns#
object
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: ui/server/src/pagesEditor.mjs:46
Parameters#
root
any
Returns#
string
findUnmappedProps()#
findUnmappedProps(
source,nodeId,root,pageAbsPath):object
Defined in: ui/server/src/pagesEditor.mjs:542
#54 — 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.
#77 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
any
nodeId
any
root
any
pageAbsPath
any
Returns#
object
candidates
candidates:
string[]
childPropsResolved
childPropsResolved:
boolean
nodeId
nodeId:
any
getNodeProps()#
getNodeProps(
source,nodeId):object
Defined in: ui/server/src/pagesEditor.mjs:272
#53 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 #54's auto-mapper).
Parameters#
source
any
nodeId
any
Returns#
object
nodeId
nodeId:
any
props
props:
any=node.props
scopeNames
scopeNames:
string[]
tag
tag:
any=node.tag
getNodeSnippet()#
getNodeSnippet(
source,nodeId):object
Defined in: ui/server/src/pagesEditor.mjs:217
The exact source snippet for one node plus its parent chain's tag names for breadcrumb display.
Parameters#
source
any
nodeId
any
Returns#
object
contentHash
contentHash:
any
nodeId
nodeId:
any
snippet
snippet:
any
getScopeLinks()#
getScopeLinks(
source,nodeId,root,pageAbsPath):any
Defined in: ui/server/src/pagesEditor.mjs:600
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
any
nodeId
any
root
any
pageAbsPath
any
Returns#
any
hashOf()#
hashOf(
source):any
Defined in: ui/server/src/pagesEditor.mjs:187
Parameters#
source
any
Returns#
any
listAllPages()#
listAllPages(
root):any
Defined in: ui/server/src/pagesEditor.mjs:75
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
any
Returns#
any
listFeatures()#
listFeatures(
root):any
Defined in: ui/server/src/pagesEditor.mjs:51
Every feature directory under the configured features root.
Parameters#
root
any
Returns#
any
listPages()#
listPages(
root,feature):string[]
Defined in: ui/server/src/pagesEditor.mjs:63
Every file under features//pages/**, relative to that pages/ folder (e.g. "Home.jsx", "billing/Detail.tsx").
Parameters#
root
any
feature
any
Returns#
string[]
moveNodeInSnippet()#
moveNodeInSnippet(
snippetSource,nodeId,direction): {error:string;ok:boolean;snippet?:undefined; } | {error?:undefined;ok:boolean;snippet:string; }
Defined in: ui/server/src/pagesEditor.mjs:414
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
any
nodeId
any
direction
any
Returns#
{ error: string; ok: boolean; snippet?: undefined; } | { error?: undefined; ok: boolean; snippet: string; }
parsePageTree()#
parsePageTree(
source):object
Defined in: ui/server/src/pagesEditor.mjs:162
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
any
Returns#
object
ast
ast:
any
byId
byId:
Map<string,any>
roots
roots:
any[]
parseSnippetToTree()#
parseSnippetToTree(
snippetSource): {contentHash:any;error:any;roots:object[]; } | {error:any;roots:any[]; }
Defined in: ui/server/src/pagesEditor.mjs:206
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
any
Returns#
{ contentHash: any; error: any; roots: object[]; } | { error: any; roots: any[]; }
patchNode()#
patchNode(
source,nodeId,newSnippet,expectedHash):string
Defined in: ui/server/src/pagesEditor.mjs:264
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
any
nodeId
any
newSnippet
any
expectedHash
any
Returns#
string
removeAttributeSnippet()#
removeAttributeSnippet(
source,nodeId,propName):string
Defined in: ui/server/src/pagesEditor.mjs:318
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
any
nodeId
any
propName
any
Returns#
string
removeNodeInSnippet()#
removeNodeInSnippet(
snippetSource,nodeId): {error:string;ok:boolean;snippet?:undefined; } | {error?:undefined;ok:boolean;snippet:string; }
Defined in: ui/server/src/pagesEditor.mjs:388
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
any
nodeId
any
Returns#
{ error: string; ok: boolean; snippet?: undefined; } | { error?: undefined; ok: boolean; snippet: string; }
resolveDeclaredPropNames()#
resolveDeclaredPropNames(
root,pageAbsPath,pageAst,tagName):any
Defined in: ui/server/src/pagesEditor.mjs:514
#77 follow-up to #54 — 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
any
pageAbsPath
any
pageAst
any
tagName
any
Returns#
any
resolvePageFile()#
resolvePageFile(
root,feature,file):object
Defined in: ui/server/src/pagesEditor.mjs:84
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
any
feature
any
file
any
Returns#
object
absPath
absPath:
any=resolved
relPath
relPath:
string=relFromRoot
rewireWireInSnippet()#
rewireWireInSnippet(
snippetSource,__namedParameters): {error?:undefined;ok:boolean;snippet:string; } | {error:any;ok:boolean;snippet?:undefined; }
Defined in: ui/server/src/pagesEditor.mjs:343
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 #119's "reject rather than write broken
code" instruction.
Parameters#
snippetSource
any
__namedParameters
fromChildId
any
parentId
any
propName
any
toChildId
any
Returns#
{ error?: undefined; ok: boolean; snippet: string; } | { error: any; ok: boolean; snippet?: undefined; }
serializeTree()#
serializeTree(
source):object
Defined in: ui/server/src/pagesEditor.mjs:169
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
any
Returns#
object
contentHash
contentHash:
any
roots
roots:
object[]