From 34fa3c201ccc80ad5723f0e0999116444b8eeec0 Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 14 Jun 2026 19:18:11 +0200 Subject: [PATCH 1/2] feat(core): add react-native framework adapter Extends the `Framework` union with 'react-native' and adds a `index.react-native.ts` adapter identical to the React one (`createSignal`, `batch`, `untrack`, `createId`) with only the `framework` constant changed. This is the signal-layer foundation for a future `@formisch/react-native` package; browser-specific overrides (`FieldElement`, `getElementInput`) are intentionally out of scope here. Refs: open-circle/formisch#117 --- .../core/src/framework/index.react-native.ts | 131 ++++++++++++++++++ packages/core/src/framework/index.ts | 1 + 2 files changed, 132 insertions(+) create mode 100644 packages/core/src/framework/index.react-native.ts diff --git a/packages/core/src/framework/index.react-native.ts b/packages/core/src/framework/index.react-native.ts new file mode 100644 index 00000000..a4bd69b1 --- /dev/null +++ b/packages/core/src/framework/index.react-native.ts @@ -0,0 +1,131 @@ +import type { Signal } from '../types/signal/index.ts'; +import type { Framework } from './index.ts'; + +/** + * The current framework being used. + */ +export const framework: Framework = 'react-native'; + +/** + * Creates a unique identifier string. + * + * @returns The unique identifier. + */ +// @__NO_SIDE_EFFECTS__ +export function createId(): string { + return Math.random().toString(36).slice(2); +} + +/** + * Listener tuple. + * + * Hint: The first element is the execute function, which notifies the listener + * about updates. The second element is the subscription set, which keeps track + * of where the listener is subscribed and is used to clean up subscriptions if + * they are no longer needed. + */ +export type Listener = [() => void, Set>]; + +/** + * The current listener being tracked. + */ +let listener: Listener | undefined; + +/** + * Sets the current listener being tracked. + * + * @param newListener The new listener to set. + */ +export function setListener(newListener: Listener | undefined): void { + listener = newListener; +} + +/** + * Subscribers collected during a batch. + */ +let batchSubscribers: Set | undefined; + +/** + * Creates a reactive signal with an initial value. + * + * @param value The initial value. + * + * @returns The created signal. + */ +// @__NO_SIDE_EFFECTS__ +export function createSignal(value: T): Signal { + const subscribers = new Set(); + return { + get value() { + if (listener) { + subscribers.add(listener); + listener[1].add(subscribers); + } + return value; + }, + set value(newValue: T) { + if (newValue !== value) { + value = newValue; + const localSubscribers: Listener[] = []; + for (const subscriber of subscribers) { + if (batchSubscribers) { + batchSubscribers.add(subscriber); + } else { + localSubscribers.push(subscriber); + } + subscriber[1].delete(subscribers); + } + subscribers.clear(); + for (const subscriber of localSubscribers) { + subscriber[0](); + } + } + }, + }; +} + +// Global batch depth counter +let batchDepth = 0; + +/** + * Batches multiple signal updates into a single update cycle. + * + * @param fn The function to execute in batch. + * + * @returns The return value of the function. + */ +export function batch(fn: () => T): T { + batchDepth++; + if (!batchSubscribers) { + batchSubscribers = new Set(); + } + try { + return fn(); + } finally { + batchDepth--; + if (batchDepth === 0) { + const subscribers = batchSubscribers; + batchSubscribers = undefined; + for (const subscriber of subscribers) { + subscriber[0](); + } + } + } +} + +/** + * Executes a function without tracking reactive dependencies. + * + * @param fn The function to execute without tracking. + * + * @returns The return value of the function. + */ +export function untrack(fn: () => T): T { + const prev = listener; + listener = undefined; + try { + return fn(); + } finally { + listener = prev; + } +} diff --git a/packages/core/src/framework/index.ts b/packages/core/src/framework/index.ts index 05a119c1..62a7291d 100644 --- a/packages/core/src/framework/index.ts +++ b/packages/core/src/framework/index.ts @@ -10,6 +10,7 @@ export type Framework = | 'preact' | 'qwik' | 'react' + | 'react-native' | 'solid' | 'svelte' | 'vue'; From d34ca7a308f399da080ec216753c5e529ea2f3dc Mon Sep 17 00:00:00 2001 From: Joaquin Terrasa Date: Sun, 14 Jun 2026 19:18:18 +0200 Subject: [PATCH 2/2] feat(core,methods): expose react-native build output Adds 'react-native' to the per-framework build matrix in both `packages/core/tsdown.config.ts` and `packages/methods/tsdown.config.ts`, and exposes `./react-native` in both packages' export maps. The `rewriteFrameworkImports` tsdown plugin rewires `@formisch/core` -> `@formisch/core/react-native` in the methods sources for the RN target, so no plugin work is required. Produces `dist/index.react-native.{js,d.ts}` in both packages. Refs: open-circle/formisch#117 --- packages/core/package.json | 4 ++++ packages/core/tsdown.config.ts | 11 ++++++++++- packages/methods/package.json | 4 ++++ packages/methods/tsdown.config.ts | 10 +++++++++- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index c43b6043..02a058f3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -50,6 +50,10 @@ "types": "./dist/index.react.d.ts", "default": "./dist/index.react.js" }, + "./react-native": { + "types": "./dist/index.react-native.d.ts", + "default": "./dist/index.react-native.js" + }, "./vue": { "types": "./dist/index.vue.d.ts", "default": "./dist/index.vue.js" diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index 113b6da9..d34f7839 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -4,7 +4,15 @@ import { join } from 'node:path'; import type { RolldownPluginOption } from 'rolldown'; import { defineConfig, type UserConfig, type UserConfigFn } from 'tsdown'; -type Framework = 'angular' | 'preact' | 'qwik' | 'react' | 'solid' | 'svelte' | 'vue'; +type Framework = + | 'angular' + | 'preact' + | 'qwik' + | 'react' + | 'react-native' + | 'solid' + | 'svelte' + | 'vue'; /** * Rolldown plugin to rewrite framework-specific imports. @@ -120,6 +128,7 @@ const config: (UserConfig | UserConfigFn)[] = [ defineFrameworkConfig('preact'), defineFrameworkConfig('qwik'), defineFrameworkConfig('react'), + defineFrameworkConfig('react-native'), defineFrameworkConfig('solid'), defineFrameworkConfig('svelte'), defineFrameworkConfig('vue'), diff --git a/packages/methods/package.json b/packages/methods/package.json index 22bf7c54..0219aa59 100644 --- a/packages/methods/package.json +++ b/packages/methods/package.json @@ -32,6 +32,10 @@ "types": "./dist/index.react.d.ts", "default": "./dist/index.react.js" }, + "./react-native": { + "types": "./dist/index.react-native.d.ts", + "default": "./dist/index.react-native.js" + }, "./solid": { "types": "./dist/index.solid.d.ts", "default": "./dist/index.solid.js" diff --git a/packages/methods/tsdown.config.ts b/packages/methods/tsdown.config.ts index 864907a3..41563405 100644 --- a/packages/methods/tsdown.config.ts +++ b/packages/methods/tsdown.config.ts @@ -4,7 +4,14 @@ import { join } from 'node:path'; import type { RolldownPluginOption } from 'rolldown'; import { defineConfig, type UserConfig, type UserConfigFn } from 'tsdown'; -type Framework = 'preact' | 'qwik' | 'react' | 'solid' | 'svelte' | 'vue'; +type Framework = + | 'preact' + | 'qwik' + | 'react' + | 'react-native' + | 'solid' + | 'svelte' + | 'vue'; /** * Rolldown plugin to rewrite framework-specific imports. @@ -116,6 +123,7 @@ const config: (UserConfig | UserConfigFn)[] = [ defineFrameworkConfig('preact'), defineFrameworkConfig('qwik'), defineFrameworkConfig('react'), + defineFrameworkConfig('react-native'), defineFrameworkConfig('solid'), defineFrameworkConfig('svelte'), defineFrameworkConfig('vue'),