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