@praxisjs/devtools 0.2.5 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/devtools",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -12,11 +12,11 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@unocss/reset": "^66.6.2",
15
- "@praxisjs/core": "0.4.2",
16
- "@praxisjs/decorators": "0.4.3",
17
- "@praxisjs/jsx": "0.3.3",
18
- "@praxisjs/shared": "0.2.0",
19
- "@praxisjs/runtime": "0.2.4"
15
+ "@praxisjs/core": "1.0.0",
16
+ "@praxisjs/decorators": "0.5.0",
17
+ "@praxisjs/jsx": "0.3.4",
18
+ "@praxisjs/runtime": "0.2.5",
19
+ "@praxisjs/shared": "0.2.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^25.3.0",
@@ -11,19 +11,31 @@ beforeEach(() => {
11
11
 
12
12
  // ── Method decorator ──────────────────────────────────────────────────────────
13
13
 
14
+ function mockMethodCtx(name: string) {
15
+ const initializers: Array<(this: object) => void> = [];
16
+ return {
17
+ kind: "method" as const,
18
+ name,
19
+ addInitializer(fn: (this: object) => void) { initializers.push(fn); },
20
+ runInitializers(instance: object) { initializers.forEach((fn) => { fn.call(instance); }); },
21
+ } as unknown as ClassMethodDecoratorContext & { runInitializers(instance: object): void };
22
+ }
23
+
14
24
  describe("Debug — method decorator", () => {
15
25
  it("records a method:call timeline entry", () => {
16
- const ctx = { kind: "method" as const, name: "doWork" } as ClassMethodDecoratorContext;
26
+ const ctx = mockMethodCtx("doWork");
17
27
  const original = vi.fn((_x: unknown) => "result");
18
28
 
19
- const wrapped = Debug()(original as never, ctx) as (...args: unknown[]) => unknown;
29
+ Debug()(original as never, ctx);
20
30
 
21
- class Comp { constructor() {} }
31
+ class Comp { doWork(_x: unknown) { return "result"; } }
22
32
  const instance = new Comp();
33
+ ctx.runInitializers(instance);
34
+
23
35
  const registry = Registry.instance;
24
36
  registry.registerComponent(instance, "Comp");
25
37
 
26
- wrapped.call(instance, 42);
38
+ instance.doWork(42);
27
39
 
28
40
  const entry = registry.getTimeline().find((t) => t.type === "method:call");
29
41
  expect(entry).toBeDefined();
@@ -33,31 +45,33 @@ describe("Debug — method decorator", () => {
33
45
  });
34
46
 
35
47
  it("records throw result when method throws", () => {
36
- const ctx = { kind: "method" as const, name: "boom" } as ClassMethodDecoratorContext;
48
+ const ctx = mockMethodCtx("boom");
37
49
  const original = () => { throw new Error("fail"); };
38
50
 
39
- const wrapped = Debug()(original as never, ctx) as () => void;
51
+ Debug()(original as never, ctx);
40
52
 
41
- class Comp {}
53
+ class Comp { boom() { throw new Error("fail"); } }
42
54
  const instance = new Comp();
55
+ ctx.runInitializers(instance);
43
56
  Registry.instance.registerComponent(instance, "Comp");
44
57
 
45
- expect(() => wrapped.call(instance)).toThrow("fail");
58
+ expect(() => (instance as unknown as Record<string, () => void>).boom()).toThrow("fail");
46
59
 
47
60
  const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
48
61
  expect(entry?.data.result).toContain("throw");
49
62
  });
50
63
 
51
64
  it("uses custom label from options", () => {
52
- const ctx = { kind: "method" as const, name: "internal" } as ClassMethodDecoratorContext;
65
+ const ctx = mockMethodCtx("internal");
53
66
  const original = vi.fn(() => undefined);
54
67
 
55
- const wrapped = Debug({ label: "myLabel" })(original as never, ctx) as () => void;
68
+ Debug({ label: "myLabel" })(original as never, ctx);
56
69
 
57
- class Comp {}
70
+ class Comp { internal() { return undefined; } }
58
71
  const instance = new Comp();
72
+ ctx.runInitializers(instance);
59
73
  Registry.instance.registerComponent(instance, "Comp");
60
- wrapped.call(instance);
74
+ (instance as unknown as Record<string, () => void>).internal();
61
75
 
62
76
  const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
63
77
  expect(entry?.label).toContain("myLabel");
@@ -1,4 +1,10 @@
1
1
  import { computed } from "@praxisjs/core/internal";
2
+ import {
3
+ createMethodDecorator,
4
+ createGetterObserverDecorator,
5
+ createFieldDecorator,
6
+ type FieldBinding,
7
+ } from "@praxisjs/decorators";
2
8
 
3
9
  import { Registry } from "../core/registry";
4
10
 
@@ -15,8 +21,7 @@ interface TrackedComputed {
15
21
  function isComputed(value: unknown): value is TrackedComputed {
16
22
  return (
17
23
  typeof value === "function" &&
18
- typeof (value as unknown as Record<string, unknown>).subscribe ===
19
- "function" &&
24
+ typeof (value as unknown as Record<string, unknown>).subscribe === "function" &&
20
25
  !("set" in (value as object))
21
26
  );
22
27
  }
@@ -27,29 +32,14 @@ interface ComputedSlot {
27
32
  }
28
33
 
29
34
  interface DebugDecorator {
30
- (
31
- value: (...args: unknown[]) => unknown,
32
- context: ClassMethodDecoratorContext,
33
- ): (...args: unknown[]) => unknown;
35
+ (value: (...args: unknown[]) => unknown, context: ClassMethodDecoratorContext): void;
34
36
  (value: unknown, context: ClassGetterDecoratorContext): void;
35
37
  (value: undefined, context: ClassFieldDecoratorContext): void;
36
38
  }
37
39
 
38
40
  /**
39
41
  * Tracks state, computed values, and methods in the devtools panel.
40
- *
41
- * On @State() properties (stacked):
42
- * @Debug()
43
- * @State() count = 0;
44
- *
45
- * On @Computed() getters (stacked):
46
- * @Debug({ label: "doubled" })
47
- * @Computed()
48
- * get doubled() { return this.count * 2; }
49
- *
50
- * On methods:
51
- * @Debug()
52
- * increment() { ... }
42
+ * Supports @State() fields, @Computed() getters, and methods.
53
43
  */
54
44
  export function Debug(options: DebugOptions = {}): DebugDecorator {
55
45
  const impl = function (
@@ -61,120 +51,83 @@ export function Debug(options: DebugOptions = {}): DebugDecorator {
61
51
  ) {
62
52
  const label = options.label ?? (context.name as string);
63
53
 
64
- // ── Method decorator ─────────────────────────────────────────────────
65
54
  if (context.kind === "method") {
66
55
  const original = value as (...args: unknown[]) => unknown;
67
-
68
- return function (this: object, ...args: unknown[]) {
69
- const componentName = (this.constructor as { name: string }).name;
70
- const start = performance.now();
71
- let result: unknown;
72
- let threw = false;
73
-
74
- try {
75
- result = original.apply(this, args);
76
- } catch (err) {
77
- threw = true;
78
- result = err;
79
- throw err;
80
- } finally {
81
- const duration = performance.now() - start;
82
- Registry.instance.recordMethodCall(
83
- this,
84
- label,
85
- args,
86
- threw ? `throw ${String(result)}` : result,
87
- duration,
88
- componentName,
89
- );
90
- }
91
-
92
- return result;
93
- };
56
+ createMethodDecorator({
57
+ wrap(_original, _instance, _name) {
58
+ return function (this: object, ...args: unknown[]) {
59
+ const componentName = (this.constructor as { name: string }).name;
60
+ const start = performance.now();
61
+ let result: unknown;
62
+ let threw = false;
63
+ try {
64
+ result = original.apply(this, args);
65
+ } catch (err) {
66
+ threw = true;
67
+ result = err;
68
+ throw err;
69
+ } finally {
70
+ const duration = performance.now() - start;
71
+ Registry.instance.recordMethodCall(
72
+ this, label, args,
73
+ threw ? `throw ${String(result)}` : result,
74
+ duration, componentName,
75
+ );
76
+ }
77
+ return result;
78
+ };
79
+ },
80
+ })(original, context);
81
+ return;
94
82
  }
95
83
 
96
- // ── Getter decorator (@Computed() getters) ────────────────────────────
97
84
  if (context.kind === "getter") {
98
85
  const originalGetter = value as (this: object) => unknown;
99
-
100
- context.addInitializer(function (this: unknown) {
101
- const instance = this as object & Record<string, unknown>;
102
- const componentName = (instance.constructor as { name: string }).name;
103
-
104
- // Defer until the current synchronous class initialization (including
105
- // field initializers like `count = 0`) has fully completed. Without
106
- // this, accessing `this.doubled` here could read state signals before
107
- // their field initializers have run, producing NaN or undefined.
108
- queueMicrotask(() => {
109
- const c = computed(() => originalGetter.call(instance));
110
- let skipFirst = true;
111
- let prevValue = c();
112
-
113
- Registry.instance.registerSignal(instance, label, prevValue, componentName);
114
-
115
- c.subscribe((newValue) => {
116
- if (skipFirst) {
117
- skipFirst = false;
118
- return;
119
- }
120
- Registry.instance.updateSignal(instance, label, newValue, prevValue);
121
- prevValue = newValue;
86
+ createGetterObserverDecorator({
87
+ observe(getter, instance, _name) {
88
+ const componentName = (instance.constructor as { name: string }).name;
89
+ // Defer so field initializers (e.g. @State()) have run before we read them.
90
+ queueMicrotask(() => {
91
+ const c = computed(() => getter.call(instance));
92
+ let skipFirst = true;
93
+ let prevValue = c();
94
+ Registry.instance.registerSignal(instance, label, prevValue, componentName);
95
+ c.subscribe((newValue) => {
96
+ if (skipFirst) { skipFirst = false; return; }
97
+ Registry.instance.updateSignal(instance, label, newValue, prevValue);
98
+ prevValue = newValue;
99
+ });
122
100
  });
123
- });
124
- });
125
-
101
+ },
102
+ })(originalGetter, context);
126
103
  return;
127
104
  }
128
105
 
129
- // ── Field decorator ───────────────────────────────────────────────────
130
- context.addInitializer(function (
131
- this: unknown,
132
- ) {
133
- const instance = this as object & Record<string, unknown>;
134
- const name = context.name as string;
135
- const componentName = (
136
- instance.constructor as { name: string }
137
- ).name;
138
-
139
- // ── Wrapping @State() getter/setter ─────────────────────────────
140
- // @State() initializer runs before @Debug() initializer (inner-first),
141
- // so the getter/setter is already installed on the instance here.
142
- const existingDesc = Object.getOwnPropertyDescriptor(instance, name);
106
+ createFieldDecorator({
107
+ bind(instance, name, _initialValue): FieldBinding {
108
+ const componentName = (instance.constructor as { name: string }).name;
143
109
 
110
+ // @State() runs inner-first, so its getter/setter is already on the instance.
111
+ const existingDesc = Object.getOwnPropertyDescriptor(instance, name);
144
112
  if (existingDesc?.get && existingDesc.set) {
145
113
  // eslint-disable-next-line @typescript-eslint/unbound-method
146
114
  const originalGet = existingDesc.get;
147
115
  // eslint-disable-next-line @typescript-eslint/unbound-method
148
116
  const originalSet = existingDesc.set;
149
-
150
- Registry.instance.registerSignal(
151
- instance,
152
- label,
153
- originalGet.call(instance),
154
- componentName,
155
- );
156
-
157
- Object.defineProperty(instance, name, {
158
- get(this: object) {
159
- return originalGet.call(this) as unknown;
160
- },
161
- set(this: object, newValue: unknown) {
162
- const oldValue: unknown = originalGet.call(this);
163
- originalSet.call(this, newValue);
164
- Registry.instance.updateSignal(this, label, newValue, oldValue);
117
+ Registry.instance.registerSignal(instance, label, originalGet.call(instance), componentName);
118
+ return {
119
+ descriptor: {
120
+ get(this: object) { return originalGet.call(this) as unknown; },
121
+ set(this: object, newValue: unknown) {
122
+ const oldValue: unknown = originalGet.call(this);
123
+ originalSet.call(this, newValue);
124
+ Registry.instance.updateSignal(this, label, newValue, oldValue);
125
+ },
165
126
  },
166
- enumerable: true,
167
- configurable: true,
168
- });
169
-
170
- return;
127
+ };
171
128
  }
172
129
 
173
- // ── Computed class field ─────────────────────────────────────────
174
- // The field initializer has already run, so read the assigned value.
175
- const initialValue = instance[name];
176
- Reflect.deleteProperty(instance, name);
177
-
130
+ const initialValue = _initialValue;
178
131
  if (!isComputed(initialValue)) {
179
132
  if (initialValue !== undefined) {
180
133
  console.warn(
@@ -182,54 +135,45 @@ export function Debug(options: DebugOptions = {}): DebugDecorator {
182
135
  `expected a computed() value but got ${typeof initialValue}. Skipping.`,
183
136
  );
184
137
  }
185
- instance[name] = initialValue;
186
- return;
138
+ return {};
187
139
  }
188
140
 
189
- // subscribe() calls the callback immediately (synchronously), so we
190
- // use a flag to skip the first call and register via registerSignal instead.
191
141
  let slot: ComputedSlot;
192
142
 
193
- function subscribe(computed: TrackedComputed): () => void {
143
+ // subscribe() fires the callback immediately — skip that first call
144
+ // and register the initial value via registerSignal instead.
145
+ function subscribe(comp: TrackedComputed): () => void {
194
146
  let skipFirst = true;
195
- let prevValue = computed();
196
-
197
- const unsub = computed.subscribe((newValue) => {
198
- if (skipFirst) {
199
- skipFirst = false;
200
- return;
201
- }
147
+ let prevValue = comp();
148
+ const unsub = comp.subscribe((newValue) => {
149
+ if (skipFirst) { skipFirst = false; return; }
202
150
  Registry.instance.updateSignal(instance, label, newValue, prevValue);
203
151
  prevValue = newValue;
204
152
  });
205
-
206
153
  Registry.instance.registerSignal(instance, label, prevValue, componentName);
207
154
  return unsub;
208
155
  }
209
156
 
210
157
  slot = { computed: initialValue, unsub: subscribe(initialValue) };
211
158
 
212
- Object.defineProperty(instance, name, {
213
- get() {
214
- return slot.computed;
215
- },
216
- set(newValue: unknown) {
217
- slot.unsub();
218
-
219
- if (!isComputed(newValue)) {
220
- console.warn(
221
- `[PraxisJS DevTools] @Debug() on "${componentName}.${name}": ` +
222
- `expected a computed() value but got ${typeof newValue}. Skipping.`,
223
- );
224
- return;
225
- }
226
-
227
- slot = { computed: newValue, unsub: subscribe(newValue) };
159
+ return {
160
+ descriptor: {
161
+ get() { return slot.computed; },
162
+ set(newValue: unknown) {
163
+ slot.unsub();
164
+ if (!isComputed(newValue)) {
165
+ console.warn(
166
+ `[PraxisJS DevTools] @Debug() on "${componentName}.${name}": ` +
167
+ `expected a computed() value but got ${typeof newValue}. Skipping.`,
168
+ );
169
+ return;
170
+ }
171
+ slot = { computed: newValue, unsub: subscribe(newValue) };
172
+ },
228
173
  },
229
- enumerable: true,
230
- configurable: true,
231
- });
232
- });
174
+ };
175
+ },
176
+ })(undefined, context);
233
177
  };
234
178
 
235
179
  return impl as DebugDecorator;
@@ -1,8 +1,17 @@
1
+ import type { RootComponent } from "@praxisjs/core/internal";
2
+ import {
3
+ createClassDecorator,
4
+ ClassBehavior,
5
+ type ClassEnhancement,
6
+ } from "@praxisjs/decorators";
7
+
1
8
  import { Registry } from "../core/registry";
2
9
 
3
10
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
11
  type AnyConstructor = new (...args: any[]) => any;
5
12
 
13
+ const EXTRA_HOOKS = ["onBeforeUpdate", "onUpdate", "onAfterUpdate"] as const;
14
+
6
15
  /**
7
16
  * Instruments a component class to report renders and lifecycle events
8
17
  * to the devtools panel.
@@ -11,58 +20,52 @@ type AnyConstructor = new (...args: any[]) => any;
11
20
  * @Component()
12
21
  * class MyComponent extends StatefulComponent { ... }
13
22
  */
14
- export function Trace() {
15
- return function <T extends AnyConstructor>(constructor: T, _context: ClassDecoratorContext): T {
16
- const name = constructor.name;
23
+ class TraceBehavior extends ClassBehavior {
24
+ create(instance: RootComponent): ClassEnhancement {
17
25
  const registry = Registry.instance;
18
- const proto = constructor.prototype as Record<string, unknown>;
19
-
20
- // ── render() ──────────────────────────────────────────────────────────
21
- const originalRender = proto.render as
22
- | ((...args: unknown[]) => unknown)
23
- | undefined;
24
26
 
25
- if (originalRender) {
26
- proto.render = function (this: object, ...args: unknown[]) {
27
+ return {
28
+ onMount() {
29
+ registry.recordLifecycle(instance, "onMount");
30
+ },
31
+ onUnmount() {
32
+ registry.recordLifecycle(instance, "onUnmount");
33
+ },
34
+ render(originalRender) {
27
35
  const start = performance.now();
28
- const result = originalRender.call(this, ...args);
36
+ const result = originalRender();
29
37
  const duration = performance.now() - start;
30
- registry.recordRender(this, duration);
38
+ registry.recordRender(instance, duration);
31
39
  return result;
32
- };
33
- }
40
+ },
41
+ };
42
+ }
34
43
 
35
- // ── onBeforeMount – register the component instance ───────────────────
36
- const originalOnBeforeMount = proto.onBeforeMount as
37
- | ((...args: unknown[]) => unknown)
38
- | undefined;
44
+ initialize(Enhanced: AnyConstructor, original: AnyConstructor): void {
45
+ const name = original.name;
46
+ const registry = Registry.instance;
47
+ const proto = Enhanced.prototype as Record<string, unknown>;
39
48
 
49
+ // onBeforeMount — also registers the component instance
50
+ const originalOnBeforeMount = proto.onBeforeMount as ((...args: unknown[]) => unknown) | undefined;
40
51
  proto.onBeforeMount = function (this: object, ...args: unknown[]) {
41
52
  registry.registerComponent(this, name);
42
53
  registry.recordLifecycle(this, "onBeforeMount");
43
54
  return originalOnBeforeMount?.call(this, ...args);
44
55
  };
45
56
 
46
- // ── remaining lifecycle hooks ─────────────────────────────────────────
47
- const hooks = [
48
- "onMount",
49
- "onUnmount",
50
- "onBeforeUpdate",
51
- "onUpdate",
52
- "onAfterUpdate",
53
- ] as const;
54
-
55
- for (const hook of hooks) {
56
- const original = proto[hook] as
57
- | ((...args: unknown[]) => unknown)
58
- | undefined;
59
-
57
+ for (const hook of EXTRA_HOOKS) {
58
+ const orig = proto[hook] as ((...args: unknown[]) => unknown) | undefined;
60
59
  proto[hook] = function (this: object, ...args: unknown[]) {
61
60
  registry.recordLifecycle(this, hook);
62
- return original?.call(this, ...args);
61
+ return orig?.call(this, ...args);
63
62
  };
64
63
  }
64
+ }
65
+ }
66
+
67
+ const traceBehavior = new TraceBehavior();
65
68
 
66
- return constructor;
67
- };
69
+ export function Trace() {
70
+ return createClassDecorator(traceBehavior);
68
71
  }