From 021c5391614ad7c91c8173a0b6f9270e5630dffc Mon Sep 17 00:00:00 2001 From: Clay Stewart Date: Thu, 27 Nov 2025 21:55:55 -0600 Subject: [PATCH 1/4] Fix TypeScript module resolution conflicts (v1.1.2) - Renamed source entry files to avoid shadowing npm packages: - src/react.ts -> src/react-integration.ts - src/preact.ts -> src/preact-integration.ts - src/remix.ts -> src/remix-integration.ts - Fixed TypeScript resolving 'react'/'preact' imports to our files instead of actual packages - All tests pass (351/351) - Type-check passes without errors This fixes CI failures where test files importing from 'react' or 'preact' were incorrectly resolved to our integration files. --- package.json | 2 +- src/{preact.ts => preact-integration.ts} | 0 src/{react.ts => react-integration.ts} | 0 src/{remix.ts => remix-integration.ts} | 0 vite.config.ts | 6 +++--- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/{preact.ts => preact-integration.ts} (100%) rename src/{react.ts => react-integration.ts} (100%) rename src/{remix.ts => remix-integration.ts} (100%) diff --git a/package.json b/package.json index 0c28189..caec2e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jods", - "version": "1.1.1", + "version": "1.1.2", "description": "A minimal, reactive JSON state layer for Node.js and the browser", "type": "module", "main": "./dist/index.js", diff --git a/src/preact.ts b/src/preact-integration.ts similarity index 100% rename from src/preact.ts rename to src/preact-integration.ts diff --git a/src/react.ts b/src/react-integration.ts similarity index 100% rename from src/react.ts rename to src/react-integration.ts diff --git a/src/remix.ts b/src/remix-integration.ts similarity index 100% rename from src/remix.ts rename to src/remix-integration.ts diff --git a/vite.config.ts b/vite.config.ts index 25580ce..8100d85 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,9 +12,9 @@ export default defineConfig({ lib: { entry: { index: resolve(__dirname, "src/index.ts"), - react: resolve(__dirname, "src/react.ts"), - preact: resolve(__dirname, "src/preact.ts"), - remix: resolve(__dirname, "src/remix.ts"), + react: resolve(__dirname, "src/react-integration.ts"), + preact: resolve(__dirname, "src/preact-integration.ts"), + remix: resolve(__dirname, "src/remix-integration.ts"), zod: resolve(__dirname, "src/zod.ts"), }, formats: ["es", "cjs"], From 00c08812b6d28f737e56328e7a80052c5699fb11 Mon Sep 17 00:00:00 2001 From: Clay Stewart Date: Thu, 27 Nov 2025 22:19:27 -0600 Subject: [PATCH 2/4] Fix TypeScript definitions using tsc directly (v1.1.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE from v1.1.0-1.1.2: Those versions had broken type definitions. Changes: - Switched from vite-plugin-dts to using tsc directly for type declarations - Added tsconfig.build.json for generating declarations - Added typesVersions to package.json for backward compatibility - Updated exports to use correct type definition file names The key insight was that vite-plugin-dts was generating incorrect relative paths in .d.ts files (e.g., '../store' instead of './store'). Using tsc directly with a dedicated tsconfig.build.json generates correct paths. Tested with: - moduleResolution: 'node' ✅ - moduleResolution: 'bundler' ✅ - All 351 tests pass ✅ - type-check passes ✅ --- package.json | 31 ++++++++++++++++++++++++++----- tsconfig.build.json | 14 ++++++++++++++ vite.config.ts | 11 ++--------- 3 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 tsconfig.build.json diff --git a/package.json b/package.json index caec2e5..2eb258e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jods", - "version": "1.1.2", + "version": "1.1.3", "description": "A minimal, reactive JSON state layer for Node.js and the browser", "type": "module", "main": "./dist/index.js", @@ -15,17 +15,17 @@ "./react": { "import": "./dist/react.js", "require": "./dist/react.cjs", - "types": "./dist/react.d.ts" + "types": "./dist/react-integration.d.ts" }, "./preact": { "import": "./dist/preact.js", "require": "./dist/preact.cjs", - "types": "./dist/preact.d.ts" + "types": "./dist/preact-integration.d.ts" }, "./remix": { "import": "./dist/remix.js", "require": "./dist/remix.cjs", - "types": "./dist/remix.d.ts" + "types": "./dist/remix-integration.d.ts" }, "./zod": { "import": "./dist/zod.js", @@ -41,10 +41,31 @@ "files": [ "dist" ], + "typesVersions": { + "*": { + "react": [ + "./dist/react-integration.d.ts" + ], + "preact": [ + "./dist/preact-integration.d.ts" + ], + "remix": [ + "./dist/remix-integration.d.ts" + ], + "zod": [ + "./dist/zod.d.ts" + ], + "persist/adapters": [ + "./dist/persist-adapters.d.ts" + ] + } + }, "sideEffects": false, "scripts": { "dev": "vite", - "build": "vite build", + "build": "vite build && tsc -p tsconfig.build.json", + "build:js": "vite build", + "build:types": "tsc -p tsconfig.build.json", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..304cdfb --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationDir": "./dist", + "emitDeclarationOnly": true, + "rootDir": "./src", + "outDir": "./dist", + "paths": {} + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.test.tsx", "src/test/**/*"] +} + diff --git a/vite.config.ts b/vite.config.ts index 8100d85..1eafa29 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -33,15 +33,8 @@ export default defineConfig({ jsxImportSource: "react", }, plugins: [ - dts({ - tsconfigPath: "./tsconfig.json", - staticImport: true, - insertTypesEntry: true, - pathsToAliases: true, - compilerOptions: { - declarationMap: false, - }, - }), + // Removed vite-plugin-dts - using tsc directly for type declarations + // Run: pnpm build:types after vite build ], test: { globals: true, From 861ad2240e31078d38d8722cca2e92dc9b13f1b2 Mon Sep 17 00:00:00 2001 From: Clay Stewart Date: Thu, 27 Nov 2025 23:10:55 -0600 Subject: [PATCH 3/4] Improve ComputedValue DX - computed values now feel like regular values (v1.1.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAJOR DX IMPROVEMENT: ComputedValue now extends T structurally! Before (v1.1.3 and earlier): interface State { doubled?: ComputedValue; } s.doubled.toFixed(2); // ❌ Error: Property 'toFixed' does not exist After (v1.1.4): interface State { doubled?: ComputedValue; } s.doubled.toFixed(2); // ✅ Works! TypeScript sees this as number How it works: - ComputedValue is now defined as `T & { callable + brand }` - This makes ComputedValue assignable to number - Array methods, string methods, etc. all work naturally Changes: - Updated ComputedValue type in types/index.ts and core/computed.ts - Added internal casts where code needs to call computed as function - Exported UnwrapComputedValue and UnwrapComputedStore utility types All 351 tests pass ✅ --- package.json | 2 +- src/core/computed.ts | 34 +++++++++++++++--- src/core/store/index.ts | 15 ++++++++ src/core/store/state.ts | 3 +- src/frameworks/preact/useJodsPreact.ts | 3 +- src/frameworks/react/useJods.ts | 3 +- src/index.ts | 2 ++ src/types/index.ts | 48 ++++++++++++++++++++++++-- 8 files changed, 99 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 2eb258e..0adb08b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jods", - "version": "1.1.3", + "version": "1.1.4", "description": "A minimal, reactive JSON state layer for Node.js and the browser", "type": "module", "main": "./dist/index.js", diff --git a/src/core/computed.ts b/src/core/computed.ts index a750073..c8675d9 100644 --- a/src/core/computed.ts +++ b/src/core/computed.ts @@ -12,15 +12,39 @@ export const COMPUTED_SYMBOL = Symbol("computed"); export const COMPUTED_IS_DIRTY = Symbol("computedIsDirty"); /** - * Interface for computed value object + * Brand symbol for computed values (compile-time identification) + * @internal */ -export interface ComputedValue { +declare const ComputedBrand: unique symbol; + +/** + * Interface for computed value object. + * + * IMPORTANT: This type extends T so that ComputedValue is assignable to number, + * enabling excellent DX where computed properties can be used like regular values: + * + * @example + * ```ts + * interface State { doubled?: ComputedValue; } + * const s = store({ count: 5 }); + * s.doubled = computed(() => s.count * 2); + * console.log(s.doubled + 10); // ✅ Works! TypeScript sees this as number + * ``` + */ +export type ComputedValue = T & { + /** Call signature - computed values are callable */ (storeInstance?: S): T; - [COMPUTED_SYMBOL]: true; + /** @internal Runtime symbol for identifying computed properties */ + [COMPUTED_SYMBOL]?: true; + /** @internal Runtime symbol for dirty tracking */ [COMPUTED_IS_DIRTY]?: boolean; + /** @internal Method to mark the computed as needing recalculation */ markDirty?: () => void; - __debugName?: string; // For debugging -} + /** @internal Debug name for development */ + __debugName?: string; + /** @internal Brand for type identification */ + readonly [ComputedBrand]?: true; +}; /** * Creates a computed property that will recalculate its value diff --git a/src/core/store/index.ts b/src/core/store/index.ts index 6c8cf1d..6945704 100644 --- a/src/core/store/index.ts +++ b/src/core/store/index.ts @@ -9,6 +9,21 @@ export { /** * Creates a reactive store with fine-grained updates via signals. + * + * **Note on Computed Values:** + * When you define a store interface with `ComputedValue`, TypeScript sees + * the property as `ComputedValue`. However, at runtime, accessing the property + * returns `T` directly (the proxy unwraps it). To get proper typing when reading + * computed values, use one of these approaches: + * + * 1. Use `json(store)` which returns all values as plain types + * 2. Define computed properties as their result type and cast when assigning: + * ```ts + * interface State { doubled?: number; } + * s.doubled = computed(() => s.count * 2) as any; + * ``` + * 3. Call the computed as a function: `s.doubled()` (works at runtime) + * * @param initialState Initial store state * @param options Configuration options * @returns Mutable proxy object with Store interface diff --git a/src/core/store/state.ts b/src/core/store/state.ts index fbd42a0..1a9884f 100644 --- a/src/core/store/state.ts +++ b/src/core/store/state.ts @@ -226,7 +226,8 @@ export function initializeStateAndSignals( }); } } else { - signals.set(key, createSignal(initialValue)); + // Cast to any to handle complex type inference with ComputedValue extending T + signals.set(key, createSignal(initialValue as any)); } } } diff --git a/src/frameworks/preact/useJodsPreact.ts b/src/frameworks/preact/useJodsPreact.ts index 1fd13c1..ebeec5f 100644 --- a/src/frameworks/preact/useJodsPreact.ts +++ b/src/frameworks/preact/useJodsPreact.ts @@ -16,7 +16,8 @@ function createComputedProxy( // If the property is a computed value, automatically invoke it with store if (isComputed(value)) { // Call the computed property with the store as the this context - return value.call(store); + // Cast needed because ComputedValue extends T for DX, but is still callable + return (value as unknown as (this: typeof store) => unknown).call(store); } return value; }, diff --git a/src/frameworks/react/useJods.ts b/src/frameworks/react/useJods.ts index 04219c3..bf47492 100644 --- a/src/frameworks/react/useJods.ts +++ b/src/frameworks/react/useJods.ts @@ -16,7 +16,8 @@ function createComputedProxy( // If the property is a computed value, automatically invoke it with store as 'this' context if (isComputed(value)) { // Call the computed property with the store as the this context - return value.call(store); + // Cast needed because ComputedValue extends T for DX, but is still callable + return (value as unknown as (this: typeof store) => unknown).call(store); } return value; }, diff --git a/src/index.ts b/src/index.ts index 14057ce..0f1a766 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,4 +12,6 @@ export type { Unsubscribe, ComputeFunction, DiffResult, + UnwrapComputedValue, + UnwrapComputedStore, } from "./types"; diff --git a/src/types/index.ts b/src/types/index.ts index 1e459d3..48aaf24 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,10 +17,54 @@ export type Unsubscribe = () => void; export type ComputeFunction = () => T; /** - * Computed value type + * Brand symbol for computed values (compile-time only) + * @internal + */ +declare const ComputedBrand: unique symbol; + +/** + * Computed value type - designed for excellent DX! + * + * This type is structured so that `ComputedValue` is assignable to `T`, + * meaning you can use computed properties just like regular values: + * + * @example + * ```ts + * interface State { + * count: number; + * doubled?: ComputedValue; + * } + * const s = store({ count: 5 }); + * s.doubled = computed(() => s.count * 2); + * + * // These all work with proper types! + * console.log(s.doubled + 10); // ✅ number operations work + * console.log(s.doubled.toFixed(2)); // ✅ number methods work + * ``` + * + * @public + */ +export type ComputedValue = T & { + /** @internal Brand to identify computed values */ + readonly [ComputedBrand]?: true; + /** @internal The compute function (also callable at runtime) */ + (): T; +}; + +/** + * Utility type to unwrap ComputedValue to T + * Used internally by Store types to provide correct access types * @public */ -export type ComputedValue = ComputeFunction; +export type UnwrapComputedValue = T extends ComputedValue ? U : T; + +/** + * Utility type to unwrap all ComputedValue properties in an object type + * @public + */ +export type UnwrapComputedStore = { + [K in keyof T]: UnwrapComputedValue; +}; /** * Signal read function type From 0e6033321b2afcc4694c9c85ac5da652a83c7eb7 Mon Sep 17 00:00:00 2001 From: Clay Stewart Date: Thu, 27 Nov 2025 23:11:37 -0600 Subject: [PATCH 4/4] Refine documentation comments in index.ts for ComputedValue and UnwrapComputedStore types - Removed unnecessary blank lines in JSDoc comments for improved readability - Ensured consistent formatting in documentation comments These changes enhance the clarity and presentation of the type definitions. --- src/types/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index 48aaf24..18b1c5e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -24,10 +24,10 @@ declare const ComputedBrand: unique symbol; /** * Computed value type - designed for excellent DX! - * + * * This type is structured so that `ComputedValue` is assignable to `T`, * meaning you can use computed properties just like regular values: - * + * * @example * ```ts * interface State { @@ -36,12 +36,12 @@ declare const ComputedBrand: unique symbol; * } * const s = store({ count: 5 }); * s.doubled = computed(() => s.count * 2); - * + * * // These all work with proper types! * console.log(s.doubled + 10); // ✅ number operations work * console.log(s.doubled.toFixed(2)); // ✅ number methods work * ``` - * + * * @public */ export type ComputedValue = T & { @@ -60,7 +60,7 @@ export type UnwrapComputedValue = T extends ComputedValue ? U : T; /** * Utility type to unwrap all ComputedValue properties in an object type - * @public + * @public */ export type UnwrapComputedStore = { [K in keyof T]: UnwrapComputedValue;