Skip to content

Instantly share code, notes, and snippets.

@christianalfoni
Created November 16, 2025 21:17
Show Gist options
  • Select an option

  • Save christianalfoni/4cf39fdf2276740c13f6c2fe58cfc735 to your computer and use it in GitHub Desktop.

Select an option

Save christianalfoni/4cf39fdf2276740c13f6c2fe58cfc735 to your computer and use it in GitHub Desktop.

Short version: If you’re okay with some rules/restrictions, this is very doable. If you want it to work with “totally arbitrary JS inside the component”, it gets hard (full control-flow/data-flow analysis territory).

Let me break it down in terms of what you probably want for Rask, and what the compiler would actually have to do.

What you have vs what you want

Today:

function MyComp() { // Reactive scope (runs once on mount) const state = createState({})

// Reconciling scope (runs on each “render”) return () => (

) }

What you want users to write:

function MyComp() { const state = createState({})

return

}

And have the compiler turn that into something equivalent to:

function MyComp() { const state = createState({})

const view = () => { return

}

return view }

Plus support for:

function MyComp() { const state = createState({})

if (state.visible) { return } else { return } }

…which should become something like:

function MyComp() { const state = createState({})

const view = () => { if (state.visible) { return } else { return } }

return view }

So the “compiler problem” is:

Given a component function, split it into: • setup (runs once) • view (a function that runs on each reconcile), and preserve semantics.

A practical strategy that isn’t insane

If you embrace a few simple rules, you can get away with a pretty clean transform and not a research project.

  1. Define some “reactive primitives”

Start by deciding which calls mark “reactive setup”: • createState • createSignal • createEffect • etc.

Your plugin looks for those at the top level of the component body (not inside loops/ifs/nested functions).

  1. (“Rules of Rask Components” – like rules of hooks)

If you enforce something like: 1. createState/friends must be: • in the component’s top-level scope • not in conditionals/loops/try/finally/nested funcs. 2. The component can return JSX anywhere (simple if, switch, ternary, early returns) – but not from inside inner functions. 3. Top-level code is “setup”; code in the execution path that leads to a return is “view”.

You can then do an 80/20 transform that’s not too insane.

Minimal viable transform (surprisingly simple)

You can get a working version with a very simple approach: 1. Detect component functions: • Name starts with capital • or it returns JSX • or it calls createState etc. 2. Inside that function: • Collect all top-level declarations that look like “setup”: • variable declarations whose initializer is: • a call to a reactive primitive (must be setup) • or a function/expression that doesn’t contain JSX and doesn’t return JSX. • Everything else (control flow, returns, JSX) goes inside the view function.

Pseudocode (conceptually):

function transformComponent(fn) { const body = fn.body.body // array of statements const setup: Statement[] = [] const view: Statement[] = []

for (const stmt of body) { if (isReactiveInit(stmt)) { setup.push(stmt) } else if (isPureSetup(stmt)) { // e.g. const handleClick = () => { state.count++ } // no JSX, no return at this level setup.push(stmt) } else { view.push(stmt) } }

fn.body.body = [ ...setup, t.variableDeclaration("const", [ t.variableDeclarator( t.identifier("__view"), t.arrowFunctionExpression([], t.blockStatement(view)) ) ]), t.returnStatement(t.identifier("__view")) ] }

This already handles:

function MyComp() { const state = createState({ count: 0 })

const handleClick = () => { state.count++ }

if (state.count > 0) { return

Positive
}

return

Zero
}

Becomes:

function MyComp() { const state = createState({ count: 0 })

const handleClick = () => { state.count++ }

const __view = () => { if (state.count > 0) { return

Positive
}

return <div>Zero</div>

}

return __view }

Which has the semantics you want: • createState & handler defined once. • __view() re-runs, reading state.count.

What about returns inside if/switch?

That falls out automatically, because you’re just wrapping all those statements into the body of __view. You don’t have to analyze control flow deeply as long as: • All returns that matter are at top level (not inside nested functions).

Where it starts to get tricky

It becomes significantly more complex if you want to support patterns like:

function MyComp() { const state = createState({ count: 0 })

const doubled = state.count * 2 // you want this to be re-evaluated on each render

if (doubled > 10) { return } return }

You have two options:

Option A – Simple rule: no reactive reads in top-level expressions

You say:

At the top level of the component body, you can create reactive things, define handlers, etc., but you may not read reactive state directly. Reactive reads must be inside the JSX/return logic.

Then users are supposed to write:

const doubled = () => state.count * 2

if (doubled() > 10) ...

or just:

if (state.count * 2 > 10) ...

This keeps the transform simple: any statement that reads a “reactive symbol” at top level is automatically classified as view (or it’s a compile-time error). No heavy analysis.

Option B – Fully smart data-flow analysis

If you don’t impose that restriction, then you need to: • Build a control-flow graph for the component body. • Track which statements must re-run on each “render”, based on: • they read reactive variables, or • they lead to a return of JSX, etc. • Split declarations if necessary (e.g. hoisting function declarations while keeping their invocation in view code).

This quickly looks like a mini compiler project, not just a macro transform.

So how “complex” is this, really?

With constraints (hooks-like rules + no top-level reactive reads): • Conceptually straightforward: • You’re basically doing: “hoist obvious setup, wrap the rest in __view”. • Implementation-wise: • A Babel/TS/SWC plugin with a few passes: 1. Find reactive primitives & mark their bindings. 2. Classify statements into setup vs view using simple structural rules. 3. Rebuild the function body as setup + const __view = () => { view } + return __view.

You’ll mostly fight with: • Edge cases like return someCondition && ; • Arrow functions vs function declarations; • Ensuring you don’t accidentally move return into setup.

But it’s all local to a single function; no cross-file analysis.

Without constraints (arbitrary JS, arbitrary reactive reads): • You’re into: • control-flow graphs • use/def chains • potentially needing to split/bisect complex statements. • Still doable, but complexity ramps up quite a bit.

My recommendation for Rask

Given what you’re doing with Rask (and that it’s nicely opinionated already): 1. Define “Rules of Rask Components” (like Rules of Hooks). E.g.: • All reactive primitives must be top-level. • No reading reactive state at top-level (outside view) – or make that a hard compile error. • return only at top level of the component body (can be inside if, switch, ternaries, but not inside nested functions). 2. Implement the simple “wrap + hoist” transform first: • Hoist reactive initializers + non-JSX helper declarations. • Wrap everything else in __view and return __view. 3. Iterate only if needed: • If you later find real-world patterns you want to support (e.g. derived values at top level), you can extend the analysis.

That path keeps it approachable, fits the mental model of “reactive setup + reconciling view”, and doesn’t require building a full-blown optimizer.

If you want, I can sketch the actual Babel/SWC plugin structure and the concrete classification rules in code next.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment