From bc0d6bbdc86e8f9494c596bcd157ccb1db78a178 Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Tue, 19 May 2026 22:38:56 -0400 Subject: [PATCH 1/4] Add Angular framework adapter to core Wires Angular signals (WritableSignal, untracked) to the Formisch framework interface. Adds @angular/core as optional peer dep and exports @formisch/core/angular. Closes #103 --- packages/core/package.json | 9 +++ packages/core/src/framework/index.angular.ts | 70 ++++++++++++++++++++ packages/core/src/framework/index.ts | 1 + pnpm-lock.yaml | 21 ++++++ 4 files changed, 101 insertions(+) create mode 100644 packages/core/src/framework/index.angular.ts diff --git a/packages/core/package.json b/packages/core/package.json index 530c72b2..8a18ccfd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -26,6 +26,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./angular": { + "types": "./dist/index.angular.d.ts", + "default": "./dist/index.angular.js" + }, "./preact": { "types": "./dist/index.preact.d.ts", "default": "./dist/index.preact.js" @@ -68,6 +72,7 @@ "build": "tsdown" }, "devDependencies": { + "@angular/core": "^21.0.0", "@formisch/eslint-config": "workspace:*", "@preact/signals": "^2.2.1", "@qwik.dev/core": "2.0.0-beta.5", @@ -86,6 +91,7 @@ "vue": "^3.5.17" }, "peerDependencies": { + "@angular/core": "^17.0.0", "@qwik.dev/core": "^2.0.0", "solid-js": "^1.6.0", "svelte": "^5.29.0", @@ -94,6 +100,9 @@ "vue": "^3.3.0" }, "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, "@preact/signals": { "optional": true }, diff --git a/packages/core/src/framework/index.angular.ts b/packages/core/src/framework/index.angular.ts new file mode 100644 index 00000000..d20af72a --- /dev/null +++ b/packages/core/src/framework/index.angular.ts @@ -0,0 +1,70 @@ +import { signal, untracked } from '@angular/core'; +import type { Signal } from '../types/signal.ts'; +import type { Framework } from './index.ts'; + +/** + * The current framework being used. + */ +export const framework: Framework = 'angular'; + +/** + * Creates a unique identifier string. + * + * @returns The unique identifier. + */ +// @__NO_SIDE_EFFECTS__ +export function createId(): string { + return Math.random().toString(36).slice(2); +} + +/** + * Creates a reactive signal without an initial value. + * + * @returns The created signal. + */ +export function createSignal(): Signal; + +/** + * Creates a reactive signal with an initial value. + * + * @param value The initial value. + * + * @returns The created signal. + */ +export function createSignal(value: T): Signal; + +// @__NO_SIDE_EFFECTS__ +export function createSignal(initialValue?: T): Signal { + const writableSignal = signal(initialValue); + return { + get value() { + return writableSignal(); + }, + set value(nextValue) { + writableSignal.set(nextValue); + }, + }; +} + +/** + * Batches multiple signal updates into a single update cycle. This is a + * no-op in Angular as batching is handled automatically. + * + * @param fn The function to execute. + * + * @returns The return value of the function. + */ +export function batch(fn: () => T): T { + return fn(); +} + +/** + * Executes a function without tracking reactive dependencies. + * + * @param fn The function to execute. + * + * @returns The return value of the function. + */ +export function untrack(fn: () => T): T { + return untracked(fn); +} diff --git a/packages/core/src/framework/index.ts b/packages/core/src/framework/index.ts index 92ca0daa..05a119c1 100644 --- a/packages/core/src/framework/index.ts +++ b/packages/core/src/framework/index.ts @@ -6,6 +6,7 @@ import type { Signal } from '../types/index.ts'; * Framework type. */ export type Framework = + | 'angular' | 'preact' | 'qwik' | 'react' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bcb4a392..e8dd7142 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -395,6 +395,9 @@ importers: packages/core: devDependencies: + '@angular/core': + specifier: ^21.0.0 + version: 21.2.13(rxjs@7.8.2) '@formisch/eslint-config': specifier: workspace:* version: link:../eslint-config @@ -976,6 +979,19 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@angular/core@21.2.13': + resolution: {integrity: sha512-23tS4oNL8nvkHcI4l9rbruQs2WS4yqQmBVQxWakqS9cmRpArLGgveR+hKNU5tPXm5EAi8oLO34/Zy7z70jUpCg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@angular/compiler': 21.2.13 + rxjs: ^6.5.3 || ^7.4.0 + zone.js: ~0.15.0 || ~0.16.0 + peerDependenciesMeta: + '@angular/compiler': + optional: true + zone.js: + optional: true + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -10638,6 +10654,11 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@angular/core@21.2.13(rxjs@7.8.2)': + dependencies: + rxjs: 7.8.2 + tslib: 2.8.1 + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) From 28f7d7252a09f39070b3701d466e4c287f95ab94 Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Tue, 19 May 2026 22:50:08 -0400 Subject: [PATCH 2/4] Address Copilot review comments - Add angular build entry to tsdown.config.ts so dist/index.angular.* is emitted and @angular/core is treated as external - Widen peer dep range from ^17.0.0 to >=17.0.0 to cover Angular 18+ - Explicitly type the createSignal setter parameter as T | undefined - Fix Signal import path after upstream type reorganization --- packages/core/package.json | 2 +- packages/core/src/framework/index.angular.ts | 4 ++-- packages/core/tsdown.config.ts | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 8a18ccfd..1d44bc1d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -91,7 +91,7 @@ "vue": "^3.5.17" }, "peerDependencies": { - "@angular/core": "^17.0.0", + "@angular/core": ">=17.0.0", "@qwik.dev/core": "^2.0.0", "solid-js": "^1.6.0", "svelte": "^5.29.0", diff --git a/packages/core/src/framework/index.angular.ts b/packages/core/src/framework/index.angular.ts index d20af72a..067c92fd 100644 --- a/packages/core/src/framework/index.angular.ts +++ b/packages/core/src/framework/index.angular.ts @@ -1,5 +1,5 @@ import { signal, untracked } from '@angular/core'; -import type { Signal } from '../types/signal.ts'; +import type { Signal } from '../types/signal/index.ts'; import type { Framework } from './index.ts'; /** @@ -40,7 +40,7 @@ export function createSignal(initialValue?: T): Signal { get value() { return writableSignal(); }, - set value(nextValue) { + set value(nextValue: T | undefined) { writableSignal.set(nextValue); }, }; diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index e323f2ce..113b6da9 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -4,7 +4,7 @@ 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 = 'angular' | 'preact' | 'qwik' | 'react' | 'solid' | 'svelte' | 'vue'; /** * Rolldown plugin to rewrite framework-specific imports. @@ -85,6 +85,7 @@ function defineFrameworkConfig( return defineConfig({ entry: ['./src/index.ts'], external: [ + '@angular/core', '@preact/signals', '@qwik.dev/core', 'solid-js', @@ -115,6 +116,7 @@ function defineFrameworkConfig( const config: (UserConfig | UserConfigFn)[] = [ defineFrameworkConfig(null), + defineFrameworkConfig('angular'), defineFrameworkConfig('preact'), defineFrameworkConfig('qwik'), defineFrameworkConfig('react'), From 4f8ae5d3737c687775ab1975f1a254a7288ca250 Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Tue, 19 May 2026 22:57:26 -0400 Subject: [PATCH 3/4] Fix createSignal overload compatibility Use non-generic implementation signature to align with the pattern in index.ts and avoid overload-compatibility errors. --- packages/core/src/framework/index.angular.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/framework/index.angular.ts b/packages/core/src/framework/index.angular.ts index 067c92fd..2f1f8ac1 100644 --- a/packages/core/src/framework/index.angular.ts +++ b/packages/core/src/framework/index.angular.ts @@ -34,13 +34,13 @@ export function createSignal(): Signal; export function createSignal(value: T): Signal; // @__NO_SIDE_EFFECTS__ -export function createSignal(initialValue?: T): Signal { +export function createSignal(initialValue?: unknown): Signal { const writableSignal = signal(initialValue); return { get value() { return writableSignal(); }, - set value(nextValue: T | undefined) { + set value(nextValue: unknown) { writableSignal.set(nextValue); }, }; From 5853918141f6e904b9f3fb591a12d97fce4abb6f Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Thu, 21 May 2026 16:36:29 -0400 Subject: [PATCH 4/4] Remove no-arg createSignal overload and re-export untrack directly Aligns with the SolidJS adapter pattern: createSignal takes a required initial value, and untrack is a direct re-export rather than a wrapper. --- packages/core/src/framework/index.angular.ts | 30 ++++---------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/packages/core/src/framework/index.angular.ts b/packages/core/src/framework/index.angular.ts index 2f1f8ac1..8b1b81f0 100644 --- a/packages/core/src/framework/index.angular.ts +++ b/packages/core/src/framework/index.angular.ts @@ -1,7 +1,9 @@ -import { signal, untracked } from '@angular/core'; +import { signal } from '@angular/core'; import type { Signal } from '../types/signal/index.ts'; import type { Framework } from './index.ts'; +export { untracked as untrack } from '@angular/core'; + /** * The current framework being used. */ @@ -17,30 +19,21 @@ export function createId(): string { return Math.random().toString(36).slice(2); } -/** - * Creates a reactive signal without an initial value. - * - * @returns The created signal. - */ -export function createSignal(): Signal; - /** * Creates a reactive signal with an initial value. * - * @param value The initial value. + * @param initialValue The initial value. * * @returns The created signal. */ -export function createSignal(value: T): Signal; - // @__NO_SIDE_EFFECTS__ -export function createSignal(initialValue?: unknown): Signal { +export function createSignal(initialValue: T): Signal { const writableSignal = signal(initialValue); return { get value() { return writableSignal(); }, - set value(nextValue: unknown) { + set value(nextValue: T) { writableSignal.set(nextValue); }, }; @@ -57,14 +50,3 @@ export function createSignal(initialValue?: unknown): Signal { export function batch(fn: () => T): T { return fn(); } - -/** - * Executes a function without tracking reactive dependencies. - * - * @param fn The function to execute. - * - * @returns The return value of the function. - */ -export function untrack(fn: () => T): T { - return untracked(fn); -}