diff --git a/packages/core/src/render3/debug/framework_injector_profiler.ts b/packages/core/src/render3/debug/framework_injector_profiler.ts index 9350865962fe..1db31e60a17c 100644 --- a/packages/core/src/render3/debug/framework_injector_profiler.ts +++ b/packages/core/src/render3/debug/framework_injector_profiler.ts @@ -15,6 +15,7 @@ import {getComponentDef} from '../def_getters'; import {getNodeInjectorLView, getNodeInjectorTNode, NodeInjector} from '../di'; import {TNode} from '../interfaces/node'; import {LView} from '../interfaces/view'; +import {EffectRef} from '../reactivity/effect'; import { InjectedService, @@ -67,6 +68,7 @@ class DIDebugData { WeakMap, InjectedService[]> >(); resolverToProviders = new WeakMap(); + resolverToEffects = new WeakMap(); standaloneInjectorToComponent = new WeakMap>(); reset() { @@ -113,9 +115,26 @@ function handleInjectorProfilerEvent(injectorProfilerEvent: InjectorProfilerEven handleInstanceCreatedByInjectorEvent(context, injectorProfilerEvent.instance); } else if (type === InjectorProfilerEventType.ProviderConfigured) { handleProviderConfiguredEvent(context, injectorProfilerEvent.providerRecord); + } else if (type === InjectorProfilerEventType.EffectCreated) { + handleEffectCreatedEvent(context, injectorProfilerEvent.effect); } } +function handleEffectCreatedEvent(context: InjectorProfilerContext, effect: EffectRef): void { + const diResolver = getDIResolver(context.injector); + if (diResolver === null) { + throwError('An EffectCreated event must be run within an injection context.'); + } + + const {resolverToEffects} = frameworkDIDebugData; + + if (!resolverToEffects.has(diResolver)) { + resolverToEffects.set(diResolver, []); + } + + resolverToEffects.get(diResolver)!.push(effect); +} + /** * * Stores the injected service in frameworkDIDebugData.resolverToTokenToDependencies diff --git a/packages/core/src/render3/debug/injector_profiler.ts b/packages/core/src/render3/debug/injector_profiler.ts index 6bb3c0037230..de3ad1b19292 100644 --- a/packages/core/src/render3/debug/injector_profiler.ts +++ b/packages/core/src/render3/debug/injector_profiler.ts @@ -16,6 +16,7 @@ import {Type} from '../../interface/type'; import {throwError} from '../../util/assert'; import type {TNode} from '../interfaces/node'; import type {LView} from '../interfaces/view'; +import type {EffectRef} from '../reactivity/effect'; /** * An enum describing the types of events that can be emitted from the injector profiler @@ -35,6 +36,11 @@ export const enum InjectorProfilerEventType { * Emits when an injector configures a provider. */ ProviderConfigured, + + /** + * Emits when an effect is created. + */ + EffectCreated, } /** @@ -74,6 +80,12 @@ export interface ProviderConfiguredEvent { providerRecord: ProviderRecord; } +export interface EffectCreatedEvent { + type: InjectorProfilerEventType.EffectCreated; + context: InjectorProfilerContext; + effect: EffectRef; +} + /** * An object representing an event that is emitted through the injector profiler */ @@ -81,7 +93,8 @@ export interface ProviderConfiguredEvent { export type InjectorProfilerEvent = | InjectedServiceEvent | InjectorCreatedInstanceEvent - | ProviderConfiguredEvent; + | ProviderConfiguredEvent + | EffectCreatedEvent; /** * An object that contains information about a provider that has been configured @@ -272,6 +285,16 @@ export function emitInjectEvent(token: Type, value: unknown, flags: Inj }); } +export function emitEffectCreatedEvent(effect: EffectRef): void { + !ngDevMode && throwError('Injector profiler should never be called in production mode'); + + injectorProfiler({ + type: InjectorProfilerEventType.EffectCreated, + context: getInjectorProfilerContext(), + effect, + }); +} + export function runInInjectorProfilerContext( injector: Injector, token: Type, diff --git a/packages/core/src/render3/reactive_lview_consumer.ts b/packages/core/src/render3/reactive_lview_consumer.ts index a58df64adf20..bd85a8a8da09 100644 --- a/packages/core/src/render3/reactive_lview_consumer.ts +++ b/packages/core/src/render3/reactive_lview_consumer.ts @@ -48,7 +48,7 @@ export function maybeReturnReactiveLViewConsumer(consumer: ReactiveLViewConsumer freeConsumers.push(consumer); } -const REACTIVE_LVIEW_CONSUMER_NODE: Omit = { +export const REACTIVE_LVIEW_CONSUMER_NODE: Omit = { ...REACTIVE_NODE, consumerIsAlwaysLive: true, kind: 'template', @@ -78,7 +78,7 @@ export function getOrCreateTemporaryConsumer(lView: LView): ReactiveLViewConsume return consumer; } -const TEMPORARY_CONSUMER_NODE = { +export const TEMPORARY_CONSUMER_NODE = { ...REACTIVE_NODE, consumerIsAlwaysLive: true, kind: 'template', diff --git a/packages/core/src/render3/reactivity/effect.ts b/packages/core/src/render3/reactivity/effect.ts index a427fbc2a499..6317b9fd3e45 100644 --- a/packages/core/src/render3/reactivity/effect.ts +++ b/packages/core/src/render3/reactivity/effect.ts @@ -27,7 +27,6 @@ import {assertInInjectionContext} from '../../di/contextual'; import {DestroyRef, NodeInjectorDestroyRef} from '../../linker/destroy_ref'; import {ViewContext} from '../view_context'; import {noop} from '../../util/noop'; -import {ErrorHandler} from '../../error_handler'; import { ChangeDetectionScheduler, NotificationSource, @@ -38,6 +37,7 @@ import {USE_MICROTASK_EFFECT_BY_DEFAULT} from './patch'; import {microtaskEffect} from './microtask_effect'; let useMicrotaskEffectsByDefault = USE_MICROTASK_EFFECT_BY_DEFAULT; +import {emitEffectCreatedEvent, setInjectorProfilerContext} from '../debug/injector_profiler'; /** * Toggle the flag on whether to use microtask effects (for testing). @@ -60,7 +60,7 @@ export interface EffectRef { destroy(): void; } -class EffectRefImpl implements EffectRef { +export class EffectRefImpl implements EffectRef { [SIGNAL]: EffectNode; constructor(node: EffectNode) { @@ -199,11 +199,19 @@ export function effect( node.onDestroyFn = destroyRef.onDestroy(() => node.destroy()); } + const effectRef = new EffectRefImpl(node); + if (ngDevMode) { node.debugName = options?.debugName ?? ''; + const prevInjectorProfilerContext = setInjectorProfilerContext({injector, token: null}); + try { + emitEffectCreatedEvent(effectRef); + } finally { + setInjectorProfilerContext(prevInjectorProfilerContext); + } } - return new EffectRefImpl(node); + return effectRef; } export interface EffectNode extends ReactiveNode, SchedulableEffect { diff --git a/packages/core/src/render3/util/global_utils.ts b/packages/core/src/render3/util/global_utils.ts index 9a4501812c8f..d56fed777b47 100644 --- a/packages/core/src/render3/util/global_utils.ts +++ b/packages/core/src/render3/util/global_utils.ts @@ -29,6 +29,7 @@ import { getInjectorProviders, getInjectorResolutionPath, } from './injector_discovery_utils'; +import {getSignalGraph} from './signal_debug'; /** * This file introduces series of globally accessible debug tools @@ -65,6 +66,7 @@ const globalUtilsFunctions = { 'ɵgetInjectorResolutionPath': getInjectorResolutionPath, 'ɵgetInjectorMetadata': getInjectorMetadata, 'ɵsetProfiler': setProfiler, + 'ɵgetSignalGraph': getSignalGraph, 'getDirectiveMetadata': getDirectiveMetadata, 'getComponent': getComponent, diff --git a/packages/core/src/render3/util/signal_debug.ts b/packages/core/src/render3/util/signal_debug.ts new file mode 100644 index 000000000000..a60aae7bb558 --- /dev/null +++ b/packages/core/src/render3/util/signal_debug.ts @@ -0,0 +1,194 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { + REACTIVE_LVIEW_CONSUMER_NODE, + ReactiveLViewConsumer, + TEMPORARY_CONSUMER_NODE, +} from '../reactive_lview_consumer'; +import {assertTNode, assertLView} from '../assert'; +import {getFrameworkDIDebugData} from '../debug/framework_injector_profiler'; +import {NodeInjector, getNodeInjectorTNode, getNodeInjectorLView} from '../di'; +import {REACTIVE_TEMPLATE_CONSUMER, HOST, LView} from '../interfaces/view'; +import {EffectNode, EffectRefImpl, ROOT_EFFECT_NODE, VIEW_EFFECT_NODE} from '../reactivity/effect'; +import {Injector} from '../../di/injector'; +import {R3Injector} from '../../di/r3_injector'; +import {throwError} from '../../util/assert'; +import { + ComputedNode, + ReactiveNode, + SIGNAL, + SIGNAL_NODE, + SignalNode, +} from '@angular/core/primitives/signals'; + +export interface DebugSignalGraphNode { + kind: string; + label?: string; + value?: unknown; +} + +export interface DebugSignalGraphEdge { + /** + * Index of a signal node in the `nodes` array that is a consumer of the signal produced by the producer node. + */ + consumer: number; + + /** + * Index of a signal node in the `nodes` array that is a producer of the signal consumed by the consumer node. + */ + producer: number; +} + +/** + * A debug representation of the signal graph. + */ +export interface DebugSignalGraph { + nodes: DebugSignalGraphNode[]; + edges: DebugSignalGraphEdge[]; +} + +function isComputedNode(node: ReactiveNode): node is ComputedNode { + return node.kind === 'computed'; +} + +function isTemplateEffectNode(node: ReactiveNode): node is ReactiveLViewConsumer { + return node.kind === 'template'; +} + +function isEffectNode(node: ReactiveNode): node is EffectNode { + return node.kind === 'effect'; +} + +function isSignalNode(node: ReactiveNode): node is SignalNode { + return node.kind === 'signal'; +} + +/** + * + * @param injector + * @returns Template consumer of given NodeInjector + */ +function getTemplateConsumer(injector: NodeInjector): ReactiveLViewConsumer | null { + const tNode = getNodeInjectorTNode(injector)!; + assertTNode(tNode); + const lView = getNodeInjectorLView(injector)!; + assertLView(lView); + const templateLView = lView[tNode.index]!; + assertLView(templateLView); + + return templateLView[REACTIVE_TEMPLATE_CONSUMER]; +} + +function getNodesAndEdgesFromSignalMap(signalMap: ReadonlyMap): { + nodes: DebugSignalGraphNode[]; + edges: DebugSignalGraphEdge[]; +} { + const nodes = Array.from(signalMap.keys()); + const debugSignalGraphNodes: DebugSignalGraphNode[] = []; + const edges: DebugSignalGraphEdge[] = []; + + for (const [consumer, producers] of signalMap.entries()) { + const consumerIndex = nodes.indexOf(consumer); + + // collect node + if (isComputedNode(consumer) || isSignalNode(consumer)) { + debugSignalGraphNodes.push({ + label: consumer.debugName, + value: consumer.value, + kind: consumer.kind, + }); + } else if (isTemplateEffectNode(consumer)) { + debugSignalGraphNodes.push({ + label: consumer.debugName ?? consumer.lView?.[HOST]?.tagName?.toLowerCase?.(), + kind: consumer.kind, + }); + } else if (isEffectNode(consumer)) { + debugSignalGraphNodes.push({ + label: consumer.debugName, + kind: consumer.kind, + }); + } else { + debugSignalGraphNodes.push({ + label: consumer.debugName, + kind: consumer.kind, + }); + } + + // collect edges for node + for (const producer of producers) { + edges.push({consumer: consumerIndex, producer: nodes.indexOf(producer)}); + } + } + + return {nodes: debugSignalGraphNodes, edges}; +} + +function extractEffectsFromInjector(injector: Injector): ReactiveNode[] { + let diResolver: Injector | LView = injector; + if (injector instanceof NodeInjector) { + const lView = getNodeInjectorLView(injector)!; + diResolver = lView; + } + + const resolverToEffects = getFrameworkDIDebugData().resolverToEffects as Map< + Injector | LView, + EffectRefImpl[] + >; + const effects = resolverToEffects.get(diResolver) ?? []; + + return effects.map((effect: EffectRefImpl) => effect[SIGNAL]); +} + +function extractSignalNodesAndEdgesFromRoots( + nodes: ReactiveNode[], + signalDependenciesMap: Map = new Map(), +): Map { + for (const node of nodes) { + if (signalDependenciesMap.has(node)) { + continue; + } + + const producerNodes = (node.producerNode ?? []) as ReactiveNode[]; + signalDependenciesMap.set(node, producerNodes); + extractSignalNodesAndEdgesFromRoots(producerNodes, signalDependenciesMap); + } + + return signalDependenciesMap; +} + +/** + * Returns a debug representation of the signal graph for the given injector. + * + * Currently only supports element injectors. Starts by discovering the consumer nodes + * and then traverses their producer nodes to build the signal graph. + * + * @param injector The injector to get the signal graph for. + * @returns A debug representation of the signal graph. + * @throws If the injector is an environment injector. + */ +export function getSignalGraph(injector: Injector): DebugSignalGraph { + let templateConsumer: ReactiveLViewConsumer | null = null; + + if (!(injector instanceof NodeInjector) && !(injector instanceof R3Injector)) { + return throwError('getSignalGraph must be called with a NodeInjector or R3Injector'); + } + + if (injector instanceof NodeInjector) { + templateConsumer = getTemplateConsumer(injector as NodeInjector); + } + + const nonTemplateEffectNodes = extractEffectsFromInjector(injector); + + const signalNodes = templateConsumer + ? [templateConsumer, ...nonTemplateEffectNodes] + : nonTemplateEffectNodes; + + const signalDependenciesMap = extractSignalNodesAndEdgesFromRoots(signalNodes); + + return getNodesAndEdgesFromSignalMap(signalDependenciesMap); +} diff --git a/packages/core/test/acceptance/signal_debug_spec.ts b/packages/core/test/acceptance/signal_debug_spec.ts new file mode 100644 index 000000000000..0ccecebd8c87 --- /dev/null +++ b/packages/core/test/acceptance/signal_debug_spec.ts @@ -0,0 +1,302 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {Component, signal, effect, computed, Injectable, inject} from '@angular/core/public_api'; +import { + setupFrameworkInjectorProfiler, + getFrameworkDIDebugData, +} from '@angular/core/src/render3/debug/framework_injector_profiler'; +import { + DebugSignalGraphEdge, + DebugSignalGraphNode, + getSignalGraph, +} from '@angular/core/src/render3/util/signal_debug'; +import {TestBed, fakeAsync, tick} from '@angular/core/testing'; + +describe('getSignalGraph', () => { + beforeEach(() => { + // Effect detection depends on the framework injector profiler being enabled + setupFrameworkInjectorProfiler(); + }); + + afterEach(() => { + getFrameworkDIDebugData().reset(); + TestBed.resetTestingModule(); + }); + + /** + * + * DebugSignalGraphEdge has integer fields representing indexes in the nodes array. + * This function maps those indexes to the actual nodes and returns an array of edges. + * + */ + function mapEdgeIndicesIntoNodes( + edges: DebugSignalGraphEdge[], + nodes: DebugSignalGraphNode[], + ): {consumer: DebugSignalGraphNode; producer: DebugSignalGraphNode}[] { + return edges.map(({consumer, producer}) => ({ + consumer: nodes[consumer], + producer: nodes[producer], + })); + } + + it('should return the signal graph for a component with signals', fakeAsync(() => { + @Component({selector: 'component-with-signals', template: `{{ primitiveSignal() }}`}) + class WithSignals { + primitiveSignal = signal(123, {debugName: 'primitiveSignal'}); + } + TestBed.configureTestingModule({imports: [WithSignals]}); + const fixture = TestBed.createComponent(WithSignals); + + tick(); + fixture.detectChanges(); + const injector = fixture.componentRef.injector; + const signalGraph = getSignalGraph(injector); + + const {nodes, edges} = signalGraph; + + // 2 nodes + // template + // primitiveSignal + expect(nodes.length).toBe(2); + + // 1 edge + // template depends on primitiveSignal + expect(edges.length).toBe(1); + + const signalNode = nodes.find((node) => node.kind === 'signal')!; + expect(signalNode).toBeDefined(); + expect(signalNode.label).toBe('primitiveSignal'); + expect(signalNode.value).toBe(123); + })); + + it('should return the signal graph for a component with effects', fakeAsync(() => { + @Component({selector: 'component-with-effect', template: ``}) + class WithEffect { + stateFromEffect = 0; + primitiveSignal = signal(123, {debugName: 'primitiveSignal'}); + primitiveSignal2 = signal(456, {debugName: 'primitiveSignal2'}); + + constructor() { + effect( + () => { + this.stateFromEffect = this.primitiveSignal() * this.primitiveSignal2(); + }, + {debugName: 'primitiveSignalEffect'}, + ); + } + } + TestBed.configureTestingModule({imports: [WithEffect]}).compileComponents(); + const fixture = TestBed.createComponent(WithEffect); + + tick(); + fixture.detectChanges(); + + const injector = fixture.componentRef.injector; + const signalGraph = getSignalGraph(injector); + + const {nodes, edges} = signalGraph; + + expect(nodes.length).toBe(3); + + const effectNode = nodes.find((node) => node.label === 'primitiveSignalEffect')!; + expect(effectNode).toBeDefined(); + + const signalNode = nodes.find((node) => node.label === 'primitiveSignal')!; + expect(signalNode).toBeDefined(); + + const signalNode2 = nodes.find((node) => node.label === 'primitiveSignal2')!; + expect(signalNode2).toBeDefined(); + + expect(edges.length).toBe(2); + const edgesWithNodes = mapEdgeIndicesIntoNodes(edges, nodes); + + expect(edgesWithNodes).toContain({consumer: effectNode, producer: signalNode}); + })); + + it('should return the signal graph for a component with a computed', fakeAsync(() => { + @Component({selector: 'component-with-computed', template: `{{ computedSignal() }}`}) + class WithComputed { + primitiveSignal = signal(123, {debugName: 'primitiveSignal'}); + primitiveSignal2 = signal(456, {debugName: 'primitiveSignal2'}); + computedSignal = computed(() => this.primitiveSignal() * this.primitiveSignal2(), { + debugName: 'computedSignal', + }); + } + TestBed.configureTestingModule({imports: [WithComputed]}).compileComponents(); + const fixture = TestBed.createComponent(WithComputed); + + tick(); + fixture.detectChanges(); + + const injector = fixture.componentRef.injector; + const signalGraph = getSignalGraph(injector); + + const {nodes, edges} = signalGraph; + + // 4 nodes + // template + // primitiveSignal + // primitiveSignal2 + // computedSignal + expect(nodes.length).toBe(4); + + const templateNode = nodes.find((node) => node.kind === 'template')!; + expect(templateNode).toBeDefined(); + + const primitiveSignalNode = nodes.find((node) => node.label === 'primitiveSignal')!; + expect(primitiveSignalNode).toBeDefined(); + expect(primitiveSignalNode.value).toBe(123); + + const primitiveSignal2Node = nodes.find((node) => node.label === 'primitiveSignal2')!; + expect(primitiveSignal2Node).toBeDefined(); + expect(primitiveSignal2Node.value).toBe(456); + + const computedSignalNode = nodes.find((node) => node.label === 'computedSignal')!; + expect(computedSignalNode).toBeDefined(); + expect(computedSignalNode.label).toBe('computedSignal'); + expect(computedSignalNode.value).toBe(123 * 456); + + // 3 edges + // computedSignal depends on primitiveSignal + // computedSignal depends on primitiveSignal2 + // template depends on computedSignal + expect(edges.length).toBe(3); + + const edgesWithNodes = mapEdgeIndicesIntoNodes(edges, nodes); + + expect(edgesWithNodes).toContain({consumer: templateNode, producer: computedSignalNode}); + expect(edgesWithNodes).toContain({ + consumer: computedSignalNode, + producer: primitiveSignalNode, + }); + expect(edgesWithNodes).toContain({ + consumer: computedSignalNode, + producer: primitiveSignal2Node, + }); + })); + + it('should return the signal graph for a component with unused reactive nodes', fakeAsync(() => { + @Component({selector: 'component-with-unused-signal', template: ``}) + class WithUnusedReactiveNodes { + primitiveSignal = signal(123, {debugName: 'primitiveSignal'}); + computedSignal = computed(() => this.primitiveSignal() * this.primitiveSignal(), { + debugName: 'computedSignal', + }); + } + TestBed.configureTestingModule({imports: [WithUnusedReactiveNodes]}).compileComponents(); + const fixture = TestBed.createComponent(WithUnusedReactiveNodes); + + tick(); + fixture.detectChanges(); + + const injector = fixture.componentRef.injector; + const signalGraph = getSignalGraph(injector); + + const {nodes, edges} = signalGraph; + expect(nodes.length).toBe(0); + expect(edges.length).toBe(0); + })); + + it('should return the signal graph for a component with no component effect signal dependencies', fakeAsync(() => { + @Component({selector: 'component-with-zero-effect', template: ``}) + class WithNoEffectSignalDependencies { + primitiveSignal = signal(123, {debugName: 'primitiveSignal'}); + primitiveSignalEffect = effect(() => {}, {debugName: 'primitiveSignalEffect'}); + } + TestBed.configureTestingModule({imports: [WithNoEffectSignalDependencies]}).compileComponents(); + const fixture = TestBed.createComponent(WithNoEffectSignalDependencies); + + tick(); + fixture.detectChanges(); + + const injector = fixture.componentRef.injector; + const signalGraph = getSignalGraph(injector); + + const {nodes, edges} = signalGraph; + expect(nodes.length).toBe(1); // 1 effect node detected + expect(edges.length).toBe(0); + })); + + it('should return the signal graph for a component with no signal dependencies in the template or component effects', fakeAsync(() => { + @Component({selector: 'component-with-no-effect-dependencies', template: ``}) + class WithNoEffectDependencies {} + TestBed.configureTestingModule({imports: [WithNoEffectDependencies]}).compileComponents(); + const fixture = TestBed.createComponent(WithNoEffectDependencies); + + tick(); + fixture.detectChanges(); + + const injector = fixture.componentRef.injector; + const signalGraph = getSignalGraph(injector); + + const {nodes, edges} = signalGraph; + expect(nodes.length).toBe(0); + expect(edges.length).toBe(0); + })); + + it('should capture signals created in external services in the signal graph', fakeAsync(() => { + @Injectable() + class ExternalService { + oneTwoThree = signal(123, {debugName: 'oneTwoThree'}); + fourFiveSix = signal(456, {debugName: 'fourFiveSix'}); + } + + @Component({ + providers: [ExternalService], + selector: 'component-with-external-service', + template: `{{externalService.oneTwoThree()}}`, + }) + class WithExternalService { + externalService = inject(ExternalService); + + constructor() { + effect( + () => { + this.externalService.fourFiveSix(); + }, + {debugName: 'externalServiceEffect'}, + ); + } + } + TestBed.configureTestingModule({imports: [WithExternalService]}).compileComponents(); + const fixture = TestBed.createComponent(WithExternalService); + + tick(); + fixture.detectChanges(); + + const injector = fixture.componentRef.injector; + const signalGraph = getSignalGraph(injector); + + const {nodes, edges} = signalGraph; + expect(nodes.length).toBe(4); + + const templateNode = nodes.find((node) => node.kind === 'template')!; + expect(templateNode).toBeDefined(); + + const externalServiceEffectNode = nodes.find((node) => node.label === 'externalServiceEffect')!; + expect(externalServiceEffectNode).toBeDefined(); + expect(externalServiceEffectNode.kind).toBe('effect'); + + const oneTwoThreeNode = nodes.find((node) => node.label === 'oneTwoThree')!; + expect(oneTwoThreeNode).toBeDefined(); + expect(oneTwoThreeNode.value).toBe(123); + + const fourFiveSixNode = nodes.find((node) => node.label === 'fourFiveSix')!; + expect(fourFiveSixNode).toBeDefined(); + expect(fourFiveSixNode.value).toBe(456); + + expect(edges.length).toBe(2); + const edgesWithNodes = mapEdgeIndicesIntoNodes(edges, nodes); + expect(edgesWithNodes).toContain({consumer: templateNode, producer: oneTwoThreeNode}); + expect(edgesWithNodes).toContain({ + consumer: externalServiceEffectNode, + producer: fourFiveSixNode, + }); + })); +}); diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json index 8d0ad0bffdc2..cd0c1ea0c4ca 100644 --- a/packages/core/test/bundling/defer/bundle.golden_symbols.json +++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json @@ -2033,6 +2033,9 @@ { "name": "init_signal2" }, + { + "name": "init_signal_debug" + }, { "name": "init_signals" },