diff --git a/.cursor/skills/data-client-v0.18-migration/SKILL.md b/.cursor/skills/data-client-v0.18-migration/SKILL.md index a318c4eeb032..5f3637309d88 100644 --- a/.cursor/skills/data-client-v0.18-migration/SKILL.md +++ b/.cursor/skills/data-client-v0.18-migration/SKILL.md @@ -1,186 +1,59 @@ --- name: data-client-v0.18-migration -description: Migrate custom @data-client schemas to v0.18 delegate signatures: denormalize(input, args, unvisit) -> denormalize(input, delegate) and normalize(input, parent, key, args, visit, delegate) -> normalize(input, parent, key, delegate). Use when upgrading to v0.18, seeing TS errors about unvisit/visit/args signatures, or adapting custom Schema implementations. +description: Migrate @data-client codebases to v0.18 delegate signatures. Drives the v0.18 codemod and then handles the cases it cannot — especially registering argsKey for schemas whose output depends on endpoint args. Use when upgrading to v0.18, seeing TS errors about unvisit/visit/args/delegate signatures, or adapting custom Schema implementations. --- # @data-client v0.18 Migration Applies to anyone implementing a custom [`Schema`](https://dataclient.io/rest/api/SchemaSimple) — `SchemaSimple`, `SchemaClass`, polymorphic wrappers, or types that subclass `EntityMixin` directly. Built-in schemas (`Entity`, `resource()`, `Collection`, `Union`, `Values`, `Array`, `Object`, `Query`, `Invalidate`, `Lazy`) are migrated by the library. -The automated codemod handles the common cases: - -```bash -npx jscodeshift -t https://dataclient.io/codemods/v0.18.js --extensions=ts,tsx,js,jsx src/ -``` - -## Codemod prerequisites / limits - -- **Edits only run in files that already import `@data-client/*`** (any subpath). If the codemod appears to do nothing, add such an import (or migrate that file by hand). -- **`denormalize` / `normalize` as class fields** — e.g. `denormalize = (input, args, unvisit) => { ... }` — are **not** transformed; use a `denormalize(...) { }` method (or rewrite manually). -- **TS interface method signatures** — only the key `denormalize` is matched for `TSMethodSignature` (not `_denormalize` / `_denormalizeNullable`). Underscore-prefixed names are updated when they appear as **`declare` or property types** with a function type annotation (see below). -- **Top-level** `function denormalize` / `function normalize` is handled; **`const denormalize = function...`** is not. - -This skill describes what the codemod does and how to handle the cases it cannot. - ## What changed -`Schema.denormalize()` now takes a single `delegate` instead of `(args, unvisit)`. Reading `delegate.args` does **not** contribute to cache invalidation — schemas whose output varies with endpoint args must register that dependency through `delegate.argsKey(fn)`. +`Schema.denormalize` and `Schema.normalize` now take a single `delegate` instead of `(args, unvisit)` / `(args, visit, delegate)`: ```ts -// before -denormalize(input, args, unvisit) { - return unvisit(this.schema, input); -} - -// after -denormalize(input, delegate) { - return delegate.unvisit(this.schema, input); -} -``` - -`Schema.normalize()` also takes a delegate, matching the denormalize shape. The -old signature was `(input, parent, key, args, visit, delegate, parentEntity?)`; -the new signature is `(input, parent, key, delegate, parentEntity?)`. - -```ts -// before -normalize(input, parent, key, args, visit, delegate) { - return visit(this.schema, input, parent, key, args); -} - -// after -normalize(input, parent, key, delegate) { +denormalize(input, delegate) { return delegate.unvisit(this.schema, input); } +normalize(input, parent, key, delegate /*, parentEntity? */) { return delegate.visit(this.schema, input, parent, key); } ``` -Full delegate surface ([`IDenormalizeDelegate`](https://dataclient.io/rest/api/SchemaSimple)): - -```ts -interface IDenormalizeDelegate { - unvisit(schema: any, input: any): any; - readonly args: readonly any[]; - argsKey(fn: (args: readonly any[]) => string | undefined): string | undefined; -} -``` - -## Migration rules +Critical semantic change: reading `delegate.args` does **not** contribute to cache invalidation. Schemas whose *output* varies with endpoint args must register that dependency through [`delegate.argsKey(fn)`](https://dataclient.io/rest/api/SchemaSimple). See [Step 2](#step-2-register-argskey-for-args-dependent-schemas-not-automatable) below. -### Class methods +## Step 1: run the codemod -`(input, args, unvisit)` → `(input, delegate)`. Inside the body: +**Pre-check — skip the rest of this skill if it doesn't apply.** Most apps that only consume `@data-client` (use `Entity`, `resource()`, `Collection`, `Query`, etc.) need *zero* code changes for v0.18. Run the search below at your repo root — `rg` respects `.gitignore` so it won't dive into `node_modules`. If it returns nothing, you're done; bump the package versions and move on. -- `unvisit(schema, value)` → `delegate.unvisit(schema, value)` -- bare `args` references (including spreads) → `delegate.args` -- pass-through `someSchema.denormalize(input, args, unvisit)` → `someSchema.denormalize(input, delegate)` - -```ts -// before -class Wrapper { - denormalize(input: {}, args: readonly any[], unvisit: any) { - const value = unvisit(this.schema, input); - return this.process(value, ...args); - } -} - -// after -class Wrapper { - denormalize(input: {}, delegate: IDenormalizeDelegate) { - const value = delegate.unvisit(this.schema, input); - return this.process(value, ...delegate.args); - } -} +```bash +# preferred (ripgrep) +rg -n 'extends (Schema|SchemaSimple|SchemaClass|EntityMixin)\b|^\s*_?(de)?normalize\s*[(=:]|\bunvisit\b|\bvisit\(' . +# fallback (POSIX grep) — adjust the path list to your source roots +grep -rEn --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \ + 'extends (Schema|SchemaSimple|SchemaClass|EntityMixin)\b|^[[:space:]]*_?(de)?normalize[[:space:]]*[(=:]|\bunvisit\b|\bvisit\(' \ + src app lib packages 2>/dev/null ``` -### Normalize methods - -`(input, parent, key, args, visit, delegate, parentEntity?)` → -`(input, parent, key, delegate, parentEntity?)`. Inside the body: +The pre-check intentionally ignores `Entity` overrides of `pk`, `process`, and `merge` — those signatures didn't change in v0.18. Only the `denormalize` / `normalize` delegate signature did. -- `visit(schema, value, parent, key, args)` → `delegate.visit(schema, value, parent, key)` -- bare `args` references (including spreads) → `delegate.args` -- pass-through `someSchema.normalize(input, parent, key, args, visit, delegate)` → - `someSchema.normalize(input, parent, key, delegate)` +Otherwise, commit first so you can diff, then run the codemod against your source roots — `src/` is the conventional location, but App Router (`app/`), Vue (`src/`), monorepo packages (`packages/`), and library (`lib/`) layouts all need the path adjusted: -```ts -// before -class Wrapper { - normalize(input: {}, parent: any, key: string, args: readonly any[], visit: any, delegate: any) { - const value = visit(this.schema, input, parent, key, args); - return this.process(value, ...args); - } -} - -// after -class Wrapper { - normalize(input: {}, parent: any, key: string, delegate: INormalizeDelegate) { - const value = delegate.visit(this.schema, input, parent, key); - return this.process(value, ...delegate.args); - } -} +```bash +npx jscodeshift -t https://dataclient.io/codemods/v0.18.js --extensions=ts,tsx,js,jsx ``` -If your normalize implementation used a different name for the existing delegate -parameter, such as `snapshot`, the codemod keeps that name and rewrites -`args`/`visit` to `snapshot.args` / `snapshot.visit`. - -### TypeScript signatures - -Update method signatures and `declare` fields the same way: - -```ts -// before -interface MySchema { - denormalize(input: {}, args: readonly any[], unvisit: (s: any, v: any) => any): any; - normalize(input: {}, parent: any, key: any, args: readonly any[], visit: (s: any, v: any, p: any, k: any, a: readonly any[]) => any, delegate: any): any; -} - -class Lazy { - declare _denormalizeNullable: ( - input: {}, - args: readonly any[], - unvisit: (s: any, v: any) => any, - ) => any; -} - -// after — the codemod adds delegate types to your existing -// `@data-client/{rest,endpoint,normalizr,...}` import as an inline -// `type` specifier. Only when no such import exists does it create -// new `import type { ... } from '@data-client/endpoint'` lines. -import { - Entity, - type IDenormalizeDelegate, - type INormalizeDelegate, -} from '@data-client/rest'; - -interface MySchema { - denormalize(input: {}, delegate: IDenormalizeDelegate): any; - normalize(input: {}, parent: any, key: any, delegate: INormalizeDelegate): any; -} +Do **not** point jscodeshift at the repo root — it doesn't respect `.gitignore` and will walk into `node_modules`. -class Lazy { - declare _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => any; -} -``` +A clean run looks like `0 errors, N unmodified, 0 skipped` — that's expected, not a failure. The codemod rewrites parameter lists, body references (`unvisit(…)`, `visit(…)`, bare `args`), pass-through calls, TS method/property signatures, and adds `IDenormalizeDelegate` / `INormalizeDelegate` imports. After it runs, only the cases below need hand work. -On types: **`interface { denormalize(...) }`** uses the literal key `denormalize` only. **`_denormalize` / `_denormalizeNullable`** (and similar) are matched on **`declare` / property signatures** whose type is a `(...)` function type. **`normalize`** type signatures use the literal key `normalize` with the old 6- or 7-parameter form. +**Single-file components (Vue/Svelte/Astro/MDX):** the codemod uses jscodeshift's `tsx` parser, which can't parse SFCs — adding `.vue` (etc.) to `--extensions` produces parse errors, not transforms. If your schemas live inside SFC `