Plugin transformation
Plugin.fs holds the transformation logic and the two public attributes. It is the heart of the package.
Entry points
Both attributes are Fable MemberDeclarationPluginAttributes. Fable hands each one the declaration it is attached to, and the attribute returns a rewritten one. Each has a constructor taking a ComponentFlag, and one taking an int.
[<SolidComponent>]
For let bindings. It creates a PluginContext, prints the AST if DebugMode is set, and runs AST.transform over the body. Nothing else: there are no props to omit or merge.
[<SolidTypeComponent>]
For members of a type, like member props.View = .... It:
- prints the AST if
DebugModeis set - checks the member with
SchemaRules.ValidMemberRef(see Spec), and stores the self identifier on the context - runs
AST.transformover the body, which collects every getter and setter it finds on the self identifier - unless
SkipOmitis set, wraps the body inconst PARTAS_OTHERS = omit(props, ...)with the distinct getters - puts
props = merge({ ... }, props)in front, if there were any setters - renames the member to the declaring type's
DisplayName, soBadge.Viewis written asfunction Badge(props)
The merge goes first in the output, so defaults are in place before omit reads the props. If the member does not match the schema, the attribute warns that you should use [<SolidComponent>], and only transforms the body.
transform
transform is the first, and main, pass. Its job: reduce the AST where that is reasonable, find tags and expand them, and drop expressions only when there is no other choice. It matches, in order:
Emitexpressions, whose arguments are transformed before they are injected. Fable uses these for operators like&&=.- tag values and their
.render - special attribute transformations, such as pojo constructors
TagConstructor, which is collected withcollectTagInfoand rendered withBaked.renderElementSequentialexpressions, transformed one by oneUncurriedPropsApply: a curried call of a function prop, such asprops.f a b, rewritten to one uncurried callPropsGetterOrSetter, which records a props read or default- a call to a
[<SolidComponent>]let binding that returns an element, wrapped inuntrack(() => ...)ascreateComponentdoes for<Comp/> - calls to imported getters, rewritten to field reads
IfThenElseandDecisionTree, transformed branch by branch- an
AttributeExpressionat the top level. This should not happen, so it is logged as a warning and reduced to its value. - everything else that can contain expressions: lambdas, delegates,
lets, calls, applications, values (arrays, lists, records, tuples, unions, string templates), operations,Gets and object expressions. Each is rebuilt with its parts transformed.
Most of this is picking up an expression that contains other expressions, transforming those, and putting the structure back together.
Following a component through
Take this component:
[<Erase>]
type Badge() =
inherit span()
[<Erase>]
member val tone: string = unbox null with get, set
[<SolidTypeComponent>]
member props.View =
props.tone <- "info"
span(class' = "badge " + props.tone).spread props { props.children }
props.tone <- "info"matchesPropsGetterOrSetteras a setter.("tone", "info")is recorded and the line is removed.span(...)...{ ... }is aRuncall with a tag as its first argument, soTagConstructormatches it.PropCollectorcollectsclassfrom the constructor.AttributeExpressionturns.spread propsinto{...PARTAS_OTHERS} n$.BuilderCollectorcollectsprops.childrenas the child.- The reads of
props.toneandprops.childrenare recorded as getters. - Last, the attribute wraps the result with
omit(props, "tone", "children")andmerge({ tone: "info" }, props), and renames the function toBadge.
The result:
export function Badge(props) {
props = merge({
tone: "info",
}, props);
const PARTAS_OTHERS = omit(props, "tone", "children");
return <span class={"badge " + props.tone}
{...PARTAS_OTHERS} n$={false}>
{props.children}
</span>;
}
When something goes missing
Wherever the transformation drops an expression, it calls PluginContext.debugDisposal. Compile the component with ComponentFlag.PrintDisposals and each drop is logged as a warning with the stage it happened in. See Submitting issues.
