@vicin/sigil 1.0.1 → 1.1.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.
Files changed (53) hide show
  1. package/README.md +157 -37
  2. package/dist/index.d.mts +777 -0
  3. package/dist/index.d.ts +777 -3
  4. package/dist/index.js +701 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +684 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +7 -4
  9. package/dist/core/classes.d.ts +0 -48
  10. package/dist/core/classes.d.ts.map +0 -1
  11. package/dist/core/classes.js +0 -18
  12. package/dist/core/classes.js.map +0 -1
  13. package/dist/core/decorator.d.ts +0 -28
  14. package/dist/core/decorator.d.ts.map +0 -1
  15. package/dist/core/decorator.js +0 -48
  16. package/dist/core/decorator.js.map +0 -1
  17. package/dist/core/enhancers.d.ts +0 -58
  18. package/dist/core/enhancers.d.ts.map +0 -1
  19. package/dist/core/enhancers.js +0 -101
  20. package/dist/core/enhancers.js.map +0 -1
  21. package/dist/core/helpers.d.ts +0 -192
  22. package/dist/core/helpers.d.ts.map +0 -1
  23. package/dist/core/helpers.js +0 -349
  24. package/dist/core/helpers.js.map +0 -1
  25. package/dist/core/index.d.ts +0 -9
  26. package/dist/core/index.d.ts.map +0 -1
  27. package/dist/core/index.js +0 -8
  28. package/dist/core/index.js.map +0 -1
  29. package/dist/core/mixin.d.ts +0 -115
  30. package/dist/core/mixin.d.ts.map +0 -1
  31. package/dist/core/mixin.js +0 -209
  32. package/dist/core/mixin.js.map +0 -1
  33. package/dist/core/options.d.ts +0 -74
  34. package/dist/core/options.d.ts.map +0 -1
  35. package/dist/core/options.js +0 -39
  36. package/dist/core/options.js.map +0 -1
  37. package/dist/core/registry.d.ts +0 -104
  38. package/dist/core/registry.d.ts.map +0 -1
  39. package/dist/core/registry.js +0 -174
  40. package/dist/core/registry.js.map +0 -1
  41. package/dist/core/symbols.d.ts +0 -96
  42. package/dist/core/symbols.d.ts.map +0 -1
  43. package/dist/core/symbols.js +0 -96
  44. package/dist/core/symbols.js.map +0 -1
  45. package/dist/core/types.d.ts +0 -169
  46. package/dist/core/types.d.ts.map +0 -1
  47. package/dist/core/types.js +0 -2
  48. package/dist/core/types.js.map +0 -1
  49. package/dist/index.d.ts.map +0 -1
  50. package/dist/utils/index.d.ts +0 -2
  51. package/dist/utils/index.d.ts.map +0 -1
  52. package/dist/utils/index.js +0 -2
  53. package/dist/utils/index.js.map +0 -1
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # Sigil
2
2
 
3
- `Sigil` is a lightweight TypeScript library for creating **nominal identity classes** with compile-time branding and reliable runtime type checks. It organize classes across your code and gives you power of **nominal typing**, **safe class checks across bundles** and **centralized registry** where reference to every class constructor is stored and enforced to have it's own unique label and symbol.
3
+ `Sigil` is a lightweight TypeScript library for creating **nominal identity classes** with compile-time branding and reliable runtime type checks. It organizes classes across your code and gives you power of **nominal typing**, **safe class checks across bundles** and **centralized registry** where reference to every class constructor is stored and enforced to have it's own unique label and symbol.
4
4
 
5
5
  > **Key ideas:**
6
6
  >
7
7
  > - **Compile-time nominal typing** via type brands so two structurally-identical types can remain distinct.
8
8
  > - **Reliable runtime guards** using `Symbol.for(...)` and lineage sets instead of `instanceof`.
9
9
  > - **Inheritance-aware identity**: lineages and sets let you test for subtype/supertype relationships.
10
- > - **Centralized class registry**: every class have it's own unique label and symbol that can be used as an id throughout the codebase.
10
+ > - **Centralized class registry**: every class have its own unique label and symbol that can be used as an id throughout the codebase.
11
11
 
12
12
  **Note: You should read these parts before implementing `Sigil` in you code:**
13
13
 
14
- - **Security note:** `Sigil` stores constructor references in the global registry. While it doesn't expose private instance data, it does mean any module can get constructor of the class. so **avoid** siglizing classes you want to make it unaccessable outside it's module. read more [Registery](#registry).
14
+ - **Security note:** By default, `Sigil` stores constructor references in the global registry. While it doesn't expose private instance data, it does mean any module can get constructor of the class. if you have sensitive classes that you want to be unaccessable outside it's module update global or per class options (e.g. `updateOptions({ storeConstructor: false })` or `@WithSigil("label", { storeConstructor: false })`). read more [Registery](#registry).
15
15
 
16
16
  - **Performance note:** `Sigil` attaches couple methods to every sigilized class instance, this is negligible in almost all cases, also `.isOfType()` although being reliable and performance optimized but it still less performant that native `instanceof` checks, so if you want maximum performance in cases like hot-path code it is not advised to use `Sigil` as it's built for consistency and maintainability mainly at the cost of minimal performance overhead.
17
17
 
@@ -34,6 +34,7 @@
34
34
  - [Registry](#registry)
35
35
  - [Troubleshooting & FAQ](#troubleshooting--faq)
36
36
  - [Best practices](#best-practices)
37
+ - [Deprecated API](#deprecated-api)
37
38
  - [Phantom](#phantom)
38
39
  - [Contributing](#contributing)
39
40
  - [License](#license)
@@ -168,7 +169,7 @@ But there is more to add to your system, which will be discussed in the [Core co
168
169
 
169
170
  ---
170
171
 
171
- ### Whe `Sigil` exists
172
+ ### Why `Sigil` exists
172
173
 
173
174
  `Sigil` was born out of real-world friction in a large **monorepo** built with **Domain-Driven Design (DDD)**.
174
175
 
@@ -296,7 +297,7 @@ The typed approach requires redefinition of public class, so you have:
296
297
 
297
298
  This separation is necessary as typescript decorators doesn't affect type system. so to reflect type update the class should be passed to HOF.
298
299
 
299
- Example of appraoch for class chain:
300
+ Example of approach for class chain:
300
301
 
301
302
  ```ts
302
303
  import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
@@ -408,7 +409,7 @@ export type X<G> = GetInstance<typeof X<G>>; // <-- Generics re-defined here, ju
408
409
  ### Anonymous classes
409
410
 
410
411
  You may see error: `Property 'x' of exported anonymous class type may not be private or protected.`, although this is rare to occur.
411
- This comes from the fact that all typed classes are `anonymous class` as they are return of HOF and ts compiler struggle to type them safely. to avoid these error entirly all you need is exporting the untyped classes even if they are un-used as a good convention.
412
+ This comes from the fact that all typed classes are `anonymous class` as they are return of HOF and ts compiler struggle to type them safely. to avoid these error entirely all you need is exporting the untyped classes even if they are un-used as a good convention.
412
413
 
413
414
  ```ts
414
415
  import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
@@ -442,8 +443,12 @@ export {
442
443
  isSigilInstance,
443
444
  } from './helpers';
444
445
  export { Sigilify } from './mixin';
445
- export { updateOptions, type SigilOptions } from './options';
446
- export { REGISTRY } from './registry';
446
+ export {
447
+ updateOptions,
448
+ SigilRegistry,
449
+ getActiveRegistry,
450
+ DEFAULT_LABEL_REGEX,
451
+ } from './options';
447
452
  export type {
448
453
  ISigil,
449
454
  ISigilInstance,
@@ -451,6 +456,7 @@ export type {
451
456
  TypedSigil,
452
457
  GetInstance,
453
458
  SigilBrandOf,
459
+ SigilOptions,
454
460
  } from './types';
455
461
  ```
456
462
 
@@ -465,8 +471,9 @@ export type {
465
471
  - `typed(Class, label?, parent?)`: type-only narrowing helper (no runtime mutation) — asserts runtime label in DEV.
466
472
  - `isSigilCtor(value)`: `true` if `value` is a sigil constructor.
467
473
  - `isSigilInstance(value)`: `true` if `value` is an instance of a sigil constructor.
468
- - `REGISTRY`: singleton wrapper around global `Sigil` registry.
469
- - `updateOptions(opts)`: change global runtime options before sigil decoration (e.g., `autofillLabels`, `devMarker`, etc.).
474
+ - `SigilRegistry`: `Sigil` Registy class used to centralize classes across app.
475
+ - `getActiveRegistry`: Getter of active registry being used by `Sigil`.
476
+ - `updateOptions(opts, mergeRegistries?)`: change global runtime options before sigil decoration (e.g., `autofillLabels`, `devMarker`, etc.).
470
477
  - `DEFAULT_LABEL_REGEX`: regex that insures structure of `@scope/package.ClassName` to all labels, it's advised to use it as your `SigilOptions.labelValidation`
471
478
 
472
479
  ### Instance & static helpers provided by Sigilified constructors
@@ -492,25 +499,34 @@ Instances of sigilified classes expose instance helpers:
492
499
 
493
500
  ## Options & configuration
494
501
 
495
- Sigil exposes a small set of runtime options that control DEV behavior. These can be modified at app startup via `updateOptions(...)`.
502
+ Sigil exposes a small set of runtime options that control registry and DEV behavior. These can be modified at app startup via `updateOptions(...)` to set global options:
496
503
 
497
504
  ```ts
498
- import { updateOptions } from '@vicin/sigil';
505
+ import { updateOptions, SigilRegistry } from '@vicin/sigil';
506
+
507
+ // Values defined in this example are defaults:
499
508
 
500
509
  updateOptions({
501
510
  autofillLabels: false, // auto-generate labels for subclasses that would otherwise inherit
502
511
  skipLabelInheritanceCheck: false, // skip DEV-only inheritance checks -- ALMOST NEVER WANT TO SET THIS TO TRUE, Use 'autofillLabels: true' instead. --
503
512
  labelValidation: null, // or a RegExp / function to validate labels
504
513
  devMarker: process.env.NODE_ENV !== 'production', // boolean used to block dev only checks in non-dev enviroments
514
+ registry: new SigilRegistry(), // setting active registry used by 'Sigil'
515
+ useGlobalRegistry: true, // append registry into 'globalThis' to insure single source in the runtime in cross bundles.
516
+ storeConstructor: true, // store reference of the constructor in registry
505
517
  });
506
518
  ```
507
519
 
520
+ Global options can be overridden per class by `opts` field in decorator and HOF.
521
+
508
522
  **Notes**:
509
523
 
510
- - It's advised to use `updateOptions({ labelValidation: DEFAULT_LABEL_REGEX })` at app entry to validate labels against `@scope/package.ClassName` structure.
524
+ - It's advised to use `updateOptions({ labelValidation: DEFAULT_LABEL_REGEX })` at app entry point to validate labels against `@scope/package.ClassName` structure.
511
525
  - `devMarker` drives DEV-only checks — when `false`, many runtime validations are no-ops (useful for production builds).
512
526
  - `autofillLabels` is useful for some HMR/test setups where you prefer not to throw on collisions and want autogenerated labels.
513
527
  - `skipLabelInheritanceCheck = true` can result on subtle bugs if enabled, so avoid setting it to true.
528
+ - When `SigilOptions.registry` is updated, old registry entries is merged and registered into new registry, to disable this behavrio pass `false` to `mergeRegistries` (`updateOptions({ registry: newRegistry }, false)`)
529
+ - `useGlobalRegistry` makes Sigil registry a central manager of classes and reliable way to enforce single label usage, so avoid setting it to `false` except if you have a strong reason. if you want to avoid making class constructor accessible via `globalThis` use `storeConstructor = true` instead.
514
530
 
515
531
  ---
516
532
 
@@ -518,28 +534,82 @@ updateOptions({
518
534
 
519
535
  `Sigil` with default options forces devs to `SigilLabel` every class defined, that allows central class registery that store a reference for every class keyed by its label, also it prevent two classes in the codebase from having the same `SigilLabel`.
520
536
 
521
- This is mainly useful in large codebases or frameworks where they need central registry or if you need class transport across API, workers, etc... where you can use `SigilLabel` reliably to serialize class identity. to interact with registry `Sigil` exposes `REGISTRY` class instance.
537
+ This is mainly useful in large codebases or frameworks where they need central registry or if you need class transport across API, workers, etc... where you can use `SigilLabel` reliably to serialize class identity. to interact with registry `Sigil` exposes `getActiveRegistry` and `SigilRegistry` class. also you can update registry related options with `updateOptions`.
522
538
 
523
- Registry is stored in `globalThis` under `Symbol.for(__SIGIL_REGISTRY__)` so there is single source of truth across the runtime, but this also exposes that map anywhere in the code.
539
+ By default, registry is stored in `globalThis` under `Symbol.for(__SIGIL_REGISTRY__)` so one instance is used across runtime even with multiple bundles, but this also exposes that map anywhere in the code, see [globalThis and security](#globalthis-and-security).
524
540
 
525
- **Note:**
541
+ ### Get registry
526
542
 
527
- - If you intentionally want to disable registry checks (for certain test environments), call `REGISTRY.replaceRegistry(null)` to opt out. although you should not do this in most cases.
543
+ You can interact with registry using `getActiveRegistry`, this function returns registry currently in use:
528
544
 
529
- ### Registry map
545
+ ```ts
546
+ import { getActiveRegistry } from '@vicin/sigil';
547
+ const registry = getActiveRegistry();
548
+ if (registry) console.log(registry.listLabels()); // check for presence as it can be 'null' if 'updateOptions({ registry: null })' is used
549
+ ```
550
+
551
+ ### Replace registry
530
552
 
531
- `Sigil` registry uses `Map<string, ISigil>` where `string` is `SigilLabel` and `ISigil` is reference to class constructor. If you want to pass your own external `Map` -**although this is not advised except if you have a strong reason**- you can use `REGISTRY.replaceRegistry` and `Sigil` will:
553
+ In most cases you don't need to replace registry, but if you wanted to define a `Map` and make `Sigil` use it aa a register (e.g. define custom side effects) you can use `SigilRegistry`:
554
+
555
+ ```ts
556
+ import { SigilRegistry, updateOptions } from '@vicin/sigil';
532
557
 
533
- - 1. Transfere all old classes to the new passed map.
534
- - 2. Attach this new map to `globalThis`.
535
- - 3. Store every new class defined in the passed map.
558
+ const myMap = new Map();
559
+ const myRegistry = new SigilRegistry(myMap);
560
+ updateOptions({ registry: myRegistry });
536
561
 
537
- ### Class typing
562
+ // Now 'Sigil' register new labels and constructors to 'myMap'.
563
+ ```
564
+
565
+ By default `Sigil` will merge old registry map into `myMap`, to prevent this behavior:
566
+
567
+ ```ts
568
+ updateOptions({ registry: myRegistry }, false); // <-- add false here
569
+ ```
570
+
571
+ Also you can set registry to `null`, but this is not advised as it disable all registry operations entirely:
572
+
573
+ ```ts
574
+ import { updateOptions } from '@vicin/sigil';
575
+ updateOptions({ registry: null }); // No label checks and registry map is freed from memory
576
+ ```
577
+
578
+ ### globalThis and security
579
+
580
+ By default registry is stored in `globalThis`. to disable this behavior you can:
581
+
582
+ ```ts
583
+ import { updateOptions } from '@vicin/sigil';
584
+ updateOptions({ useGlobalRegistry: false });
585
+ ```
586
+
587
+ Before applying this change, for registry to function normally you should insure that `Sigil` is not bundles twice in your app.
588
+ however if you can't insure that only bundle of `Sigil` is used and don't want class constructors to be accessible globally do this:
589
+
590
+ ```ts
591
+ import { updateOptions } from '@vicin/sigil';
592
+ updateOptions({ storeConstructor: false });
593
+ ```
594
+
595
+ Now registry only stores label of this classes and all class constructors are in the map are replaced with `null`.
596
+ If you need even more control and like the global registry for classes but want to obscure only some of your classes you can pass this option per class and keep global options as is:
597
+
598
+ ```ts
599
+ import { withSigil, Sigil } from '@vicin/sigil';
600
+
601
+ class _X extends Sigil {}
602
+ const X = withSigil(_X, 'X', { storeConstructor: false });
603
+ ```
604
+
605
+ Pick whatever pattern you like!
606
+
607
+ ### Class typing in registry
538
608
 
539
609
  Unfortunately concrete types of classes is not supported and all classes are stored as `ISigil` type. if you want concrete typings you can wrap registery:
540
610
 
541
611
  ```ts
542
- import { REGISTRY } from '@vicin/sigil';
612
+ import { getActiveRegistry } from '@vicin/sigil';
543
613
  import { MySigilClass1 } from './file1';
544
614
  import { MySigilClass2 } from './file2';
545
615
 
@@ -550,25 +620,25 @@ interface MyClasses {
550
620
 
551
621
  class MySigilRegistry {
552
622
  listLabels(): (keyof MyClasses)[] {
553
- return REGISTRY.listLabels();
623
+ return getActiveRegistry()?.listLabels();
554
624
  }
555
625
  has(label: string): boolean {
556
- return REGISTRY.has(label);
626
+ return getActiveRegistry()?.has(label);
557
627
  }
558
628
  get<L extends keyof MyClasses>(label: L): MyClasses[L] {
559
- return REGISTRY.get(label) as any;
629
+ return getActiveRegistry()?.get(label) as any;
560
630
  }
561
631
  unregister(label: string): boolean {
562
- return REGISTRY.unregister(label);
632
+ return getActiveRegistry()?.unregister(label);
563
633
  }
564
634
  clear(): void {
565
- REGISTRY.clear();
635
+ getActiveRegistry()?.clear();
566
636
  }
567
637
  replaceRegistry(newRegistry: Map<string, ISigil> | null): void {
568
- REGISTRY.replaceRegistry(newRegistry);
638
+ getActiveRegistry()?.replaceRegistry(newRegistry);
569
639
  }
570
640
  get size(): number {
571
- return REGISTRY.size;
641
+ return getActiveRegistry()?.size;
572
642
  }
573
643
  }
574
644
 
@@ -577,10 +647,30 @@ export const MY_SIGIL_REGISTRY = new MySigilRegistry();
577
647
 
578
648
  Now you have fully typed central class registry!
579
649
 
580
- ### Hot module relodes
650
+ ### I don't care about nominal types or central registry, i just want a runtime replacement of 'instanceof'
651
+
652
+ You can run this at the start of your app:
653
+
654
+ ```ts
655
+ import { updateOptions } from '@vicin/sigil';
656
+ updateOptions({ autofillLabels: true, storeConstructor: false });
657
+ ```
658
+
659
+ now you can omit all `HOF`, `Decorators` and make `Sigil` work in the background:
660
+
661
+ ```ts
662
+ import { Sigil } from '@vicin/sigil';
663
+
664
+ class X extends Sigil {}
665
+ class Y extends X {}
666
+ class Z extends Y {}
667
+
668
+ Z.isOfType(new Y()); // true
669
+ Z.isOfType(new X()); // true
670
+ Y.isOfType(new Y()); // false
671
+ ```
581
672
 
582
- - Sigil keeps a global registry (backed by `Symbol.for("@Sigil.__SIGIL_REGISTRY__")` on `globalThis`) so label uniqueness checks survive module reloads.
583
- - In development with HMR, duplicate class definitions are common; Sigil prints friendly warnings in DEV rather than throwing to keep the feedback loop fast.
673
+ No class constructors are stored globally and no code overhead, moreover if you can insure that `Sigil` is not bundles twice you can disable `useGlobalRegistry` and no trace of sigil in `globalThis`.
584
674
 
585
675
  ---
586
676
 
@@ -596,7 +686,7 @@ A: Use `@WithSigil("@your/label")`, or wrap the subclass with `withSigil` / `wit
596
686
 
597
687
  **Q: I got this error: 'Property 'x' of exported anonymous class type may not be private or protected.', How to fix it?**
598
688
 
599
- A: This error come's from the fact that all typed classes (return from `withSigil`, `withSigilTyped` or `typed`) are 'anonymous class' as they are the return of HOF. all you need to do it to export untyped classes (`_Class`) that have private or protected properties. or even export all untyped classes as a good convention even if they are not used.
689
+ A: This error comes from the fact that all typed classes (return from `withSigil`, `withSigilTyped` or `typed`) are 'anonymous class' as they are the return of HOF. all you need to do is to export untyped classes (`_Class`) that have private or protected properties. or even export all untyped classes as a good convention even if they are not used.
600
690
 
601
691
  **Q: I need nominal types in TypeScript. Which helper do I use?**
602
692
 
@@ -604,9 +694,9 @@ A: Use `withSigilTyped` to both attach runtime metadata and apply compile-time b
604
694
 
605
695
  **Q: How do I inspect currently registered labels?**
606
696
 
607
- A: Use `REGISTRY.list()` to get an array of registered labels.
697
+ A: Use `getActiveRegistry()?.list()` to get an array of registered labels.
608
698
 
609
- **Q: What if i want to omit labeling in some classes while inforce others?**
699
+ **Q: What if i want to omit labeling in some classes while enforce others?**
610
700
 
611
701
  A: You can set `SigilOptions.autofillLabels` to `true`. or if you more strict enviroment you can define empty `@WithSigil()` decorator above classes you don't care about labeling and `Sigil` will generate random label for it, but still throw if you forgot to use a decorator or HOF on a class.
612
702
 
@@ -621,6 +711,36 @@ A: You can set `SigilOptions.autofillLabels` to `true`. or if you more strict en
621
711
 
622
712
  ---
623
713
 
714
+ ## Deprecated API
715
+
716
+ ### REGISTRY
717
+
718
+ `Sigil` have moved from static reference registry to dynamic access and updates, now devs can create `SigilRegistry` class and pass it to `SigilOptions` to be be used by the library internals. however change is done gracefully and `REGISTRY` is still supported with no change in behavior but it's **marked with `deprecated` and will be removed in v2.0.0**.
719
+
720
+ ```ts
721
+ import { REGISTRY, getActiveRegistry } from '@vicin/sigil';
722
+
723
+ // from:
724
+ const present = REGISTRY.has('label');
725
+
726
+ // to:
727
+ const present = getActiveRegistry()?.has('label'); // Active registy can be 'null' if 'SigilOptions.registy' is set to null so we used the '?' mark
728
+ ```
729
+
730
+ ```ts
731
+ import { REGISTRY, updateOptions, SigilRegistry } from '@vicin/sigil';
732
+
733
+ // from:
734
+ const newRegistry = new Map();
735
+ REGISTRY.replaceRegistry(newRegistry);
736
+
737
+ // to
738
+ const newRegistry = new SigilRegistry(); // can pass external map to constructor if needed.
739
+ updateOptions({ registry: newRegistry });
740
+ ```
741
+
742
+ ---
743
+
624
744
  ## Phantom
625
745
 
626
746
  `Phantom` is another lightweight TypeScript library I created for achieving **nominal typing** on primitives and objects through type-only metadata. It solves the problem of structural typing in TypeScript allowing accidental misuse of identical shapes (e.g., confusing `UserId` and `PostId` as both strings) by enabling compile-time distinctions with features like **brands**, **constrained identities**, **variants for states**, **additive traits**, and **reversible transformations**. This makes it ideal for domain-driven design (DDD) without runtime overhead.