mutts 1.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/README.md +150 -0
- package/dist/chunks/decorator-BXsign4Z.js +176 -0
- package/dist/chunks/decorator-BXsign4Z.js.map +1 -0
- package/dist/chunks/decorator-CPbZNnsX.esm.js +168 -0
- package/dist/chunks/decorator-CPbZNnsX.esm.js.map +1 -0
- package/dist/decorator.d.ts +50 -0
- package/dist/decorator.esm.js +2 -0
- package/dist/decorator.esm.js.map +1 -0
- package/dist/decorator.js +11 -0
- package/dist/decorator.js.map +1 -0
- package/dist/destroyable.d.ts +48 -0
- package/dist/destroyable.esm.js +91 -0
- package/dist/destroyable.esm.js.map +1 -0
- package/dist/destroyable.js +98 -0
- package/dist/destroyable.js.map +1 -0
- package/dist/eventful.d.ts +11 -0
- package/dist/eventful.esm.js +88 -0
- package/dist/eventful.esm.js.map +1 -0
- package/dist/eventful.js +90 -0
- package/dist/eventful.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.esm.js +7 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/indexable.d.ts +31 -0
- package/dist/indexable.esm.js +85 -0
- package/dist/indexable.esm.js.map +1 -0
- package/dist/indexable.js +89 -0
- package/dist/indexable.js.map +1 -0
- package/dist/mutts.umd.js +2 -0
- package/dist/mutts.umd.js.map +1 -0
- package/dist/mutts.umd.min.js +2 -0
- package/dist/mutts.umd.min.js.map +1 -0
- package/dist/promiseChain.d.ts +11 -0
- package/dist/promiseChain.esm.js +72 -0
- package/dist/promiseChain.esm.js.map +1 -0
- package/dist/promiseChain.js +74 -0
- package/dist/promiseChain.js.map +1 -0
- package/dist/reactive.d.ts +114 -0
- package/dist/reactive.esm.js +1455 -0
- package/dist/reactive.esm.js.map +1 -0
- package/dist/reactive.js +1472 -0
- package/dist/reactive.js.map +1 -0
- package/dist/std-decorators.d.ts +17 -0
- package/dist/std-decorators.esm.js +161 -0
- package/dist/std-decorators.esm.js.map +1 -0
- package/dist/std-decorators.js +169 -0
- package/dist/std-decorators.js.map +1 -0
- package/docs/decorator.md +300 -0
- package/docs/destroyable.md +294 -0
- package/docs/events.md +225 -0
- package/docs/indexable.md +561 -0
- package/docs/promiseChain.md +218 -0
- package/docs/reactive.md +2072 -0
- package/docs/std-decorators.md +558 -0
- package/package.json +132 -0
- package/src/decorator.test.ts +495 -0
- package/src/decorator.ts +205 -0
- package/src/destroyable.test.ts +155 -0
- package/src/destroyable.ts +158 -0
- package/src/eventful.test.ts +380 -0
- package/src/eventful.ts +69 -0
- package/src/index.ts +7 -0
- package/src/indexable.test.ts +388 -0
- package/src/indexable.ts +124 -0
- package/src/promiseChain.test.ts +201 -0
- package/src/promiseChain.ts +99 -0
- package/src/reactive/array.test.ts +923 -0
- package/src/reactive/array.ts +352 -0
- package/src/reactive/core.test.ts +1663 -0
- package/src/reactive/core.ts +866 -0
- package/src/reactive/index.ts +28 -0
- package/src/reactive/interface.test.ts +1477 -0
- package/src/reactive/interface.ts +231 -0
- package/src/reactive/map.test.ts +866 -0
- package/src/reactive/map.ts +162 -0
- package/src/reactive/set.test.ts +289 -0
- package/src/reactive/set.ts +142 -0
- package/src/std-decorators.test.ts +679 -0
- package/src/std-decorators.ts +182 -0
- package/src/utils.ts +52 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
// Test the decorator system with all decorator types
|
|
2
|
+
import { decorator } from './decorator'
|
|
3
|
+
|
|
4
|
+
describe('Decorator System', () => {
|
|
5
|
+
describe('Method Decorators', () => {
|
|
6
|
+
it('should wrap method calls', () => {
|
|
7
|
+
const methodDecorator = decorator({
|
|
8
|
+
method(original, _name) {
|
|
9
|
+
return function (this: any, ...args: any[]) {
|
|
10
|
+
return `wrapped: ${original.apply(this, args)}`
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
class TestClass {
|
|
16
|
+
@methodDecorator
|
|
17
|
+
greet(name: string) {
|
|
18
|
+
return `Hello ${name}`
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const instance = new TestClass()
|
|
23
|
+
expect(instance.greet('World')).toBe('wrapped: Hello World')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should work with multiple methods', () => {
|
|
27
|
+
const methodDecorator = decorator({
|
|
28
|
+
method(original, name) {
|
|
29
|
+
return function (this: any, ...args: any[]) {
|
|
30
|
+
return `${String(name)}: ${original.apply(this, args)}`
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
class TestClass {
|
|
36
|
+
@methodDecorator
|
|
37
|
+
first() {
|
|
38
|
+
return 'first'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@methodDecorator
|
|
42
|
+
second() {
|
|
43
|
+
return 'second'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const instance = new TestClass()
|
|
48
|
+
expect(instance.first()).toBe('first: first')
|
|
49
|
+
expect(instance.second()).toBe('second: second')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('Class Decorators', () => {
|
|
54
|
+
it('should modify class behavior', () => {
|
|
55
|
+
const classDecorator = decorator({
|
|
56
|
+
class: (target) => {
|
|
57
|
+
// Add a static property to the class
|
|
58
|
+
;(target as any).decorated = true
|
|
59
|
+
return target
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
@classDecorator
|
|
64
|
+
class TestClass {
|
|
65
|
+
static original = 'original'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
expect((TestClass as any).decorated).toBe(true)
|
|
69
|
+
expect(TestClass.original).toBe('original')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should work with class inheritance', () => {
|
|
73
|
+
const classDecorator = decorator({
|
|
74
|
+
class: (target) => {
|
|
75
|
+
;(target as any).decorated = true
|
|
76
|
+
return target
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
@classDecorator
|
|
81
|
+
class BaseClass {}
|
|
82
|
+
|
|
83
|
+
class ExtendedClass extends BaseClass {}
|
|
84
|
+
|
|
85
|
+
expect((BaseClass as any).decorated).toBe(true)
|
|
86
|
+
// In legacy decorators, the decorator is applied to the class itself
|
|
87
|
+
// so ExtendedClass should also have the decorated property
|
|
88
|
+
expect((ExtendedClass as any).decorated).toBe(true)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('Getter Decorators', () => {
|
|
93
|
+
it('should wrap getter calls', () => {
|
|
94
|
+
const getterDecorator = decorator({
|
|
95
|
+
getter(original, _name) {
|
|
96
|
+
return function (this: any) {
|
|
97
|
+
return `wrapped: ${original.call(this)}`
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
class TestClass {
|
|
103
|
+
private _value = 'test'
|
|
104
|
+
|
|
105
|
+
@getterDecorator
|
|
106
|
+
get value() {
|
|
107
|
+
return this._value
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const instance = new TestClass()
|
|
112
|
+
expect(instance.value).toBe('wrapped: test')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should work with multiple getters', () => {
|
|
116
|
+
const getterDecorator = decorator({
|
|
117
|
+
getter(original, name) {
|
|
118
|
+
return function (this: any) {
|
|
119
|
+
return `${String(name)}: ${original.call(this)}`
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
class TestClass {
|
|
125
|
+
private _first = 'first'
|
|
126
|
+
private _second = 'second'
|
|
127
|
+
|
|
128
|
+
@getterDecorator
|
|
129
|
+
get first() {
|
|
130
|
+
return this._first
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@getterDecorator
|
|
134
|
+
get second() {
|
|
135
|
+
return this._second
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const instance = new TestClass()
|
|
140
|
+
expect(instance.first).toBe('first: first')
|
|
141
|
+
expect(instance.second).toBe('second: second')
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('Setter Decorators', () => {
|
|
146
|
+
it('should wrap setter calls', () => {
|
|
147
|
+
const setterDecorator = decorator({
|
|
148
|
+
setter(original, _name) {
|
|
149
|
+
return function (this: any, value: any) {
|
|
150
|
+
return original.call(this, `wrapped: ${value}`)
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
class TestClass {
|
|
156
|
+
private _value = ''
|
|
157
|
+
|
|
158
|
+
@setterDecorator
|
|
159
|
+
set value(v: string) {
|
|
160
|
+
this._value = v
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get value() {
|
|
164
|
+
return this._value
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const instance = new TestClass()
|
|
169
|
+
instance.value = 'test'
|
|
170
|
+
expect(instance.value).toBe('wrapped: test')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should work with multiple setters', () => {
|
|
174
|
+
const setterDecorator = decorator({
|
|
175
|
+
setter(original, name) {
|
|
176
|
+
return function (this: any, value: any) {
|
|
177
|
+
return original.call(this, `${String(name)}: ${value}`)
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
class TestClass {
|
|
183
|
+
private _first = ''
|
|
184
|
+
private _second = ''
|
|
185
|
+
|
|
186
|
+
@setterDecorator
|
|
187
|
+
set first(v: string) {
|
|
188
|
+
this._first = v
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@setterDecorator
|
|
192
|
+
set second(v: string) {
|
|
193
|
+
this._second = v
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get first() {
|
|
197
|
+
return this._first
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get second() {
|
|
201
|
+
return this._second
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const instance = new TestClass()
|
|
206
|
+
instance.first = 'test1'
|
|
207
|
+
instance.second = 'test2'
|
|
208
|
+
expect(instance.first).toBe('first: test1')
|
|
209
|
+
expect(instance.second).toBe('second: test2')
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('Combined Decorators', () => {
|
|
214
|
+
it('should work with method and class decorators together', () => {
|
|
215
|
+
const myDecorator = decorator({
|
|
216
|
+
class: (target) => {
|
|
217
|
+
;(target as any).decorated = true
|
|
218
|
+
return target
|
|
219
|
+
},
|
|
220
|
+
method(original, _name) {
|
|
221
|
+
return function (this: any, ...args: any[]) {
|
|
222
|
+
return `method: ${original.apply(this, args)}`
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
@myDecorator
|
|
228
|
+
class TestClass {
|
|
229
|
+
@myDecorator
|
|
230
|
+
greet(name: string) {
|
|
231
|
+
return `Hello ${name}`
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
expect((TestClass as any).decorated).toBe(true)
|
|
236
|
+
const instance = new TestClass()
|
|
237
|
+
expect(instance.greet('World')).toBe('method: Hello World')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('should work with getter and setter decorators on different properties', () => {
|
|
241
|
+
const myDecorator = decorator({
|
|
242
|
+
getter(original, _name) {
|
|
243
|
+
return function (this: any) {
|
|
244
|
+
return `get: ${original.call(this)}`
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
setter(original, _name) {
|
|
248
|
+
return function (this: any, value: any) {
|
|
249
|
+
return original.call(this, `set: ${value}`)
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
class TestClass {
|
|
255
|
+
private _value1 = ''
|
|
256
|
+
private _value2 = ''
|
|
257
|
+
|
|
258
|
+
@myDecorator
|
|
259
|
+
get value1() {
|
|
260
|
+
return this._value1
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@myDecorator
|
|
264
|
+
set value2(v: string) {
|
|
265
|
+
this._value2 = v
|
|
266
|
+
}
|
|
267
|
+
//@ts-ignore: The end-user should put a decorator here if modern, and not if legacy
|
|
268
|
+
@myDecorator
|
|
269
|
+
get value2() {
|
|
270
|
+
return this._value2
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const instance = new TestClass()
|
|
275
|
+
instance.value2 = 'test'
|
|
276
|
+
expect(instance.value1).toBe('get: ')
|
|
277
|
+
expect(instance.value2).toBe('get: set: test')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should work with all decorator types together', () => {
|
|
281
|
+
const myDecorator = decorator({
|
|
282
|
+
class: (target) => {
|
|
283
|
+
;(target as any).decorated = true
|
|
284
|
+
return target
|
|
285
|
+
},
|
|
286
|
+
method(original, _name) {
|
|
287
|
+
return function (this: any, ...args: any[]) {
|
|
288
|
+
return `method: ${original.apply(this, args)}`
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
getter(original, _name) {
|
|
292
|
+
return function (this: any) {
|
|
293
|
+
return `get: ${original.call(this)}`
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
default(...args) {
|
|
297
|
+
return args.length
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
@myDecorator
|
|
302
|
+
class TestClass {
|
|
303
|
+
value = 'initial'
|
|
304
|
+
|
|
305
|
+
@myDecorator
|
|
306
|
+
greet(name: string) {
|
|
307
|
+
return `Hello ${name}`
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private _data = ''
|
|
311
|
+
|
|
312
|
+
@myDecorator
|
|
313
|
+
get data() {
|
|
314
|
+
return this._data
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
set data(v: string) {
|
|
318
|
+
this._data = v
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Test class decoration
|
|
323
|
+
expect((TestClass as any).decorated).toBe(true)
|
|
324
|
+
|
|
325
|
+
const instance = new TestClass()
|
|
326
|
+
|
|
327
|
+
// Test method decoration
|
|
328
|
+
expect(instance.greet('World')).toBe('method: Hello World')
|
|
329
|
+
|
|
330
|
+
// Test accessor decoration
|
|
331
|
+
instance.data = 'test'
|
|
332
|
+
expect(instance.data).toBe('get: test')
|
|
333
|
+
expect(myDecorator(1, 2, 3)).toBe(3)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('should call all decorator types without changing behavior', () => {
|
|
337
|
+
const callLog: string[] = []
|
|
338
|
+
|
|
339
|
+
const noOpDecorator = decorator({
|
|
340
|
+
class(original) {
|
|
341
|
+
callLog.push('class decorator called')
|
|
342
|
+
return original // Return unchanged
|
|
343
|
+
},
|
|
344
|
+
method(original, name) {
|
|
345
|
+
callLog.push(`method decorator called for ${String(name)}`)
|
|
346
|
+
return original // Return unchanged
|
|
347
|
+
},
|
|
348
|
+
getter(original, name) {
|
|
349
|
+
callLog.push(`getter decorator called for ${String(name)}`)
|
|
350
|
+
return original // Return unchanged
|
|
351
|
+
},
|
|
352
|
+
setter(original, name) {
|
|
353
|
+
callLog.push(`setter decorator called for ${String(name)}`)
|
|
354
|
+
return original // Return unchanged
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
@noOpDecorator
|
|
359
|
+
class TestClass {
|
|
360
|
+
value = 'initial'
|
|
361
|
+
|
|
362
|
+
@noOpDecorator
|
|
363
|
+
greet(name: string) {
|
|
364
|
+
return `Hello ${name}`
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private _data = ''
|
|
368
|
+
|
|
369
|
+
@noOpDecorator
|
|
370
|
+
get data() {
|
|
371
|
+
return this._data
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
set data(v: string) {
|
|
375
|
+
this._data = v
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Verify all decorators were called
|
|
380
|
+
expect(callLog).toContain('class decorator called')
|
|
381
|
+
expect(callLog).toContain('method decorator called for greet')
|
|
382
|
+
expect(callLog).toContain('getter decorator called for data')
|
|
383
|
+
|
|
384
|
+
const instance = new TestClass()
|
|
385
|
+
|
|
386
|
+
// Verify behavior is unchanged (no wrapping occurred)
|
|
387
|
+
expect(instance.greet('World')).toBe('Hello World') // No "method:" prefix
|
|
388
|
+
expect(instance.value).toBe('initial')
|
|
389
|
+
|
|
390
|
+
instance.data = 'test'
|
|
391
|
+
expect(instance.data).toBe('test') // No "get:" prefix
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('should call setter decorator without changing behavior', () => {
|
|
395
|
+
const callLog: string[] = []
|
|
396
|
+
|
|
397
|
+
const noOpDecorator = decorator({
|
|
398
|
+
setter(original, name) {
|
|
399
|
+
callLog.push(`setter decorator called for ${String(name)}`)
|
|
400
|
+
return original // Return unchanged
|
|
401
|
+
},
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
class TestClass {
|
|
405
|
+
private _value = ''
|
|
406
|
+
|
|
407
|
+
@noOpDecorator
|
|
408
|
+
set value(v: string) {
|
|
409
|
+
this._value = v
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
get value() {
|
|
413
|
+
return this._value
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Verify setter decorator was called
|
|
418
|
+
expect(callLog).toContain('setter decorator called for value')
|
|
419
|
+
|
|
420
|
+
const instance = new TestClass()
|
|
421
|
+
|
|
422
|
+
// Verify behavior is unchanged (no wrapping occurred)
|
|
423
|
+
instance.value = 'test'
|
|
424
|
+
expect(instance.value).toBe('test') // No wrapping
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
describe('Error Handling', () => {
|
|
429
|
+
it('should throw error when decorator is applied to wrong target', () => {
|
|
430
|
+
const methodOnlyDecorator = decorator({
|
|
431
|
+
method(original, _name) {
|
|
432
|
+
return original
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
expect(() => {
|
|
437
|
+
class TestClass {
|
|
438
|
+
// @ts-ignore
|
|
439
|
+
@methodOnlyDecorator
|
|
440
|
+
value = 'test'
|
|
441
|
+
}
|
|
442
|
+
void new TestClass()
|
|
443
|
+
}).toThrow('Decorator cannot be applied to a field')
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('should throw error when class decorator is applied to method', () => {
|
|
447
|
+
const classOnlyDecorator = decorator({
|
|
448
|
+
class: (target) => target,
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
expect(() => {
|
|
452
|
+
class TestClass {
|
|
453
|
+
// @ts-ignore
|
|
454
|
+
@classOnlyDecorator
|
|
455
|
+
method() {}
|
|
456
|
+
}
|
|
457
|
+
void new TestClass()
|
|
458
|
+
}).toThrow('Decorator cannot be applied to a method')
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('should throw error when getter decorator is applied to method', () => {
|
|
462
|
+
const getterOnlyDecorator = decorator({
|
|
463
|
+
getter(original, _name) {
|
|
464
|
+
return original
|
|
465
|
+
},
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
expect(() => {
|
|
469
|
+
class TestClass {
|
|
470
|
+
// @ts-ignore
|
|
471
|
+
@getterOnlyDecorator
|
|
472
|
+
method() {}
|
|
473
|
+
}
|
|
474
|
+
void new TestClass()
|
|
475
|
+
}).toThrow('Decorator cannot be applied to a method')
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('should throw error when decorating a field', () => {
|
|
479
|
+
const anyDecorator = decorator({
|
|
480
|
+
method(original, _name) {
|
|
481
|
+
return original
|
|
482
|
+
},
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
expect(() => {
|
|
486
|
+
class TestClass {
|
|
487
|
+
// @ts-ignore
|
|
488
|
+
@anyDecorator
|
|
489
|
+
field = 'value'
|
|
490
|
+
}
|
|
491
|
+
void new TestClass()
|
|
492
|
+
}).toThrow('Decorator cannot be applied to a field')
|
|
493
|
+
})
|
|
494
|
+
})
|
|
495
|
+
})
|
package/src/decorator.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noConfusingVoidType: We *love* voids
|
|
2
|
+
// Standardized decorator system that works with both Legacy and Modern decorators
|
|
3
|
+
|
|
4
|
+
import { isConstructor } from './utils'
|
|
5
|
+
|
|
6
|
+
export class DecoratorError extends Error {
|
|
7
|
+
constructor(message: string) {
|
|
8
|
+
super(message)
|
|
9
|
+
this.name = 'DecoratorException'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//#region all decorator types
|
|
13
|
+
|
|
14
|
+
// Used for get/set and method decorators
|
|
15
|
+
export type LegacyPropertyDecorator<T> = (
|
|
16
|
+
target: T,
|
|
17
|
+
name: string | symbol,
|
|
18
|
+
descriptor: PropertyDescriptor
|
|
19
|
+
) => any
|
|
20
|
+
|
|
21
|
+
export type LegacyClassDecorator<T> = (target: T) => any
|
|
22
|
+
|
|
23
|
+
export type ModernMethodDecorator<T> = (target: T, context: ClassMethodDecoratorContext) => any
|
|
24
|
+
|
|
25
|
+
export type ModernGetterDecorator<T> = (target: T, context: ClassGetterDecoratorContext) => any
|
|
26
|
+
|
|
27
|
+
export type ModernSetterDecorator<T> = (target: T, context: ClassSetterDecoratorContext) => any
|
|
28
|
+
|
|
29
|
+
export type ModernAccessorDecorator<T> = (target: T, context: ClassAccessorDecoratorContext) => any
|
|
30
|
+
|
|
31
|
+
export type ModernClassDecorator<T> = (target: T, context: ClassDecoratorContext) => any
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
|
|
35
|
+
type DDMethod<T> = (
|
|
36
|
+
original: (this: T, ...args: any[]) => any,
|
|
37
|
+
name: PropertyKey
|
|
38
|
+
) => ((this: T, ...args: any[]) => any) | void
|
|
39
|
+
|
|
40
|
+
type DDGetter<T> = (original: (this: T) => any, name: PropertyKey) => ((this: T) => any) | void
|
|
41
|
+
|
|
42
|
+
type DDSetter<T> = (
|
|
43
|
+
original: (this: T, value: any) => void,
|
|
44
|
+
name: PropertyKey
|
|
45
|
+
) => ((this: T, value: any) => void) | void
|
|
46
|
+
|
|
47
|
+
type DDClass<T> = <Ctor extends new (...args: any[]) => T = new (...args: any[]) => T>(
|
|
48
|
+
target: Ctor
|
|
49
|
+
) => Ctor | void
|
|
50
|
+
export interface DecoratorDescription<T> {
|
|
51
|
+
method?: DDMethod<T>
|
|
52
|
+
class?: DDClass<T>
|
|
53
|
+
getter?: DDGetter<T>
|
|
54
|
+
setter?: DDSetter<T>
|
|
55
|
+
default?: (...args: any[]) => any
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type Decorator<T, Description extends DecoratorDescription<T>> = (Description extends {
|
|
59
|
+
method: DDMethod<T>
|
|
60
|
+
}
|
|
61
|
+
? LegacyPropertyDecorator<T> & ModernMethodDecorator<T>
|
|
62
|
+
: unknown) &
|
|
63
|
+
(Description extends { class: DDClass<new (...args: any[]) => T> }
|
|
64
|
+
? LegacyClassDecorator<new (...args: any[]) => T> &
|
|
65
|
+
ModernClassDecorator<new (...args: any[]) => T>
|
|
66
|
+
: unknown) &
|
|
67
|
+
(Description extends { getter: DDGetter<T> }
|
|
68
|
+
? LegacyPropertyDecorator<T> & ModernGetterDecorator<T> & ModernAccessorDecorator<T>
|
|
69
|
+
: unknown) &
|
|
70
|
+
(Description extends { setter: DDSetter<T> }
|
|
71
|
+
? LegacyPropertyDecorator<T> & ModernSetterDecorator<T> & ModernAccessorDecorator<T>
|
|
72
|
+
: unknown) &
|
|
73
|
+
(Description extends { default: infer Signature } ? Signature : unknown)
|
|
74
|
+
|
|
75
|
+
export type DecoratorFactory<T> = <Description extends DecoratorDescription<T>>(
|
|
76
|
+
description: Description
|
|
77
|
+
) => (Description extends { method: DDMethod<T> }
|
|
78
|
+
? LegacyPropertyDecorator<T> & ModernMethodDecorator<T>
|
|
79
|
+
: unknown) &
|
|
80
|
+
(Description extends { class: DDClass<new (...args: any[]) => T> }
|
|
81
|
+
? LegacyClassDecorator<new (...args: any[]) => T> &
|
|
82
|
+
ModernClassDecorator<new (...args: any[]) => T>
|
|
83
|
+
: unknown) &
|
|
84
|
+
(Description extends { getter: DDGetter<T> }
|
|
85
|
+
? LegacyPropertyDecorator<T> & ModernGetterDecorator<T> & ModernAccessorDecorator<T>
|
|
86
|
+
: unknown) &
|
|
87
|
+
(Description extends { setter: DDSetter<T> }
|
|
88
|
+
? LegacyPropertyDecorator<T> & ModernSetterDecorator<T> & ModernAccessorDecorator<T>
|
|
89
|
+
: unknown) &
|
|
90
|
+
(Description extends { default: infer Signature } ? Signature : unknown)
|
|
91
|
+
|
|
92
|
+
export function legacyDecorator<T = any>(description: DecoratorDescription<T>): any {
|
|
93
|
+
return function (
|
|
94
|
+
target: any,
|
|
95
|
+
propertyKey?: PropertyKey,
|
|
96
|
+
descriptor?: PropertyDescriptor,
|
|
97
|
+
...args: any[]
|
|
98
|
+
) {
|
|
99
|
+
if (propertyKey === undefined) {
|
|
100
|
+
if (isConstructor(target)) {
|
|
101
|
+
if (!('class' in description)) throw new Error('Decorator cannot be applied to a class')
|
|
102
|
+
return description.class?.(target)
|
|
103
|
+
}
|
|
104
|
+
} else if (typeof target === 'object' && ['string', 'symbol'].includes(typeof propertyKey)) {
|
|
105
|
+
if (!descriptor) throw new Error('Decorator cannot be applied to a field')
|
|
106
|
+
else if (typeof descriptor === 'object' && 'configurable' in descriptor) {
|
|
107
|
+
if ('get' in descriptor || 'set' in descriptor) {
|
|
108
|
+
if (!('getter' in description || 'setter' in description))
|
|
109
|
+
throw new Error('Decorator cannot be applied to a getter or setter')
|
|
110
|
+
if ('getter' in description) {
|
|
111
|
+
const newGetter = description.getter?.(descriptor.get, propertyKey)
|
|
112
|
+
if (newGetter) descriptor.get = newGetter
|
|
113
|
+
}
|
|
114
|
+
if ('setter' in description) {
|
|
115
|
+
const newSetter = description.setter?.(descriptor.set, propertyKey)
|
|
116
|
+
if (newSetter) descriptor.set = newSetter
|
|
117
|
+
}
|
|
118
|
+
return descriptor
|
|
119
|
+
} else if (typeof descriptor.value === 'function') {
|
|
120
|
+
if (!('method' in description)) throw new Error('Decorator cannot be applied to a method')
|
|
121
|
+
const newMethod = description.method?.(descriptor.value, propertyKey)
|
|
122
|
+
if (newMethod) descriptor.value = newMethod
|
|
123
|
+
return descriptor
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!('default' in description))
|
|
128
|
+
throw new Error('Decorator do not have a default implementation')
|
|
129
|
+
return description.default.call(this, target, propertyKey, descriptor, ...args)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function modernDecorator<T = any>(description: DecoratorDescription<T>): any {
|
|
134
|
+
return function (target: any, context?: DecoratorContext, ...args: any[]) {
|
|
135
|
+
if (!context?.kind || typeof context.kind !== 'string') {
|
|
136
|
+
if (!('default' in description))
|
|
137
|
+
throw new Error('Decorator do not have a default implementation')
|
|
138
|
+
return description.default.call(this, target, context, ...args)
|
|
139
|
+
}
|
|
140
|
+
switch (context.kind) {
|
|
141
|
+
case 'class':
|
|
142
|
+
if (!('class' in description)) throw new Error('Decorator cannot be applied to a class')
|
|
143
|
+
return description.class?.(target)
|
|
144
|
+
case 'field':
|
|
145
|
+
throw new Error('Decorator cannot be applied to a field')
|
|
146
|
+
case 'getter':
|
|
147
|
+
if (!('getter' in description)) throw new Error('Decorator cannot be applied to a getter')
|
|
148
|
+
return description.getter?.(target, context.name)
|
|
149
|
+
case 'setter':
|
|
150
|
+
if (!('setter' in description)) throw new Error('Decorator cannot be applied to a setter')
|
|
151
|
+
return description.setter?.(target, context.name)
|
|
152
|
+
case 'method':
|
|
153
|
+
if (!('method' in description)) throw new Error('Decorator cannot be applied to a method')
|
|
154
|
+
return description.method?.(target, context.name)
|
|
155
|
+
case 'accessor': {
|
|
156
|
+
if (!('getter' in description || 'setter' in description))
|
|
157
|
+
throw new Error('Decorator cannot be applied to a getter or setter')
|
|
158
|
+
const rv: Partial<ClassAccessorDecoratorResult<any, any>> = {}
|
|
159
|
+
if ('getter' in description) {
|
|
160
|
+
const newGetter = description.getter?.(target.get, context.name)
|
|
161
|
+
if (newGetter) rv.get = newGetter
|
|
162
|
+
}
|
|
163
|
+
if ('setter' in description) {
|
|
164
|
+
const newSetter = description.setter?.(target.set, context.name)
|
|
165
|
+
if (newSetter) rv.set = newSetter
|
|
166
|
+
}
|
|
167
|
+
return rv
|
|
168
|
+
}
|
|
169
|
+
//return description.accessor?.(target, context.name, target)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Detects if the decorator is being called in modern (Modern) or legacy (Legacy) mode
|
|
176
|
+
* based on the arguments passed to the decorator function
|
|
177
|
+
*/
|
|
178
|
+
function detectDecoratorMode(
|
|
179
|
+
_target: any,
|
|
180
|
+
contextOrKey?: any,
|
|
181
|
+
_descriptor?: any
|
|
182
|
+
): 'modern' | 'legacy' {
|
|
183
|
+
// Modern decorators have a context object as the second parameter
|
|
184
|
+
// Legacy decorators have a string/symbol key as the second parameter
|
|
185
|
+
if (
|
|
186
|
+
typeof contextOrKey === 'object' &&
|
|
187
|
+
contextOrKey !== null &&
|
|
188
|
+
typeof contextOrKey.kind === 'string'
|
|
189
|
+
) {
|
|
190
|
+
return 'modern'
|
|
191
|
+
}
|
|
192
|
+
return 'legacy'
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const decorator: DecoratorFactory<any> = (description: DecoratorDescription<any>) => {
|
|
196
|
+
return ((target: any, contextOrKey?: any, ...args: any[]) => {
|
|
197
|
+
const mode = detectDecoratorMode(target, contextOrKey, args[0])
|
|
198
|
+
return mode === 'modern'
|
|
199
|
+
? modernDecorator(description)(target, contextOrKey, ...args)
|
|
200
|
+
: legacyDecorator(description)(target, contextOrKey, ...args)
|
|
201
|
+
}) as any
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type GenericClassDecorator<T> = LegacyClassDecorator<new (...args: any[]) => T> &
|
|
205
|
+
ModernClassDecorator<new (...args: any[]) => T>
|