@vicin/sigil 1.1.1 → 1.2.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/README.md +230 -132
- package/dist/index.d.mts +102 -44
- package/dist/index.d.ts +102 -44
- package/dist/index.global.js +8 -8
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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 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
|
|
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 its own unique label and symbol.
|
|
4
4
|
|
|
5
5
|
> **Key ideas:**
|
|
6
6
|
>
|
|
@@ -9,11 +9,21 @@
|
|
|
9
9
|
> - **Inheritance-aware identity**: lineages and sets let you test for subtype/supertype relationships.
|
|
10
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
|
-
**Note: You should read these parts before implementing `Sigil` in
|
|
12
|
+
**Note: You should read these parts before implementing `Sigil` in your code:**
|
|
13
13
|
|
|
14
|
-
- **Security note:** By default, `Sigil` stores constructor references in
|
|
14
|
+
- **Security note:** By default, `Sigil` stores constructor references in a global registry. While this does not expose private instance data, it allows any module to retrieve a class constructor via its label. If you have sensitive classes that should not be accessible globally, update your options:
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```ts
|
|
17
|
+
@WithSigil("label", { storeConstructor: false })
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
See the [Registry](#registry) section for more details.
|
|
21
|
+
|
|
22
|
+
- **Performance & Hot-Paths note:** `Sigil` attaches minimal metadata to instances, which is negligible for 99% of use cases. While `.isOfType()` is optimized, it is inherently slower than the native `instanceof` operator. For extreme hot-path code where every microsecond counts, stick to native checks—but keep in mind you'll lose Sigil's cross-bundle reliability.
|
|
23
|
+
|
|
24
|
+
- **HOF & Private Constructors note:** Due to a known TypeScript limitation with class expressions, using HOF helpers (like `withSigilTyped`) on classes with private constructors still allow those classes to be extended in the type system. If strict constructor encapsulation is a priority, please review the [Private Constructors](#private-constructors) guide.
|
|
25
|
+
|
|
26
|
+
- **Simplified instanceof Replacement:** `Sigil` relies on unique labels to identify classes. If your primary goal is simply to fix `instanceof` across bundles without the overhead of nominal typing or a registry, you can jump straight to our [Minimal Setup Guide](#i-dont-care-about-nominal-types-or-central-registry-i-just-want-a-runtime-replacement-of-instanceof).
|
|
17
27
|
|
|
18
28
|
---
|
|
19
29
|
|
|
@@ -24,16 +34,17 @@
|
|
|
24
34
|
- [Install](#install)
|
|
25
35
|
- [Basic usage (mixin / base class)](#basic-usage-mixin--base-class)
|
|
26
36
|
- [Decorator style](#decorator-style)
|
|
27
|
-
- [HOF helpers
|
|
28
|
-
- [
|
|
37
|
+
- [HOF helpers](#hof-higher-order-function-helpers)
|
|
38
|
+
- [Minimal “first-run” example](#minimal-first-run-example)
|
|
29
39
|
- [Migration](#migration)
|
|
40
|
+
- [Limitations & guarantees](#limitations--guarantees)
|
|
30
41
|
- [Core concepts](#core-concepts)
|
|
31
|
-
- [
|
|
42
|
+
- [Nominal typing](#nominal-typing)
|
|
32
43
|
- [API reference](#api-reference)
|
|
33
44
|
- [Options & configuration](#options--configuration)
|
|
34
45
|
- [Registry](#registry)
|
|
46
|
+
- [Security guidance](#security-guidance)
|
|
35
47
|
- [Troubleshooting & FAQ](#troubleshooting--faq)
|
|
36
|
-
- [Best practices](#best-practices)
|
|
37
48
|
- [Deprecated API](#deprecated-api)
|
|
38
49
|
- [Phantom](#phantom)
|
|
39
50
|
- [Contributing](#contributing)
|
|
@@ -52,7 +63,7 @@
|
|
|
52
63
|
|
|
53
64
|
- Easy to use: decorator (`@WithSigil`), mixin (`Sigilify`), and HOF (Higher order function) helpers (`withSigil`, `withSigilTyped`, `typed`).
|
|
54
65
|
|
|
55
|
-
- **Global registry** to centralize classes (query any class by
|
|
66
|
+
- **Global registry** to centralize classes (query any class by its label in run-time) and guard against duplicate labels.
|
|
56
67
|
|
|
57
68
|
- Minimal runtime overhead in production (DEV checks can be toggled off).
|
|
58
69
|
|
|
@@ -70,7 +81,7 @@ yarn add @vicin/sigil
|
|
|
70
81
|
pnpm add @vicin/sigil
|
|
71
82
|
```
|
|
72
83
|
|
|
73
|
-
**Requirements**: TypeScript 5.0+ (for stage-3 decorators) and Node.js 18+ recommended.
|
|
84
|
+
**Requirements**: TypeScript 5.0+ (for stage-3 decorators) and Node.js 18+ recommended. however HOF can be used in older TypeScript versions.
|
|
74
85
|
|
|
75
86
|
### Basic usage (mixin / base class)
|
|
76
87
|
|
|
@@ -101,7 +112,7 @@ class User extends Sigil {}
|
|
|
101
112
|
|
|
102
113
|
> Note: When extending an already sigilized class (for example `Sigil`), you must decorate the subclass or use the HOF helpers in DEV mode unless you configured the library otherwise.
|
|
103
114
|
|
|
104
|
-
### HOF (Higher-Order Function) helpers
|
|
115
|
+
### HOF (Higher-Order Function) helpers
|
|
105
116
|
|
|
106
117
|
HOFs work well in many build setups and are idempotent-safe for HMR flows.
|
|
107
118
|
|
|
@@ -115,24 +126,27 @@ const user = new User();
|
|
|
115
126
|
console.log(User.SigilLabel); // "@myorg/mypkg.User"
|
|
116
127
|
```
|
|
117
128
|
|
|
118
|
-
###
|
|
119
|
-
|
|
120
|
-
If you want TypeScript to treat identities nominally (so `UserId` !== `PostId` despite identical shape), use the typed helpers.
|
|
129
|
+
### Minimal “first-run” example
|
|
121
130
|
|
|
122
131
|
```ts
|
|
123
|
-
import { Sigil,
|
|
132
|
+
import { Sigil, withSigil } from '@vicin/sigil';
|
|
124
133
|
|
|
125
|
-
class _User extends Sigil {
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
class _User extends Sigil {
|
|
135
|
+
constructor(public name: string) {
|
|
136
|
+
super();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export const User = withSigil(_User, '@myorg/mypkg.User');
|
|
128
140
|
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
const u = new User('alice');
|
|
142
|
+
|
|
143
|
+
console.log(User.SigilLabel); // "@myorg/mypkg.User"
|
|
144
|
+
console.log(User.isOfType(u)); // true
|
|
131
145
|
```
|
|
132
146
|
|
|
133
147
|
### Migration
|
|
134
148
|
|
|
135
|
-
|
|
149
|
+
Migrating old code into `Sigil` can be done seamlessly with this set-up:
|
|
136
150
|
|
|
137
151
|
1. Set `SigilOptions.autofillLabels` to `true` at the start of the app so no errors are thrown in the migration stage:
|
|
138
152
|
|
|
@@ -141,7 +155,7 @@ import { updateOptions } from '@vicin/sigil';
|
|
|
141
155
|
updateOptions({ autofillLabels: true });
|
|
142
156
|
```
|
|
143
157
|
|
|
144
|
-
2. Make
|
|
158
|
+
2. Make your base classes extends `Sigil`:
|
|
145
159
|
|
|
146
160
|
```ts
|
|
147
161
|
import { Sigil } from '@vicin/sigil';
|
|
@@ -151,11 +165,51 @@ class MyBaseClass {} // original
|
|
|
151
165
|
class MyBaseClass extends Sigil {} // <-- add 'extends Sigil' here
|
|
152
166
|
```
|
|
153
167
|
|
|
154
|
-
Just like this, your entire classes are
|
|
168
|
+
Just like this, your entire classes are sigilized and you can start using `.isOfType()` as a replacement of `instanceof` in cross bundle checks.
|
|
155
169
|
But there is more to add to your system, which will be discussed in the [Core concepts](#core-concepts).
|
|
156
170
|
|
|
157
171
|
---
|
|
158
172
|
|
|
173
|
+
## Limitations & guarantees
|
|
174
|
+
|
|
175
|
+
This section states clearly what `Sigil` provides and what it does **not** provide.
|
|
176
|
+
|
|
177
|
+
### What Sigil guarantees
|
|
178
|
+
|
|
179
|
+
**1. Stable label → symbol mapping within the same JS global symbol registry.**
|
|
180
|
+
|
|
181
|
+
- If two bundles share the same **global symbol registry** (the thing `Symbol.for(...)` uses), then `Symbol.for(label)` is identical across them — enabling cross-bundle identity checks when both sides use the same label string.
|
|
182
|
+
|
|
183
|
+
**2. Reliable runtime identity (when used as intended).**
|
|
184
|
+
|
|
185
|
+
- When classes are sigilified and their labels are used consistently, `.isOfType()` and the `SigilTypeSet` checks produce stable results across bundles/Hot Module Replacement flows that share the same runtime/global symbol registry.
|
|
186
|
+
|
|
187
|
+
**3. Optional central registry for discovery & serialization helpers.**
|
|
188
|
+
|
|
189
|
+
- If enabled, the registry centralizes labels (and optionally constructor references), which can be used for label-based serialization or runtime lookups within a trusted runtime.
|
|
190
|
+
|
|
191
|
+
**4. Nominal typing that is inheritance-aware**
|
|
192
|
+
|
|
193
|
+
- With couple extra lines of code you can have nominally typed classes.
|
|
194
|
+
|
|
195
|
+
### What Sigil does not guarantee
|
|
196
|
+
|
|
197
|
+
**2. It is not for across isolated JS realms.**
|
|
198
|
+
|
|
199
|
+
Examples of isolated realms where Sigil may not work as expected:
|
|
200
|
+
|
|
201
|
+
- iframe with a different global context that does not share the same window (and therefore a different symbol registry).
|
|
202
|
+
- Workers or processes that do not share the same `globalThis` / symbol registry.
|
|
203
|
+
- Cross-origin frames where symbols are not shared.
|
|
204
|
+
|
|
205
|
+
In such cases you must provide a bridging/serialization protocol that maps labels to constructors on each side. however `Sigil` if used as intended makes serialization protocol much easier as each class will have a unique label.
|
|
206
|
+
|
|
207
|
+
**3. Not a security or access-control mechanism.**
|
|
208
|
+
|
|
209
|
+
Presence of a constructor or label in the registry is discoverable (unless you purposely set `storeConstructor: false`). Do **not** use `Sigil` as an authorization or secrets mechanism.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
159
213
|
## Core concepts
|
|
160
214
|
|
|
161
215
|
### Terminology
|
|
@@ -216,12 +270,11 @@ The goal is simple:
|
|
|
216
270
|
|
|
217
271
|
#### Problem A — `instanceof` is unreliable
|
|
218
272
|
|
|
219
|
-
|
|
220
|
-
To make runtime identity reliable across bundles, HMR, and transpiled code, `Sigil` explicitly attaches identity metadata and tracks inheritance lineage on classes. That tracking starts with a contract created by `Sigilify()` (a mixin / class factory) or by extending the `Sigil` base class. Sigilify augments a plain JS class with the metadata and helper methods Sigil needs (for example, `isOfType`).
|
|
273
|
+
To make runtime identity reliable across bundles, HMR, and transpiled code, `Sigil` explicitly attaches identity metadata and tracks inheritance lineage on classes. That tracking starts with a contract created by `Sigilify()` (a mixin) or by extending the `Sigil` base class. Sigilify augments a plain JS class with the metadata and helper methods Sigil needs (for example, `isOfType`).
|
|
221
274
|
|
|
222
275
|
Basic patterns:
|
|
223
276
|
|
|
224
|
-
Mixin
|
|
277
|
+
**Mixin:**
|
|
225
278
|
|
|
226
279
|
```ts
|
|
227
280
|
import { Sigilify } from '@vicin/sigil';
|
|
@@ -229,27 +282,27 @@ import { Sigilify } from '@vicin/sigil';
|
|
|
229
282
|
const MyClass = Sigilify(class {}, 'MyClass');
|
|
230
283
|
```
|
|
231
284
|
|
|
232
|
-
Direct base-class extend
|
|
285
|
+
**Direct base-class extend:**
|
|
233
286
|
|
|
234
287
|
```ts
|
|
235
288
|
import { Sigil, WithSigil } from '@vicin/sigil';
|
|
236
289
|
|
|
290
|
+
@WithSigil('MyClass')
|
|
237
291
|
class MyClass extends Sigil {}
|
|
238
292
|
```
|
|
239
293
|
|
|
240
|
-
|
|
241
|
-
Once you opt into the runtime contract, Sigil enforces consistency: in DEV mode, extending a sigil-aware class without using decorator `@WithSigil` or a provided HOF (e.g. `withSigil` or `withSigilTyped`) will throw a helpful error. If you prefer a laxer setup, Sigil can be configured to auto-label or disable the strict enforcement.
|
|
294
|
+
Once you opt into the runtime contract, Sigil enforces consistency: in DEV mode, extending a sigil-aware class without using decorator `@WithSigil` or a provided HOF (e.g. `withSigil` or `withSigilTyped`) will throw a helpful error. If you prefer a laxer setup, Sigil can be configured to auto-label.
|
|
242
295
|
|
|
243
|
-
Decorator
|
|
296
|
+
**Decorator:**
|
|
244
297
|
|
|
245
298
|
```ts
|
|
246
299
|
import { Sigil, WithSigil } from '@vicin/sigil';
|
|
247
300
|
|
|
248
|
-
@WithSigil('MyClass')
|
|
301
|
+
@WithSigil('MyClass')
|
|
249
302
|
class MyClass extends Sigil {}
|
|
250
303
|
```
|
|
251
304
|
|
|
252
|
-
HOF
|
|
305
|
+
**HOF:**
|
|
253
306
|
|
|
254
307
|
```ts
|
|
255
308
|
import { Sigil, withSigil } from '@vicin/sigil';
|
|
@@ -258,17 +311,22 @@ class _MyClass extends Sigil {}
|
|
|
258
311
|
const MyClass = withSigil(_MyClass, 'MyClass');
|
|
259
312
|
```
|
|
260
313
|
|
|
261
|
-
|
|
314
|
+
#### Problem B — Branding can get messy
|
|
262
315
|
|
|
263
|
-
-
|
|
264
|
-
- Use the HOFs when you want better ergonomics or plan to opt into typed branding later.
|
|
316
|
+
Runtime metadata alone does not change TypeScript types. To get compile-time nominal typing (so `UserId` ≠ `PostId` even with the same shape), Sigil provides two patterns:
|
|
265
317
|
|
|
266
|
-
|
|
318
|
+
**Decorator with brand field:**
|
|
267
319
|
|
|
268
|
-
|
|
269
|
-
|
|
320
|
+
```ts
|
|
321
|
+
import { Sigil, WithSigil, UpdateSigilBrand } from '@vicin/sigil';
|
|
270
322
|
|
|
271
|
-
|
|
323
|
+
@WithSigil('User')
|
|
324
|
+
class User extends Sigil {
|
|
325
|
+
declare __SIGIL_BRAND__: UpdateSigilBrand<'User', Sigil>; // <-- inject type
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**HOF with `_Class` / `Class`:**
|
|
272
330
|
|
|
273
331
|
```ts
|
|
274
332
|
import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
|
|
@@ -281,76 +339,47 @@ const User = withSigilTyped(_User, 'User');
|
|
|
281
339
|
type User = GetInstance<typeof User>;
|
|
282
340
|
```
|
|
283
341
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
- The emitted runtime class still has sigil metadata (symbols, lineage).
|
|
287
|
-
- The TypeScript type for the class is narrowed to the `UserId` label at compile time. Assignments between different labels become type errors.
|
|
288
|
-
|
|
289
|
-
---
|
|
290
|
-
|
|
291
|
-
#### Why there are '\_Class' and 'Class'
|
|
292
|
-
|
|
293
|
-
The typed approach requires redefinition of public class, so you have:
|
|
294
|
-
|
|
295
|
-
- **Untyped class:** `_User` — regular class code used for inheritance and implementation.
|
|
296
|
-
- **Typed class:** `User` — the result of the typed HOF; this is the sigil-aware, branded class used by the rest of your codebase.
|
|
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.
|
|
299
|
-
|
|
300
|
-
Example of approach for class chain:
|
|
342
|
+
Typings are lineage aware as well:
|
|
301
343
|
|
|
302
344
|
```ts
|
|
303
345
|
import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
|
|
304
346
|
|
|
305
|
-
|
|
306
|
-
|
|
347
|
+
class _A extends Sigil {}
|
|
348
|
+
const A = withSigilTyped(_A, 'A');
|
|
349
|
+
type A = GetInstance<typeof A>;
|
|
307
350
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
class _Admin extends User {}
|
|
351
|
+
class _B extends A {}
|
|
352
|
+
const B = withSigilTyped(_B, 'B');
|
|
353
|
+
type B = GetInstance<typeof B>;
|
|
312
354
|
|
|
313
|
-
|
|
314
|
-
type
|
|
315
|
-
|
|
316
|
-
// Type relationships:
|
|
317
|
-
type test1 = User extends Admin ? true : false; // false
|
|
318
|
-
type test2 = Admin extends User ? true : false; // true
|
|
355
|
+
type test1 = A extends B ? true : false; // false
|
|
356
|
+
type test2 = B extends A ? true : false; // true
|
|
319
357
|
```
|
|
320
358
|
|
|
321
|
-
This demonstrates:
|
|
322
|
-
|
|
323
|
-
- `Admin` is recognized as a subtype of `User` (both at runtime and in types) if it was created via the appropriate typed helpers.
|
|
324
|
-
|
|
325
|
-
---
|
|
326
|
-
|
|
327
359
|
#### SigilLabel & SigilType
|
|
328
360
|
|
|
329
361
|
If library is used with default options, `SigilLabel` & `SigilType` are **100% unique** for each class, which make them perfect replacement of manual labeling across your code that comes shipped with Sigil by default. you can access them in class constructor directly of via `getSigilLabel()` and `getSigilType()` in instances.
|
|
330
362
|
|
|
331
363
|
---
|
|
332
364
|
|
|
333
|
-
##
|
|
365
|
+
## Nominal typing
|
|
334
366
|
|
|
335
367
|
In this part we will discuss conventions to avoid any type errors and have normal developing experience with just extra few definition lines at the bottom of the file.
|
|
368
|
+
First we have two patterns, **HOF with `_Class` / `Class`** and **Decorators with brand field**:
|
|
336
369
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
### Typed vs Untyped classes
|
|
340
|
-
|
|
341
|
-
The update of Sigil brand types happens via HOF that are defined below actual class definition.
|
|
370
|
+
### HOF with `_Class` / `Class`
|
|
342
371
|
|
|
343
|
-
|
|
372
|
+
The update of `Sigil` brand types happens via HOF that are defined below actual class definition:
|
|
344
373
|
|
|
345
374
|
```ts
|
|
346
|
-
import { Sigil, withSigilTyped } from '@vicin/sigil';
|
|
375
|
+
import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
|
|
347
376
|
|
|
348
377
|
class _X extends Sigil {
|
|
349
378
|
// All logic for class
|
|
350
379
|
}
|
|
351
380
|
|
|
352
381
|
export const X = withSigilTyped(_X, 'Label.X'); // <-- Pass class with label to uniquely identify it from other classes
|
|
353
|
-
export type X =
|
|
382
|
+
export type X = GetInstance<typeof X>;
|
|
354
383
|
```
|
|
355
384
|
|
|
356
385
|
In other parts of the code:
|
|
@@ -363,17 +392,14 @@ class _Y extends X {
|
|
|
363
392
|
}
|
|
364
393
|
|
|
365
394
|
export const Y = withSigilTyped(_Y, 'Label.Y');
|
|
366
|
-
export type Y =
|
|
395
|
+
export type Y = GetInstance<typeof Y>;
|
|
367
396
|
```
|
|
368
397
|
|
|
369
398
|
So as we have seen nominal identity is introduced with few lines only below each class. and the bulk code where logic lives is untouched.
|
|
370
399
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
### `InstanceType<>` vs `GetInstance<>`
|
|
400
|
+
#### `InstanceType<>` vs `GetInstance<>`
|
|
374
401
|
|
|
375
|
-
|
|
376
|
-
So alternative in introduced which is `GetInstance`.
|
|
402
|
+
You should depend on `GetInstance` to get type of instance and avoid using `InstanceType` as it returns `any` if the class constructor is `protected` or `private`.
|
|
377
403
|
|
|
378
404
|
```ts
|
|
379
405
|
import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
|
|
@@ -381,14 +407,12 @@ import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
|
|
|
381
407
|
class _X extends Sigil {}
|
|
382
408
|
|
|
383
409
|
export const X = withSigilTyped(_X, 'Label.X');
|
|
384
|
-
export type X = GetInstance<typeof X>; // <--
|
|
410
|
+
export type X = GetInstance<typeof X>; // <-- works with 'private' and 'protected' constructors as well
|
|
385
411
|
```
|
|
386
412
|
|
|
387
|
-
Internally `GetInstance` is just `T extends { prototype: infer R }
|
|
388
|
-
|
|
389
|
-
---
|
|
413
|
+
Internally `GetInstance` is just `T extends { prototype: infer R }`.
|
|
390
414
|
|
|
391
|
-
|
|
415
|
+
#### Generic propagation
|
|
392
416
|
|
|
393
417
|
One of the downsides of defining typed class at the bottom is that we need to redefine generics as well in the type.
|
|
394
418
|
|
|
@@ -404,9 +428,7 @@ export const X = withSigilTyped(_X, 'Label.X');
|
|
|
404
428
|
export type X<G> = GetInstance<typeof X<G>>; // <-- Generics re-defined here, just copy-paste and pass them
|
|
405
429
|
```
|
|
406
430
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
### Anonymous classes
|
|
431
|
+
#### Anonymous classes
|
|
410
432
|
|
|
411
433
|
You may see error: `Property 'x' of exported anonymous class type may not be private or protected.`, although this is rare to occur.
|
|
412
434
|
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.
|
|
@@ -420,6 +442,61 @@ export const X = withSigilTyped(_X, 'Label.X');
|
|
|
420
442
|
export type X = GetInstance<typeof X>;
|
|
421
443
|
```
|
|
422
444
|
|
|
445
|
+
#### Private constructors
|
|
446
|
+
|
|
447
|
+
The only limitation in HOF approach is **extending private constructors**:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
import { Sigil, withSigilTyped, GetInstance } from '@vicin/sigil';
|
|
451
|
+
class _X extends Sigil {
|
|
452
|
+
private constructor() {}
|
|
453
|
+
}
|
|
454
|
+
const X = withSigilTyped(_X, 'X');
|
|
455
|
+
type X = GetInstance<typeof X>;
|
|
456
|
+
|
|
457
|
+
class _Y extends X {} // <-- This is allowed!
|
|
458
|
+
const Y = withSigilTyped(_Y, 'Y');
|
|
459
|
+
type Y = GetInstance<typeof Y>;
|
|
460
|
+
|
|
461
|
+
const y = new Y(); // <-- Type here is any
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Unfortunately this is a limitation in typescript and i couldn't find any solution to adress it.
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
### Decorators with brand field
|
|
469
|
+
|
|
470
|
+
The update of `Sigil` brand type happens directly by overriding `__SIGIL_BRAND__` field:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
import { Sigil, WithSigil, UpdateSigilBrand } from '@vicin/sigil';
|
|
474
|
+
|
|
475
|
+
@WithSigil('X')
|
|
476
|
+
class X extends Sigil {
|
|
477
|
+
declare __SIGIL_BRAND__: UpdateSigilBrand<'X', Sigil>;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
@WithSigil('Y')
|
|
481
|
+
class Y extends X {
|
|
482
|
+
declare __SIGIL_BRAND__: UpdateSigilBrand<'Y', Sigil>;
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
As you see no `_Class`/`Class` pattern, no `private constructor` issue and no type hacks, but our branding logic now lives in class body.
|
|
487
|
+
|
|
488
|
+
To be more explicit and prevent mismatch between run-time and compile-time labels we can:
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
import { Sigil, WithSigil, UpdateSigilBrand } from '@vicin/sigil';
|
|
492
|
+
|
|
493
|
+
const label = 'X';
|
|
494
|
+
@WithSigil(label)
|
|
495
|
+
class X extends Sigil {
|
|
496
|
+
declare __SIGIL_BRAND__: UpdateSigilBrand<typeof label, Sigil>;
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
423
500
|
---
|
|
424
501
|
|
|
425
502
|
## API reference
|
|
@@ -433,7 +510,7 @@ Top-level exports from the library:
|
|
|
433
510
|
```ts
|
|
434
511
|
export { Sigil, SigilError } from './classes';
|
|
435
512
|
export { WithSigil } from './decorator';
|
|
436
|
-
export {
|
|
513
|
+
export { withSigil, withSigilTyped } from './enhancers';
|
|
437
514
|
export {
|
|
438
515
|
isDecorated,
|
|
439
516
|
isInheritanceChecked,
|
|
@@ -451,12 +528,11 @@ export {
|
|
|
451
528
|
} from './options';
|
|
452
529
|
export type {
|
|
453
530
|
ISigil,
|
|
454
|
-
ISigilInstance,
|
|
455
|
-
ISigilStatic,
|
|
456
531
|
TypedSigil,
|
|
457
532
|
GetInstance,
|
|
458
533
|
SigilBrandOf,
|
|
459
534
|
SigilOptions,
|
|
535
|
+
UpdateSigilBrand,
|
|
460
536
|
} from './types';
|
|
461
537
|
```
|
|
462
538
|
|
|
@@ -468,13 +544,12 @@ export type {
|
|
|
468
544
|
- `Sigilify(Base, label?, opts?)`: mixin function that returns a new constructor with sigil types and instance helpers.
|
|
469
545
|
- `withSigil(Class, label?, opts?)`: HOF that validates and decorates an existing class constructor.
|
|
470
546
|
- `withSigilTyped(Class, label?, opts?)`: like `withSigil` but narrows the TypeScript type to include brands.
|
|
471
|
-
- `typed(Class, label?, parent?)`: type-only narrowing helper (no runtime mutation) — asserts runtime label in DEV.
|
|
472
547
|
- `isSigilCtor(value)`: `true` if `value` is a sigil constructor.
|
|
473
548
|
- `isSigilInstance(value)`: `true` if `value` is an instance of a sigil constructor.
|
|
474
549
|
- `SigilRegistry`: `Sigil` Registy class used to centralize classes across app.
|
|
475
550
|
- `getActiveRegistry`: Getter of active registry being used by `Sigil`.
|
|
476
551
|
- `updateOptions(opts, mergeRegistries?)`: change global runtime options before sigil decoration (e.g., `autofillLabels`, `devMarker`, etc.).
|
|
477
|
-
- `DEFAULT_LABEL_REGEX`: regex that
|
|
552
|
+
- `DEFAULT_LABEL_REGEX`: regex that ensures structure of `@scope/package.ClassName` to all labels, it's advised to use it as your `SigilOptions.labelValidation`
|
|
478
553
|
|
|
479
554
|
### Instance & static helpers provided by Sigilified constructors
|
|
480
555
|
|
|
@@ -510,29 +585,20 @@ updateOptions({
|
|
|
510
585
|
autofillLabels: false, // auto-generate labels for subclasses that would otherwise inherit
|
|
511
586
|
skipLabelInheritanceCheck: false, // skip DEV-only inheritance checks -- ALMOST NEVER WANT TO SET THIS TO TRUE, Use 'autofillLabels: true' instead. --
|
|
512
587
|
labelValidation: null, // or a RegExp / function to validate labels
|
|
513
|
-
devMarker: process.env.NODE_ENV !== 'production', // boolean used to block dev only checks in non-dev
|
|
588
|
+
devMarker: process.env.NODE_ENV !== 'production', // boolean used to block dev only checks in non-dev environments
|
|
514
589
|
registry: new SigilRegistry(), // setting active registry used by 'Sigil'
|
|
515
|
-
useGlobalRegistry: true, // append registry into 'globalThis' to
|
|
590
|
+
useGlobalRegistry: true, // append registry into 'globalThis' to ensure single source in the runtime in cross bundles.
|
|
516
591
|
storeConstructor: true, // store reference of the constructor in registry
|
|
517
592
|
});
|
|
518
593
|
```
|
|
519
594
|
|
|
520
595
|
Global options can be overridden per class by `opts` field in decorator and HOF.
|
|
521
596
|
|
|
522
|
-
**Notes**:
|
|
523
|
-
|
|
524
|
-
- It's advised to use `updateOptions({ labelValidation: DEFAULT_LABEL_REGEX })` at app entry point to validate labels against `@scope/package.ClassName` structure.
|
|
525
|
-
- `devMarker` drives DEV-only checks — when `false`, many runtime validations are no-ops (useful for production builds).
|
|
526
|
-
- `autofillLabels` is useful for some HMR/test setups where you prefer not to throw on collisions and want autogenerated labels.
|
|
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.
|
|
530
|
-
|
|
531
597
|
---
|
|
532
598
|
|
|
533
599
|
## Registry
|
|
534
600
|
|
|
535
|
-
`Sigil`
|
|
601
|
+
`Sigil`'s default options require developers to label every class, that allows central class registry that stores a reference for every class keyed by its label, also it prevent two classes in the codebase from having the same `SigilLabel`.
|
|
536
602
|
|
|
537
603
|
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`.
|
|
538
604
|
|
|
@@ -584,8 +650,8 @@ import { updateOptions } from '@vicin/sigil';
|
|
|
584
650
|
updateOptions({ useGlobalRegistry: false });
|
|
585
651
|
```
|
|
586
652
|
|
|
587
|
-
Before applying this change, for registry to function normally you should
|
|
588
|
-
however if you can't
|
|
653
|
+
Before applying this change, for registry to function normally you should ensure that `Sigil` is not bundles twice in your app.
|
|
654
|
+
however if you can't ensure that only bundle of `Sigil` is used and don't want class constructors to be accessible globally do this:
|
|
589
655
|
|
|
590
656
|
```ts
|
|
591
657
|
import { updateOptions } from '@vicin/sigil';
|
|
@@ -606,7 +672,7 @@ Pick whatever pattern you like!
|
|
|
606
672
|
|
|
607
673
|
### Class typing in registry
|
|
608
674
|
|
|
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
|
|
675
|
+
Unfortunately concrete types of classes is not supported and all classes are stored as `ISigil` type. if you want concrete typings you can wrap registry:
|
|
610
676
|
|
|
611
677
|
```ts
|
|
612
678
|
import { getActiveRegistry } from '@vicin/sigil';
|
|
@@ -670,7 +736,48 @@ Z.isOfType(new X()); // true
|
|
|
670
736
|
Y.isOfType(new Y()); // false
|
|
671
737
|
```
|
|
672
738
|
|
|
673
|
-
No class constructors are stored globally and no code overhead, moreover if you can
|
|
739
|
+
No class constructors are stored globally and no code overhead, moreover if you can ensure that `Sigil` is not bundles twice you can disable `useGlobalRegistry` and no trace of sigil in `globalThis`.
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## Security guidance
|
|
744
|
+
|
|
745
|
+
### Recommended defaults & quick rules
|
|
746
|
+
|
|
747
|
+
- Default recommendation for public/shared runtimes (web pages, untrusted workers, serverless):
|
|
748
|
+
|
|
749
|
+
```ts
|
|
750
|
+
updateOptions({ useGlobalRegistry: false, storeConstructor: false });
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
This prevents constructors from being put on `globalThis` and prevents constructors from being stored in the registry map (labels remain, but constructors are `null`).
|
|
754
|
+
|
|
755
|
+
- For private server runtimes (single controlled Node process) where a central registry is desired:
|
|
756
|
+
|
|
757
|
+
```ts
|
|
758
|
+
updateOptions({ useGlobalRegistry: true, storeConstructor: true });
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Only enable this if you control all bundles and trust the runtime environment.
|
|
762
|
+
|
|
763
|
+
### Per-class sensitivity control
|
|
764
|
+
|
|
765
|
+
If you want the registry in general but need to hide particular classes (e.g., internal or security-sensitive classes), pass `storeConstructor: false` for those classes:
|
|
766
|
+
|
|
767
|
+
```ts
|
|
768
|
+
class _Sensitive extends Sigil {}
|
|
769
|
+
export const Sensitive = withSigil(_Sensitive, '@myorg/internal.Sensitive', {
|
|
770
|
+
storeConstructor: false, // label kept, constructor not stored
|
|
771
|
+
});
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
This keeps the declarative identity but avoids exposing the constructor reference in the registry.
|
|
775
|
+
|
|
776
|
+
### Short warnings (do not rely on Sigil for)
|
|
777
|
+
|
|
778
|
+
- **Not a security boundary:** Registry labels/constructors are discovery metadata — do not put secrets or private instance data in them or rely on them for access control.
|
|
779
|
+
|
|
780
|
+
- **Third-party code can access the registry if `useGlobalRegistry: true`** — only enable that in fully trusted runtimes.
|
|
674
781
|
|
|
675
782
|
---
|
|
676
783
|
|
|
@@ -688,13 +795,9 @@ A: Use `@WithSigil("@your/label")`, or wrap the subclass with `withSigil` / `wit
|
|
|
688
795
|
|
|
689
796
|
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.
|
|
690
797
|
|
|
691
|
-
**Q: I need nominal types in TypeScript. Which helper do I use?**
|
|
692
|
-
|
|
693
|
-
A: Use `withSigilTyped` to both attach runtime metadata and apply compile-time brands. If runtime metadata already exists and you only want to narrow types, use `typed(...)` (which is type-only but asserts the runtime label in DEV).
|
|
694
|
-
|
|
695
798
|
**Q: How do I inspect currently registered labels?**
|
|
696
799
|
|
|
697
|
-
A: Use `getActiveRegistry()?.
|
|
800
|
+
A: Use `getActiveRegistry()?.listLabels()` to get an array of registered labels.
|
|
698
801
|
|
|
699
802
|
**Q: What if i want to omit labeling in some classes while enforce others?**
|
|
700
803
|
|
|
@@ -702,15 +805,6 @@ A: You can set `SigilOptions.autofillLabels` to `true`. or if you more strict en
|
|
|
702
805
|
|
|
703
806
|
---
|
|
704
807
|
|
|
705
|
-
## Best practices
|
|
706
|
-
|
|
707
|
-
- Prefer `withSigil`/`withSigilTyped` for predictable, explicit decoration.
|
|
708
|
-
- Keep labels globally unique and descriptive (including scope and package like `@myorg/mypkg.ClassName`).
|
|
709
|
-
- Use typed helpers for domain-level identities (IDs, tokens, domain types) so the compiler helps you avoid mistakes.
|
|
710
|
-
- Run with `devMarker` enabled during local development / CI to catch label collisions early.
|
|
711
|
-
|
|
712
|
-
---
|
|
713
|
-
|
|
714
808
|
## Deprecated API
|
|
715
809
|
|
|
716
810
|
### REGISTRY
|
|
@@ -739,6 +833,10 @@ const newRegistry = new SigilRegistry(); // can pass external map to constructor
|
|
|
739
833
|
updateOptions({ registry: newRegistry });
|
|
740
834
|
```
|
|
741
835
|
|
|
836
|
+
### typed
|
|
837
|
+
|
|
838
|
+
Typed was added to add types to output from `Sigilify` mixin, but now mixin do this by default.
|
|
839
|
+
|
|
742
840
|
---
|
|
743
841
|
|
|
744
842
|
## Phantom
|