Line

You are reading docs for v0.8. See latest (v0.9)

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.8. 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

PagesEditorError

Overrides

Error.constructor

Properties#

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:418

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:590

Parameters#

source

any

nodeId

any

propNames

any

Returns#

any


applyWrapConfirm()#

applyWrapConfirm(root, absPath, source, nodeId, name): object

Defined in: ui/server/src/pagesEditor.mjs:775

#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?

optional dryRun?: true

expression

expression: object

expression.file

file: string

expression.name

name: string

page

page: object

page.file

file: string


buildAttributeSnippet()#

buildAttributeSnippet(source, nodeId, propName, kind, value, index): string

Defined in: ui/server/src/pagesEditor.mjs:263

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:639

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:674

#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:650

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:742

#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:792

#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:45

Parameters#

root

any

Returns#

string


findUnmappedProps()#

findUnmappedProps(source, nodeId, root, pageAbsPath): object

Defined in: ui/server/src/pagesEditor.mjs:512

#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:242

#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:194

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(source, nodeId, root, pageAbsPath): any

Defined in: ui/server/src/pagesEditor.mjs:570

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:164

Parameters#

source

any

Returns#

any


listAllPages()#

listAllPages(root): any

Defined in: ui/server/src/pagesEditor.mjs:74

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:50

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:62

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:384

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:139

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:183

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:216

Patch one node's JSX back into the file. We re-parse the current disk content, re-locate the node by id, verify the caller's contentHash still matches (optimistic-concurrency guard), verify the replacement parses as a standalone JSX expression on its own, then splice source.slice(0, node.start) + newSnippet + source.slice(node.end). Splicing the original text (rather than handing the whole file to a generator) is what guarantees every other line in the file — imports, comments, unrelated formatting — comes out byte-for-byte unchanged.

Parameters#

source

any

nodeId

any

newSnippet

any

expectedHash

any

Returns#

string


removeAttributeSnippet()#

removeAttributeSnippet(source, nodeId, propName): string

Defined in: ui/server/src/pagesEditor.mjs:288

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:358

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:484

#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:83

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:313

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:146

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[]