preact-alchemy
v1.0.0
Published
A tiny transform that turns `let` bindings into @preact/signals when you opt in with a "use alchemy" directive. It lets you write normal JS and get reactive updates without manual `.value` plumbing.
Readme
preact-alchemy
A tiny transform that turns let bindings into @preact/signals when you opt in with a "use alchemy" directive.
It lets you write normal JS and get reactive updates without manual .value plumbing.
Install
pnpm add preact-alchemyQuick start
Add the directive as the first statement inside a function body:
function counter() {
'use alchemy'
let count = 0
count += 1
return count
}This is rewritten to:
import { signal as __alchemy_signal } from '@preact/signals'
function counter() {
let count = __alchemy_signal(0)
count.value += 1
return count.value
}How it works
When a function body starts with the string literal directive "use alchemy", the transform:
- Rewrites
letdeclarations in that function into signals. - Rewrites reads/writes of those bindings to use
.value. - Projects object literal shorthands that reference reactive bindings.
- Injects
import { signal as __alchemy_signal } from "@preact/signals";when needed.
Scope rules
Only let declarations are restricted: let declarations inside loops or nested functions are
not converted into signals. Reads and writes of a reactive binding remain reactive anywhere it is
in scope, including inside loops and nested functions.
function demo() {
'use alchemy'
let count = 0 // becomes a signal
for (let i = 0; i < 2; i++) {
count++ // reactive read/write
let local = 0 // NOT converted
}
function inner() {
count++ // reactive read/write
let local = 0 // NOT converted
}
}Object literal projection
Shorthand properties referencing a reactive binding are rewritten so you don’t leak signals by accident:
return { count }- At the top level: becomes a getter so reads stay reactive.
- Inside loops/nested functions: becomes an eager value.
return {
get count() {
return count.value
},
}
// or
return { count: count.value }Destructuring
Destructuring let declarations are supported. Each extracted name becomes a signal:
let { a, b } = obj
// ->
let { a, b } = obj
a = __alchemy_signal(a)
b = __alchemy_signal(b)Vite plugin
Use the built-in Vite plugin for zero-config integration:
// vite.config.ts
import { defineConfig } from 'vite'
import preactAlchemy from 'preact-alchemy/vite'
export default defineConfig({
plugins: [preactAlchemy()],
})The plugin:
- Runs on
.js/.jsx/.mjs/.cjs/.ts/.tsx/.mts/.ctsfiles, after Vite compiles TypeScript to JavaScript. - Skips files in
node_modules. - Only transforms files that include the
"use alchemy"directive.
Programmatic API
import { transform } from 'preact-alchemy'
const result = transform(code, id)
console.log(result.code)
console.log(result.map) // Source map if changes were madetransform(code, id?)
code: string input source.id: optional file name for warnings and source maps.- Returns
{ code, map? }.
Limitations
- JavaScript + JSX only. TypeScript/Flow syntax is not supported and is skipped with a warning. TypeScript is supported only after compilation to JavaScript (for example, via the Vite plugin).
- Only
letdeclarations are converted to signals.constandvarare left untouched. - The directive must be the first statement in the function body.
Tips
- You can keep normal JS semantics and sprinkle reactivity only where needed.
- If you already use a
__alchemy_signalvariable, the import is auto-aliased.
License
MIT
