Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
131 changes: 131 additions & 0 deletions packages/core/src/framework/index.react-native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { Signal } from '../types/signal/index.ts';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: React Native adapter duplicates the entire React adapter signal implementation. The two files differ only by the framework constant, creating a maintenance burden and drift risk for any future signal-layer changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/framework/index.react-native.ts, line 7:

<comment>React Native adapter duplicates the entire React adapter signal implementation. The two files differ only by the `framework` constant, creating a maintenance burden and drift risk for any future signal-layer changes.</comment>

<file context>
@@ -0,0 +1,131 @@
+/**
+ * The current framework being used.
+ */
+export const framework: Framework = 'react-native';
+
+/**
</file context>

import type { Framework } from './index.ts';

/**
* The current framework being used.
*/
export const framework: Framework = 'react-native';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this file is fully identically to index.react.ts we could try re-exporting the functions of index.react.ts so that we only have to maintain one implementation. Just a spontaneous idea we should at least test and consider. But this is not a blocker.

@espetro espetro Jun 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check it, nevertheless take into account that the development teams at react and react-native don't sync on their releases, and this could create issues in the future. For instance, the React team announced the v19 release in Dec. 2024 and it took the React Native team until Feb. 2025 to ship it. That's 3 months worth of potential bugs (now with AI-assisted development it'd be much less of course, but it still implies some risk)


/**
* 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<Set<Listener>>];

/**
* 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<Listener> | undefined;

/**
* Creates a reactive signal with an initial value.
*
* @param value The initial value.
*
* @returns The created signal.
*/
// @__NO_SIDE_EFFECTS__
export function createSignal<T>(value: T): Signal<T> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify createSignal signature consistency across framework entrypoints and call sites.
fd -i 'index*.ts' packages/core/src/framework
rg -nP --type=ts '^export function createSignal<.*>\(\): Signal<.*>;' packages/core/src/framework -C2
rg -nP --type=ts '^export function createSignal<.*>\(value: .*' packages/core/src/framework -C2
rg -nP --type=ts '\bcreateSignal\s*(<[^>]+>)?\s*\(\s*\)' packages -C2

Repository: open-circle/formisch

Length of output: 2059


createSignal is missing the zero-arg overload from the base framework API.

The base contract in packages/core/src/framework/index.ts declares two overloads for createSignal—one without arguments and one with an initial value. At line 56 in the react-native adapter, only the required-argument overload is exposed, creating an API mismatch. The same issue exists in index.react.ts.

Suggested fix
 // `@__NO_SIDE_EFFECTS__`
+export function createSignal<T>(): Signal<T | undefined>;
 export function createSignal<T>(value: T): Signal<T>;
-export function createSignal<T>(value: T): Signal<T | undefined> {
+export function createSignal<T>(value?: T): Signal<T | undefined> {
   const subscribers = new Set<Listener>();
   return {
     get value() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/framework/index.react-native.ts` at line 56, The
createSignal function is missing the zero-argument overload that exists in the
base framework contract. In both index.react-native.ts and index.react.ts, add
an overload declaration for createSignal that accepts no parameters and returns
Signal<T>, in addition to the existing overload that takes an initial value
parameter. This ensures both adapters match the complete API contract defined in
the base framework, allowing users to call createSignal() with no arguments as
well as with an initial value.

const subscribers = new Set<Listener>();
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<T>(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<T>(fn: () => T): T {
const prev = listener;
listener = undefined;
try {
return fn();
} finally {
listener = prev;
}
}
1 change: 1 addition & 0 deletions packages/core/src/framework/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type Framework =
| 'preact'
| 'qwik'
| 'react'
| 'react-native'
| 'solid'
| 'svelte'
| 'vue';
Expand Down
11 changes: 10 additions & 1 deletion packages/core/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -120,6 +128,7 @@ const config: (UserConfig | UserConfigFn)[] = [
defineFrameworkConfig('preact'),
defineFrameworkConfig('qwik'),
defineFrameworkConfig('react'),
defineFrameworkConfig('react-native'),
defineFrameworkConfig('solid'),
defineFrameworkConfig('svelte'),
defineFrameworkConfig('vue'),
Expand Down
4 changes: 4 additions & 0 deletions packages/methods/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion packages/methods/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -116,6 +123,7 @@ const config: (UserConfig | UserConfigFn)[] = [
defineFrameworkConfig('preact'),
defineFrameworkConfig('qwik'),
defineFrameworkConfig('react'),
defineFrameworkConfig('react-native'),
defineFrameworkConfig('solid'),
defineFrameworkConfig('svelte'),
defineFrameworkConfig('vue'),
Expand Down