Migrating a pota app across versions

SkillDev tools

Migrate a pota app (code that uses pota) across pota's breaking changes, update an app when bumping the pota dependency or when it calls a removed, renamed, or changed API. Use when upgrading pota, fixing errors after a pota bump, or modernizing old pota usage (signal tuples, use:* behavior plugins, @static, the pota/web→pota/components renames, attribute-first props). When a migration here is unclear or missing, the installed pota source in node_modules/pota is the ground truth.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Migrating a pota app across versions skill

What this skill tells your AI

The instructions your AI receives, as published by potahtml/pota in .claude/skills/breaking-changes/SKILL.md and read by ahel’s review.

Use this when updating code that uses pota — bumping the dependency, or fixing an app that calls an API pota removed, renamed, or changed. It collects the migrations you're most likely to hit; when something here is unclear or missing, the installed pota source is the ground truth (see the end).

Scope it: find the pota version the app is on and the one it's moving to, then apply every change below tagged with a version in that range. The big, every-app migrations follow.

createPartial — inlined DOM walk (0.22.236)

The compiled JSX output navigates the cloned template directly (root.firstChild.nextSibling…) instead of walking it at runtime, so createPartial no longer takes the props-index → walk-index map. The preset and runtime ship together — recompiling the app is the whole migration. Only code calling createPartial directly changes: the second argument keeps only x (xmlns) and i (importNode), and the partial takes one function receiving the cloned root (or the DocumentFragment for multi-node content) instead of an array of per-element functions.

// before
const partial = createPartial('<p></p><span></span>', { 0: 1, m: 2 })
partial([node => { /* <span> */ }])

// after
const partial = createPartial('<p></p><span></span>')
partial(node => {
	const span = node.firstChild.nextSibling
	/* … */
})

Component props are frozen only in dev mode (0.22.236)

Object.freeze(props) on component creation is now a dev-only guard. In production, writing to props no longer throws — enable dev mode to keep the check while developing:

// once, before rendering — e.g. the app entry
import 'pota/dev'

Bundlers that select pota/jsx-dev-runtime for development builds (jsx: 'automatic' with development: true — Vite dev serve does) enable it automatically; their production builds import pota/jsx-runtime and stay on the fast path. Correct code needs no migration — only code relying on the production TypeError when mutating props behaves differently.

Babel preset requires Babel 8 (0.22.235)

pota/babel-preset now peer-depends on Babel 8 (@babel/* ^8.0.0). Babel 8 is ESM-only and requires Node ^22.18.0 || >=24.11.0. This is a toolchain bump only — the JSX → partials output is unchanged, so no app code changes. Bump the Babel packages the app provides for the preset:

// before — package.json
"@babel/core": "^7",
"@babel/helper-module-imports": "^7",
"@babel/helper-plugin-utils": "^7",
"@babel/plugin-syntax-jsx": "^7"

// after
"@babel/core": "^8",
"@babel/helper-module-imports": "^8",
"@babel/helper-plugin-utils": "^8",
"@babel/plugin-syntax-jsx": "^8"
  • There's no dual-support build: the preset uses Babel 8 APIs, so Babel 7 will no longer load it.
  • On Node older than 22.18, upgrade Node first — Babel 8 won't install otherwise.

Signals — tuple form removed (0.21.234)

signal() returns a single object, not a destructurable tuple.

// before
const [read, write, update] = signal(0)

// after
const count = signal(0)
count.read()
count.write(1)
count.update(n => n + 1)

Types: SignalObject<T>Signal<T>; SignalTuple<T> is gone.

use:* behavior plugins → use:ref factories (0.21.234)

The built-in use:clickoutside, use:clipboard, use:fullscreen, use:prevent-enter, use:click-selects-all, use:click-focus-children-input, use:enter-focus-next, use:size-to-input, and use:clickoutsideonce directives are gone. Import the matching factory from pota/use/* and pass it through the single use:ref:

// before
<div use:clickoutside={handler}/>
<button use:clipboard="copy"/>
<input use:prevent-enter={true}/>

// after
import { clickOutside } from 'pota/use/clickoutside'
import { clipboard } from 'pota/use/clipboard'
import { preventEnter } from 'pota/use/form'

<div use:ref={clickOutside(handler)}/>
<button use:ref={clipboard('copy')}/>
<input use:ref={preventEnter}/>
  • use:clickoutsideonceclickOutside(handler, { once: true }).
  • Parameterized helpers (clickOutside, clipboard, fullscreen) are factories — call them. Parameterless ones (preventEnter, clickSelectsAll, enterFocusNext, …) are the ref directly.
  • Compose several with an array: use:ref={[clickOutside(h), preventEnter]}.
  • use:ref, use:connected, use:disconnected, use:css, and use:bind are unchanged.

@static JSX marker removed (0.21.234)

Just delete the comment — the expression compiles the same.

// before
<div prop:value={/* @static */ compute()} />
// after
<div prop:value={compute()} />

Removed / moved exports (0.21.234, 0.20.233)

  • Linkify is no longer re-exported from pota/components — import it from pota/components/Linkify.
  • propsPlugin / propsPluginNS are no longer exported; for per-element behavior use a use:ref factory from pota/use/*.
  • firewall removed from pota/store; validatePassword removed from pota/use/string (both were placeholders).
  • Derived.run() removed — store the dependency in a signal and write to that signal instead.

Boolean children no longer render as text (0.20.228)

<p>{true}</p> now renders nothing (matching React/Solid). So {cond && <X/>} renders nothing when cond is falsy instead of printing "false". To show a boolean as text, wrap it: {String(value)}.

The rename wave (0.18.184)

If the app predates 0.18.184, expect a broad sweep:

beforeafter
ref=use:ref=
css=use:css=
onMountuse:connected
onUnmountuse:disconnected
import … 'pota/web''pota/components'
import … 'pota/html''pota/xml'
html tagged templatexml
RouterRoute
location.querylocation.searchParams
pota/plugin / pota/plugin/useFoo.jspota/use / pota/use/foo.js
onLoaded (async fns)onLoad

Same release made DOM attribute-first, so several properties now need the prop: namespace:

  • innerHTMLprop:innerHTML
  • textContentprop:textContent
  • srcObjectprop:srcObject
  • indeterminateprop:indeterminate (on a checkbox)
  • <textarea value={v}/><textarea>{v}</textarea>

Behavior changes that may need no code edit

These changed runtime behavior rather than syntax — check assumptions instead of swapping calls:

  • Duplicate attributes — last wins: <div class="1" class="2"/> yields class="2" (0.19.211).
  • Spreads behave like JavaScript now — multiple spreads or a mix of attrs and spreads merge into one object, later keys win (0.19.207).
  • Collapse no longer renders a <pota-collapse> custom element; it wraps children in a plain <div>. Re-target any pota-collapse query (0.20.230).
  • Rejected promises in bare promise children, derived, and action chains now route to the nearest Errored / catchError boundary instead of becoming unhandled rejections (0.20.229, 0.20.228).

Workflow

Grep the app for the old pattern, replace per above, then run the app's type-check and tests.

When in doubt, read the installed source. The pota the app actually runs lives in node_modules/pota/, and it is the ground truth for that exact installed version — trust it over any summary, this skill included. Check there before assuming an API's shape:

  • Exact versionnode_modules/pota/package.json "version".
  • Real source — follow that file's "exports" map to the module (potasrc/exports.js, pota/componentssrc/components/@main.js, pota/storesrc/lib/store.js, …) and read the actual signature; types sit under typescript/ and generated/types/.

(Maintainers: how to add an entry to this skill when you ship a breaking change lives in .claude/rules/release.md, not in this consumer-facing body.)

Signals

GitHub stars
66
Forks
2
Last commit
Jul 2026
Advanced
Catalog kind
skill
Gateway key
breaking-changes-potahtml
Source
github.com/potahtml/pota