Merge remote-tracking branch 'upstream/main' into prototype/signal-forms

This commit is contained in:
Miles Malerba
2025-08-26 15:14:08 -07:00
36 changed files with 269 additions and 77 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ git_override(
bazel_dep(name = "devinfra")
git_override(
module_name = "devinfra",
commit = "80db036355181684ed07021a175e9f039190d3d6",
commit = "00a4cf14cdc374867673a52ecb556f356da8bec0",
remote = "https://github.com/angular/dev-infra.git",
)
@@ -27,9 +27,12 @@ ts_project(
),
deps = [
":entities",
"//adev:node_modules/marked",
"//adev:node_modules/preact",
"//adev:node_modules/preact-render-to-string",
"//adev:node_modules/prettier",
"//adev:node_modules/shiki",
"//adev/shared-docs/pipeline/shared:linking",
"//adev/shared-docs/pipeline/shared:shiki",
"//adev/shared-docs/pipeline/shared/marked",
"//adev/shared-docs/pipeline/shared/regions",
@@ -21,6 +21,7 @@ ts_project(
"//adev:node_modules/@types/jsdom",
"//adev:node_modules/jsdom",
"//adev/shared-docs/pipeline/api-gen/rendering:render_api_to_html_lib",
"//adev/shared-docs/pipeline/shared:shiki",
],
)
@@ -76,5 +76,7 @@ ts_project(
deps = [
":guides_lib",
"//adev:node_modules/@types/node",
"//adev/shared-docs/pipeline/shared:shiki",
"//adev/shared-docs/pipeline/shared/marked",
],
)
@@ -18,6 +18,7 @@ ts_project(
"//adev:node_modules/marked",
"//adev:node_modules/mermaid",
"//adev:node_modules/playwright-core",
"//adev:node_modules/shiki",
"//adev/shared-docs/pipeline/shared:linking",
"//adev/shared-docs/pipeline/shared:shiki",
"//adev/shared-docs/pipeline/shared/regions",
@@ -4,5 +4,8 @@ ts_project(
name = "renderer_context",
srcs = ["renderer-context.mts"],
visibility = ["//adev/shared-docs/pipeline/shared/marked/test:__subpackages__"],
deps = ["//adev/shared-docs/pipeline/shared/marked"],
deps = [
"//adev/shared-docs/pipeline/shared:shiki",
"//adev/shared-docs/pipeline/shared/marked",
],
)
@@ -36,7 +36,7 @@ export function provideSettings(): (Provider | EnvironmentProviders)[] {
}
/**
* Migrrates the provided data to the latest data format, if needed.
* Migrates the provided data to the latest data format, if needed.
* Returns a new object with the migrated data.
*
* @param data Non-migrated data
@@ -67,7 +67,6 @@
[showCommentNodes]="showCommentNodes()"
[isHydrationEnabled]="isHydrationEnabled()"
(toggleInspector)="toggleInspector()"
[signalGraphEnabled]="signalGraphEnabled()"
/>
@let profilerVisible = activeTab() === 'Profiler';
@@ -43,7 +43,6 @@
@if (currentSelectedElement(); as currentSelectedElement) {
<ng-property-tab
[currentSelectedElement]="currentSelectedElement"
[signalGraphEnabled]="signalGraphEnabled()"
(showSignalGraph)="showSignalGraph($event)"
(inspect)="inspect($event)"
(viewSource)="viewSource($event)"
@@ -118,8 +118,6 @@ export class DirectiveExplorerComponent {
read: ElementRef,
});
readonly signalGraphEnabled = input.required<boolean>();
readonly currentSelectedElement = signal<IndexedNode | null>(null);
readonly forest = signal<DevToolsNode[]>([]);
readonly splitDirection = signal<'horizontal' | 'vertical'>('horizontal');
@@ -41,6 +41,7 @@ ng_project(
"//:node_modules/@angular/core",
"//:node_modules/@angular/material",
"//:node_modules/rxjs",
"//devtools/projects/ng-devtools/src/lib/application-services:settings",
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest",
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver",
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/defer-view",
@@ -6,13 +6,14 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ChangeDetectionStrategy, Component, input, output, signal} from '@angular/core';
import {ChangeDetectionStrategy, Component, input, output, signal, inject} from '@angular/core';
import {MatExpansionModule} from '@angular/material/expansion';
import {MatIcon} from '@angular/material/icon';
import {IndexedNode} from '../directive-forest/index-forest';
import {ComponentMetadataComponent} from './component-metadata.component';
import {ButtonComponent} from '../../../shared/button/button.component';
import {Settings} from '../../../application-services/settings';
@Component({
templateUrl: './property-tab-header.component.html',
@@ -22,9 +23,12 @@ import {ButtonComponent} from '../../../shared/button/button.component';
imports: [MatExpansionModule, MatIcon, ComponentMetadataComponent, ButtonComponent],
})
export class PropertyTabHeaderComponent {
private readonly settings = inject(Settings);
protected readonly currentSelectedElement = input.required<IndexedNode>();
protected readonly signalGraphEnabled = input.required<boolean>();
protected readonly showSignalGraph = output<void>();
protected readonly expanded = signal(false);
protected readonly signalGraphEnabled = this.settings.signalGraphEnabled;
}
@@ -3,7 +3,6 @@
@if (currentSelectedElement) {
<ng-property-tab-header
[currentSelectedElement]="currentSelectedElement"
[signalGraphEnabled]="signalGraphEnabled()"
(showSignalGraph)="showSignalGraph.emit(null)"
/>
<ng-property-tab-body
@@ -11,7 +10,6 @@
(viewSource)="viewSource.emit($event)"
(showSignalGraph)="showSignalGraph.emit($event)"
[currentSelectedElement]="currentSelectedElement"
[signalGraphEnabled]="signalGraphEnabled()"
/>
@let hydration = currentSelectedElement.hydration;
@@ -24,7 +24,6 @@ import {DeferViewComponent} from './defer-view/defer-view.component';
})
export class PropertyTabComponent {
readonly currentSelectedElement = input.required<IndexedNode | null>();
readonly signalGraphEnabled = input.required<boolean>();
readonly viewSource = output<string>();
readonly inspect = output<{node: FlatNode; directivePosition: DirectivePosition}>();
@@ -61,6 +61,7 @@ ng_project(
"//devtools/projects/ng-devtools/src/lib/application-environment",
"//devtools/projects/ng-devtools/src/lib/application-providers:supported_apis",
"//devtools/projects/ng-devtools/src/lib/application-services:frame_manager",
"//devtools/projects/ng-devtools/src/lib/application-services:settings",
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest",
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver",
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/resolution-path",
@@ -5,7 +5,6 @@
(viewSource)="viewSource.emit(directive.name)"
(showSignalGraph)="showSignalGraph.emit($event)"
[directive]="directive"
[signalGraphEnabled]="signalGraphEnabled()"
/>
</div>
}
@@ -22,7 +22,6 @@ import {PropertyViewComponent} from './property-view.component';
})
export class PropertyTabBodyComponent {
readonly currentSelectedElement = input.required<IndexedNode>();
readonly signalGraphEnabled = input.required<boolean>();
readonly inspect = output<{node: FlatNode; directivePosition: DirectivePosition}>();
readonly viewSource = output<string>();
@@ -24,7 +24,6 @@
<ng-property-view-tree
[dataSource]="panel.controls().dataSource"
[treeControl]="panel.controls().treeControl"
[signalGraphEnabled]="signalGraphEnabled()"
(updateValue)="updateValue($event)"
(inspect)="handleInspect($event)"
(showSignalGraph)="showSignalGraph.emit($event)"
@@ -53,7 +53,6 @@ export class PropertyViewBodyComponent {
readonly directivePropControls = input.required<DirectiveTreeData>();
readonly directiveOutputControls = input.required<DirectiveTreeData>();
readonly directiveStateControls = input.required<DirectiveTreeData>();
readonly signalGraphEnabled = input.required<boolean>();
readonly inspect = output<{node: FlatNode; directivePosition: DirectivePosition}>();
readonly showSignalGraph = output<DebugSignalGraphNode>();
@@ -20,6 +20,7 @@ import {MatTree, MatTreeNode, MatTreeNodeDef, MatTreeNodePadding} from '@angular
import {SUPPORTED_APIS} from '../../../../application-providers/supported_apis';
import {SignalGraphManager} from '../../signal-graph/signal-graph-manager';
import {DebugSignalGraphNode} from '../../../../../../../protocol';
import {Settings} from '../../../../application-services/settings';
@Component({
selector: 'ng-property-view-tree',
@@ -41,14 +42,16 @@ import {DebugSignalGraphNode} from '../../../../../../../protocol';
export class PropertyViewTreeComponent {
protected readonly supportedApis = inject(SUPPORTED_APIS);
private readonly signalGraph = inject(SignalGraphManager);
private readonly settings = inject(Settings);
readonly dataSource = input.required<PropertyDataSource>();
readonly treeControl = input.required<FlatTreeControl<FlatNode>>();
readonly signalGraphEnabled = input.required<boolean>();
readonly updateValue = output<any>();
readonly inspect = output<any>();
readonly showSignalGraph = output<DebugSignalGraphNode>();
protected readonly signalGraphEnabled = this.settings.signalGraphEnabled;
hasChild = (_: number, node: FlatNode): boolean => node.expandable;
toggle(node: FlatNode): void {
@@ -8,7 +8,6 @@
[directivePropControls]="directivePropControls()!"
[directiveOutputControls]="directiveOutputControls()!"
[directiveStateControls]="directiveStateControls()!"
[signalGraphEnabled]="signalGraphEnabled()"
(inspect)="inspect.emit($event)"
(showSignalGraph)="showSignalGraph.emit($event)"
></ng-property-view-body>
@@ -22,7 +22,6 @@ import {PropertyViewHeaderComponent} from './property-view-header.component';
})
export class PropertyViewComponent {
readonly directive = input.required<{name: string}>();
readonly signalGraphEnabled = input.required<boolean>();
readonly inspect = output<{node: FlatNode; directivePosition: DirectivePosition}>();
readonly viewSource = output<void>();
@@ -23,7 +23,7 @@
[class.drag-scrolling]="dragScrolling()"
>
<div
*cdkVirtualFor="let d of graphData(); let i = index"
*cdkVirtualFor="let d of graphData(); let i = index; trackBy: trackByIndex"
[ngStyle]="d.style"
class="frame-bar"
[class.selected]="selectedFrameIndexes().has(i)"
@@ -250,6 +250,10 @@ export class FrameSelectorComponent {
this._viewportScrollState.scrollLeft - dx * DRAG_SCROLL_SPEED;
}
trackByIndex(index: number): number {
return index;
}
private getBarStyles(frame: ProfilerFrame, multiplicationFactor: number): GraphNode {
const height = frame.duration * multiplicationFactor;
const colorPercentage = Math.max(10, Math.round((height / MAX_HEIGHT) * 100));
@@ -4,6 +4,23 @@
```ts
// @public (undocumented)
export const BASE_EFFECT_NODE: Omit<BaseEffectNode, 'fn' | 'destroy' | 'cleanup' | 'run'>;
// @public (undocumented)
export interface BaseEffectNode extends ReactiveNode {
// (undocumented)
cleanup(): void;
// (undocumented)
destroy(): void;
// (undocumented)
fn: () => void;
// (undocumented)
hasRun: boolean;
// (undocumented)
run(): void;
}
// @public (undocumented)
export type ComputationFn<S, D> = (source: S, previous?: {
source: S;
@@ -134,6 +151,9 @@ export interface ReactiveNode {
version: Version;
}
// @public (undocumented)
export function runEffect(node: BaseEffectNode): void;
// @public (undocumented)
export function runPostProducerCreatedFn(node: ReactiveNode): void;
@@ -32,6 +32,57 @@ export declare class MyComponent {
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* PARTIAL FILE: animate_enter_with_structural_directive.js
****************************************************************************************************/
import { Component, Directive } from '@angular/core';
import * as i0 from "@angular/core";
export class AnyStructuralDirective {
}
AnyStructuralDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: AnyStructuralDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
AnyStructuralDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: AnyStructuralDirective, isStandalone: true, selector: "[any-structural-directive]", ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: AnyStructuralDirective, decorators: [{
type: Directive,
args: [{
selector: '[any-structural-directive]',
standalone: true,
}]
}] });
export class MyComponent {
}
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: `
<div>
<p *any-structural-directive animate.enter="slide">Sliding Content</p>
</div>
`, isInline: true, dependencies: [{ kind: "directive", type: AnyStructuralDirective, selector: "[any-structural-directive]" }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
type: Component,
args: [{
selector: 'my-component',
imports: [AnyStructuralDirective],
standalone: true,
template: `
<div>
<p *any-structural-directive animate.enter="slide">Sliding Content</p>
</div>
`,
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: animate_enter_with_structural_directive.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class AnyStructuralDirective {
static ɵfac: i0.ɵɵFactoryDeclaration<AnyStructuralDirective, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<AnyStructuralDirective, "[any-structural-directive]", never, {}, {}, never, never, true, never>;
}
export declare class MyComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* PARTIAL FILE: animate_enter_with_string_host_bindings.js
****************************************************************************************************/
@@ -18,6 +18,23 @@
}
]
},
{
"description": "should generate animate enter instructions on element with a structural directive",
"inputFiles": [
"animate_enter_with_structural_directive.ts"
],
"expectations": [
{
"files": [
{
"expected": "animate_enter_with_structural_directive_template.js",
"generated": "animate_enter_with_structural_directive.js"
}
],
"failureMessage": "Incorrect ɵɵanimateEnter() call"
}
]
},
{
"description": "should generate animate enter instructions with host binding and simple string",
"inputFiles": [
@@ -0,0 +1,21 @@
import {Component, Directive} from '@angular/core';
@Directive({
selector: '[any-structural-directive]',
standalone: true,
})
export class AnyStructuralDirective {}
@Component({
selector: 'my-component',
imports: [AnyStructuralDirective],
standalone: true,
template: `
<div>
<p *any-structural-directive animate.enter="slide">Sliding Content</p>
</div>
`,
})
export class MyComponent {
}
@@ -0,0 +1,13 @@
import * as i0 from "@angular/core";
function MyComponent_p_1_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "p");
i0.ɵɵanimateEnter("slide");
i0.ɵɵtext(1, "Sliding Content");
i0.ɵɵelementEnd();
} }
…
MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 2, vars: 0, consts: [[4, "any-structural-directive"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div");
i0.ɵɵtemplate(1, MyComponent_p_1_Template, 2, 0, "p", 0);
i0.ɵɵelementEnd();
} }, dependencies: [AnyStructuralDirective], encapsulation: 2 });
@@ -961,6 +961,14 @@ class HtmlAstToIvyAst implements html.Visitor {
return directives;
}
private filterAnimationAttributes(attributes: t.TextAttribute[]): t.TextAttribute[] {
return attributes.filter((a) => !a.name.startsWith('animate.'));
}
private filterAnimationInputs(attributes: t.BoundAttribute[]): t.BoundAttribute[] {
return attributes.filter((a) => a.type !== BindingType.Animation);
}
private wrapInTemplate(
node: t.Element | t.Component | t.Content | t.Template,
templateProperties: ParsedProperty[],
@@ -986,8 +994,8 @@ class HtmlAstToIvyAst implements html.Visitor {
};
if (node instanceof t.Element || node instanceof t.Component) {
hoistedAttrs.attributes.push(...node.attributes);
hoistedAttrs.inputs.push(...node.inputs);
hoistedAttrs.attributes.push(...this.filterAnimationAttributes(node.attributes));
hoistedAttrs.inputs.push(...this.filterAnimationInputs(node.inputs));
hoistedAttrs.outputs.push(...node.outputs);
}
@@ -55,3 +55,4 @@ export {
export {Watch, WatchCleanupFn, WatchCleanupRegisterFn, createWatch} from './src/watch';
export {setAlternateWeakRefImpl} from './src/weak_ref';
export {untracked} from './src/untracked';
export {runEffect, BASE_EFFECT_NODE, BaseEffectNode} from './src/effect';
@@ -0,0 +1,60 @@
/**
* @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 {
consumerAfterComputation,
consumerBeforeComputation,
consumerPollProducersForChange,
REACTIVE_NODE,
ReactiveNode,
} from './graph';
/**
* An effect can, optionally, register a cleanup function. If registered, the cleanup is executed
* before the next effect run. The cleanup function makes it possible to "cancel" any work that the
* previous effect run might have started.
*/
export type EffectCleanupFn = () => void;
/**
* A callback passed to the effect function that makes it possible to register cleanup logic.
*/
export type EffectCleanupRegisterFn = (cleanupFn: EffectCleanupFn) => void;
export interface BaseEffectNode extends ReactiveNode {
hasRun: boolean;
fn: () => void;
destroy(): void;
cleanup(): void;
run(): void;
}
export const BASE_EFFECT_NODE: Omit<BaseEffectNode, 'fn' | 'destroy' | 'cleanup' | 'run'> =
/* @__PURE__ */ (() => ({
...REACTIVE_NODE,
consumerIsAlwaysLive: true,
consumerAllowSignalWrites: true,
dirty: true,
hasRun: false,
kind: 'effect',
}))();
export function runEffect(node: BaseEffectNode) {
node.dirty = false;
if (node.hasRun && !consumerPollProducersForChange(node)) {
return;
}
node.hasRun = true;
const prevNode = consumerBeforeComputation(node);
try {
node.cleanup();
node.fn();
} finally {
consumerAfterComputation(node, prevNode);
}
}
@@ -166,6 +166,9 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
const tNode = getCurrentTNode()!;
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
const renderer = lView[RENDERER];
const ngZone = lView[INJECTOR]!.get(NgZone);
@@ -270,6 +273,8 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa
const tNode = getCurrentTNode()!;
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
cancelLeavingNodes(tNode, lView);
value.call(lView[CONTEXT], {target: nativeElement, animationComplete: noOpAnimationComplete});
@@ -306,6 +311,8 @@ export function ɵɵanimateLeave(value: string | Function): typeof ɵɵanimateLe
const tNode = getCurrentTNode()!;
const nativeElement = getNativeByTNode(tNode, lView) as Element;
ngDevMode && assertElementNodes(nativeElement, 'animate.leave');
// This instruction is called in the update pass.
const renderer = lView[RENDERER];
const elementRegistry = getAnimationElementRemovalRegistry();
@@ -374,9 +381,7 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
const tView = getTView();
const nativeElement = getNativeByTNode(tNode, lView) as Element;
if ((nativeElement as Node).nodeType !== Node.ELEMENT_NODE) {
return ɵɵanimateLeaveListener;
}
ngDevMode && assertElementNodes(nativeElement, 'animate.leave');
const elementRegistry = getAnimationElementRemovalRegistry();
ngDevMode &&
@@ -524,6 +529,15 @@ function assertAnimationTypes(value: string | Function, instruction: string) {
}
}
function assertElementNodes(nativeElement: Element, instruction: string) {
if ((nativeElement as Node).nodeType !== Node.ELEMENT_NODE) {
throw new RuntimeError(
RuntimeErrorCode.ANIMATE_INVALID_VALUE,
`'${instruction}' can only be used on an element node, got ${stringify((nativeElement as Node).nodeType)}`,
);
}
}
/**
* This function actually adds the classes that animate element that's leaving the DOM.
* Once it finishes, it calls the remove function that was provided by the DOM renderer.
@@ -16,10 +16,8 @@ import {
SIGNAL_NODE,
type SignalNode,
} from '../../../primitives/signals';
import {type Signal} from '../reactivity/api';
import {type EffectCleanupFn, type EffectCleanupRegisterFn} from './effect';
import {type Signal} from '../reactivity/api';
import {TracingService, TracingSnapshot} from '../../application/tracing';
import {
ChangeDetectionScheduler,
+20 -41
View File
@@ -7,15 +7,13 @@
*/
import {
REACTIVE_NODE,
ReactiveNode,
SIGNAL,
consumerAfterComputation,
consumerBeforeComputation,
consumerDestroy,
consumerPollProducersForChange,
isInNotificationPhase,
setActiveConsumer,
BaseEffectNode,
BASE_EFFECT_NODE,
runEffect,
} from '../../../primitives/signals';
import {FLAGS, LViewFlags, LView, EFFECTS} from '../interfaces/view';
import {markAncestorsForTraversal} from '../util/view_utils';
@@ -191,17 +189,12 @@ export function effect(
return effectRef;
}
export interface EffectNode extends ReactiveNode, SchedulableEffect {
hasRun: boolean;
export interface EffectNode extends BaseEffectNode, SchedulableEffect {
cleanupFns: EffectCleanupFn[] | undefined;
injector: Injector;
notifier: ChangeDetectionScheduler;
onDestroyFn: () => void;
fn: (cleanupFn: EffectCleanupRegisterFn) => void;
run(): void;
destroy(): void;
maybeCleanup(): void;
}
export interface ViewEffectNode extends EffectNode {
@@ -212,47 +205,27 @@ export interface RootEffectNode extends EffectNode {
scheduler: EffectScheduler;
}
export const BASE_EFFECT_NODE: Omit<EffectNode, 'fn' | 'destroy' | 'injector' | 'notifier'> =
export const EFFECT_NODE: Omit<EffectNode, 'fn' | 'destroy' | 'injector' | 'notifier'> =
/* @__PURE__ */ (() => ({
...REACTIVE_NODE,
consumerIsAlwaysLive: true,
consumerAllowSignalWrites: true,
dirty: true,
hasRun: false,
...BASE_EFFECT_NODE,
cleanupFns: undefined,
zone: null,
kind: 'effect',
onDestroyFn: noop,
run(this: EffectNode): void {
this.dirty = false;
if (ngDevMode && isInNotificationPhase()) {
throw new Error(`Schedulers cannot synchronously execute watches while scheduling.`);
}
if (this.hasRun && !consumerPollProducersForChange(this)) {
return;
}
this.hasRun = true;
const registerCleanupFn: EffectCleanupRegisterFn = (cleanupFn) =>
(this.cleanupFns ??= []).push(cleanupFn);
const prevNode = consumerBeforeComputation(this);
// We clear `setIsRefreshingViews` so that `markForCheck()` within the body of an effect will
// cause CD to reach the component in question.
const prevRefreshingViews = setIsRefreshingViews(false);
try {
this.maybeCleanup();
this.fn(registerCleanupFn);
runEffect(this);
} finally {
setIsRefreshingViews(prevRefreshingViews);
consumerAfterComputation(this, prevNode);
}
},
maybeCleanup(this: EffectNode): void {
cleanup(this: EffectNode): void {
if (!this.cleanupFns?.length) {
return;
}
@@ -273,7 +246,7 @@ export const BASE_EFFECT_NODE: Omit<EffectNode, 'fn' | 'destroy' | 'injector' |
export const ROOT_EFFECT_NODE: Omit<RootEffectNode, 'fn' | 'scheduler' | 'notifier' | 'injector'> =
/* @__PURE__ */ (() => ({
...BASE_EFFECT_NODE,
...EFFECT_NODE,
consumerMarkedDirty(this: RootEffectNode) {
this.scheduler.schedule(this);
this.notifier.notify(NotificationSource.RootEffect);
@@ -281,14 +254,14 @@ export const ROOT_EFFECT_NODE: Omit<RootEffectNode, 'fn' | 'scheduler' | 'notifi
destroy(this: RootEffectNode) {
consumerDestroy(this);
this.onDestroyFn();
this.maybeCleanup();
this.cleanup();
this.scheduler.remove(this);
},
}))();
export const VIEW_EFFECT_NODE: Omit<ViewEffectNode, 'fn' | 'view' | 'injector' | 'notifier'> =
/* @__PURE__ */ (() => ({
...BASE_EFFECT_NODE,
...EFFECT_NODE,
consumerMarkedDirty(this: ViewEffectNode): void {
this.view[FLAGS] |= LViewFlags.HasChildViewsToRefresh;
markAncestorsForTraversal(this.view);
@@ -297,7 +270,7 @@ export const VIEW_EFFECT_NODE: Omit<ViewEffectNode, 'fn' | 'view' | 'injector' |
destroy(this: ViewEffectNode): void {
consumerDestroy(this);
this.onDestroyFn();
this.maybeCleanup();
this.cleanup();
this.view[EFFECTS]?.delete(this);
},
}))();
@@ -311,7 +284,7 @@ export function createViewEffect(
node.view = view;
node.zone = typeof Zone !== 'undefined' ? Zone.current : null;
node.notifier = notifier;
node.fn = fn;
node.fn = createEffectFn(node, fn);
view[EFFECTS] ??= new Set();
view[EFFECTS].add(node);
@@ -326,7 +299,7 @@ export function createRootEffect(
notifier: ChangeDetectionScheduler,
): RootEffectNode {
const node = Object.create(ROOT_EFFECT_NODE) as RootEffectNode;
node.fn = fn;
node.fn = createEffectFn(node, fn);
node.scheduler = scheduler;
node.notifier = notifier;
node.zone = typeof Zone !== 'undefined' ? Zone.current : null;
@@ -334,3 +307,9 @@ export function createRootEffect(
node.notifier.notify(NotificationSource.RootEffect);
return node;
}
function createEffectFn(node: EffectNode, fn: (onCleanup: EffectCleanupRegisterFn) => void) {
return () => {
fn((cleanupFn) => (node.cleanupFns ??= []).push(cleanupFn));
};
}
+8 -8
View File
@@ -4,11 +4,10 @@ load("@rules_angular//src/ts_project:index.bzl", _ts_project = "ts_project")
def ts_project(
name,
deps = [],
srcs = [],
tsconfig = None,
testonly = False,
visibility = None,
# TODO: Enable this for all `ts_project` targets at end of migration.
ignore_strict_deps = True,
rule_impl = _ts_project,
**kwargs):
rule_impl(
@@ -17,13 +16,14 @@ def ts_project(
declaration = True,
tsconfig = tsconfig,
visibility = visibility,
srcs = srcs,
deps = deps,
**kwargs
)
if not ignore_strict_deps:
strict_deps_test(
name = "%s_strict_deps_test" % name,
srcs = kwargs.get("srcs", []),
deps = deps,
)
strict_deps_test(
name = "%s_strict_deps_test" % name,
srcs = srcs,
tsconfig = tsconfig,
deps = deps,
)