Skip to content

feat: v1.0.0-alpha.0 — Stage-3 decorators rework#20

Closed
Zendrex wants to merge 13 commits into
mainfrom
release/v1.0.0-alpha.0
Closed

feat: v1.0.0-alpha.0 — Stage-3 decorators rework#20
Zendrex wants to merge 13 commits into
mainfrom
release/v1.0.0-alpha.0

Conversation

@Zendrex
Copy link
Copy Markdown
Owner

@Zendrex Zendrex commented Apr 26, 2026

Summary

First v1 alpha. Hard replace of the v0.x experimental-decorator surface — TC39 Stage-3 throughout, no reflect-metadata, branded cardinality-typed metadata keys, consolidated decorate.* / intercept.* namespaces, brand-driven reflector.

Squashed from 118 working commits on feat/stage3-decorators into 6 thematic commits + version bump:

  1. build: Stage-3 tsconfig + Symbol.metadata runtime + shim + helper-leak guard
  2. feat(metadata): WeakMap stores, branded keys, cardinality registry
  3. feat(errors): unified hierarchy via defineAnnotateError + formatSlot
  4. feat(factories): Stage-3 decorate.* / intercept.* with unique + list cardinality
  5. feat(reflector): WeakMap-backed brand-driven Reflector + ScopedReflector
  6. feat: public index.ts, integration tests, README rewrite, consolidated changeset
  7. chore: release v1.0.0-alpha.0 (changeset version bump)

Full breaking-change list and migration table in CHANGELOG.md / .changeset/stage3-alpha.md.

Test plan

  • bun run check-types clean (tsc -b)
  • bun test — 235/235 pass (37 files)
  • bunx ultracite check clean (86 files)
  • bun run build (tsdown) produces dist/index.{mjs,cjs,d.mts,d.cts} + dist/shim.{mjs,cjs,...}
  • bun run check:dist-helpers confirms no helper-package leaks in dist/
  • Manual smoke: install tarball into a Node ≥ 20.4 consumer, run a decorate.method + reflect() round-trip
  • Manual smoke: same in a TS 5.2 + experimentalDecorators: false project
  • Manual smoke: shim path — Node < 20.4 with import "@zendrex/annotate/shim"

Zendrex added 7 commits April 25, 2026 23:37
- tsconfig: enable Stage-3 (`useDefineForClassFields`,
  `experimentalDecorators` off), drop legacy emit
- Drop `reflect-metadata` peer dep; rely on host `Symbol.metadata`
  with cross-runtime fallback in `src/runtime/symbol-metadata.ts`
- Add `src/shim.ts` side-effect entry exposing `Symbol.metadata`
  for hosts without it; expose `./shim` subpath export with
  `sideEffects` declared
- Add `src/runtime/prepare.ts` (deferred-metadata flush) with
  WeakSet sentinel short-circuit and degraded-path guard
- Add `src/runtime/prototype-chain.ts` walker shared by reader
  helpers
- Add `scripts/check-helper-imports.ts` build guard against
  helper-package leaks in dist/
- Cover runtime + shim with unit tests and `shim-harness` fixture
- Replace single `store.ts` with focused modules:
  `class-meta-store` (one value per class+key, dup-detected) and
  `member-meta-store` (per-class WeakMap of member buckets with
  static flag folded into entries)
- Introduce `MetadataKey<T,C>` brand and `cardinality-registry` so
  reads/writes share one source of truth for unique vs list keys
- Add `metadata-deferred-queue` for instance-member metadata
  buffered until first `new`, drained by `runtime/prepare`
- Add `metadata-ctor-correlation` (bidirectional ctor↔metadata-bag
  WeakMaps) for ancestor-walking reads
- Add `prepared-sentinel` (WeakSet) so `prepare(ctor)` is idempotent
- Extract `append-guards` (`requireCardinality`,
  `assertNotDuplicate`) and `store-walk` (`readValues<T>` is the
  single trust boundary for `unknown[]`→`readonly T[]` cast) to
  share between class and member stores
- Add `has-any-meta` (single-pass chain probe) and `get-or-create`
  Map/WeakMap upsert helper
- Tighten `types.ts`: `MetadataKey` brand, `MemberEntry`,
  `MemberBucket`, `Deferred` shapes
- Full unit coverage for every new module
- Collapse five subclass bodies behind a single
  `defineAnnotateError` factory; each subclass now declares only
  its name + context shape
- Centralize slot rendering in `formatSlot` (handles symbol,
  anonymous, static-vs-instance, list-cardinality + label)
- New subclasses: `ValidationError`,
  `InvalidDecorationTargetError`, `MissingMetadataError`,
  `UnregisteredClassError`; `DuplicateMetadataError` carries
  cardinality + label, looks up cardinality dynamically
- `ValidationError` extracts message-aware reason from non-Error
  throws
- Tightened context-shape naming and the constructor's
  no-narration policy
- Full unit suite covers every subclass and the slot formatter
- Rewrite all four decorator/interceptor factories for TC39
  Stage-3 (`createClass/Method/Property/AccessorDecorator`,
  `createMethod/AccessorInterceptor`) with constraint generics:
  `TInstance`, `TMethod`, `TField`
- Auto-`prepare(ctor)` on `applied`/`appliedOwn`/`metadata`
  reader paths so deferred instance-member metadata flushes on
  first reflective read
- Mint metadata keys via `mintMetadataKey<T>` overloads
  (unique vs list cardinality), removing the old `unique` option
  and ad-hoc `generateKey`
- Add list-cardinality variants: `createList*Decorator/Interceptor`
  (inner-first ordering preserved end-to-end)
- Public surface consolidates onto `decorate.{class,method,
  property,accessor}[.list]` and
  `intercept.{method,accessor}[.list]` namespaces
- Extract `validator-chain` (composes per-decorator validators
  with `requireInstanceOf`/typed validator chain) and
  `validator-types` aliases
- `shared`: hoist `prepareFactoryShell`, `commitDecoration`
  envelope, label/compose plumbing; `compose` overloads for
  identity-tuple vs configured-mapper paths
- `types`: factories carry `FactoryGenerics`
  (`MetadataOf`/`ArgsOf`/`ThisOf`/`CardinalityOf`); `derive`
  drops redundant generics
- Drop `createParameterDecorator` (Stage-3 has no parameter
  primitive) and the standalone `property-interceptor`
  (subsumed by accessor-interceptor)
- Full unit suite per factory + cardinality variant + derive +
  validator-chain + shared
- Rewrite `Reflector` for WeakMap-backed reads (no host
  `Symbol.metadata` walking required for own-bag semantics)
- Brand-driven typing: `DecoratedMethod/Property/Class` split
  into `Unique` and `List` flavors that infer from
  `MetadataKey<T,C>` cardinality; `methods()`/`properties()`/
  `class()` and `all()` overloads narrow on brand
- Specialize factory readers; drop scalar reader methods +
  `Scalar` types now subsumed by `Unique`
- Add `ScopedReflector` overloads (cardinality-narrowing) and
  `createScopedReflector` brand inference
- Cache `ReflectorImpl` per ctor (perf) with covered tests
- Single-pass member snapshot (T2.3); document cardinality
  fallthrough; remove `DecoratedBase`
- `class-name` helper extracted for consistent slot rendering
- `resolve-instance` aligned to new reader path
- Unit suite: brand narrowing, reflect-cache, cross-cardinality
  rejection, scoped-reflector, name helper
- `src/index.ts`: Stage-3 public API barrel — `decorate.*` /
  `intercept.*` namespaces, `mintUniqueKey` / `mintListKey`,
  `prepare`, free `reflect`, `createScopedReflector`, branded
  `MetadataKey<T,C>`, all `Decorated*Unique` / `*List` types,
  full error class set
- README: rewrite for Stage-3 v1 — constraint generics, lazy
  prepare semantics, accessor-vs-property interception, list
  cardinality + Tag example, validator/`requireInstanceOf`
  fire-on-first-`new` semantics
- Integration suite: cross-class isolation, interceptor timing
  (decoration-order independence), introspection-semantics
  contract post-removal of `ensureProperty`, materialization +
  subclass regression, mixed unique+list cardinality through one
  reflector, reflect-instance rewrite for Stage-3 fixtures
- `tests/unit/types.test-d.ts`: consolidated type-level
  assertions covering factory generics, metadata key brand,
  reflector brand narrowing, scoped-reflector inference
- `.changeset/pre.json`: enter pre-release `alpha` mode, reset
  changesets array
- `.changeset/stage3-alpha.md`: single consolidated alpha.0
  changeset (supersedes the staged stage3-decorators /
  alpha-1-api-rework / cardinality-typed-keys entries)
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 26, 2026

Preview published — install with:

bun

bun add https://pkg.pr.new/@zendrex/annotate@1295c8c

npm

npm install https://pkg.pr.new/@zendrex/annotate@1295c8c

Built from 1295c8c

@Zendrex Zendrex mentioned this pull request Apr 26, 2026
Zendrex added 6 commits April 26, 2026 02:50
Moves the per-ctor WeakMap next to the class it backs, matching file-private cache placement in the class-design guideline. Behavior unchanged.
…lacement

Stage-3 field decorators that return a value-replacement initializer
closure are unsafe under Bun 1.3: the transformer emits a module-scope
`var _init = __decoratorStart(...)` per class, so multiple decorated
classes in one module shadow the binding and share addInit slots — only
the last-registered initializer survives.

intercept.field sidesteps this by resolving all per-decoration state
from `this.constructor` and a module-global factory registry inside an
addInitializer body. Field decorators never return a replacement
closure, so the Bun shadowing bug cannot bite. Companion
intercept.field.list ships for list-cardinality metadata.

`InterceptorContext.kind` gains `"field"`. New exported type
`FieldInterceptorOptions<TMeta, TArgs, TField>`.
Replace local makeInstanceReader/makeStaticReader with
createMemberMetadataReader from ./shared (already used by accessor
and method factories). Inline the trivial registerFieldInterceptor
Map.set wrapper. Drop the Bun-bug rationale duplication on
FieldInterceptorOptions; canonical explanation lives on
createFieldInterceptor.
…nstruction recovery

Added the applyFieldInterceptors function to enable idempotent recovery of field values after class construction. This function leverages a per-constructor cache to optimize performance and ensure correct behavior under Bun 1.3's addInit shadowing issue. Updated the intercept.field API to include this new functionality, enhancing the field decorator's capabilities. Also, added integration tests to validate the new behavior across multiple classes.
@Zendrex
Copy link
Copy Markdown
Owner Author

Zendrex commented May 19, 2026

Superseded by the dev branch.

@Zendrex Zendrex closed this May 19, 2026
@Zendrex Zendrex deleted the release/v1.0.0-alpha.0 branch May 19, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant