Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
3 changes: 0 additions & 3 deletions docs/adr/0007-release-v2-without-a-prerelease-line.md

This file was deleted.

3 changes: 3 additions & 0 deletions docs/adr/0007-release-v3-without-a-prerelease-line.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 10 additions & 8 deletions docs/migration-v2.md → docs/migration-v3.md
Original file line number Diff line number Diff line change
@@ -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))` |
Expand All @@ -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";
Expand All @@ -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";
Expand All @@ -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

Expand Down Expand Up @@ -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.
14 changes: 7 additions & 7 deletions docs/plans/effect-v4-rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -153,7 +153,7 @@ evolveAndDecode<E = never, R = never>(

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.

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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))`
Expand Down Expand Up @@ -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

Expand Down
57 changes: 10 additions & 47 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"!**/__mocks__/**",
"!**/*.json",
"README.md",
"docs/migration-v2.md"
"docs/migration-v3.md"
],
"engines": {
"node": ">=24"
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
12 changes: 6 additions & 6 deletions scripts/package-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand All @@ -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"],
}),
);

Expand All @@ -117,7 +117,7 @@ try {
});
execFileSync(
process.execPath,
[resolve(fixture, "migration-v2.example.ts")],
[resolve(fixture, "migration-v3.example.ts")],
{cwd: fixture, stdio: "inherit"},
);

Expand Down
6 changes: 3 additions & 3 deletions test/type-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>("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.
Expand Down
Loading