From d47cf58f1041d5cde8062a6cffd471730febda52 Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Thu, 6 Mar 2025 16:00:55 -0800 Subject: [PATCH] refactor(core): update `ng.getDirectiveMetadata` to support Wiz and ACX (#60475) This allows `ng.getDirectiveMetadata` to be implemented by Wiz and ACX with subtly different shapes to match the nuances of those frameworks. Existing usage of `{Component,Directive}DebugMetadata` was moved over to `Angular{Component,Directive}DebugMetadata` as appropriate, since the implementation of `ng` in `@angular/core` is specific to Angular. Only the types support Wiz and ACX. I opted to merge `ComponentDebugMetadata` and `DirectiveDebugMetadata` into a single type of all the frameworks including both components and directives (recall that components extend directives). The reasoning for this is because Wiz does not support directives (you can kind of think of "Wiz Directive" as an abstract class extended by "Wiz Components"). I felt that a `DirectiveDebugMetadata` containing only Angular and ACX types would be a bit of a trap and lead to bugs when used. It's safer to just have the single type containing all the possible results from `ng.getDirectiveMetadata`. I also chose to leave the `ng` type as is internally, since `@angular/core` implements a specific concrete version of it narrowed to Angular types. Separately I defined an expanded `FrameworkAgnosticGlobalUtils` which redefines `ng.getDirectiveMetadata` to include Wiz and ACX. We want this type to exist in the Angular GitHub repo so it can be referenced as a common primitive across all three frameworks. This is sufficient for now, however longer term we will likely want to actually manually define the function types in this framework-agnostic interface and make Angular's version properly implement it rather than extend and overwrite Angular's type. PR Close #60475 --- .../src/lib/component-tree.ts | 4 +- .../src/lib/ng-debug-api/ng-debug-api.ts | 12 +-- goldens/public-api/core/global_utils.api.md | 17 +---- .../core/src/core_render3_private_export.ts | 16 +++- packages/core/src/render3/global_utils_api.ts | 1 - packages/core/src/render3/index.ts | 20 ++++- .../core/src/render3/util/discovery_utils.ts | 73 +++++++++++++++---- .../core/src/render3/util/global_utils.ts | 16 ++++ .../test/acceptance/discover_utils_spec.ts | 14 ++-- 9 files changed, 123 insertions(+), 50 deletions(-) diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts b/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts index acda1fb7928..3053b5f66cb 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts @@ -15,7 +15,7 @@ import type { Injector, Type, ValueProvider, - ɵComponentDebugMetadata as ComponentDebugMetadata, + ɵAngularComponentDebugMetadata as AngularComponentDebugMetadata, ɵProviderRecord as ProviderRecord, } from '@angular/core'; import { @@ -219,7 +219,7 @@ const enum DirectiveMetadataKey { // the method directly interacts with the directive/component definition. const getDirectiveMetadata = (dir: any): DirectiveMetadata => { const getMetadata = ngDebugClient().getDirectiveMetadata!; - const metadata = getMetadata?.(dir) as ComponentDebugMetadata; + const metadata = getMetadata?.(dir) as AngularComponentDebugMetadata; if (metadata) { return { inputs: metadata.inputs, diff --git a/devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/ng-debug-api.ts b/devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/ng-debug-api.ts index b642f7af5a2..77d0d1e2be8 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/ng-debug-api.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/ng-debug-api.ts @@ -6,24 +6,24 @@ * found in the LICENSE file at https://angular.dev/license */ -import type {ɵGlobalDevModeUtils as GlobalDevModeUtils} from '@angular/core'; +import type {ɵFrameworkAgnosticGlobalUtils as GlobalUtils} from '@angular/core'; /** * Returns a handle to window.ng APIs (global angular debugging). * * @returns window.ng */ -export const ngDebugClient = () => (window as any).ng as Partial; +export const ngDebugClient = () => (window as any).ng as Partial; /** * Type guard that checks whether a given debug API is supported within window.ng * * @returns whether the ng object includes the given debug API */ -export function ngDebugApiIsSupported< - T extends Partial, - K extends keyof T, ->(ng: T, api: K): ng is T & Record> { +export function ngDebugApiIsSupported, K extends keyof T>( + ng: T, + api: K, +): ng is T & Record> { return typeof ng[api] === 'function'; } diff --git a/goldens/public-api/core/global_utils.api.md b/goldens/public-api/core/global_utils.api.md index 5b7650e9543..5e225ebbe6e 100644 --- a/goldens/public-api/core/global_utils.api.md +++ b/goldens/public-api/core/global_utils.api.md @@ -8,20 +8,7 @@ export function applyChanges(component: {}): void; // @public -export interface ComponentDebugMetadata extends DirectiveDebugMetadata { - // (undocumented) - changeDetection: ChangeDetectionStrategy; - // (undocumented) - encapsulation: ViewEncapsulation; -} - -// @public -export interface DirectiveDebugMetadata { - // (undocumented) - inputs: Record; - // (undocumented) - outputs: Record; -} +export type DirectiveDebugMetadata = AngularDirectiveDebugMetadata | AcxDirectiveDebugMetadata | AngularComponentDebugMetadata | AcxComponentDebugMetadata | WizComponentDebugMetadata; // @public export function getComponent(element: Element): T | null; @@ -30,7 +17,7 @@ export function getComponent(element: Element): T | null; export function getContext(element: Element): T | null; // @public -export function getDirectiveMetadata(directiveOrComponentInstance: any): ComponentDebugMetadata | DirectiveDebugMetadata | null; +export function getDirectiveMetadata(directiveOrComponentInstance: any): AngularComponentDebugMetadata | AngularDirectiveDebugMetadata | null; // @public export function getDirectives(node: Node): {}[]; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index ee80cf956e4..9b65dbee6c9 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -39,7 +39,6 @@ export { export { AttributeMarker as ɵAttributeMarker, ComponentDef as ɵComponentDef, - ComponentDebugMetadata as ɵComponentDebugMetadata, ComponentFactory as ɵRender3ComponentFactory, ComponentRef as ɵRender3ComponentRef, ComponentType as ɵComponentType, @@ -62,6 +61,16 @@ export { ɵDeferBlockDependencyInterceptor, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, ɵDEFER_BLOCK_CONFIG, + Framework as ɵFramework, + BaseDirectiveDebugMetadata as ɵBaseDirectiveDebugMetadata, + AngularDirectiveDebugMetadata as ɵAngularDirectiveDebugMetadata, + AngularComponentDebugMetadata as ɵAngularComponentDebugMetadata, + AcxChangeDetectionStrategy as ɵAcxChangeDetectionStrategy, + AcxViewEncapsulation as ɵAcxViewEncapsulation, + AcxDirectiveDebugMetadata as ɵAcxDirectiveDebugMetadata, + AcxComponentDebugMetadata as ɵAcxComponentDebugMetadata, + WizComponentDebugMetadata as ɵWizComponentDebugMetadata, + DirectiveDebugMetadata as ɵDirectiveDebugMetadata, ɵɵadvance, ɵɵattribute, ɵɵattributeInterpolate1, @@ -282,7 +291,10 @@ export { export {compilePipe as ɵcompilePipe} from './render3/jit/pipe'; export {isNgModule as ɵisNgModule} from './render3/jit/util'; export {Profiler as ɵProfiler, ProfilerEvent as ɵProfilerEvent} from './render3/profiler_types'; -export {GlobalDevModeUtils as ɵGlobalDevModeUtils} from './render3/util/global_utils'; +export { + FrameworkAgnosticGlobalUtils as ɵFrameworkAgnosticGlobalUtils, + GlobalDevModeUtils as ɵGlobalDevModeUtils, +} from './render3/util/global_utils'; export { ViewRef as ɵViewRef, isViewDirty as ɵisViewDirty, diff --git a/packages/core/src/render3/global_utils_api.ts b/packages/core/src/render3/global_utils_api.ts index 672d97821ba..3e621a6d32d 100644 --- a/packages/core/src/render3/global_utils_api.ts +++ b/packages/core/src/render3/global_utils_api.ts @@ -17,7 +17,6 @@ export {applyChanges} from './util/change_detection_utils'; export { - ComponentDebugMetadata, DirectiveDebugMetadata, getComponent, getContext, diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 1ddd36cac18..def4206f36f 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -30,7 +30,15 @@ import { } from './interfaces/public_definitions'; import {ɵɵsetComponentScope, ɵɵsetNgModuleScope} from './scope'; import { - ComponentDebugMetadata, + Framework, + BaseDirectiveDebugMetadata, + AngularDirectiveDebugMetadata, + AngularComponentDebugMetadata, + AcxChangeDetectionStrategy, + AcxViewEncapsulation, + AcxDirectiveDebugMetadata, + AcxComponentDebugMetadata, + WizComponentDebugMetadata, DirectiveDebugMetadata, getComponent, getDirectiveMetadata, @@ -223,10 +231,18 @@ export {ɵsetClassDebugInfo} from './debug/set_debug_info'; export {ɵɵreplaceMetadata} from './hmr'; export { - ComponentDebugMetadata, ComponentDef, ComponentTemplate, ComponentType, + Framework, + BaseDirectiveDebugMetadata, + AngularDirectiveDebugMetadata, + AngularComponentDebugMetadata, + AcxChangeDetectionStrategy, + AcxViewEncapsulation, + AcxDirectiveDebugMetadata, + AcxComponentDebugMetadata, + WizComponentDebugMetadata, DirectiveDebugMetadata, DirectiveDef, DirectiveType, diff --git a/packages/core/src/render3/util/discovery_utils.ts b/packages/core/src/render3/util/discovery_utils.ts index ab638cc3aaa..d28bd245401 100644 --- a/packages/core/src/render3/util/discovery_utils.ts +++ b/packages/core/src/render3/util/discovery_utils.ts @@ -224,34 +224,79 @@ export function getDirectives(node: Node): {}[] { return context.directives === null ? [] : [...context.directives]; } +/** The framework used to author a particular application or component. */ +export enum Framework { + Angular = 'angular', + ACX = 'acx', + Wiz = 'wiz', +} + +/** Metadata common to directives from all frameworks. */ +export interface BaseDirectiveDebugMetadata { + name?: string; + framework?: Framework; +} + /** - * Partial metadata for a given directive instance. - * This information might be useful for debugging purposes or tooling. - * Currently only `inputs` and `outputs` metadata is available. + * Partial metadata for a given Angular directive instance. * * @publicApi */ -export interface DirectiveDebugMetadata { +export interface AngularDirectiveDebugMetadata extends BaseDirectiveDebugMetadata { + framework?: Framework.Angular; // Optional for backwards compatibility. inputs: Record; outputs: Record; } /** - * Partial metadata for a given component instance. - * This information might be useful for debugging purposes or tooling. - * Currently the following fields are available: - * - inputs - * - outputs - * - encapsulation - * - changeDetection + * Partial metadata for a given Angular component instance. * * @publicApi */ -export interface ComponentDebugMetadata extends DirectiveDebugMetadata { +export interface AngularComponentDebugMetadata extends AngularDirectiveDebugMetadata { encapsulation: ViewEncapsulation; changeDetection: ChangeDetectionStrategy; } +/** ACX change detection strategies. */ +export enum AcxChangeDetectionStrategy { + Default = 0, + OnPush = 1, +} + +/** ACX view encapsulation modes. */ +export enum AcxViewEncapsulation { + Emulated = 0, + None = 1, +} + +/** Partial metadata for a given ACX directive instance. */ +export interface AcxDirectiveDebugMetadata extends BaseDirectiveDebugMetadata { + framework: Framework.ACX; + inputs: Record; + outputs: Record; +} + +/** Partial metadata for a given ACX component instance. */ +export interface AcxComponentDebugMetadata extends AcxDirectiveDebugMetadata { + changeDetection: AcxChangeDetectionStrategy; + encapsulation: AcxViewEncapsulation; +} + +/** Partial metadata for a given Wiz component instance. */ +export interface WizComponentDebugMetadata extends BaseDirectiveDebugMetadata { + framework: Framework.Wiz; + props: Record; +} + +/** All potential debug metadata types across all frameworks. */ +export type DirectiveDebugMetadata = + | AngularDirectiveDebugMetadata + | AcxDirectiveDebugMetadata + | AngularComponentDebugMetadata + | AcxComponentDebugMetadata + | WizComponentDebugMetadata; + /** * Returns the debug (partial) metadata for a particular directive or component instance. * The function accepts an instance of a directive or component and returns the corresponding @@ -264,7 +309,7 @@ export interface ComponentDebugMetadata extends DirectiveDebugMetadata { */ export function getDirectiveMetadata( directiveOrComponentInstance: any, -): ComponentDebugMetadata | DirectiveDebugMetadata | null { +): AngularComponentDebugMetadata | AngularDirectiveDebugMetadata | null { const {constructor} = directiveOrComponentInstance; if (!constructor) { throw new Error('Unable to find the instance constructor'); @@ -478,7 +523,7 @@ function assertDomElement(value: any) { * mappings for backwards compatibility. */ function extractInputDebugMetadata(inputs: DirectiveDef['inputs']) { - const res: DirectiveDebugMetadata['inputs'] = {}; + const res: AngularDirectiveDebugMetadata['inputs'] = {}; for (const key in inputs) { if (inputs.hasOwnProperty(key)) { diff --git a/packages/core/src/render3/util/global_utils.ts b/packages/core/src/render3/util/global_utils.ts index 57918222ea0..3ff0d93e1a9 100644 --- a/packages/core/src/render3/util/global_utils.ts +++ b/packages/core/src/render3/util/global_utils.ts @@ -14,6 +14,7 @@ import {isSignal} from '../reactivity/api'; import {applyChanges} from './change_detection_utils'; import {getDeferBlocks} from './defer'; import { + DirectiveDebugMetadata, getComponent, getContext, getDirectiveMetadata, @@ -125,6 +126,21 @@ export function publishGlobalUtil( publishUtil(name, fn); } +/** + * Defines the framework-agnostic `ng` global type, not just the `@angular/core` implementation. + * + * `typeof globalUtilsFunctions` is specifically the `@angular/core` implementation, so we + * overwrite some properties to make them more framework-agnostic. Longer term, we should define + * the `ng` global type as an interface implemented by `globalUtilsFunctions` rather than a type + * derived from it. + */ +export type FrameworkAgnosticGlobalUtils = Omit< + typeof globalUtilsFunctions, + 'getDirectiveMetadata' +> & { + getDirectiveMetadata(directiveOrComponentInstance: any): DirectiveDebugMetadata | null; +}; + /** * Publishes the given function to `window.ng` from package other than @angular/core * So that it can be used from the browser console when an application is not in production. diff --git a/packages/core/test/acceptance/discover_utils_spec.ts b/packages/core/test/acceptance/discover_utils_spec.ts index b4a72d2472f..03b2c7a040d 100644 --- a/packages/core/test/acceptance/discover_utils_spec.ts +++ b/packages/core/test/acceptance/discover_utils_spec.ts @@ -24,7 +24,7 @@ import {ComponentFixture, TestBed} from '../../testing'; import {getLContext} from '../../src/render3/context_discovery'; import {getHostElement} from '../../src/render3/index'; import { - ComponentDebugMetadata, + AngularComponentDebugMetadata, getComponent, getComponentLView, getContext, @@ -383,13 +383,11 @@ describe('discovery utils', () => { describe('getDirectiveMetadata', () => { it('should work with components', () => { - const metadata = getDirectiveMetadata(myApp); - expect(metadata!.inputs).toEqual({a: 'b'}); - expect(metadata!.outputs).toEqual({c: 'd'}); - expect((metadata as ComponentDebugMetadata).changeDetection).toBe( - ChangeDetectionStrategy.Default, - ); - expect((metadata as ComponentDebugMetadata).encapsulation).toBe(ViewEncapsulation.None); + const metadata = getDirectiveMetadata(myApp)! as AngularComponentDebugMetadata; + expect(metadata.inputs).toEqual({a: 'b'}); + expect(metadata.outputs).toEqual({c: 'd'}); + expect(metadata.changeDetection).toBe(ChangeDetectionStrategy.Default); + expect(metadata.encapsulation).toBe(ViewEncapsulation.None); }); it('should work with directives', () => {