Skip to content

aleclarson/preact-alchemy

Repository files navigation

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-alchemy

Quick 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 let declarations 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

Our Vite plugin should work with Vite, Rolldown, and Rollup. You don't need Vite installed to use it.

// 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/.cts files, after Vite compiles TypeScript to JavaScript.
  • Skips files in node_modules.
  • Only transforms files that include the "use alchemy" directive.

Esbuild plugin

Use the built-in esbuild plugin when you want to integrate with esbuild or tools built on top of it:

pnpm add @preact-alchemy/esbuild-plugin
import { build } from 'esbuild'
import preactAlchemy from '@preact-alchemy/esbuild-plugin'

await build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  plugins: [preactAlchemy()],
})

The plugin:

  • Runs after TypeScript/JSX are compiled to JavaScript (via onTransform).
  • Applies to .js/.jsx/.mjs/.cjs/.ts/.tsx/.mts/.cts files.
  • Skips files in node_modules by default.
  • Only transforms files that include the "use alchemy" directive.

Options

type PreactAlchemyEsbuildOptions = {
  include?: RegExp
  exclude?: RegExp
}
  • include: RegExp to opt into files (defaults to common JS/TS extensions).
  • exclude: RegExp to skip files (defaults to /node_modules/).

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 made

transform(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 let declarations are converted to signals. const and var are 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_signal variable, the import is auto-aliased.

License

MIT

About

Preact signals without .value

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors