@vicin/sigil 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.4.0] - 2026-02-28
6
+
7
+ ### Changed
8
+
9
+ - Updated internal logic to handle edge-cases
10
+ - Edge cases part added to tests
11
+ - Updated README.md
12
+
13
+ ### Added
14
+
15
+ - `AttachSigil` / `attachSigil` which are `WithSigil` / `withSigil` renamed for clarity
16
+
17
+ ### Deprecated
18
+
19
+ - `WithSigil` / `withSigil` renamed for clarity. old names will be removed in v4
20
+ - `SigilLabelSet` / `getSigilLabelSet` methods to minimize api surface and bundle size as they are redundant (internall 'new Set(this.SigilLabelLineage)' only). will be removed in v4
21
+
5
22
  ## [3.3.0] - 2026-02-27
6
23
 
7
24
  ### Changed
@@ -27,7 +44,7 @@ All notable changes to this project will be documented in this file.
27
44
 
28
45
  ### Deprecated
29
46
 
30
- - `DEFAULT_LABEL_REGEX` is deprecated and will be removed in v4, use `RECOMMENDED_LABEL_REGEX` instead
47
+ - `DEFAULT_LABEL_REGEX` is deprecated use `RECOMMENDED_LABEL_REGEX` instead, will be removed in v4
31
48
 
32
49
  ## [3.1.4] - 2026-02-26
33
50
 
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  ## Features
11
11
 
12
- - ✅ **Drop-in `instanceof` replacement** that works across bundles, HMR and monorepos, Also add check for **exact class instance**
12
+ - ✅ **Drop-in `instanceof` replacement** that works across bundles, HMR and monorepos, and adds an **exact-class-instance check**
13
13
  - ✅ **Simple nominal typing** with just one line of code for each class (e.g., `UserId` vs. `PostId`)
14
14
  - ✅ **Tiny less than 1.6 KB minified and brotlied** measured using [size-limit](https://www.npmjs.com/package/size-limit)
15
15
  - ✅ **Performant as native instanceof** but with guaranteed checks
@@ -24,7 +24,7 @@
24
24
  - [Install](#install)
25
25
  - [Basic usage](#basic-usage)
26
26
  - [Decorator pattern](#decorator-pattern)
27
- - [HOF pattern](#hof-higher-order-function-pattern)
27
+ - [Function pattern](#function-pattern)
28
28
  - [Migration](#migration)
29
29
  - [Core concepts](#core-concepts)
30
30
  - [Terminology](#terminology)
@@ -37,6 +37,7 @@
37
37
  - [Minimal mode](#minimal-mode)
38
38
  - [Strict mode](#strict-mode)
39
39
  - [Hot module reload](#hot-module-reload)
40
+ - [Edge cases](#edge-cases)
40
41
  - [Benchmarks](#benchmarks)
41
42
  - [Bundle Size](#bundle-size)
42
43
  - [Tests](#tests)
@@ -58,7 +59,9 @@ yarn add @vicin/sigil
58
59
  pnpm add @vicin/sigil
59
60
  ```
60
61
 
61
- Requires TypeScript 5.0+ for decorators; HOFs work on older versions. Node.js 18+ recommended.
62
+ Requires TypeScript 5.0+ for decorators; attach functions work on older versions. Node.js 18+ recommended.
63
+
64
+ > No tsconfig changes are needed as we use **Stage 3 decorators** which are supported by default in TypeScript 5.0+
62
65
 
63
66
  ### Basic usage
64
67
 
@@ -92,26 +95,28 @@ After opting into the `Sigil` contract, labels are passed to child classes to un
92
95
 
93
96
  ##### Decorator pattern
94
97
 
95
- Apply a label with the `@WithSigil` decorator:
98
+ Apply a label with the `@AttachSigil` decorator:
96
99
 
97
100
  ```ts
98
- import { Sigil, WithSigil } from '@vicin/sigil';
101
+ import { Sigil, AttachSigil } from '@vicin/sigil';
99
102
 
100
- @WithSigil('@myorg/mypkg.User')
103
+ @AttachSigil('@myorg/mypkg.User')
101
104
  class User extends Sigil {}
102
105
  ```
103
106
 
104
- ##### HOF (Higher-Order Function) pattern
107
+ ##### Function pattern
105
108
 
106
- Apply a label using `withSigil` HOF:
109
+ Apply a label using `attachSigil` function:
107
110
 
108
111
  ```ts
109
- import { Sigil, withSigil } from '@vicin/sigil';
112
+ import { Sigil, attachSigil } from '@vicin/sigil';
110
113
 
111
- class _User extends Sigil {}
112
- const User = withSigil(_User, '@myorg/mypkg.User');
114
+ class User extends Sigil {}
115
+ attachSigil(User, '@myorg/mypkg.User');
113
116
  ```
114
117
 
118
+ > Note: Function pattern is susceptible to some [Edge case subtle pitfalls](#edge-cases) if not used appropriately, so we advise to use decorator pattern
119
+
115
120
  ### Migration
116
121
 
117
122
  Migrating old code into `Sigil` can be done with extra couple lines of code only:
@@ -163,9 +168,9 @@ if (User.isOfType(obj)) { ... } // This still works even if User was bundled twi
163
168
  if (User.isExactType(obj)) { ... } // Or check for exactly same constructor not its children
164
169
  ```
165
170
 
166
- Also by utilizing unique passed labels it solve another problem in Domain-Driven Design (DDD):
171
+ Also by utilizing unique passed labels it solves another problem in Domain-Driven Design (DDD):
167
172
 
168
- - **Manual Branding Overhead:** Custom identifiers lead to boilerplate and maintenance issues, `Sigil` add reliable inheritance-aware nominal branding with just one line of code.
173
+ - **Manual Branding Overhead:** Custom identifiers lead to boilerplate and maintenance issues, `Sigil` adds reliable inheritance-aware nominal branding with just one line of code.
169
174
 
170
175
  ```ts
171
176
  import { sigil } from '@vicin/sigil';
@@ -181,43 +186,40 @@ type test2 = Sigil extends User ? true : false; // false
181
186
  ### Implementation Mechanics
182
187
 
183
188
  - **Runtime Contract:** Established via extending `Sigil` or using `Sigilify` mixin.
184
- - **Update metadata:** With each new child, use decorators or HOF to attach run-time metadata and `ExtendSigil` to update nominal type.
189
+ - **Update metadata:** With each new child, use decorator (`AttachSigil`) or function (`attachSigil`) to attach run-time metadata, also use `ExtendSigil` on `[sigil]` field to update nominal type.
185
190
 
186
191
  ```ts
187
- import { Sigil, WithSigil, sigil, ExtendSigil } from '@vicin/sigil';
192
+ import { Sigil, AttachSigil, sigil, ExtendSigil } from '@vicin/sigil';
188
193
 
189
- @WithSigil('@scope/package.MyClass') // <-- Run-time values update
194
+ @AttachSigil('@scope/package.MyClass') // <-- Run-time values update
190
195
  class MyClass extends Sigil {
191
196
  declare [sigil]: ExtendSigil<'MyClass', Sigil>; // <-- compile-time type update
192
197
  }
193
198
  ```
194
199
 
195
- You can avoid decorators and use HOF if needed:
200
+ You can avoid decorators and use normal functions if needed:
196
201
 
197
202
  ```ts
198
- import { Sigil, withSigil, sigil, ExtendSigil } from '@vicin/sigil';
203
+ import { Sigil, attachSigil, sigil, ExtendSigil } from '@vicin/sigil';
199
204
 
200
- class _MyClass extends Sigil {
205
+ class MyClass extends Sigil {
201
206
  declare [sigil]: ExtendSigil<'MyClass', Sigil>;
202
207
  }
203
208
 
204
- const MyClass = withSigil(_MyClass, '@scope/package.MyClass');
205
- type MyClass = InstanceType<typeof MyClass>;
209
+ attachSigil(MyClass, '@scope/package.MyClass');
206
210
  ```
207
211
 
208
- Note that you can't use `InstanceType` on `private` or `protected` classes, however you can use `GetPrototype<T>` in such cases.
209
-
210
212
  ### Example
211
213
 
212
214
  ```ts
213
- import { Sigil, WithSigil } from '@vicin/sigil';
215
+ import { Sigil, AttachSigil } from '@vicin/sigil';
214
216
 
215
- @WithSigil('@myorg/User')
217
+ @AttachSigil('@myorg/User')
216
218
  class User extends Sigil {
217
219
  declare [sigil]: ExtendSigil<'User', Sigil>;
218
220
  }
219
221
 
220
- @WithSigil('@myorg/Admin')
222
+ @AttachSigil('@myorg/Admin')
221
223
  class Admin extends User {
222
224
  declare [sigil]: ExtendSigil<'Admin', User>;
223
225
  }
@@ -248,51 +250,40 @@ type test1 = Admin extends User ? true : false; // true
248
250
  type test2 = User extends Admin ? true : false; // false
249
251
 
250
252
  // Passed label must be unique (enforced by Sigil) so can be used as stable Id for class
251
- // Also 'SigilLabelLineage' and 'SigilLabelSet' are useful for logging & debugging
253
+ // Also 'SigilLabelLineage' is useful for logging & debugging
252
254
  console.log(Admin.SigilLabel); // '@myorg/Admin'
253
255
  console.log(Admin.SigilEffectiveLabel); // '@myorg/Admin'
254
256
  console.log(Admin.SigilLabelLineage); // ['Sigil', '@myorg/User', '@myorg/Admin']
255
- console.log(Admin.SigilLabelSet); // Set(['Sigil', '@myorg/User', '@myorg/Admin'])
256
257
  console.log(admin.getSigilLabel()); // '@myorg/Admin'
257
258
  console.log(admin.getSigilEffectiveLabel()); // '@myorg/Admin'
258
259
  console.log(admin.getSigilLabelLineage()); // ['Sigil', '@myorg/User', '@myorg/Admin']
259
- console.log(admin.getSigilLabelSet()); // Set(['Sigil', '@myorg/User', '@myorg/Admin'])
260
260
  ```
261
261
 
262
262
  ### Errors & throws
263
263
 
264
264
  Run-time errors that can be thrown by `Sigil`:
265
265
 
266
- #### Double `Sigilify() / SigilifyAbstract()`
266
+ #### Double Sigilify
267
267
 
268
268
  ```ts
269
269
  class A {}
270
- const B = Sigilify(A, 'A');
271
- const C = Sigilify(B, 'B'); // Throws: [Sigil Error] Class 'Sigilified' with label 'A' is already sigilified
270
+ Sigilify(Sigilify(A, 'A'), 'B'); // Throws: [Sigil Error] Class 'Sigilified' with label 'A' is already sigilified
272
271
 
273
- abstract class AbsA {}
274
- const AbsB = SigilifyAbstract(AbsA, 'AbsA');
275
- const AbsC = SigilifyAbstract(AbsB, 'AbsB'); // Throws: [Sigil Error] Class 'Sigilified' with label 'AbsA' is already sigilified
276
- ```
277
-
278
- #### `@WithSigil() / withSigil()` on non-Sigil class
279
-
280
- ```ts
281
- @WithSigil('A')
282
- class A {} // Throws: [Sigil Error] 'WithSigil' decorator accept only Sigil classes but used on class 'A'
272
+ @AttachSigil('B')
273
+ @AttachSigil('A')
274
+ class A extends Sigil {} // Throws: [Sigil Error] Class 'A' with label 'A' is already sigilified
283
275
 
284
- withSigil(class A {}); // Throws: [Sigil Error] 'WithSigil' decorator accept only Sigil classes but used on class 'A'
276
+ class A extends Sigil {}
277
+ attachSigil(attachSigil(A, 'A'), 'B'); // Throws: [Sigil Error] Class 'A' with label 'A' is already sigilified
285
278
  ```
286
279
 
287
- #### Double `@WithSigil() / withSigil()`
280
+ #### `@AttachSigil() / attachSigil()` on non-Sigil class
288
281
 
289
282
  ```ts
290
- @WithSigil('B')
291
- @WithSigil('A')
292
- class A extends Sigil {} // Throws: [Sigil Error] Class 'A' with label 'A' is already sigilified
283
+ @AttachSigil('A') // Throws: [Sigil Error] 'AttachSigil' decorator accept only Sigil classes but used on class 'A'
284
+ class A {}
293
285
 
294
- class _A extends Sigil {}
295
- withSigil(withSigil(_A, 'A'), 'B'); // Throws: [Sigil Error] Class 'A' with label 'A' is already sigilified
286
+ attachSigil(class A {}); // Throws: [Sigil Error] 'AttachSigil' function accept only Sigil classes but used on class 'A'
296
287
  ```
297
288
 
298
289
  #### No label is passed with `autofillLabels: false`
@@ -307,10 +298,10 @@ new A(); // Throws: [Sigil Error] Class 'A' is not sigilified, Make sure to sigi
307
298
  #### Same label is passed twice to `Sigil`
308
299
 
309
300
  ```ts
310
- @WithSigil('Label')
301
+ @AttachSigil('Label')
311
302
  class A extends Sigil {}
312
303
 
313
- @WithSigil('Label')
304
+ @AttachSigil('Label')
314
305
  class B extends Sigil {} // Throws: [Sigil Error] Passed label 'Label' to class 'B' is re-used, passed labels must be unique
315
306
  ```
316
307
 
@@ -319,15 +310,15 @@ class B extends Sigil {} // Throws: [Sigil Error] Passed label 'Label' to class
319
310
  ```ts
320
311
  updateSigilOptions({ labelValidation: RECOMMENDED_LABEL_REGEX });
321
312
 
322
- @WithSigil('InvalidLabel')
313
+ @AttachSigil('InvalidLabel')
323
314
  class A extends Sigil {} // Throws: [Sigil Error] Invalid Sigil label 'InvalidLabel'. Make sure that supplied label matches validation regex or function
324
315
  ```
325
316
 
326
317
  #### Using '@Sigil-auto' prefix
327
318
 
328
319
  ```ts
329
- @WithSigil('@Sigil-auto:label')
330
- class X extends Sigil {} // Throws: '@Sigil-auto' is a prefex reserved by the library
320
+ @AttachSigil('@Sigil-auto:label')
321
+ class X extends Sigil {} // Throws: '@Sigil-auto' is a prefix reserved by the library
331
322
  ```
332
323
 
333
324
  #### Invalid options passed to `updateOptions`
@@ -353,10 +344,10 @@ updateSigilOptions({ skipLabelUniquenessCheck: 'str' as any }); // Throws: 'upda
353
344
  - `SigilError`
354
345
 
355
346
  - **Decorator:**
356
- - `WithSigil(label, opts?)`
347
+ - `AttachSigil(label, opts?)`
357
348
 
358
- - **HOFs:**
359
- - `withSigil(Class, label, opts?)`
349
+ - **Attach function:**
350
+ - `attachSigil(Class, label, opts?)`
360
351
 
361
352
  - **Helpers:**
362
353
  - `isSigilCtor(ctor)`
@@ -382,8 +373,8 @@ updateSigilOptions({ skipLabelUniquenessCheck: 'str' as any }); // Throws: 'upda
382
373
  - `SigilError`: an `Error` class decorated with a `Sigil` so it can be identified at runtime.
383
374
  - `Sigilify(Base, label, opts?)`: mixin function that returns a new constructor with `Sigil` types and instance helpers.
384
375
  - `SigilifyAbstract(Base, label, opts?)`: Same as `Sigilify` but for abstract classes.
385
- - `WithSigil(label, opts?)`: class decorator that attaches `Sigil` metadata at declaration time.
386
- - `withSigil(Class, label, opts?)`: HOF that validates and decorates an existing class constructor.
376
+ - `AttachSigil(label, opts?)`: class decorator that attaches `Sigil` metadata at declaration time.
377
+ - `attachSigil(Class, label, opts?)`: function that validates and decorates an existing class constructor.
387
378
  - `isSigilCtor(value)`: `true` if `value` is a `Sigil` constructor.
388
379
  - `isSigilInstance(value)`: `true` if `value` is an instance of a `Sigil` constructor.
389
380
  - `getSigilLabels()`: Get `Sigil` labels registered.
@@ -397,7 +388,6 @@ When a constructor is sigilified it will expose the following **static** getters
397
388
  - `SigilLabel` — the identity label string.
398
389
  - `SigilEffectiveLabel` — the human label string.
399
390
  - `SigilLabelLineage` — readonly array of labels representing parent → child for debugging.
400
- - `SigilLabelSet` — readonly `Set<string>` of sigil labels for debugging.
401
391
  - `isOfType(other)` — check if other is an instance of this constructor or its children.
402
392
  - `isExactType(other) `— check if other is an instance exactly this constructor.
403
393
 
@@ -406,7 +396,6 @@ Instances of sigilified classes expose instance helpers:
406
396
  - `getSigilLabel()` — returns the identity label.
407
397
  - `getSigilEffectiveLabel()` — returns the human label.
408
398
  - `getSigilLabelLineage()` — returns lineage array.
409
- - `getSigilLabelSet()` — returns readonly Set.
410
399
  - `isOfType(other)` — check if other is an instance of the same class or its children as this.
411
400
  - `isExactType(other) `— check if other is an instance exactly the same constructor.
412
401
 
@@ -426,18 +415,18 @@ updateSigilOptions({
426
415
  });
427
416
  ```
428
417
 
429
- Values defined in previous example are defaults, per-class overrides available in mixin, decorators, and HOFs.
418
+ Values defined in previous example are defaults, per-class overrides available in mixin and attach function / decorator.
430
419
 
431
420
  ---
432
421
 
433
422
  ## Minimal mode
434
423
 
435
- By default `Sigil` works with minimal mode, You can ignore all decorators and HOFs and just make base class extend `Sigil`:
424
+ By default `Sigil` works with minimal mode, You can ignore all decorators and functions and just make base class extend `Sigil`:
436
425
 
437
426
  ```ts
438
427
  import { Sigil, updateSigilOptions } from '@vicin/sigil';
439
428
 
440
- // No decorators or HOF needed to use 'isOfType' ('instanceof' replacement)
429
+ // No decorators or functions needed to use 'isOfType' ('instanceof' replacement)
441
430
  class A extends Sigil {}
442
431
  class B extends A {}
443
432
  class C extends B {}
@@ -468,11 +457,11 @@ import { updateSigilOptions } from '@vicin/sigil';
468
457
  updateSigilOptions({ skipLabelUniquenessCheck: true });
469
458
  ```
470
459
 
471
- But this can cause bugs if same label is used for two different classes as checks are disables globally.
460
+ But this can cause unexpected behavior if same label is used for two different classes as checks are disabled globally.
472
461
  If you need more strict mode you can pass this options to the re-loaded class only:
473
462
 
474
463
  ```ts
475
- @WithSigil('HmrClassLabel', { skipLabelUniquenessCheck: true })
464
+ @AttachSigil('HmrClassLabel', { skipLabelUniquenessCheck: true })
476
465
  class HmrClass extends Sigil {}
477
466
  ```
478
467
 
@@ -480,6 +469,57 @@ With this approach `skipLabelUniquenessCheck` affects only `HmrClass`, and if `H
480
469
 
481
470
  ---
482
471
 
472
+ ## Edge cases
473
+
474
+ ### Accessing Sigil `metadata` before running 'attachSigil' function
475
+
476
+ If you didn't make sure that `attachSigil` runs right after class declaration and used one of `Sigil` methods this will occur:
477
+
478
+ ```ts
479
+ class A extends Sigil {}
480
+
481
+ console.log(A.SigilLabel); // returns auto-generated label (e.g. @Sigil-auto:A:6:a3f15bhl) or throws in strict mode
482
+
483
+ attachSigil(A, 'A');
484
+
485
+ console.log(A.SigilLabel); // A
486
+ ```
487
+
488
+ To avoid this bug entirely you can use the return of `attachSigil` in your code so you are enforced to respect order:
489
+
490
+ ```ts
491
+ class _A extends Sigil {}
492
+
493
+ const A = attachSigil(_A, 'A');
494
+ type A = InstanceType<typeof A>;
495
+
496
+ console.log(A.SigilLabel); // A
497
+ ```
498
+
499
+ Note that you can't use `InstanceType` on `private` or `protected` classes, however you can use `GetPrototype<T>` in such cases.
500
+
501
+ #### Static blocks & IIFE static initializer
502
+
503
+ Decorators ensure that metadata is appended before static blocks or IIFE static initializers, however `attachSigil` function runs after them so accessing label inside them will return auto-generated label or throw:
504
+
505
+ ```ts
506
+ class A extends Sigil {
507
+ static IIFE = (() => {
508
+ const label = A.SigilLabel; // returns auto-generated label (e.g. @Sigil-auto:A:6:a3f15bhl) or throws in strict mode
509
+ })();
510
+
511
+ static {
512
+ const label = this.SigilLabel; // returns auto-generated label (e.g. @Sigil-auto:A:6:a3f15bhl) or throws in strict mode
513
+ }
514
+ }
515
+
516
+ attachSigil(A, 'A');
517
+ ```
518
+
519
+ This behavior can't be avoided, so make sure not to call any `Sigil` method inside them or move to decorators (`@AttachSigil`)
520
+
521
+ ---
522
+
483
523
  ## Benchmarks
484
524
 
485
525
  Sigil is built for **real-world performance**. Below are the latest micro-benchmark results (run on **Node.js v20.12.0**).
@@ -529,7 +569,7 @@ npm run bench
529
569
 
530
570
  ## Bundle Size
531
571
 
532
- **Less than 1.6 KB (1.54 KB)** (minified + Brotli, including all dependencies)
572
+ **Less than 1.6 KB (1.52 KB)** (minified + Brotli, including all dependencies)
533
573
 
534
574
  This makes Sigil one of the smallest full-featured solutions for nominal typing + reliable runtime identity.
535
575
 
@@ -546,7 +586,7 @@ npm run size
546
586
 
547
587
  ## Tests
548
588
 
549
- Reliability is a core pillar of `Sigil`. The library is backed by a comprehensive suite of unit tests that cover everything from basic mixins to lazy evaluation.
589
+ Reliability is a core pillar of `Sigil`. The library is backed by a comprehensive suite of unit tests ( 71 total tests ) that cover everything from basic mixins to edge cases.
550
590
 
551
591
  **Coverage Status**
552
592
 
@@ -561,11 +601,12 @@ We maintain **100%** test coverage across the entire codebase to ensure that run
561
601
 
562
602
  **Key Test Areas**
563
603
 
564
- - **Mixins, Decorators & HOFs:** Validating `Sigilify`, `WithSigil` and `withSigil` behaviors.
604
+ - **Mixins, Attach function & decorator:** Validating `Sigilify`, `AttachSigil` and `attachSigil` behaviors.
565
605
  - **Sigil methods:** Ensuring `Sigil` class methods (e.g. `SigilLabel`, `getSigilLabel`) work as expected.
566
- - **Lazy Evaluation:** Ensuring metadata is attached before being accessed via `Sigil` methods.
606
+ - **Lazy Evaluation:** Ensuring metadata is attached before being accessed via `Sigil` methods even when no attach function or decorator is used.
567
607
  - **Lineage:** Verifying that `isOfType` and `isExactType` work across complex inheritance chains.
568
608
  - **Error Handling:** Strict validation for all errors and throws.
609
+ - **Edge cases**: Known edge cases.
569
610
 
570
611
  **Running Tests**
571
612