From b50f6c52906653e8b5ad4af60c405f56071b40a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 11:32:15 +0000 Subject: [PATCH 1/6] fix(endpoint): Make Entity.pk() args readonly to match EntityInterface Entity classes now satisfy normalizr's EntityInterface, so Controller drops its private EntityLike workaround. Endpoint's own EntityInterface also takes readonly args, so both contracts match. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01F92DVcdVpL6sVLSA69App5 --- .changeset/entity-pk-readonly-args.md | 30 +++++++++++++++++++ packages/core/src/controller/setManyTypes.ts | 19 +++--------- packages/endpoint/src/interface.ts | 2 +- packages/endpoint/src/schemas/Entity.ts | 2 +- packages/endpoint/src/schemas/EntityTypes.ts | 2 +- .../src/schemas/__tests__/Entity.test.ts | 9 ++++++ 6 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 .changeset/entity-pk-readonly-args.md diff --git a/.changeset/entity-pk-readonly-args.md b/.changeset/entity-pk-readonly-args.md new file mode 100644 index 000000000000..c5daf16234d9 --- /dev/null +++ b/.changeset/entity-pk-readonly-args.md @@ -0,0 +1,30 @@ +--- +'@data-client/endpoint': patch +'@data-client/rest': patch +'@data-client/graphql': patch +'@data-client/core': patch +'@data-client/react': patch +'@data-client/vue': patch +--- + +Fix Entity classes not assignable to `EntityInterface` + +[Entity.pk()](https://dataclient.io/rest/api/Entity#pk)'s static `args` parameter is now `readonly any[]`, matching `EntityInterface`. Entity classes can be passed where an `EntityInterface` is expected. + +```ts +import type { EntityInterface } from '@data-client/react'; + +// Before: TypeScript error (args is readonly in EntityInterface) +// After: typechecks +const schema: EntityInterface = User; +``` + +If you override `static pk()`, or implement `EntityInterface.pk`, and annotate `args` as a mutable array, make it `readonly`: + +```ts +class User extends Entity { + static pk(value: any, parent?: any, key?: string, args?: readonly any[]) { + return `${value.id}-${args?.[0]?.org}`; + } +} +``` diff --git a/packages/core/src/controller/setManyTypes.ts b/packages/core/src/controller/setManyTypes.ts index 3f7350654c10..58d8a3021658 100644 --- a/packages/core/src/controller/setManyTypes.ts +++ b/packages/core/src/controller/setManyTypes.ts @@ -1,16 +1,5 @@ /** Types for batch `Controller.set([Entity], rows)` */ -import type { Denormalize } from '@data-client/normalizr'; - -/** Matches Entity classes (same members Denormalize<> checks). - * Not EntityInterface: Entity's declared pk() takes mutable `args`, which fails its readonly `args`. */ -interface EntityLike { - createIfValid(...args: any): any; - pk(...args: any): any; - readonly key: string; - prototype: any; -} - -type EntityMapLike = { readonly [k: string]: EntityLike }; +import type { Denormalize, EntityInterface } from '@data-client/normalizr'; /** What one row normalizes to: a reference to one stored entity */ type EntityRef = string | { readonly id: string; readonly schema: string }; @@ -18,7 +7,7 @@ type EntityRef = string | { readonly id: string; readonly schema: string }; /** Schemas that write each row to one stored entity: Entity, Union, or Invalidate (batch delete). * Query, All and Collection don't: they normalize to lists, or Collection keys by args batch set() lacks. */ type SetEntitySchema = - | EntityLike + | EntityInterface | { _normalizeNullable(): EntityRef | undefined; // excludes Collection @@ -29,7 +18,7 @@ type SetEntitySchema = export type SetManySchema = | readonly SetEntitySchema[] | { - readonly schema: SetEntitySchema | EntityMapLike; + readonly schema: SetEntitySchema | Record; // Array and Values; excludes schema.Object, whose queryKey() returns any schemaKey(): string; queryKey(...args: any): undefined; @@ -65,7 +54,7 @@ type SetRow = /** Polymorphic rows may carry a discriminator that is not an Entity field */ type SetRowOf = - Sch extends EntityLike ? SetRow + Sch extends EntityInterface ? SetRow : SetRow & { readonly [k: string]: unknown }; export type SetManyValue = diff --git a/packages/endpoint/src/interface.ts b/packages/endpoint/src/interface.ts index 3eeef055feac..dbba22f2e168 100644 --- a/packages/endpoint/src/interface.ts +++ b/packages/endpoint/src/interface.ts @@ -61,7 +61,7 @@ export interface EntityInterface extends SchemaSimple { params: any, parent: any, key: string | undefined, - args: any[], + args: readonly any[], ): string | number | undefined; readonly key: string; indexes?: any; diff --git a/packages/endpoint/src/schemas/Entity.ts b/packages/endpoint/src/schemas/Entity.ts index 3337db9d9711..684529b7a0fb 100644 --- a/packages/endpoint/src/schemas/Entity.ts +++ b/packages/endpoint/src/schemas/Entity.ts @@ -50,7 +50,7 @@ export default abstract class Entity extends EntityMixin(EmptyBase) { value: Partial>, parent?: any, key?: string, - args?: any[], + args?: readonly any[], ) => string | number | undefined; /** Do any transformations when first receiving input diff --git a/packages/endpoint/src/schemas/EntityTypes.ts b/packages/endpoint/src/schemas/EntityTypes.ts index 89187468b3a3..7878680ccd93 100644 --- a/packages/endpoint/src/schemas/EntityTypes.ts +++ b/packages/endpoint/src/schemas/EntityTypes.ts @@ -61,7 +61,7 @@ export interface IEntityClass { value: Partial>, parent?: any, key?: string, - args?: any[], + args?: readonly any[], ): string | number | undefined; /** Return true to merge incoming data; false keeps existing entity * diff --git a/packages/endpoint/src/schemas/__tests__/Entity.test.ts b/packages/endpoint/src/schemas/__tests__/Entity.test.ts index 3e49e0a55ce2..aee7346569cb 100644 --- a/packages/endpoint/src/schemas/__tests__/Entity.test.ts +++ b/packages/endpoint/src/schemas/__tests__/Entity.test.ts @@ -1,4 +1,5 @@ import { normalize, INVALID } from '@data-client/normalizr'; +import type { EntityInterface } from '@data-client/normalizr'; import { denormalize as plainDenormalize } from '@data-client/normalizr'; import { denormalize as immDenormalize } from '@data-client/normalizr/imm'; import { IDEntity } from '__tests__/new'; @@ -455,6 +456,14 @@ describe(`${Entity.name} normalization`, () => { }), ).toMatchSnapshot(); }); + + test('Entity classes satisfy EntityInterface', () => { + class User extends Entity { + readonly id: string = ''; + } + const entity: EntityInterface = User; + expect(entity.pk({ id: '5' }, undefined, undefined, [])).toBe('5'); + }); }); describe('mergeStrategy', () => { From 71ee4acc34fa996dfaf58be318309826e1a7aae6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 16:23:58 +0000 Subject: [PATCH 2/6] docs(changeset): Only static pk() overrides need readonly args Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01F92DVcdVpL6sVLSA69App5 --- .changeset/entity-pk-readonly-args.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/entity-pk-readonly-args.md b/.changeset/entity-pk-readonly-args.md index c5daf16234d9..a5b757be2710 100644 --- a/.changeset/entity-pk-readonly-args.md +++ b/.changeset/entity-pk-readonly-args.md @@ -19,7 +19,7 @@ import type { EntityInterface } from '@data-client/react'; const schema: EntityInterface = User; ``` -If you override `static pk()`, or implement `EntityInterface.pk`, and annotate `args` as a mutable array, make it `readonly`: +If you override `static pk()` and annotate `args` as a mutable array, make it `readonly`: ```ts class User extends Entity { From 8598990a19b950ef261ca5f7e511400221846f90 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 16:40:41 +0000 Subject: [PATCH 3/6] fix(endpoint): Keep Entity.pk() readonly-args change non-breaking Declare static pk() with method syntax so subclass overrides that type args as a mutable array still compile. Revert endpoint EntityInterface and keep core's loose EntityLike so mixed package versions keep working. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01F92DVcdVpL6sVLSA69App5 --- .changeset/entity-pk-readonly-args.md | 13 ------------- packages/core/src/controller/setManyTypes.ts | 19 +++++++++++++++---- packages/endpoint/src/interface.ts | 2 +- packages/endpoint/src/schemas/Entity.ts | 17 ++++++++++------- .../src/schemas/__tests__/Entity.test.ts | 12 +++++++++++- 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/.changeset/entity-pk-readonly-args.md b/.changeset/entity-pk-readonly-args.md index a5b757be2710..4539a107a16a 100644 --- a/.changeset/entity-pk-readonly-args.md +++ b/.changeset/entity-pk-readonly-args.md @@ -2,9 +2,6 @@ '@data-client/endpoint': patch '@data-client/rest': patch '@data-client/graphql': patch -'@data-client/core': patch -'@data-client/react': patch -'@data-client/vue': patch --- Fix Entity classes not assignable to `EntityInterface` @@ -18,13 +15,3 @@ import type { EntityInterface } from '@data-client/react'; // After: typechecks const schema: EntityInterface = User; ``` - -If you override `static pk()` and annotate `args` as a mutable array, make it `readonly`: - -```ts -class User extends Entity { - static pk(value: any, parent?: any, key?: string, args?: readonly any[]) { - return `${value.id}-${args?.[0]?.org}`; - } -} -``` diff --git a/packages/core/src/controller/setManyTypes.ts b/packages/core/src/controller/setManyTypes.ts index 58d8a3021658..f7f1aac80d2c 100644 --- a/packages/core/src/controller/setManyTypes.ts +++ b/packages/core/src/controller/setManyTypes.ts @@ -1,5 +1,16 @@ /** Types for batch `Controller.set([Entity], rows)` */ -import type { Denormalize, EntityInterface } from '@data-client/normalizr'; +import type { Denormalize } from '@data-client/normalizr'; + +/** Matches Entity classes (same members Denormalize<> checks). + * Not EntityInterface: older @data-client/endpoint versions declare Entity.pk() with mutable `args`. */ +interface EntityLike { + createIfValid(...args: any): any; + pk(...args: any): any; + readonly key: string; + prototype: any; +} + +type EntityMapLike = { readonly [k: string]: EntityLike }; /** What one row normalizes to: a reference to one stored entity */ type EntityRef = string | { readonly id: string; readonly schema: string }; @@ -7,7 +18,7 @@ type EntityRef = string | { readonly id: string; readonly schema: string }; /** Schemas that write each row to one stored entity: Entity, Union, or Invalidate (batch delete). * Query, All and Collection don't: they normalize to lists, or Collection keys by args batch set() lacks. */ type SetEntitySchema = - | EntityInterface + | EntityLike | { _normalizeNullable(): EntityRef | undefined; // excludes Collection @@ -18,7 +29,7 @@ type SetEntitySchema = export type SetManySchema = | readonly SetEntitySchema[] | { - readonly schema: SetEntitySchema | Record; + readonly schema: SetEntitySchema | EntityMapLike; // Array and Values; excludes schema.Object, whose queryKey() returns any schemaKey(): string; queryKey(...args: any): undefined; @@ -54,7 +65,7 @@ type SetRow = /** Polymorphic rows may carry a discriminator that is not an Entity field */ type SetRowOf = - Sch extends EntityInterface ? SetRow + Sch extends EntityLike ? SetRow : SetRow & { readonly [k: string]: unknown }; export type SetManyValue = diff --git a/packages/endpoint/src/interface.ts b/packages/endpoint/src/interface.ts index dbba22f2e168..3eeef055feac 100644 --- a/packages/endpoint/src/interface.ts +++ b/packages/endpoint/src/interface.ts @@ -61,7 +61,7 @@ export interface EntityInterface extends SchemaSimple { params: any, parent: any, key: string | undefined, - args: readonly any[], + args: any[], ): string | number | undefined; readonly key: string; indexes?: any; diff --git a/packages/endpoint/src/schemas/Entity.ts b/packages/endpoint/src/schemas/Entity.ts index 684529b7a0fb..e5bec07b09f4 100644 --- a/packages/endpoint/src/schemas/Entity.ts +++ b/packages/endpoint/src/schemas/Entity.ts @@ -45,13 +45,16 @@ export default abstract class Entity extends EntityMixin(EmptyBase) { * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint */ - declare static pk: ( - this: T, - value: Partial>, - parent?: any, - key?: string, - args?: readonly any[], - ) => string | number | undefined; + declare static pk: { + // method syntax: overrides that type `args` as a mutable array still compile + pk( + this: T, + value: Partial>, + parent?: any, + key?: string, + args?: readonly any[], + ): string | number | undefined; + }['pk']; /** Do any transformations when first receiving input * diff --git a/packages/endpoint/src/schemas/__tests__/Entity.test.ts b/packages/endpoint/src/schemas/__tests__/Entity.test.ts index aee7346569cb..a804380262e9 100644 --- a/packages/endpoint/src/schemas/__tests__/Entity.test.ts +++ b/packages/endpoint/src/schemas/__tests__/Entity.test.ts @@ -1,5 +1,5 @@ -import { normalize, INVALID } from '@data-client/normalizr'; import type { EntityInterface } from '@data-client/normalizr'; +import { normalize, INVALID } from '@data-client/normalizr'; import { denormalize as plainDenormalize } from '@data-client/normalizr'; import { denormalize as immDenormalize } from '@data-client/normalizr/imm'; import { IDEntity } from '__tests__/new'; @@ -464,6 +464,16 @@ describe(`${Entity.name} normalization`, () => { const entity: EntityInterface = User; expect(entity.pk({ id: '5' }, undefined, undefined, [])).toBe('5'); }); + + test('static pk() overrides may type args as a mutable array', () => { + class User extends Entity { + readonly id: string = ''; + static pk(value: any, parent?: any, key?: string, args?: any[]) { + return `${value.id}-${args?.[0]}`; + } + } + expect(User.pk({ id: '5' }, undefined, undefined, ['a'])).toBe('5-a'); + }); }); describe('mergeStrategy', () => { From d9776d1ddd9dcb88e614e7639d1ca346cd2b3472 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 16:44:56 +0000 Subject: [PATCH 4/6] fix(core): Use EntityInterface for batch set() schemas Entity now satisfies EntityInterface, so the private EntityLike type is no longer needed. Batch set() is unreleased, so this only requires matching endpoint versions for a new feature. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01F92DVcdVpL6sVLSA69App5 --- .changeset/controller-set-array.md | 2 ++ packages/core/src/controller/setManyTypes.ts | 19 ++++--------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/.changeset/controller-set-array.md b/.changeset/controller-set-array.md index 220efce32ab9..9cdc9a779990 100644 --- a/.changeset/controller-set-array.md +++ b/.changeset/controller-set-array.md @@ -10,6 +10,8 @@ Fix `controller.set()` types for Array schemas Rows are typed by the Entity's fields. The schema holds one Entity, [Union](https://dataclient.io/rest/api/Union) (for mixed types) or [Invalidate](https://dataclient.io/rest/api/Invalidate) (to delete), in an Array or [Values](https://dataclient.io/rest/api/Values) (which takes an object keyed by id). +Batch `set()` needs `@data-client/rest` (or `endpoint`/`graphql`) from this release, since older Entity classes don't type as `EntityInterface`. + ```ts // Before: TypeScript error on [Ticker], so batches became one set() per row for (const row of rows) { diff --git a/packages/core/src/controller/setManyTypes.ts b/packages/core/src/controller/setManyTypes.ts index f7f1aac80d2c..58d8a3021658 100644 --- a/packages/core/src/controller/setManyTypes.ts +++ b/packages/core/src/controller/setManyTypes.ts @@ -1,16 +1,5 @@ /** Types for batch `Controller.set([Entity], rows)` */ -import type { Denormalize } from '@data-client/normalizr'; - -/** Matches Entity classes (same members Denormalize<> checks). - * Not EntityInterface: older @data-client/endpoint versions declare Entity.pk() with mutable `args`. */ -interface EntityLike { - createIfValid(...args: any): any; - pk(...args: any): any; - readonly key: string; - prototype: any; -} - -type EntityMapLike = { readonly [k: string]: EntityLike }; +import type { Denormalize, EntityInterface } from '@data-client/normalizr'; /** What one row normalizes to: a reference to one stored entity */ type EntityRef = string | { readonly id: string; readonly schema: string }; @@ -18,7 +7,7 @@ type EntityRef = string | { readonly id: string; readonly schema: string }; /** Schemas that write each row to one stored entity: Entity, Union, or Invalidate (batch delete). * Query, All and Collection don't: they normalize to lists, or Collection keys by args batch set() lacks. */ type SetEntitySchema = - | EntityLike + | EntityInterface | { _normalizeNullable(): EntityRef | undefined; // excludes Collection @@ -29,7 +18,7 @@ type SetEntitySchema = export type SetManySchema = | readonly SetEntitySchema[] | { - readonly schema: SetEntitySchema | EntityMapLike; + readonly schema: SetEntitySchema | Record; // Array and Values; excludes schema.Object, whose queryKey() returns any schemaKey(): string; queryKey(...args: any): undefined; @@ -65,7 +54,7 @@ type SetRow = /** Polymorphic rows may carry a discriminator that is not an Entity field */ type SetRowOf = - Sch extends EntityLike ? SetRow + Sch extends EntityInterface ? SetRow : SetRow & { readonly [k: string]: unknown }; export type SetManyValue = From 64fc733344612b4935ff21a6758468bd4eb1a197 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 16:48:07 +0000 Subject: [PATCH 5/6] docs: Track deferred Entity pk() cleanups; recommend readonly args Add plans/next-breaking-release.md for the compatibility shims, and note in the v0.19 blog and changeset that static pk() overrides should type args as readonly ahead of that release. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01F92DVcdVpL6sVLSA69App5 --- .changeset/entity-pk-readonly-args.md | 10 ++++++++ plans/next-breaking-release.md | 12 +++++++++ website/blog/2026-10-03-v0.19-batch-set.md | 30 ++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 plans/next-breaking-release.md diff --git a/.changeset/entity-pk-readonly-args.md b/.changeset/entity-pk-readonly-args.md index 4539a107a16a..6cd0e9e187fc 100644 --- a/.changeset/entity-pk-readonly-args.md +++ b/.changeset/entity-pk-readonly-args.md @@ -15,3 +15,13 @@ import type { EntityInterface } from '@data-client/react'; // After: typechecks const schema: EntityInterface = User; ``` + +Overrides of `static pk()` that type `args` as a mutable array still compile, but a future breaking release will require `readonly any[]`, so update them now: + +```ts +class User extends Entity { + static pk(value: any, parent?: any, key?: string, args?: readonly any[]) { + return `${value.id}-${args?.[0]?.org}`; + } +} +``` diff --git a/plans/next-breaking-release.md b/plans/next-breaking-release.md new file mode 100644 index 000000000000..7a7b714a8e1d --- /dev/null +++ b/plans/next-breaking-release.md @@ -0,0 +1,12 @@ +# Next Breaking Release TODO + +Type and API cleanups deferred because they would break users or mixed package versions in a minor release. Do them in the next release that already breaks compatibility and requires matching `@data-client/*` versions, and list each in that release's blog migration guide. + +## Entity `pk()` args + +[#4149](https://github.com/reactive/data-client/pull/4149) made Entity classes assignable to `EntityInterface` without breaking anyone, using two compatibility shims. + +- **Plain `Entity.pk` declaration**: `packages/endpoint/src/schemas/Entity.ts` declares `static pk` with method syntax (`{ pk(...): ... }['pk']`) so subclass overrides that type `args?: any[]` still compile. Replace it with a plain function type taking `args?: readonly any[]`. + - Breaks: `static pk()` overrides that annotate `args` as a mutable array. Migration: change the annotation to `readonly any[]` (the v0.19 blog already recommends this). +- **Readonly `args` in endpoint's `EntityInterface`**: `packages/endpoint/src/interface.ts` still declares `pk(..., args: any[])`, while normalizr's `EntityInterface` takes `readonly any[]`. Make them match, or have endpoint re-export normalizr's. + - Breaks: Entity subclasses with a mutable-`args` `static pk()` override assigned to endpoint's `EntityInterface`. diff --git a/website/blog/2026-10-03-v0.19-batch-set.md b/website/blog/2026-10-03-v0.19-batch-set.md index d08e8a6e8caf..286f95fcf288 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -23,6 +23,7 @@ import StackBlitz from '@site/src/components/StackBlitz'; - Vue `DataClientPlugin` installs on Vue versions before 3.5 instead of throwing `app.onUnmount is not a function` ([#4146](https://github.com/reactive/data-client/pull/4146)) - Fix `Cannot find name 'NoInfer'` and `export type` errors on TypeScript 4.x with `skipLibCheck` off ([#4138](https://github.com/reactive/data-client/pull/4138)) - Fix `Entity`, `Endpoint`, `Union` and `RestEndpoint` type errors on TypeScript 4.0–4.5 with `skipLibCheck` off ([#4140](https://github.com/reactive/data-client/pull/4140)) +- [Entity](/rest/api/Entity) classes can be used where an `EntityInterface` is expected ([#4149](https://github.com/reactive/data-client/pull/4149)); [prepare `pk()` overrides](/blog/2026/10/03/v0.19-batch-set#entity-pk-args) for a future release **Breaking Changes:** @@ -102,3 +103,32 @@ If you filter [DevToolsManager](/docs/api/DevToolsManager) actions by schema, a is the Array schema, so match `action.schema[0]` for `[Ticker]` rather than the Entity itself. ::: + +## Other improvements + +### Entity pk() args {#entity-pk-args} + +[Entity.pk()](/rest/api/Entity#pk) now types its `args` parameter as `readonly any[]`, so Entity classes can be passed +where an `EntityInterface` is expected ([#4149](https://github.com/reactive/data-client/pull/4149)). + +```ts +import type { EntityInterface } from '@data-client/react'; + +const schema: EntityInterface = User; +``` + +:::tip + +Overrides of `static pk()` that annotate `args` as a mutable array still compile, but a future breaking release +will require `readonly`. Update them now: + +```ts +class User extends Entity { + // highlight-next-line + static pk(value: any, parent?: any, key?: string, args?: readonly any[]) { + return `${value.id}-${args?.[0]?.org}`; + } +} +``` + +::: From 2da5e5cf3fd3ebb67f4d009d731d40bba35b5c0e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 16:49:17 +0000 Subject: [PATCH 6/6] internal: Document breaking change strategy as a cursor rule Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01F92DVcdVpL6sVLSA69App5 --- .agents/skills/changeset/SKILL.md | 2 +- .cursor/rules/breaking-changes.mdc | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .cursor/rules/breaking-changes.mdc diff --git a/.agents/skills/changeset/SKILL.md b/.agents/skills/changeset/SKILL.md index 7bcd8f3c87b2..f99a4b3f7aab 100644 --- a/.agents/skills/changeset/SKILL.md +++ b/.agents/skills/changeset/SKILL.md @@ -56,7 +56,7 @@ All user-facing text (changesets, blog entries, docs) should be written from the 2. **User vocabulary** — Name public APIs (`RestEndpoint`, `resource()`, hook names). Do not explain how the fix was implemented. 3. **When to add code** — Prefer a minimal example when the change is TypeScript-only or subtle: show the pattern that was broken and now works (subclass, `extend`, option object). Skip examples for trivial renames or obvious one-line fixes. 4. **Examples** — Realistic imports and types; omit unrelated options. For fixes, you can show one “now types correctly” snippet instead of a long before/after if the before state was “TypeScript error on …”. -5. **Breaking changes** — Still say what the user must do; use Before/After sections with code when the migration is non-obvious. +5. **Breaking changes** — Still say what the user must do; use Before/After sections with code when the migration is non-obvious. Before marking anything breaking, follow `.cursor/rules/breaking-changes.mdc` (ship a compatible version, recommend the future-proof form, track the cleanup). ## Changeset format - **First line**: Action verb ("Add", "Fix", "Update", "Remove") diff --git a/.cursor/rules/breaking-changes.mdc b/.cursor/rules/breaking-changes.mdc new file mode 100644 index 000000000000..cb205e2c8a04 --- /dev/null +++ b/.cursor/rules/breaking-changes.mdc @@ -0,0 +1,25 @@ +--- +description: Strategy for type/API changes that would break users or mixed package versions +globs: packages/**, .changeset/**, plans/next-breaking-release.md +alwaysApply: false +--- + +# Breaking Change Strategy + +Get users the fix now with a clean upgrade path; batch the actual break into a later release. + +## Before shipping a change that could break + +Check both: +- **User code**: subclasses, overrides, explicit annotations, and classes implementing exported interfaces. +- **Mixed versions**: `@data-client/rest`/`endpoint`/`graphql` aren't dependencies of `react`/`vue`/`core`, so users can pair a newer client with an older endpoint (and vice versa). Type checks in core against endpoint types must still accept older endpoint versions. + +Requiring a matching version for a feature that is new in the same release is not breaking. + +## When it would break + +1. **Ship a compatible version**: a shim that keeps old code compiling and working (e.g. method-syntax declarations for parameter bivariance, a loose structural type instead of the strict interface, a deprecated alias). +2. **Prepare users**: in the changeset and the release's draft blog post, recommend the future-proof form now with a code example, so the later break is a no-op for them. +3. **Track the cleanup**: add an entry to [plans/next-breaking-release.md](../../plans/next-breaking-release.md) naming the shim to remove, what it breaks, and the migration. + +Mark a change `BREAKING` (minor bump while under 1.0) only when no compatible version is reasonable. When a release is already breaking, work through `plans/next-breaking-release.md`, move each finished item into that release's blog migration guide, and delete it from the plan.