From 0471d356cd977baddac18ec6f8c907e5783fbccc Mon Sep 17 00:00:00 2001 From: Tobias Binna Date: Fri, 14 Aug 2026 12:40:40 +0800 Subject: [PATCH] feat: update to Effect 4 RC (#8) --- README.md | 8 +-- ...07-release-v2-without-a-prerelease-line.md | 3 - ...07-release-v3-without-a-prerelease-line.md | 3 + docs/{migration-v2.md => migration-v3.md} | 18 +++--- docs/plans/effect-v4-rewrite.md | 14 ++--- package-lock.json | 57 ++++--------------- package.json | 10 ++-- scripts/package-smoke.mjs | 12 ++-- test/type-contracts.ts | 6 +- 9 files changed, 48 insertions(+), 83 deletions(-) delete mode 100644 docs/adr/0007-release-v2-without-a-prerelease-line.md create mode 100644 docs/adr/0007-release-v3-without-a-prerelease-line.md rename docs/{migration-v2.md => migration-v3.md} (87%) diff --git a/README.md b/README.md index cfe3921..2d62882 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ `@toolsplus/json-evolutions` evolves stored JSON objects through explicitly versioned changesets while application code works with the latest Effect Schema representation. -Version 2 is Effect 4-native and ESM-only. It deliberately replaces the v1 io-ts/fp-ts interface while preserving the stored `_version` protocol. +Version 3 is Effect 4-native and ESM-only. It deliberately replaces the v1 io-ts/fp-ts interface while preserving the stored `_version` protocol. ## Install ```shell -npm install @toolsplus/json-evolutions effect@4.0.0-beta.107 +npm install @toolsplus/json-evolutions effect@^4.0.0-rc.109 ``` -The Effect peer is pinned exactly while Effect 4 Schema remains beta-sensitive. Node.js 24 or newer is required. +The Effect peer accepts RC 109 or newer compatible Effect 4 releases. Development and package smoke tests remain pinned to RC 109 as the supported baseline. Node.js 24 or newer is required. ## Complete example @@ -111,4 +111,4 @@ A stored value must be a genuine JSON root object with an own non-negative safe- Functions, `undefined`, symbols, bigint, `Date`, `Map`, `Set`, non-finite numbers, cycles, and root arrays are rejected. Changesets never mutate the original input. The engine owns `_version`, stamps it after each successful changeset, and validates the newly stamped value before continuing. Business structs may not declare the reserved root marker. -See [the v1-to-v2 migration guide](docs/migration-v2.md) for source migration details. +See [the v1-to-v3 migration guide](docs/migration-v3.md) for source migration details. diff --git a/docs/adr/0007-release-v2-without-a-prerelease-line.md b/docs/adr/0007-release-v2-without-a-prerelease-line.md deleted file mode 100644 index 50446da..0000000 --- a/docs/adr/0007-release-v2-without-a-prerelease-line.md +++ /dev/null @@ -1,3 +0,0 @@ -# Release v2 without a prerelease line - -The Effect 4 rewrite will be released directly as stable `2.0.0`, without a `2.0.0-beta.x` line, even though it initially peers on exact `effect@4.0.0-beta.107`. The package's own interface and behavior will be treated as release-ready; exact Effect pinning contains upstream beta drift, and later Effect upgrades can be released normally. diff --git a/docs/adr/0007-release-v3-without-a-prerelease-line.md b/docs/adr/0007-release-v3-without-a-prerelease-line.md new file mode 100644 index 0000000..d315217 --- /dev/null +++ b/docs/adr/0007-release-v3-without-a-prerelease-line.md @@ -0,0 +1,3 @@ +# Release v3 without a prerelease line + +The Effect 4 rewrite was released directly as stable `3.0.0`, without a `3.0.0-beta.x` line. It initially pinned exact `effect@4.0.0-beta.107` while upstream interfaces were beta-sensitive; the current peer contract accepts `effect@^4.0.0-rc.109`. The package's own interface and behavior were treated as release-ready, while development and package smoke tests remain pinned to RC 109 as the supported baseline. diff --git a/docs/migration-v2.md b/docs/migration-v3.md similarity index 87% rename from docs/migration-v2.md rename to docs/migration-v3.md index 0cf4ae1..13d5b50 100644 --- a/docs/migration-v2.md +++ b/docs/migration-v3.md @@ -1,17 +1,17 @@ -# Migrating from v1 to v2 +# Migrating from v1 to v3 -Version 2 preserves stored values but intentionally replaces the source API. Existing JSON objects carrying sequential `_version` markers remain the compatibility boundary. +Version 3 preserves stored values but intentionally replaces the source API. Existing JSON objects carrying sequential `_version` markers remain the compatibility boundary. ## Package requirements - Use Node.js 24 or newer. - Consume the package as ESM. -- Install the exact peer `effect@4.0.0-beta.107`. +- Install a compatible Effect 4 peer with `effect@^4.0.0-rc.109`. - Remove direct `io-ts` and `fp-ts` usage that existed only for JSON Evolutions. ## API replacements -| v1 | v2 | +| v1 | v3 | | -------------------------------------------- | ---------------------------------------------- | | io-ts codec | `Schema.Struct` | | `versioned(codec, latestVersion(changelog))` | `schema.pipe(versioned(changelog))` | @@ -24,6 +24,8 @@ Version 2 preserves stored values but intentionally replaces the source API. Exi ## Before +This schematic legacy snippet shows the old call shape. `input` and `throwError` stand for application-specific values and are intentionally omitted. + ```typescript legacy import * as E from "fp-ts/Either"; import * as t from "io-ts"; @@ -45,7 +47,7 @@ const decoded = pipe(evolve(changelog)(input), E.chain(Configuration.decode)); ## After -The v2 example below is compiled against the packed npm artifact during the package smoke test. +The v3 example below is compiled against the packed npm artifact during the package smoke test. ```typescript package-smoke import {Effect, Result, Schema} from "effect"; @@ -64,14 +66,14 @@ const input = {_version: 1, enabled: true}; const decoded = await Effect.runPromise( evolveAndDecode(StoredConfiguration)(input), ); -if (!decoded.enabled) throw new Error("v2 migration example failed"); +if (!decoded.enabled) throw new Error("v3 migration example failed"); ``` ## Behavior changes ### Strict recursive JSON -Version 2 validates the entire stored value. JavaScript-only values such as functions, `undefined`, bigint, `Date`, `Map`, `Set`, non-finite numbers, and cycles now fail as `InvalidStoredValue`. +Version 3 validates the entire stored value. JavaScript-only values such as functions, `undefined`, bigint, `Date`, `Map`, `Set`, non-finite numbers, and cycles now fail as `InvalidStoredValue`. ### Exact latest-version decoding @@ -99,4 +101,4 @@ Replace `errorCode` branching with `_tag` matching. Errors are yieldable `Schema ### ESM-only consumption -Version 2 exposes one ESM root entry. Replace `require()` with `import`; no CommonJS condition is advertised. +Version 3 exposes one ESM root entry. Replace `require()` with `import`; no CommonJS condition is advertised. diff --git a/docs/plans/effect-v4-rewrite.md b/docs/plans/effect-v4-rewrite.md index a93e256..8c19eb6 100644 --- a/docs/plans/effect-v4-rewrite.md +++ b/docs/plans/effect-v4-rewrite.md @@ -2,7 +2,7 @@ ## Outcome -Release `@toolsplus/json-evolutions` 2.0.0 as an Effect 4-native, ESM-only library. The release replaces the io-ts and fp-ts interface, preserves the persisted stored-value protocol, validates genuine JSON at runtime, and continues delegating changeset execution to `fast-json-patch` and `immutability-helper`. +Release `@toolsplus/json-evolutions` 3.0.0 as an Effect 4-native, ESM-only library. The release replaces the io-ts and fp-ts interface, preserves the persisted stored-value protocol, validates genuine JSON at runtime, and continues delegating changeset execution to `fast-json-patch` and `immutability-helper`. The rewrite is intentionally source-incompatible with v1. Existing stored values remain compatible. @@ -153,7 +153,7 @@ evolveAndDecode( The final implementation must preserve the supplied struct's decoding and encoding service requirements. `VersionedSchema` is nominally marked and retains its validated changelog through private symbol metadata. -`versioned` initially supports `Schema.Struct` only. It rejects a business schema that declares the reserved root `_version` field. Do not add an opaque arbitrary-object-codec overload in v2.0. +`versioned` initially supports `Schema.Struct` only. It rejects a business schema that declares the reserved root `_version` field. Do not add an opaque arbitrary-object-codec overload in v3.0. All transformation functions are data-last and pipe-friendly. Do not add duplicate data-first overloads. @@ -318,8 +318,8 @@ Use exact Effect beta ranges. Do not use `^` or `~` while the public Schema type - Update CI and release workflow test commands for Vitest. - Preserve build, coverage, lint, formatting, documentation, semantic-release, npm provenance, and Node 24 checks. -- Ensure the release commit communicates a breaking change under the repository's Angular conventional-commit rules so semantic-release selects `2.0.0`. -- Publish directly as stable `2.0.0`; do not create a package prerelease line. +- Ensure the release commit communicates a breaking change under the repository's Angular conventional-commit rules so semantic-release selects `3.0.0`. +- Publish directly as stable `3.0.0`; do not create a package prerelease line. ## Test strategy @@ -417,7 +417,7 @@ State explicitly that changelogs are trusted source declarations and that stored ### Migration guide -Add a concise v1-to-v2 guide covering: +Add a concise v1-to-v3 guide covering: - `io-ts` codec to Effect `Schema.Struct` - `versioned(codec, latestVersion(changelog))` to `schema.pipe(versioned(changelog))` @@ -495,11 +495,11 @@ Do this after the interface works end to end so packaging failures remain separa ### Phase 7: Consumer migration and release readiness 1. Rewrite the README. -2. Add the v1-to-v2 migration guide. +2. Add the v1-to-v3 migration guide. 3. Verify generated API documentation. 4. Audit exports and tarball contents. 5. Run the full quality gate. -6. Prepare a breaking conventional commit/release path for stable 2.0.0. +6. Prepare a breaking conventional commit/release path for stable 3.0.0. ## Quality gate diff --git a/package-lock.json b/package-lock.json index 7b374fd..da891eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@typescript-eslint/eslint-plugin": "8.43.0", "@typescript-eslint/parser": "8.43.0", "@vitest/coverage-v8": "4.1.10", - "effect": "4.0.0-beta.107", + "effect": "4.0.0-rc.109", "eslint": "9.35.0", "eslint-config-prettier": "10.1.8", "fast-check": "4.9.0", @@ -41,7 +41,7 @@ "node": ">=24" }, "peerDependencies": { - "effect": "4.0.0-beta.107" + "effect": "^4.0.0-rc.109" } }, "node_modules/@actions/core": { @@ -3381,17 +3381,15 @@ } }, "node_modules/effect": { - "version": "4.0.0-beta.107", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.107.tgz", - "integrity": "sha512-OoBAv8eF+yanc+C6xhgEUnWeXUSHA6ynnscYqpkAY9GSnzZWystsIjBowVqCkLpHGlnRtdIqYT3wHwpOY6JDnQ==", + "version": "4.0.0-rc.109", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-rc.109.tgz", + "integrity": "sha512-6ubcOCtfdbmFO5+vgcT2HsTw5s+n3aMUj4eAIbVpUxP7+VYCwXxxcBHgiWgizOrGO1eGmuOBFek3mM0dFcwaWA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", - "kubernetes-types": "^1.30.0", - "msgpackr": "^2.0.4", - "uuid": "^14.0.1" + "msgpackr": "^2.0.4" } }, "node_modules/emoji-regex": { @@ -5257,13 +5255,6 @@ "node": ">=0.10.0" } }, - "node_modules/kubernetes-types": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", - "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -10734,20 +10725,6 @@ "dev": true, "license": "MIT" }, - "node_modules/uuid": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", - "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -13351,16 +13328,14 @@ } }, "effect": { - "version": "4.0.0-beta.107", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.107.tgz", - "integrity": "sha512-OoBAv8eF+yanc+C6xhgEUnWeXUSHA6ynnscYqpkAY9GSnzZWystsIjBowVqCkLpHGlnRtdIqYT3wHwpOY6JDnQ==", + "version": "4.0.0-rc.109", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-rc.109.tgz", + "integrity": "sha512-6ubcOCtfdbmFO5+vgcT2HsTw5s+n3aMUj4eAIbVpUxP7+VYCwXxxcBHgiWgizOrGO1eGmuOBFek3mM0dFcwaWA==", "dev": true, "requires": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", - "kubernetes-types": "^1.30.0", - "msgpackr": "^2.0.4", - "uuid": "^14.0.1" + "msgpackr": "^2.0.4" } }, "emoji-regex": { @@ -14565,12 +14540,6 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, - "kubernetes-types": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", - "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", - "dev": true - }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -18098,12 +18067,6 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, - "uuid": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", - "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", - "dev": true - }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 9c3b7e3..d20fb7a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "!**/__mocks__/**", "!**/*.json", "README.md", - "docs/migration-v2.md" + "docs/migration-v3.md" ], "engines": { "node": ">=24" @@ -31,10 +31,10 @@ "build": "node scripts/build.mjs", "typecheck": "tsc -p tsconfig.test.json --noEmit", "fix": "run-s fix:*", - "fix:prettier": "prettier \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.mjs\" \"vitest.config.ts\" \"README.md\" \"docs/migration-v2.md\" --write", + "fix:prettier": "prettier \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.mjs\" \"vitest.config.ts\" \"README.md\" \"docs/migration-v3.md\" --write", "fix:lint": "eslint src test --fix", "lint": "eslint src test", - "prettier": "prettier \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.mjs\" \"vitest.config.ts\" \"README.md\" \"docs/migration-v2.md\" --check", + "prettier": "prettier \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.mjs\" \"vitest.config.ts\" \"README.md\" \"docs/migration-v3.md\" --check", "test": "vitest run", "test:coverage": "vitest run --coverage", "smoke:package": "node scripts/package-smoke.mjs", @@ -58,7 +58,7 @@ "@typescript-eslint/eslint-plugin": "8.43.0", "@typescript-eslint/parser": "8.43.0", "@vitest/coverage-v8": "4.1.10", - "effect": "4.0.0-beta.107", + "effect": "4.0.0-rc.109", "eslint": "9.35.0", "eslint-config-prettier": "10.1.8", "fast-check": "4.9.0", @@ -72,7 +72,7 @@ "vitest": "4.1.10" }, "peerDependencies": { - "effect": "4.0.0-beta.107" + "effect": "^4.0.0-rc.109" }, "dependencies": { "fast-json-patch": "^3.1.1", diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index 9ba7ce0..a38e1b0 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -40,7 +40,7 @@ try { const packedPaths = new Set(files.map(({path}) => path)); for (const requiredPath of [ "README.md", - "docs/migration-v2.md", + "docs/migration-v3.md", "build/index.js", "build/index.d.ts", "package.json", @@ -75,16 +75,16 @@ try { "--ignore-scripts", "--no-package-lock", tarball, - "effect@4.0.0-beta.107", + "effect@4.0.0-rc.109", ], {cwd: fixture, stdio: "inherit", env: npmEnvironment}, ); const readmeExample = extractPackageSmokeExample("README.md"); - const migrationExample = extractPackageSmokeExample("docs/migration-v2.md"); + const migrationExample = extractPackageSmokeExample("docs/migration-v3.md"); writeFileSync(resolve(fixture, "README.example.ts"), readmeExample); writeFileSync( - resolve(fixture, "migration-v2.example.ts"), + resolve(fixture, "migration-v3.example.ts"), migrationExample, ); writeFileSync( @@ -98,7 +98,7 @@ try { noEmit: true, skipLibCheck: true, }, - include: ["README.example.ts", "migration-v2.example.ts"], + include: ["README.example.ts", "migration-v3.example.ts"], }), ); @@ -117,7 +117,7 @@ try { }); execFileSync( process.execPath, - [resolve(fixture, "migration-v2.example.ts")], + [resolve(fixture, "migration-v3.example.ts")], {cwd: fixture, stdio: "inherit"}, ); diff --git a/test/type-contracts.ts b/test/type-contracts.ts index 8b89ff9..55435e5 100644 --- a/test/type-contracts.ts +++ b/test/type-contracts.ts @@ -74,19 +74,19 @@ export type CombinedServicesContract = Expect< // @ts-expect-error Business schemas cannot own the reserved root marker. versioned(changelog)(Schema.Struct({_version: Schema.Number})); -// @ts-expect-error Arbitrary object codecs are outside the v2 interface. +// @ts-expect-error Arbitrary object codecs are outside the v3 interface. versioned(changelog)(Schema.Record(Schema.String, Schema.String)); class BusinessClass extends Schema.Class("BusinessClass")({ value: Schema.String, }) {} -// @ts-expect-error Schema classes are outside the v2 interface. +// @ts-expect-error Schema classes are outside the v3 interface. versioned(changelog)(BusinessClass); const StructWithRest = Schema.StructWithRest(Schema.Struct({}), [ Schema.Record(Schema.String, Schema.String), ]); -// @ts-expect-error Structs with rest records are outside the v2 interface. +// @ts-expect-error Structs with rest records are outside the v3 interface. versioned(changelog)(StructWithRest); // @ts-expect-error evolveAndDecode accepts only nominal VersionedSchema values.