@vicin/sigil 3.3.0 → 4.0.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 +4 -235
- package/CONTRIBUTING.md +24 -0
- package/LICENSE +3 -1
- package/README.md +199 -204
- package/dist/index.cjs +269 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +374 -0
- package/dist/index.d.ts +86 -258
- package/dist/index.global.js +113 -367
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +110 -376
- package/dist/index.js.map +1 -1
- package/package.json +43 -59
- package/dist/index.d.mts +0 -546
- package/dist/index.mjs +0 -510
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
# Sigil
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@vicin/sigil) [](https://www.npmjs.com/package/@vicin/sigil) [](LICENSE)  [](https://www.npmjs.com/package/@vicin/sigil) [](https://www.npmjs.com/package/@vicin/sigil) [](LICENSE)  [](https://github.com/ZiadTaha62/vicin-packages/actions/workflows/ci.yml) 
|
|
4
4
|
|
|
5
|
-
> - 🎉
|
|
5
|
+
> - 🎉 v4.0.0 is out! Happy coding! 😄💻
|
|
6
6
|
> - 📄 **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
**Sigil** — bulletproof class identity for large TypeScript projects.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Reliable `instanceof`-style checks that survive bundling, HMR, monorepos and realms + exact/subtype discrimination + lightweight nominal typing + and debug/log friendly labels/lineage.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
12
|
+
## Why Sigil?
|
|
13
|
+
|
|
14
|
+
- **Works when `instanceof` breaks** (multi-bundle, HMR, separate realms)
|
|
15
|
+
- **Exact class match (`isExactInstance`)** — not just "inherits from"
|
|
16
|
+
- **One-line nominal branding** (`declare [sigil]: ExtendSigil<…>`)
|
|
17
|
+
- **Human-readable class IDs in logs & debugging** (`SigilLabel`/`SigilLineage`)
|
|
18
|
+
- **Tiny (~0.9 kB brotli)**
|
|
19
|
+
- **Fast (near native `instanceof` speed)**
|
|
20
|
+
- **100% test coverage**
|
|
21
|
+
|
|
22
|
+
## Limitations
|
|
23
|
+
|
|
24
|
+
- **Identity is explicit and dependent on passed labels**, If same label is passed to two different classes `Sigil` will treat them as one
|
|
25
|
+
- **Identity is updated with every new label passed only**, If you stopped passing labels to child classes their identity will stop at last passed label
|
|
26
|
+
- **`Sigil` is not built for security**, identity can be forged
|
|
27
|
+
|
|
28
|
+
Earlier versions of `Sigil` tried to solve these limitations, however it was not `100%` reliable, so we decided to make this package minimal and stable, while adding new features in future package `@vicin/sigil-extend`
|
|
18
29
|
|
|
19
30
|
---
|
|
20
31
|
|
|
@@ -24,7 +35,7 @@
|
|
|
24
35
|
- [Install](#install)
|
|
25
36
|
- [Basic usage](#basic-usage)
|
|
26
37
|
- [Decorator pattern](#decorator-pattern)
|
|
27
|
-
- [
|
|
38
|
+
- [Function pattern](#function-pattern)
|
|
28
39
|
- [Migration](#migration)
|
|
29
40
|
- [Core concepts](#core-concepts)
|
|
30
41
|
- [Terminology](#terminology)
|
|
@@ -37,6 +48,7 @@
|
|
|
37
48
|
- [Minimal mode](#minimal-mode)
|
|
38
49
|
- [Strict mode](#strict-mode)
|
|
39
50
|
- [Hot module reload](#hot-module-reload)
|
|
51
|
+
- [Edge cases](#edge-cases)
|
|
40
52
|
- [Benchmarks](#benchmarks)
|
|
41
53
|
- [Bundle Size](#bundle-size)
|
|
42
54
|
- [Tests](#tests)
|
|
@@ -58,7 +70,9 @@ yarn add @vicin/sigil
|
|
|
58
70
|
pnpm add @vicin/sigil
|
|
59
71
|
```
|
|
60
72
|
|
|
61
|
-
Requires TypeScript 5.0+ for decorators;
|
|
73
|
+
Requires TypeScript 5.0+ for decorators; attach functions work on older versions. Node.js 18+ recommended.
|
|
74
|
+
|
|
75
|
+
> No tsconfig changes are needed as we use **Stage 3 decorators** which are supported by default in TypeScript 5.0+
|
|
62
76
|
|
|
63
77
|
### Basic usage
|
|
64
78
|
|
|
@@ -92,30 +106,28 @@ After opting into the `Sigil` contract, labels are passed to child classes to un
|
|
|
92
106
|
|
|
93
107
|
##### Decorator pattern
|
|
94
108
|
|
|
95
|
-
Apply a label with the `@
|
|
109
|
+
Apply a label with the `@AttachSigil` decorator:
|
|
96
110
|
|
|
97
111
|
```ts
|
|
98
|
-
import { Sigil,
|
|
112
|
+
import { Sigil, AttachSigil } from '@vicin/sigil';
|
|
99
113
|
|
|
100
|
-
@
|
|
114
|
+
@AttachSigil('@myorg/mypkg.User')
|
|
101
115
|
class User extends Sigil {}
|
|
102
116
|
```
|
|
103
117
|
|
|
104
|
-
#####
|
|
118
|
+
##### Function pattern
|
|
105
119
|
|
|
106
|
-
Apply a label using `
|
|
120
|
+
Apply a label using `attachSigil` function:
|
|
107
121
|
|
|
108
122
|
```ts
|
|
109
|
-
import { Sigil,
|
|
123
|
+
import { Sigil, attachSigil } from '@vicin/sigil';
|
|
110
124
|
|
|
111
|
-
class
|
|
112
|
-
|
|
125
|
+
class User extends Sigil {}
|
|
126
|
+
attachSigil(User, '@myorg/mypkg.User');
|
|
113
127
|
```
|
|
114
128
|
|
|
115
129
|
### Migration
|
|
116
130
|
|
|
117
|
-
Migrating old code into `Sigil` can be done with extra couple lines of code only:
|
|
118
|
-
|
|
119
131
|
1. Pass your base class to `Sigilify` mixin:
|
|
120
132
|
|
|
121
133
|
```ts
|
|
@@ -132,7 +144,7 @@ import { Sigil } from '@vicin/sigil';
|
|
|
132
144
|
class MyBaseClass extends Sigil {} // <-- add 'extends Sigil' here
|
|
133
145
|
```
|
|
134
146
|
|
|
135
|
-
Congratulations — you’ve opted into `Sigil
|
|
147
|
+
Congratulations — you’ve opted into `Sigil`, now you can start giving your classes identity by using `attachSigil` or `AttachSigil` helpers.
|
|
136
148
|
|
|
137
149
|
---
|
|
138
150
|
|
|
@@ -141,16 +153,15 @@ Congratulations — you’ve opted into `Sigil` and you can start replacing `ins
|
|
|
141
153
|
### Terminology
|
|
142
154
|
|
|
143
155
|
- **Label**: An identity (string) such as `@scope/pkg.ClassName`, must be unique for each `Sigil` class otherwise error is thrown.
|
|
144
|
-
- **
|
|
145
|
-
- **
|
|
146
|
-
- **isExactType**: Takes object argument and check if this object is an instance of calling class only. Can be called from class instances as well.
|
|
156
|
+
- **isInstance**: Takes object argument and check if this object is an instance of calling class or it's children. Can be called from class instances as well.
|
|
157
|
+
- **isExactInstance**: Takes object argument and check if this object is an instance of calling class only. Can be called from class instances as well.
|
|
147
158
|
- **[sigil]**: TypeScript symbol marker for nominal types.
|
|
148
159
|
|
|
149
160
|
---
|
|
150
161
|
|
|
151
162
|
### Purpose and Origins
|
|
152
163
|
|
|
153
|
-
Sigil addresses issues in large monorepos
|
|
164
|
+
Sigil addresses issues in large codebases and monorepos:
|
|
154
165
|
|
|
155
166
|
- **Unreliable `instanceof`:** Bundling cause class redefinitions, breaking checks.
|
|
156
167
|
|
|
@@ -159,13 +170,13 @@ Sigil addresses issues in large monorepos, HMR:
|
|
|
159
170
|
if (obj instanceof User) { ... }
|
|
160
171
|
|
|
161
172
|
// With Sigil
|
|
162
|
-
if (User.
|
|
163
|
-
if (User.
|
|
173
|
+
if (User.isInstance(obj)) { ... } // This still works even if User was bundled twice.
|
|
174
|
+
if (User.isExactInstance(obj)) { ... } // Or check for exactly same constructor not its children
|
|
164
175
|
```
|
|
165
176
|
|
|
166
|
-
Also by utilizing unique passed labels it
|
|
177
|
+
Also by utilizing unique passed labels it solves another problem in Domain-Driven Design (DDD):
|
|
167
178
|
|
|
168
|
-
- **Manual Branding Overhead:** Custom identifiers lead to boilerplate and maintenance issues, `Sigil`
|
|
179
|
+
- **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
180
|
|
|
170
181
|
```ts
|
|
171
182
|
import { sigil } from '@vicin/sigil';
|
|
@@ -178,46 +189,45 @@ type test1 = User extends Sigil ? true : false; // true
|
|
|
178
189
|
type test2 = Sigil extends User ? true : false; // false
|
|
179
190
|
```
|
|
180
191
|
|
|
192
|
+
- **Need for stable class id**: Most large projects implement their own labels (e.g. to be used in logs)
|
|
193
|
+
|
|
181
194
|
### Implementation Mechanics
|
|
182
195
|
|
|
183
196
|
- **Runtime Contract:** Established via extending `Sigil` or using `Sigilify` mixin.
|
|
184
|
-
- **Update metadata:** With each new child, use
|
|
197
|
+
- **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
198
|
|
|
186
199
|
```ts
|
|
187
|
-
import { Sigil,
|
|
200
|
+
import { Sigil, AttachSigil, sigil, ExtendSigil } from '@vicin/sigil';
|
|
188
201
|
|
|
189
|
-
@
|
|
202
|
+
@AttachSigil('@scope/package.MyClass') // <-- Run-time values update
|
|
190
203
|
class MyClass extends Sigil {
|
|
191
204
|
declare [sigil]: ExtendSigil<'MyClass', Sigil>; // <-- compile-time type update
|
|
192
205
|
}
|
|
193
206
|
```
|
|
194
207
|
|
|
195
|
-
You can avoid decorators and use
|
|
208
|
+
You can avoid decorators and use normal functions if needed:
|
|
196
209
|
|
|
197
210
|
```ts
|
|
198
|
-
import { Sigil,
|
|
211
|
+
import { Sigil, attachSigil, sigil, ExtendSigil } from '@vicin/sigil';
|
|
199
212
|
|
|
200
|
-
class
|
|
213
|
+
class MyClass extends Sigil {
|
|
201
214
|
declare [sigil]: ExtendSigil<'MyClass', Sigil>;
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
|
|
205
|
-
type MyClass = InstanceType<typeof MyClass>;
|
|
217
|
+
attachSigil(MyClass, '@scope/package.MyClass');
|
|
206
218
|
```
|
|
207
219
|
|
|
208
|
-
Note that you can't use `InstanceType` on `private` or `protected` classes, however you can use `GetPrototype<T>` in such cases.
|
|
209
|
-
|
|
210
220
|
### Example
|
|
211
221
|
|
|
212
222
|
```ts
|
|
213
|
-
import { Sigil,
|
|
223
|
+
import { Sigil, AttachSigil } from '@vicin/sigil';
|
|
214
224
|
|
|
215
|
-
@
|
|
225
|
+
@AttachSigil('@myorg/User')
|
|
216
226
|
class User extends Sigil {
|
|
217
227
|
declare [sigil]: ExtendSigil<'User', Sigil>;
|
|
218
228
|
}
|
|
219
229
|
|
|
220
|
-
@
|
|
230
|
+
@AttachSigil('@myorg/Admin')
|
|
221
231
|
class Admin extends User {
|
|
222
232
|
declare [sigil]: ExtendSigil<'Admin', User>;
|
|
223
233
|
}
|
|
@@ -226,92 +236,60 @@ const admin = new Admin();
|
|
|
226
236
|
const user = new User();
|
|
227
237
|
|
|
228
238
|
// Instanceof like behavior
|
|
229
|
-
console.log(Admin.
|
|
230
|
-
console.log(Admin.
|
|
231
|
-
console.log(User.
|
|
232
|
-
console.log(User.
|
|
239
|
+
console.log(Admin.isInstance(admin)); // true
|
|
240
|
+
console.log(Admin.isInstance(user)); // false
|
|
241
|
+
console.log(User.isInstance(admin)); // true
|
|
242
|
+
console.log(User.isInstance(user)); // true
|
|
233
243
|
|
|
234
244
|
// Exact checks
|
|
235
|
-
console.log(Admin.
|
|
236
|
-
console.log(Admin.
|
|
237
|
-
console.log(User.
|
|
238
|
-
console.log(User.
|
|
245
|
+
console.log(Admin.isExactInstance(admin)); // true
|
|
246
|
+
console.log(Admin.isExactInstance(user)); // false
|
|
247
|
+
console.log(User.isExactInstance(user)); // true
|
|
248
|
+
console.log(User.isExactInstance(admin)); // false (Admin is child indeed but this checks for user specifically)
|
|
239
249
|
|
|
240
250
|
// Can use checks from instances
|
|
241
|
-
console.log(admin.
|
|
242
|
-
console.log(user.
|
|
243
|
-
console.log(admin.
|
|
244
|
-
console.log(user.
|
|
251
|
+
console.log(admin.isInstance(user)); // false
|
|
252
|
+
console.log(user.isInstance(admin)); // true
|
|
253
|
+
console.log(admin.isExactInstance(user)); // false
|
|
254
|
+
console.log(user.isExactInstance(admin)); // false
|
|
245
255
|
|
|
246
256
|
// Type checks are nominal
|
|
247
257
|
type test1 = Admin extends User ? true : false; // true
|
|
248
258
|
type test2 = User extends Admin ? true : false; // false
|
|
249
259
|
|
|
250
260
|
// Passed label must be unique (enforced by Sigil) so can be used as stable Id for class
|
|
251
|
-
// Also 'SigilLabelLineage'
|
|
261
|
+
// Also 'SigilLabelLineage' is useful for logging & debugging
|
|
252
262
|
console.log(Admin.SigilLabel); // '@myorg/Admin'
|
|
253
|
-
console.log(Admin.SigilEffectiveLabel); // '@myorg/Admin'
|
|
254
263
|
console.log(Admin.SigilLabelLineage); // ['Sigil', '@myorg/User', '@myorg/Admin']
|
|
255
|
-
console.log(
|
|
256
|
-
console.log(admin.
|
|
257
|
-
console.log(admin.getSigilEffectiveLabel()); // '@myorg/Admin'
|
|
258
|
-
console.log(admin.getSigilLabelLineage()); // ['Sigil', '@myorg/User', '@myorg/Admin']
|
|
259
|
-
console.log(admin.getSigilLabelSet()); // Set(['Sigil', '@myorg/User', '@myorg/Admin'])
|
|
264
|
+
console.log(admin.SigilLabel); // '@myorg/Admin'
|
|
265
|
+
console.log(admin.SigilLabelLineage); // ['Sigil', '@myorg/User', '@myorg/Admin']
|
|
260
266
|
```
|
|
261
267
|
|
|
262
268
|
### Errors & throws
|
|
263
269
|
|
|
264
270
|
Run-time errors that can be thrown by `Sigil`:
|
|
265
271
|
|
|
266
|
-
#### Double
|
|
272
|
+
#### Double Sigilify
|
|
267
273
|
|
|
268
274
|
```ts
|
|
269
275
|
class A {}
|
|
270
|
-
|
|
271
|
-
const C = Sigilify(B, 'B'); // Throws: [Sigil Error] Class 'Sigilified' with label 'A' is already sigilified
|
|
272
|
-
|
|
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
|
|
276
|
+
Sigilify(Sigilify(A, 'A'), 'B'); // Throws: [Sigil Error] Class 'Sigilified' with label 'A' is already sigilified
|
|
279
277
|
|
|
280
|
-
|
|
281
|
-
@
|
|
282
|
-
class A {} // Throws: [Sigil Error] 'WithSigil' decorator accept only Sigil classes but used on class 'A'
|
|
283
|
-
|
|
284
|
-
withSigil(class A {}); // Throws: [Sigil Error] 'WithSigil' decorator accept only Sigil classes but used on class 'A'
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
#### Double `@WithSigil() / withSigil()`
|
|
288
|
-
|
|
289
|
-
```ts
|
|
290
|
-
@WithSigil('B')
|
|
291
|
-
@WithSigil('A')
|
|
278
|
+
@AttachSigil('B')
|
|
279
|
+
@AttachSigil('A')
|
|
292
280
|
class A extends Sigil {} // Throws: [Sigil Error] Class 'A' with label 'A' is already sigilified
|
|
293
281
|
|
|
294
|
-
class _A extends Sigil {}
|
|
295
|
-
withSigil(withSigil(_A, 'A'), 'B'); // Throws: [Sigil Error] Class 'A' with label 'A' is already sigilified
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
#### No label is passed with `autofillLabels: false`
|
|
299
|
-
|
|
300
|
-
```ts
|
|
301
|
-
updateSigilOptions({ autofillLabels: false });
|
|
302
|
-
|
|
303
282
|
class A extends Sigil {}
|
|
304
|
-
|
|
283
|
+
attachSigil(attachSigil(A, 'A'), 'B'); // Throws: [Sigil Error] Class 'A' with label 'A' is already sigilified
|
|
305
284
|
```
|
|
306
285
|
|
|
307
|
-
####
|
|
286
|
+
#### `@AttachSigil() / attachSigil()` on non-Sigil class
|
|
308
287
|
|
|
309
288
|
```ts
|
|
310
|
-
@
|
|
311
|
-
class A
|
|
289
|
+
@AttachSigil('A') // Throws: [Sigil Error] 'AttachSigil' decorator accept only Sigil classes but used on class 'A'
|
|
290
|
+
class A {}
|
|
312
291
|
|
|
313
|
-
|
|
314
|
-
class B extends Sigil {} // Throws: [Sigil Error] Passed label 'Label' to class 'B' is re-used, passed labels must be unique
|
|
292
|
+
attachSigil(class A {}); // Throws: [Sigil Error] 'AttachSigil' function accept only Sigil classes but used on class 'A'
|
|
315
293
|
```
|
|
316
294
|
|
|
317
295
|
#### Invalid label format
|
|
@@ -319,23 +297,18 @@ class B extends Sigil {} // Throws: [Sigil Error] Passed label 'Label' to class
|
|
|
319
297
|
```ts
|
|
320
298
|
updateSigilOptions({ labelValidation: RECOMMENDED_LABEL_REGEX });
|
|
321
299
|
|
|
322
|
-
@
|
|
300
|
+
@AttachSigil('InvalidLabel')
|
|
323
301
|
class A extends Sigil {} // Throws: [Sigil Error] Invalid Sigil label 'InvalidLabel'. Make sure that supplied label matches validation regex or function
|
|
324
302
|
```
|
|
325
303
|
|
|
326
|
-
####
|
|
304
|
+
#### Attach sigil to parent after child
|
|
327
305
|
|
|
328
306
|
```ts
|
|
329
|
-
|
|
330
|
-
class
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
#### Invalid options passed to `updateOptions`
|
|
307
|
+
class Parent extends Sigil {}
|
|
308
|
+
class Child extends Parent {}
|
|
334
309
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
updateSigilOptions({ labelValidation: 123 as any }); // Throws: 'updateSigilOptions.labelValidation' must be null, function or RegExp
|
|
338
|
-
updateSigilOptions({ skipLabelUniquenessCheck: 'str' as any }); // Throws: 'updateSigilOptions.skipLabelUniquenessCheck' must be boolean
|
|
310
|
+
attachSigil(Child, 'Child');
|
|
311
|
+
attachSigil(Parent, 'Parent'); // Throws: [Sigil Error] Class 'Parent' with label 'Sigil' is already sigilified
|
|
339
312
|
```
|
|
340
313
|
|
|
341
314
|
---
|
|
@@ -353,24 +326,21 @@ updateSigilOptions({ skipLabelUniquenessCheck: 'str' as any }); // Throws: 'upda
|
|
|
353
326
|
- `SigilError`
|
|
354
327
|
|
|
355
328
|
- **Decorator:**
|
|
356
|
-
- `
|
|
329
|
+
- `AttachSigil(label, opts?)`
|
|
357
330
|
|
|
358
|
-
- **
|
|
359
|
-
- `
|
|
331
|
+
- **Attach function:**
|
|
332
|
+
- `attachSigil(Class, label, opts?)`
|
|
360
333
|
|
|
361
334
|
- **Helpers:**
|
|
362
335
|
- `isSigilCtor(ctor)`
|
|
363
336
|
- `isSigilInstance(inst)`
|
|
364
|
-
- `
|
|
337
|
+
- `hasOwnSigil(ctor)`
|
|
365
338
|
|
|
366
339
|
- **Options:**
|
|
367
340
|
- `updateSigilOptions(opts)`
|
|
368
341
|
- `RECOMMENDED_LABEL_REGEX`
|
|
369
342
|
|
|
370
343
|
- **Types:**
|
|
371
|
-
- `ISigil<Label, ParentSigil?>`
|
|
372
|
-
- `ISigilStatic<Label, ParentSigil?>`
|
|
373
|
-
- `ISigilInstance<Label, ParentSigil?>`
|
|
374
344
|
- `SigilOf<T>`
|
|
375
345
|
- `ExtendSigil<Label, Parent>`
|
|
376
346
|
- `GetPrototype<Class>`
|
|
@@ -382,12 +352,12 @@ updateSigilOptions({ skipLabelUniquenessCheck: 'str' as any }); // Throws: 'upda
|
|
|
382
352
|
- `SigilError`: an `Error` class decorated with a `Sigil` so it can be identified at runtime.
|
|
383
353
|
- `Sigilify(Base, label, opts?)`: mixin function that returns a new constructor with `Sigil` types and instance helpers.
|
|
384
354
|
- `SigilifyAbstract(Base, label, opts?)`: Same as `Sigilify` but for abstract classes.
|
|
385
|
-
- `
|
|
386
|
-
- `
|
|
355
|
+
- `AttachSigil(label, opts?)`: class decorator that attaches `Sigil` metadata at declaration time.
|
|
356
|
+
- `attachSigil(Class, label, opts?)`: function that validates and decorates an existing class constructor.
|
|
387
357
|
- `isSigilCtor(value)`: `true` if `value` is a `Sigil` constructor.
|
|
388
358
|
- `isSigilInstance(value)`: `true` if `value` is an instance of a `Sigil` constructor.
|
|
389
|
-
- `
|
|
390
|
-
- `updateSigilOptions(opts)`: change global runtime options of `Sigil` library (e.g., `
|
|
359
|
+
- `hasOwnSigil(ctor)`: `true` if new sigil label is attached to `ctor`
|
|
360
|
+
- `updateSigilOptions(opts)`: change global runtime options of `Sigil` library (e.g., `labelValidation`).
|
|
391
361
|
- `RECOMMENDED_LABEL_REGEX`: regex that ensures structure of `@scope/package.ClassName` to all labels, it's advised to use it as your `SigilOptions.labelValidation`
|
|
392
362
|
|
|
393
363
|
### Instance & static helpers provided by Sigilified constructors
|
|
@@ -395,20 +365,18 @@ updateSigilOptions({ skipLabelUniquenessCheck: 'str' as any }); // Throws: 'upda
|
|
|
395
365
|
When a constructor is sigilified it will expose the following **static** getters/methods:
|
|
396
366
|
|
|
397
367
|
- `SigilLabel` — the identity label string.
|
|
398
|
-
- `SigilEffectiveLabel` — the human label string.
|
|
399
368
|
- `SigilLabelLineage` — readonly array of labels representing parent → child for debugging.
|
|
400
|
-
- `
|
|
401
|
-
- `
|
|
402
|
-
- `
|
|
369
|
+
- `hasOwnSigil` — check if new sigil label is attached to this class.
|
|
370
|
+
- `isInstance(other)` — check if other is an instance of this constructor or its children.
|
|
371
|
+
- `isExactInstance(other) `— check if other is an instance exactly this constructor.
|
|
403
372
|
|
|
404
373
|
Instances of sigilified classes expose instance helpers:
|
|
405
374
|
|
|
406
|
-
- `
|
|
407
|
-
- `
|
|
408
|
-
- `
|
|
409
|
-
- `
|
|
410
|
-
- `
|
|
411
|
-
- `isExactType(other) `— check if other is an instance exactly the same constructor.
|
|
375
|
+
- `SigilLabel` — the identity label string.
|
|
376
|
+
- `SigilLabelLineage` — readonly array of labels representing parent → child for debugging.
|
|
377
|
+
- `hasOwnSigil` — check if new sigil label is attached to this class.
|
|
378
|
+
- `isInstance(other)` — check if other is an instance of the same class or its children as this.
|
|
379
|
+
- `isExactInstance(other) `— check if other is an instance exactly the same constructor.
|
|
412
380
|
|
|
413
381
|
---
|
|
414
382
|
|
|
@@ -420,64 +388,72 @@ Customize behavior globally at startup:
|
|
|
420
388
|
import { updateSigilOptions } from '@vicin/sigil';
|
|
421
389
|
|
|
422
390
|
updateSigilOptions({
|
|
423
|
-
autofillLabels: true, // Automatically label unlabeled subclasses
|
|
424
391
|
labelValidation: null, // Function or regex, Enforce label format
|
|
425
|
-
skipLabelUniquenessCheck: false, // Skip uniqueness check for labels, should be used in HMR set-ups only
|
|
426
392
|
});
|
|
427
393
|
```
|
|
428
394
|
|
|
429
|
-
Values defined in previous example are defaults, per-class overrides available in mixin
|
|
395
|
+
Values defined in previous example are defaults, per-class overrides available in mixin and attach function / decorator.
|
|
430
396
|
|
|
431
397
|
---
|
|
432
398
|
|
|
433
|
-
##
|
|
399
|
+
## Edge cases
|
|
400
|
+
|
|
401
|
+
### Attach sigil to parent after child
|
|
434
402
|
|
|
435
|
-
|
|
403
|
+
When attaching `Sigil` labels to classes order from parent -> child must be respected
|
|
436
404
|
|
|
437
405
|
```ts
|
|
438
|
-
|
|
406
|
+
class Parent extends Sigil {}
|
|
407
|
+
class Child extends Parent {}
|
|
439
408
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
class B extends A {}
|
|
443
|
-
class C extends B {}
|
|
409
|
+
attachSigil(Child, 'Child');
|
|
410
|
+
attachSigil(Parent, 'Parent'); // Throws: [Sigil Error] Class 'Parent' with label 'Sigil' is already sigilified
|
|
444
411
|
```
|
|
445
412
|
|
|
446
|
-
|
|
413
|
+
### Accessing Sigil `metadata` before running `attachSigil` function
|
|
447
414
|
|
|
448
|
-
If you
|
|
415
|
+
If you didn't make sure that `attachSigil` runs right after class declaration and used one of `Sigil` methods this will occur:
|
|
449
416
|
|
|
450
417
|
```ts
|
|
451
|
-
|
|
418
|
+
class A extends Sigil {}
|
|
452
419
|
|
|
453
|
-
|
|
454
|
-
```
|
|
420
|
+
console.log(A.SigilLabel); // returns label of the parent (Sigil in our case)
|
|
455
421
|
|
|
456
|
-
|
|
422
|
+
attachSigil(A, 'A');
|
|
457
423
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
## Hot module reload
|
|
424
|
+
console.log(A.SigilLabel); // A
|
|
425
|
+
```
|
|
461
426
|
|
|
462
|
-
|
|
463
|
-
To avoid this you can set global options to skip label uniqueness check at the start of app:
|
|
427
|
+
To avoid this bug entirely you can use the return of `attachSigil` in your code so you are enforced to respect order:
|
|
464
428
|
|
|
465
429
|
```ts
|
|
466
|
-
|
|
430
|
+
class _A extends Sigil {}
|
|
431
|
+
|
|
432
|
+
const A = attachSigil(_A, 'A');
|
|
433
|
+
type A = InstanceType<typeof A>;
|
|
467
434
|
|
|
468
|
-
|
|
435
|
+
console.log(A.SigilLabel); // A
|
|
469
436
|
```
|
|
470
437
|
|
|
471
|
-
|
|
472
|
-
|
|
438
|
+
Note that you can't use `InstanceType` on `private` or `protected` classes, however you can use `GetPrototype<T>` in such cases.
|
|
439
|
+
|
|
440
|
+
#### Static blocks & IIFE static initializer
|
|
441
|
+
|
|
442
|
+
Stage 3 decorators (`AttachSigil`) and function (`attachSigil`) runs after IIFE and static blocks, so accessing `Sigil` metadata inside them should be avoided
|
|
473
443
|
|
|
474
444
|
```ts
|
|
475
|
-
@
|
|
476
|
-
class
|
|
445
|
+
@AttachSigil('A')
|
|
446
|
+
class A extends Sigil {
|
|
447
|
+
static IIFE = (() => {
|
|
448
|
+
const label = A.SigilLabel; // returns label of the parent (Sigil in our case)
|
|
449
|
+
})();
|
|
450
|
+
|
|
451
|
+
static {
|
|
452
|
+
const label = this.SigilLabel; // returns label of the parent (Sigil in our case)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
477
455
|
```
|
|
478
456
|
|
|
479
|
-
With this approach `skipLabelUniquenessCheck` affects only `HmrClass`, and if `HmrClassLabel` or any other label is re-used error is thrown immediately
|
|
480
|
-
|
|
481
457
|
---
|
|
482
458
|
|
|
483
459
|
## Benchmarks
|
|
@@ -486,68 +462,95 @@ Sigil is built for **real-world performance**. Below are the latest micro-benchm
|
|
|
486
462
|
|
|
487
463
|
**Running Tests**
|
|
488
464
|
|
|
489
|
-
To run benchmarks on your machine fetch source code from [github](https://github.com/ZiadTaha62/
|
|
465
|
+
To run benchmarks on your machine fetch source code from [github](https://github.com/ZiadTaha62/vicin-packages) then:
|
|
490
466
|
|
|
491
467
|
```bash
|
|
492
|
-
|
|
493
|
-
|
|
468
|
+
pnpm install
|
|
469
|
+
pnpm run bench --filter @vicin/sigil
|
|
494
470
|
```
|
|
495
471
|
|
|
496
472
|
### 1. Runtime Type Checking
|
|
497
473
|
|
|
498
|
-
|
|
|
499
|
-
|
|
|
500
|
-
|
|
|
501
|
-
|
|
|
502
|
-
|
|
|
503
|
-
|
|
|
504
|
-
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
474
|
+
| Test Name | Ops/sec (hz) | Mean (ms) | p99 (ms) | RME | Samples |
|
|
475
|
+
| -------------------------- | ------------ | --------- | -------- | ------ | --------- |
|
|
476
|
+
| **Depth 0** | | | | | |
|
|
477
|
+
| instanceof | 11,986,628 | 0.0001 | 0.0002 | ±0.58% | 5,993,314 |
|
|
478
|
+
| isInstance (Ctor) | 8,848,040 | 0.0001 | 0.0003 | ±0.63% | 4,424,020 |
|
|
479
|
+
| isInstance (instance) | 9,246,878 | 0.0001 | 0.0002 | ±0.86% | 4,623,440 |
|
|
480
|
+
| isExactInstance (Ctor) | 5,114,916 | 0.0002 | 0.0003 | ±0.44% | 2,557,459 |
|
|
481
|
+
| isExactInstance (instance) | 5,553,370 | 0.0002 | 0.0003 | ±0.41% | 2,776,686 |
|
|
482
|
+
| **Depth 5** | | | | | |
|
|
483
|
+
| instanceof | 8,399,150 | 0.0001 | 0.0002 | ±0.48% | 4,199,651 |
|
|
484
|
+
| isInstance (Ctor) | 7,385,890 | 0.0001 | 0.0003 | ±2.01% | 3,692,945 |
|
|
485
|
+
| isInstance (instance) | 7,862,474 | 0.0001 | 0.0003 | ±0.77% | 3,931,238 |
|
|
486
|
+
| isExactInstance (Ctor) | 4,433,700 | 0.0002 | 0.0004 | ±0.64% | 2,216,851 |
|
|
487
|
+
| isExactInstance (instance) | 4,898,498 | 0.0002 | 0.0004 | ±0.34% | 2,449,249 |
|
|
488
|
+
| **Depth 10** | | | | | |
|
|
489
|
+
| instanceof | 7,144,486 | 0.0001 | 0.0003 | ±0.63% | 3,572,243 |
|
|
490
|
+
| isInstance (Ctor) | 7,165,920 | 0.0001 | 0.0003 | ±0.49% | 3,582,960 |
|
|
491
|
+
| isInstance (instance) | 7,834,056 | 0.0001 | 0.0003 | ±1.16% | 3,917,029 |
|
|
492
|
+
| isExactInstance (Ctor) | 4,292,578 | 0.0002 | 0.0004 | ±0.34% | 2,146,290 |
|
|
493
|
+
| isExactInstance (instance) | 5,020,923 | 0.0002 | 0.0004 | ±0.28% | 2,510,462 |
|
|
494
|
+
| **Depth 15** | | | | | |
|
|
495
|
+
| instanceof | 7,116,266 | 0.0001 | 0.0002 | ±0.38% | 3,558,134 |
|
|
496
|
+
| isInstance (Ctor) | 6,308,498 | 0.0002 | 0.0003 | ±0.41% | 3,154,249 |
|
|
497
|
+
| isInstance (instance) | 6,403,126 | 0.0002 | 0.0003 | ±0.70% | 3,201,564 |
|
|
498
|
+
| isExactInstance (Ctor) | 3,678,280 | 0.0003 | 0.0005 | ±0.37% | 1,839,141 |
|
|
499
|
+
| isExactInstance (instance) | 3,753,618 | 0.0003 | 0.0005 | ±0.39% | 1,876,810 |
|
|
500
|
+
|
|
501
|
+
> **Key takeaway**:
|
|
502
|
+
>
|
|
503
|
+
> `isInstance` Efficiency: Demonstrates high parity with native instanceof, maintaining a minimal performance delta (approx. 15% overhead).
|
|
504
|
+
> `isExactInstance` Cost-Benefit: While introducing a ~2x overhead compared to native operations, it remains performant in absolute terms. The throughput cost is a deliberate trade-off for the increased precision required for exact matching.
|
|
508
505
|
|
|
509
506
|
### 2. Class Definition & Instance Creation
|
|
510
507
|
|
|
511
|
-
|
|
|
512
|
-
|
|
|
513
|
-
|
|
|
514
|
-
| Empty Sigil
|
|
515
|
-
| Small (
|
|
516
|
-
| Large (
|
|
517
|
-
|
|
|
518
|
-
|
|
|
519
|
-
|
|
|
520
|
-
|
|
|
521
|
-
|
|
|
508
|
+
| Test Name | Ops/sec (hz) | Mean (ms) | p99 (ms) | RME | Samples |
|
|
509
|
+
| ------------------------------------- | ----------------- | ---------------- | ---------------- | ------- | --------- |
|
|
510
|
+
| **Definition (Module Load Time)** | | | | | |
|
|
511
|
+
| Define Empty (Plain vs Sigil) | 122,640 vs 13,154 | 0.0082 vs 0.0760 | 0.0206 vs 0.1413 | ±11.36% | 6,578 |
|
|
512
|
+
| Define Small (Plain vs Sigil) | 75,363 vs 12,003 | 0.0133 vs 0.0833 | 0.0267 vs 0.1596 | ±5.98% | 6,002 |
|
|
513
|
+
| Define Large (Plain vs Sigil) | 53,392 vs 11,978 | 0.0187 vs 0.0835 | 0.0352 vs 0.1471 | ±6.13% | 5,990 |
|
|
514
|
+
| Define Depth 3 (Plain vs Sigil) | 22,802 vs 5,205 | 0.0439 vs 0.1921 | 0.0850 vs 0.3139 | ±14.68% | 2,603 |
|
|
515
|
+
| Define Depth 5 (Plain vs Sigil) | 12,003 vs 3,369 | 0.0833 vs 0.2968 | 0.1864 vs 0.5488 | ±7.31% | 1,685 |
|
|
516
|
+
| Define Depth 10 (Plain vs Sigil) | 5,477 vs 1,758 | 0.1826 vs 0.5685 | 0.3402 vs 1.1148 | ±9.62% | 880 |
|
|
517
|
+
| **Instantiation (Runtime/Hot Path)** | | | | | |
|
|
518
|
+
| Instantiate Empty (Plain vs Sigil) | 12.0M vs 11.9M | 0.0001 vs 0.0001 | 0.0002 vs 0.0002 | ±0.76% | 5,999,555 |
|
|
519
|
+
| Instantiate Small (Plain vs Sigil) | 12.5M vs 11.4M | 0.0001 vs 0.0001 | 0.0001 vs 0.0002 | ±0.89% | 5,739,986 |
|
|
520
|
+
| Instantiate Large (Plain vs Sigil) | 12.4M vs 12.4M | 0.0001 vs 0.0001 | 0.0002 vs 0.0001 | ±0.74% | 6,205,306 |
|
|
521
|
+
| Instantiate Depth 3 (Plain vs Sigil) | 12.0M vs 12.1M | 0.0001 vs 0.0001 | 0.0002 vs 0.0002 | ±1.01% | 6,092,517 |
|
|
522
|
+
| Instantiate Depth 5 (Plain vs Sigil) | 9.09M vs 8.54M | 0.0001 vs 0.0001 | 0.0002 vs 0.0003 | ±2.24% | 4,272,328 |
|
|
523
|
+
| Instantiate Depth 10 (Plain vs Sigil) | 2.87M vs 1.72M | 0.0003 vs 0.0006 | 0.0008 vs 0.0011 | ±0.75% | 862,564 |
|
|
522
524
|
|
|
523
525
|
> **Key takeaways**:
|
|
524
526
|
>
|
|
525
|
-
> -
|
|
526
|
-
>
|
|
527
|
+
> Front-Loaded Definition Overhead: Sigil introduces main overhead during the class definition phase (roughly 6x to 10x slower than plain classes). This is a one-time cost per class, typically occurring during module evaluation or registry setup.
|
|
528
|
+
>
|
|
529
|
+
> Runtime Performance Parity: Once the class is defined, Sigil achieves near-native instantiation throughput. For standard object creation, the delta is negligible, ensuring that the library does not bottleneck high-frequency allocation patterns.
|
|
530
|
+
>
|
|
531
|
+
> Scale Stability: The overhead of Sigil remains constant regardless of the number of properties or methods added (Small vs. Large). The definition speed for a "Small Sigil" and a "Large Sigil" is nearly identical (~12k hz), suggesting that the setup logic is O(1) relative to class member count.
|
|
527
532
|
|
|
528
533
|
---
|
|
529
534
|
|
|
530
535
|
## Bundle Size
|
|
531
536
|
|
|
532
|
-
**Less than 1
|
|
537
|
+
**Less than 1 KB (908 B)** (minified + Brotli, including all dependencies)
|
|
533
538
|
|
|
534
539
|
This makes Sigil one of the smallest full-featured solutions for nominal typing + reliable runtime identity.
|
|
535
540
|
|
|
536
541
|
**Running Tests**
|
|
537
542
|
|
|
538
|
-
To verify bundle size fetch source code from [github](https://github.com/ZiadTaha62/
|
|
543
|
+
To verify bundle size fetch source code from [github](https://github.com/ZiadTaha62/vicin-packages) then:
|
|
539
544
|
|
|
540
545
|
```bash
|
|
541
|
-
|
|
542
|
-
|
|
546
|
+
pnpm install
|
|
547
|
+
pnpm run size --filter @vicin/sigil
|
|
543
548
|
```
|
|
544
549
|
|
|
545
550
|
---
|
|
546
551
|
|
|
547
552
|
## Tests
|
|
548
553
|
|
|
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.
|
|
550
|
-
|
|
551
554
|
**Coverage Status**
|
|
552
555
|
|
|
553
556
|
We maintain **100%** test coverage across the entire codebase to ensure that runtime metadata remains consistent and predictable.
|
|
@@ -559,21 +562,13 @@ We maintain **100%** test coverage across the entire codebase to ensure that run
|
|
|
559
562
|
| Funcs | 100% |
|
|
560
563
|
| Lines | 100% |
|
|
561
564
|
|
|
562
|
-
**Key Test Areas**
|
|
563
|
-
|
|
564
|
-
- **Mixins, Decorators & HOFs:** Validating `Sigilify`, `WithSigil` and `withSigil` behaviors.
|
|
565
|
-
- **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.
|
|
567
|
-
- **Lineage:** Verifying that `isOfType` and `isExactType` work across complex inheritance chains.
|
|
568
|
-
- **Error Handling:** Strict validation for all errors and throws.
|
|
569
|
-
|
|
570
565
|
**Running Tests**
|
|
571
566
|
|
|
572
|
-
To run the test suite locally and generate a coverage report, fetch source code from [github](https://github.com/ZiadTaha62/
|
|
567
|
+
To run the test suite locally and generate a coverage report, fetch source code from [github](https://github.com/ZiadTaha62/vicin-packages) then:
|
|
573
568
|
|
|
574
569
|
```bash
|
|
575
|
-
|
|
576
|
-
|
|
570
|
+
pnpm install
|
|
571
|
+
pnpm run test --filter @vicin/sigil
|
|
577
572
|
```
|
|
578
573
|
|
|
579
574
|
---
|