Skip to content

Commit efe773c

Browse files
committed
feat: add @ember/inspector-support public API for Ember Inspector
Implements the public inspector API as outlined in emberjs/ember-inspector#2723. This provides a stable, high-level interface for the Ember Inspector to access Ember internals without depending on private APIs. The API includes: - debug: captureRenderTree, inspect, registerDeprecationHandler - environment: getEnv, VERSION - instrumentation: subscribe, unsubscribe - objectInternals: cacheFor, guidFor, meta, get, set - owner: getOwner, lookup, factoryFor, hasRegistration, getContainerInstances - libraries: getRegistry - typeChecking: isEmberObject, isComponent, isGlimmerComponent, isService, etc. - naming: getClassName (resolves Ember class/mixin names) - tracking: createPropertyTracker, hasPropertyChanged, getPropertyDependencies, etc. - computed: isComputed, getComputedMetadata, isMandatorySetter, isCached - renderTree: getDebugRenderTree - runloop: getBackburner, join, debounce, cancel
1 parent d4fd1fd commit efe773c

15 files changed

Lines changed: 1333 additions & 0 deletions
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @ember/inspector-support
3+
*
4+
* Public API for the Ember Inspector to access Ember internals in a stable,
5+
* version-independent way. This package is intended to be accessed by the
6+
* Ember Inspector via `appLoader.loadCompatInspector()`.
7+
*
8+
* ## Usage
9+
*
10+
* ```javascript
11+
* // In ember-inspector:
12+
* const api = globalThis.emberInspectorApps[0].loadCompatInspector();
13+
*
14+
* // Use the API:
15+
* const renderTree = api.debug.captureRenderTree();
16+
* const guid = api.objectInternals.guidFor(someObject);
17+
* const tracker = api.tracking.createPropertyTracker(obj, 'myProp');
18+
* ```
19+
*
20+
* ## Design Goals
21+
*
22+
* - **Stability**: Public API contract prevents breaking changes
23+
* - **Simplicity**: High-level APIs are easier to use and understand
24+
* - **Encapsulation**: Implementation details hidden behind function boundaries
25+
* - **Reduced Coupling**: Inspector doesn't need to import or reference Ember classes
26+
*
27+
* @module @ember/inspector-support
28+
* @public
29+
*/
30+
31+
export { debug } from './lib/debug';
32+
export { environment } from './lib/environment';
33+
export { instrumentation } from './lib/instrumentation';
34+
export { objectInternals } from './lib/object-internals';
35+
export { owner } from './lib/owner';
36+
export { libraries } from './lib/libraries';
37+
export { typeChecking } from './lib/type-checking';
38+
export { naming } from './lib/naming';
39+
export { tracking } from './lib/tracking';
40+
export { computed } from './lib/computed';
41+
export { renderTree } from './lib/render-tree';
42+
export { runloop } from './lib/runloop';
43+
44+
export type {
45+
EmberInspectorAPI,
46+
RenderNode,
47+
Bounds,
48+
Library,
49+
Owner,
50+
PropertyTracker,
51+
PropertyDependency,
52+
ComputedMetadata,
53+
DeprecationOptions,
54+
DeprecationHandler,
55+
InstrumentationCallbacks,
56+
EmberEnvironment,
57+
ContainerInstance,
58+
GetContainerInstancesOptions,
59+
} from './types';
60+
61+
import { debug } from './lib/debug';
62+
import { environment } from './lib/environment';
63+
import { instrumentation } from './lib/instrumentation';
64+
import { objectInternals } from './lib/object-internals';
65+
import { owner } from './lib/owner';
66+
import { libraries } from './lib/libraries';
67+
import { typeChecking } from './lib/type-checking';
68+
import { naming } from './lib/naming';
69+
import { tracking } from './lib/tracking';
70+
import { computed } from './lib/computed';
71+
import { renderTree } from './lib/render-tree';
72+
import { runloop } from './lib/runloop';
73+
import type { EmberInspectorAPI } from './types';
74+
75+
/**
76+
* The complete Ember Inspector API object.
77+
*
78+
* This is the object returned by `appLoader.loadCompatInspector()`.
79+
* It provides a stable, high-level interface for the Ember Inspector
80+
* to access Ember internals without depending on private APIs.
81+
*/
82+
export const emberInspectorAPI: EmberInspectorAPI = {
83+
debug,
84+
environment,
85+
instrumentation,
86+
objectInternals,
87+
owner,
88+
libraries,
89+
typeChecking,
90+
naming,
91+
tracking,
92+
computed,
93+
renderTree,
94+
runloop,
95+
};
96+
97+
export default emberInspectorAPI;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { descriptorForProperty, isComputed } from '@ember/-internals/metal';
2+
import type { ComputedMetadata } from '../types';
3+
4+
export const computed = {
5+
/**
6+
* Check if a property is a computed property.
7+
*/
8+
isComputed(obj: object, key: string): boolean {
9+
try {
10+
return isComputed(obj, key);
11+
} catch {
12+
return false;
13+
}
14+
},
15+
16+
/**
17+
* Get the computed property descriptor for a property.
18+
* Returns null if the property is not computed.
19+
*/
20+
getComputedPropertyDescriptor(obj: object, key: string): unknown | null {
21+
try {
22+
return descriptorForProperty(obj, key) ?? null;
23+
} catch {
24+
return null;
25+
}
26+
},
27+
28+
/**
29+
* Get the dependent keys for a computed property.
30+
* Returns an empty array if the property is not computed or has no dependent keys.
31+
*/
32+
getDependentKeys(obj: object, key: string): string[] {
33+
try {
34+
const desc = descriptorForProperty(obj, key) as any;
35+
return desc?._dependentKeys ?? [];
36+
} catch {
37+
return [];
38+
}
39+
},
40+
41+
/**
42+
* Get computed property metadata without accessing private properties directly.
43+
* Replaces direct access to desc._getter, desc._readOnly, desc._auto, etc.
44+
*
45+
* @param descriptor - The computed property descriptor
46+
* @returns Public metadata object, or null if descriptor is invalid
47+
*/
48+
getComputedMetadata(descriptor: unknown): ComputedMetadata | null {
49+
if (!descriptor) {
50+
return null;
51+
}
52+
53+
const desc = descriptor as any;
54+
55+
const getter: Function | undefined = desc._getter ?? desc.get;
56+
const setter: Function | undefined = desc.set;
57+
58+
return {
59+
getter,
60+
setter,
61+
readOnly: desc._readOnly ?? false,
62+
auto: desc._auto ?? false,
63+
dependentKeys: desc._dependentKeys ?? [],
64+
code: getter ? Function.prototype.toString.call(getter) : undefined,
65+
};
66+
},
67+
68+
/**
69+
* Check if a descriptor is Ember's mandatory setter.
70+
* Replaces checking for "You attempted to update" string in setter code.
71+
*
72+
* @param descriptor - The property descriptor
73+
* @returns true if this is a mandatory setter
74+
*/
75+
isMandatorySetter(descriptor: unknown): boolean {
76+
if (!descriptor) return false;
77+
78+
const desc = descriptor as any;
79+
if (typeof desc.set !== 'function') return false;
80+
81+
try {
82+
return Function.prototype.toString.call(desc.set).includes('You attempted to update');
83+
} catch {
84+
return false;
85+
}
86+
},
87+
88+
/**
89+
* Check if a property uses the @cached decorator from @glimmer/tracking.
90+
* The @cached decorator memoizes getter results and invalidates when dependencies change.
91+
*
92+
* @param obj - The object
93+
* @param key - The property name
94+
* @returns true if the property uses @cached decorator
95+
*/
96+
isCached(obj: object, key: string): boolean {
97+
try {
98+
// @cached replaces the getter with one that uses createCache/getValue from @glimmer/validator
99+
// It is NOT a ComputedProperty descriptor - it's a native getter
100+
if (this.isComputed(obj, key)) {
101+
return false;
102+
}
103+
104+
// Check the prototype chain for a native getter
105+
let proto: object | null = Object.getPrototypeOf(obj);
106+
while (proto !== null && proto !== Object.prototype) {
107+
const nativeDesc = Object.getOwnPropertyDescriptor(proto, key);
108+
if (nativeDesc?.get) {
109+
// @cached wraps the getter with a WeakMap-based cache using @glimmer/validator's createCache
110+
// We detect it by checking if the getter's source references the cache WeakMap pattern
111+
// This is a best-effort heuristic
112+
const src = Function.prototype.toString.call(nativeDesc.get);
113+
if (src.includes('caches') && src.includes('getValue')) {
114+
return true;
115+
}
116+
break;
117+
}
118+
proto = Object.getPrototypeOf(proto);
119+
}
120+
121+
return false;
122+
} catch {
123+
return false;
124+
}
125+
},
126+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
captureRenderTree as _captureRenderTree,
3+
inspect,
4+
registerDeprecationHandler,
5+
} from '@ember/debug';
6+
import type { Owner, DeprecationHandler } from '../types';
7+
8+
export const debug = {
9+
/**
10+
* Captures the current component render tree for the component inspector.
11+
*/
12+
captureRenderTree(app: Owner) {
13+
return _captureRenderTree(app as any);
14+
},
15+
16+
/**
17+
* Convert any value to a human-readable string representation.
18+
*/
19+
inspect(value: unknown): string {
20+
return inspect(value);
21+
},
22+
23+
/**
24+
* Register a custom handler for deprecation warnings.
25+
*/
26+
registerDeprecationHandler(handler: DeprecationHandler): void {
27+
registerDeprecationHandler(handler as any);
28+
},
29+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ENV } from '@ember/-internals/environment';
2+
import VERSION from 'ember/version';
3+
import type { EmberEnvironment } from '../types';
4+
5+
export const environment = {
6+
/**
7+
* Get Ember's environment configuration.
8+
*/
9+
getEnv(): EmberEnvironment {
10+
return ENV as EmberEnvironment;
11+
},
12+
13+
/**
14+
* The current Ember version string.
15+
*/
16+
VERSION,
17+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { subscribe, unsubscribe } from '@ember/instrumentation';
2+
import type { InstrumentationCallbacks } from '../types';
3+
4+
export const instrumentation = {
5+
/**
6+
* Subscribe to an Ember instrumentation event.
7+
*
8+
* @param eventName - Namespaced event name (e.g. "render", "render.component")
9+
* @param callbacks - Before and after hooks
10+
* @returns A subscriber token that can be passed to unsubscribe
11+
*/
12+
subscribe<T>(
13+
eventName: string,
14+
callbacks: InstrumentationCallbacks<T>
15+
): { pattern: string; regex: RegExp; object: InstrumentationCallbacks<T> } {
16+
return subscribe(eventName, {
17+
before: callbacks.before ?? (() => undefined as unknown as T),
18+
after: callbacks.after ?? (() => {}),
19+
}) as { pattern: string; regex: RegExp; object: InstrumentationCallbacks<T> };
20+
},
21+
22+
/**
23+
* Unsubscribe from an instrumentation event.
24+
*
25+
* @param subscriber - The subscriber token returned by subscribe
26+
*/
27+
unsubscribe(subscriber: {
28+
pattern: string;
29+
regex: RegExp;
30+
object: InstrumentationCallbacks;
31+
}): void {
32+
unsubscribe(subscriber as any);
33+
},
34+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { libraries as LIBRARIES } from '@ember/-internals/metal';
2+
import type { Library } from '../types';
3+
4+
export const libraries = {
5+
/**
6+
* Get the registry of all loaded Ember libraries and addons.
7+
* Used to display loaded libraries in the Info tab of the inspector.
8+
*/
9+
getRegistry(): Library[] {
10+
return LIBRARIES._registry.map((lib) => ({
11+
name: lib.name,
12+
version: lib.version,
13+
}));
14+
},
15+
};

0 commit comments

Comments
 (0)