mobx-keystone 1.23.0 → 1.24.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.
@@ -9,7 +9,7 @@ import { AnyFunction } from '../utils/AnyFunction';
9
9
  * @param name Unique name for the model type. Note that this name must be unique for your whole
10
10
  * application, so it is usually a good idea to use some prefix unique to your application domain.
11
11
  */
12
- export declare const model: (name: string) => <MC extends ModelClass<AnyModel | AnyDataModel>>(clazz: MC, ...args: any[]) => MC;
12
+ export declare const model: (name: string) => <MC extends ModelClass<AnyModel | AnyDataModel>>(clazz: MC, _context?: ClassDecoratorContext<MC>) => MC;
13
13
  /**
14
14
  * Marks a class (which MUST inherit from the `Model` abstract class)
15
15
  * as a model and decorates some of its methods/properties.
@@ -4,3 +4,27 @@
4
4
  export declare class MobxKeystoneError extends Error {
5
5
  constructor(msg: string);
6
6
  }
7
+ /**
8
+ * MobX method action decorator compatible with both legacy decorators and
9
+ * MobX 7's standard decorator API.
10
+ *
11
+ * This is a decorator compatibility helper, not a replacement for other
12
+ * `action` overloads such as `action(fn)`. Field decorators are not supported.
13
+ *
14
+ * @hidden
15
+ */
16
+ export declare function mobxAction<This, Args extends unknown[], Return>(value: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>): (this: This, ...args: Args) => Return;
17
+ export declare function mobxAction<This, Args extends unknown[], Return>(target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Return>): TypedPropertyDescriptor<(this: This, ...args: Args) => Return> | void;
18
+ /**
19
+ * MobX getter computed decorator compatible with both legacy decorators and
20
+ * MobX 7's standard decorator API.
21
+ *
22
+ * This is a decorator compatibility helper, not a replacement for other
23
+ * `computed` overloads. Field decorators and decorator options are not supported.
24
+ * Under MobX 7's legacy decorator transform the getter is backed by a computed
25
+ * value, but is not registered as a computed property for MobX introspection.
26
+ *
27
+ * @hidden
28
+ */
29
+ export declare function mobxComputed<This, Value>(value: (this: This) => Value, context: ClassGetterDecoratorContext<This, Value>): ((this: This) => Value) | void;
30
+ export declare function mobxComputed<Value>(target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<Value>): TypedPropertyDescriptor<Value> | void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobx-keystone",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more",
5
5
  "keywords": [
6
6
  "mobx",
@@ -63,11 +63,12 @@
63
63
  "build-docs": "shx rm -rf api-docs && node --import ./scripts/typedoc-typescript6-register.mjs ./node_modules/typedoc/dist/cli.js --options ./typedocconfig.js src/index.ts && shx rm -rf ../../apps/site/generated-static/api && shx mkdir -p ../../apps/site/generated-static/api && shx cp -R ./api-docs/* ../../apps/site/generated-static/api"
64
64
  },
65
65
  "peerDependencies": {
66
- "mobx": "^6.0.0 || ^5.0.0 || ^4.0.0"
66
+ "mobx": "^7.0.0 || ^6.0.0 || ^5.0.0 || ^4.0.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@babel/core": "^8.0.1",
70
70
  "@babel/plugin-proposal-decorators": "^8.0.2",
71
+ "@babel/plugin-transform-class-static-block": "^8.0.1",
71
72
  "@babel/plugin-transform-class-properties": "^8.0.1",
72
73
  "@babel/plugin-transform-typescript": "^8.0.1",
73
74
  "@babel/preset-env": "^8.0.2",
@@ -78,6 +79,8 @@
78
79
  "@vitest/coverage-v8": "^4.1.10",
79
80
  "mobx-v4": "npm:mobx@^4.15.7",
80
81
  "mobx-v5": "npm:mobx@^5.15.7",
82
+ "mobx-v6": "npm:mobx@^6.0.0",
83
+ "mobx-v7": "npm:mobx@^7.0.0",
81
84
  "shx": "^0.4.0",
82
85
  "spec.ts": "^1.1.3",
83
86
  "ts-node": "^10.9.2",
@@ -1,4 +1,3 @@
1
- import { action, computed } from "mobx"
2
1
  import type { ActionMiddlewareDisposer } from "../action/middleware"
3
2
  import { modelAction } from "../action/modelAction"
4
3
  import { Model } from "../model/Model"
@@ -10,7 +9,14 @@ import { assertTweakedObject } from "../tweaker/core"
10
9
  import { typesArray } from "../types/arrayBased/typesArray"
11
10
  import { tProp } from "../types/tProp"
12
11
  import { typesUnchecked } from "../types/utility/typesUnchecked"
13
- import { failure, getMobxVersion, mobx6, namespace } from "../utils"
12
+ import {
13
+ failure,
14
+ getMobxVersion,
15
+ makeObservableCompat,
16
+ mobxAction,
17
+ mobxComputed,
18
+ namespace,
19
+ } from "../utils"
14
20
  import { actionTrackingMiddleware, type SimpleActionContext } from "./actionTrackingMiddleware"
15
21
 
16
22
  /**
@@ -306,7 +312,7 @@ export class UndoManager {
306
312
  * The undo stack, where the first operation to undo will be the last of the array.
307
313
  * Do not manipulate this array directly.
308
314
  */
309
- @computed
315
+ @mobxComputed
310
316
  get undoQueue(): ReadonlyArray<UndoEvent> {
311
317
  return this.store.undoEvents
312
318
  }
@@ -315,7 +321,7 @@ export class UndoManager {
315
321
  * The redo stack, where the first operation to redo will be the last of the array.
316
322
  * Do not manipulate this array directly.
317
323
  */
318
- @computed
324
+ @mobxComputed
319
325
  get redoQueue(): ReadonlyArray<UndoEvent> {
320
326
  return this.store.redoEvents
321
327
  }
@@ -323,7 +329,7 @@ export class UndoManager {
323
329
  /**
324
330
  * The number of undo actions available.
325
331
  */
326
- @computed
332
+ @mobxComputed
327
333
  get undoLevels() {
328
334
  return this.undoQueue.length
329
335
  }
@@ -331,7 +337,7 @@ export class UndoManager {
331
337
  /**
332
338
  * If undo can be performed (if there is at least one undo action available).
333
339
  */
334
- @computed
340
+ @mobxComputed
335
341
  get canUndo() {
336
342
  return this.undoLevels > 0
337
343
  }
@@ -339,7 +345,7 @@ export class UndoManager {
339
345
  /**
340
346
  * Clears the undo queue.
341
347
  */
342
- @action
348
+ @mobxAction
343
349
  clearUndo() {
344
350
  this.store._clearUndo()
345
351
  }
@@ -347,7 +353,7 @@ export class UndoManager {
347
353
  /**
348
354
  * The number of redo actions available.
349
355
  */
350
- @computed
356
+ @mobxComputed
351
357
  get redoLevels() {
352
358
  return this.redoQueue.length
353
359
  }
@@ -355,7 +361,7 @@ export class UndoManager {
355
361
  /**
356
362
  * If redo can be performed (if there is at least one redo action available)
357
363
  */
358
- @computed
364
+ @mobxComputed
359
365
  get canRedo() {
360
366
  return this.redoLevels > 0
361
367
  }
@@ -363,7 +369,7 @@ export class UndoManager {
363
369
  /**
364
370
  * Clears the redo queue.
365
371
  */
366
- @action
372
+ @mobxAction
367
373
  clearRedo() {
368
374
  this.store._clearRedo()
369
375
  }
@@ -372,7 +378,7 @@ export class UndoManager {
372
378
  * Undoes the last action.
373
379
  * Will throw if there is no action to undo.
374
380
  */
375
- @action
381
+ @mobxAction
376
382
  undo() {
377
383
  if (!this.canUndo) {
378
384
  throw failure("nothing to undo")
@@ -397,7 +403,7 @@ export class UndoManager {
397
403
  * Redoes the previous action.
398
404
  * Will throw if there is no action to redo.
399
405
  */
400
- @action
406
+ @mobxAction
401
407
  redo() {
402
408
  if (!this.canRedo) {
403
409
  throw failure("nothing to redo")
@@ -619,7 +625,7 @@ export class UndoManager {
619
625
  this.options = options
620
626
 
621
627
  if (getMobxVersion() >= 6) {
622
- mobx6.makeObservable(this)
628
+ makeObservableCompat(this)
623
629
  }
624
630
 
625
631
  this.store = store ?? new UndoStore({})
@@ -15,10 +15,11 @@ import {
15
15
  } from "../modelShared/modelSymbols"
16
16
  import { refreshInternalSnapshot } from "../snapshot/internal"
17
17
  import {
18
+ addLateInitializationFunction,
18
19
  failure,
19
20
  getMobxVersion,
20
21
  logWarning,
21
- mobx6,
22
+ makeObservableCompat,
22
23
  runAfterNewSymbol,
23
24
  runBeforeOnInitSymbol,
24
25
  runLateInitializationFunctions,
@@ -34,11 +35,11 @@ import type { AnyFunction } from "../utils/AnyFunction"
34
35
  */
35
36
  export const model =
36
37
  (name: string) =>
37
- <MC extends ModelClass<AnyModel | AnyDataModel>>(clazz: MC, ...args: any[]): MC => {
38
- const ctx = typeof args[1] === "object" ? (args[1] as ClassDecoratorContext) : undefined
39
-
40
- return internalModel(name, clazz, ctx?.addInitializer) as any
41
- }
38
+ <MC extends ModelClass<AnyModel | AnyDataModel>>(
39
+ clazz: MC,
40
+ _context?: ClassDecoratorContext<MC>
41
+ ): MC =>
42
+ internalModel(name, clazz) as any
42
43
 
43
44
  const afterClassInitializationData = new WeakMap<
44
45
  ModelClass<AnyModel | AnyDataModel>,
@@ -56,14 +57,14 @@ const runAfterClassInitialization = (
56
57
 
57
58
  const tag = afterClassInitializationData.get(target)!
58
59
 
59
- // compatibility with mobx 6
60
+ // compatibility with mobx 6 and 7
60
61
  if (tag.needsMakeObservable) {
61
62
  // we know it can be done and shouldn't fail
62
- mobx6.makeObservable(instance)
63
+ makeObservableCompat(instance)
63
64
  } else if (tag.needsMakeObservable === undefined) {
64
65
  if (getMobxVersion() >= 6) {
65
66
  try {
66
- mobx6.makeObservable(instance)
67
+ makeObservableCompat(instance)
67
68
  tag.needsMakeObservable = true
68
69
  } catch (e) {
69
70
  const err = e as Error
@@ -121,8 +122,7 @@ const proxyClassHandler: ProxyHandler<ModelClass<AnyModel | AnyDataModel>> = {
121
122
 
122
123
  const internalModel = <MC extends ModelClass<AnyModel | AnyDataModel>>(
123
124
  name: string,
124
- clazz: MC,
125
- addInitializer: ((initializer: (this: any) => void) => void) | undefined
125
+ clazz: MC
126
126
  ): MC | void => {
127
127
  const type = isModelClass(clazz) ? "class" : isDataModelClass(clazz) ? "data" : undefined
128
128
  if (!type) {
@@ -151,48 +151,24 @@ const internalModel = <MC extends ModelClass<AnyModel | AnyDataModel>>(
151
151
  // track if we fail so we only try it once per class
152
152
  afterClassInitializationData.set(clazz, { needsMakeObservable: undefined, type })
153
153
 
154
- if (addInitializer) {
155
- // standard decorator API, avoid proxies
156
- addInitializer(function (this: any) {
157
- runAfterClassInitialization(clazz, this)
158
- })
159
-
160
- const modelInfo = {
161
- name,
162
- class: clazz,
163
- }
164
-
165
- modelInfoByName[name] = modelInfo
154
+ const decoratedClass = new Proxy<MC>(clazz, proxyClassHandler)
155
+ ;(decoratedClass as any)[modelUnwrappedClassSymbol] = clazz
166
156
 
167
- modelInfoByClass.set(clazz, modelInfo)
157
+ decoratedClass.prototype.constructor = decoratedClass
168
158
 
169
- runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol)
170
-
171
- return undefined // use same class
172
- } else {
173
- // non-standard decorator API, use proxies
174
-
175
- // trick so plain new works
176
- const proxyClass = new Proxy<MC>(clazz, proxyClassHandler)
177
-
178
- // set or else it points to the undecorated class
179
- proxyClass.prototype.constructor = proxyClass
180
- ;(proxyClass as any)[modelUnwrappedClassSymbol] = clazz
181
-
182
- const modelInfo = {
183
- name,
184
- class: proxyClass,
185
- }
159
+ const modelInfo = {
160
+ name,
161
+ class: decoratedClass,
162
+ }
186
163
 
187
- modelInfoByName[name] = modelInfo
164
+ modelInfoByName[name] = modelInfo
188
165
 
189
- modelInfoByClass.set(proxyClass, modelInfo)
190
- modelInfoByClass.set(clazz, modelInfo)
166
+ modelInfoByClass.set(decoratedClass, modelInfo)
167
+ modelInfoByClass.set(clazz, modelInfo)
191
168
 
192
- runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol)
169
+ runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol)
193
170
 
194
- return proxyClass
195
- }
171
+ return decoratedClass
196
172
  }
197
173
 
198
174
  // basically taken from TS
@@ -243,18 +219,53 @@ export function decoratedModel<M, MC extends abstract new (...ags: any) => M>(
243
219
  [k in keyof M]?: AnyFunction | ReadonlyArray<AnyFunction>
244
220
  }
245
221
  ): MC {
222
+ const mobx7Annotations: Record<string, unknown> = {}
223
+
246
224
  // decorate class members
247
225
  for (const [k, decorator] of Object.entries(decorators)) {
226
+ const decoratorList = Array.isArray(decorator) ? decorator : [decorator]
227
+ const mobx7Annotation = decoratorList.find(isMobxAnnotation)
228
+ if (getMobxVersion() >= 7 && mobx7Annotation) {
229
+ if (decoratorList.length !== 1) {
230
+ throw failure("MobX 7 decorators cannot be combined in decoratedModel")
231
+ }
232
+ mobx7Annotations[k] = mobx7Annotation
233
+ continue
234
+ }
235
+
248
236
  const prototypeValueDesc = Object.getOwnPropertyDescriptor(clazz.prototype, k)
249
237
  // TS seems to send null for methods in the prototype
250
238
  // (which we substitute for the descriptor to avoid a double look-up) and void 0 (undefined) for props
251
- tsDecorate(
252
- Array.isArray(decorator) ? decorator : [decorator],
253
- clazz.prototype,
254
- k,
255
- prototypeValueDesc ? prototypeValueDesc : void 0
256
- )
239
+ tsDecorate(decoratorList, clazz.prototype, k, prototypeValueDesc ? prototypeValueDesc : void 0)
240
+ }
241
+
242
+ if (getMobxVersion() >= 7 && Object.keys(mobx7Annotations).length > 0) {
243
+ addLateInitializationFunction(clazz.prototype, runAfterNewSymbol, (instance) => {
244
+ makeObservableCompat(instance, mobx7Annotations)
245
+ })
257
246
  }
258
247
 
259
248
  return (name ? model(name)(clazz as unknown as ModelClass<AnyModel>) : clazz) as MC
260
249
  }
250
+
251
+ type MobxAnnotation = {
252
+ annotationType_: string
253
+ make_: AnyFunction
254
+ extend_: AnyFunction
255
+ }
256
+
257
+ // MobX uses this same structural check in its internal `isAnnotation` helper.
258
+ // That helper is not part of MobX's public root API, so mirror it here instead
259
+ // of depending on a private deep import that may not exist in every supported version.
260
+ function isMobxAnnotation(value: unknown): value is MobxAnnotation {
261
+ if ((typeof value !== "object" || value === null) && typeof value !== "function") {
262
+ return false
263
+ }
264
+
265
+ const annotation = value as Partial<MobxAnnotation>
266
+ return (
267
+ typeof annotation.annotationType_ === "string" &&
268
+ typeof annotation.make_ === "function" &&
269
+ typeof annotation.extend_ === "function"
270
+ )
271
+ }
package/src/ref/Ref.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { computed } from "mobx"
2
1
  import { Model } from "../model/Model"
3
2
  import { modelTypeKey } from "../model/metadata"
4
3
  import type { ModelClass } from "../modelShared/BaseModelShared"
5
4
  import { typesString } from "../types/primitiveBased/typesPrimitive"
6
5
  import { tProp } from "../types/tProp"
7
- import { failure } from "../utils"
6
+ import { failure, mobxComputed } from "../utils"
8
7
 
9
8
  /**
10
9
  * A reference model base type.
@@ -21,7 +20,7 @@ export abstract class Ref<T extends object> extends Model({
21
20
  /**
22
21
  * The object this reference points to, or `undefined` if the reference is currently invalid.
23
22
  */
24
- @computed
23
+ @mobxComputed
25
24
  get maybeCurrent(): T | undefined {
26
25
  return this.resolve()
27
26
  }
@@ -29,7 +28,7 @@ export abstract class Ref<T extends object> extends Model({
29
28
  /**
30
29
  * If the reference is currently valid.
31
30
  */
32
- @computed
31
+ @mobxComputed
33
32
  get isValid(): boolean {
34
33
  return !!this.maybeCurrent
35
34
  }
@@ -37,7 +36,7 @@ export abstract class Ref<T extends object> extends Model({
37
36
  /**
38
37
  * The object this reference points to, or throws if invalid.
39
38
  */
40
- @computed
39
+ @mobxComputed
41
40
  get current(): T {
42
41
  const current = this.maybeCurrent
43
42
 
package/src/ref/core.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  import { fastGetRoot } from "../parent/path"
10
10
  import { type ComputedWalkTreeAggregate, computedWalkTreeAggregate } from "../parent/walkTree"
11
11
  import { assertTweakedObject } from "../tweaker/core"
12
- import { assertIsObject, failure } from "../utils"
12
+ import { assertIsObject, failure, mobxAction } from "../utils"
13
13
  import { getOrCreate } from "../utils/mapUtils"
14
14
  import { Ref, type RefConstructor } from "./Ref"
15
15
 
@@ -103,7 +103,7 @@ export function internalCustomRef<T extends object>(
103
103
  return true
104
104
  }
105
105
 
106
- @action
106
+ @mobxAction
107
107
  forceUpdateBackRefs() {
108
108
  const currentTarget = this.maybeCurrent
109
109
  if (!this.startTracking(currentTarget)) {
@@ -1,4 +1,3 @@
1
- import { action, computed } from "mobx"
2
1
  import { pathToTargetPathIds } from "../actionMiddlewares/utils"
3
2
  import { resolvePath, resolvePathCheckingIds, skipIdChecking } from "../parent/path"
4
3
  import type { Path } from "../parent/pathTypes"
@@ -7,7 +6,7 @@ import { applySnapshot } from "../snapshot/applySnapshot"
7
6
  import { fromSnapshot } from "../snapshot/fromSnapshot"
8
7
  import { getSnapshot } from "../snapshot/getSnapshot"
9
8
  import { assertTweakedObject } from "../tweaker/core"
10
- import { failure } from "../utils"
9
+ import { failure, mobxAction, mobxComputed } from "../utils"
11
10
  import { deepEquals } from "./deepEquals"
12
11
 
13
12
  /**
@@ -25,7 +24,7 @@ export class Draft<T extends object> {
25
24
  /**
26
25
  * Commits current draft changes to the original object.
27
26
  */
28
- @action
27
+ @mobxAction
29
28
  commit(): void {
30
29
  applySnapshot(this.originalData, getSnapshot(this.data))
31
30
  }
@@ -37,7 +36,7 @@ export class Draft<T extends object> {
37
36
  *
38
37
  * @param path Path to commit.
39
38
  */
40
- @action
39
+ @mobxAction
41
40
  commitByPath(path: Path): void {
42
41
  const draftTarget = resolvePath(this.data, path)
43
42
  if (!draftTarget.resolved) {
@@ -63,7 +62,7 @@ export class Draft<T extends object> {
63
62
  /**
64
63
  * Resets the draft to be an exact copy of the current state of the original object.
65
64
  */
66
- @action
65
+ @mobxAction
67
66
  reset(): void {
68
67
  applySnapshot(this.data, this.originalSnapshot)
69
68
  }
@@ -75,7 +74,7 @@ export class Draft<T extends object> {
75
74
  *
76
75
  * @param path Path to reset.
77
76
  */
78
- @action
77
+ @mobxAction
79
78
  resetByPath(path: Path): void {
80
79
  const originalTarget = resolvePath(this.originalData, path)
81
80
  if (!originalTarget.resolved) {
@@ -101,7 +100,7 @@ export class Draft<T extends object> {
101
100
  /**
102
101
  * Returns `true` if the draft has changed compared to the original object, `false` otherwise.
103
102
  */
104
- @computed
103
+ @mobxComputed
105
104
  get isDirty(): boolean {
106
105
  return !deepEquals(getSnapshot(this.data), this.originalSnapshot)
107
106
  }
@@ -135,7 +134,7 @@ export class Draft<T extends object> {
135
134
  */
136
135
  readonly originalData: T
137
136
 
138
- @computed
137
+ @mobxComputed
139
138
  private get originalSnapshot() {
140
139
  return getSnapshot(this.originalData)
141
140
  }
@@ -48,7 +48,8 @@ export function tweakPlainObject<T extends Record<string, any>>(
48
48
  isDataObject: boolean
49
49
  ): T {
50
50
  const originalObj: Record<string, any> = value
51
- const tweakedObj = isObservableObject(originalObj)
51
+ const originalIsObservableObject = isObservableObject(originalObj)
52
+ const tweakedObj = originalIsObservableObject
52
53
  ? originalObj
53
54
  : observable.object({}, undefined, observableOptions)
54
55
 
@@ -78,7 +79,7 @@ export function tweakPlainObject<T extends Record<string, any>>(
78
79
  const v = originalObj[k]
79
80
 
80
81
  if (isPrimitive(v)) {
81
- if (!doNotTweakChildren) {
82
+ if (!doNotTweakChildren && !originalIsObservableObject) {
82
83
  setIfDifferent(tweakedObj, k, v)
83
84
  }
84
85
  if (k === "__proto__") {
@@ -1,5 +1,6 @@
1
1
  import * as mobx from "mobx"
2
2
  import {
3
+ type IComputedValue,
3
4
  type IObservableArray,
4
5
  isObservableArray,
5
6
  isObservableMap,
@@ -419,6 +420,127 @@ export const mobx6 = {
419
420
  ],
420
421
  }
421
422
 
423
+ /**
424
+ * MobX method action decorator compatible with both legacy decorators and
425
+ * MobX 7's standard decorator API.
426
+ *
427
+ * This is a decorator compatibility helper, not a replacement for other
428
+ * `action` overloads such as `action(fn)`. Field decorators are not supported.
429
+ *
430
+ * @hidden
431
+ */
432
+ export function mobxAction<This, Args extends unknown[], Return>(
433
+ value: (this: This, ...args: Args) => Return,
434
+ context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
435
+ ): (this: This, ...args: Args) => Return
436
+ export function mobxAction<This, Args extends unknown[], Return>(
437
+ target: object,
438
+ propertyKey: string | symbol,
439
+ descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Return>
440
+ ): TypedPropertyDescriptor<(this: This, ...args: Args) => Return> | void
441
+ export function mobxAction(...args: any[]): any {
442
+ if (isStandardDecoratorContext(args[1])) {
443
+ return (mobx.action as any)(args[0], args[1])
444
+ }
445
+
446
+ if (getMobxVersion() < 7) {
447
+ return (mobx.action as any)(...args)
448
+ }
449
+
450
+ const descriptor = args[2] as PropertyDescriptor | undefined
451
+ if (!descriptor?.value) {
452
+ return descriptor
453
+ }
454
+
455
+ if (mobx.isAction(descriptor.value)) {
456
+ return descriptor
457
+ }
458
+
459
+ return {
460
+ ...descriptor,
461
+ value: mobx.action(String(args[1]), descriptor.value),
462
+ }
463
+ }
464
+
465
+ /**
466
+ * MobX getter computed decorator compatible with both legacy decorators and
467
+ * MobX 7's standard decorator API.
468
+ *
469
+ * This is a decorator compatibility helper, not a replacement for other
470
+ * `computed` overloads. Field decorators and decorator options are not supported.
471
+ * Under MobX 7's legacy decorator transform the getter is backed by a computed
472
+ * value, but is not registered as a computed property for MobX introspection.
473
+ *
474
+ * @hidden
475
+ */
476
+ export function mobxComputed<This, Value>(
477
+ value: (this: This) => Value,
478
+ context: ClassGetterDecoratorContext<This, Value>
479
+ ): ((this: This) => Value) | void
480
+ export function mobxComputed<Value>(
481
+ target: object,
482
+ propertyKey: string | symbol,
483
+ descriptor: TypedPropertyDescriptor<Value>
484
+ ): TypedPropertyDescriptor<Value> | void
485
+ export function mobxComputed(...args: any[]): any {
486
+ if (isStandardDecoratorContext(args[1])) {
487
+ return (mobx.computed as any)(args[0], args[1])
488
+ }
489
+
490
+ if (getMobxVersion() < 7) {
491
+ return (mobx.computed as any)(...args)
492
+ }
493
+
494
+ const descriptor = args[2] as PropertyDescriptor | undefined
495
+ if (!descriptor?.get) {
496
+ return descriptor
497
+ }
498
+
499
+ const getter = descriptor.get
500
+ const computedValues = new WeakMap<object, IComputedValue<unknown>>()
501
+
502
+ return {
503
+ ...descriptor,
504
+ get(this: object) {
505
+ let computedValue = computedValues.get(this)
506
+ if (!computedValue) {
507
+ const newComputedValue = mobx.computed(() => getter.call(this), {
508
+ name: String(args[1]),
509
+ })
510
+ computedValues.set(this, newComputedValue)
511
+ computedValue = newComputedValue
512
+ }
513
+ return computedValue.get()
514
+ },
515
+ }
516
+ }
517
+
518
+ // Mirrors MobX's standard-decorator call detection. Legacy decorator calls use
519
+ // a string or symbol property key as their second argument, so checking `kind`
520
+ // distinguishes the call shapes without claiming to validate the full context.
521
+ function isStandardDecoratorContext(value: unknown): boolean {
522
+ return (
523
+ typeof value === "object" &&
524
+ value !== null &&
525
+ typeof (value as { kind?: unknown }).kind === "string"
526
+ )
527
+ }
528
+
529
+ /**
530
+ * Calls `makeObservable` across MobX versions. MobX 7 requires an annotations
531
+ * object, while MobX 6 and older versions use decorator metadata when no
532
+ * annotations are passed.
533
+ *
534
+ * @internal
535
+ */
536
+ export function makeObservableCompat(target: object, annotations?: object): void {
537
+ if (getMobxVersion() >= 7) {
538
+ mobx6.makeObservable(target, annotations ?? {})
539
+ } else {
540
+ mobx6.makeObservable(target)
541
+ }
542
+ }
543
+
422
544
  /**
423
545
  * @internal
424
546
  */
@@ -426,17 +548,26 @@ export function propNameToSetterName(propName: string): string {
426
548
  return `set${propName[0].toUpperCase()}${propName.slice(1)}`
427
549
  }
428
550
 
551
+ let cachedMobxVersion: number | undefined
552
+
429
553
  /**
430
554
  * @internal
431
555
  */
432
556
  export function getMobxVersion(): number {
557
+ if (cachedMobxVersion !== undefined) {
558
+ return cachedMobxVersion
559
+ }
560
+
433
561
  if (mobx6.makeObservable!) {
434
- return 6
562
+ const version = (mobx as any)._getGlobalState?.().version
563
+ cachedMobxVersion = typeof version === "number" && version >= 7 ? version : 6
435
564
  } else if ("extendShallowObservable" in mobx) {
436
- return 4
565
+ cachedMobxVersion = 4
437
566
  } else {
438
- return 5
567
+ cachedMobxVersion = 5
439
568
  }
569
+
570
+ return cachedMobxVersion
440
571
  }
441
572
 
442
573
  /**