WildflowerJS Reactive JS, No BS*

A no-build reactive JavaScript framework, rooted in the web platform.
No build step. No dependencies. No lock-in.

<script src="wildflower.min.js"></script> ...and start building.

Back to Basics

The code you write is 100% web standard code. HTML stays HTML. JavaScript stays JavaScript. CSS stays CSS. No JSX, no templating language, no custom syntax to learn. If you know the web platform, you already know how to use this.

WildflowerJS extends the web platform. It doesn't replace it.

Your Development Simplified

Because you develop with 100% web standards, every tool in your existing chain already understands the code: IDE, browser DevTools, linter, formatter, screen reader, SEO crawler. Nothing to install, no custom file types, no sourcemaps. Save the file, refresh, and your change is live.

Just be a web developer.

Batteries Included: One Mental Model

Router, SSR, stores, computed properties, two-way binding, event modifiers, data pools, and TypeScript types, all built in, all speaking the same language. Learn data-bind once and you know binding everywhere: lists, pools, stores, forms. There's no five-library stack to keep in sync.

One script tag. Everything you need.

<div data-component="counter">
  <span data-bind="count"></span>
  <button data-action="increment">
    +1
  </button>
</div>

<script>
wildflower.component('counter', {
  state: { count: 0 },
  increment() { this.count++ }
})
</script>

How It Works

data-bind connects state to the DOM.

data-action connects events to methods.

this.count++ triggers a precise DOM update.

Mutate state. The DOM updates.

Two Reactivity Modes

data-list for automatic reactivity: mutate state, DOM updates. data-pool for explicit control: plain objects, zero proxy overhead, you say what changed.

Same template syntax. Different performance profile. From interactive forms to per-frame particle systems. You choose the right tradeoff for the job.

Try it. Right-click, inspect this demo. Every dot is a real DOM element.

See full demo →

* Build Step

Zero Toolchain

Modern frameworks ask you to install a compiler, a bundler, a package manager, hundreds of fragile transitive dependencies, and a framework-specific file format, before you write a single line of your application.

WildflowerJS was built starting from a single principle: no build step, no tooling. Ever.

WildflowerJS asks you to add a script tag.

There's no CLI scaffolding step, no config files, no .vue/.jsx/.svelte source format. You don't debug through sourcemaps or wait on a build pipeline. Your project has zero dependencies.

Performance isn't a tradeoff. Build steps optimize bundle delivery, not the runtime work that follows it. WildflowerJS writes directly to the DOM, with no virtual DOM or reconciliation pass between state change and update, so it doesn't need a build step to be fast.

The framework is full-featured without the toolchain: router, SSR, stores, computed properties, transitions, pools. You don't need a toolchain to use any of it.

my-app/
  index.html
  app.js
  style.css
  wildflower.min.js

That's the entire project. No package.json.
No node_modules. No config files. Ship it.

Zero Install. Zero Attack Surface.

Every dependency you install is trust extended to a maintainer you've never met, running scripts on your dev machine and in your CI. A typical React + Vite + UI‑lib setup pulls in 300+ transitive packages before you write a feature.

Each one is a potential intrusion vector. NPM worms, OAuth chains compromising deploy platforms, postinstall hijacking: the supply chain is now where production code gets compromised, not the deploy. And signing isn't a backstop: Mini Shai‑Hulud (May 2026) compromised 170+ packages whose malicious versions carried valid SLSA Build Level 3 provenance, because the attestation came from build infrastructure the worm had already taken over.

WildflowerJS users don't have this attack surface, by construction. There is no npm install, no postinstall script, no transitive package graph. The framework is one file you copy or pin by hash.

As of v1.1, the same holds for building the framework itself. WildflowerJS bundles with a vendored rollup and terser pipeline pulled as three SHA‑512‑pinned tarballs: no npm install, no transitive packages, no postinstall scripts in the build path. The entire toolchain is three files you verify by hash.

Zero dependencies is the absence of a problem the rest of the industry has not properly addressed.

A typical React/Vue project:

  npm install
  ├── hundreds of packages
  ├── from hundreds of maintainers
  ├── postinstall scripts run on install
  └── tens to hundreds of MB of transitive code

WildflowerJS:

  <script src="wildflower.min.js"></script>
  └── 1 file.
      No transitive dependencies.

Zero Lock-in

WildflowerJS works with the DOM, not instead of it. There's no virtual DOM intercepting your code and no compiler rewriting your markup. The render cycle is yours.

That means Leaflet, DataTables, Chart.js, D3, Three.js, any library that touches the DOM, just works. No wrapper packages or framework-specific escape hatches required. Drop in a script tag and use it.

Because your code is standard HTML and JavaScript, you're never locked in. Your skills transfer and your code is more portable. If you outgrow the framework, your knowledge doesn't expire.

This also means your "ecosystem" is all of the world of vanilla JS. Without compromises or hacks.

<!-- Use any library directly -->
<div data-component="map-view">
  <div id="map" style="height: 400px"></div>
</div>
wildflower.component('map-view', {
  state: { lat: 51.505, lng: -0.09 },
  init() {
    // Leaflet works as-is. No wrappers.
    this._map = L.map('map')
      .setView([this.lat, this.lng], 13);
    L.tileLayer('https://{s}.tile.osm.org'
      + '/{z}/{x}/{y}.png').addTo(this._map);
  }
})

Precise Reactivity

When you write this.count++, WildflowerJS updates the single DOM node bound to count. Nothing else is touched. There's no tree diffing or reconciliation pass to figure that out.

This isn't a tradeoff. You get fine-grained updates and a simple mental model. Change a property, the bound element updates. That's the entire reactivity model.

Other frameworks ask you to learn signals, accessors, memos, effects, and subscription lifecycles to achieve what WildflowerJS does with a property assignment.

wildflower.component('dashboard', {
  state: {
    users: 1420,
    status: 'healthy'
  },
  computed: {
    summary() {
      return this.users + ' users, ' + this.status;
    }
  },
  refresh() {
    this.users = 1421;
    // Only the elements bound to 'users'
    // and 'summary' update. Everything
    // else on the page is untouched.
  }
})

One Reactivity Model. Everywhere.

Components, Stores, and Plugins all share the same reactive foundation. State, computed properties, and methods work identically no matter where they live. Learn it once, it works the same way in a UI component, a global store, or a framework plugin.

Other frameworks make you learn a different system for each layer. React components use hooks, but stores need Redux or Zustand, which are completely different APIs. Vue components use reactive data, but Pinia stores have their own patterns. Every layer is a new mental model.

In WildflowerJS, there's one model. A store is a component without a template. A plugin is an entity that extends the framework itself, adding directives, lifecycle hooks, and services. The same this.count++ triggers the same reactivity everywhere.

This unlocks patterns other frameworks can't express. A store can run headless physics simulations with tick(), feeding data into a component that renders it through a pool, all using the same reactive primitives, no glue code required.

// Component: reactive UI
wildflower.component('cart', {
  state: { items: [] },
  computed: {
    total() { return this.items.length; }
  }
})

// Store: global shared state
wildflower.store('user', {
  state: { name: '', role: 'guest' },
  computed: {
    isAdmin() { return this.role === 'admin'; }
  }
})

// Plugin: extends the framework
wildflower.plugin({
  name: 'notifications',
  state: { items: [], unreadCount: 0 },
  computed: {
    hasUnread() { return this.unreadCount > 0; }
  },
  add(msg) { this.items.push(msg); this.unreadCount++; }
})
// Access globally: wildflower.$notifications.add(...)

// Same state. Same computed. Same methods.

Data Pools

Every framework wraps collection items in reactive proxies, whether the item needs it or not. WildflowerJS gives you a choice: data-list for push reactivity (automatic), data-pool for pull reactivity (explicit control, zero proxy overhead).

Pools render plain objects with the same template syntax as lists. Mutate the object, call markDirty(), and only that item updates. Full CRUD, selection, bulk operations, all faster than the push-reactive path.

And because pools use pull-based rendering, they scale to simulations, games, particle systems, and data visualizations at native frame rate. Use cases that would choke a virtual DOM. No other framework has anything like this.

<div data-component="user-table">
  <tbody data-pool="users" data-key="id">
    <template>
      <tr>
        <td data-bind="name"></td>
        <td data-bind="status"
            data-bind-class="status === 'active'
              ? 'badge success'
              : 'badge inactive'"></td>
      </tr>
    </template>
  </tbody>
</div>
wildflower.component('user-table', {
  pools: { users: {} },

  init() {
    // Populate: plain objects, no proxies
    data.forEach(u => this.pools.users.add(u));
  },

  // Optional: add tick() and the same pool
  // renders every frame. Same template, same
  // data, different rendering frequency.
  // That's the only difference between a
  // display table and a particle system.
})

Built for AI-Assisted Development

Because WildflowerJS is standard HTML and JavaScript, AI code assistants already know how to write it. There's no custom syntax to hallucinate or compiler quirks to work around. The code an AI generates runs exactly as written, with no build step between generation and execution.

We go further. WildflowerJS ships an AI-optimized reference page with patterns, anti-patterns, and examples designed for code generation context windows. Our llms.txt file follows the llms.txt convention for machine-readable documentation.

And for structured app generation, our Universal App Manifest lets you describe an entire application as a JSON schema (components, state, computed properties, methods, templates) and have an AI generate the working code from the manifest, mediated through framework-specific idiom files.

You: "Build me a todo app with
WildflowerJS"

AI reads llms.txt or ai-assistant.html
     ↓
Generates standard HTML + JS
     ↓
<div data-component="todo-app">
  <input data-model="newItem">
  <button data-action="addItem">
    Add
  </button>
  <ul data-list="items">
    <template>
      <li data-bind="text"></li>
    </template>
  </ul>
</div>
     ↓
Open in your browser. It works, and you can read and understand the code.

Migrating from Solid

Solid and WildflowerJS share a core philosophy: fine-grained reactivity without a virtual DOM. Both frameworks update only the specific DOM nodes that change, avoiding the overhead of diffing an entire tree. The key differences are syntax — JSX with signal getters vs. HTML with data-* attributes — and state access patterns: Solid requires explicit getter calls (count()) and setter functions (setCount()), while WildflowerJS resolves properties directly (this.count) and accepts plain assignment (this.count = 5).

Good news: Because both frameworks use fine-grained reactivity, your mental model of when things update transfers directly. The migration is primarily syntactic: replacing JSX and signal accessors with HTML attributes and direct property access.

Mental Model Shift

Solid and WildflowerJS are closer in architecture than most framework pairs. Both avoid the virtual DOM entirely. Fine-grained reactivity is the shared philosophy. Here is where the two approaches diverge:

Aspect Solid WildflowerJS
Reactivity granularity Fine-grained (no VDOM) Fine-grained (no VDOM)
State access Signal getter functions: count() Direct property access: this.count
State updates setCount(5) / produce() this.count = 5 / direct mutation
Template syntax JSX (components as functions) HTML with data-* attributes
Derived state createMemo() with explicit tracking computed: {} with auto-tracking
Side effects createEffect() init() lifecycle hook
Build step Required (JSX compilation) None, works directly in the browser
Shared strength: Both frameworks update only the exact DOM nodes tied to changed data. If you set count to a new value, only the element displaying count is touched, not the entire component tree. This precision is preserved in the migration.

Concept Mapping

A quick reference for translating Solid concepts to their WildflowerJS equivalents:

Solid WildflowerJS Notes
createSignal(value) state: { prop: value } No getter/setter pair needed
signal() (read) this.prop Direct property access, no function call
setSignal(value) this.prop = value Plain assignment triggers reactivity
createMemo(() => ...) computed: { prop() { ... } } Auto-tracked, cached until dependencies change
createEffect(() => ...) init() lifecycle Runs once on mount; use watch for ongoing observation
onMount(fn) init() Called after component is connected to the DOM
onCleanup(fn) destroy() Called when component is removed from the DOM
createStore([]) wildflower.store('name', {}) Named global stores with state, computed, and methods
produce(fn) Direct mutation this.items.push(item), no wrapper needed
<Show when={...}> data-show / data-render data-show toggles display; data-render adds/removes from DOM
<For each={...}> data-list With <template> child for item markup
<Switch> / <Match> Multiple data-show elements Or use data-render for mutually exclusive sections
createContext() wildflower.store('name', {}) Stores are globally accessible; no provider tree needed
useContext(ctx) subscribe: { storeName: [...] } Declarative subscription with automatic cleanup

Side-by-Side: Todo List

A complete todo application in both frameworks, showing how the same functionality maps across:

Solid

import { createSignal, createMemo, For } from 'solid-js';
import { createStore, produce } from 'solid-js/store';

function TodoApp() {
    const [todos, setTodos] = createStore([]);
    const [newTodo, setNewTodo] = createSignal('');
    const remaining = createMemo(() =>
        todos.filter(t => !t.done).length);

    function addTodo() {
        if (!newTodo().trim()) return;
        setTodos(produce(t =>
            t.push({ id: Date.now(), text: newTodo(), done: false })));
        setNewTodo('');
    }

    function toggleTodo(index) {
        setTodos(index, 'done', d => !d);
    }

    function removeTodo(index) {
        setTodos(produce(t => t.splice(index, 1)));
    }

    return (
        <div>
            <h3>Todos ({remaining()} remaining)</h3>
            <input value={newTodo()}
                   onInput={e => setNewTodo(e.target.value)}
                   onKeyDown={e => e.key === 'Enter' && addTodo()} />
            <button onClick={addTodo}>Add</button>
            <ul>
                <For each={todos}>
                    {(todo, index) => (
                        <li>
                            <input type="checkbox" checked={todo.done}
                                   onChange={() => toggleTodo(index())} />
                            <span style={{ 'text-decoration':
                                todo.done ? 'line-through' : 'none' }}>
                                {todo.text}
                            </span>
                            <button onClick={() => removeTodo(index())}>×</button>
                        </li>
                    )}
                </For>
            </ul>
        </div>
    );
}

WildflowerJS

HTML
<div data-component="todo-app">
    <h3>Todos (<span data-bind="remaining">0</span> remaining)</h3>
    <input data-model="newTodo"
           data-action="keydown.enter:addTodo" />
    <button data-action="addTodo">Add</button>
    <ul data-list="todos" data-key="id">
        <template>
            <li>
                <input type="checkbox" data-model="done" />
                <span data-bind="text"
                      data-bind-style="{ textDecoration: done ? 'line-through' : 'none' }"></span>
                <button data-action="removeTodo">×</button>
            </li>
        </template>
    </ul>
</div>
JavaScript
wildflower.component('todo-app', {
    state: { todos: [], newTodo: '' },
    computed: {
        remaining() {
            return this.todos.filter(t => !t.done).length;
        }
    },
    addTodo() {
        if (!this.newTodo.trim()) return;
        this.todos.push({
            id: Date.now(),
            text: this.newTodo,
            done: false
        });
        this.newTodo = '';
    },
    removeTodo(event, element, details) {
        this.todos.splice(details.index, 1);
    }
});
What changed: The toggleTodo function is gone entirely. The data-model="done" on the checkbox handles two-way binding automatically. The produce() wrapper around array mutations is replaced with direct .push() and .splice() calls. The signal getter remaining() becomes a computed property accessed as data-bind="remaining".

Pattern-by-Pattern Migration

Signals → State Properties

Solid signals use a getter/setter pair. WildflowerJS state properties are plain values on this, with no function calls for reading or writing.

Solid
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal('');
const [items, setItems] = createStore([]);

// Read
console.log(count());
console.log(name());

// Write
setCount(count() + 1);
setName('Alice');
setItems(produce(s => s.push(item)));
WildflowerJS
wildflower.component('example', {
    state: { count: 0, name: '', items: [] },

    someMethod() {
        // Read
        console.log(this.count);
        console.log(this.name);

        // Write
        this.count++;
        this.name = 'Alice';
        this.items.push(item);
    }
});

Derived Values: createMemo → Computed

Both are cached and only re-evaluate when dependencies change. The difference: Solid requires you to call the memo as a function; WildflowerJS resolves computed properties like any other property.

Solid
const [price, setPrice] = createSignal(10);
const [quantity, setQuantity] = createSignal(2);

const total = createMemo(() =>
    price() * quantity());
const formatted = createMemo(() =>
    `$${total().toFixed(2)}`);

// Access: total(), formatted()
WildflowerJS
wildflower.component('invoice', {
    state: { price: 10, quantity: 2 },
    computed: {
        total() {
            return this.price * this.quantity;
        },
        formatted() {
            return `$${this.total.toFixed(2)}`;
        }
    }
});
// Access: this.total, this.formatted
// Or in HTML: data-bind="total", data-bind="formatted"

Effects: createEffect → init Lifecycle

Solid's createEffect runs whenever its tracked signals change. WildflowerJS handles most of those cases automatically through bindings. For one-time setup logic, use the init() lifecycle hook.

Solid
const [count, setCount] = createSignal(0);

// Runs on mount + whenever count changes
createEffect(() => {
    console.log('Count is now', count());
});

// One-time setup
onMount(() => {
    fetchData();
});

// Cleanup
onCleanup(() => {
    clearInterval(timer);
});
WildflowerJS
wildflower.component('example', {
    state: { count: 0 },

    // One-time setup on mount
    init() {
        this.fetchData();
    },

    // Cleanup on unmount
    destroy() {
        clearInterval(this.timer);
    }

    // Reactivity is handled by bindings:
    // data-bind="count" updates automatically
    // No explicit effect needed
});
Tip: Most Solid createEffect calls exist to keep the DOM in sync with state. In WildflowerJS, data-bind, data-show, and data-list handle that automatically. You only need init() for truly imperative setup like starting timers, fetching data, or integrating third-party libraries.

Event Handling: onClick → data-action

Solid uses JSX event attributes with inline functions. WildflowerJS uses data-action to call named methods.

Solid
// Click (default)
<button onClick={handleClick}>Go</button>

// With arguments
<button onClick={() => remove(id)}>×</button>

// Other events
<input onInput={e => setName(e.target.value)} />
<form onSubmit={e => {
    e.preventDefault();
    save();
}}>
WildflowerJS
<!-- Click (default event) -->
<button data-action="handleClick">Go</button>

<!-- In a list, details.index is available -->
<button data-action="remove">×</button>

<!-- Other events: prefix with event name -->
<input data-action="input:handleInput" />

<!-- Submit with prevent default -->
<form data-action="submit.prevent:save">

Conditional Rendering: Show → data-show / data-render

Solid's <Show> component conditionally renders children. WildflowerJS offers two attributes: data-show (CSS display toggle, keeps element in DOM) and data-render (adds/removes element from DOM).

Solid
// Conditional render
<Show when={isLoggedIn()}>
    <Dashboard />
</Show>

// With fallback
<Show when={isLoggedIn()}
      fallback={<Login />}>
    <Dashboard />
</Show>

// Multi-branch
<Switch>
    <Match when={status() === 'loading'}>
        <Spinner />
    </Match>
    <Match when={status() === 'error'}>
        <Error />
    </Match>
    <Match when={status() === 'ready'}>
        <Content />
    </Match>
</Switch>
WildflowerJS
<!-- CSS toggle (stays in DOM) -->
<div data-show="isLoggedIn">
    <div data-component="dashboard">...</div>
</div>

<!-- DOM insertion/removal -->
<div data-render="isLoggedIn">
    <div data-component="dashboard">...</div>
</div>
<div data-render="!isLoggedIn">
    <div data-component="login">...</div>
</div>

<!-- Multi-branch with data-show -->
<div data-show="status == 'loading'">
    <div data-component="spinner">...</div>
</div>
<div data-show="status == 'error'">
    <div data-component="error-view">...</div>
</div>
<div data-show="status == 'ready'">
    <div data-component="content-view">...</div>
</div>

List Rendering: For → data-list

Solid's <For> iterates with a callback. WildflowerJS uses data-list with a <template> child that defines the item markup. Inside the template, data-bind resolves against each item.

Solid
<For each={todos()}>
    {(todo, index) => (
        <li>
            <span>{todo.text}</span>
            <button onClick={() =>
                removeTodo(index())}>
                ×
            </button>
        </li>
    )}
</For>
WildflowerJS
<ul data-list="todos" data-key="id">
    <template>
        <li>
            <span data-bind="text"></span>
            <button data-action="removeTodo">
                ×
            </button>
        </li>
    </template>
</ul>
// In the component definition:
removeTodo(event, element, details) {
    // details.index = position in array
    // details.item = the todo object
    this.todos.splice(details.index, 1);
}
Index access: In Solid, you call index() as a function inside the callback. In WildflowerJS, action handlers inside lists receive details.index and details.item as their third argument. No tracking function needed.

Store / Context: createStore → wildflower.store

Solid's stores require produce() for deep mutations and createContext/useContext for sharing. WildflowerJS stores are globally named, support direct mutation, and components subscribe declaratively.

Solid
// Define store
const [state, setState] = createStore({
    user: { name: '', role: '' },
    items: []
});

// Update nested property
setState('user', 'name', 'Alice');

// Array mutation
setState(produce(s =>
    s.items.push({ id: 1, text: 'New' })));

// Share via context
const AppContext = createContext();
<AppContext.Provider value={[state, setState]}>
    {props.children}
</AppContext.Provider>

// Consume in child
const [state, setState] = useContext(AppContext);
WildflowerJS
// Define store (globally accessible by name)
wildflower.store('app', {
    state: {
        user: { name: '', role: '' },
        items: []
    },
    updateUser(name) {
        this.user.name = name;  // Direct mutation
    },
    addItem(item) {
        this.items.push(item);  // Direct mutation
    }
});

// Subscribe from any component
wildflower.component('profile', {
    subscribe: { app: ['user'] },
    init() {
        // this.stores.app.user is reactive
        console.log(this.stores.app.user.name);
    }
});

Forms: value + onInput → data-model

Solid requires manual wiring of value and event handler for two-way binding. WildflowerJS uses a single data-model attribute.

Solid
const [name, setName] = createSignal('');
const [agreed, setAgreed] = createSignal(false);

<input type="text"
       value={name()}
       onInput={e => setName(e.target.value)} />

<input type="checkbox"
       checked={agreed()}
       onChange={e => setAgreed(e.target.checked)} />

<select value={choice()}
        onChange={e => setChoice(e.target.value)}>
    <option value="a">A</option>
    <option value="b">B</option>
</select>
WildflowerJS
<input type="text" data-model="name" />

<input type="checkbox" data-model="agreed" />

<select data-model="choice">
    <option value="a">A</option>
    <option value="b">B</option>
</select>
wildflower.component('my-form', {
    state: {
        name: '',
        agreed: false,
        choice: 'a'
    }
});

Common Gotchas

No getter calls

Write this.count, not count(). Properties resolve automatically. There are no signal accessor functions in WildflowerJS. In templates, data-bind="count" handles it.

No setter functions

Write this.count = 5, not setCount(5). Direct assignment triggers the reactive system. There are no set* functions to import or call.

No produce()

Mutate arrays and objects directly: this.items.push(item), this.items.splice(index, 1), this.user.name = 'Alice'. No wrapper functions needed; the proxy-based reactivity system detects mutations at the property level.

No createEffect for init logic

Use the init() lifecycle hook for setup that runs once on mount. For ongoing reactive updates, bindings (data-bind, data-show, etc.) handle DOM synchronization automatically without explicit effects.

No JSX

Templates are plain HTML with data-* attributes. No compilation step, no transpiler, no build tooling required. The HTML is valid as-is and can be served from any static file host.

Reactivity is similar, that is a good thing

Both Solid and WildflowerJS track at the property level, so your mental model of when updates happen transfers directly. If you understood why createMemo only re-runs when its tracked signals change, you already understand how computed works in WildflowerJS.

Migration Checklist

A step-by-step plan for converting a Solid application to WildflowerJS:

  1. Remove JSX build tooling. Babel, Vite Solid plugin, or other JSX transpilers are no longer needed. WildflowerJS requires no build step.
  2. Convert JSX to HTML with data-* attributes. Replace JSX expressions ({count()}) with data-bind="count". Replace event handlers (onClick) with data-action. Replace <Show>/<For> with data-show/data-list.
  3. Replace createSignal pairs with state properties. Gather all signals into a single state: {} object. Remove getter/setter destructuring.
  4. Replace createMemo with computed properties. Move memo functions into a computed: {} block. Remove the getter-call syntax (access as this.prop, not prop()).
  5. Replace createEffect with init() / destroy() lifecycle. One-time setup goes in init(). Cleanup goes in destroy(). Reactive DOM updates are handled by bindings automatically.
  6. Convert createStore/produce to wildflower.store() with direct mutation. Remove all produce() wrappers. Replace setState(path, value) with direct property assignment.
  7. Replace <Show>/<For>/<Switch> with data-show/data-list/data-render. Use <template> children for list items. Use multiple data-show or data-render elements for conditional branches.
  8. Remove Solid Router, add RouteManager if needed. WildflowerJS includes a built-in RouteManager for SPA routing. Replace <Router>/<Route> with data-route configuration.
  9. Test each component. Verify that state updates, computed properties, list rendering, and conditional rendering all work as expected. The fine-grained reactivity model is the same, so behavior should match.
The payoff: After migration, you have zero build tooling, zero compilation, and the same fine-grained reactivity performance you valued in Solid, with simpler state access patterns and standard HTML templates that work in any browser, any editor, and any hosting environment.