You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(clone): add CloneError class with code + cause
* feat(clone)!: throw CloneError on unsupported types
Replaces silent `undefined` return with `throw new CloneError('UNSUPPORTED_TYPE', ...)`
for functions, Promises, Intl.*, WeakMap, WeakSet, and constructor functions.
BREAKING CHANGE: previously these inputs returned `undefined`. Calls that relied on
that contract must now wrap in try/catch or filter inputs upstream.
* fix(clone): guard Buffer behind runtime check for browser bundles
Captures `Buffer` from `globalThis` at module init via a `BufferRef` constant. The
Buffer branch only runs when the runtime exposes Buffer; otherwise the path is
inert. The `node:buffer` import is type-only — no runtime side-effect on bundlers
that strip `node:` externals.
* feat(clone): add cycles, preservePrototype, copyDescriptors options
Three new opt-out flags on `CloneOptions`, all default true so existing callers see
no behavior change. Composing all three `false` gives a fast plain-JSON clone path.
- `cycles: false` skips the WeakMap visited cache. Caller asserts no cycles.
- `preservePrototype: false` flattens custom objects to plain objects.
- `copyDescriptors: false` uses `for...in` + assign for plain objects, drops
symbol keys and non-enumerable descriptors. Errors keep message + name only;
boxed wrappers keep their value only.
* perf(clone): drop typedArrays Set and short-circuit plain-object prototype
- TypedArray dispatch now uses one `ArrayBuffer.isView` call instead of a
9-entry Set lookup, after DataView and Buffer have already been claimed.
- Plain-object path skips `Object.create(Object.prototype)` and uses a literal,
which V8 compiles into a faster object-creation map for the most common case.
- `getType` returns a tighter `AnyConstructor` union instead of `unknown`.
* test(clone): add fast-check property suite + fix null-prototype throw
Property tests caught a real bug: clone(Object.create(null)) threw CloneError
because the constructor lookup returned undefined for null-prototype objects.
The unsupported-type check now triggers only on functions and explicitly-listed
constructors, so null-prototype objects fall through to the plain-object path
and are cloned with their null prototype preserved.
Restructure tests to match sparkline's "one spec per source module" convention:
fold cycle.test.ts into clone.test.ts and freeze.test.ts under describe('cycles')
blocks; drop cycle.test.ts.
Adds fast-check as a devDependency.
* test(clone): add mitata bench vs structuredClone, lodash, rfdc, fast-copy
`bench/clone.bench.mjs` runs five fixture buckets — flat-10, nested-100,
large-1000, with-cycles, class-instances — across the in-package `clone`
(default and fast variants) and the four established cloners. `bench/baseline.md`
captures the Apple M1 / Node 22.22.2 numbers as the 1.0.0 baseline with a
10 percent regression budget going forward.
Adds `mitata`, `lodash.clonedeep`, `rfdc`, `fast-copy`, and the lodash types
as devDependencies; adds `pnpm bench` script.
* ci(clone): add ci.yml calling coroboros/ci@v0 reusable workflow
* docs(clone): rewrite README with optim API + comparison rationale + CI badge
* docs(clone): update CLAUDE.md with bench, fast-check, regression budget, CI rule
* docs(clone): tighten brand-voice compliance (active voice, sentence length)
Copy file name to clipboardExpand all lines: CHANGELOG.md
+16Lines changed: 16 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -3,3 +3,19 @@
3
3
## v1.0.0 - 15/05/2026
4
4
5
5
Initial release of `@coroboros/clone`.
6
+
7
+
### Features
8
+
-`clone<T>(thing, options?)` — deep clone preserving the prototype chain, property descriptors, boxed primitives, native types (`Map`, `Set`, `Date`, `RegExp`, `Error` subclasses, `TypedArray`, `DataView`, `ArrayBuffer`, `Buffer`), and null-prototype objects. Cycle-safe via a `WeakMap` visited cache.
9
+
-`freeze<T>(thing)` — recursive deep freeze with a `WeakSet` cycle guard. Skips `ArrayBufferView` instances (`TypedArray`, `DataView`, `Buffer`) since `Object.freeze` throws on them.
10
+
- Three granular opt-out flags on `CloneOptions`: `cycles`, `preservePrototype`, `copyDescriptors`. All default `true`. Composing all three `false` gives a fast plain-JSON path competitive with `rfdc`.
11
+
-`CloneError` with `code: 'UNSUPPORTED_TYPE'` and `Error.cause` support. `clone` throws it on functions, `Promise`, `Intl.*`, `WeakMap`, `WeakSet`, and constructor functions.
12
+
- Browser-safe — the Buffer branch reads `globalThis.Buffer` at module init, so the package works in non-Node bundles.
13
+
14
+
### Documentation
15
+
- README ships with a full API reference, the comparison table vs `structuredClone` / `lodash.cloneDeep` / `rfdc` / `fast-copy`, and the rationale for the prototype + descriptor preservation contract.
16
+
-`bench/baseline.md` documents the 1.0.0 numbers across five fixture buckets (flat-10, nested-100, large-1000, with-cycles, class-instances) on Apple M1 / Node 22.22.2.
17
+
18
+
### Configuration
19
+
- TypeScript strict, ES modules + CJS dual build via `tsdown` targeting Node 22 LTS. Zero runtime dependencies.
20
+
-`mitata` benchmark suite via `pnpm bench`. `fast-check` property-test suite alongside the unit tests.
21
+
-`.github/workflows/ci.yml` calls the `coroboros/ci@v0` reusable workflow with OIDC Trusted Publisher and `npm provenance`.
-**NEVER** break the public API above. The signatures and semantics are the 1.0.0 contract.
36
43
-**NEVER** add a runtime dependency. Zero-dep is a feature.
37
-
-**NEVER** export `exists`, `is`, `getType` — they are internal helpers only.
44
+
-**NEVER** export `exists`, `getType`, `primitives` — they are internal helpers only.
38
45
-**NEVER** use `axios`, `request`, or `node-fetch` — use native `fetch` (Node 22+).
39
-
- Cycle handling via WeakMap visited-cache is contract — never remove.
46
+
- Cycle handling via WeakMap visited-cache is contract — never remove the default.
40
47
- Run `pnpm lint && pnpm typecheck && pnpm test` before every commit.
48
+
- Run `pnpm bench` against `bench/baseline.md` when touching `src/clone.ts` — no regression > 10 % at fixed feature set.
41
49
- Scoped package — `publishConfig.access = "public"` is mandatory, do not remove.
50
+
-**Git** — branch `main`; CI owns `npm publish` exclusively (tag-push triggers `ci.yml` → reusable workflow `coroboros/ci/.github/workflows/javascript-npm-packages.yml@v0` → OIDC Trusted Publisher with `npm provenance`, never manual — manual bypasses attestation and the pre-publish gates); run `pnpm lint && pnpm typecheck && pnpm test && pnpm build` locally before tagging; tag MUST equal `package.json` version (the reusable workflow pins `package.json` to the tag automatically); bump via `pnpm version patch|minor`; release body in `gh release create` stays minimal (no install snippet unless the install command changed). All other rules in `@~/.claude/rules/git-conventions.md` apply.
@@ -17,6 +18,12 @@ Deep clones objects while preserving the prototype chain, property descriptors,
17
18
18
19
</div>
19
20
21
+
## Why this exists
22
+
23
+
`structuredClone` ships in every modern runtime. It strips the prototype chain, so class instances come back as plain objects. It drops property descriptors — non-enumerable fields, accessors, and `configurable: false` flags vanish. Boxed primitives throw. ORM entities, builders, event emitters, frozen state objects, and any custom-constructed value lose information when round-tripped.
24
+
25
+
`@coroboros/clone` keeps all three. Three opt-out flags trade those guarantees for speed on plain JSON-shaped data, landing in `rfdc`-grade territory without switching libraries.
26
+
20
27
## Requirements
21
28
22
29
- Node.js `>=22` LTS. Use [fnm](https://github.com/Schniz/fnm) for version management — Rust-based, faster than nvm.
@@ -88,10 +95,13 @@ The clone preserves the prototype chain, property descriptors (including non-enu
|`thing`|`T`|*(required)*| Value to clone. Any JavaScript value or object. |
101
+
|`options.ignoreUndefinedProperties`|`boolean`|`false`| When `true`, omit properties whose value is `undefined`. Recursive. |
102
+
|`options.cycles`|`boolean`|`true`| When `false`, skips the WeakMap visited cache. Caller asserts no cycles. Faster, infinite-recursion if wrong. |
103
+
|`options.preservePrototype`|`boolean`|`true`| When `false`, custom objects flatten to plain `{}` (lose `instanceof` and method inheritance). |
104
+
|`options.copyDescriptors`|`boolean`|`true`| When `false`, plain objects skip `Reflect.ownKeys` + descriptor walk. Symbol keys and non-enumerable fields drop. Errors keep `message` + `name` only; boxed wrappers keep their value only. |
95
105
96
106
**Returns** — a deep copy of `thing`, typed as `T`.
97
107
@@ -105,11 +115,12 @@ Native types clone with type-specific semantics:
105
115
-`RegExp` — `source` and `flags` preserved.
106
116
-`TypedArray` (`Int8Array` through `Float64Array`) — cloned via the constructor.
107
117
-`DataView` — buffer copied; `byteOffset` and `byteLength` preserved.
108
-
-`Buffer` — bytes copied via `Buffer.allocUnsafe` and `Buffer#copy`.
118
+
-`Buffer` — bytes copied via `Buffer.allocUnsafe` and `Buffer#copy`. Browser bundles skip this branch via a runtime guard; the type is Node-only.
109
119
-`ArrayBuffer` — sliced.
110
120
-`Error` and subclasses (`EvalError`, `RangeError`, `ReferenceError`, `SyntaxError`, `TypeError`, `URIError`) — own properties copied with full descriptors.
|`Error` subclasses with descriptors | partial | no | no | no | yes |
180
221
| Functions, Promises, `WeakMap`, `WeakSet`| no | no | no | no | no (by design) |
181
222
182
-
The market gap is the prototype chain plus property descriptors. Class instances cloned with `structuredClone`lose their prototype and become plain objects. `lodash.cloneDeep` drops descriptor flags. ORM entities, builders, event emitters, and any custom-constructed state object stay intact through `clone`.
223
+
The market gap is the prototype chain plus property descriptors. `structuredClone`strips the prototype from class instances; they return as plain objects. `lodash.cloneDeep` drops descriptor flags. ORM entities, builders, event emitters, and any custom-constructed state object stay intact through `clone`.
183
224
184
225
## Contributing
185
226
@@ -188,6 +229,7 @@ Bug reports and PRs welcome.
188
229
- Open an issue before submitting non-trivial PRs.
0 commit comments