refactor: migrate core to prettier formatting (#55488)

Migrate formatting to prettier for core from clang-format

PR Close #55488
This commit is contained in:
Joey Perrott 2024-04-23 16:04:28 +00:00 committed by Andrew Kushnir
parent be17de53d4
commit 31fdf0fbea
581 changed files with 52758 additions and 41450 deletions

View file

@ -18,7 +18,7 @@ export const format: FormatConfig = {
'packages/benchpress/**/*.{js,ts}',
'packages/common/**/*.{js,ts}',
'packages/compiler/**/*.{js,ts}',
'packages/core/primitives/**/*.{js,ts}',
'packages/core/**/*.{js,ts}',
'packages/docs/**/*.{js,ts}',
'packages/elements/**/*.{js,ts}',
'packages/examples/**/*.{js,ts}',
@ -40,6 +40,11 @@ export const format: FormatConfig = {
// not be modified.
'!third_party/**',
'!.yarn/**',
// Do not format the locale files which are checked-in for Google3, but generated using
// the `generate-locales-tool` from `packages/common/locales`.
'!packages/core/src/i18n/locale_en.ts',
'!packages/common/locales/closure-locale.ts',
'!packages/common/src/i18n/currencies.ts',
],
},
'clang-format': {
@ -77,7 +82,7 @@ export const format: FormatConfig = {
'!packages/benchpress/**/*.{js,ts}',
'!packages/common/**/*.{js,ts}',
'!packages/compiler/**/*.{js,ts}',
'!packages/core/primitives/**/*.{js,ts}',
'!packages/core/**/*.{js,ts}',
'!packages/docs/**/*.{js,ts}',
'!packages/elements/**/*.{js,ts}',
'!packages/examples/**/*.{js,ts}',

View file

@ -1205,7 +1205,7 @@ export class NgProbeToken {
// @public
export class NgZone {
constructor({ enableLongStackTrace, shouldCoalesceEventChangeDetection, shouldCoalesceRunChangeDetection }: {
constructor({ enableLongStackTrace, shouldCoalesceEventChangeDetection, shouldCoalesceRunChangeDetection, }: {
enableLongStackTrace?: boolean | undefined;
shouldCoalesceEventChangeDetection?: boolean | undefined;
shouldCoalesceRunChangeDetection?: boolean | undefined;

View file

@ -6,7 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertInInjectionContext, DestroyRef, inject, OutputOptions, OutputRef, OutputRefSubscription, ɵRuntimeError, ɵRuntimeErrorCode} from '@angular/core';
import {
assertInInjectionContext,
DestroyRef,
inject,
OutputOptions,
OutputRef,
OutputRefSubscription,
ɵRuntimeError,
ɵRuntimeErrorCode,
} from '@angular/core';
import {Observable} from 'rxjs';
import {takeUntilDestroyed} from './take_until_destroyed';
@ -31,15 +40,16 @@ class OutputFromObservableRef<T> implements OutputRef<T> {
subscribe(callbackFn: (value: T) => void): OutputRefSubscription {
if (this.destroyed) {
throw new ɵRuntimeError(
ɵRuntimeErrorCode.OUTPUT_REF_DESTROYED,
ngDevMode &&
'Unexpected subscription to destroyed `OutputRef`. ' +
'The owning directive/component is destroyed.');
ɵRuntimeErrorCode.OUTPUT_REF_DESTROYED,
ngDevMode &&
'Unexpected subscription to destroyed `OutputRef`. ' +
'The owning directive/component is destroyed.',
);
}
// Stop yielding more values when the directive/component is already destroyed.
const subscription = this.source.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
next: value => callbackFn(value),
next: (value) => callbackFn(value),
});
return {
@ -73,7 +83,9 @@ class OutputFromObservableRef<T> implements OutputRef<T> {
* @developerPreview
*/
export function outputFromObservable<T>(
observable: Observable<T>, opts?: OutputOptions): OutputRef<T> {
observable: Observable<T>,
opts?: OutputOptions,
): OutputRef<T> {
ngDevMode && assertInInjectionContext(outputFromObservable);
return new OutputFromObservableRef<T>(observable);
}

View file

@ -20,13 +20,13 @@ import {Observable} from 'rxjs';
export function outputToObservable<T>(ref: OutputRef<T>): Observable<T> {
const destroyRef = ɵgetOutputDestroyRef(ref);
return new Observable<T>(observer => {
return new Observable<T>((observer) => {
// Complete the observable upon directive/component destroy.
// Note: May be `undefined` if an `EventEmitter` is declared outside
// of an injection context.
destroyRef?.onDestroy(() => observer.complete());
const subscription = ref.subscribe(v => observer.next(v));
const subscription = ref.subscribe((v) => observer.next(v));
return () => subscription.unsubscribe();
});
}

View file

@ -26,7 +26,7 @@ export function takeUntilDestroyed<T>(destroyRef?: DestroyRef): MonoTypeOperator
destroyRef = inject(DestroyRef);
}
const destroyed$ = new Observable<void>(observer => {
const destroyed$ = new Observable<void>((observer) => {
const unregisterFn = destroyRef!.onDestroy(observer.next.bind(observer));
return unregisterFn;
});

View file

@ -6,7 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertInInjectionContext, DestroyRef, effect, inject, Injector, Signal, untracked} from '@angular/core';
import {
assertInInjectionContext,
DestroyRef,
effect,
inject,
Injector,
Signal,
untracked,
} from '@angular/core';
import {Observable, ReplaySubject} from 'rxjs';
/**
@ -33,24 +41,24 @@ export interface ToObservableOptions {
*
* @developerPreview
*/
export function toObservable<T>(
source: Signal<T>,
options?: ToObservableOptions,
): Observable<T> {
export function toObservable<T>(source: Signal<T>, options?: ToObservableOptions): Observable<T> {
!options?.injector && assertInInjectionContext(toObservable);
const injector = options?.injector ?? inject(Injector);
const subject = new ReplaySubject<T>(1);
const watcher = effect(() => {
let value: T;
try {
value = source();
} catch (err) {
untracked(() => subject.error(err));
return;
}
untracked(() => subject.next(value));
}, {injector, manualCleanup: true});
const watcher = effect(
() => {
let value: T;
try {
value = source();
} catch (err) {
untracked(() => subject.error(err));
return;
}
untracked(() => subject.next(value));
},
{injector, manualCleanup: true},
);
injector.get(DestroyRef).onDestroy(() => {
watcher.destroy();

View file

@ -6,7 +6,19 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertInInjectionContext, assertNotInReactiveContext, computed, DestroyRef, inject, Injector, signal, Signal, WritableSignal, ɵRuntimeError, ɵRuntimeErrorCode} from '@angular/core';
import {
assertInInjectionContext,
assertNotInReactiveContext,
computed,
DestroyRef,
inject,
Injector,
signal,
Signal,
WritableSignal,
ɵRuntimeError,
ɵRuntimeErrorCode,
} from '@angular/core';
import {Observable, Subscribable} from 'rxjs';
/**
@ -61,23 +73,27 @@ export interface ToSignalOptions {
}
// Base case: no options -> `undefined` in the result type.
export function toSignal<T>(source: Observable<T>|Subscribable<T>): Signal<T|undefined>;
export function toSignal<T>(source: Observable<T> | Subscribable<T>): Signal<T | undefined>;
// Options with `undefined` initial value and no `requiredSync` -> `undefined`.
export function toSignal<T>(
source: Observable<T>|Subscribable<T>,
options: ToSignalOptions&{initialValue?: undefined, requireSync?: false}): Signal<T|undefined>;
source: Observable<T> | Subscribable<T>,
options: ToSignalOptions & {initialValue?: undefined; requireSync?: false},
): Signal<T | undefined>;
// Options with `null` initial value -> `null`.
export function toSignal<T>(
source: Observable<T>|Subscribable<T>,
options: ToSignalOptions&{initialValue?: null, requireSync?: false}): Signal<T|null>;
source: Observable<T> | Subscribable<T>,
options: ToSignalOptions & {initialValue?: null; requireSync?: false},
): Signal<T | null>;
// Options with `undefined` initial value and `requiredSync` -> strict result type.
export function toSignal<T>(
source: Observable<T>|Subscribable<T>,
options: ToSignalOptions&{initialValue?: undefined, requireSync: true}): Signal<T>;
source: Observable<T> | Subscribable<T>,
options: ToSignalOptions & {initialValue?: undefined; requireSync: true},
): Signal<T>;
// Options with a more specific initial value type.
export function toSignal<T, const U extends T>(
source: Observable<T>|Subscribable<T>,
options: ToSignalOptions&{initialValue: U, requireSync?: false}): Signal<T|U>;
source: Observable<T> | Subscribable<T>,
options: ToSignalOptions & {initialValue: U; requireSync?: false},
): Signal<T | U>;
/**
* Get the current value of an `Observable` as a reactive `Signal`.
@ -104,28 +120,31 @@ export function toSignal<T, const U extends T>(
* @developerPreview
*/
export function toSignal<T, U = undefined>(
source: Observable<T>|Subscribable<T>,
options?: ToSignalOptions&{initialValue?: U}): Signal<T|U> {
source: Observable<T> | Subscribable<T>,
options?: ToSignalOptions & {initialValue?: U},
): Signal<T | U> {
ngDevMode &&
assertNotInReactiveContext(
toSignal,
'Invoking `toSignal` causes new subscriptions every time. ' +
'Consider moving `toSignal` outside of the reactive context and read the signal value where needed.');
assertNotInReactiveContext(
toSignal,
'Invoking `toSignal` causes new subscriptions every time. ' +
'Consider moving `toSignal` outside of the reactive context and read the signal value where needed.',
);
const requiresCleanup = !options?.manualCleanup;
requiresCleanup && !options?.injector && assertInInjectionContext(toSignal);
const cleanupRef =
requiresCleanup ? options?.injector?.get(DestroyRef) ?? inject(DestroyRef) : null;
const cleanupRef = requiresCleanup
? options?.injector?.get(DestroyRef) ?? inject(DestroyRef)
: null;
// Note: T is the Observable value type, and U is the initial value type. They don't have to be
// the same - the returned signal gives values of type `T`.
let state: WritableSignal<State<T|U>>;
let state: WritableSignal<State<T | U>>;
if (options?.requireSync) {
// Initially the signal is in a `NoValue` state.
state = signal({kind: StateKind.NoValue});
} else {
// If an initial value was passed, use it. Otherwise, use `undefined` as the initial value.
state = signal<State<T|U>>({kind: StateKind.Value, value: options?.initialValue as U});
state = signal<State<T | U>>({kind: StateKind.Value, value: options?.initialValue as U});
}
// Note: This code cannot run inside a reactive context (see assertion above). If we'd support
@ -135,8 +154,8 @@ export function toSignal<T, U = undefined>(
// subscription. Additional context (related to async pipe):
// https://github.com/angular/angular/pull/50522.
const sub = source.subscribe({
next: value => state.set({kind: StateKind.Value, value}),
error: error => {
next: (value) => state.set({kind: StateKind.Value, value}),
error: (error) => {
if (options?.rejectErrors) {
// Kick the error back to RxJS. It will be caught and rethrown in a macrotask, which causes
// the error to end up as an uncaught exception.
@ -150,8 +169,9 @@ export function toSignal<T, U = undefined>(
if (ngDevMode && options?.requireSync && state().kind === StateKind.NoValue) {
throw new ɵRuntimeError(
ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT,
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.');
ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT,
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.',
);
}
// Unsubscribe when the current context is destroyed, if requested.
@ -170,8 +190,9 @@ export function toSignal<T, U = undefined>(
// This shouldn't really happen because the error is thrown on creation.
// TODO(alxhub): use a RuntimeError when we finalize the error semantics
throw new ɵRuntimeError(
ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT,
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.');
ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT,
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.',
);
}
});
}
@ -196,4 +217,4 @@ interface ErrorState {
error: unknown;
}
type State<T> = NoValueState|ValueState<T>|ErrorState;
type State<T> = NoValueState | ValueState<T> | ErrorState;

View file

@ -14,14 +14,14 @@ import {outputFromObservable} from '../src';
describe('outputFromObservable()', () => {
// Safety clean-up as we are patching `onUnhandledError` in this test.
afterEach(() => config.onUnhandledError = null);
afterEach(() => (config.onUnhandledError = null));
it('should support emitting values via BehaviorSubject', () => {
const subject = new BehaviorSubject(0);
const output = TestBed.runInInjectionContext(() => outputFromObservable(subject));
const values: number[] = [];
output.subscribe(v => values.push(v));
output.subscribe((v) => values.push(v));
expect(values).toEqual([0]);
@ -38,7 +38,7 @@ describe('outputFromObservable()', () => {
subject.next(1);
const values: number[] = [];
output.subscribe(v => values.push(v));
output.subscribe((v) => values.push(v));
expect(values).toEqual([1]);
@ -55,7 +55,7 @@ describe('outputFromObservable()', () => {
subject.next(1);
const values: number[] = [];
output.subscribe(v => values.push(v));
output.subscribe((v) => values.push(v));
expect(values).toEqual([]);
@ -72,7 +72,7 @@ describe('outputFromObservable()', () => {
emitter.next(1);
const values: number[] = [];
output.subscribe(v => values.push(v));
output.subscribe((v) => values.push(v));
expect(values).toEqual([]);
@ -89,7 +89,7 @@ describe('outputFromObservable()', () => {
expect(subject.observed).toBe(false);
const subscription = output.subscribe(v => values.push(v));
const subscription = output.subscribe((v) => values.push(v));
expect(subject.observed).toBe(true);
expect(values).toEqual([]);
@ -109,7 +109,7 @@ describe('outputFromObservable()', () => {
expect(subject.observed).toBe(false);
output.subscribe(v => values.push(v));
output.subscribe((v) => values.push(v));
expect(subject.observed).toBe(true);
expect(values).toEqual([]);
@ -130,17 +130,17 @@ describe('outputFromObservable()', () => {
// initiate destroy.
TestBed.resetTestingModule();
expect(() => output.subscribe(() => {}))
.toThrowError(/Unexpected subscription to destroyed `OutputRef`/);
expect(() => output.subscribe(() => {})).toThrowError(
/Unexpected subscription to destroyed `OutputRef`/,
);
});
it('should be a noop when the source observable completes', () => {
const subject = new Subject<number>();
const outputRef = TestBed.runInInjectionContext(() => outputFromObservable(subject));
const values: number[] = [];
outputRef.subscribe(v => values.push(v));
outputRef.subscribe((v) => values.push(v));
subject.next(1);
subject.next(2);
@ -157,13 +157,13 @@ describe('outputFromObservable()', () => {
const outputRef = TestBed.runInInjectionContext(() => outputFromObservable(subject));
const values: number[] = [];
outputRef.subscribe(v => values.push(v));
outputRef.subscribe((v) => values.push(v));
subject.next(1);
subject.next(2);
expect(values).toEqual([1, 2]);
config.onUnhandledError = err => {
config.onUnhandledError = (err) => {
config.onUnhandledError = null;
expect((err as Error).message).toEqual('test error message');

View file

@ -18,7 +18,7 @@ describe('outputToObservable()', () => {
const observable = outputToObservable(outputRef);
const values: number[] = [];
observable.subscribe({next: v => values.push(v)});
observable.subscribe({next: (v) => values.push(v)});
expect(values).toEqual([]);
outputRef.emit(1);
@ -33,7 +33,7 @@ describe('outputToObservable()', () => {
let completed = false;
const subscription = observable.subscribe({
complete: () => completed = true,
complete: () => (completed = true),
});
outputRef.emit(1);
@ -56,7 +56,7 @@ describe('outputToObservable()', () => {
const observable = outputToObservable(outputRef);
const values: number[] = [];
observable.subscribe({next: v => values.push(v)});
observable.subscribe({next: (v) => values.push(v)});
expect(values).toEqual([]);
subject.next(1);
@ -72,7 +72,7 @@ describe('outputToObservable()', () => {
let completed = false;
const subscription = observable.subscribe({
complete: () => completed = true,
complete: () => (completed = true),
});
subject.next(1);
@ -90,32 +90,34 @@ describe('outputToObservable()', () => {
expect(subject.observed).toBe(false);
});
it('may not complete the observable with an improperly ' +
'configured `OutputRef` without a destroy ref as source',
() => {
const outputRef = new EventEmitter<number>();
const observable = outputToObservable(outputRef);
it(
'may not complete the observable with an improperly ' +
'configured `OutputRef` without a destroy ref as source',
() => {
const outputRef = new EventEmitter<number>();
const observable = outputToObservable(outputRef);
let completed = false;
const subscription = observable.subscribe({
complete: () => completed = true,
});
let completed = false;
const subscription = observable.subscribe({
complete: () => (completed = true),
});
outputRef.next(1);
outputRef.next(2);
outputRef.next(1);
outputRef.next(2);
expect(completed).toBe(false);
expect(subscription.closed).toBe(false);
expect(outputRef.observed).toBe(true);
expect(completed).toBe(false);
expect(subscription.closed).toBe(false);
expect(outputRef.observed).toBe(true);
// destroy `EnvironmentInjector`.
TestBed.resetTestingModule();
// destroy `EnvironmentInjector`.
TestBed.resetTestingModule();
expect(completed)
.withContext('Should not be completed as there is no known time when to destroy')
.toBe(false);
expect(subscription.closed).toBe(false);
expect(outputRef.observed).toBe(true);
});
expect(completed)
.withContext('Should not be completed as there is no known time when to destroy')
.toBe(false);
expect(subscription.closed).toBe(false);
expect(outputRef.observed).toBe(true);
},
);
});
});

View file

@ -26,7 +26,7 @@ describe('takeUntilDestroyed', () => {
},
complete() {
completed = true;
}
},
});
source$.next(1);
@ -52,7 +52,7 @@ describe('takeUntilDestroyed', () => {
},
complete() {
completed = true;
}
},
});
source$.next(1);

View file

@ -6,7 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, computed, createEnvironmentInjector, EnvironmentInjector, Injector, Signal, signal} from '@angular/core';
import {
Component,
computed,
createEnvironmentInjector,
EnvironmentInjector,
Injector,
Signal,
signal,
} from '@angular/core';
import {toObservable} from '@angular/core/rxjs-interop';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {take, toArray} from 'rxjs/operators';
@ -19,8 +27,7 @@ describe('toObservable()', () => {
template: '',
standalone: true,
})
class Cmp {
}
class Cmp {}
beforeEach(() => {
fixture = TestBed.createComponent(Cmp);
@ -67,8 +74,8 @@ describe('toObservable()', () => {
let currentError: any = null;
const sub = counter$.subscribe({
next: value => currentValue = value,
error: err => currentError = err,
next: (value) => (currentValue = value),
error: (err) => (currentError = err),
});
flushEffects();
@ -163,7 +170,7 @@ describe('toObservable()', () => {
// Read emits. If we are still tracked in the effect, this will cause an infinite loop by
// triggering the effect again.
emits();
emits.update(v => v + 1);
emits.update((v) => v + 1);
});
flushEffects();
expect(emits()).toBe(1);

View file

@ -6,85 +6,114 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectionStrategy, Component, computed, EnvironmentInjector, Injector, runInInjectionContext, Signal} from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
EnvironmentInjector,
Injector,
runInInjectionContext,
Signal,
} from '@angular/core';
import {toSignal} from '@angular/core/rxjs-interop';
import {TestBed} from '@angular/core/testing';
import {BehaviorSubject, Observable, Observer, ReplaySubject, Subject, Subscribable, Unsubscribable} from 'rxjs';
import {
BehaviorSubject,
Observable,
Observer,
ReplaySubject,
Subject,
Subscribable,
Unsubscribable,
} from 'rxjs';
describe('toSignal()', () => {
it('should reflect the last emitted value of an Observable', test(() => {
const counter$ = new BehaviorSubject(0);
const counter = toSignal(counter$);
it(
'should reflect the last emitted value of an Observable',
test(() => {
const counter$ = new BehaviorSubject(0);
const counter = toSignal(counter$);
expect(counter()).toBe(0);
counter$.next(1);
expect(counter()).toBe(1);
counter$.next(3);
expect(counter()).toBe(3);
}));
expect(counter()).toBe(0);
counter$.next(1);
expect(counter()).toBe(1);
counter$.next(3);
expect(counter()).toBe(3);
}),
);
it('should notify when the last emitted value of an Observable changes', test(() => {
let seenValue: number = 0;
const counter$ = new BehaviorSubject(1);
const counter = toSignal(counter$);
it(
'should notify when the last emitted value of an Observable changes',
test(() => {
let seenValue: number = 0;
const counter$ = new BehaviorSubject(1);
const counter = toSignal(counter$);
expect(counter()).toBe(1);
expect(counter()).toBe(1);
counter$.next(2);
expect(counter()).toBe(2);
}));
counter$.next(2);
expect(counter()).toBe(2);
}),
);
it('should propagate an error returned by the Observable', test(() => {
const counter$ = new BehaviorSubject(1);
const counter = toSignal(counter$);
it(
'should propagate an error returned by the Observable',
test(() => {
const counter$ = new BehaviorSubject(1);
const counter = toSignal(counter$);
expect(counter()).toBe(1);
expect(counter()).toBe(1);
counter$.error('fail');
expect(counter).toThrow('fail');
}));
counter$.error('fail');
expect(counter).toThrow('fail');
}),
);
it('should unsubscribe when the current context is destroyed', test(() => {
const counter$ = new BehaviorSubject(0);
const injector = Injector.create({providers: []}) as EnvironmentInjector;
const counter = runInInjectionContext(injector, () => toSignal(counter$));
it(
'should unsubscribe when the current context is destroyed',
test(() => {
const counter$ = new BehaviorSubject(0);
const injector = Injector.create({providers: []}) as EnvironmentInjector;
const counter = runInInjectionContext(injector, () => toSignal(counter$));
expect(counter()).toBe(0);
counter$.next(1);
expect(counter()).toBe(1);
expect(counter()).toBe(0);
counter$.next(1);
expect(counter()).toBe(1);
// Destroying the injector should unsubscribe the Observable.
injector.destroy();
// Destroying the injector should unsubscribe the Observable.
injector.destroy();
// The signal should have the last value observed.
expect(counter()).toBe(1);
// The signal should have the last value observed.
expect(counter()).toBe(1);
// And this value should no longer be updating (unsubscribed).
counter$.next(2);
expect(counter()).toBe(1);
}));
// And this value should no longer be updating (unsubscribed).
counter$.next(2);
expect(counter()).toBe(1);
}),
);
it(
'should unsubscribe when an explicitly provided injector is destroyed',
test(() => {
const counter$ = new BehaviorSubject(0);
const injector = Injector.create({providers: []}) as EnvironmentInjector;
const counter = toSignal(counter$, {injector});
expect(counter()).toBe(0);
counter$.next(1);
expect(counter()).toBe(1);
it('should unsubscribe when an explicitly provided injector is destroyed', test(() => {
const counter$ = new BehaviorSubject(0);
const injector = Injector.create({providers: []}) as EnvironmentInjector;
const counter = toSignal(counter$, {injector});
// Destroying the injector should unsubscribe the Observable.
injector.destroy();
expect(counter()).toBe(0);
counter$.next(1);
expect(counter()).toBe(1);
// The signal should have the last value observed.
expect(counter()).toBe(1);
// Destroying the injector should unsubscribe the Observable.
injector.destroy();
// The signal should have the last value observed.
expect(counter()).toBe(1);
// And this value should no longer be updating (unsubscribed).
counter$.next(2);
expect(counter()).toBe(1);
}));
// And this value should no longer be updating (unsubscribed).
counter$.next(2);
expect(counter()).toBe(1);
}),
);
it('should not require an injection context when manualCleanup is passed', () => {
const counter$ = new BehaviorSubject(0);
@ -95,8 +124,9 @@ describe('toSignal()', () => {
it('should not unsubscribe when manualCleanup is passed', () => {
const counter$ = new BehaviorSubject(0);
const injector = Injector.create({providers: []}) as EnvironmentInjector;
const counter =
runInInjectionContext(injector, () => toSignal(counter$, {manualCleanup: true}));
const counter = runInInjectionContext(injector, () =>
toSignal(counter$, {manualCleanup: true}),
);
injector.destroy();
@ -117,9 +147,9 @@ describe('toSignal()', () => {
return counter() * 2;
});
expect(() => doubleCounter())
.toThrowError(
/toSignal\(\) cannot be called from within a reactive context. Invoking `toSignal` causes new subscriptions every time./);
expect(() => doubleCounter()).toThrowError(
/toSignal\(\) cannot be called from within a reactive context. Invoking `toSignal` causes new subscriptions every time./,
);
});
it('should throw the error back to RxJS if rejectErrors is set', () => {
@ -145,55 +175,73 @@ describe('toSignal()', () => {
});
describe('with no initial value', () => {
it('should return `undefined` if read before a value is emitted', test(() => {
const counter$ = new Subject<number>();
const counter = toSignal(counter$);
it(
'should return `undefined` if read before a value is emitted',
test(() => {
const counter$ = new Subject<number>();
const counter = toSignal(counter$);
expect(counter()).toBeUndefined();
counter$.next(1);
expect(counter()).toBe(1);
}));
expect(counter()).toBeUndefined();
counter$.next(1);
expect(counter()).toBe(1);
}),
);
it('should not throw if a value is emitted before called', test(() => {
const counter$ = new Subject<number>();
const counter = toSignal(counter$);
it(
'should not throw if a value is emitted before called',
test(() => {
const counter$ = new Subject<number>();
const counter = toSignal(counter$);
counter$.next(1);
expect(() => counter()).not.toThrow();
}));
counter$.next(1);
expect(() => counter()).not.toThrow();
}),
);
});
describe('with requireSync', () => {
it('should throw if created before a value is emitted', test(() => {
const counter$ = new Subject<number>();
expect(() => toSignal(counter$, {requireSync: true})).toThrow();
}));
it(
'should throw if created before a value is emitted',
test(() => {
const counter$ = new Subject<number>();
expect(() => toSignal(counter$, {requireSync: true})).toThrow();
}),
);
it('should not throw if a value emits synchronously on creation', test(() => {
const counter$ = new ReplaySubject<number>(1);
counter$.next(1);
const counter = toSignal(counter$);
expect(counter()).toBe(1);
}));
it(
'should not throw if a value emits synchronously on creation',
test(() => {
const counter$ = new ReplaySubject<number>(1);
counter$.next(1);
const counter = toSignal(counter$);
expect(counter()).toBe(1);
}),
);
});
describe('with an initial value', () => {
it('should return the initial value if called before a value is emitted', test(() => {
const counter$ = new Subject<number>();
const counter = toSignal(counter$, {initialValue: null});
it(
'should return the initial value if called before a value is emitted',
test(() => {
const counter$ = new Subject<number>();
const counter = toSignal(counter$, {initialValue: null});
expect(counter()).toBeNull();
counter$.next(1);
expect(counter()).toBe(1);
}));
expect(counter()).toBeNull();
counter$.next(1);
expect(counter()).toBe(1);
}),
);
it('should not return the initial value if called after a value is emitted', test(() => {
const counter$ = new Subject<number>();
const counter = toSignal(counter$, {initialValue: null});
it(
'should not return the initial value if called after a value is emitted',
test(() => {
const counter$ = new Subject<number>();
const counter = toSignal(counter$, {initialValue: null});
counter$.next(1);
expect(counter()).not.toBeNull();
}));
counter$.next(1);
expect(counter()).not.toBeNull();
}),
);
});
describe('in a @Component', () => {
@ -223,51 +271,63 @@ describe('toSignal()', () => {
describe('type tests', () => {
const src = new Subject<any>();
it('should allow empty array initial values', test(() => {
const res: Signal<string[]> = toSignal(src as Observable<string[]>, {initialValue: []});
expect(res).toBeDefined();
}));
it(
'should allow empty array initial values',
test(() => {
const res: Signal<string[]> = toSignal(src as Observable<string[]>, {initialValue: []});
expect(res).toBeDefined();
}),
);
it('should allow literal types', test(() => {
type Animal = 'cat'|'dog';
const res: Signal<Animal> = toSignal(src as Observable<Animal>, {initialValue: 'cat'});
expect(res).toBeDefined();
}));
it(
'should allow literal types',
test(() => {
type Animal = 'cat' | 'dog';
const res: Signal<Animal> = toSignal(src as Observable<Animal>, {initialValue: 'cat'});
expect(res).toBeDefined();
}),
);
it('should not allow initial values outside of the observable type', test(() => {
type Animal = 'cat'|'dog';
// @ts-expect-error
const res = toSignal(src as Observable<Animal>, {initialValue: 'cow'});
expect(res).toBeDefined();
}));
it(
'should not allow initial values outside of the observable type',
test(() => {
type Animal = 'cat' | 'dog';
// @ts-expect-error
const res = toSignal(src as Observable<Animal>, {initialValue: 'cow'});
expect(res).toBeDefined();
}),
);
it('allows null as an initial value', test(() => {
const res = toSignal(src as Observable<string>, {initialValue: null});
const res2: Signal<string|null> = res;
// @ts-expect-error
const res3: Signal<string|undefined> = res;
expect(res2).toBeDefined();
expect(res3).toBeDefined();
}));
it(
'allows null as an initial value',
test(() => {
const res = toSignal(src as Observable<string>, {initialValue: null});
const res2: Signal<string | null> = res;
// @ts-expect-error
const res3: Signal<string | undefined> = res;
expect(res2).toBeDefined();
expect(res3).toBeDefined();
}),
);
it('allows undefined as an initial value', test(() => {
const res = toSignal(src as Observable<string>, {initialValue: undefined});
const res2: Signal<string|undefined> = res;
// @ts-expect-error
const res3: Signal<string|null> = res;
expect(res2).toBeDefined();
expect(res3).toBeDefined();
}));
it(
'allows undefined as an initial value',
test(() => {
const res = toSignal(src as Observable<string>, {initialValue: undefined});
const res2: Signal<string | undefined> = res;
// @ts-expect-error
const res3: Signal<string | null> = res;
expect(res2).toBeDefined();
expect(res3).toBeDefined();
}),
);
});
});
function test(fn: () => void|Promise<void>): () => Promise<void> {
function test(fn: () => void | Promise<void>): () => Promise<void> {
return async () => {
const injector = Injector.create({
providers: [
{provide: EnvironmentInjector, useFactory: () => injector},
]
providers: [{provide: EnvironmentInjector, useFactory: () => injector}],
}) as EnvironmentInjector;
try {
return await runInInjectionContext(injector, fn);

View file

@ -25,18 +25,23 @@ const newFunction = 'waitForAsync';
export class Rule extends Rules.TypedRule {
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
const failures: RuleFailure[] = [];
const asyncImportSpecifier =
getImportSpecifier(sourceFile, '@angular/core/testing', deprecatedFunction);
const asyncImport =
asyncImportSpecifier ? closestNode(asyncImportSpecifier, ts.isNamedImports) : null;
const asyncImportSpecifier = getImportSpecifier(
sourceFile,
'@angular/core/testing',
deprecatedFunction,
);
const asyncImport = asyncImportSpecifier
? closestNode(asyncImportSpecifier, ts.isNamedImports)
: null;
// If there are no imports of `async`, we can exit early.
if (asyncImportSpecifier && asyncImport) {
const typeChecker = program.getTypeChecker();
const printer = ts.createPrinter();
failures.push(this._getNamedImportsFailure(asyncImport, sourceFile, printer));
this.findAsyncReferences(sourceFile, typeChecker, asyncImportSpecifier)
.forEach((node) => failures.push(this._getIdentifierNodeFailure(node, sourceFile)));
this.findAsyncReferences(sourceFile, typeChecker, asyncImportSpecifier).forEach((node) =>
failures.push(this._getIdentifierNodeFailure(node, sourceFile)),
);
}
return failures;
@ -44,35 +49,52 @@ export class Rule extends Rules.TypedRule {
/** Gets a failure for an import of the `async` function. */
private _getNamedImportsFailure(
node: ts.NamedImports, sourceFile: ts.SourceFile, printer: ts.Printer): RuleFailure {
node: ts.NamedImports,
sourceFile: ts.SourceFile,
printer: ts.Printer,
): RuleFailure {
const replacementText = printer.printNode(
ts.EmitHint.Unspecified, replaceImport(node, deprecatedFunction, newFunction), sourceFile);
ts.EmitHint.Unspecified,
replaceImport(node, deprecatedFunction, newFunction),
sourceFile,
);
return new RuleFailure(
sourceFile, node.getStart(), node.getEnd(),
`Imports of the deprecated ${deprecatedFunction} function are not allowed. Use ${
newFunction} instead.`,
this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText));
sourceFile,
node.getStart(),
node.getEnd(),
`Imports of the deprecated ${deprecatedFunction} function are not allowed. Use ${newFunction} instead.`,
this.ruleName,
new Replacement(node.getStart(), node.getWidth(), replacementText),
);
}
/** Gets a failure for an identifier node. */
private _getIdentifierNodeFailure(node: ts.Identifier, sourceFile: ts.SourceFile): RuleFailure {
return new RuleFailure(
sourceFile, node.getStart(), node.getEnd(),
`References to the deprecated ${deprecatedFunction} function are not allowed. Use ${
newFunction} instead.`,
this.ruleName, new Replacement(node.getStart(), node.getWidth(), newFunction));
sourceFile,
node.getStart(),
node.getEnd(),
`References to the deprecated ${deprecatedFunction} function are not allowed. Use ${newFunction} instead.`,
this.ruleName,
new Replacement(node.getStart(), node.getWidth(), newFunction),
);
}
/** Finds calls to the `async` function. */
private findAsyncReferences(
sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker,
asyncImportSpecifier: ts.ImportSpecifier) {
sourceFile: ts.SourceFile,
typeChecker: ts.TypeChecker,
asyncImportSpecifier: ts.ImportSpecifier,
) {
const results = new Set<ts.Identifier>();
ts.forEachChild(sourceFile, function visitNode(node: ts.Node) {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) &&
node.expression.text === deprecatedFunction &&
isReferenceToImport(typeChecker, node.expression, asyncImportSpecifier)) {
if (
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === deprecatedFunction &&
isReferenceToImport(typeChecker, node.expression, asyncImportSpecifier)
) {
results.add(node.expression);
}

View file

@ -13,8 +13,7 @@ import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/com
import {migrateFile} from './utils';
export default function(): Rule {
export default function (): Rule {
return async (tree: Tree) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
const basePath = process.cwd();
@ -22,7 +21,8 @@ export default function(): Rule {
if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot run the transfer state migration.');
'Could not find any tsconfig file. Cannot run the transfer state migration.',
);
}
for (const tsconfigPath of allPaths) {
@ -31,16 +31,16 @@ export default function(): Rule {
};
}
function runMigration(tree: Tree, tsconfigPath: string, basePath: string) {
const program = createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles =
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => canMigrateFile(basePath, sourceFile, program));
for (const sourceFile of sourceFiles) {
let update: UpdateRecorder|null = null;
let update: UpdateRecorder | null = null;
const rewriter = (startPos: number, width: number, text: string|null) => {
const rewriter = (startPos: number, width: number, text: string | null) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(relative(basePath, sourceFile.fileName));

View file

@ -36,7 +36,10 @@ const HTTP_TESTING_MODULES = new Set([HTTP_CLIENT_TESTING_MODULE]);
export type RewriteFn = (startPos: number, width: number, text: string) => void;
export function migrateFile(
sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, rewriteFn: RewriteFn) {
sourceFile: ts.SourceFile,
typeChecker: ts.TypeChecker,
rewriteFn: RewriteFn,
) {
const changeTracker = new ChangeTracker(ts.createPrinter());
const addedImports = new Map<string, Set<string>>([
[COMMON_HTTP, new Set()],
@ -44,12 +47,14 @@ export function migrateFile(
]);
const commonHttpIdentifiers = new Set(
getImportSpecifiers(sourceFile, COMMON_HTTP, [...HTTP_MODULES])
.map((specifier) => specifier.getText()),
getImportSpecifiers(sourceFile, COMMON_HTTP, [...HTTP_MODULES]).map((specifier) =>
specifier.getText(),
),
);
const commonHttpTestingIdentifiers = new Set(
getImportSpecifiers(sourceFile, COMMON_HTTP_TESTING, [...HTTP_TESTING_MODULES])
.map((specifier) => specifier.getText()),
getImportSpecifiers(sourceFile, COMMON_HTTP_TESTING, [...HTTP_TESTING_MODULES]).map(
(specifier) => specifier.getText(),
),
);
ts.forEachChild(sourceFile, function visit(node: ts.Node) {
@ -57,7 +62,7 @@ export function migrateFile(
if (ts.isClassDeclaration(node)) {
const decorators = getAngularDecorators(typeChecker, ts.getDecorators(node) || []);
decorators.forEach(decorator => {
decorators.forEach((decorator) => {
migrateDecorator(decorator, commonHttpIdentifiers, addedImports, changeTracker);
});
}
@ -77,9 +82,9 @@ export function migrateFile(
...commonHttpImports.elements.filter((current) => !symbolImportsToRemove.includes(current)),
...[...(addedImports.get(COMMON_HTTP) ?? [])].map((entry) => {
return ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier(entry),
false,
undefined,
ts.factory.createIdentifier(entry),
);
}),
]);
@ -95,13 +100,13 @@ export function migrateFile(
const newHttpTestingImports = ts.factory.updateNamedImports(commonHttpTestingImports, [
...commonHttpTestingImports.elements.filter(
(current) => !symbolImportsToRemove.includes(current),
),
(current) => !symbolImportsToRemove.includes(current),
),
...[...(addedImports.get(COMMON_HTTP_TESTING) ?? [])].map((entry) => {
return ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier(entry),
false,
undefined,
ts.factory.createIdentifier(entry),
);
}),
]);
@ -117,12 +122,17 @@ export function migrateFile(
}
function migrateDecorator(
decorator: NgDecorator, commonHttpIdentifiers: Set<string>,
addedImports: Map<string, Set<string>>, changeTracker: ChangeTracker) {
decorator: NgDecorator,
commonHttpIdentifiers: Set<string>,
addedImports: Map<string, Set<string>>,
changeTracker: ChangeTracker,
) {
// Only @NgModule and @Component support `imports`.
// Also skip decorators with no arguments.
if ((decorator.name !== 'NgModule' && decorator.name !== 'Component') ||
decorator.node.expression.arguments.length < 1) {
if (
(decorator.name !== 'NgModule' && decorator.name !== 'Component') ||
decorator.node.expression.arguments.length < 1
) {
return;
}
@ -163,10 +173,10 @@ function migrateDecorator(
} else {
addedImports.get(COMMON_HTTP)?.add(WITH_XSRF_CONFIGURATION);
addedProviders.add(
createCallExpression(
WITH_XSRF_CONFIGURATION,
importedModules.xsrfOptions?.options ? [importedModules.xsrfOptions.options] : [],
),
createCallExpression(
WITH_XSRF_CONFIGURATION,
importedModules.xsrfOptions?.options ? [importedModules.xsrfOptions.options] : [],
),
);
}
}
@ -174,9 +184,11 @@ function migrateDecorator(
// Removing the imported Http modules from the imports list
const newImports = ts.factory.createArrayLiteralExpression([
...moduleImports.elements.filter(
(item) => item !== importedModules.client && item !== importedModules.clientJsonp &&
item !== importedModules.xsrf,
),
(item) =>
item !== importedModules.client &&
item !== importedModules.clientJsonp &&
item !== importedModules.xsrf,
),
]);
// Adding the new providers
@ -206,15 +218,21 @@ function migrateDecorator(
}
function migrateTestingModuleImports(
node: ts.Node, commonHttpTestingIdentifiers: Set<string>,
addedImports: Map<string, Set<string>>, changeTracker: ChangeTracker) {
node: ts.Node,
commonHttpTestingIdentifiers: Set<string>,
addedImports: Map<string, Set<string>>,
changeTracker: ChangeTracker,
) {
// Look for calls to `TestBed.configureTestingModule` with at least one argument.
// TODO: this won't work if `TestBed` is aliased or type cast.
if (!ts.isCallExpression(node) || node.arguments.length < 1 ||
!ts.isPropertyAccessExpression(node.expression) ||
!ts.isIdentifier(node.expression.expression) ||
node.expression.expression.text !== 'TestBed' ||
node.expression.name.text !== 'configureTestingModule') {
if (
!ts.isCallExpression(node) ||
node.arguments.length < 1 ||
!ts.isPropertyAccessExpression(node.expression) ||
!ts.isIdentifier(node.expression.expression) ||
node.expression.expression.text !== 'TestBed' ||
node.expression.name.text !== 'configureTestingModule'
) {
return;
}
@ -232,7 +250,7 @@ function migrateTestingModuleImports(
// Does the imports array contain the HttpClientTestingModule?
const httpClientTesting = importsArray.elements.find(
(elt) => elt.getText() === HTTP_CLIENT_TESTING_MODULE,
(elt) => elt.getText() === HTTP_CLIENT_TESTING_MODULE,
);
if (!httpClientTesting || !commonHttpTestingIdentifiers.has(HTTP_CLIENT_TESTING_MODULE)) {
return;
@ -306,18 +324,18 @@ function getProvidersFromLiteralExpr(literal: ts.ObjectLiteralExpression) {
}
function getImportedHttpModules(
imports: ts.ArrayLiteralExpression,
commonHttpIdentifiers: Set<string>,
imports: ts.ArrayLiteralExpression,
commonHttpIdentifiers: Set<string>,
) {
let client: ts.Identifier|ts.CallExpression|null = null;
let clientJsonp: ts.Identifier|ts.CallExpression|null = null;
let xsrf: ts.Identifier|ts.CallExpression|null = null;
let client: ts.Identifier | ts.CallExpression | null = null;
let clientJsonp: ts.Identifier | ts.CallExpression | null = null;
let xsrf: ts.Identifier | ts.CallExpression | null = null;
// represents respectively:
// HttpClientXsrfModule.disable()
// HttpClientXsrfModule.withOptions(options)
// base HttpClientXsrfModule
let xsrfOptions: 'disable'|{options: ts.Expression}|null = null;
let xsrfOptions: 'disable' | {options: ts.Expression} | null = null;
// Handling the 3 http modules from @angular/common/http and skipping the rest
for (const item of imports.elements) {
@ -364,8 +382,8 @@ function getImportedHttpModules(
function createCallExpression(functionName: string, args: ts.Expression[] = []) {
return ts.factory.createCallExpression(
ts.factory.createIdentifier(functionName),
undefined,
args,
ts.factory.createIdentifier(functionName),
undefined,
args,
);
}

View file

@ -38,8 +38,9 @@ export class AnalyzedFile {
analyzedFiles.set(path, analysis);
}
const duplicate =
analysis.ranges.find(current => current[0] === range[0] && current[1] === range[1]);
const duplicate = analysis.ranges.find(
(current) => current[0] === range[0] && current[1] === range[1],
);
if (!duplicate) {
analysis.ranges.push(range);
@ -53,20 +54,24 @@ export class AnalyzedFile {
* @param analyzedFiles Map in which to store the results.
*/
export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map<string, AnalyzedFile>) {
forEachClass(sourceFile, node => {
forEachClass(sourceFile, (node) => {
// Note: we have a utility to resolve the Angular decorators from a class declaration already.
// We don't use it here, because it requires access to the type checker which makes it more
// time-consuming to run internally.
const decorator = ts.getDecorators(node)?.find(dec => {
return ts.isCallExpression(dec.expression) && ts.isIdentifier(dec.expression.expression) &&
dec.expression.expression.text === 'Component';
}) as (ts.Decorator & {expression: ts.CallExpression}) |
undefined;
const decorator = ts.getDecorators(node)?.find((dec) => {
return (
ts.isCallExpression(dec.expression) &&
ts.isIdentifier(dec.expression.expression) &&
dec.expression.expression.text === 'Component'
);
}) as (ts.Decorator & {expression: ts.CallExpression}) | undefined;
const metadata = decorator && decorator.expression.arguments.length > 0 &&
ts.isObjectLiteralExpression(decorator.expression.arguments[0]) ?
decorator.expression.arguments[0] :
null;
const metadata =
decorator &&
decorator.expression.arguments.length > 0 &&
ts.isObjectLiteralExpression(decorator.expression.arguments[0])
? decorator.expression.arguments[0]
: null;
if (!metadata) {
return;
@ -75,17 +80,21 @@ export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map<string, An
for (const prop of metadata.properties) {
// All the properties we care about should have static
// names and be initialized to a static string.
if (!ts.isPropertyAssignment(prop) || !ts.isStringLiteralLike(prop.initializer) ||
(!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name))) {
if (
!ts.isPropertyAssignment(prop) ||
!ts.isStringLiteralLike(prop.initializer) ||
(!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name))
) {
continue;
}
switch (prop.name.text) {
case 'template':
// +1/-1 to exclude the opening/closing characters from the range.
AnalyzedFile.addRange(
sourceFile.fileName, analyzedFiles,
[prop.initializer.getStart() + 1, prop.initializer.getEnd() - 1]);
AnalyzedFile.addRange(sourceFile.fileName, analyzedFiles, [
prop.initializer.getStart() + 1,
prop.initializer.getEnd() - 1,
]);
break;
case 'templateUrl':

View file

@ -15,7 +15,7 @@ import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/com
import {analyze, AnalyzedFile} from './analysis';
import {migrateTemplate} from './migration';
export default function(): Rule {
export default function (): Rule {
return async (tree: Tree) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
const basePath = process.cwd();
@ -23,7 +23,8 @@ export default function(): Rule {
if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot run the invalid two-way bindings migration.');
'Could not find any tsconfig file. Cannot run the invalid two-way bindings migration.',
);
}
for (const tsconfigPath of allPaths) {
@ -34,8 +35,9 @@ export default function(): Rule {
function runInvalidTwoWayBindingsMigration(tree: Tree, tsconfigPath: string, basePath: string) {
const program = createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles =
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => canMigrateFile(basePath, sourceFile, program));
const analysis = new Map<string, AnalyzedFile>();
for (const sourceFile of sourceFiles) {

View file

@ -6,7 +6,19 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ASTWithSource, BindingType, ParsedEventType, parseTemplate, ReadKeyExpr, ReadPropExpr, TmplAstBoundAttribute, TmplAstElement, TmplAstNode, TmplAstRecursiveVisitor, TmplAstTemplate} from '@angular/compiler';
import {
ASTWithSource,
BindingType,
ParsedEventType,
parseTemplate,
ReadKeyExpr,
ReadPropExpr,
TmplAstBoundAttribute,
TmplAstElement,
TmplAstNode,
TmplAstRecursiveVisitor,
TmplAstTemplate,
} from '@angular/compiler';
import ts from 'typescript';
/**
@ -14,13 +26,13 @@ import ts from 'typescript';
* Returns null if no changes had to be made to the file.
* @param template Template to be migrated.
*/
export function migrateTemplate(template: string): string|null {
export function migrateTemplate(template: string): string | null {
// Don't attempt to parse templates that don't contain two-way bindings.
if (!template.includes(')]=')) {
return null;
}
let rootNodes: TmplAstNode[]|null = null;
let rootNodes: TmplAstNode[] | null = null;
try {
const parsed = parseTemplate(template, '', {allowInvalidAssignmentEvents: true});
@ -28,8 +40,7 @@ export function migrateTemplate(template: string): string|null {
if (parsed.errors === null) {
rootNodes = parsed.nodes;
}
} catch {
}
} catch {}
// Don't migrate invalid templates.
if (rootNodes === null) {
@ -37,8 +48,9 @@ export function migrateTemplate(template: string): string|null {
}
const visitor = new InvalidTwoWayBindingCollector();
const bindings = visitor.collectInvalidBindings(rootNodes).sort(
(a, b) => b.sourceSpan.start.offset - a.sourceSpan.start.offset);
const bindings = visitor
.collectInvalidBindings(rootNodes)
.sort((a, b) => b.sourceSpan.start.offset - a.sourceSpan.start.offset);
if (bindings.length === 0) {
return null;
@ -79,7 +91,10 @@ function migrateTwoWayInput(binding: TmplAstBoundAttribute, value: string): stri
* @param value String value of the binding.
*/
function migrateTwoWayEvent(
value: string, binding: TmplAstBoundAttribute, printer: ts.Printer): string|null {
value: string,
binding: TmplAstBoundAttribute,
printer: ts.Printer,
): string | null {
// Note that we use the TypeScript parser, as opposed to our own, because even though we have
// an expression AST here already, our AST is harder to work with in a migration context.
// To use it here, we would have to solve the following:
@ -93,15 +108,15 @@ function migrateTwoWayEvent(
// we can get away with using the TypeScript AST instead.
const sourceFile = ts.createSourceFile('temp.ts', value, ts.ScriptTarget.Latest);
const expression =
sourceFile.statements.length === 1 && ts.isExpressionStatement(sourceFile.statements[0]) ?
sourceFile.statements[0].expression :
null;
sourceFile.statements.length === 1 && ts.isExpressionStatement(sourceFile.statements[0])
? sourceFile.statements[0].expression
: null;
if (expression === null) {
return null;
}
let migrated: ts.Expression|null = null;
let migrated: ts.Expression | null = null;
// Historically the expression parser was handling two-way events by appending `=$event`
// to the raw string before attempting to parse it. This has led to bugs over the years (see
@ -113,13 +128,21 @@ function migrateTwoWayEvent(
if (ts.isBinaryExpression(expression) && isReadExpression(expression.right)) {
// `a && b` -> `a && (b = $event)`
migrated = ts.factory.updateBinaryExpression(
expression, expression.left, expression.operatorToken,
wrapInEventAssignment(expression.right));
expression,
expression.left,
expression.operatorToken,
wrapInEventAssignment(expression.right),
);
} else if (ts.isConditionalExpression(expression) && isReadExpression(expression.whenFalse)) {
// `a ? b : c` -> `a ? b : c = $event`
migrated = ts.factory.updateConditionalExpression(
expression, expression.condition, expression.questionToken, expression.whenTrue,
expression.colonToken, wrapInEventAssignment(expression.whenFalse));
expression,
expression.condition,
expression.questionToken,
expression.whenTrue,
expression.colonToken,
wrapInEventAssignment(expression.whenFalse),
);
} else if (isPrefixNot(expression)) {
// `!!a` -> `a = $event`
let innerExpression = expression.operand;
@ -147,18 +170,24 @@ function migrateTwoWayEvent(
/** Wraps an expression in an assignment to `$event`, e.g. `foo.bar = $event`. */
function wrapInEventAssignment(node: ts.Expression): ts.Expression {
return ts.factory.createBinaryExpression(
node, ts.factory.createToken(ts.SyntaxKind.EqualsToken),
ts.factory.createIdentifier('$event'));
node,
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
ts.factory.createIdentifier('$event'),
);
}
/**
* Checks whether an expression is a valid read expression. Note that identifiers
* are considered read expressions in Angular templates as well.
*/
function isReadExpression(node: ts.Expression): node is ts.Identifier|ts.PropertyAccessExpression|
ts.ElementAccessExpression {
return ts.isIdentifier(node) || ts.isPropertyAccessExpression(node) ||
ts.isElementAccessExpression(node);
function isReadExpression(
node: ts.Expression,
): node is ts.Identifier | ts.PropertyAccessExpression | ts.ElementAccessExpression {
return (
ts.isIdentifier(node) ||
ts.isPropertyAccessExpression(node) ||
ts.isElementAccessExpression(node)
);
}
/** Checks whether an expression is in the form of `!x`. */
@ -168,11 +197,11 @@ function isPrefixNot(node: ts.Expression): node is ts.PrefixUnaryExpression {
/** Traverses a template AST and collects any invalid two-way bindings. */
class InvalidTwoWayBindingCollector extends TmplAstRecursiveVisitor {
private invalidBindings: TmplAstBoundAttribute[]|null = null;
private invalidBindings: TmplAstBoundAttribute[] | null = null;
collectInvalidBindings(rootNodes: TmplAstNode[]): TmplAstBoundAttribute[] {
const result = this.invalidBindings = [];
rootNodes.forEach(node => node.visit(this));
const result = (this.invalidBindings = []);
rootNodes.forEach((node) => node.visit(this));
this.invalidBindings = null;
return result;
}
@ -187,7 +216,7 @@ class InvalidTwoWayBindingCollector extends TmplAstRecursiveVisitor {
super.visitTemplate(template);
}
private visitNodeWithBindings(node: TmplAstElement|TmplAstTemplate) {
private visitNodeWithBindings(node: TmplAstElement | TmplAstTemplate) {
const seenOneWayBindings = new Set<string>();
// Collect all of the regular event and input binding
@ -211,8 +240,11 @@ class InvalidTwoWayBindingCollector extends TmplAstRecursiveVisitor {
// something like `[(ngModel)]="invalid" (ngModelChange)="foo()"` to
// `[ngModel]="invalid" (ngModelChange)="invalid = $event" (ngModelChange)="foo()"` which
// would break the app.
if (input.type !== BindingType.TwoWay || seenOneWayBindings.has(input.name) ||
seenOneWayBindings.has(input.name + 'Change')) {
if (
input.type !== BindingType.TwoWay ||
seenOneWayBindings.has(input.name) ||
seenOneWayBindings.has(input.name + 'Change')
) {
continue;
}

View file

@ -8,8 +8,22 @@
import {visitAll} from '@angular/compiler';
import {ElementCollector, ElementToMigrate, endMarker, MigrateError, Result, startMarker} from './types';
import {calculateNesting, getMainBlock, getOriginals, hasLineBreaks, parseTemplate, reduceNestingOffset} from './util';
import {
ElementCollector,
ElementToMigrate,
endMarker,
MigrateError,
Result,
startMarker,
} from './types';
import {
calculateNesting,
getMainBlock,
getOriginals,
hasLineBreaks,
parseTemplate,
reduceNestingOffset,
} from './util';
export const boundcase = '[ngSwitchCase]';
export const switchcase = '*ngSwitchCase';
@ -17,20 +31,17 @@ export const nakedcase = 'ngSwitchCase';
export const switchdefault = '*ngSwitchDefault';
export const nakeddefault = 'ngSwitchDefault';
export const cases = [
boundcase,
switchcase,
nakedcase,
switchdefault,
nakeddefault,
];
export const cases = [boundcase, switchcase, nakedcase, switchdefault, nakeddefault];
/**
* Replaces structural directive ngSwitch instances with new switch.
* Returns null if the migration failed (e.g. there was a syntax error).
*/
export function migrateCase(template: string):
{migrated: string, errors: MigrateError[], changed: boolean} {
export function migrateCase(template: string): {
migrated: string;
errors: MigrateError[];
changed: boolean;
} {
let errors: MigrateError[] = [];
let parsed = parseTemplate(template);
if (parsed.tree === undefined) {
@ -88,8 +99,7 @@ function migrateNgSwitchCase(etm: ElementToMigrate, tmpl: string, offset: number
const originals = getOriginals(etm, tmpl, offset);
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
const startBlock =
`${startMarker}${leadingSpace}@case (${condition}) {${leadingSpace}${lbString}${start}`;
const startBlock = `${startMarker}${leadingSpace}@case (${condition}) {${leadingSpace}${lbString}${start}`;
const endBlock = `${end}${lbString}${leadingSpace}}${endMarker}`;
const defaultBlock = startBlock + middle + endBlock;

View file

@ -8,15 +8,28 @@
import {visitAll} from '@angular/compiler';
import {ElementCollector, ElementToMigrate, endMarker, MigrateError, Result, startMarker} from './types';
import {calculateNesting, getMainBlock, getOriginals, getPlaceholder, hasLineBreaks, parseTemplate, PlaceholderKind, reduceNestingOffset} from './util';
import {
ElementCollector,
ElementToMigrate,
endMarker,
MigrateError,
Result,
startMarker,
} from './types';
import {
calculateNesting,
getMainBlock,
getOriginals,
getPlaceholder,
hasLineBreaks,
parseTemplate,
PlaceholderKind,
reduceNestingOffset,
} from './util';
export const ngfor = '*ngFor';
export const nakedngfor = 'ngFor';
const fors = [
ngfor,
nakedngfor,
];
const fors = [ngfor, nakedngfor];
export const commaSeparatedSyntax = new Map([
['(', ')'],
@ -32,8 +45,11 @@ export const stringPairs = new Map([
* Replaces structural directive ngFor instances with new for.
* Returns null if the migration failed (e.g. there was a syntax error).
*/
export function migrateFor(template: string):
{migrated: string, errors: MigrateError[], changed: boolean} {
export function migrateFor(template: string): {
migrated: string;
errors: MigrateError[];
changed: boolean;
} {
let errors: MigrateError[] = [];
let parsed = parseTemplate(template);
if (parsed.tree === undefined) {
@ -93,14 +109,15 @@ function migrateStandardNgFor(etm: ElementToMigrate, tmpl: string, offset: numbe
// first portion should always be the loop definition prefixed with `let`
const condition = parts[0].replace('let ', '');
if (condition.indexOf(' as ') > -1) {
let errorMessage = `Found an aliased collection on an ngFor: "${condition}".` +
' Collection aliasing is not supported with @for.' +
' Refactor the code to remove the `as` alias and re-run the migration.';
let errorMessage =
`Found an aliased collection on an ngFor: "${condition}".` +
' Collection aliasing is not supported with @for.' +
' Refactor the code to remove the `as` alias and re-run the migration.';
throw new Error(errorMessage);
}
const loopVar = condition.split(' of ')[0];
let trackBy = loopVar;
let aliasedIndex: string|null = null;
let aliasedIndex: string | null = null;
let tmplPlaceholder = '';
for (let i = 1; i < parts.length; i++) {
const part = parts[i].trim();
@ -146,7 +163,7 @@ function migrateStandardNgFor(etm: ElementToMigrate, tmpl: string, offset: numbe
trackBy = trackBy.replace('$index', aliasedIndex);
}
const aliasStr = (aliases.length > 0) ? `;${aliases.join(';')}` : '';
const aliasStr = aliases.length > 0 ? `;${aliases.join(';')}` : '';
let startBlock = `${startMarker}@for (${condition}; track ${trackBy}${aliasStr}) {${lbString}`;
let endBlock = `${lbString}}${endMarker}`;
@ -186,7 +203,7 @@ function migrateBoundNgFor(etm: ElementToMigrate, tmpl: string, offset: number):
aliasedIndex = key;
}
}
const aliasStr = (aliases.length > 0) ? `;${aliases.join(';')}` : '';
const aliasStr = aliases.length > 0 ? `;${aliases.join(';')}` : '';
let trackBy = aliasAttrs.item;
if (forAttrs.trackBy !== '') {
@ -220,8 +237,11 @@ function getNgForParts(expression: string): string[] {
const isInCommaSeparated = commaSeparatedStack.length === 0;
// Any semicolon is a delimiter, as well as any comma outside
// of comma-separated syntax, as long as they're outside of a string.
if (isInString && current.length > 0 &&
(char === ';' || (char === ',' && isInCommaSeparated))) {
if (
isInString &&
current.length > 0 &&
(char === ';' || (char === ',' && isInCommaSeparated))
) {
parts.push(current);
current = '';
continue;
@ -236,8 +256,9 @@ function getNgForParts(expression: string): string[] {
if (commaSeparatedSyntax.has(char)) {
commaSeparatedStack.push(commaSeparatedSyntax.get(char)!);
} else if (
commaSeparatedStack.length > 0 &&
commaSeparatedStack[commaSeparatedStack.length - 1] === char) {
commaSeparatedStack.length > 0 &&
commaSeparatedStack[commaSeparatedStack.length - 1] === char
) {
commaSeparatedStack.pop();
}

View file

@ -9,7 +9,9 @@
import ts from 'typescript';
export function lookupIdentifiersInSourceFile(
sourceFile: ts.SourceFile, names: string[]): Set<ts.Identifier> {
sourceFile: ts.SourceFile,
names: string[],
): Set<ts.Identifier> {
const results = new Set<ts.Identifier>();
const visit = (node: ts.Node): void => {
if (ts.isIdentifier(node) && names.includes(node.text)) {

View file

@ -8,25 +8,40 @@
import {visitAll} from '@angular/compiler';
import {ElementCollector, ElementToMigrate, endMarker, MigrateError, Result, startMarker} from './types';
import {calculateNesting, getMainBlock, getOriginals, getPlaceholder, hasLineBreaks, parseTemplate, PlaceholderKind, reduceNestingOffset} from './util';
import {
ElementCollector,
ElementToMigrate,
endMarker,
MigrateError,
Result,
startMarker,
} from './types';
import {
calculateNesting,
getMainBlock,
getOriginals,
getPlaceholder,
hasLineBreaks,
parseTemplate,
PlaceholderKind,
reduceNestingOffset,
} from './util';
export const ngif = '*ngIf';
export const boundngif = '[ngIf]';
export const nakedngif = 'ngIf';
const ifs = [
ngif,
nakedngif,
boundngif,
];
const ifs = [ngif, nakedngif, boundngif];
/**
* Replaces structural directive ngif instances with new if.
* Returns null if the migration failed (e.g. there was a syntax error).
*/
export function migrateIf(template: string):
{migrated: string, errors: MigrateError[], changed: boolean} {
export function migrateIf(template: string): {
migrated: string;
errors: MigrateError[];
changed: boolean;
} {
let errors: MigrateError[] = [];
let parsed = parseTemplate(template);
if (parsed.tree === undefined) {
@ -80,7 +95,7 @@ function migrateNgIf(etm: ElementToMigrate, tmpl: string, offset: number): Resul
} else if (matchThen && matchThen.length > 0) {
// just then
return buildStandardIfThenBlock(etm, tmpl, matchThen[0], offset);
} else if ((matchElse && matchElse.length > 0)) {
} else if (matchElse && matchElse.length > 0) {
// just else
return buildStandardIfElseBlock(etm, tmpl, matchElse![0], offset);
}
@ -98,13 +113,14 @@ function buildIfBlock(etm: ElementToMigrate, tmpl: string, offset: number): Resu
// includes the mandatory semicolon before as
const lbString = etm.hasLineBreaks ? '\n' : '';
let condition = etm.attr.value
.replace(' as ', '; as ')
// replace 'let' with 'as' whatever spaces are between ; and 'let'
.replace(/;\s*let/g, '; as');
.replace(' as ', '; as ')
// replace 'let' with 'as' whatever spaces are between ; and 'let'
.replace(/;\s*let/g, '; as');
if (aliases.length > 1 || (aliases.length === 1 && condition.indexOf('; as') > -1)) {
// only 1 alias allowed
throw new Error(
'Found more than one alias on your ngIf. Remove one of them and re-run the migration.');
'Found more than one alias on your ngIf. Remove one of them and re-run the migration.',
);
} else if (aliases.length === 1) {
condition += `; as ${aliases[0]}`;
}
@ -127,12 +143,17 @@ function buildIfBlock(etm: ElementToMigrate, tmpl: string, offset: number): Resu
}
function buildStandardIfElseBlock(
etm: ElementToMigrate, tmpl: string, elseString: string, offset: number): Result {
etm: ElementToMigrate,
tmpl: string,
elseString: string,
offset: number,
): Result {
// includes the mandatory semicolon before as
const condition = etm.getCondition()
.replace(' as ', '; as ')
// replace 'let' with 'as' whatever spaces are between ; and 'let'
.replace(/;\s*let/g, '; as');
const condition = etm
.getCondition()
.replace(' as ', '; as ')
// replace 'let' with 'as' whatever spaces are between ; and 'let'
.replace(/;\s*let/g, '; as');
const elsePlaceholder = getPlaceholder(etm.getTemplateName(elseString));
return buildIfElseBlock(etm, tmpl, condition, elsePlaceholder, offset);
}
@ -149,7 +170,8 @@ function buildBoundIfElseBlock(etm: ElementToMigrate, tmpl: string, offset: numb
if (aliases.length > 1 || (aliases.length === 1 && condition.indexOf('; as') > -1)) {
// only 1 alias allowed
throw new Error(
'Found more than one alias on your ngIf. Remove one of them and re-run the migration.');
'Found more than one alias on your ngIf. Remove one of them and re-run the migration.',
);
} else if (aliases.length === 1) {
condition += `; as ${aliases[0]}`;
}
@ -162,8 +184,12 @@ function buildBoundIfElseBlock(etm: ElementToMigrate, tmpl: string, offset: numb
}
function buildIfElseBlock(
etm: ElementToMigrate, tmpl: string, condition: string, elsePlaceholder: string,
offset: number): Result {
etm: ElementToMigrate,
tmpl: string,
condition: string,
elsePlaceholder: string,
offset: number,
): Result {
const lbString = etm.hasLineBreaks ? '\n' : '';
const originals = getOriginals(etm, tmpl, offset);
@ -187,32 +213,47 @@ function buildIfElseBlock(
}
function buildStandardIfThenElseBlock(
etm: ElementToMigrate, tmpl: string, thenString: string, elseString: string,
offset: number): Result {
etm: ElementToMigrate,
tmpl: string,
thenString: string,
elseString: string,
offset: number,
): Result {
// includes the mandatory semicolon before as
const condition = etm.getCondition()
.replace(' as ', '; as ')
// replace 'let' with 'as' whatever spaces are between ; and 'let'
.replace(/;\s*let/g, '; as');
const condition = etm
.getCondition()
.replace(' as ', '; as ')
// replace 'let' with 'as' whatever spaces are between ; and 'let'
.replace(/;\s*let/g, '; as');
const thenPlaceholder = getPlaceholder(etm.getTemplateName(thenString, elseString));
const elsePlaceholder = getPlaceholder(etm.getTemplateName(elseString));
return buildIfThenElseBlock(etm, tmpl, condition, thenPlaceholder, elsePlaceholder, offset);
}
function buildStandardIfThenBlock(
etm: ElementToMigrate, tmpl: string, thenString: string, offset: number): Result {
etm: ElementToMigrate,
tmpl: string,
thenString: string,
offset: number,
): Result {
// includes the mandatory semicolon before as
const condition = etm.getCondition()
.replace(' as ', '; as ')
// replace 'let' with 'as' whatever spaces are between ; and 'let'
.replace(/;\s*let/g, '; as');
const condition = etm
.getCondition()
.replace(' as ', '; as ')
// replace 'let' with 'as' whatever spaces are between ; and 'let'
.replace(/;\s*let/g, '; as');
const thenPlaceholder = getPlaceholder(etm.getTemplateName(thenString));
return buildIfThenBlock(etm, tmpl, condition, thenPlaceholder, offset);
}
function buildIfThenElseBlock(
etm: ElementToMigrate, tmpl: string, condition: string, thenPlaceholder: string,
elsePlaceholder: string, offset: number): Result {
etm: ElementToMigrate,
tmpl: string,
condition: string,
thenPlaceholder: string,
elsePlaceholder: string,
offset: number,
): Result {
const lbString = etm.hasLineBreaks ? '\n' : '';
const originals = getOriginals(etm, tmpl, offset);
@ -237,8 +278,12 @@ function buildIfThenElseBlock(
}
function buildIfThenBlock(
etm: ElementToMigrate, tmpl: string, condition: string, thenPlaceholder: string,
offset: number): Result {
etm: ElementToMigrate,
tmpl: string,
condition: string,
thenPlaceholder: string,
offset: number,
): Result {
const lbString = etm.hasLineBreaks ? '\n' : '';
const originals = getOriginals(etm, tmpl, offset);

View file

@ -21,7 +21,7 @@ interface Options {
format: boolean;
}
export default function(options: Options): Rule {
export default function (options: Options): Rule {
return async (tree: Tree, context: SchematicContext) => {
const basePath = process.cwd();
const pathToMigrate = normalizePath(join(basePath, options.path));
@ -32,14 +32,20 @@ export default function(options: Options): Rule {
if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot run the control flow migration.');
'Could not find any tsconfig file. Cannot run the control flow migration.',
);
}
let errors: string[] = [];
for (const tsconfigPath of allPaths) {
const migrateErrors =
runControlFlowMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
const migrateErrors = runControlFlowMigration(
tree,
tsconfigPath,
basePath,
pathToMigrate,
options,
);
errors = [...errors, ...migrateErrors];
}
@ -53,21 +59,31 @@ export default function(options: Options): Rule {
}
function runControlFlowMigration(
tree: Tree, tsconfigPath: string, basePath: string, pathToMigrate: string,
schematicOptions: Options): string[] {
tree: Tree,
tsconfigPath: string,
basePath: string,
pathToMigrate: string,
schematicOptions: Options,
): string[] {
if (schematicOptions.path.startsWith('..')) {
throw new SchematicsException(
'Cannot run control flow migration outside of the current project.');
'Cannot run control flow migration outside of the current project.',
);
}
const program = createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program.getSourceFiles().filter(
sourceFile => sourceFile.fileName.startsWith(pathToMigrate) &&
canMigrateFile(basePath, sourceFile, program));
const sourceFiles = program
.getSourceFiles()
.filter(
(sourceFile) =>
sourceFile.fileName.startsWith(pathToMigrate) &&
canMigrateFile(basePath, sourceFile, program),
);
if (sourceFiles.length === 0) {
throw new SchematicsException(`Could not find any files to migrate under the path ${
pathToMigrate}. Cannot run the control flow migration.`);
throw new SchematicsException(
`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the control flow migration.`,
);
}
const analysis = new Map<string, AnalyzedFile>();
@ -92,8 +108,14 @@ function runControlFlowMigration(
const template = content.slice(start, end);
const length = (end ?? content.length) - start;
const {migrated, errors} =
migrateTemplate(template, type, node, file, schematicOptions.format, analysis);
const {migrated, errors} = migrateTemplate(
template,
type,
node,
file,
schematicOptions.format,
analysis,
);
if (migrated !== null) {
update.remove(start, length);
@ -118,12 +140,12 @@ function runControlFlowMigration(
}
function sortFilePaths(names: string[]): string[] {
names.sort((a, _) => a.endsWith('.html') ? -1 : 0);
names.sort((a, _) => (a.endsWith('.html') ? -1 : 0));
return names;
}
function generateErrorMessage(path: string, errors: MigrateError[]): string {
let errorMessage = `Template "${path}" encountered ${errors.length} errors during migration:\n`;
errorMessage += errors.map(e => ` - ${e.type}: ${e.error}\n`);
errorMessage += errors.map((e) => ` - ${e.type}: ${e.error}\n`);
return errorMessage;
}

View file

@ -12,16 +12,35 @@ import {migrateCase} from './cases';
import {migrateFor} from './fors';
import {migrateIf} from './ifs';
import {migrateSwitch} from './switches';
import {AnalyzedFile, endI18nMarker, endMarker, MigrateError, startI18nMarker, startMarker} from './types';
import {canRemoveCommonModule, formatTemplate, parseTemplate, processNgTemplates, removeImports, validateI18nStructure, validateMigratedTemplate} from './util';
import {
AnalyzedFile,
endI18nMarker,
endMarker,
MigrateError,
startI18nMarker,
startMarker,
} from './types';
import {
canRemoveCommonModule,
formatTemplate,
parseTemplate,
processNgTemplates,
removeImports,
validateI18nStructure,
validateMigratedTemplate,
} from './util';
/**
* Actually migrates a given template to the new syntax
*/
export function migrateTemplate(
template: string, templateType: string, node: ts.Node, file: AnalyzedFile,
format: boolean = true,
analyzedFiles: Map<string, AnalyzedFile>|null): {migrated: string, errors: MigrateError[]} {
template: string,
templateType: string,
node: ts.Node,
file: AnalyzedFile,
format: boolean = true,
analyzedFiles: Map<string, AnalyzedFile> | null,
): {migrated: string; errors: MigrateError[]} {
let errors: MigrateError[] = [];
let migrated = template;
if (templateType === 'template' || templateType === 'templateUrl') {
@ -38,7 +57,7 @@ export function migrateTemplate(
}
migrated = templateResult.migrated;
const changed =
ifResult.changed || forResult.changed || switchResult.changed || caseResult.changed;
ifResult.changed || forResult.changed || switchResult.changed || caseResult.changed;
if (changed) {
// determine if migrated template is a valid structure
// if it is not, fail out
@ -51,8 +70,10 @@ export function migrateTemplate(
if (format && changed) {
migrated = formatTemplate(migrated, templateType);
}
const markerRegex =
new RegExp(`${startMarker}|${endMarker}|${startI18nMarker}|${endI18nMarker}`, 'gm');
const markerRegex = new RegExp(
`${startMarker}|${endMarker}|${startI18nMarker}|${endI18nMarker}`,
'gm',
);
migrated = migrated.replace(markerRegex, '');
file.removeCommonModule = canRemoveCommonModule(template);
@ -61,8 +82,11 @@ export function migrateTemplate(
// when migrating an external template, we have to pass back
// whether it's safe to remove the CommonModule to the
// original component class source file
if (templateType === 'templateUrl' && analyzedFiles !== null &&
analyzedFiles.has(file.sourceFile.fileName)) {
if (
templateType === 'templateUrl' &&
analyzedFiles !== null &&
analyzedFiles.has(file.sourceFile.fileName)
) {
const componentFile = analyzedFiles.get(file.sourceFile.fileName)!;
componentFile.getSortedRanges();
// we have already checked the template file to see if it is safe to remove the imports

View file

@ -9,21 +9,36 @@
import {Element, Node, Text, visitAll} from '@angular/compiler';
import {cases} from './cases';
import {ElementCollector, ElementToMigrate, endMarker, MigrateError, Result, startMarker} from './types';
import {calculateNesting, getMainBlock, getOriginals, hasLineBreaks, parseTemplate, reduceNestingOffset} from './util';
import {
ElementCollector,
ElementToMigrate,
endMarker,
MigrateError,
Result,
startMarker,
} from './types';
import {
calculateNesting,
getMainBlock,
getOriginals,
hasLineBreaks,
parseTemplate,
reduceNestingOffset,
} from './util';
export const ngswitch = '[ngSwitch]';
const switches = [
ngswitch,
];
const switches = [ngswitch];
/**
* Replaces structural directive ngSwitch instances with new switch.
* Returns null if the migration failed (e.g. there was a syntax error).
*/
export function migrateSwitch(template: string):
{migrated: string, errors: MigrateError[], changed: boolean} {
export function migrateSwitch(template: string): {
migrated: string;
errors: MigrateError[];
changed: boolean;
} {
let errors: MigrateError[] = [];
let parsed = parseTemplate(template);
if (parsed.tree === undefined) {
@ -70,8 +85,9 @@ function assertValidSwitchStructure(children: Node[]): void {
for (const child of children) {
if (child instanceof Text && child.value.trim() !== '') {
throw new Error(
`Text node: "${child.value}" would result in invalid migrated @switch block structure. ` +
`@switch can only have @case or @default as children.`);
`Text node: "${child.value}" would result in invalid migrated @switch block structure. ` +
`@switch can only have @case or @default as children.`,
);
} else if (child instanceof Element) {
let hasCase = false;
for (const attr of child.attrs) {
@ -81,9 +97,9 @@ function assertValidSwitchStructure(children: Node[]): void {
}
if (!hasCase) {
throw new Error(
`Element node: "${
child.name}" would result in invalid migrated @switch block structure. ` +
`@switch can only have @case or @default as children.`);
`Element node: "${child.name}" would result in invalid migrated @switch block structure. ` +
`@switch can only have @case or @default as children.`,
);
}
}
}

View file

@ -24,18 +24,21 @@ export const startI18nMarker = '⚈';
export const endI18nMarker = '⚉';
export const importRemovals = [
'NgIf', 'NgIfElse', 'NgIfThenElse', 'NgFor', 'NgForOf', 'NgForTrackBy', 'NgSwitch',
'NgSwitchCase', 'NgSwitchDefault'
'NgIf',
'NgIfElse',
'NgIfThenElse',
'NgFor',
'NgForOf',
'NgForTrackBy',
'NgSwitch',
'NgSwitchCase',
'NgSwitchDefault',
];
export const importWithCommonRemovals = [...importRemovals, 'CommonModule'];
function allFormsOf(selector: string): string[] {
return [
selector,
`*${selector}`,
`[${selector}]`,
];
return [selector, `*${selector}`, `[${selector}]`];
}
const commonModuleDirectives = new Set([
@ -72,25 +75,28 @@ const commonModulePipes = [
'titlecase',
'percent',
'titlecase',
].map(name => pipeMatchRegExpFor(name));
].map((name) => pipeMatchRegExpFor(name));
/**
* Represents a range of text within a file. Omitting the end
* means that it's until the end of the file.
*/
type Range = {
start: number,
end?: number, node: ts.Node, type: string, remove: boolean,
start: number;
end?: number;
node: ts.Node;
type: string;
remove: boolean;
};
export type Offsets = {
pre: number,
post: number,
pre: number;
post: number;
};
export type Result = {
tmpl: string,
offsets: Offsets,
tmpl: string;
offsets: Offsets;
};
export interface ForAttributes {
@ -104,7 +110,7 @@ export interface AliasAttributes {
}
export interface ParseResult {
tree: ParseTreeResult|undefined;
tree: ParseTreeResult | undefined;
errors: MigrateError[];
}
@ -112,8 +118,8 @@ export interface ParseResult {
* Represents an error that happened during migration
*/
export type MigrateError = {
type: string,
error: unknown,
type: string;
error: unknown;
};
/**
@ -122,17 +128,21 @@ export type MigrateError = {
export class ElementToMigrate {
el: Element;
attr: Attribute;
elseAttr: Attribute|undefined;
thenAttr: Attribute|undefined;
forAttrs: ForAttributes|undefined;
aliasAttrs: AliasAttributes|undefined;
elseAttr: Attribute | undefined;
thenAttr: Attribute | undefined;
forAttrs: ForAttributes | undefined;
aliasAttrs: AliasAttributes | undefined;
nestCount = 0;
hasLineBreaks = false;
constructor(
el: Element, attr: Attribute, elseAttr: Attribute|undefined = undefined,
thenAttr: Attribute|undefined = undefined, forAttrs: ForAttributes|undefined = undefined,
aliasAttrs: AliasAttributes|undefined = undefined) {
el: Element,
attr: Attribute,
elseAttr: Attribute | undefined = undefined,
thenAttr: Attribute | undefined = undefined,
forAttrs: ForAttributes | undefined = undefined,
aliasAttrs: AliasAttributes | undefined = undefined,
) {
this.el = el;
this.attr = attr;
this.elseAttr = elseAttr;
@ -154,15 +164,17 @@ export class ElementToMigrate {
condition = condition.slice(0, elseIx);
}
let letVar = chunks.find(c => c.search(/\s*let\s/) > -1);
let letVar = chunks.find((c) => c.search(/\s*let\s/) > -1);
return condition + (letVar ? ';' + letVar : '');
}
getTemplateName(targetStr: string, secondStr?: string): string {
const targetLocation = this.attr.value.indexOf(targetStr);
const secondTargetLocation = secondStr ? this.attr.value.indexOf(secondStr) : undefined;
let templateName =
this.attr.value.slice(targetLocation + targetStr.length, secondTargetLocation);
let templateName = this.attr.value.slice(
targetLocation + targetStr.length,
secondTargetLocation,
);
if (templateName.startsWith(':')) {
templateName = templateName.slice(1).trim();
}
@ -170,24 +182,27 @@ export class ElementToMigrate {
}
getValueEnd(offset: number): number {
return (this.attr.valueSpan ? (this.attr.valueSpan.end.offset + 1) :
this.attr.keySpan!.end.offset) -
offset;
return (
(this.attr.valueSpan ? this.attr.valueSpan.end.offset + 1 : this.attr.keySpan!.end.offset) -
offset
);
}
hasChildren(): boolean {
return this.el.children.length > 0;
}
getChildSpan(offset: number): {childStart: number, childEnd: number} {
getChildSpan(offset: number): {childStart: number; childEnd: number} {
const childStart = this.el.children[0].sourceSpan.start.offset - offset;
const childEnd = this.el.children[this.el.children.length - 1].sourceSpan.end.offset - offset;
return {childStart, childEnd};
}
shouldRemoveElseAttr(): boolean {
return (this.el.name === 'ng-template' || this.el.name === 'ng-container') &&
this.elseAttr !== undefined;
return (
(this.el.name === 'ng-template' || this.el.name === 'ng-container') &&
this.elseAttr !== undefined
);
}
getElseAttrStr(): string {
@ -220,10 +235,10 @@ export class Template {
count: number = 0;
contents: string = '';
children: string = '';
i18n: Attribute|null = null;
i18n: Attribute | null = null;
attributes: Attribute[];
constructor(el: Element, name: string, i18n: Attribute|null) {
constructor(el: Element, name: string, i18n: Attribute | null) {
this.el = el;
this.name = name;
this.attributes = el.attrs;
@ -231,16 +246,16 @@ export class Template {
}
get isNgTemplateOutlet() {
return this.attributes.find(attr => attr.name === '*ngTemplateOutlet') !== undefined;
return this.attributes.find((attr) => attr.name === '*ngTemplateOutlet') !== undefined;
}
get outletContext() {
const letVar = this.attributes.find(attr => attr.name.startsWith('let-'));
const letVar = this.attributes.find((attr) => attr.name.startsWith('let-'));
return letVar ? `; context: {$implicit: ${letVar.name.split('-')[1]}}` : '';
}
generateTemplateOutlet() {
const attr = this.attributes.find(attr => attr.name === '*ngTemplateOutlet');
const attr = this.attributes.find((attr) => attr.name === '*ngTemplateOutlet');
const outletValue = attr?.value ?? this.name.slice(1);
return `<ng-container *ngTemplateOutlet="${outletValue}${this.outletContext}"></ng-container>`;
}
@ -250,8 +265,9 @@ export class Template {
this.children = '';
if (this.el.children.length > 0) {
this.children = tmpl.slice(
this.el.children[0].sourceSpan.start.offset,
this.el.children[this.el.children.length - 1].sourceSpan.end.offset);
this.el.children[0].sourceSpan.start.offset,
this.el.children[this.el.children.length - 1].sourceSpan.end.offset,
);
}
}
}
@ -272,13 +288,14 @@ export class AnalyzedFile {
/** Returns the ranges in the order in which they should be migrated. */
getSortedRanges(): Range[] {
// templates first for checking on whether certain imports can be safely removed
this.templateRanges = this.ranges.slice()
.filter(x => x.type === 'template' || x.type === 'templateUrl')
.sort((aStart, bStart) => bStart.start - aStart.start);
this.importRanges =
this.ranges.slice()
.filter(x => x.type === 'importDecorator' || x.type === 'importDeclaration')
.sort((aStart, bStart) => bStart.start - aStart.start);
this.templateRanges = this.ranges
.slice()
.filter((x) => x.type === 'template' || x.type === 'templateUrl')
.sort((aStart, bStart) => bStart.start - aStart.start);
this.importRanges = this.ranges
.slice()
.filter((x) => x.type === 'importDecorator' || x.type === 'importDeclaration')
.sort((aStart, bStart) => bStart.start - aStart.start);
return [...this.templateRanges, ...this.importRanges];
}
@ -289,8 +306,11 @@ export class AnalyzedFile {
* @param range Range to be added.
*/
static addRange(
path: string, sourceFile: ts.SourceFile, analyzedFiles: Map<string, AnalyzedFile>,
range: Range): void {
path: string,
sourceFile: ts.SourceFile,
analyzedFiles: Map<string, AnalyzedFile>,
range: Range,
): void {
let analysis = analyzedFiles.get(path);
if (!analysis) {
@ -298,8 +318,9 @@ export class AnalyzedFile {
analyzedFiles.set(path, analysis);
}
const duplicate =
analysis.ranges.find(current => current.start === range.start && current.end === range.end);
const duplicate = analysis.ranges.find(
(current) => current.start === range.start && current.end === range.end,
);
if (!duplicate) {
analysis.ranges.push(range);
@ -311,7 +332,7 @@ export class AnalyzedFile {
* It is only run on .ts files.
*/
verifyCanRemoveImports() {
const importDeclaration = this.importRanges.find(r => r.type === 'importDeclaration');
const importDeclaration = this.importRanges.find((r) => r.type === 'importDeclaration');
const instances = lookupIdentifiersInSourceFile(this.sourceFile, importWithCommonRemovals);
let foundImportDeclaration = false;
let count = 0;
@ -357,7 +378,7 @@ export class CommonCollector extends RecursiveVisitor {
}
private hasPipes(input: string): boolean {
return commonModulePipes.some(regexp => regexp.test(input));
return commonModulePipes.some((regexp) => regexp.test(input));
}
}
@ -366,7 +387,7 @@ export class i18nCollector extends RecursiveVisitor {
readonly elements: Element[] = [];
override visitElement(el: Element): void {
if (el.attrs.find(a => a.name === 'i18n') !== undefined) {
if (el.attrs.find((a) => a.name === 'i18n') !== undefined) {
this.elements.push(el);
}
super.visitElement(el, null);
@ -385,13 +406,15 @@ export class ElementCollector extends RecursiveVisitor {
if (el.attrs.length > 0) {
for (const attr of el.attrs) {
if (this._attributes.includes(attr.name)) {
const elseAttr = el.attrs.find(x => x.name === boundngifelse);
const thenAttr =
el.attrs.find(x => x.name === boundngifthenelse || x.name === boundngifthen);
const elseAttr = el.attrs.find((x) => x.name === boundngifelse);
const thenAttr = el.attrs.find(
(x) => x.name === boundngifthenelse || x.name === boundngifthen,
);
const forAttrs = attr.name === nakedngfor ? this.getForAttrs(el) : undefined;
const aliasAttrs = this.getAliasAttrs(el);
this.elements.push(
new ElementToMigrate(el, attr, elseAttr, thenAttr, forAttrs, aliasAttrs));
new ElementToMigrate(el, attr, elseAttr, thenAttr, forAttrs, aliasAttrs),
);
}
}
}
@ -452,8 +475,9 @@ export class TemplateCollector extends RecursiveVisitor {
this.elements.push(new ElementToMigrate(el, templateAttr));
} else if (templateAttr !== null) {
throw new Error(
`A duplicate ng-template name "${templateAttr.name}" was found. ` +
`The control flow migration requires unique ng-template names within a component.`);
`A duplicate ng-template name "${templateAttr.name}" was found. ` +
`The control flow migration requires unique ng-template names within a component.`,
);
}
}
super.visitElement(el, null);

View file

@ -10,7 +10,23 @@ import {Attribute, Element, HtmlParser, Node, ParseTreeResult, visitAll} from '@
import {dirname, join} from 'path';
import ts from 'typescript';
import {AnalyzedFile, CommonCollector, ElementCollector, ElementToMigrate, endI18nMarker, endMarker, i18nCollector, importRemovals, importWithCommonRemovals, MigrateError, ParseResult, startI18nMarker, startMarker, Template, TemplateCollector} from './types';
import {
AnalyzedFile,
CommonCollector,
ElementCollector,
ElementToMigrate,
endI18nMarker,
endMarker,
i18nCollector,
importRemovals,
importWithCommonRemovals,
MigrateError,
ParseResult,
startI18nMarker,
startMarker,
Template,
TemplateCollector,
} from './types';
const startMarkerRegex = new RegExp(startMarker, 'gm');
const endMarkerRegex = new RegExp(endMarker, 'gm');
@ -24,7 +40,7 @@ const replaceMarkerRegex = new RegExp(`${startMarker}|${endMarker}`, 'gm');
* @param analyzedFiles Map in which to store the results.
*/
export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map<string, AnalyzedFile>) {
forEachClass(sourceFile, node => {
forEachClass(sourceFile, (node) => {
if (ts.isClassDeclaration(node)) {
analyzeDecorators(node, sourceFile, analyzedFiles);
} else {
@ -34,7 +50,7 @@ export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map<string, An
}
function checkIfShouldChange(decl: ts.ImportDeclaration, file: AnalyzedFile) {
const range = file.importRanges.find(r => r.type === 'importDeclaration');
const range = file.importRanges.find((r) => r.type === 'importDeclaration');
if (range === undefined || !range.remove) {
return false;
}
@ -44,9 +60,12 @@ function checkIfShouldChange(decl: ts.ImportDeclaration, file: AnalyzedFile) {
// and that's the only thing there, we should do nothing.
const clause = decl.getChildAt(1) as ts.ImportClause;
return !(
!file.removeCommonModule && clause.namedBindings && ts.isNamedImports(clause.namedBindings) &&
clause.namedBindings.elements.length === 1 &&
clause.namedBindings.elements[0].getText() === 'CommonModule');
!file.removeCommonModule &&
clause.namedBindings &&
ts.isNamedImports(clause.namedBindings) &&
clause.namedBindings.elements.length === 1 &&
clause.namedBindings.elements[0].getText() === 'CommonModule'
);
}
function updateImportDeclaration(decl: ts.ImportDeclaration, removeCommonModule: boolean): string {
@ -63,26 +82,39 @@ function updateImportDeclaration(decl: ts.ImportDeclaration, removeCommonModule:
removeComments: true,
});
const updated = ts.factory.updateImportDeclaration(
decl, decl.modifiers, updatedClause, decl.moduleSpecifier, undefined);
decl,
decl.modifiers,
updatedClause,
decl.moduleSpecifier,
undefined,
);
return printer.printNode(ts.EmitHint.Unspecified, updated, clause.getSourceFile());
}
function updateImportClause(clause: ts.ImportClause, removeCommonModule: boolean): ts.ImportClause|
null {
function updateImportClause(
clause: ts.ImportClause,
removeCommonModule: boolean,
): ts.ImportClause | null {
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
const removals = removeCommonModule ? importWithCommonRemovals : importRemovals;
const elements = clause.namedBindings.elements.filter(el => !removals.includes(el.getText()));
const elements = clause.namedBindings.elements.filter((el) => !removals.includes(el.getText()));
if (elements.length === 0) {
return null;
}
clause = ts.factory.updateImportClause(
clause, clause.isTypeOnly, clause.name, ts.factory.createNamedImports(elements));
clause,
clause.isTypeOnly,
clause.name,
ts.factory.createNamedImports(elements),
);
}
return clause;
}
function updateClassImports(
propAssignment: ts.PropertyAssignment, removeCommonModule: boolean): string|null {
propAssignment: ts.PropertyAssignment,
removeCommonModule: boolean,
): string | null {
const printer = ts.createPrinter();
const importList = propAssignment.initializer;
@ -92,57 +124,73 @@ function updateClassImports(
}
const removals = removeCommonModule ? importWithCommonRemovals : importRemovals;
const elements =
importList.elements.filter(el => !ts.isIdentifier(el) || !removals.includes(el.text));
const elements = importList.elements.filter(
(el) => !ts.isIdentifier(el) || !removals.includes(el.text),
);
if (elements.length === importList.elements.length) {
// nothing changed
return null;
}
const updatedElements = ts.factory.updateArrayLiteralExpression(importList, elements);
const updatedAssignment =
ts.factory.updatePropertyAssignment(propAssignment, propAssignment.name, updatedElements);
const updatedAssignment = ts.factory.updatePropertyAssignment(
propAssignment,
propAssignment.name,
updatedElements,
);
return printer.printNode(
ts.EmitHint.Unspecified, updatedAssignment, updatedAssignment.getSourceFile());
ts.EmitHint.Unspecified,
updatedAssignment,
updatedAssignment.getSourceFile(),
);
}
function analyzeImportDeclarations(
node: ts.ImportDeclaration, sourceFile: ts.SourceFile,
analyzedFiles: Map<string, AnalyzedFile>) {
node: ts.ImportDeclaration,
sourceFile: ts.SourceFile,
analyzedFiles: Map<string, AnalyzedFile>,
) {
if (node.getText().indexOf('@angular/common') === -1) {
return;
}
const clause = node.getChildAt(1) as ts.ImportClause;
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
const elements =
clause.namedBindings.elements.filter(el => importWithCommonRemovals.includes(el.getText()));
const elements = clause.namedBindings.elements.filter((el) =>
importWithCommonRemovals.includes(el.getText()),
);
if (elements.length > 0) {
AnalyzedFile.addRange(sourceFile.fileName, sourceFile, analyzedFiles, {
start: node.getStart(),
end: node.getEnd(),
node,
type: 'importDeclaration',
remove: true
remove: true,
});
}
}
}
function analyzeDecorators(
node: ts.ClassDeclaration, sourceFile: ts.SourceFile,
analyzedFiles: Map<string, AnalyzedFile>) {
node: ts.ClassDeclaration,
sourceFile: ts.SourceFile,
analyzedFiles: Map<string, AnalyzedFile>,
) {
// Note: we have a utility to resolve the Angular decorators from a class declaration already.
// We don't use it here, because it requires access to the type checker which makes it more
// time-consuming to run internally.
const decorator = ts.getDecorators(node)?.find(dec => {
return ts.isCallExpression(dec.expression) && ts.isIdentifier(dec.expression.expression) &&
dec.expression.expression.text === 'Component';
}) as (ts.Decorator & {expression: ts.CallExpression}) |
undefined;
const decorator = ts.getDecorators(node)?.find((dec) => {
return (
ts.isCallExpression(dec.expression) &&
ts.isIdentifier(dec.expression.expression) &&
dec.expression.expression.text === 'Component'
);
}) as (ts.Decorator & {expression: ts.CallExpression}) | undefined;
const metadata = decorator && decorator.expression.arguments.length > 0 &&
ts.isObjectLiteralExpression(decorator.expression.arguments[0]) ?
decorator.expression.arguments[0] :
null;
const metadata =
decorator &&
decorator.expression.arguments.length > 0 &&
ts.isObjectLiteralExpression(decorator.expression.arguments[0])
? decorator.expression.arguments[0]
: null;
if (!metadata) {
return;
@ -151,8 +199,10 @@ function analyzeDecorators(
for (const prop of metadata.properties) {
// All the properties we care about should have static
// names and be initialized to a static string.
if (!ts.isPropertyAssignment(prop) ||
(!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name))) {
if (
!ts.isPropertyAssignment(prop) ||
(!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name))
) {
continue;
}
@ -182,9 +232,12 @@ function analyzeDecorators(
// Leave the end as undefined which means that the range is until the end of the file.
if (ts.isStringLiteralLike(prop.initializer)) {
const path = join(dirname(sourceFile.fileName), prop.initializer.text);
AnalyzedFile.addRange(
path, sourceFile, analyzedFiles,
{start: 0, node: prop, type: 'templateUrl', remove: true});
AnalyzedFile.addRange(path, sourceFile, analyzedFiles, {
start: 0,
node: prop,
type: 'templateUrl',
remove: true,
});
}
break;
}
@ -198,8 +251,10 @@ function getNestedCount(etm: ElementToMigrate, aggregator: number[]) {
if (aggregator.length === 0) {
return 0;
}
if (etm.el.sourceSpan.start.offset < aggregator[aggregator.length - 1] &&
etm.el.sourceSpan.end.offset !== aggregator[aggregator.length - 1]) {
if (
etm.el.sourceSpan.start.offset < aggregator[aggregator.length - 1] &&
etm.el.sourceSpan.end.offset !== aggregator[aggregator.length - 1]
) {
// element is nested
aggregator.push(etm.el.sourceSpan.end.offset);
return aggregator.length - 1;
@ -231,7 +286,7 @@ export function parseTemplate(template: string): ParseResult {
// Don't migrate invalid templates.
if (parsed.errors && parsed.errors.length > 0) {
const errors = parsed.errors.map(e => ({type: 'parse', error: e}));
const errors = parsed.errors.map((e) => ({type: 'parse', error: e}));
return {tree: undefined, errors};
}
} catch (e: any) {
@ -247,8 +302,9 @@ export function validateMigratedTemplate(migrated: string, fileName: string): Mi
errors.push({
type: 'parse',
error: new Error(
`The migration resulted in invalid HTML for ${fileName}. ` +
`Please check the template for valid HTML structures and run the migration again.`)
`The migration resulted in invalid HTML for ${fileName}. ` +
`Please check the template for valid HTML structures and run the migration again.`,
),
});
}
if (parsed.tree) {
@ -260,18 +316,19 @@ export function validateMigratedTemplate(migrated: string, fileName: string): Mi
return errors;
}
export function validateI18nStructure(parsed: ParseTreeResult, fileName: string): Error|null {
export function validateI18nStructure(parsed: ParseTreeResult, fileName: string): Error | null {
const visitor = new i18nCollector();
visitAll(visitor, parsed.rootNodes);
const parents = visitor.elements.filter(el => el.children.length > 0);
const parents = visitor.elements.filter((el) => el.children.length > 0);
for (const p of parents) {
for (const el of visitor.elements) {
if (el === p) continue;
if (isChildOf(p, el)) {
return new Error(
`i18n Nesting error: The migration would result in invalid i18n nesting for ` +
`i18n Nesting error: The migration would result in invalid i18n nesting for ` +
`${fileName}. Element with i18n attribute "${p.name}" would result having a child of ` +
`element with i18n attribute "${el.name}". Please fix and re-run the migration.`);
`element with i18n attribute "${el.name}". Please fix and re-run the migration.`,
);
}
}
}
@ -279,8 +336,10 @@ export function validateI18nStructure(parsed: ParseTreeResult, fileName: string)
}
function isChildOf(parent: Element, el: Element): boolean {
return parent.sourceSpan.start.offset < el.sourceSpan.start.offset &&
parent.sourceSpan.end.offset > el.sourceSpan.end.offset;
return (
parent.sourceSpan.start.offset < el.sourceSpan.start.offset &&
parent.sourceSpan.end.offset > el.sourceSpan.end.offset
);
}
/** Possible placeholders that can be generated by `getPlaceholder`. */
@ -293,7 +352,9 @@ export enum PlaceholderKind {
* Wraps a string in a placeholder that makes it easier to identify during replacement operations.
*/
export function getPlaceholder(
value: string, kind: PlaceholderKind = PlaceholderKind.Default): string {
value: string,
kind: PlaceholderKind = PlaceholderKind.Default,
): string {
const name = `<<<ɵɵngControlFlowMigration_${kind}ɵɵ>>>`;
return `___${name}${value}${name}___`;
}
@ -302,7 +363,9 @@ export function getPlaceholder(
* calculates the level of nesting of the items in the collector
*/
export function calculateNesting(
visitor: ElementCollector|TemplateCollector, hasLineBreaks: boolean): void {
visitor: ElementCollector | TemplateCollector,
hasLineBreaks: boolean,
): void {
// start from top of template
// loop through each element
let nestedQueue: number[] = [];
@ -323,7 +386,7 @@ export function calculateNesting(
}
function escapeRegExp(val: string) {
return val.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
return val.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/**
@ -337,7 +400,11 @@ export function hasLineBreaks(template: string): boolean {
* properly adjusts template offsets based on current nesting levels
*/
export function reduceNestingOffset(
el: ElementToMigrate, nestLevel: number, offset: number, postOffsets: number[]): number {
el: ElementToMigrate,
nestLevel: number,
offset: number,
postOffsets: number[],
): number {
if (el.nestCount <= nestLevel) {
const count = nestLevel - el.nestCount;
// reduced nesting, add postoffset
@ -373,7 +440,9 @@ export function getTemplates(template: string): Map<string, Template> {
}
export function updateTemplates(
template: string, templates: Map<string, Template>): Map<string, Template> {
template: string,
templates: Map<string, Template>,
): Map<string, Template> {
const updatedTemplates = getTemplates(template);
for (let [key, tmpl] of updatedTemplates) {
templates.set(key, tmpl);
@ -387,7 +456,9 @@ function wrapIntoI18nContainer(i18nAttr: Attribute, content: string) {
}
function generatei18nContainer(
i18nAttr: Attribute, middle: string): {start: string, middle: string, end: string} {
i18nAttr: Attribute,
middle: string,
): {start: string; middle: string; end: string} {
const i18n = i18nAttr.value === '' ? 'i18n' : `i18n="${i18nAttr.value}"`;
return {start: `<ng-container ${i18n}>`, middle, end: `</ng-container>`};
}
@ -395,7 +466,7 @@ function generatei18nContainer(
/**
* Counts, replaces, and removes any necessary ng-templates post control flow migration
*/
export function processNgTemplates(template: string): {migrated: string, err: Error|undefined} {
export function processNgTemplates(template: string): {migrated: string; err: Error | undefined} {
// count usage
try {
const templates = getTemplates(template);
@ -451,10 +522,14 @@ function replaceRemainingPlaceholders(template: string): string {
const placeholders = [...template.matchAll(replaceRegex)];
for (let ph of placeholders) {
const placeholder = ph[0];
const name =
placeholder.slice(placeholderStart.length, placeholder.length - placeholderEnd.length);
template =
template.replace(placeholder, `<ng-template [ngTemplateOutlet]="${name}"></ng-template>`);
const name = placeholder.slice(
placeholderStart.length,
placeholder.length - placeholderEnd.length,
);
template = template.replace(
placeholder,
`<ng-template [ngTemplateOutlet]="${name}"></ng-template>`,
);
}
return template;
}
@ -490,42 +565,49 @@ export function removeImports(template: string, node: ts.Node, file: AnalyzedFil
* retrieves the original block of text in the template for length comparison during migration
* processing
*/
export function getOriginals(etm: ElementToMigrate, tmpl: string, offset: number):
{start: string, end: string, childLength: number, children: string[], childNodes: Node[]} {
export function getOriginals(
etm: ElementToMigrate,
tmpl: string,
offset: number,
): {start: string; end: string; childLength: number; children: string[]; childNodes: Node[]} {
// original opening block
if (etm.el.children.length > 0) {
const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
const start = tmpl.slice(
etm.el.sourceSpan.start.offset - offset,
etm.el.children[0].sourceSpan.start.offset - offset);
etm.el.sourceSpan.start.offset - offset,
etm.el.children[0].sourceSpan.start.offset - offset,
);
// original closing block
const end = tmpl.slice(
etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset,
etm.el.sourceSpan.end.offset - offset);
etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset,
etm.el.sourceSpan.end.offset - offset,
);
const childLength = childEnd - childStart;
return {
start,
end,
childLength,
children: getOriginalChildren(etm.el.children, tmpl, offset),
childNodes: etm.el.children
childNodes: etm.el.children,
};
}
// self closing or no children
const start =
tmpl.slice(etm.el.sourceSpan.start.offset - offset, etm.el.sourceSpan.end.offset - offset);
const start = tmpl.slice(
etm.el.sourceSpan.start.offset - offset,
etm.el.sourceSpan.end.offset - offset,
);
// original closing block
return {start, end: '', childLength: 0, children: [], childNodes: []};
}
function getOriginalChildren(children: Node[], tmpl: string, offset: number) {
return children.map(child => {
return children.map((child) => {
return tmpl.slice(child.sourceSpan.start.offset - offset, child.sourceSpan.end.offset - offset);
});
}
function isI18nTemplate(etm: ElementToMigrate, i18nAttr: Attribute|undefined): boolean {
function isI18nTemplate(etm: ElementToMigrate, i18nAttr: Attribute | undefined): boolean {
let attrCount = countAttributes(etm);
const safeToRemove = etm.el.attrs.length === attrCount + (i18nAttr !== undefined ? 1 : 0);
return etm.el.name === 'ng-template' && i18nAttr !== undefined && safeToRemove;
@ -555,9 +637,12 @@ function countAttributes(etm: ElementToMigrate): number {
/**
* builds the proper contents of what goes inside a given control flow block after migration
*/
export function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number):
{start: string, middle: string, end: string} {
const i18nAttr = etm.el.attrs.find(x => x.name === 'i18n');
export function getMainBlock(
etm: ElementToMigrate,
tmpl: string,
offset: number,
): {start: string; middle: string; end: string} {
const i18nAttr = etm.el.attrs.find((x) => x.name === 'i18n');
// removable containers are ng-templates or ng-containers that no longer need to exist
// post migration
@ -584,8 +669,9 @@ export function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number
const valEnd = etm.getValueEnd(offset);
// the index of the children start and end span, if they exist. Otherwise use the value end.
const {childStart, childEnd} =
etm.hasChildren() ? etm.getChildSpan(offset) : {childStart: valEnd, childEnd: valEnd};
const {childStart, childEnd} = etm.hasChildren()
? etm.getChildSpan(offset)
: {childStart: valEnd, childEnd: valEnd};
// the beginning of the updated string in the main block, for example: <div some="attributes">
let start = tmpl.slice(etm.start(offset), attrStart) + tmpl.slice(valEnd, childStart);
@ -627,8 +713,13 @@ function generateI18nMarkers(tmpl: string): string {
function addI18nMarkers(tmpl: string, el: Element, offset: number): string {
const startPos = el.children[0].sourceSpan.start.offset + offset;
const endPos = el.children[el.children.length - 1].sourceSpan.end.offset + offset;
return tmpl.slice(0, startPos) + startI18nMarker + tmpl.slice(startPos, endPos) + endI18nMarker +
tmpl.slice(endPos);
return (
tmpl.slice(0, startPos) +
startI18nMarker +
tmpl.slice(startPos, endPos) +
endI18nMarker +
tmpl.slice(endPos)
);
}
const selfClosingList = 'input|br|img|base|wbr|area|col|embed|hr|link|meta|param|source|track';
@ -706,18 +797,25 @@ export function formatTemplate(tmpl: string, templateType: string): string {
let isDoubleQuotes = false;
for (let [index, line] of lines.entries()) {
depth +=
[...line.matchAll(startMarkerRegex)].length - [...line.matchAll(endMarkerRegex)].length;
[...line.matchAll(startMarkerRegex)].length - [...line.matchAll(endMarkerRegex)].length;
inMigratedBlock = depth > 0;
i18nDepth += [...line.matchAll(startI18nMarkerRegex)].length -
[...line.matchAll(endI18nMarkerRegex)].length;
i18nDepth +=
[...line.matchAll(startI18nMarkerRegex)].length -
[...line.matchAll(endI18nMarkerRegex)].length;
let lineWasMigrated = false;
if (line.match(replaceMarkerRegex)) {
line = line.replace(replaceMarkerRegex, '');
lineWasMigrated = true;
}
if ((line.trim() === '' && index !== 0 && index !== lines.length - 1) &&
(inMigratedBlock || lineWasMigrated) && !inI18nBlock && !inAttribute) {
if (
line.trim() === '' &&
index !== 0 &&
index !== lines.length - 1 &&
(inMigratedBlock || lineWasMigrated) &&
!inI18nBlock &&
!inAttribute
) {
// skip blank lines except if it's the first line or last line
// this preserves leading and trailing spaces if they are already present
continue;
@ -726,15 +824,18 @@ export function formatTemplate(tmpl: string, templateType: string): string {
if (templateType === 'template' && index <= 1) {
// first real line of an inline template
const ind = line.search(/\S/);
mindent = (ind > -1) ? line.slice(0, ind) : '';
mindent = ind > -1 ? line.slice(0, ind) : '';
}
// if a block closes, an element closes, and it's not an element on a single line or the end
// of a self closing tag
if ((closeBlockRegex.test(line) ||
(closeElRegex.test(line) &&
(!singleLineElRegex.test(line) && !closeMultiLineElRegex.test(line)))) &&
indent !== '') {
if (
(closeBlockRegex.test(line) ||
(closeElRegex.test(line) &&
!singleLineElRegex.test(line) &&
!closeMultiLineElRegex.test(line))) &&
indent !== ''
) {
// close block, reduce indent
indent = indent.slice(2);
}
@ -750,14 +851,18 @@ export function formatTemplate(tmpl: string, templateType: string): string {
isDoubleQuotes = false;
}
const newLine = (inI18nBlock || inAttribute) ?
line :
mindent + (line.trim() !== '' ? indent : '') + line.trim();
const newLine =
inI18nBlock || inAttribute
? line
: mindent + (line.trim() !== '' ? indent : '') + line.trim();
formatted.push(newLine);
if (!isOpenDoubleAttr && !isOpenSingleAttr &&
((inAttribute && isDoubleQuotes && closeAttrDoubleRegex.test(line)) ||
(inAttribute && !isDoubleQuotes && closeAttrSingleRegex.test(line)))) {
if (
!isOpenDoubleAttr &&
!isOpenSingleAttr &&
((inAttribute && isDoubleQuotes && closeAttrDoubleRegex.test(line)) ||
(inAttribute && !isDoubleQuotes && closeAttrSingleRegex.test(line)))
) {
inAttribute = false;
}
@ -778,8 +883,12 @@ export function formatTemplate(tmpl: string, templateType: string): string {
// this matches an open control flow block, an open HTML element, but excludes single line
// self closing tags
if ((openBlockRegex.test(line) || openElRegex.test(line)) && !singleLineElRegex.test(line) &&
!selfClosingRegex.test(line) && !openSelfClosingRegex.test(line)) {
if (
(openBlockRegex.test(line) || openElRegex.test(line)) &&
!singleLineElRegex.test(line) &&
!selfClosingRegex.test(line) &&
!openSelfClosingRegex.test(line)
) {
// open block, increase indent
indent += ' ';
}
@ -799,7 +908,9 @@ export function formatTemplate(tmpl: string, templateType: string): string {
/** Executes a callback on each class declaration in a file. */
function forEachClass(
sourceFile: ts.SourceFile, callback: (node: ts.ClassDeclaration|ts.ImportDeclaration) => void) {
sourceFile: ts.SourceFile,
callback: (node: ts.ClassDeclaration | ts.ImportDeclaration) => void,
) {
sourceFile.forEachChild(function walk(node) {
if (ts.isClassDeclaration(node) || ts.isImportDeclaration(node)) {
callback(node);

View file

@ -32,7 +32,7 @@ interface Options {
mode: MigrationMode;
}
export default function(options: Options): Rule {
export default function (options: Options): Rule {
return async (tree, context) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
const basePath = process.cwd();
@ -44,7 +44,8 @@ export default function(options: Options): Rule {
if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot run the standalone migration.');
'Could not find any tsconfig file. Cannot run the standalone migration.',
);
}
for (const tsconfigPath of allPaths) {
@ -52,69 +53,109 @@ export default function(options: Options): Rule {
}
if (migratedFiles === 0) {
throw new SchematicsException(`Could not find any files to migrate under the path ${
pathToMigrate}. Cannot run the standalone migration.`);
throw new SchematicsException(
`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the standalone migration.`,
);
}
context.logger.info('🎉 Automated migration step has finished! 🎉');
context.logger.info(
'IMPORTANT! Please verify manually that your application builds and behaves as expected.');
'IMPORTANT! Please verify manually that your application builds and behaves as expected.',
);
context.logger.info(
`See https://angular.dev/reference/migrations/standalone for more information.`);
`See https://angular.dev/reference/migrations/standalone for more information.`,
);
};
}
function standaloneMigration(
tree: Tree, tsconfigPath: string, basePath: string, pathToMigrate: string,
schematicOptions: Options, oldProgram?: NgtscProgram): number {
tree: Tree,
tsconfigPath: string,
basePath: string,
pathToMigrate: string,
schematicOptions: Options,
oldProgram?: NgtscProgram,
): number {
if (schematicOptions.path.startsWith('..')) {
throw new SchematicsException(
'Cannot run standalone migration outside of the current project.');
'Cannot run standalone migration outside of the current project.',
);
}
const {host, options, rootNames} = createProgramOptions(
tree, tsconfigPath, basePath, undefined, undefined,
{
_enableTemplateTypeChecker: true, // Required for the template type checker to work.
compileNonExportedClasses: true, // We want to migrate non-exported classes too.
// Avoid checking libraries to speed up the migration.
skipLibCheck: true,
skipDefaultLibCheck: true,
});
tree,
tsconfigPath,
basePath,
undefined,
undefined,
{
_enableTemplateTypeChecker: true, // Required for the template type checker to work.
compileNonExportedClasses: true, // We want to migrate non-exported classes too.
// Avoid checking libraries to speed up the migration.
skipLibCheck: true,
skipDefaultLibCheck: true,
},
);
const referenceLookupExcludedFiles = /node_modules|\.ngtypecheck\.ts/;
const program = createProgram({rootNames, host, options, oldProgram}) as NgtscProgram;
const printer = ts.createPrinter();
if (existsSync(pathToMigrate) && !statSync(pathToMigrate).isDirectory()) {
throw new SchematicsException(`Migration path ${
pathToMigrate} has to be a directory. Cannot run the standalone migration.`);
throw new SchematicsException(
`Migration path ${pathToMigrate} has to be a directory. Cannot run the standalone migration.`,
);
}
const sourceFiles = program.getTsProgram().getSourceFiles().filter(
sourceFile => sourceFile.fileName.startsWith(pathToMigrate) &&
canMigrateFile(basePath, sourceFile, program.getTsProgram()));
const sourceFiles = program
.getTsProgram()
.getSourceFiles()
.filter(
(sourceFile) =>
sourceFile.fileName.startsWith(pathToMigrate) &&
canMigrateFile(basePath, sourceFile, program.getTsProgram()),
);
if (sourceFiles.length === 0) {
return 0;
}
let pendingChanges: ChangesByFile;
let filesToRemove: Set<ts.SourceFile>|null = null;
let filesToRemove: Set<ts.SourceFile> | null = null;
if (schematicOptions.mode === MigrationMode.pruneModules) {
const result = pruneNgModules(
program, host, basePath, rootNames, sourceFiles, printer, undefined,
referenceLookupExcludedFiles);
program,
host,
basePath,
rootNames,
sourceFiles,
printer,
undefined,
referenceLookupExcludedFiles,
);
pendingChanges = result.pendingChanges;
filesToRemove = result.filesToRemove;
} else if (schematicOptions.mode === MigrationMode.standaloneBootstrap) {
pendingChanges = toStandaloneBootstrap(
program, host, basePath, rootNames, sourceFiles, printer, undefined,
referenceLookupExcludedFiles, knownInternalAliasRemapper);
program,
host,
basePath,
rootNames,
sourceFiles,
printer,
undefined,
referenceLookupExcludedFiles,
knownInternalAliasRemapper,
);
} else {
// This shouldn't happen, but default to `MigrationMode.toStandalone` just in case.
pendingChanges =
toStandalone(sourceFiles, program, printer, undefined, knownInternalAliasRemapper);
pendingChanges = toStandalone(
sourceFiles,
program,
printer,
undefined,
knownInternalAliasRemapper,
);
}
for (const [file, changes] of pendingChanges.entries()) {
@ -125,7 +166,7 @@ function standaloneMigration(
const update = tree.beginUpdate(relative(basePath, file.fileName));
changes.forEach(change => {
changes.forEach((change) => {
if (change.removeLength != null) {
update.remove(change.start, change.removeLength);
}
@ -145,10 +186,16 @@ function standaloneMigration(
// Note that we can't run the module pruning internally without propagating the changes to disk,
// because there may be conflicting AST node changes.
if (schematicOptions.mode === MigrationMode.standaloneBootstrap) {
return standaloneMigration(
tree, tsconfigPath, basePath, pathToMigrate,
{...schematicOptions, mode: MigrationMode.pruneModules}, program) +
sourceFiles.length;
return (
standaloneMigration(
tree,
tsconfigPath,
basePath,
pathToMigrate,
{...schematicOptions, mode: MigrationMode.pruneModules},
program,
) + sourceFiles.length
);
}
return sourceFiles.length;

View file

@ -13,7 +13,14 @@ import {ChangeTracker, ImportRemapper} from '../../utils/change_tracker';
import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators';
import {closestNode} from '../../utils/typescript/nodes';
import {findClassDeclaration, findLiteralProperty, getNodeLookup, offsetsToNodes, ReferenceResolver, UniqueItemTracker} from './util';
import {
findClassDeclaration,
findLiteralProperty,
getNodeLookup,
offsetsToNodes,
ReferenceResolver,
UniqueItemTracker,
} from './util';
/** Keeps track of the places from which we need to remove AST nodes. */
interface RemovalLocations {
@ -24,20 +31,31 @@ interface RemovalLocations {
}
export function pruneNgModules(
program: NgtscProgram, host: ts.CompilerHost, basePath: string, rootFileNames: string[],
sourceFiles: ts.SourceFile[], printer: ts.Printer, importRemapper?: ImportRemapper,
referenceLookupExcludedFiles?: RegExp) {
program: NgtscProgram,
host: ts.CompilerHost,
basePath: string,
rootFileNames: string[],
sourceFiles: ts.SourceFile[],
printer: ts.Printer,
importRemapper?: ImportRemapper,
referenceLookupExcludedFiles?: RegExp,
) {
const filesToRemove = new Set<ts.SourceFile>();
const tracker = new ChangeTracker(printer, importRemapper);
const tsProgram = program.getTsProgram();
const typeChecker = tsProgram.getTypeChecker();
const referenceResolver =
new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
const referenceResolver = new ReferenceResolver(
program,
host,
rootFileNames,
basePath,
referenceLookupExcludedFiles,
);
const removalLocations: RemovalLocations = {
arrays: new UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>(),
imports: new UniqueItemTracker<ts.NamedImports, ts.Node>(),
exports: new UniqueItemTracker<ts.NamedExports, ts.Node>(),
unknown: new Set<ts.Node>()
unknown: new Set<ts.Node>(),
};
const classesToRemove = new Set<ts.ClassDeclaration>();
const barrelExports = new UniqueItemTracker<ts.SourceFile, ts.ExportDeclaration>();
@ -48,10 +66,15 @@ export function pruneNgModules(
collectRemovalLocations(node, removalLocations, referenceResolver, program);
classesToRemove.add(node);
} else if (
ts.isExportDeclaration(node) && !node.exportClause && node.moduleSpecifier &&
ts.isStringLiteralLike(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith('.')) {
const exportedSourceFile =
typeChecker.getSymbolAtLocation(node.moduleSpecifier)?.valueDeclaration?.getSourceFile();
ts.isExportDeclaration(node) &&
!node.exportClause &&
node.moduleSpecifier &&
ts.isStringLiteralLike(node.moduleSpecifier) &&
node.moduleSpecifier.text.startsWith('.')
) {
const exportedSourceFile = typeChecker
.getSymbolAtLocation(node.moduleSpecifier)
?.valueDeclaration?.getSourceFile();
if (exportedSourceFile) {
barrelExports.track(exportedSourceFile, node);
@ -105,8 +128,11 @@ export function pruneNgModules(
* @param program
*/
function collectRemovalLocations(
ngModule: ts.ClassDeclaration, removalLocations: RemovalLocations,
referenceResolver: ReferenceResolver, program: NgtscProgram) {
ngModule: ts.ClassDeclaration,
removalLocations: RemovalLocations,
referenceResolver: ReferenceResolver,
program: NgtscProgram,
) {
const refsByFile = referenceResolver.findReferencesInProject(ngModule.name!);
const tsProgram = program.getTsProgram();
const nodes = new Set<ts.Node>();
@ -148,14 +174,18 @@ function collectRemovalLocations(
* @param tracker Tracker in which to register the changes.
*/
function removeArrayReferences(
locations: UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>,
tracker: ChangeTracker): void {
locations: UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>,
tracker: ChangeTracker,
): void {
for (const [array, toRemove] of locations.getEntries()) {
const newElements = filterRemovedElements(array.elements, toRemove);
tracker.replaceNode(
array,
ts.factory.updateArrayLiteralExpression(
array,
ts.factory.updateArrayLiteralExpression(
array, ts.factory.createNodeArray(newElements, array.elements.hasTrailingComma)));
ts.factory.createNodeArray(newElements, array.elements.hasTrailingComma),
),
);
}
}
@ -165,7 +195,9 @@ function removeArrayReferences(
* @param tracker Tracker in which to register the changes.
*/
function removeImportReferences(
locations: UniqueItemTracker<ts.NamedImports, ts.Node>, tracker: ChangeTracker) {
locations: UniqueItemTracker<ts.NamedImports, ts.Node>,
tracker: ChangeTracker,
) {
for (const [namedImports, toRemove] of locations.getEntries()) {
const newElements = filterRemovedElements(namedImports.elements, toRemove);
@ -177,9 +209,14 @@ function removeImportReferences(
// e.g. `import Foo, {ModuleToRemove} from './foo';` becomes `import Foo from './foo';`.
if (importClause && importClause.name) {
tracker.replaceNode(
importClause,
ts.factory.updateImportClause(
importClause,
ts.factory.updateImportClause(
importClause, importClause.isTypeOnly, importClause.name, undefined));
importClause.isTypeOnly,
importClause.name,
undefined,
),
);
} else {
// Otherwise we can drop the entire declaration.
const declaration = closestNode(namedImports, ts.isImportDeclaration);
@ -201,7 +238,9 @@ function removeImportReferences(
* @param tracker Tracker in which to register the changes.
*/
function removeExportReferences(
locations: UniqueItemTracker<ts.NamedExports, ts.Node>, tracker: ChangeTracker) {
locations: UniqueItemTracker<ts.NamedExports, ts.Node>,
tracker: ChangeTracker,
) {
for (const [namedExports, toRemove] of locations.getEntries()) {
const newElements = filterRemovedElements(namedExports.elements, toRemove);
@ -238,14 +277,16 @@ function canRemoveClass(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker):
}
// Unsupported case, e.g. `@NgModule(SOME_VALUE)`.
if (decorator.expression.arguments.length > 0 &&
!ts.isObjectLiteralExpression(decorator.expression.arguments[0])) {
if (
decorator.expression.arguments.length > 0 &&
!ts.isObjectLiteralExpression(decorator.expression.arguments[0])
) {
return false;
}
// We can't remove modules that have class members. We make an exception for an
// empty constructor which may have been generated by a tool and forgotten.
if (node.members.length > 0 && node.members.some(member => !isEmptyConstructor(member))) {
if (node.members.length > 0 && node.members.some((member) => !isEmptyConstructor(member))) {
return false;
}
@ -266,13 +307,17 @@ function canRemoveClass(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker):
}
const depDeclaration = findClassDeclaration(dep, typeChecker);
const depNgModule =
depDeclaration ? findNgModuleDecorator(depDeclaration, typeChecker) : null;
const depNgModule = depDeclaration
? findNgModuleDecorator(depDeclaration, typeChecker)
: null;
// If any of the dependencies of the class is an `NgModule` that can't be removed, the class
// itself can't be removed either, because it may be part of a transitive dependency chain.
if (depDeclaration !== null && depNgModule !== null &&
!canRemoveClass(depDeclaration, typeChecker)) {
if (
depDeclaration !== null &&
depNgModule !== null &&
!canRemoveClass(depDeclaration, typeChecker)
) {
return false;
}
}
@ -282,9 +327,12 @@ function canRemoveClass(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker):
// Also err on the side of caution and don't remove modules where any of the aforementioned
// properties aren't initialized to an array literal.
for (const prop of literal.properties) {
if (isNonEmptyNgModuleProperty(prop) &&
(prop.name.text === 'declarations' || prop.name.text === 'providers' ||
prop.name.text === 'bootstrap')) {
if (
isNonEmptyNgModuleProperty(prop) &&
(prop.name.text === 'declarations' ||
prop.name.text === 'providers' ||
prop.name.text === 'bootstrap')
) {
return false;
}
}
@ -298,10 +346,15 @@ function canRemoveClass(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker):
* element.
* @param node Node to be checked.
*/
function isNonEmptyNgModuleProperty(node: ts.Node): node is ts.PropertyAssignment&
{name: ts.Identifier, initializer: ts.ArrayLiteralExpression} {
return ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) &&
ts.isArrayLiteralExpression(node.initializer) && node.initializer.elements.length > 0;
function isNonEmptyNgModuleProperty(
node: ts.Node,
): node is ts.PropertyAssignment & {name: ts.Identifier; initializer: ts.ArrayLiteralExpression} {
return (
ts.isPropertyAssignment(node) &&
ts.isIdentifier(node.name) &&
ts.isArrayLiteralExpression(node.initializer) &&
node.initializer.elements.length > 0
);
}
/**
@ -316,9 +369,11 @@ function canRemoveFile(sourceFile: ts.SourceFile, nodesToBeRemoved: Set<ts.Node>
continue;
}
if (ts.isExportDeclaration(node) ||
(ts.canHaveModifiers(node) &&
ts.getModifiers(node)?.some(m => m.kind === ts.SyntaxKind.ExportKeyword))) {
if (
ts.isExportDeclaration(node) ||
(ts.canHaveModifiers(node) &&
ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
) {
return false;
}
}
@ -332,9 +387,12 @@ function canRemoveFile(sourceFile: ts.SourceFile, nodesToBeRemoved: Set<ts.Node>
* @param child Child node that is being checked.
*/
function contains(parent: ts.Node, child: ts.Node): boolean {
return parent === child ||
(parent.getSourceFile().fileName === child.getSourceFile().fileName &&
child.getStart() >= parent.getStart() && child.getStart() <= parent.getEnd());
return (
parent === child ||
(parent.getSourceFile().fileName === child.getSourceFile().fileName &&
child.getStart() >= parent.getStart() &&
child.getStart() <= parent.getEnd())
);
}
/**
@ -343,8 +401,10 @@ function contains(parent: ts.Node, child: ts.Node): boolean {
* @param toRemove Nodes that should be removed.
*/
function filterRemovedElements<T extends ts.Node>(
elements: ts.NodeArray<T>, toRemove: Set<ts.Node>): T[] {
return elements.filter(el => {
elements: ts.NodeArray<T>,
toRemove: Set<ts.Node>,
): T[] {
return elements.filter((el) => {
for (const node of toRemove) {
// Check that the element contains the node, despite knowing with relative certainty that it
// does, because this allows us to unwrap some nodes. E.g. if we have `[((toRemove))]`, we
@ -359,8 +419,11 @@ function filterRemovedElements<T extends ts.Node>(
/** Returns whether a node as an empty constructor. */
function isEmptyConstructor(node: ts.Node): boolean {
return ts.isConstructorDeclaration(node) && node.parameters.length === 0 &&
(node.body == null || node.body.statements.length === 0);
return (
ts.isConstructorDeclaration(node) &&
node.parameters.length === 0 &&
(node.body == null || node.body.statements.length === 0)
);
}
/**
@ -376,14 +439,18 @@ function addRemovalTodos(nodes: Set<ts.Node>, tracker: ChangeTracker) {
// the same node. In practice it is unlikely, because the second time the node won't be picked
// up by the language service as a reference, because the class won't exist anymore.
tracker.insertText(
node.getSourceFile(), node.getFullStart(),
` /* TODO(standalone-migration): clean up removed NgModule reference manually. */ `);
node.getSourceFile(),
node.getFullStart(),
` /* TODO(standalone-migration): clean up removed NgModule reference manually. */ `,
);
}
}
/** Finds the `NgModule` decorator in a class, if it exists. */
function findNgModuleDecorator(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): NgDecorator|
null {
function findNgModuleDecorator(
node: ts.ClassDeclaration,
typeChecker: ts.TypeChecker,
): NgDecorator | null {
const decorators = getAngularDecorators(typeChecker, ts.getDecorators(node) || []);
return decorators.find(decorator => decorator.name === 'NgModule') || null;
return decorators.find((decorator) => decorator.name === 'NgModule') || null;
}

View file

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import {NgtscProgram} from '@angular/compiler-cli';
import {TemplateTypeChecker} from '@angular/compiler-cli/private/migrations';
import {dirname, join} from 'path';
@ -16,8 +15,26 @@ import {ChangeTracker, ImportRemapper} from '../../utils/change_tracker';
import {getAngularDecorators} from '../../utils/ng_decorators';
import {closestNode} from '../../utils/typescript/nodes';
import {ComponentImportsRemapper, convertNgModuleDeclarationToStandalone, extractDeclarationsFromModule, findTestObjectsToMigrate, migrateTestDeclarations} from './to-standalone';
import {closestOrSelf, findClassDeclaration, findLiteralProperty, getNodeLookup, getRelativeImportPath, isClassReferenceInAngularModule, NamedClassDeclaration, NodeLookup, offsetsToNodes, ReferenceResolver, UniqueItemTracker} from './util';
import {
ComponentImportsRemapper,
convertNgModuleDeclarationToStandalone,
extractDeclarationsFromModule,
findTestObjectsToMigrate,
migrateTestDeclarations,
} from './to-standalone';
import {
closestOrSelf,
findClassDeclaration,
findLiteralProperty,
getNodeLookup,
getRelativeImportPath,
isClassReferenceInAngularModule,
NamedClassDeclaration,
NodeLookup,
offsetsToNodes,
ReferenceResolver,
UniqueItemTracker,
} from './util';
/** Information extracted from a `bootstrapModule` call necessary to migrate it. */
interface BootstrapCallAnalysis {
@ -34,29 +51,44 @@ interface BootstrapCallAnalysis {
}
export function toStandaloneBootstrap(
program: NgtscProgram, host: ts.CompilerHost, basePath: string, rootFileNames: string[],
sourceFiles: ts.SourceFile[], printer: ts.Printer, importRemapper?: ImportRemapper,
referenceLookupExcludedFiles?: RegExp, componentImportRemapper?: ComponentImportsRemapper) {
program: NgtscProgram,
host: ts.CompilerHost,
basePath: string,
rootFileNames: string[],
sourceFiles: ts.SourceFile[],
printer: ts.Printer,
importRemapper?: ImportRemapper,
referenceLookupExcludedFiles?: RegExp,
componentImportRemapper?: ComponentImportsRemapper,
) {
const tracker = new ChangeTracker(printer, importRemapper);
const typeChecker = program.getTsProgram().getTypeChecker();
const templateTypeChecker = program.compiler.getTemplateTypeChecker();
const referenceResolver =
new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
const referenceResolver = new ReferenceResolver(
program,
host,
rootFileNames,
basePath,
referenceLookupExcludedFiles,
);
const bootstrapCalls: BootstrapCallAnalysis[] = [];
const testObjects = new Set<ts.ObjectLiteralExpression>();
const allDeclarations = new Set<ts.ClassDeclaration>();
// `bootstrapApplication` doesn't include Protractor support by default
// anymore so we have to opt the app in, if we detect it being used.
const additionalProviders = hasImport(program, rootFileNames, 'protractor') ?
new Map([['provideProtractorTestingSupport', '@angular/platform-browser']]) :
null;
const additionalProviders = hasImport(program, rootFileNames, 'protractor')
? new Map([['provideProtractorTestingSupport', '@angular/platform-browser']])
: null;
for (const sourceFile of sourceFiles) {
sourceFile.forEachChild(function walk(node) {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'bootstrapModule' &&
isClassReferenceInAngularModule(node.expression, 'PlatformRef', 'core', typeChecker)) {
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'bootstrapModule' &&
isClassReferenceInAngularModule(node.expression, 'PlatformRef', 'core', typeChecker)
) {
const call = analyzeBootstrapCall(node, typeChecker, templateTypeChecker);
if (call) {
@ -66,20 +98,31 @@ export function toStandaloneBootstrap(
node.forEachChild(walk);
});
findTestObjectsToMigrate(sourceFile, typeChecker).forEach(obj => testObjects.add(obj));
findTestObjectsToMigrate(sourceFile, typeChecker).forEach((obj) => testObjects.add(obj));
}
for (const call of bootstrapCalls) {
call.declarations.forEach(decl => allDeclarations.add(decl));
call.declarations.forEach((decl) => allDeclarations.add(decl));
migrateBootstrapCall(
call, tracker, additionalProviders, referenceResolver, typeChecker, printer);
call,
tracker,
additionalProviders,
referenceResolver,
typeChecker,
printer,
);
}
// The previous migrations explicitly skip over bootstrapped
// declarations so we have to migrate them now.
for (const declaration of allDeclarations) {
convertNgModuleDeclarationToStandalone(
declaration, allDeclarations, tracker, templateTypeChecker, componentImportRemapper);
declaration,
allDeclarations,
tracker,
templateTypeChecker,
componentImportRemapper,
);
}
migrateTestDeclarations(testObjects, allDeclarations, tracker, templateTypeChecker, typeChecker);
@ -94,8 +137,10 @@ export function toStandaloneBootstrap(
* @param templateTypeChecker
*/
function analyzeBootstrapCall(
call: ts.CallExpression, typeChecker: ts.TypeChecker,
templateTypeChecker: TemplateTypeChecker): BootstrapCallAnalysis|null {
call: ts.CallExpression,
typeChecker: ts.TypeChecker,
templateTypeChecker: TemplateTypeChecker,
): BootstrapCallAnalysis | null {
if (call.arguments.length === 0 || !ts.isIdentifier(call.arguments[0])) {
return null;
}
@ -106,21 +151,28 @@ function analyzeBootstrapCall(
return null;
}
const decorator = getAngularDecorators(typeChecker, ts.getDecorators(declaration) || [])
.find(decorator => decorator.name === 'NgModule');
const decorator = getAngularDecorators(typeChecker, ts.getDecorators(declaration) || []).find(
(decorator) => decorator.name === 'NgModule',
);
if (!decorator || decorator.node.expression.arguments.length === 0 ||
!ts.isObjectLiteralExpression(decorator.node.expression.arguments[0])) {
if (
!decorator ||
decorator.node.expression.arguments.length === 0 ||
!ts.isObjectLiteralExpression(decorator.node.expression.arguments[0])
) {
return null;
}
const metadata = decorator.node.expression.arguments[0];
const bootstrapProp = findLiteralProperty(metadata, 'bootstrap');
if (!bootstrapProp || !ts.isPropertyAssignment(bootstrapProp) ||
!ts.isArrayLiteralExpression(bootstrapProp.initializer) ||
bootstrapProp.initializer.elements.length === 0 ||
!ts.isIdentifier(bootstrapProp.initializer.elements[0])) {
if (
!bootstrapProp ||
!ts.isPropertyAssignment(bootstrapProp) ||
!ts.isArrayLiteralExpression(bootstrapProp.initializer) ||
bootstrapProp.initializer.elements.length === 0 ||
!ts.isIdentifier(bootstrapProp.initializer.elements[0])
) {
return null;
}
@ -132,7 +184,7 @@ function analyzeBootstrapCall(
metadata,
component: component as NamedClassDeclaration,
call,
declarations: extractDeclarationsFromModule(declaration, templateTypeChecker)
declarations: extractDeclarationsFromModule(declaration, templateTypeChecker),
};
}
@ -150,9 +202,13 @@ function analyzeBootstrapCall(
* @param printer
*/
function migrateBootstrapCall(
analysis: BootstrapCallAnalysis, tracker: ChangeTracker,
additionalProviders: Map<string, string>|null, referenceResolver: ReferenceResolver,
typeChecker: ts.TypeChecker, printer: ts.Printer) {
analysis: BootstrapCallAnalysis,
tracker: ChangeTracker,
additionalProviders: Map<string, string> | null,
referenceResolver: ReferenceResolver,
typeChecker: ts.TypeChecker,
printer: ts.Printer,
) {
const sourceFile = analysis.call.getSourceFile();
const moduleSourceFile = analysis.metadata.getSourceFile();
const providers = findLiteralProperty(analysis.metadata, 'providers');
@ -160,13 +216,15 @@ function migrateBootstrapCall(
const nodesToCopy = new Set<ts.Node>();
const providersInNewCall: ts.Expression[] = [];
const moduleImportsInNewCall: ts.Expression[] = [];
let nodeLookup: NodeLookup|null = null;
let nodeLookup: NodeLookup | null = null;
// Comment out the metadata so that it'll be removed when we run the module pruning afterwards.
// If the pruning is left for some reason, the user will still have an actionable TODO.
tracker.insertText(
moduleSourceFile, analysis.metadata.getStart(),
'/* TODO(standalone-migration): clean up removed NgModule class manually. \n');
moduleSourceFile,
analysis.metadata.getStart(),
'/* TODO(standalone-migration): clean up removed NgModule class manually. \n',
);
tracker.insertText(moduleSourceFile, analysis.metadata.getEnd(), ' */');
if (providers && ts.isPropertyAssignment(providers)) {
@ -184,20 +242,33 @@ function migrateBootstrapCall(
if (imports && ts.isPropertyAssignment(imports)) {
nodeLookup = nodeLookup || getNodeLookup(moduleSourceFile);
migrateImportsForBootstrapCall(
sourceFile, imports, nodeLookup, moduleImportsInNewCall, providersInNewCall, tracker,
nodesToCopy, referenceResolver, typeChecker);
sourceFile,
imports,
nodeLookup,
moduleImportsInNewCall,
providersInNewCall,
tracker,
nodesToCopy,
referenceResolver,
typeChecker,
);
}
if (additionalProviders) {
additionalProviders.forEach((moduleSpecifier, name) => {
providersInNewCall.push(ts.factory.createCallExpression(
tracker.addImport(sourceFile, name, moduleSpecifier), undefined, undefined));
providersInNewCall.push(
ts.factory.createCallExpression(
tracker.addImport(sourceFile, name, moduleSpecifier),
undefined,
undefined,
),
);
});
}
if (nodesToCopy.size > 0) {
let text = '\n\n';
nodesToCopy.forEach(node => {
nodesToCopy.forEach((node) => {
const transformedNode = remapDynamicImports(sourceFile.fileName, node);
// Use `getText` to try an preserve the original formatting. This only works if the node
@ -223,45 +294,66 @@ function migrateBootstrapCall(
* @param tracker Object keeping track of the changes to the different files.
*/
function replaceBootstrapCallExpression(
analysis: BootstrapCallAnalysis, providers: ts.Expression[], modules: ts.Expression[],
tracker: ChangeTracker): void {
analysis: BootstrapCallAnalysis,
providers: ts.Expression[],
modules: ts.Expression[],
tracker: ChangeTracker,
): void {
const sourceFile = analysis.call.getSourceFile();
const componentPath =
getRelativeImportPath(sourceFile.fileName, analysis.component.getSourceFile().fileName);
const componentPath = getRelativeImportPath(
sourceFile.fileName,
analysis.component.getSourceFile().fileName,
);
const args = [tracker.addImport(sourceFile, analysis.component.name.text, componentPath)];
const bootstrapExpression =
tracker.addImport(sourceFile, 'bootstrapApplication', '@angular/platform-browser');
const bootstrapExpression = tracker.addImport(
sourceFile,
'bootstrapApplication',
'@angular/platform-browser',
);
if (providers.length > 0 || modules.length > 0) {
const combinedProviders: ts.Expression[] = [];
if (modules.length > 0) {
const importProvidersExpression =
tracker.addImport(sourceFile, 'importProvidersFrom', '@angular/core');
const importProvidersExpression = tracker.addImport(
sourceFile,
'importProvidersFrom',
'@angular/core',
);
combinedProviders.push(
ts.factory.createCallExpression(importProvidersExpression, [], modules));
ts.factory.createCallExpression(importProvidersExpression, [], modules),
);
}
// Push the providers after `importProvidersFrom` call for better readability.
combinedProviders.push(...providers);
const providersArray = ts.factory.createNodeArray(
combinedProviders,
analysis.metadata.properties.hasTrailingComma && combinedProviders.length > 2);
combinedProviders,
analysis.metadata.properties.hasTrailingComma && combinedProviders.length > 2,
);
const initializer = remapDynamicImports(
sourceFile.fileName,
ts.factory.createArrayLiteralExpression(providersArray, combinedProviders.length > 1));
sourceFile.fileName,
ts.factory.createArrayLiteralExpression(providersArray, combinedProviders.length > 1),
);
args.push(ts.factory.createObjectLiteralExpression(
[ts.factory.createPropertyAssignment('providers', initializer)], true));
args.push(
ts.factory.createObjectLiteralExpression(
[ts.factory.createPropertyAssignment('providers', initializer)],
true,
),
);
}
tracker.replaceNode(
analysis.call, ts.factory.createCallExpression(bootstrapExpression, [], args),
// Note: it's important to pass in the source file that the nodes originated from!
// Otherwise TS won't print out literals inside of the providers that we're copying
// over from the module file.
undefined, analysis.metadata.getSourceFile());
analysis.call,
ts.factory.createCallExpression(bootstrapExpression, [], args),
// Note: it's important to pass in the source file that the nodes originated from!
// Otherwise TS won't print out literals inside of the providers that we're copying
// over from the module file.
undefined,
analysis.metadata.getSourceFile(),
);
}
/**
@ -278,10 +370,16 @@ function replaceBootstrapCallExpression(
* @param typeChecker
*/
function migrateImportsForBootstrapCall(
sourceFile: ts.SourceFile, imports: ts.PropertyAssignment, nodeLookup: NodeLookup,
importsForNewCall: ts.Expression[], providersInNewCall: ts.Expression[], tracker: ChangeTracker,
nodesToCopy: Set<ts.Node>, referenceResolver: ReferenceResolver,
typeChecker: ts.TypeChecker): void {
sourceFile: ts.SourceFile,
imports: ts.PropertyAssignment,
nodeLookup: NodeLookup,
importsForNewCall: ts.Expression[],
providersInNewCall: ts.Expression[],
tracker: ChangeTracker,
nodesToCopy: Set<ts.Node>,
referenceResolver: ReferenceResolver,
typeChecker: ts.TypeChecker,
): void {
if (!ts.isArrayLiteralExpression(imports.initializer)) {
importsForNewCall.push(imports.initializer);
return;
@ -289,21 +387,39 @@ function migrateImportsForBootstrapCall(
for (const element of imports.initializer.elements) {
// If the reference is to a `RouterModule.forRoot` call, we can try to migrate it.
if (ts.isCallExpression(element) && ts.isPropertyAccessExpression(element.expression) &&
element.arguments.length > 0 && element.expression.name.text === 'forRoot' &&
isClassReferenceInAngularModule(
element.expression.expression, 'RouterModule', 'router', typeChecker)) {
if (
ts.isCallExpression(element) &&
ts.isPropertyAccessExpression(element.expression) &&
element.arguments.length > 0 &&
element.expression.name.text === 'forRoot' &&
isClassReferenceInAngularModule(
element.expression.expression,
'RouterModule',
'router',
typeChecker,
)
) {
const options = element.arguments[1] as ts.Expression | undefined;
const features = options ? getRouterModuleForRootFeatures(sourceFile, options, tracker) : [];
// If the features come back as null, it means that the router
// has a configuration that can't be migrated automatically.
if (features !== null) {
providersInNewCall.push(ts.factory.createCallExpression(
tracker.addImport(sourceFile, 'provideRouter', '@angular/router'), [],
[element.arguments[0], ...features]));
providersInNewCall.push(
ts.factory.createCallExpression(
tracker.addImport(sourceFile, 'provideRouter', '@angular/router'),
[],
[element.arguments[0], ...features],
),
);
addNodesToCopy(
sourceFile, element.arguments[0], nodeLookup, tracker, nodesToCopy, referenceResolver);
sourceFile,
element.arguments[0],
nodeLookup,
tracker,
nodesToCopy,
referenceResolver,
);
if (options) {
addNodesToCopy(sourceFile, options, nodeLookup, tracker, nodesToCopy, referenceResolver);
}
@ -316,52 +432,85 @@ function migrateImportsForBootstrapCall(
const animationsModule = 'platform-browser/animations';
const animationsImport = `@angular/${animationsModule}`;
if (isClassReferenceInAngularModule(
element, 'BrowserAnimationsModule', animationsModule, typeChecker)) {
providersInNewCall.push(ts.factory.createCallExpression(
tracker.addImport(sourceFile, 'provideAnimations', animationsImport), [], []));
if (
isClassReferenceInAngularModule(
element,
'BrowserAnimationsModule',
animationsModule,
typeChecker,
)
) {
providersInNewCall.push(
ts.factory.createCallExpression(
tracker.addImport(sourceFile, 'provideAnimations', animationsImport),
[],
[],
),
);
continue;
}
// `NoopAnimationsModule` can be replaced with `provideNoopAnimations`.
if (isClassReferenceInAngularModule(
element, 'NoopAnimationsModule', animationsModule, typeChecker)) {
providersInNewCall.push(ts.factory.createCallExpression(
tracker.addImport(sourceFile, 'provideNoopAnimations', animationsImport), [], []));
if (
isClassReferenceInAngularModule(
element,
'NoopAnimationsModule',
animationsModule,
typeChecker,
)
) {
providersInNewCall.push(
ts.factory.createCallExpression(
tracker.addImport(sourceFile, 'provideNoopAnimations', animationsImport),
[],
[],
),
);
continue;
}
// `HttpClientModule` can be replaced with `provideHttpClient()`.
const httpClientModule = 'common/http';
const httpClientImport = `@angular/${httpClientModule}`;
if (isClassReferenceInAngularModule(
element, 'HttpClientModule', httpClientModule, typeChecker)) {
if (
isClassReferenceInAngularModule(element, 'HttpClientModule', httpClientModule, typeChecker)
) {
const callArgs = [
// we add `withInterceptorsFromDi()` to the call to ensure that class-based interceptors
// still work
ts.factory.createCallExpression(
tracker.addImport(sourceFile, 'withInterceptorsFromDi', httpClientImport), [], [])
tracker.addImport(sourceFile, 'withInterceptorsFromDi', httpClientImport),
[],
[],
),
];
providersInNewCall.push(ts.factory.createCallExpression(
tracker.addImport(sourceFile, 'provideHttpClient', httpClientImport), [], callArgs));
providersInNewCall.push(
ts.factory.createCallExpression(
tracker.addImport(sourceFile, 'provideHttpClient', httpClientImport),
[],
callArgs,
),
);
continue;
}
}
const target =
// If it's a call, it'll likely be a `ModuleWithProviders`
// expression so the target is going to be call's expression.
ts.isCallExpression(element) && ts.isPropertyAccessExpression(element.expression) ?
element.expression.expression :
element;
// If it's a call, it'll likely be a `ModuleWithProviders`
// expression so the target is going to be call's expression.
ts.isCallExpression(element) && ts.isPropertyAccessExpression(element.expression)
? element.expression.expression
: element;
const classDeclaration = findClassDeclaration(target, typeChecker);
const decorators = classDeclaration ?
getAngularDecorators(typeChecker, ts.getDecorators(classDeclaration) || []) :
undefined;
const decorators = classDeclaration
? getAngularDecorators(typeChecker, ts.getDecorators(classDeclaration) || [])
: undefined;
if (!decorators || decorators.length === 0 ||
decorators.every(
({name}) => name !== 'Directive' && name !== 'Component' && name !== 'Pipe')) {
if (
!decorators ||
decorators.length === 0 ||
decorators.every(({name}) => name !== 'Directive' && name !== 'Component' && name !== 'Pipe')
) {
importsForNewCall.push(element);
addNodesToCopy(sourceFile, element, nodeLookup, tracker, nodesToCopy, referenceResolver);
}
@ -377,8 +526,10 @@ function migrateImportsForBootstrapCall(
* @returns Null if the options can't be migrated, otherwise an array of call expressions.
*/
function getRouterModuleForRootFeatures(
sourceFile: ts.SourceFile, options: ts.Expression, tracker: ChangeTracker): ts.CallExpression[]|
null {
sourceFile: ts.SourceFile,
options: ts.Expression,
tracker: ChangeTracker,
): ts.CallExpression[] | null {
// Options that aren't a static object literal can't be migrated.
if (!ts.isObjectLiteralExpression(options)) {
return null;
@ -387,12 +538,14 @@ function getRouterModuleForRootFeatures(
const featureExpressions: ts.CallExpression[] = [];
const configOptions: ts.PropertyAssignment[] = [];
const inMemoryScrollingOptions: ts.PropertyAssignment[] = [];
const features = new UniqueItemTracker<string, ts.Expression|null>();
const features = new UniqueItemTracker<string, ts.Expression | null>();
for (const prop of options.properties) {
// We can't migrate options that we can't easily analyze.
if (!ts.isPropertyAssignment(prop) ||
(!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name))) {
if (
!ts.isPropertyAssignment(prop) ||
(!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name))
) {
return null;
}
@ -451,8 +604,9 @@ function getRouterModuleForRootFeatures(
if (inMemoryScrollingOptions.length > 0) {
features.track(
'withInMemoryScrolling',
ts.factory.createObjectLiteralExpression(inMemoryScrollingOptions));
'withInMemoryScrolling',
ts.factory.createObjectLiteralExpression(inMemoryScrollingOptions),
);
}
if (configOptions.length > 0) {
@ -461,13 +615,18 @@ function getRouterModuleForRootFeatures(
for (const [feature, featureArgs] of features.getEntries()) {
const callArgs: ts.Expression[] = [];
featureArgs.forEach(arg => {
featureArgs.forEach((arg) => {
if (arg !== null) {
callArgs.push(arg);
}
});
featureExpressions.push(ts.factory.createCallExpression(
tracker.addImport(sourceFile, feature, '@angular/router'), [], callArgs));
featureExpressions.push(
ts.factory.createCallExpression(
tracker.addImport(sourceFile, feature, '@angular/router'),
[],
callArgs,
),
);
}
return featureExpressions;
@ -484,38 +643,51 @@ function getRouterModuleForRootFeatures(
* @param referenceResolver
*/
function addNodesToCopy(
targetFile: ts.SourceFile, rootNode: ts.Node, nodeLookup: NodeLookup, tracker: ChangeTracker,
nodesToCopy: Set<ts.Node>, referenceResolver: ReferenceResolver): void {
targetFile: ts.SourceFile,
rootNode: ts.Node,
nodeLookup: NodeLookup,
tracker: ChangeTracker,
nodesToCopy: Set<ts.Node>,
referenceResolver: ReferenceResolver,
): void {
const refs = findAllSameFileReferences(rootNode, nodeLookup, referenceResolver);
for (const ref of refs) {
const importSpecifier = closestOrSelf(ref, ts.isImportSpecifier);
const importDeclaration =
importSpecifier ? closestNode(importSpecifier, ts.isImportDeclaration) : null;
const importDeclaration = importSpecifier
? closestNode(importSpecifier, ts.isImportDeclaration)
: null;
// If the reference is in an import, we need to add an import to the main file.
if (importDeclaration && importSpecifier &&
ts.isStringLiteralLike(importDeclaration.moduleSpecifier)) {
const moduleName = importDeclaration.moduleSpecifier.text.startsWith('.') ?
remapRelativeImport(targetFile.fileName, importDeclaration.moduleSpecifier) :
importDeclaration.moduleSpecifier.text;
const symbolName = importSpecifier.propertyName ? importSpecifier.propertyName.text :
importSpecifier.name.text;
if (
importDeclaration &&
importSpecifier &&
ts.isStringLiteralLike(importDeclaration.moduleSpecifier)
) {
const moduleName = importDeclaration.moduleSpecifier.text.startsWith('.')
? remapRelativeImport(targetFile.fileName, importDeclaration.moduleSpecifier)
: importDeclaration.moduleSpecifier.text;
const symbolName = importSpecifier.propertyName
? importSpecifier.propertyName.text
: importSpecifier.name.text;
const alias = importSpecifier.propertyName ? importSpecifier.name.text : null;
tracker.addImport(targetFile, symbolName, moduleName, alias);
continue;
}
const variableDeclaration = closestOrSelf(ref, ts.isVariableDeclaration);
const variableStatement =
variableDeclaration ? closestNode(variableDeclaration, ts.isVariableStatement) : null;
const variableStatement = variableDeclaration
? closestNode(variableDeclaration, ts.isVariableStatement)
: null;
// If the reference is a variable, we can attempt to import it or copy it over.
if (variableDeclaration && variableStatement && ts.isIdentifier(variableDeclaration.name)) {
if (isExported(variableStatement)) {
tracker.addImport(
targetFile, variableDeclaration.name.text,
getRelativeImportPath(targetFile.fileName, ref.getSourceFile().fileName));
targetFile,
variableDeclaration.name.text,
getRelativeImportPath(targetFile.fileName, ref.getSourceFile().fileName),
);
} else {
nodesToCopy.add(variableStatement);
}
@ -528,8 +700,10 @@ function addNodesToCopy(
if (closestExportable) {
if (isExported(closestExportable) && closestExportable.name) {
tracker.addImport(
targetFile, closestExportable.name.text,
getRelativeImportPath(targetFile.fileName, ref.getSourceFile().fileName));
targetFile,
closestExportable.name.text,
getRelativeImportPath(targetFile.fileName, ref.getSourceFile().fileName),
);
} else {
nodesToCopy.add(closestExportable);
}
@ -544,7 +718,10 @@ function addNodesToCopy(
* @param referenceResolver
*/
function findAllSameFileReferences(
rootNode: ts.Node, nodeLookup: NodeLookup, referenceResolver: ReferenceResolver): Set<ts.Node> {
rootNode: ts.Node,
nodeLookup: NodeLookup,
referenceResolver: ReferenceResolver,
): Set<ts.Node> {
const results = new Set<ts.Node>();
const traversedTopLevelNodes = new Set<ts.Node>();
const excludeStart = rootNode.getStart();
@ -557,7 +734,12 @@ function findAllSameFileReferences(
}
const refs = referencesToNodeWithinSameFile(
node, nodeLookup, excludeStart, excludeEnd, referenceResolver);
node,
nodeLookup,
excludeStart,
excludeEnd,
referenceResolver,
);
if (refs === null) {
return;
@ -578,9 +760,15 @@ function findAllSameFileReferences(
// Keep searching, starting from the closest top-level node. We skip import declarations,
// because we already know about them and they may put the search into an infinite loop.
if (!ts.isImportDeclaration(closestTopLevel) &&
isOutsideRange(
excludeStart, excludeEnd, closestTopLevel.getStart(), closestTopLevel.getEnd())) {
if (
!ts.isImportDeclaration(closestTopLevel) &&
isOutsideRange(
excludeStart,
excludeEnd,
closestTopLevel.getStart(),
closestTopLevel.getEnd(),
)
) {
traversedTopLevelNodes.add(closestTopLevel);
walk(closestTopLevel);
}
@ -599,11 +787,15 @@ function findAllSameFileReferences(
* @param referenceResolver
*/
function referencesToNodeWithinSameFile(
node: ts.Identifier, nodeLookup: NodeLookup, excludeStart: number, excludeEnd: number,
referenceResolver: ReferenceResolver): Set<ts.Node>|null {
const offsets =
referenceResolver.findSameFileReferences(node, node.getSourceFile().fileName)
.filter(([start, end]) => isOutsideRange(excludeStart, excludeEnd, start, end));
node: ts.Identifier,
nodeLookup: NodeLookup,
excludeStart: number,
excludeEnd: number,
referenceResolver: ReferenceResolver,
): Set<ts.Node> | null {
const offsets = referenceResolver
.findSameFileReferences(node, node.getSourceFile().fileName)
.filter(([start, end]) => isOutsideRange(excludeStart, excludeEnd, start, end));
if (offsets.length > 0) {
const nodes = offsetsToNodes(nodeLookup, offsets, new Set());
@ -625,20 +817,26 @@ function referencesToNodeWithinSameFile(
*/
function remapDynamicImports<T extends ts.Node>(targetFileName: string, rootNode: T): T {
let hasChanged = false;
const transformer: ts.TransformerFactory<ts.Node> = context => {
return sourceFile => ts.visitNode(sourceFile, function walk(node: ts.Node): ts.Node {
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword &&
node.arguments.length > 0 && ts.isStringLiteralLike(node.arguments[0]) &&
node.arguments[0].text.startsWith('.')) {
hasChanged = true;
return context.factory.updateCallExpression(node, node.expression, node.typeArguments, [
context.factory.createStringLiteral(
remapRelativeImport(targetFileName, node.arguments[0])),
...node.arguments.slice(1)
]);
}
return ts.visitEachChild(node, walk, context);
});
const transformer: ts.TransformerFactory<ts.Node> = (context) => {
return (sourceFile) =>
ts.visitNode(sourceFile, function walk(node: ts.Node): ts.Node {
if (
ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
node.arguments.length > 0 &&
ts.isStringLiteralLike(node.arguments[0]) &&
node.arguments[0].text.startsWith('.')
) {
hasChanged = true;
return context.factory.updateCallExpression(node, node.expression, node.typeArguments, [
context.factory.createStringLiteral(
remapRelativeImport(targetFileName, node.arguments[0]),
),
...node.arguments.slice(1),
]);
}
return ts.visitEachChild(node, walk, context);
});
};
const result = ts.transform(rootNode, [transformer]).transformed[0] as T;
@ -659,9 +857,11 @@ function isTopLevelStatement(node: ts.Node): node is ts.Node {
* @param node Node to be checked.
*/
function isReferenceIdentifier(node: ts.Node): node is ts.Identifier {
return ts.isIdentifier(node) &&
(!ts.isPropertyAssignment(node.parent) && !ts.isParameter(node.parent) ||
node.parent.name !== node);
return (
ts.isIdentifier(node) &&
((!ts.isPropertyAssignment(node.parent) && !ts.isParameter(node.parent)) ||
node.parent.name !== node)
);
}
/**
@ -672,7 +872,11 @@ function isReferenceIdentifier(node: ts.Node): node is ts.Identifier {
* @param end End of the range that is being checked.
*/
function isOutsideRange(
excludeStart: number, excludeEnd: number, start: number, end: number): boolean {
excludeStart: number,
excludeEnd: number,
start: number,
end: number,
): boolean {
return (start < excludeStart && end < excludeStart) || start > excludeEnd;
}
@ -683,7 +887,9 @@ function isOutsideRange(
*/
function remapRelativeImport(targetFileName: string, specifier: ts.StringLiteralLike): string {
return getRelativeImportPath(
targetFileName, join(dirname(specifier.getSourceFile().fileName), specifier.text));
targetFileName,
join(dirname(specifier.getSourceFile().fileName), specifier.text),
);
}
/**
@ -691,9 +897,9 @@ function remapRelativeImport(targetFileName: string, specifier: ts.StringLiteral
* @param node Node to be checked.
*/
function isExported(node: ts.Node): node is ts.Node {
return ts.canHaveModifiers(node) && node.modifiers ?
node.modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) :
false;
return ts.canHaveModifiers(node) && node.modifiers
? node.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
: false;
}
/**
@ -701,11 +907,21 @@ function isExported(node: ts.Node): node is ts.Node {
* it can be safely copied into another file.
* @param node Node to be checked.
*/
function isExportableDeclaration(node: ts.Node): node is ts.EnumDeclaration|ts.ClassDeclaration|
ts.FunctionDeclaration|ts.InterfaceDeclaration|ts.TypeAliasDeclaration {
return ts.isEnumDeclaration(node) || ts.isClassDeclaration(node) ||
ts.isFunctionDeclaration(node) || ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node);
function isExportableDeclaration(
node: ts.Node,
): node is
| ts.EnumDeclaration
| ts.ClassDeclaration
| ts.FunctionDeclaration
| ts.InterfaceDeclaration
| ts.TypeAliasDeclaration {
return (
ts.isEnumDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isFunctionDeclaration(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node)
);
}
/**
@ -739,9 +955,12 @@ function hasImport(program: NgtscProgram, rootFileNames: string[], moduleName: s
}
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier) &&
(statement.moduleSpecifier.text === moduleName ||
statement.moduleSpecifier.text.startsWith(deepImportStart))) {
if (
ts.isImportDeclaration(statement) &&
ts.isStringLiteralLike(statement.moduleSpecifier) &&
(statement.moduleSpecifier.text === moduleName ||
statement.moduleSpecifier.text.startsWith(deepImportStart))
) {
return true;
}
}

View file

@ -7,7 +7,13 @@
*/
import {NgtscProgram} from '@angular/compiler-cli';
import {PotentialImport, PotentialImportKind, PotentialImportMode, Reference, TemplateTypeChecker} from '@angular/compiler-cli/private/migrations';
import {
PotentialImport,
PotentialImportKind,
PotentialImportMode,
Reference,
TemplateTypeChecker,
} from '@angular/compiler-cli/private/migrations';
import ts from 'typescript';
import {ChangesByFile, ChangeTracker, ImportRemapper} from '../../utils/change_tracker';
@ -16,14 +22,21 @@ import {getImportSpecifier} from '../../utils/typescript/imports';
import {closestNode} from '../../utils/typescript/nodes';
import {isReferenceToImport} from '../../utils/typescript/symbol';
import {findClassDeclaration, findLiteralProperty, isClassReferenceInAngularModule, NamedClassDeclaration} from './util';
import {
findClassDeclaration,
findLiteralProperty,
isClassReferenceInAngularModule,
NamedClassDeclaration,
} from './util';
/**
* Function that can be used to prcess the dependencies that
* are going to be added to the imports of a component.
*/
export type ComponentImportsRemapper =
(imports: PotentialImport[], component: ts.ClassDeclaration) => PotentialImport[];
export type ComponentImportsRemapper = (
imports: PotentialImport[],
component: ts.ClassDeclaration,
) => PotentialImport[];
/**
* Converts all declarations in the specified files to standalone.
@ -35,9 +48,12 @@ export type ComponentImportsRemapper =
* imports.
*/
export function toStandalone(
sourceFiles: ts.SourceFile[], program: NgtscProgram, printer: ts.Printer,
fileImportRemapper?: ImportRemapper,
componentImportRemapper?: ComponentImportsRemapper): ChangesByFile {
sourceFiles: ts.SourceFile[],
program: NgtscProgram,
printer: ts.Printer,
fileImportRemapper?: ImportRemapper,
componentImportRemapper?: ComponentImportsRemapper,
): ChangesByFile {
const templateTypeChecker = program.compiler.getTemplateTypeChecker();
const typeChecker = program.getTsProgram().getTypeChecker();
const modulesToMigrate = new Set<ts.ClassDeclaration>();
@ -52,20 +68,29 @@ export function toStandalone(
for (const module of modules) {
const allModuleDeclarations = extractDeclarationsFromModule(module, templateTypeChecker);
const unbootstrappedDeclarations = filterNonBootstrappedDeclarations(
allModuleDeclarations, module, templateTypeChecker, typeChecker);
allModuleDeclarations,
module,
templateTypeChecker,
typeChecker,
);
if (unbootstrappedDeclarations.length > 0) {
modulesToMigrate.add(module);
unbootstrappedDeclarations.forEach(decl => declarations.add(decl));
unbootstrappedDeclarations.forEach((decl) => declarations.add(decl));
}
}
testObjects.forEach(obj => testObjectsToMigrate.add(obj));
testObjects.forEach((obj) => testObjectsToMigrate.add(obj));
}
for (const declaration of declarations) {
convertNgModuleDeclarationToStandalone(
declaration, declarations, tracker, templateTypeChecker, componentImportRemapper);
declaration,
declarations,
tracker,
templateTypeChecker,
componentImportRemapper,
);
}
for (const node of modulesToMigrate) {
@ -73,7 +98,12 @@ export function toStandalone(
}
migrateTestDeclarations(
testObjectsToMigrate, declarations, tracker, templateTypeChecker, typeChecker);
testObjectsToMigrate,
declarations,
tracker,
templateTypeChecker,
typeChecker,
);
return tracker.recordChanges();
}
@ -86,8 +116,12 @@ export function toStandalone(
* @param importRemapper
*/
export function convertNgModuleDeclarationToStandalone(
decl: ts.ClassDeclaration, allDeclarations: Set<ts.ClassDeclaration>, tracker: ChangeTracker,
typeChecker: TemplateTypeChecker, importRemapper?: ComponentImportsRemapper): void {
decl: ts.ClassDeclaration,
allDeclarations: Set<ts.ClassDeclaration>,
tracker: ChangeTracker,
typeChecker: TemplateTypeChecker,
importRemapper?: ComponentImportsRemapper,
): void {
const directiveMeta = typeChecker.getDirectiveMetadata(decl);
if (directiveMeta && directiveMeta.decorator && !directiveMeta.isStandalone) {
@ -95,18 +129,28 @@ export function convertNgModuleDeclarationToStandalone(
if (directiveMeta.isComponent) {
const importsToAdd = getComponentImportExpressions(
decl, allDeclarations, tracker, typeChecker, importRemapper);
decl,
allDeclarations,
tracker,
typeChecker,
importRemapper,
);
if (importsToAdd.length > 0) {
const hasTrailingComma = importsToAdd.length > 2 &&
!!extractMetadataLiteral(directiveMeta.decorator)?.properties.hasTrailingComma;
const hasTrailingComma =
importsToAdd.length > 2 &&
!!extractMetadataLiteral(directiveMeta.decorator)?.properties.hasTrailingComma;
decorator = addPropertyToAngularDecorator(
decorator,
ts.factory.createPropertyAssignment(
'imports',
ts.factory.createArrayLiteralExpression(
// Create a multi-line array when it has a trailing comma.
ts.factory.createNodeArray(importsToAdd, hasTrailingComma), hasTrailingComma)));
decorator,
ts.factory.createPropertyAssignment(
'imports',
ts.factory.createArrayLiteralExpression(
// Create a multi-line array when it has a trailing comma.
ts.factory.createNodeArray(importsToAdd, hasTrailingComma),
hasTrailingComma,
),
),
);
}
}
@ -130,21 +174,29 @@ export function convertNgModuleDeclarationToStandalone(
* @param importRemapper
*/
function getComponentImportExpressions(
decl: ts.ClassDeclaration, allDeclarations: Set<ts.ClassDeclaration>, tracker: ChangeTracker,
typeChecker: TemplateTypeChecker, importRemapper?: ComponentImportsRemapper): ts.Expression[] {
decl: ts.ClassDeclaration,
allDeclarations: Set<ts.ClassDeclaration>,
tracker: ChangeTracker,
typeChecker: TemplateTypeChecker,
importRemapper?: ComponentImportsRemapper,
): ts.Expression[] {
const templateDependencies = findTemplateDependencies(decl, typeChecker);
const usedDependenciesInMigration =
new Set(templateDependencies.filter(dep => allDeclarations.has(dep.node)));
const usedDependenciesInMigration = new Set(
templateDependencies.filter((dep) => allDeclarations.has(dep.node)),
);
const imports: ts.Expression[] = [];
const seenImports = new Set<string>();
const resolvedDependencies: PotentialImport[] = [];
for (const dep of templateDependencies) {
const importLocation = findImportLocation(
dep as Reference<NamedClassDeclaration>, decl,
usedDependenciesInMigration.has(dep) ? PotentialImportMode.ForceDirect :
PotentialImportMode.Normal,
typeChecker);
dep as Reference<NamedClassDeclaration>,
decl,
usedDependenciesInMigration.has(dep)
? PotentialImportMode.ForceDirect
: PotentialImportMode.Normal,
typeChecker,
);
if (importLocation && !seenImports.has(importLocation.symbolName)) {
seenImports.add(importLocation.symbolName);
@ -152,24 +204,38 @@ function getComponentImportExpressions(
}
}
const processedDependencies =
importRemapper ? importRemapper(resolvedDependencies, decl) : resolvedDependencies;
const processedDependencies = importRemapper
? importRemapper(resolvedDependencies, decl)
: resolvedDependencies;
for (const importLocation of processedDependencies) {
if (importLocation.moduleSpecifier) {
const identifier = tracker.addImport(
decl.getSourceFile(), importLocation.symbolName, importLocation.moduleSpecifier);
decl.getSourceFile(),
importLocation.symbolName,
importLocation.moduleSpecifier,
);
imports.push(identifier);
} else {
const identifier = ts.factory.createIdentifier(importLocation.symbolName);
if (importLocation.isForwardReference) {
const forwardRefExpression =
tracker.addImport(decl.getSourceFile(), 'forwardRef', '@angular/core');
const forwardRefExpression = tracker.addImport(
decl.getSourceFile(),
'forwardRef',
'@angular/core',
);
const arrowFunction = ts.factory.createArrowFunction(
undefined, undefined, [], undefined, undefined, identifier);
undefined,
undefined,
[],
undefined,
undefined,
identifier,
);
imports.push(
ts.factory.createCallExpression(forwardRefExpression, undefined, [arrowFunction]));
ts.factory.createCallExpression(forwardRefExpression, undefined, [arrowFunction]),
);
} else {
imports.push(identifier);
}
@ -188,8 +254,12 @@ function getComponentImportExpressions(
* @param templateTypeChecker
*/
function migrateNgModuleClass(
node: ts.ClassDeclaration, allDeclarations: Set<ts.ClassDeclaration>, tracker: ChangeTracker,
typeChecker: ts.TypeChecker, templateTypeChecker: TemplateTypeChecker) {
node: ts.ClassDeclaration,
allDeclarations: Set<ts.ClassDeclaration>,
tracker: ChangeTracker,
typeChecker: ts.TypeChecker,
templateTypeChecker: TemplateTypeChecker,
) {
const decorator = templateTypeChecker.getNgModuleMetadata(node)?.decorator;
const metadata = decorator ? extractMetadataLiteral(decorator) : null;
@ -207,9 +277,12 @@ function migrateNgModuleClass(
* @param tracker
*/
function moveDeclarationsToImports(
literal: ts.ObjectLiteralExpression, allDeclarations: Set<ts.ClassDeclaration>,
typeChecker: ts.TypeChecker, templateTypeChecker: TemplateTypeChecker,
tracker: ChangeTracker): void {
literal: ts.ObjectLiteralExpression,
allDeclarations: Set<ts.ClassDeclaration>,
typeChecker: ts.TypeChecker,
templateTypeChecker: TemplateTypeChecker,
tracker: ChangeTracker,
): void {
const declarationsProp = findLiteralProperty(literal, 'declarations');
if (!declarationsProp) {
@ -221,8 +294,11 @@ function moveDeclarationsToImports(
const properties: ts.ObjectLiteralElementLike[] = [];
const importsProp = findLiteralProperty(literal, 'imports');
const hasAnyArrayTrailingComma = literal.properties.some(
prop => ts.isPropertyAssignment(prop) && ts.isArrayLiteralExpression(prop.initializer) &&
prop.initializer.elements.hasTrailingComma);
(prop) =>
ts.isPropertyAssignment(prop) &&
ts.isArrayLiteralExpression(prop.initializer) &&
prop.initializer.elements.hasTrailingComma,
);
// Separate the declarations that we want to keep and ones we need to copy into the `imports`.
if (ts.isPropertyAssignment(declarationsProp)) {
@ -233,12 +309,14 @@ function moveDeclarationsToImports(
if (ts.isIdentifier(el)) {
const correspondingClass = findClassDeclaration(el, typeChecker);
if (!correspondingClass ||
// Check whether the declaration is either standalone already or is being converted
// in this migration. We need to check if it's standalone already, in order to correct
// some cases where the main app and the test files are being migrated in separate
// programs.
isStandaloneDeclaration(correspondingClass, allDeclarations, templateTypeChecker)) {
if (
!correspondingClass ||
// Check whether the declaration is either standalone already or is being converted
// in this migration. We need to check if it's standalone already, in order to correct
// some cases where the main app and the test files are being migrated in separate
// programs.
isStandaloneDeclaration(correspondingClass, allDeclarations, templateTypeChecker)
) {
declarationsToCopy.push(el);
} else {
declarationsToPreserve.push(el);
@ -255,10 +333,17 @@ function moveDeclarationsToImports(
// If there are no `imports`, create them with the declarations we want to copy.
if (!importsProp && declarationsToCopy.length > 0) {
properties.push(ts.factory.createPropertyAssignment(
properties.push(
ts.factory.createPropertyAssignment(
'imports',
ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(
declarationsToCopy, hasAnyArrayTrailingComma && declarationsToCopy.length > 2))));
ts.factory.createArrayLiteralExpression(
ts.factory.createNodeArray(
declarationsToCopy,
hasAnyArrayTrailingComma && declarationsToCopy.length > 2,
),
),
),
);
}
for (const prop of literal.properties) {
@ -270,13 +355,21 @@ function moveDeclarationsToImports(
// If we have declarations to preserve, update the existing property, otherwise drop it.
if (prop === declarationsProp) {
if (declarationsToPreserve.length > 0) {
const hasTrailingComma = ts.isArrayLiteralExpression(prop.initializer) ?
prop.initializer.elements.hasTrailingComma :
hasAnyArrayTrailingComma;
properties.push(ts.factory.updatePropertyAssignment(
prop, prop.name,
ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(
declarationsToPreserve, hasTrailingComma && declarationsToPreserve.length > 2))));
const hasTrailingComma = ts.isArrayLiteralExpression(prop.initializer)
? prop.initializer.elements.hasTrailingComma
: hasAnyArrayTrailingComma;
properties.push(
ts.factory.updatePropertyAssignment(
prop,
prop.name,
ts.factory.createArrayLiteralExpression(
ts.factory.createNodeArray(
declarationsToPreserve,
hasTrailingComma && declarationsToPreserve.length > 2,
),
),
),
);
}
continue;
}
@ -288,16 +381,21 @@ function moveDeclarationsToImports(
if (ts.isArrayLiteralExpression(prop.initializer)) {
initializer = ts.factory.updateArrayLiteralExpression(
prop.initializer,
ts.factory.createNodeArray(
[...prop.initializer.elements, ...declarationsToCopy],
prop.initializer.elements.hasTrailingComma));
prop.initializer,
ts.factory.createNodeArray(
[...prop.initializer.elements, ...declarationsToCopy],
prop.initializer.elements.hasTrailingComma,
),
);
} else {
initializer = ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(
initializer = ts.factory.createArrayLiteralExpression(
ts.factory.createNodeArray(
[ts.factory.createSpreadElement(prop.initializer), ...declarationsToCopy],
// Expect the declarations to be greater than 1 since
// we have the pre-existing initializer already.
hasAnyArrayTrailingComma && declarationsToCopy.length > 1));
hasAnyArrayTrailingComma && declarationsToCopy.length > 1,
),
);
}
properties.push(ts.factory.updatePropertyAssignment(prop, prop.name, initializer));
@ -309,18 +407,24 @@ function moveDeclarationsToImports(
}
tracker.replaceNode(
literal,
ts.factory.updateObjectLiteralExpression(
literal,
ts.factory.updateObjectLiteralExpression(
literal, ts.factory.createNodeArray(properties, literal.properties.hasTrailingComma)),
ts.EmitHint.Expression);
ts.factory.createNodeArray(properties, literal.properties.hasTrailingComma),
),
ts.EmitHint.Expression,
);
}
/** Adds `standalone: true` to a decorator node. */
function addStandaloneToDecorator(node: ts.Decorator): ts.Decorator {
return addPropertyToAngularDecorator(
node,
ts.factory.createPropertyAssignment(
'standalone', ts.factory.createToken(ts.SyntaxKind.TrueKeyword)));
node,
ts.factory.createPropertyAssignment(
'standalone',
ts.factory.createToken(ts.SyntaxKind.TrueKeyword),
),
);
}
/**
@ -329,7 +433,9 @@ function addStandaloneToDecorator(node: ts.Decorator): ts.Decorator {
* @param property Property to add.
*/
function addPropertyToAngularDecorator(
node: ts.Decorator, property: ts.PropertyAssignment): ts.Decorator {
node: ts.Decorator,
property: ts.PropertyAssignment,
): ts.Decorator {
// Invalid decorator.
if (!ts.isCallExpression(node.expression) || node.expression.arguments.length > 1) {
return node;
@ -350,16 +456,20 @@ function addPropertyToAngularDecorator(
// Use `createDecorator` instead of `updateDecorator`, because
// the latter ends up duplicating the node's leading comment.
return ts.factory.createDecorator(ts.factory.createCallExpression(
node.expression.expression, node.expression.typeArguments,
[ts.factory.createObjectLiteralExpression(
ts.factory.createNodeArray(literalProperties, hasTrailingComma),
literalProperties.length > 1)]));
return ts.factory.createDecorator(
ts.factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
ts.factory.createObjectLiteralExpression(
ts.factory.createNodeArray(literalProperties, hasTrailingComma),
literalProperties.length > 1,
),
]),
);
}
/** Checks if a node is a `PropertyAssignment` with a name. */
function isNamedPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment&
{name: ts.Identifier} {
function isNamedPropertyAssignment(
node: ts.Node,
): node is ts.PropertyAssignment & {name: ts.Identifier} {
return ts.isPropertyAssignment(node) && node.name && ts.isIdentifier(node.name);
}
@ -371,11 +481,14 @@ function isNamedPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment
* @param typeChecker
*/
function findImportLocation(
target: Reference<NamedClassDeclaration>, inComponent: ts.ClassDeclaration,
importMode: PotentialImportMode, typeChecker: TemplateTypeChecker): PotentialImport|null {
target: Reference<NamedClassDeclaration>,
inComponent: ts.ClassDeclaration,
importMode: PotentialImportMode,
typeChecker: TemplateTypeChecker,
): PotentialImport | null {
const importLocations = typeChecker.getPotentialImportsFor(target, inComponent, importMode);
let firstSameFileImport: PotentialImport|null = null;
let firstModuleImport: PotentialImport|null = null;
let firstSameFileImport: PotentialImport | null = null;
let firstModuleImport: PotentialImport | null = null;
for (const location of importLocations) {
// Prefer a standalone import, if we can find one.
@ -386,9 +499,12 @@ function findImportLocation(
if (!location.moduleSpecifier && !firstSameFileImport) {
firstSameFileImport = location;
}
if (location.kind === PotentialImportKind.NgModule && !firstModuleImport &&
// ɵ is used for some internal Angular modules that we want to skip over.
!location.symbolName.startsWith('ɵ')) {
if (
location.kind === PotentialImportKind.NgModule &&
!firstModuleImport &&
// ɵ is used for some internal Angular modules that we want to skip over.
!location.symbolName.startsWith('ɵ')
) {
firstModuleImport = location;
}
}
@ -401,10 +517,13 @@ function findImportLocation(
* E.g. `declarations: [Foo]` or `declarations: SOME_VAR` would match this description,
* but not `declarations: []`.
*/
function hasNgModuleMetadataElements(node: ts.Node): node is ts.PropertyAssignment&
{initializer: ts.ArrayLiteralExpression} {
return ts.isPropertyAssignment(node) &&
(!ts.isArrayLiteralExpression(node.initializer) || node.initializer.elements.length > 0);
function hasNgModuleMetadataElements(
node: ts.Node,
): node is ts.PropertyAssignment & {initializer: ts.ArrayLiteralExpression} {
return (
ts.isPropertyAssignment(node) &&
(!ts.isArrayLiteralExpression(node.initializer) || node.initializer.elements.length > 0)
);
}
/** Finds all modules whose declarations can be migrated. */
@ -414,8 +533,9 @@ function findNgModuleClassesToMigrate(sourceFile: ts.SourceFile, typeChecker: ts
if (getImportSpecifier(sourceFile, '@angular/core', 'NgModule')) {
sourceFile.forEachChild(function walk(node) {
if (ts.isClassDeclaration(node)) {
const decorator = getAngularDecorators(typeChecker, ts.getDecorators(node) || [])
.find(current => current.name === 'NgModule');
const decorator = getAngularDecorators(typeChecker, ts.getDecorators(node) || []).find(
(current) => current.name === 'NgModule',
);
const metadata = decorator ? extractMetadataLiteral(decorator.node) : null;
if (metadata) {
@ -442,23 +562,32 @@ export function findTestObjectsToMigrate(sourceFile: ts.SourceFile, typeChecker:
if (testBedImport || catalystImport) {
sourceFile.forEachChild(function walk(node) {
const isObjectLiteralCall = ts.isCallExpression(node) && node.arguments.length > 0 &&
// `arguments[0]` is the testing module config.
ts.isObjectLiteralExpression(node.arguments[0]);
const config = isObjectLiteralCall ? node.arguments[0] as ts.ObjectLiteralExpression : null;
const isTestBedCall = isObjectLiteralCall &&
(testBedImport && ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'configureTestingModule' &&
isReferenceToImport(typeChecker, node.expression.expression, testBedImport));
const isCatalystCall = isObjectLiteralCall &&
(catalystImport && ts.isIdentifier(node.expression) &&
isReferenceToImport(typeChecker, node.expression, catalystImport));
const isObjectLiteralCall =
ts.isCallExpression(node) &&
node.arguments.length > 0 &&
// `arguments[0]` is the testing module config.
ts.isObjectLiteralExpression(node.arguments[0]);
const config = isObjectLiteralCall ? (node.arguments[0] as ts.ObjectLiteralExpression) : null;
const isTestBedCall =
isObjectLiteralCall &&
testBedImport &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'configureTestingModule' &&
isReferenceToImport(typeChecker, node.expression.expression, testBedImport);
const isCatalystCall =
isObjectLiteralCall &&
catalystImport &&
ts.isIdentifier(node.expression) &&
isReferenceToImport(typeChecker, node.expression, catalystImport);
if ((isTestBedCall || isCatalystCall) && config) {
const declarations = findLiteralProperty(config, 'declarations');
if (declarations && ts.isPropertyAssignment(declarations) &&
ts.isArrayLiteralExpression(declarations.initializer) &&
declarations.initializer.elements.length > 0) {
if (
declarations &&
ts.isPropertyAssignment(declarations) &&
ts.isArrayLiteralExpression(declarations.initializer) &&
declarations.initializer.elements.length > 0
) {
testObjects.push(config);
}
}
@ -475,8 +604,10 @@ export function findTestObjectsToMigrate(sourceFile: ts.SourceFile, typeChecker:
* @param decl Component in whose template we're looking for dependencies.
* @param typeChecker
*/
function findTemplateDependencies(decl: ts.ClassDeclaration, typeChecker: TemplateTypeChecker):
Reference<NamedClassDeclaration>[] {
function findTemplateDependencies(
decl: ts.ClassDeclaration,
typeChecker: TemplateTypeChecker,
): Reference<NamedClassDeclaration>[] {
const results: Reference<NamedClassDeclaration>[] = [];
const usedDirectives = typeChecker.getUsedDirectives(decl);
const usedPipes = typeChecker.getUsedPipes(decl);
@ -493,8 +624,10 @@ function findTemplateDependencies(decl: ts.ClassDeclaration, typeChecker: Templa
const potentialPipes = typeChecker.getPotentialPipes(decl);
for (const pipe of potentialPipes) {
if (ts.isClassDeclaration(pipe.ref.node) &&
usedPipes.some(current => pipe.name === current)) {
if (
ts.isClassDeclaration(pipe.ref.node) &&
usedPipes.some((current) => pipe.name === current)
) {
results.push(pipe.ref as Reference<NamedClassDeclaration>);
}
}
@ -512,11 +645,14 @@ function findTemplateDependencies(decl: ts.ClassDeclaration, typeChecker: Templa
* @param typeChecker
*/
function filterNonBootstrappedDeclarations(
declarations: ts.ClassDeclaration[], ngModule: ts.ClassDeclaration,
templateTypeChecker: TemplateTypeChecker, typeChecker: ts.TypeChecker) {
declarations: ts.ClassDeclaration[],
ngModule: ts.ClassDeclaration,
templateTypeChecker: TemplateTypeChecker,
typeChecker: ts.TypeChecker,
) {
const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
const metaLiteral =
metadata && metadata.decorator ? extractMetadataLiteral(metadata.decorator) : null;
metadata && metadata.decorator ? extractMetadataLiteral(metadata.decorator) : null;
const bootstrapProp = metaLiteral ? findLiteralProperty(metaLiteral, 'bootstrap') : null;
// If there's no `bootstrap`, we can't filter.
@ -526,8 +662,10 @@ function filterNonBootstrappedDeclarations(
// If we can't analyze the `bootstrap` property, we can't safely determine which
// declarations aren't bootstrapped so we assume that all of them are.
if (!ts.isPropertyAssignment(bootstrapProp) ||
!ts.isArrayLiteralExpression(bootstrapProp.initializer)) {
if (
!ts.isPropertyAssignment(bootstrapProp) ||
!ts.isArrayLiteralExpression(bootstrapProp.initializer)
) {
return [];
}
@ -545,7 +683,7 @@ function filterNonBootstrappedDeclarations(
}
}
return declarations.filter(ref => !bootstrappedClasses.has(ref));
return declarations.filter((ref) => !bootstrappedClasses.has(ref));
}
/**
@ -554,12 +692,15 @@ function filterNonBootstrappedDeclarations(
* @param templateTypeChecker
*/
export function extractDeclarationsFromModule(
ngModule: ts.ClassDeclaration,
templateTypeChecker: TemplateTypeChecker): ts.ClassDeclaration[] {
ngModule: ts.ClassDeclaration,
templateTypeChecker: TemplateTypeChecker,
): ts.ClassDeclaration[] {
const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
return metadata ? metadata.declarations.filter(decl => ts.isClassDeclaration(decl.node))
.map(decl => decl.node) as ts.ClassDeclaration[] :
[];
return metadata
? (metadata.declarations
.filter((decl) => ts.isClassDeclaration(decl.node))
.map((decl) => decl.node) as ts.ClassDeclaration[])
: [];
}
/**
@ -571,9 +712,12 @@ export function extractDeclarationsFromModule(
* @param typeChecker
*/
export function migrateTestDeclarations(
testObjects: Set<ts.ObjectLiteralExpression>,
declarationsOutsideOfTestFiles: Set<ts.ClassDeclaration>, tracker: ChangeTracker,
templateTypeChecker: TemplateTypeChecker, typeChecker: ts.TypeChecker) {
testObjects: Set<ts.ObjectLiteralExpression>,
declarationsOutsideOfTestFiles: Set<ts.ClassDeclaration>,
tracker: ChangeTracker,
templateTypeChecker: TemplateTypeChecker,
typeChecker: ts.TypeChecker,
) {
const {decorators, componentImports} = analyzeTestingModules(testObjects, typeChecker);
const allDeclarations = new Set(declarationsOutsideOfTestFiles);
@ -595,16 +739,21 @@ export function migrateTestDeclarations(
}
if (importsToAdd && importsToAdd.size > 0) {
const hasTrailingComma = importsToAdd.size > 2 &&
!!extractMetadataLiteral(decorator.node)?.properties.hasTrailingComma;
const hasTrailingComma =
importsToAdd.size > 2 &&
!!extractMetadataLiteral(decorator.node)?.properties.hasTrailingComma;
const importsArray = ts.factory.createNodeArray(Array.from(importsToAdd), hasTrailingComma);
tracker.replaceNode(
decorator.node,
addPropertyToAngularDecorator(
newDecorator,
ts.factory.createPropertyAssignment(
'imports', ts.factory.createArrayLiteralExpression(importsArray))));
decorator.node,
addPropertyToAngularDecorator(
newDecorator,
ts.factory.createPropertyAssignment(
'imports',
ts.factory.createArrayLiteralExpression(importsArray),
),
),
);
} else {
tracker.replaceNode(decorator.node, newDecorator);
}
@ -623,7 +772,9 @@ export function migrateTestDeclarations(
* @param testObjects Object literals that should be analyzed.
*/
function analyzeTestingModules(
testObjects: Set<ts.ObjectLiteralExpression>, typeChecker: ts.TypeChecker) {
testObjects: Set<ts.ObjectLiteralExpression>,
typeChecker: ts.TypeChecker,
) {
const seenDeclarations = new Set<ts.Declaration>();
const decorators: NgDecorator[] = [];
const componentImports = new Map<ts.Decorator, Set<ts.Expression>>();
@ -636,18 +787,24 @@ function analyzeTestingModules(
}
const importsProp = findLiteralProperty(obj, 'imports');
const importElements = importsProp && hasNgModuleMetadataElements(importsProp) ?
importsProp.initializer.elements.filter(el => {
// Filter out calls since they may be a `ModuleWithProviders`.
return !ts.isCallExpression(el) &&
const importElements =
importsProp && hasNgModuleMetadataElements(importsProp)
? importsProp.initializer.elements.filter((el) => {
// Filter out calls since they may be a `ModuleWithProviders`.
return (
!ts.isCallExpression(el) &&
// Also filter out the animations modules since they throw errors if they're imported
// multiple times and it's common for apps to use the `NoopAnimationsModule` to
// disable animations in screenshot tests.
!isClassReferenceInAngularModule(
el, /^BrowserAnimationsModule|NoopAnimationsModule$/,
'platform-browser/animations', typeChecker);
}) :
null;
el,
/^BrowserAnimationsModule|NoopAnimationsModule$/,
'platform-browser/animations',
typeChecker,
)
);
})
: null;
for (const decl of declarations) {
if (seenDeclarations.has(decl)) {
@ -668,7 +825,7 @@ function analyzeTestingModules(
imports = new Set();
componentImports.set(decorator.node, imports);
}
importElements.forEach(imp => imports!.add(imp));
importElements.forEach((imp) => imports!.add(imp));
}
}
}
@ -684,7 +841,9 @@ function analyzeTestingModules(
* @param typeChecker
*/
function extractDeclarationsFromTestObject(
obj: ts.ObjectLiteralExpression, typeChecker: ts.TypeChecker): ts.ClassDeclaration[] {
obj: ts.ObjectLiteralExpression,
typeChecker: ts.TypeChecker,
): ts.ClassDeclaration[] {
const results: ts.ClassDeclaration[] = [];
const declarations = findLiteralProperty(obj, 'declarations');
@ -705,12 +864,13 @@ function extractDeclarationsFromTestObject(
}
/** Extracts the metadata object literal from an Angular decorator. */
function extractMetadataLiteral(decorator: ts.Decorator): ts.ObjectLiteralExpression|null {
function extractMetadataLiteral(decorator: ts.Decorator): ts.ObjectLiteralExpression | null {
// `arguments[0]` is the metadata object literal.
return ts.isCallExpression(decorator.expression) && decorator.expression.arguments.length === 1 &&
ts.isObjectLiteralExpression(decorator.expression.arguments[0]) ?
decorator.expression.arguments[0] :
null;
return ts.isCallExpression(decorator.expression) &&
decorator.expression.arguments.length === 1 &&
ts.isObjectLiteralExpression(decorator.expression.arguments[0])
? decorator.expression.arguments[0]
: null;
}
/**
@ -720,13 +880,15 @@ function extractMetadataLiteral(decorator: ts.Decorator): ts.ObjectLiteralExpres
* @param templateTypeChecker
*/
function isStandaloneDeclaration(
node: ts.ClassDeclaration, declarationsInMigration: Set<ts.ClassDeclaration>,
templateTypeChecker: TemplateTypeChecker): boolean {
node: ts.ClassDeclaration,
declarationsInMigration: Set<ts.ClassDeclaration>,
templateTypeChecker: TemplateTypeChecker,
): boolean {
if (declarationsInMigration.has(node)) {
return true;
}
const metadata =
templateTypeChecker.getDirectiveMetadata(node) || templateTypeChecker.getPipeMetadata(node);
templateTypeChecker.getDirectiveMetadata(node) || templateTypeChecker.getPipeMetadata(node);
return metadata != null && metadata.isStandalone;
}

View file

@ -18,7 +18,7 @@ import {closestNode} from '../../utils/typescript/nodes';
export type NodeLookup = Map<number, ts.Node[]>;
/** Utility to type a class declaration with a name. */
export type NamedClassDeclaration = ts.ClassDeclaration&{name: ts.Identifier};
export type NamedClassDeclaration = ts.ClassDeclaration & {name: ts.Identifier};
/** Text span of an AST node. */
export type ReferenceSpan = [start: number, end: number];
@ -40,7 +40,7 @@ export class UniqueItemTracker<K, V> {
}
}
get(key: K): Set<V>|undefined {
get(key: K): Set<V> | undefined {
return this._nodes.get(key);
}
@ -51,18 +51,21 @@ export class UniqueItemTracker<K, V> {
/** Resolves references to nodes. */
export class ReferenceResolver {
private _languageService: ts.LanguageService|undefined;
private _languageService: ts.LanguageService | undefined;
/**
* If set, allows the language service to *only* read a specific file.
* Used to speed up single-file lookups.
*/
private _tempOnlyFile: string|null = null;
private _tempOnlyFile: string | null = null;
constructor(
private _program: NgtscProgram, private _host: ts.CompilerHost,
private _rootFileNames: string[], private _basePath: string,
private _excludedFiles?: RegExp) {}
private _program: NgtscProgram,
private _host: ts.CompilerHost,
private _rootFileNames: string[],
private _basePath: string,
private _excludedFiles?: RegExp,
) {}
/** Finds all references to a node within the entire project. */
findReferencesInProject(node: ts.Node): ReferencesByFile {
@ -89,8 +92,9 @@ export class ReferenceResolver {
results.set(ref.fileName, []);
}
results.get(ref.fileName)!.push(
[ref.textSpan.start, ref.textSpan.start + ref.textSpan.length]);
results
.get(ref.fileName)!
.push([ref.textSpan.start, ref.textSpan.start + ref.textSpan.length]);
}
}
}
@ -108,13 +112,14 @@ export class ReferenceResolver {
const nodeStart = node.getStart();
const results: ReferenceSpan[] = [];
let highlights: ts.DocumentHighlights[]|undefined;
let highlights: ts.DocumentHighlights[] | undefined;
// The language service can throw if it fails to read a file.
// Silently continue since we're making the lookup on a best effort basis.
try {
highlights =
this._getLanguageService().getDocumentHighlights(fileName, nodeStart, [fileName]);
highlights = this._getLanguageService().getDocumentHighlights(fileName, nodeStart, [
fileName,
]);
} catch (e: any) {
console.error('Failed reference lookup for node ' + node.getText(), e.message);
}
@ -124,7 +129,10 @@ export class ReferenceResolver {
// We are pretty much guaranteed to only have one match from the current file since it is
// the only one being passed in `getDocumentHighlight`, but we check here just in case.
if (file.fileName === fileName) {
for (const {textSpan: {start, length}, kind} of file.highlightSpans) {
for (const {
textSpan: {start, length},
kind,
} of file.highlightSpans) {
if (kind !== ts.HighlightSpanKind.none) {
results.push([start, start + length]);
}
@ -140,8 +148,10 @@ export class ReferenceResolver {
/** Used by the language service */
private _readFile(path: string) {
if ((this._tempOnlyFile !== null && path !== this._tempOnlyFile) ||
this._excludedFiles?.test(path)) {
if (
(this._tempOnlyFile !== null && path !== this._tempOnlyFile) ||
this._excludedFiles?.test(path)
) {
return '';
}
return this._host.readFile(path);
@ -152,28 +162,33 @@ export class ReferenceResolver {
if (!this._languageService) {
const rootFileNames = this._rootFileNames.slice();
this._program.getTsProgram().getSourceFiles().forEach(({fileName}) => {
if (!this._excludedFiles?.test(fileName) && !rootFileNames.includes(fileName)) {
rootFileNames.push(fileName);
}
});
this._program
.getTsProgram()
.getSourceFiles()
.forEach(({fileName}) => {
if (!this._excludedFiles?.test(fileName) && !rootFileNames.includes(fileName)) {
rootFileNames.push(fileName);
}
});
this._languageService = ts.createLanguageService(
{
getCompilationSettings: () => this._program.getTsProgram().getCompilerOptions(),
getScriptFileNames: () => rootFileNames,
// The files won't change so we can return the same version.
getScriptVersion: () => '0',
getScriptSnapshot: (path: string) => {
const content = this._readFile(path);
return content ? ts.ScriptSnapshot.fromString(content) : undefined;
},
getCurrentDirectory: () => this._basePath,
getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
readFile: path => this._readFile(path),
fileExists: (path: string) => this._host.fileExists(path)
{
getCompilationSettings: () => this._program.getTsProgram().getCompilerOptions(),
getScriptFileNames: () => rootFileNames,
// The files won't change so we can return the same version.
getScriptVersion: () => '0',
getScriptSnapshot: (path: string) => {
const content = this._readFile(path);
return content ? ts.ScriptSnapshot.fromString(content) : undefined;
},
ts.createDocumentRegistry(), ts.LanguageServiceMode.PartialSemantic);
getCurrentDirectory: () => this._basePath,
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
readFile: (path) => this._readFile(path),
fileExists: (path: string) => this._host.fileExists(path),
},
ts.createDocumentRegistry(),
ts.LanguageServiceMode.PartialSemantic,
);
}
return this._languageService;
@ -206,9 +221,12 @@ export function getNodeLookup(sourceFile: ts.SourceFile): NodeLookup {
* @param results Set in which to store the results.
*/
export function offsetsToNodes(
lookup: NodeLookup, offsets: ReferenceSpan[], results: Set<ts.Node>): Set<ts.Node> {
lookup: NodeLookup,
offsets: ReferenceSpan[],
results: Set<ts.Node>,
): Set<ts.Node> {
for (const [start, end] of offsets) {
const match = lookup.get(start)?.find(node => node.getEnd() === end);
const match = lookup.get(start)?.find((node) => node.getEnd() === end);
if (match) {
results.add(match);
@ -224,16 +242,22 @@ export function offsetsToNodes(
* @param typeChecker
*/
export function findClassDeclaration(
reference: ts.Node, typeChecker: ts.TypeChecker): ts.ClassDeclaration|null {
return typeChecker.getTypeAtLocation(reference).getSymbol()?.declarations?.find(
ts.isClassDeclaration) ||
null;
reference: ts.Node,
typeChecker: ts.TypeChecker,
): ts.ClassDeclaration | null {
return (
typeChecker
.getTypeAtLocation(reference)
.getSymbol()
?.declarations?.find(ts.isClassDeclaration) || null
);
}
/** Finds a property with a specific name in an object literal expression. */
export function findLiteralProperty(literal: ts.ObjectLiteralExpression, name: string) {
return literal.properties.find(
prop => prop.name && ts.isIdentifier(prop.name) && prop.name.text === name);
(prop) => prop.name && ts.isIdentifier(prop.name) && prop.name.text === name,
);
}
/** Gets a relative path between two files that can be used inside a TypeScript import. */
@ -251,10 +275,11 @@ export function getRelativeImportPath(fromFile: string, toFile: string): string
/** Function used to remap the generated `imports` for a component to known shorter aliases. */
export function knownInternalAliasRemapper(imports: PotentialImport[]) {
return imports.map(
current => current.moduleSpecifier === '@angular/common' && current.symbolName === 'NgForOf' ?
{...current, symbolName: 'NgFor'} :
current);
return imports.map((current) =>
current.moduleSpecifier === '@angular/common' && current.symbolName === 'NgForOf'
? {...current, symbolName: 'NgFor'}
: current,
);
}
/**
@ -263,7 +288,9 @@ export function knownInternalAliasRemapper(imports: PotentialImport[]) {
* @param predicate Predicate that the result needs to pass.
*/
export function closestOrSelf<T extends ts.Node>(
node: ts.Node, predicate: (n: ts.Node) => n is T): T|null {
node: ts.Node,
predicate: (n: ts.Node) => n is T,
): T | null {
return predicate(node) ? node : closestNode(node, predicate);
}
@ -275,24 +302,31 @@ export function closestOrSelf<T extends ts.Node>(
* @param typeChecker
*/
export function isClassReferenceInAngularModule(
node: ts.Node, className: string|RegExp, moduleName: string,
typeChecker: ts.TypeChecker): boolean {
node: ts.Node,
className: string | RegExp,
moduleName: string,
typeChecker: ts.TypeChecker,
): boolean {
const symbol = typeChecker.getTypeAtLocation(node).getSymbol();
const externalName = `@angular/${moduleName}`;
const internalName = `angular2/rc/packages/${moduleName}`;
return !!symbol?.declarations?.some(decl => {
return !!symbol?.declarations?.some((decl) => {
const closestClass = closestOrSelf(decl, ts.isClassDeclaration);
const closestClassFileName = closestClass?.getSourceFile().fileName;
if (!closestClass || !closestClassFileName || !closestClass.name ||
!ts.isIdentifier(closestClass.name) ||
(!closestClassFileName.includes(externalName) &&
!closestClassFileName.includes(internalName))) {
if (
!closestClass ||
!closestClassFileName ||
!closestClass.name ||
!ts.isIdentifier(closestClass.name) ||
(!closestClassFileName.includes(externalName) && !closestClassFileName.includes(internalName))
) {
return false;
}
return typeof className === 'string' ? closestClass.name.text === className :
className.test(closestClass.name.text);
return typeof className === 'string'
? closestClass.name.text === className
: className.test(closestClass.name.text);
});
}

View file

@ -22,8 +22,9 @@ describe('all migrations', () => {
let previousWorkingDir: string;
const migrationCollectionPath = runfiles.resolvePackageRelative('../migrations.json');
const allMigrationSchematics =
Object.keys((JSON.parse(fs.readFileSync(migrationCollectionPath, 'utf8')) as any).schematics);
const allMigrationSchematics = Object.keys(
(JSON.parse(fs.readFileSync(migrationCollectionPath, 'utf8')) as any).schematics,
);
beforeEach(() => {
runner = new SchematicTestRunner('test', migrationCollectionPath);
@ -31,13 +32,15 @@ describe('all migrations', () => {
tree = new UnitTestTree(new HostTree(host));
writeFile('/node_modules/@angular/core/index.d.ts', `export const MODULE: any;`);
writeFile('/angular.json', JSON.stringify({
version: 1,
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
}));
writeFile(
'/angular.json',
JSON.stringify({
version: 1,
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}},
}),
);
writeFile('/tsconfig.json', `{}`);
previousWorkingDir = shx.pwd();
tmpDirPath = getSystemPath(host.root);
@ -63,21 +66,24 @@ describe('all migrations', () => {
throw Error('No migration schematics found.');
}
allMigrationSchematics.forEach(name => {
allMigrationSchematics.forEach((name) => {
describe(name, () => createTests(name));
});
function createTests(migrationName: string) {
// Regression test for: https://github.com/angular/angular/issues/36346.
it('should not throw if non-existent symbols are imported with rootDirs', async () => {
writeFile(`/tsconfig.json`, JSON.stringify({
compilerOptions: {
rootDirs: [
'./generated',
]
}
}));
writeFile('/index.ts', `
writeFile(
`/tsconfig.json`,
JSON.stringify({
compilerOptions: {
rootDirs: ['./generated'],
},
}),
);
writeFile(
'/index.ts',
`
import {Renderer} from '@angular/core';
const variableDecl: Renderer = null;
@ -85,7 +91,8 @@ describe('all migrations', () => {
export class Test {
constructor(renderer: Renderer) {}
}
`);
`,
);
let error: any = null;
try {

View file

@ -13,8 +13,9 @@ import shx from 'shelljs';
import {Configuration, Linter} from 'tslint';
describe('Google3 waitForAsync TSLint rule', () => {
const rulesDirectory =
dirname(runfiles.resolvePackageRelative('../../migrations/google3/waitForAsyncCjsRule.js'));
const rulesDirectory = dirname(
runfiles.resolvePackageRelative('../../migrations/google3/waitForAsyncCjsRule.js'),
);
let tmpDir: string;
@ -23,19 +24,25 @@ describe('Google3 waitForAsync TSLint rule', () => {
shx.mkdir('-p', tmpDir);
// We need to declare the Angular symbols we're testing for, otherwise type checking won't work.
writeFile('testing.d.ts', `
writeFile(
'testing.d.ts',
`
export declare function async(fn: Function): any;
`);
`,
);
writeFile('tsconfig.json', JSON.stringify({
compilerOptions: {
module: 'es2015',
baseUrl: './',
paths: {
'@angular/core/testing': ['testing.d.ts'],
}
},
}));
writeFile(
'tsconfig.json',
JSON.stringify({
compilerOptions: {
module: 'es2015',
baseUrl: './',
paths: {
'@angular/core/testing': ['testing.d.ts'],
},
},
}),
);
});
afterEach(() => shx.rm('-r', tmpDir));
@ -45,7 +52,7 @@ describe('Google3 waitForAsync TSLint rule', () => {
const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program);
const config = Configuration.parseConfigFile({rules: {'wait-for-async-cjs': true}});
program.getRootFileNames().forEach(fileName => {
program.getRootFileNames().forEach((fileName) => {
linter.lint(fileName, program.getSourceFile(fileName)!.getFullText(), config);
});
@ -61,7 +68,9 @@ describe('Google3 waitForAsync TSLint rule', () => {
}
it('should flag async imports and usages', () => {
writeFile('/index.ts', `
writeFile(
'/index.ts',
`
import { async, inject } from '@angular/core/testing';
it('should work', async(() => {
@ -71,10 +80,11 @@ describe('Google3 waitForAsync TSLint rule', () => {
it('should also work', async(() => {
expect(inject('bar')).toBe('bar');
}));
`);
`,
);
const linter = runTSLint(false);
const failures = linter.getResult().failures.map(failure => failure.getFailure());
const failures = linter.getResult().failures.map((failure) => failure.getFailure());
expect(failures.length).toBe(3);
expect(failures[0]).toMatch(/Imports of the deprecated async function are not allowed/);
expect(failures[1]).toMatch(/References to the deprecated async function are not allowed/);
@ -82,42 +92,53 @@ describe('Google3 waitForAsync TSLint rule', () => {
});
it('should change async imports to waitForAsync', () => {
writeFile('/index.ts', `
writeFile(
'/index.ts',
`
import { async, inject } from '@angular/core/testing';
it('should work', async(() => {
expect(inject('foo')).toBe('foo');
}));
`);
`,
);
runTSLint(true);
expect(getFile('/index.ts'))
.toContain(`import { inject, waitForAsync } from '@angular/core/testing';`);
expect(getFile('/index.ts')).toContain(
`import { inject, waitForAsync } from '@angular/core/testing';`,
);
});
it('should change aliased async imports to waitForAsync', () => {
writeFile('/index.ts', `
writeFile(
'/index.ts',
`
import { async as renamedAsync, inject } from '@angular/core/testing';
it('should work', renamedAsync(() => {
expect(inject('foo')).toBe('foo');
}));
`);
`,
);
runTSLint(true);
expect(getFile('/index.ts'))
.toContain(`import { inject, waitForAsync as renamedAsync } from '@angular/core/testing';`);
expect(getFile('/index.ts')).toContain(
`import { inject, waitForAsync as renamedAsync } from '@angular/core/testing';`,
);
});
it('should not change async imports if they are not from @angular/core/testing', () => {
writeFile('/index.ts', `
writeFile(
'/index.ts',
`
import { inject } from '@angular/core/testing';
import { async } from './my-test-library';
it('should work', async(() => {
expect(inject('foo')).toBe('foo');
}));
`);
`,
);
runTSLint(true);
const content = getFile('/index.ts');
@ -126,7 +147,9 @@ describe('Google3 waitForAsync TSLint rule', () => {
});
it('should not change imports if waitForAsync was already imported', () => {
writeFile('/index.ts', `
writeFile(
'/index.ts',
`
import { async, inject, waitForAsync } from '@angular/core/testing';
it('should work', async(() => {
@ -136,15 +159,19 @@ describe('Google3 waitForAsync TSLint rule', () => {
it('should also work', waitForAsync(() => {
expect(inject('bar')).toBe('bar');
}));
`);
`,
);
runTSLint(true);
expect(getFile('/index.ts'))
.toContain(`import { async, inject, waitForAsync } from '@angular/core/testing';`);
expect(getFile('/index.ts')).toContain(
`import { async, inject, waitForAsync } from '@angular/core/testing';`,
);
});
it('should change calls from `async` to `waitForAsync`', () => {
writeFile('/index.ts', `
writeFile(
'/index.ts',
`
import { async, inject } from '@angular/core/testing';
it('should work', async(() => {
@ -154,7 +181,8 @@ describe('Google3 waitForAsync TSLint rule', () => {
it('should also work', async(() => {
expect(inject('bar')).toBe('bar');
}));
`);
`,
);
runTSLint(true);
@ -165,19 +193,23 @@ describe('Google3 waitForAsync TSLint rule', () => {
});
it('should not change aliased calls', () => {
writeFile('/index.ts', `
writeFile(
'/index.ts',
`
import { async as renamedAsync, inject } from '@angular/core/testing';
it('should work', renamedAsync(() => {
expect(inject('foo')).toBe('foo');
}));
`);
`,
);
runTSLint(true);
const content = getFile('/index.ts');
expect(content).toContain(
`import { inject, waitForAsync as renamedAsync } from '@angular/core/testing';`);
`import { inject, waitForAsync as renamedAsync } from '@angular/core/testing';`,
);
expect(content).toContain(`it('should work', renamedAsync(() => {`);
});
});

View file

@ -23,7 +23,7 @@ export function dedent(strings: TemplateStringsArray, ...values: any[]) {
return joinedString;
}
const minLineIndent = Math.min(...matches.map(el => el.length));
const minLineIndent = Math.min(...matches.map((el) => el.length));
const omitMinIndentRegex = new RegExp(`^[ \\t]{${minLineIndent}}`, 'gm');
const omitEmptyLineWhitespaceRegex = /^[ \t]+$/gm;
const result = minLineIndent > 0 ? joinedString.replace(omitMinIndentRegex, '') : joinedString;

View file

@ -34,21 +34,21 @@ describe('Http providers migration', () => {
tree = new UnitTestTree(new HostTree(host));
writeFile(
'/tsconfig.json',
JSON.stringify({
compilerOptions: {
lib: ['es2015'],
strictNullChecks: true,
},
}),
'/tsconfig.json',
JSON.stringify({
compilerOptions: {
lib: ['es2015'],
strictNullChecks: true,
},
}),
);
writeFile(
'/angular.json',
JSON.stringify({
version: 1,
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}},
}),
'/angular.json',
JSON.stringify({
version: 1,
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}},
}),
);
previousWorkingDir = shx.pwd();
@ -66,8 +66,8 @@ describe('Http providers migration', () => {
it('should replace HttpClientModule', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@angular/common/http';
import { CommonModule } from '@angular/common';
@ -96,14 +96,14 @@ describe('Http providers migration', () => {
expect(content).toMatch(/import.*withJsonpSupport/);
expect(content).toMatch(/import.*withXsrfConfiguration/);
expect(content).toContain(
`provideHttpClient(withInterceptorsFromDi(), withJsonpSupport(), withXsrfConfiguration({ cookieName: 'foobar' }))`,
`provideHttpClient(withInterceptorsFromDi(), withJsonpSupport(), withXsrfConfiguration({ cookieName: 'foobar' }))`,
);
});
it('should replace HttpClientModule with existing providers ', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@angular/common/http';
@ -131,14 +131,14 @@ describe('Http providers migration', () => {
expect(content).toContain(`HttpTransferCacheOptions`);
expect(content).toContain(`provideConfig({ someConfig: 'foobar' })`);
expect(content).toContain(
`provideHttpClient(withInterceptorsFromDi(), withJsonpSupport(), withXsrfConfiguration({ cookieName: 'foobar' }))`,
`provideHttpClient(withInterceptorsFromDi(), withJsonpSupport(), withXsrfConfiguration({ cookieName: 'foobar' }))`,
);
});
it('should replace HttpClientModule & HttpClientXsrfModule.disable()', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@angular/common/http';
@ -166,14 +166,14 @@ describe('Http providers migration', () => {
expect(content).toContain(`HttpTransferCacheOptions`);
expect(content).toContain(`provideConfig({ someConfig: 'foobar' })`);
expect(content).toContain(
`provideHttpClient(withInterceptorsFromDi(), withJsonpSupport(), withNoXsrfProtection())`,
`provideHttpClient(withInterceptorsFromDi(), withJsonpSupport(), withNoXsrfProtection())`,
);
});
it('should replace HttpClientModule & base HttpClientXsrfModule', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@angular/common/http';
@ -201,14 +201,14 @@ describe('Http providers migration', () => {
expect(content).toContain(`HttpTransferCacheOptions`);
expect(content).toContain(`provideConfig({ someConfig: 'foobar' })`);
expect(content).toContain(
`provideHttpClient(withInterceptorsFromDi(), withXsrfConfiguration())`,
`provideHttpClient(withInterceptorsFromDi(), withXsrfConfiguration())`,
);
});
it('should handle a migration with 2 modules in the same file ', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@angular/common/http';
@ -237,14 +237,14 @@ describe('Http providers migration', () => {
expect(content).toContain(`provideConfig({ someConfig: 'foobar' })`);
expect(content).toContain(`provideHttpClient(withInterceptorsFromDi(), withJsonpSupport())`);
expect(content).toContain(
`provideHttpClient(withInterceptorsFromDi(), withNoXsrfProtection())`,
`provideHttpClient(withInterceptorsFromDi(), withNoXsrfProtection())`,
);
});
it('should handle a migration for acomponent ', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { Component } from '@angular/core';
import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';
@ -266,8 +266,8 @@ describe('Http providers migration', () => {
it('should not migrate HttpClientModule from another package', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientJsonpModule, HttpClientXsrfModule, HttpTransferCacheOptions } from '@not-angular/common/http';
@ -295,17 +295,17 @@ describe('Http providers migration', () => {
expect(content).toContain(`HttpTransferCacheOptions`);
expect(content).toContain(`provideConfig({ someConfig: 'foobar' })`);
expect(content).not.toContain(
`provideHttpClient(withInterceptorsFromDi(), withJsonpSupport())`,
`provideHttpClient(withInterceptorsFromDi(), withJsonpSupport())`,
);
expect(content).not.toContain(
`provideHttpClient(withInterceptorsFromDi(), withNoXsrfProtection())`,
`provideHttpClient(withInterceptorsFromDi(), withNoXsrfProtection())`,
);
});
it('should migrate HttpClientTestingModule', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
@ -322,13 +322,14 @@ describe('Http providers migration', () => {
expect(content).not.toContain(`HttpClientTestingModule`);
expect(content).toMatch(/import.*provideHttpClientTesting/);
expect(content).toContain(
`provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()`);
`provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()`,
);
});
it('should not migrate HttpClientTestingModule from outside package', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@not-angular/common/http/testing';
@ -348,8 +349,8 @@ describe('Http providers migration', () => {
it('shouldmigrate NgModule + TestBed.configureTestingModule in the same file', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
@ -378,18 +379,21 @@ describe('Http providers migration', () => {
expect(content).toContain('provideHttpClientTesting');
expect(content).toContain('provideHttpClient(withInterceptorsFromDi(), withJsonpSupport())');
expect(content).toContain(
'provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()');
'provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()',
);
expect(content).toContain(
`import { withInterceptorsFromDi, withJsonpSupport, provideHttpClient } from '@angular/common/http';`);
`import { withInterceptorsFromDi, withJsonpSupport, provideHttpClient } from '@angular/common/http';`,
);
expect(content).toContain(
`import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';`);
`import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';`,
);
});
it('should not change a decorator with no arguments', async () => {
writeFile(
'/index.ts',
`
'/index.ts',
`
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';

View file

@ -34,10 +34,13 @@ describe('Invalid two-way bindings migration', () => {
tree = new UnitTestTree(new HostTree(host));
writeFile('/tsconfig.json', '{}');
writeFile('/angular.json', JSON.stringify({
version: 1,
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
}));
writeFile(
'/angular.json',
JSON.stringify({
version: 1,
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}},
}),
);
previousWorkingDir = shx.pwd();
tmpDirPath = getSystemPath(host.root);
@ -53,71 +56,89 @@ describe('Invalid two-way bindings migration', () => {
});
it('should migrate a two-way binding with a binary expression', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="a && b.c"/>\`
})
class Comp {}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="a && b.c" (ngModelChange)="a && (b.c = $event)"/>`');
'template: `<input [ngModel]="a && b.c" (ngModelChange)="a && (b.c = $event)"/>`',
);
});
it('should migrate a two-way binding with a single unary expression', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="!a.b"/>\`
})
class Comp {}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="!a.b" (ngModelChange)="a.b = $event"/>`');
'template: `<input [ngModel]="!a.b" (ngModelChange)="a.b = $event"/>`',
);
});
it('should migrate a two-way binding with a nested unary expression', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="!!!!!!!a.b"/>\`
})
class Comp {}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="!!!!!!!a.b" (ngModelChange)="a.b = $event"/>`');
'template: `<input [ngModel]="!!!!!!!a.b" (ngModelChange)="a.b = $event"/>`',
);
});
it('should migrate a two-way binding with a conditional expression', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="a ? b : c.d"/>\`
})
class Comp {}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="a ? b : c.d" (ngModelChange)="a ? b : c.d = $event"/>`');
'template: `<input [ngModel]="a ? b : c.d" (ngModelChange)="a ? b : c.d = $event"/>`',
);
});
it('should migrate multiple inline templates in the same file', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
@ -129,124 +150,142 @@ describe('Invalid two-way bindings migration', () => {
template: \`<input [(ngModel)]="a || b"/>\`
})
class Comp2 {}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>`');
'template: `<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>`',
);
expect(content).toContain(
'template: `<input [ngModel]="a || b" (ngModelChange)="a || (b = $event)"/>`');
'template: `<input [ngModel]="a || b" (ngModelChange)="a || (b = $event)"/>`',
);
});
it('should migrate an external template', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
templateUrl: './comp.html'
})
class Comp {}
`);
`,
);
writeFile('/comp.html', [
`<div>`,
`hello`,
`<span>`,
`<input [(ngModel)]="a && b"/>`,
`</span>`,
`</div>`,
].join('\n'));
writeFile(
'/comp.html',
[`<div>`, `hello`, `<span>`, `<input [(ngModel)]="a && b"/>`, `</span>`, `</div>`].join('\n'),
);
await runMigration();
const content = tree.readContent('/comp.html');
expect(content).toBe([
`<div>`,
`hello`,
`<span>`,
`<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>`,
`</span>`,
`</div>`,
].join('\n'));
expect(content).toBe(
[
`<div>`,
`hello`,
`<span>`,
`<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>`,
`</span>`,
`</div>`,
].join('\n'),
);
});
it('should migrate a template referenced by multiple components', async () => {
writeFile('/comp-a.ts', `
writeFile(
'/comp-a.ts',
`
import {Component} from '@angular/core';
@Component({
templateUrl: './comp.html'
})
class CompA {}
`);
`,
);
writeFile('/comp-b.ts', `
writeFile(
'/comp-b.ts',
`
import {Component} from '@angular/core';
@Component({
templateUrl: './comp.html'
})
class CompB {}
`);
`,
);
writeFile('/comp.html', [
`<div>`,
`hello`,
`<span>`,
`<input [(ngModel)]="a && b"/>`,
`</span>`,
`</div>`,
].join('\n'));
writeFile(
'/comp.html',
[`<div>`, `hello`, `<span>`, `<input [(ngModel)]="a && b"/>`, `</span>`, `</div>`].join('\n'),
);
await runMigration();
const content = tree.readContent('/comp.html');
expect(content).toBe([
`<div>`,
`hello`,
`<span>`,
`<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>`,
`</span>`,
`</div>`,
].join('\n'));
expect(content).toBe(
[
`<div>`,
`hello`,
`<span>`,
`<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>`,
`</span>`,
`</div>`,
].join('\n'),
);
});
it('should migrate multiple two-way bindings on the same element', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(foo)]="a && b" bar="123" [(baz)]="!!a"/>\`
})
class Comp {}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: \`<input [foo]="a && b" (fooChange)="a && (b = $event)" ' +
'bar="123" [baz]="!!a" (bazChange)="a = $event"/>\`');
'template: `<input [foo]="a && b" (fooChange)="a && (b = $event)" ' +
'bar="123" [baz]="!!a" (bazChange)="a = $event"/>`',
);
});
it('should not stop the migration if a file cannot be read', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
templateUrl: './does-not-exist.html'
})
class BrokenComp {}
`);
`,
);
writeFile('/other-comp.ts', `
writeFile(
'/other-comp.ts',
`
import {Component} from '@angular/core';
@Component({
templateUrl: './comp.html'
})
class Comp {}
`);
`,
);
writeFile('/comp.html', '<input [(ngModel)]="a || b"/>');
@ -257,7 +296,9 @@ describe('Invalid two-way bindings migration', () => {
});
it('should migrate a component that is not at the top level', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
function foo() {
@ -266,76 +307,90 @@ describe('Invalid two-way bindings migration', () => {
})
class Comp {}
}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [ngModel]="a || b" (ngModelChange)="a || (b = $event)"/>`');
'template: `<input [ngModel]="a || b" (ngModelChange)="a || (b = $event)"/>`',
);
});
it('should preserve a valid expression', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="a.b.c"/>\`
})
class Comp {}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain('template: `<input [(ngModel)]="a.b.c"/>`');
});
it('should not migrate an invalid expression if an event listener for the same binding exists',
async () => {
writeFile('/comp.ts', `
it('should not migrate an invalid expression if an event listener for the same binding exists', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="a || b" (ngModelChange)="foo($event)"/>\`
})
class Comp {}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [(ngModel)]="a || b" (ngModelChange)="foo($event)"/>`');
});
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<input [(ngModel)]="a || b" (ngModelChange)="foo($event)"/>`',
);
});
it('should not migrate an invalid expression if a property binding for the same binding exists',
async () => {
writeFile('/comp.ts', `
it('should not migrate an invalid expression if a property binding for the same binding exists', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<input [(ngModel)]="a || b" [ngModel]="foo"/>\`
})
class Comp {}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain('template: `<input [(ngModel)]="a || b" [ngModel]="foo"/>`');
});
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain('template: `<input [(ngModel)]="a || b" [ngModel]="foo"/>`');
});
it('should migrate a two-way binding on an ng-template', async () => {
writeFile('/comp.ts', `
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
@Component({
template: \`<ng-template [(ngModel)]="a && b.c"></ng-template>\`
})
class Comp {}
`);
`,
);
await runMigration();
const content = tree.readContent('/comp.ts');
expect(content).toContain(
'template: `<ng-template [ngModel]="a && b.c" (ngModelChange)="a && (b.c = $event)"></ng-template>`');
'template: `<ng-template [ngModel]="a && b.c" (ngModelChange)="a && (b.c = $event)"></ng-template>`',
);
});
});

View file

@ -20,20 +20,24 @@ describe('project tsconfig paths', () => {
it('should detect build tsconfig path inside of angular.json file', async () => {
testTree.create('/my-custom-config.json', '');
testTree.create('/angular.json', JSON.stringify({
version: 1,
projects: {
my_name: {root: '', architect: {build: {options: {tsConfig: './my-custom-config.json'}}}}
}
}));
testTree.create(
'/angular.json',
JSON.stringify({
version: 1,
projects: {
my_name: {root: '', architect: {build: {options: {tsConfig: './my-custom-config.json'}}}},
},
}),
);
expect((await getProjectTsConfigPaths(testTree)).buildPaths).toEqual(['my-custom-config.json']);
});
it('should be able to read workspace configuration which is using jsconc-parser features',
async () => {
testTree.create('/my-build-config.json', '');
testTree.create('/angular.json', `{
it('should be able to read workspace configuration which is using jsconc-parser features', async () => {
testTree.create('/my-build-config.json', '');
testTree.create(
'/angular.json',
`{
"version": 1,
// Comments are supported in the workspace configurations.
"projects": {
@ -48,42 +52,51 @@ describe('project tsconfig paths', () => {
}
}
},
}`);
}`,
);
expect((await getProjectTsConfigPaths(testTree)).buildPaths).toEqual([
'my-build-config.json'
]);
});
expect((await getProjectTsConfigPaths(testTree)).buildPaths).toEqual(['my-build-config.json']);
});
it('should detect test tsconfig path inside of angular.json file', async () => {
testTree.create('/my-test-config.json', '');
testTree.create('/angular.json', JSON.stringify({
version: 1,
projects:
{my_name: {root: '', architect: {test: {options: {tsConfig: './my-test-config.json'}}}}}
}));
testTree.create(
'/angular.json',
JSON.stringify({
version: 1,
projects: {
my_name: {root: '', architect: {test: {options: {tsConfig: './my-test-config.json'}}}},
},
}),
);
expect((await getProjectTsConfigPaths(testTree)).testPaths).toEqual(['my-test-config.json']);
});
it('should detect test tsconfig path inside of .angular.json file', async () => {
testTree.create('/my-test-config.json', '');
testTree.create('/.angular.json', JSON.stringify({
version: 1,
projects: {
with_tests: {root: '', architect: {test: {options: {tsConfig: './my-test-config.json'}}}}
}
}));
testTree.create(
'/.angular.json',
JSON.stringify({
version: 1,
projects: {
with_tests: {root: '', architect: {test: {options: {tsConfig: './my-test-config.json'}}}},
},
}),
);
expect((await getProjectTsConfigPaths(testTree)).testPaths).toEqual(['my-test-config.json']);
});
it('should not return duplicate tsconfig files', async () => {
testTree.create('/tsconfig.json', '');
testTree.create('/.angular.json', JSON.stringify({
version: 1,
projects: {app: {root: '', architect: {build: {options: {tsConfig: 'tsconfig.json'}}}}}
}));
testTree.create(
'/.angular.json',
JSON.stringify({
version: 1,
projects: {app: {root: '', architect: {build: {options: {tsConfig: 'tsconfig.json'}}}}},
}),
);
expect((await getProjectTsConfigPaths(testTree)).buildPaths).toEqual(['tsconfig.json']);
});

File diff suppressed because it is too large Load diff

View file

@ -34,14 +34,18 @@ export class ChangeTracker {
private readonly _changes = new Map<ts.SourceFile, PendingChange[]>();
private readonly _importManager: ImportManager;
constructor(private _printer: ts.Printer, private _importRemapper?: ImportRemapper) {
constructor(
private _printer: ts.Printer,
private _importRemapper?: ImportRemapper,
) {
this._importManager = new ImportManager(
currentFile => ({
addNewImport: (start, text) => this.insertText(currentFile, start, text),
updateExistingImport: (namedBindings, text) => this.replaceText(
currentFile, namedBindings.getStart(), namedBindings.getWidth(), text),
}),
this._printer);
(currentFile) => ({
addNewImport: (start, text) => this.insertText(currentFile, start, text),
updateExistingImport: (namedBindings, text) =>
this.replaceText(currentFile, namedBindings.getStart(), namedBindings.getWidth(), text),
}),
this._printer,
);
}
/**
@ -75,12 +79,18 @@ export class ChangeTracker {
* without it.
*/
replaceNode(
oldNode: ts.Node, newNode: ts.Node, emitHint = ts.EmitHint.Unspecified,
sourceFileWhenPrinting?: ts.SourceFile): void {
oldNode: ts.Node,
newNode: ts.Node,
emitHint = ts.EmitHint.Unspecified,
sourceFileWhenPrinting?: ts.SourceFile,
): void {
const sourceFile = oldNode.getSourceFile();
this.replaceText(
sourceFile, oldNode.getStart(), oldNode.getWidth(),
this._printer.printNode(emitHint, newNode, sourceFileWhenPrinting || sourceFile));
sourceFile,
oldNode.getStart(),
oldNode.getWidth(),
this._printer.printNode(emitHint, newNode, sourceFileWhenPrinting || sourceFile),
);
}
/**
@ -88,8 +98,11 @@ export class ChangeTracker {
* @param node Node whose text should be removed.
*/
removeNode(node: ts.Node): void {
this._trackChange(
node.getSourceFile(), {start: node.getStart(), removeLength: node.getWidth(), text: ''});
this._trackChange(node.getSourceFile(), {
start: node.getStart(),
removeLength: node.getWidth(),
text: '',
});
}
/**
@ -99,8 +112,12 @@ export class ChangeTracker {
* @param moduleName Module from which the symbol is imported.
*/
addImport(
sourceFile: ts.SourceFile, symbolName: string, moduleName: string, alias: string|null = null,
keepSymbolName = false): ts.Expression {
sourceFile: ts.SourceFile,
symbolName: string,
moduleName: string,
alias: string | null = null,
keepSymbolName = false,
): ts.Expression {
if (this._importRemapper) {
moduleName = this._importRemapper(moduleName, sourceFile.fileName);
}
@ -111,7 +128,13 @@ export class ChangeTracker {
moduleName = normalizePath(moduleName);
return this._importManager.addImportToSourceFile(
sourceFile, symbolName, moduleName, alias, false, keepSymbolName);
sourceFile,
symbolName,
moduleName,
alias,
false,
keepSymbolName,
);
}
/**
@ -142,7 +165,7 @@ export class ChangeTracker {
// Insert the changes in reverse so that they're applied in reverse order.
// This ensures that the offsets of subsequent changes aren't affected by
// previous changes changing the file's text.
const insertIndex = changes.findIndex(current => current.start <= change.start);
const insertIndex = changes.findIndex((current) => current.start <= change.start);
if (insertIndex === -1) {
changes.push(change);

View file

@ -13,13 +13,15 @@ import {unwrapExpression} from './typescript/functions';
/** Interface describing metadata of an Angular class. */
export interface AngularClassMetadata {
type: 'component'|'directive';
type: 'component' | 'directive';
node: ts.ObjectLiteralExpression;
}
/** Extracts `@Directive` or `@Component` metadata from the given class. */
export function extractAngularClassMetadata(
typeChecker: ts.TypeChecker, node: ts.ClassDeclaration): AngularClassMetadata|null {
typeChecker: ts.TypeChecker,
node: ts.ClassDeclaration,
): AngularClassMetadata | null {
const decorators = ts.getDecorators(node);
if (!decorators || !decorators.length) {
@ -27,8 +29,8 @@ export function extractAngularClassMetadata(
}
const ngDecorators = getAngularDecorators(typeChecker, decorators);
const componentDecorator = ngDecorators.find(dec => dec.name === 'Component');
const directiveDecorator = ngDecorators.find(dec => dec.name === 'Directive');
const componentDecorator = ngDecorators.find((dec) => dec.name === 'Component');
const directiveDecorator = ngDecorators.find((dec) => dec.name === 'Directive');
const decorator = componentDecorator ?? directiveDecorator;
// In case no decorator could be found on the current class, skip.

View file

@ -28,16 +28,21 @@ const enum QuoteStyle {
*/
export class ImportManager {
/** Map of import declarations that need to be updated to include the given symbols. */
private updatedImports =
new Map<ts.ImportDeclaration, {propertyName?: ts.Identifier, importName: ts.Identifier}[]>();
private updatedImports = new Map<
ts.ImportDeclaration,
{propertyName?: ts.Identifier; importName: ts.Identifier}[]
>();
/** Map of source-files and their previously used identifier names. */
private usedIdentifierNames = new Map<ts.SourceFile, string[]>();
/** Map of source files and the new imports that have to be added to them. */
private newImports: Map<ts.SourceFile, {
importStartIndex: number,
defaultImports: Map<string, ts.Identifier>,
namedImports: Map<string, ts.ImportSpecifier[]>,
}> = new Map();
private newImports: Map<
ts.SourceFile,
{
importStartIndex: number;
defaultImports: Map<string, ts.Identifier>;
namedImports: Map<string, ts.ImportSpecifier[]>;
}
> = new Map();
/** Map between a file and the implied quote style for imports. */
private quoteStyles: Record<string, QuoteStyle> = {};
@ -46,33 +51,43 @@ export class ImportManager {
* the same identifier without checking the source-file again.
*/
private importCache: {
sourceFile: ts.SourceFile,
symbolName: string|null,
alias: string|null,
moduleName: string,
identifier: ts.Identifier
sourceFile: ts.SourceFile;
symbolName: string | null;
alias: string | null;
moduleName: string;
identifier: ts.Identifier;
}[] = [];
constructor(
private getUpdateRecorder: (sf: ts.SourceFile) => ImportManagerUpdateRecorder,
private printer: ts.Printer) {}
private getUpdateRecorder: (sf: ts.SourceFile) => ImportManagerUpdateRecorder,
private printer: ts.Printer,
) {}
/**
* Adds an import to the given source-file and returns the TypeScript
* identifier that can be used to access the newly imported symbol.
*/
addImportToSourceFile(
sourceFile: ts.SourceFile, symbolName: string|null, moduleName: string,
alias: string|null = null, typeImport = false, keepSymbolName = false): ts.Expression {
sourceFile: ts.SourceFile,
symbolName: string | null,
moduleName: string,
alias: string | null = null,
typeImport = false,
keepSymbolName = false,
): ts.Expression {
const sourceDir = dirname(sourceFile.fileName);
let importStartIndex = 0;
let existingImport: ts.ImportDeclaration|null = null;
let existingImport: ts.ImportDeclaration | null = null;
// In case the given import has been already generated previously, we just return
// the previous generated identifier in order to avoid duplicate generated imports.
const cachedImport = this.importCache.find(
c => c.sourceFile === sourceFile && c.symbolName === symbolName &&
c.moduleName === moduleName && c.alias === alias);
(c) =>
c.sourceFile === sourceFile &&
c.symbolName === symbolName &&
c.moduleName === moduleName &&
c.alias === alias,
);
if (cachedImport) {
return cachedImport.identifier;
}
@ -84,8 +99,11 @@ export class ImportManager {
for (let i = sourceFile.statements.length - 1; i >= 0; i--) {
const statement = sourceFile.statements[i];
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) ||
!statement.importClause) {
if (
!ts.isImportDeclaration(statement) ||
!ts.isStringLiteral(statement.moduleSpecifier) ||
!statement.importClause
) {
continue;
}
@ -95,9 +113,11 @@ export class ImportManager {
const moduleSpecifier = statement.moduleSpecifier.text;
if (moduleSpecifier.startsWith('.') &&
resolve(sourceDir, moduleSpecifier) !== resolve(sourceDir, moduleName) ||
moduleSpecifier !== moduleName) {
if (
(moduleSpecifier.startsWith('.') &&
resolve(sourceDir, moduleSpecifier) !== resolve(sourceDir, moduleName)) ||
moduleSpecifier !== moduleName
) {
continue;
}
@ -108,10 +128,11 @@ export class ImportManager {
// because these only export symbols available at runtime (no types)
if (ts.isNamespaceImport(namedBindings) && !typeImport) {
return ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier(namedBindings.name.text),
ts.factory.createIdentifier(alias || symbolName || 'default'));
ts.factory.createIdentifier(namedBindings.name.text),
ts.factory.createIdentifier(alias || symbolName || 'default'),
);
} else if (ts.isNamedImports(namedBindings) && symbolName) {
const existingElement = namedBindings.elements.find(e => {
const existingElement = namedBindings.elements.find((e) => {
// TODO(crisbeto): if an alias conflicts with an existing import, it may cause invalid
// code to be generated. This is unlikely, but we may want to revisit it in the future.
if (alias) {
@ -135,8 +156,12 @@ export class ImportManager {
}
if (existingImport) {
const {propertyName, name} =
this._getImportParts(sourceFile, symbolName!, alias, keepSymbolName);
const {propertyName, name} = this._getImportParts(
sourceFile,
symbolName!,
alias,
keepSymbolName,
);
// Since it can happen that multiple classes need to be imported within the
// specified source file and we want to add the identifiers to the existing
@ -145,8 +170,9 @@ export class ImportManager {
// would throw off the recorder offsets. We need to keep track of the new identifiers
// for the import and perform the import transformation as batches per source-file.
this.updatedImports.set(
existingImport,
(this.updatedImports.get(existingImport) || []).concat({propertyName, importName: name}));
existingImport,
(this.updatedImports.get(existingImport) || []).concat({propertyName, importName: name}),
);
// Keep track of all updated imports so that we don't generate duplicate
// similar imports as these can't be statically analyzed in the source-file yet.
@ -155,7 +181,7 @@ export class ImportManager {
return name;
}
let identifier: ts.Identifier|null = null;
let identifier: ts.Identifier | null = null;
if (!this.newImports.has(sourceFile)) {
this.newImports.set(sourceFile, {
@ -166,8 +192,12 @@ export class ImportManager {
}
if (symbolName) {
const {propertyName, name} =
this._getImportParts(sourceFile, symbolName, alias, keepSymbolName);
const {propertyName, name} = this._getImportParts(
sourceFile,
symbolName,
alias,
keepSymbolName,
);
const importMap = this.newImports.get(sourceFile)!.namedImports;
identifier = name;
@ -200,13 +230,19 @@ export class ImportManager {
const recorder = this.getUpdateRecorder(sourceFile);
const namedBindings = importDecl.importClause!.namedBindings as ts.NamedImports;
const newNamedBindings = ts.factory.updateNamedImports(
namedBindings,
namedBindings.elements.concat(expressions.map(
({propertyName, importName}) =>
ts.factory.createImportSpecifier(false, propertyName, importName))));
namedBindings,
namedBindings.elements.concat(
expressions.map(({propertyName, importName}) =>
ts.factory.createImportSpecifier(false, propertyName, importName),
),
),
);
const newNamedBindingsText =
this.printer.printNode(ts.EmitHint.Unspecified, newNamedBindings, sourceFile);
const newNamedBindingsText = this.printer.printNode(
ts.EmitHint.Unspecified,
newNamedBindings,
sourceFile,
);
recorder.updateExistingImport(namedBindings, newNamedBindingsText);
});
@ -216,22 +252,32 @@ export class ImportManager {
defaultImports.forEach((identifier, moduleName) => {
const newImport = ts.factory.createImportDeclaration(
undefined, ts.factory.createImportClause(false, identifier, undefined),
ts.factory.createStringLiteral(moduleName, useSingleQuotes));
undefined,
ts.factory.createImportClause(false, identifier, undefined),
ts.factory.createStringLiteral(moduleName, useSingleQuotes),
);
recorder.addNewImport(
importStartIndex, this._getNewImportText(importStartIndex, newImport, sourceFile));
importStartIndex,
this._getNewImportText(importStartIndex, newImport, sourceFile),
);
});
namedImports.forEach((specifiers, moduleName) => {
const newImport = ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createImportClause(
false, undefined, ts.factory.createNamedImports(specifiers)),
ts.factory.createStringLiteral(moduleName, useSingleQuotes));
ts.factory.createNamedImports(specifiers),
),
ts.factory.createStringLiteral(moduleName, useSingleQuotes),
);
recorder.addNewImport(
importStartIndex, this._getNewImportText(importStartIndex, newImport, sourceFile));
importStartIndex,
this._getNewImportText(importStartIndex, newImport, sourceFile),
);
});
});
}
@ -258,8 +304,10 @@ export class ImportManager {
* source file.
*/
private isUniqueIdentifierName(sourceFile: ts.SourceFile, name: string) {
if (this.usedIdentifierNames.has(sourceFile) &&
this.usedIdentifierNames.get(sourceFile)!.indexOf(name) !== -1) {
if (
this.usedIdentifierNames.has(sourceFile) &&
this.usedIdentifierNames.get(sourceFile)!.indexOf(name) !== -1
) {
return false;
}
@ -269,10 +317,13 @@ export class ImportManager {
const nodeQueue: ts.Node[] = [sourceFile];
while (nodeQueue.length) {
const node = nodeQueue.shift()!;
if (ts.isIdentifier(node) && node.text === name &&
// Identifiers that are aliased in an import aren't
// problematic since they're used under a different name.
(!ts.isImportSpecifier(node.parent) || node.parent.propertyName !== node)) {
if (
ts.isIdentifier(node) &&
node.text === name &&
// Identifiers that are aliased in an import aren't
// problematic since they're used under a different name.
(!ts.isImportSpecifier(node.parent) || node.parent.propertyName !== node)
) {
return false;
}
nodeQueue.push(...node.getChildren());
@ -282,7 +333,9 @@ export class ImportManager {
private _recordUsedIdentifier(sourceFile: ts.SourceFile, identifierName: string) {
this.usedIdentifierNames.set(
sourceFile, (this.usedIdentifierNames.get(sourceFile) || []).concat(identifierName));
sourceFile,
(this.usedIdentifierNames.get(sourceFile) || []).concat(identifierName),
);
}
/**
@ -300,8 +353,10 @@ export class ImportManager {
/** Gets the text that should be added to the file for a newly-created import declaration. */
private _getNewImportText(
importStartIndex: number, newImport: ts.ImportDeclaration,
sourceFile: ts.SourceFile): string {
importStartIndex: number,
newImport: ts.ImportDeclaration,
sourceFile: ts.SourceFile,
): string {
const text = this.printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile);
// If the import is generated at the start of the source file, we want to add
@ -321,12 +376,16 @@ export class ImportManager {
* corresponds to `import {name}`.
*/
private _getImportParts(
sourceFile: ts.SourceFile, symbolName: string, alias: string|null, keepSymbolName: boolean) {
sourceFile: ts.SourceFile,
symbolName: string,
alias: string | null,
keepSymbolName: boolean,
) {
const symbolIdentifier = ts.factory.createIdentifier(symbolName);
const aliasIdentifier = alias ? ts.factory.createIdentifier(alias) : null;
const generatedUniqueIdentifier = this._getUniqueIdentifier(sourceFile, alias || symbolName);
const needsGeneratedUniqueName = generatedUniqueIdentifier.text !== (alias || symbolName);
let propertyName: ts.Identifier|undefined;
let propertyName: ts.Identifier | undefined;
let name: ts.Identifier;
if (needsGeneratedUniqueName && !keepSymbolName) {
@ -345,16 +404,18 @@ export class ImportManager {
/** Gets the quote style that is used for a file's imports. */
private _getQuoteStyle(sourceFile: ts.SourceFile): QuoteStyle {
if (!this.quoteStyles.hasOwnProperty(sourceFile.fileName)) {
let quoteStyle: QuoteStyle|undefined;
let quoteStyle: QuoteStyle | undefined;
// Walk through the top-level imports and try to infer the quotes.
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement) &&
ts.isStringLiteralLike(statement.moduleSpecifier)) {
if (
ts.isImportDeclaration(statement) &&
ts.isStringLiteralLike(statement.moduleSpecifier)
) {
// Use `getText` instead of the actual text since it includes the quotes.
quoteStyle = statement.moduleSpecifier.getText().trim().startsWith('"') ?
QuoteStyle.Double :
QuoteStyle.Single;
quoteStyle = statement.moduleSpecifier.getText().trim().startsWith('"')
? QuoteStyle.Double
: QuoteStyle.Single;
break;
}
}

View file

@ -43,7 +43,11 @@ export function computeLineStartsMap(text: string): number[] {
/** Finds the closest line start for the given position. */
function findClosestLineStartPosition<T>(
linesMap: T[], position: T, low = 0, high = linesMap.length - 1) {
linesMap: T[],
position: T,
low = 0,
high = linesMap.length - 1,
) {
while (low <= high) {
const pivotIdx = Math.floor((low + high) / 2);
const pivotEl = linesMap[pivotIdx];

View file

@ -21,9 +21,10 @@ import {URL} from 'url';
* @param modulePath The path of the module to load.
* @returns A Promise that resolves to the dynamically imported module.
*/
export async function loadEsmModule<T>(modulePath: string|URL): Promise<T> {
const namespaceObject =
(await new Function('modulePath', `return import(modulePath);`)(modulePath));
export async function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
const namespaceObject = await new Function('modulePath', `return import(modulePath);`)(
modulePath,
);
// If it is not ESM then the values needed will be stored in the `default` property.
// TODO_ESM: This can be removed once `@angular/*` packages are ESM only.
@ -41,8 +42,9 @@ export async function loadEsmModule<T>(modulePath: string|URL): Promise<T> {
* @returns A Promise that resolves to the dynamically imported compiler-cli private migrations
* entry or an equivalent object if not available.
*/
export async function loadCompilerCliMigrationsModule():
Promise<typeof import('@angular/compiler-cli/private/migrations')> {
export async function loadCompilerCliMigrationsModule(): Promise<
typeof import('@angular/compiler-cli/private/migrations')
> {
try {
return await loadEsmModule('@angular/compiler-cli/private/migrations');
} catch {

View file

@ -31,7 +31,8 @@ export interface ResolvedTemplate {
* character are based on the full source file content.
*/
getCharacterAndLineOfPosition: (pos: number) => {
character: number, line: number
character: number;
line: number;
};
}
@ -42,14 +43,18 @@ export interface ResolvedTemplate {
export class NgComponentTemplateVisitor {
resolvedTemplates: ResolvedTemplate[] = [];
constructor(public typeChecker: ts.TypeChecker, private _basePath: string, private _tree: Tree) {}
constructor(
public typeChecker: ts.TypeChecker,
private _basePath: string,
private _tree: Tree,
) {}
visitNode(node: ts.Node) {
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
this.visitClassDeclaration(node as ts.ClassDeclaration);
}
ts.forEachChild(node, n => this.visitNode(n));
ts.forEachChild(node, (n) => this.visitNode(n));
}
private visitClassDeclaration(node: ts.ClassDeclaration) {
@ -63,7 +68,7 @@ export class NgComponentTemplateVisitor {
// Walk through all component metadata properties and determine the referenced
// HTML templates (either external or inline)
metadata.node.properties.forEach(property => {
metadata.node.properties.forEach((property) => {
if (!ts.isPropertyAssignment(property)) {
return;
}
@ -90,8 +95,8 @@ export class NgComponentTemplateVisitor {
content,
inline: true,
start: start,
getCharacterAndLineOfPosition: pos =>
ts.getLineAndCharacterOfPosition(sourceFile, pos + start)
getCharacterAndLineOfPosition: (pos) =>
ts.getLineAndCharacterOfPosition(sourceFile, pos + start),
});
}
if (propertyName === 'templateUrl' && ts.isStringLiteralLike(property.initializer)) {
@ -118,7 +123,8 @@ export class NgComponentTemplateVisitor {
content: fileContent,
inline: false,
start: 0,
getCharacterAndLineOfPosition: pos => getLineAndCharacterFromPosition(lineStartsMap, pos),
getCharacterAndLineOfPosition: (pos) =>
getLineAndCharacterFromPosition(lineStartsMap, pos),
});
}
});

View file

@ -9,7 +9,7 @@
import ts from 'typescript';
import {getCallDecoratorImport} from './typescript/decorators';
export type CallExpressionDecorator = ts.Decorator&{
export type CallExpressionDecorator = ts.Decorator & {
expression: ts.CallExpression;
};
@ -25,13 +25,16 @@ export interface NgDecorator {
* from a list of decorators.
*/
export function getAngularDecorators(
typeChecker: ts.TypeChecker, decorators: ReadonlyArray<ts.Decorator>): NgDecorator[] {
return decorators.map(node => ({node, importData: getCallDecoratorImport(typeChecker, node)}))
.filter(({importData}) => importData && importData.importModule.startsWith('@angular/'))
.map(({node, importData}) => ({
node: node as CallExpressionDecorator,
name: importData!.name,
moduleName: importData!.importModule,
importNode: importData!.node
}));
typeChecker: ts.TypeChecker,
decorators: ReadonlyArray<ts.Decorator>,
): NgDecorator[] {
return decorators
.map((node) => ({node, importData: getCallDecoratorImport(typeChecker, node)}))
.filter(({importData}) => importData && importData.importModule.startsWith('@angular/'))
.map(({node, importData}) => ({
node: node as CallExpressionDecorator,
name: importData!.name,
moduleName: importData!.importModule,
importNode: importData!.node,
}));
}

View file

@ -13,8 +13,10 @@ import type {TmplAstNode} from '@angular/compiler';
* fails, null is being returned.
*/
export function parseHtmlGracefully(
htmlContent: string, filePath: string,
compilerModule: typeof import('@angular/compiler')): TmplAstNode[]|null {
htmlContent: string,
filePath: string,
compilerModule: typeof import('@angular/compiler'),
): TmplAstNode[] | null {
try {
return compilerModule.parseTemplate(htmlContent, filePath).nodes;
} catch {

View file

@ -13,8 +13,9 @@ import {Tree} from '@angular-devkit/schematics';
* Gets all tsconfig paths from a CLI project by reading the workspace configuration
* and looking for common tsconfig locations.
*/
export async function getProjectTsConfigPaths(tree: Tree):
Promise<{buildPaths: string[]; testPaths: string[];}> {
export async function getProjectTsConfigPaths(
tree: Tree,
): Promise<{buildPaths: string[]; testPaths: string[]}> {
// Start with some tsconfig paths that are generally used within CLI projects. Note
// that we are not interested in IDE-specific tsconfig files (e.g. /tsconfig.json)
const buildPaths = new Set<string>();
@ -50,9 +51,9 @@ export async function getProjectTsConfigPaths(tree: Tree):
}
/** Get options for all configurations for the passed builder target. */
function*
allTargetOptions(target: workspaces.TargetDefinition):
Iterable<[string | undefined, Record<string, json.JsonValue|undefined>]> {
function* allTargetOptions(
target: workspaces.TargetDefinition,
): Iterable<[string | undefined, Record<string, json.JsonValue | undefined>]> {
if (target.options) {
yield [undefined, target.options];
}

View file

@ -6,7 +6,33 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstContent, TmplAstDeferredBlock, TmplAstDeferredBlockError, TmplAstDeferredBlockLoading, TmplAstDeferredBlockPlaceholder, TmplAstDeferredTrigger, TmplAstElement, TmplAstIfBlockBranch, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstIcu, TmplAstIfBlock, TmplAstNode, TmplAstRecursiveVisitor, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstText, TmplAstTextAttribute, TmplAstVariable, TmplAstUnknownBlock} from '@angular/compiler';
import type {
TmplAstBoundAttribute,
TmplAstBoundEvent,
TmplAstBoundText,
TmplAstContent,
TmplAstDeferredBlock,
TmplAstDeferredBlockError,
TmplAstDeferredBlockLoading,
TmplAstDeferredBlockPlaceholder,
TmplAstDeferredTrigger,
TmplAstElement,
TmplAstIfBlockBranch,
TmplAstForLoopBlock,
TmplAstForLoopBlockEmpty,
TmplAstIcu,
TmplAstIfBlock,
TmplAstNode,
TmplAstRecursiveVisitor,
TmplAstReference,
TmplAstSwitchBlock,
TmplAstSwitchBlockCase,
TmplAstTemplate,
TmplAstText,
TmplAstTextAttribute,
TmplAstVariable,
TmplAstUnknownBlock,
} from '@angular/compiler';
/**
* A base class that can be used to implement a Render3 Template AST visitor.

View file

@ -23,7 +23,7 @@ export function createHtmlSourceFile(filePath: string, content: string): ts.Sour
// At the time of writing, TSLint loads files manually if the actual rule source file is not
// equal to the source file of the replacement. This means that the replacements need proper
// offsets without the string literal quote symbols.
sourceFile.getFullText = function() {
sourceFile.getFullText = function () {
return sourceFile.text.substring(1, sourceFile.text.length - 1);
};

View file

@ -9,19 +9,20 @@
import ts from 'typescript';
/** Determines the base type identifiers of a specified class declaration. */
export function getBaseTypeIdentifiers(node: ts.ClassDeclaration): ts.Identifier[]|null {
export function getBaseTypeIdentifiers(node: ts.ClassDeclaration): ts.Identifier[] | null {
if (!node.heritageClauses) {
return null;
}
return node.heritageClauses.filter(clause => clause.token === ts.SyntaxKind.ExtendsKeyword)
.reduce((types, clause) => types.concat(clause.types), [] as ts.ExpressionWithTypeArguments[])
.map(typeExpression => typeExpression.expression)
.filter(ts.isIdentifier);
return node.heritageClauses
.filter((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)
.reduce((types, clause) => types.concat(clause.types), [] as ts.ExpressionWithTypeArguments[])
.map((typeExpression) => typeExpression.expression)
.filter(ts.isIdentifier);
}
/** Gets the first found parent class declaration of a given node. */
export function findParentClassDeclaration(node: ts.Node): ts.ClassDeclaration|null {
export function findParentClassDeclaration(node: ts.Node): ts.ClassDeclaration | null {
while (!ts.isClassDeclaration(node)) {
if (ts.isSourceFile(node)) {
return null;

View file

@ -11,7 +11,7 @@ import ts from 'typescript';
import {parseTsconfigFile} from './parse_tsconfig';
type FakeReadFileFn = (fileName: string) => string|undefined;
type FakeReadFileFn = (fileName: string) => string | undefined;
/**
* Creates a TypeScript program instance for a TypeScript project within
@ -24,10 +24,19 @@ type FakeReadFileFn = (fileName: string) => string|undefined;
* @param additionalFiles Additional file paths that should be added to the program.
*/
export function createMigrationProgram(
tree: Tree, tsconfigPath: string, basePath: string, fakeFileRead?: FakeReadFileFn,
additionalFiles?: string[]) {
const {rootNames, options, host} =
createProgramOptions(tree, tsconfigPath, basePath, fakeFileRead, additionalFiles);
tree: Tree,
tsconfigPath: string,
basePath: string,
fakeFileRead?: FakeReadFileFn,
additionalFiles?: string[],
) {
const {rootNames, options, host} = createProgramOptions(
tree,
tsconfigPath,
basePath,
fakeFileRead,
additionalFiles,
);
return ts.createProgram(rootNames, options, host);
}
@ -42,8 +51,13 @@ export function createMigrationProgram(
* @param optionOverrides Overrides of the parsed compiler options.
*/
export function createProgramOptions(
tree: Tree, tsconfigPath: string, basePath: string, fakeFileRead?: FakeReadFileFn,
additionalFiles?: string[], optionOverrides?: ts.CompilerOptions) {
tree: Tree,
tsconfigPath: string,
basePath: string,
fakeFileRead?: FakeReadFileFn,
additionalFiles?: string[],
optionOverrides?: ts.CompilerOptions,
) {
// Resolve the tsconfig path to an absolute path. This is needed as TypeScript otherwise
// is not able to resolve root directories in the given tsconfig. More details can be found
// in the following issue: https://github.com/microsoft/TypeScript/issues/37731.
@ -55,8 +69,11 @@ export function createProgramOptions(
}
function createMigrationCompilerHost(
tree: Tree, options: ts.CompilerOptions, basePath: string,
fakeRead?: FakeReadFileFn): ts.CompilerHost {
tree: Tree,
options: ts.CompilerOptions,
basePath: string,
fakeRead?: FakeReadFileFn,
): ts.CompilerHost {
const host = ts.createCompilerHost(options, true);
const defaultReadFile = host.readFile;
@ -64,15 +81,16 @@ function createMigrationCompilerHost(
// program to be based on the file contents in the virtual file tree. Otherwise
// if we run multiple migrations we might have intersecting changes and
// source files.
host.readFile = fileName => {
host.readFile = (fileName) => {
const treeRelativePath = relative(basePath, fileName);
let result: string|undefined = fakeRead?.(treeRelativePath);
let result: string | undefined = fakeRead?.(treeRelativePath);
if (typeof result !== 'string') {
// If the relative path resolved to somewhere outside of the tree, fall back to
// TypeScript's default file reading function since the `tree` will throw an error.
result = treeRelativePath.startsWith('..') ? defaultReadFile.call(host, fileName) :
tree.read(treeRelativePath)?.toString();
result = treeRelativePath.startsWith('..')
? defaultReadFile.call(host, fileName)
: tree.read(treeRelativePath)?.toString();
}
// Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset,
@ -91,10 +109,16 @@ function createMigrationCompilerHost(
* @param program Program that includes the source file.
*/
export function canMigrateFile(
basePath: string, sourceFile: ts.SourceFile, program: ts.Program): boolean {
basePath: string,
sourceFile: ts.SourceFile,
program: ts.Program,
): boolean {
// We shouldn't migrate .d.ts files, files from an external library or type checking files.
if (sourceFile.fileName.endsWith('.ngtypecheck.ts') || sourceFile.isDeclarationFile ||
program.isSourceFileFromExternalLibrary(sourceFile)) {
if (
sourceFile.fileName.endsWith('.ngtypecheck.ts') ||
sourceFile.isDeclarationFile ||
program.isSourceFileFromExternalLibrary(sourceFile)
) {
return false;
}

View file

@ -11,11 +11,15 @@ import ts from 'typescript';
import {getImportOfIdentifier, Import} from './imports';
export function getCallDecoratorImport(
typeChecker: ts.TypeChecker, decorator: ts.Decorator): Import|null {
typeChecker: ts.TypeChecker,
decorator: ts.Decorator,
): Import | null {
// Note that this does not cover the edge case where decorators are called from
// a namespace import: e.g. "@core.Component()". This is not handled by Ngtsc either.
if (!ts.isCallExpression(decorator.expression) ||
!ts.isIdentifier(decorator.expression.expression)) {
if (
!ts.isCallExpression(decorator.expression) ||
!ts.isIdentifier(decorator.expression.expression)
) {
return null;
}

View file

@ -11,7 +11,7 @@ import {getBaseTypeIdentifiers} from './class_declaration';
/** Gets all base class declarations of the specified class declaration. */
export function findBaseClassDeclarations(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker) {
const result: {identifier: ts.Identifier, node: ts.ClassDeclaration}[] = [];
const result: {identifier: ts.Identifier; node: ts.ClassDeclaration}[] = [];
let currentClass = node;
while (currentClass) {

View file

@ -10,9 +10,14 @@ import ts from 'typescript';
/** Checks whether a given node is a function like declaration. */
export function isFunctionLikeDeclaration(node: ts.Node): node is ts.FunctionLikeDeclaration {
return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) ||
ts.isArrowFunction(node) || ts.isFunctionExpression(node) ||
ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
return (
ts.isFunctionDeclaration(node) ||
ts.isMethodDeclaration(node) ||
ts.isArrowFunction(node) ||
ts.isFunctionExpression(node) ||
ts.isGetAccessorDeclaration(node) ||
ts.isSetAccessorDeclaration(node)
);
}
/**
@ -20,7 +25,7 @@ export function isFunctionLikeDeclaration(node: ts.Node): node is ts.FunctionLik
* parentheses or as expression. e.g. "(((({exp}))))()". The function should return the
* TypeScript node referring to the inner expression. e.g "exp".
*/
export function unwrapExpression(node: ts.Expression|ts.ParenthesizedExpression): ts.Expression {
export function unwrapExpression(node: ts.Expression | ts.ParenthesizedExpression): ts.Expression {
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node)) {
return unwrapExpression(node.expression);
} else {

View file

@ -9,14 +9,16 @@
import ts from 'typescript';
export type Import = {
name: string,
importModule: string,
node: ts.ImportDeclaration
name: string;
importModule: string;
node: ts.ImportDeclaration;
};
/** Gets import information about the specified identifier by using the Type checker. */
export function getImportOfIdentifier(typeChecker: ts.TypeChecker, node: ts.Identifier): Import|
null {
export function getImportOfIdentifier(
typeChecker: ts.TypeChecker,
node: ts.Identifier,
): Import | null {
const symbol = typeChecker.getSymbolAtLocation(node);
if (!symbol || symbol.declarations === undefined || !symbol.declarations.length) {
@ -39,11 +41,10 @@ export function getImportOfIdentifier(typeChecker: ts.TypeChecker, node: ts.Iden
// Handles aliased imports: e.g. "import {Component as myComp} from ...";
name: decl.propertyName ? decl.propertyName.text : decl.name.text,
importModule: importDecl.moduleSpecifier.text,
node: importDecl
node: importDecl,
};
}
/**
* Gets a top-level import specifier with a specific name that is imported from a particular module.
* E.g. given a file that looks like:
@ -62,19 +63,25 @@ export function getImportOfIdentifier(typeChecker: ts.TypeChecker, node: ts.Iden
* their original name.
*/
export function getImportSpecifier(
sourceFile: ts.SourceFile, moduleName: string|RegExp,
specifierName: string): ts.ImportSpecifier|null {
sourceFile: ts.SourceFile,
moduleName: string | RegExp,
specifierName: string,
): ts.ImportSpecifier | null {
return getImportSpecifiers(sourceFile, moduleName, [specifierName])[0] ?? null;
}
export function getImportSpecifiers(
sourceFile: ts.SourceFile, moduleName: string|RegExp,
specifierNames: string[]): ts.ImportSpecifier[] {
sourceFile: ts.SourceFile,
moduleName: string | RegExp,
specifierNames: string[],
): ts.ImportSpecifier[] {
const matches: ts.ImportSpecifier[] = [];
for (const node of sourceFile.statements) {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const isMatch = typeof moduleName === 'string' ? node.moduleSpecifier.text === moduleName :
moduleName.test(node.moduleSpecifier.text);
const isMatch =
typeof moduleName === 'string'
? node.moduleSpecifier.text === moduleName
: moduleName.test(node.moduleSpecifier.text);
const namedBindings = node.importClause?.namedBindings;
if (isMatch && namedBindings && ts.isNamedImports(namedBindings)) {
for (const specifierName of specifierNames) {
@ -90,11 +97,15 @@ export function getImportSpecifiers(
}
export function getNamedImports(
sourceFile: ts.SourceFile, moduleName: string|RegExp): ts.NamedImports|null {
sourceFile: ts.SourceFile,
moduleName: string | RegExp,
): ts.NamedImports | null {
for (const node of sourceFile.statements) {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const isMatch = typeof moduleName === 'string' ? node.moduleSpecifier.text === moduleName :
moduleName.test(node.moduleSpecifier.text);
const isMatch =
typeof moduleName === 'string'
? node.moduleSpecifier.text === moduleName
: moduleName.test(node.moduleSpecifier.text);
const namedBindings = node.importClause?.namedBindings;
if (isMatch && namedBindings && ts.isNamedImports(namedBindings)) {
return namedBindings;
@ -104,7 +115,6 @@ export function getNamedImports(
return null;
}
/**
* Replaces an import inside a named imports node with a different one.
*
@ -113,7 +123,10 @@ export function getNamedImports(
* @param newImportName Import that should be inserted.
*/
export function replaceImport(
node: ts.NamedImports, existingImport: string, newImportName: string) {
node: ts.NamedImports,
existingImport: string,
newImportName: string,
) {
const isAlreadyImported = findImportSpecifier(node.elements, newImportName);
if (isAlreadyImported) {
return node;
@ -124,15 +137,17 @@ export function replaceImport(
return node;
}
const importPropertyName =
existingImportNode.propertyName ? ts.factory.createIdentifier(newImportName) : undefined;
const importName = existingImportNode.propertyName ? existingImportNode.name :
ts.factory.createIdentifier(newImportName);
const importPropertyName = existingImportNode.propertyName
? ts.factory.createIdentifier(newImportName)
: undefined;
const importName = existingImportNode.propertyName
? existingImportNode.name
: ts.factory.createIdentifier(newImportName);
return ts.factory.updateNamedImports(node, [
...node.elements.filter(current => current !== existingImportNode),
...node.elements.filter((current) => current !== existingImportNode),
// Create a new import while trying to preserve the alias of the old one.
ts.factory.createImportSpecifier(false, importPropertyName, importName)
ts.factory.createImportSpecifier(false, importPropertyName, importName),
]);
}
@ -146,14 +161,16 @@ export function replaceImport(
*/
export function removeSymbolFromNamedImports(node: ts.NamedImports, symbol: ts.ImportSpecifier) {
return ts.factory.updateNamedImports(node, [
...node.elements.filter(current => current !== symbol),
...node.elements.filter((current) => current !== symbol),
]);
}
/** Finds an import specifier with a particular name. */
export function findImportSpecifier(
nodes: ts.NodeArray<ts.ImportSpecifier>, specifierName: string): ts.ImportSpecifier|undefined {
return nodes.find(element => {
nodes: ts.NodeArray<ts.ImportSpecifier>,
specifierName: string,
): ts.ImportSpecifier | undefined {
return nodes.find((element) => {
const {name, propertyName} = element;
return propertyName ? propertyName.text === specifierName : name.text === specifierName;
});

View file

@ -10,13 +10,18 @@ import ts from 'typescript';
/** Checks whether the given TypeScript node has the specified modifier set. */
export function hasModifier(node: ts.Node, modifierKind: ts.SyntaxKind) {
return ts.canHaveModifiers(node) && !!node.modifiers &&
node.modifiers.some(m => m.kind === modifierKind);
return (
ts.canHaveModifiers(node) &&
!!node.modifiers &&
node.modifiers.some((m) => m.kind === modifierKind)
);
}
/** Find the closest parent node of a particular kind. */
export function closestNode<T extends ts.Node>(node: ts.Node, predicate: (n: ts.Node) => n is T): T|
null {
export function closestNode<T extends ts.Node>(
node: ts.Node,
predicate: (n: ts.Node) => n is T,
): T | null {
let current = node.parent;
while (current && !ts.isSourceFile(current)) {
@ -45,8 +50,11 @@ export function isNullCheck(node: ts.Node): boolean {
// `foo.bar && foo.bar.parent && foo.bar.parent.value`
// where `node` is `foo.bar`.
if (node.parent.parent && ts.isBinaryExpression(node.parent.parent) &&
node.parent.parent.left === node.parent) {
if (
node.parent.parent &&
ts.isBinaryExpression(node.parent.parent) &&
node.parent.parent.left === node.parent
) {
return true;
}
@ -65,6 +73,10 @@ export function isNullCheck(node: ts.Node): boolean {
/** Checks whether a property access is safe (e.g. `foo.parent?.value`). */
export function isSafeAccess(node: ts.Node): boolean {
return node.parent != null && ts.isPropertyAccessExpression(node.parent) &&
node.parent.expression === node && node.parent.questionDotToken != null;
return (
node.parent != null &&
ts.isPropertyAccessExpression(node.parent) &&
node.parent.expression === node &&
node.parent.questionDotToken != null
);
}

View file

@ -15,7 +15,7 @@ type PropertyNameWithText = Exclude<ts.PropertyName, ts.ComputedPropertyName>;
* Gets the text of the given property name. Returns null if the property
* name couldn't be determined statically.
*/
export function getPropertyNameText(node: ts.PropertyName): string|null {
export function getPropertyNameText(node: ts.PropertyName): string | null {
if (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) {
return node.text;
}

View file

@ -8,8 +8,10 @@
import ts from 'typescript';
export function getValueSymbolOfDeclaration(node: ts.Node, typeChecker: ts.TypeChecker): ts.Symbol|
undefined {
export function getValueSymbolOfDeclaration(
node: ts.Node,
typeChecker: ts.TypeChecker,
): ts.Symbol | undefined {
let symbol = typeChecker.getSymbolAtLocation(node);
while (symbol && symbol.flags & ts.SymbolFlags.Alias) {
@ -21,11 +23,16 @@ export function getValueSymbolOfDeclaration(node: ts.Node, typeChecker: ts.TypeC
/** Checks whether a node is referring to a specific import specifier. */
export function isReferenceToImport(
typeChecker: ts.TypeChecker, node: ts.Node, importSpecifier: ts.ImportSpecifier): boolean {
typeChecker: ts.TypeChecker,
node: ts.Node,
importSpecifier: ts.ImportSpecifier,
): boolean {
const nodeSymbol = typeChecker.getTypeAtLocation(node).getSymbol();
const importSymbol = typeChecker.getTypeAtLocation(importSpecifier).getSymbol();
return !!(nodeSymbol?.declarations?.[0] && importSymbol?.declarations?.[0]) &&
nodeSymbol.declarations[0] === importSymbol.declarations[0];
return (
!!(nodeSymbol?.declarations?.[0] && importSymbol?.declarations?.[0]) &&
nodeSymbol.declarations[0] === importSymbol.declarations[0]
);
}
/** Checks whether a node's type is nullable (`null`, `undefined` or `void`). */
@ -44,9 +51,11 @@ export function isNullableType(typeChecker: ts.TypeChecker, node: ts.Node) {
// through all of its sub-nodes and look for nullable types.
if (typeNode) {
(function walk(current: ts.Node) {
if (current.kind === ts.SyntaxKind.NullKeyword ||
current.kind === ts.SyntaxKind.UndefinedKeyword ||
current.kind === ts.SyntaxKind.VoidKeyword) {
if (
current.kind === ts.SyntaxKind.NullKeyword ||
current.kind === ts.SyntaxKind.UndefinedKeyword ||
current.kind === ts.SyntaxKind.VoidKeyword
) {
hasSeenNullableType = true;
// Note that we don't descend into type literals, because it may cause
// us to mis-identify the root type as nullable, because it has a nullable
@ -65,10 +74,14 @@ export function isNullableType(typeChecker: ts.TypeChecker, node: ts.Node) {
* type that has the same name as one of the passed-in ones.
*/
export function hasOneOfTypes(
typeChecker: ts.TypeChecker, node: ts.Node, types: string[]): boolean {
typeChecker: ts.TypeChecker,
node: ts.Node,
types: string[],
): boolean {
const type = typeChecker.getTypeAtLocation(node);
const typeNode =
type ? typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.None) : undefined;
const typeNode = type
? typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.None)
: undefined;
let hasMatch = false;
if (typeNode) {
(function walk(current: ts.Node) {

View file

@ -1,4 +1,3 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
@ -18,7 +17,7 @@ export interface ApplicationConfig {
/**
* List of providers that should be available to the root component and all its children.
*/
providers: Array<Provider|EnvironmentProviders>;
providers: Array<Provider | EnvironmentProviders>;
}
/**
@ -30,7 +29,10 @@ export interface ApplicationConfig {
* @publicApi
*/
export function mergeApplicationConfig(...configs: ApplicationConfig[]): ApplicationConfig {
return configs.reduce((prev, curr) => {
return Object.assign(prev, curr, {providers: [...prev.providers, ...curr.providers]});
}, {providers: []});
return configs.reduce(
(prev, curr) => {
return Object.assign(prev, curr, {providers: [...prev.providers, ...curr.providers]});
},
{providers: []},
);
}

View file

@ -131,9 +131,9 @@ import {isPromise, isSubscribable} from '../util/lang';
*
* @publicApi
*/
export const APP_INITIALIZER =
new InjectionToken<ReadonlyArray<() => Observable<unknown>| Promise<unknown>| void>>(
ngDevMode ? 'Application Initializer' : '');
export const APP_INITIALIZER = new InjectionToken<
ReadonlyArray<() => Observable<unknown> | Promise<unknown> | void>
>(ngDevMode ? 'Application Initializer' : '');
/**
* A class that reflects the state of running {@link APP_INITIALIZER} functions.
@ -159,11 +159,12 @@ export class ApplicationInitStatus {
constructor() {
if ((typeof ngDevMode === 'undefined' || ngDevMode) && !Array.isArray(this.appInits)) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_MULTI_PROVIDER,
'Unexpected type of the `APP_INITIALIZER` token value ' +
`(expected an array, but got ${typeof this.appInits}). ` +
'Please check that the `APP_INITIALIZER` token is configured as a ' +
'`multi: true` provider.');
RuntimeErrorCode.INVALID_MULTI_PROVIDER,
'Unexpected type of the `APP_INITIALIZER` token value ' +
`(expected an array, but got ${typeof this.appInits}). ` +
'Please check that the `APP_INITIALIZER` token is configured as a ' +
'`multi: true` provider.',
);
}
}
@ -193,12 +194,12 @@ export class ApplicationInitStatus {
};
Promise.all(asyncInitPromises)
.then(() => {
complete();
})
.catch(e => {
this.reject(e);
});
.then(() => {
complete();
})
.catch((e) => {
this.reject(e);
});
if (asyncInitPromises.length === 0) {
complete();

View file

@ -11,14 +11,19 @@ import {Injector} from '../di/injector';
import {Type} from '../interface/type';
import {COMPILER_OPTIONS, CompilerOptions} from '../linker/compiler';
import {NgModuleFactory} from '../linker/ng_module_factory';
import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from '../metadata/resource_loading';
import {
isComponentResourceResolutionQueueEmpty,
resolveComponentResources,
} from '../metadata/resource_loading';
import {assertNgModuleType} from '../render3/assert';
import {setJitOptions} from '../render3/jit/jit_options';
import {NgModuleFactory as R3NgModuleFactory} from '../render3/ng_module_ref';
export function compileNgModuleFactory<M>(
injector: Injector, options: CompilerOptions,
moduleType: Type<M>): Promise<NgModuleFactory<M>> {
injector: Injector,
options: CompilerOptions,
moduleType: Type<M>,
): Promise<NgModuleFactory<M>> {
ngDevMode && assertNgModuleType(moduleType);
const moduleFactory = new R3NgModuleFactory(moduleType);
@ -34,8 +39,8 @@ export function compileNgModuleFactory<M>(
// are bootstrapped with incompatible options, as a component can only be compiled according to
// a single set of options.
setJitOptions({
defaultEncapsulation: _lastDefined(compilerOptions.map(opts => opts.defaultEncapsulation)),
preserveWhitespaces: _lastDefined(compilerOptions.map(opts => opts.preserveWhitespaces)),
defaultEncapsulation: _lastDefined(compilerOptions.map((opts) => opts.defaultEncapsulation)),
preserveWhitespaces: _lastDefined(compilerOptions.map((opts) => opts.preserveWhitespaces)),
});
if (isComponentResourceResolutionQueueEmpty()) {
@ -61,11 +66,12 @@ export function compileNgModuleFactory<M>(
const resourceLoader = compilerInjector.get(compiler.ResourceLoader);
// The resource loader can also return a string while the "resolveComponentResources"
// always expects a promise. Therefore we need to wrap the returned value in a promise.
return resolveComponentResources(url => Promise.resolve(resourceLoader.get(url)))
.then(() => moduleFactory);
return resolveComponentResources((url) => Promise.resolve(resourceLoader.get(url))).then(
() => moduleFactory,
);
}
function _lastDefined<T>(args: T[]): T|undefined {
function _lastDefined<T>(args: T[]): T | undefined {
for (let i = args.length - 1; i >= 0; i--) {
if (args[i] !== undefined) {
return args[i];

View file

@ -8,7 +8,10 @@
import '../util/ng_jit_mode';
import {setActiveConsumer, setThrowInvalidWriteToSignalError} from '@angular/core/primitives/signals';
import {
setActiveConsumer,
setThrowInvalidWriteToSignalError,
} from '@angular/core/primitives/signals';
import {Observable, Subject} from 'rxjs';
import {first, map} from 'rxjs/operators';
@ -51,9 +54,9 @@ import {ApplicationInitStatus} from './application_init';
*
* @publicApi
*/
export const APP_BOOTSTRAP_LISTENER =
new InjectionToken<ReadonlyArray<(compRef: ComponentRef<any>) => void>>(
ngDevMode ? 'appBootstrapListener' : '');
export const APP_BOOTSTRAP_LISTENER = new InjectionToken<
ReadonlyArray<(compRef: ComponentRef<any>) => void>
>(ngDevMode ? 'appBootstrapListener' : '');
export function publishDefaultGlobalUtils() {
ngDevMode && _publishDefaultGlobalUtils();
@ -65,10 +68,11 @@ export function publishDefaultGlobalUtils() {
export function publishSignalConfiguration(): void {
setThrowInvalidWriteToSignalError(() => {
throw new RuntimeError(
RuntimeErrorCode.SIGNAL_WRITE_FROM_ILLEGAL_CONTEXT,
ngDevMode &&
'Writing to signals is not allowed in a `computed` or an `effect` by default. ' +
'Use `allowSignalWrites` in the `CreateEffectOptions` to enable this inside effects.');
RuntimeErrorCode.SIGNAL_WRITE_FROM_ILLEGAL_CONTEXT,
ngDevMode &&
'Writing to signals is not allowed in a `computed` or an `effect` by default. ' +
'Use `allowSignalWrites` in the `CreateEffectOptions` to enable this inside effects.',
);
});
}
@ -83,7 +87,10 @@ export function isBoundToModule<C>(cf: ComponentFactory<C>): boolean {
* @publicApi
*/
export class NgProbeToken {
constructor(public name: string, public token: any) {}
constructor(
public name: string,
public token: any,
) {}
}
/**
@ -99,7 +106,7 @@ export interface BootstrapOptions {
* - `zone.js` - Use default `NgZone` which requires `Zone.js`.
* - `noop` - Use `NoopNgZone` which does nothing.
*/
ngZone?: NgZone|'zone.js'|'noop';
ngZone?: NgZone | 'zone.js' | 'noop';
/**
* Optionally specify coalescing event change detections or not.
@ -163,7 +170,10 @@ export interface BootstrapOptions {
const MAXIMUM_REFRESH_RERUNS = 10;
export function _callAndReportToErrorHandler(
errorHandler: ErrorHandler, ngZone: NgZone, callback: () => any): any {
errorHandler: ErrorHandler,
ngZone: NgZone,
callback: () => any,
): any {
try {
const result = callback();
if (isPromise(result)) {
@ -182,7 +192,7 @@ export function _callAndReportToErrorHandler(
}
}
export function optionsReducer<T extends Object>(dst: T, objs: T|T[]): T {
export function optionsReducer<T extends Object>(dst: T, objs: T | T[]): T {
if (Array.isArray(objs)) {
return objs.reduce(optionsReducer, dst);
}
@ -323,8 +333,9 @@ export class ApplicationRef {
/**
* Returns an Observable that indicates when the application is stable or unstable.
*/
public readonly isStable: Observable<boolean> =
inject(PendingTasks).hasPendingTasks.pipe(map(pending => !pending));
public readonly isStable: Observable<boolean> = inject(PendingTasks).hasPendingTasks.pipe(
map((pending) => !pending),
);
private readonly _injector = inject(EnvironmentInjector);
/**
@ -371,7 +382,7 @@ export class ApplicationRef {
*
* {@example core/ts/platform/platform.ts region='domNode'}
*/
bootstrap<C>(component: Type<C>, rootSelectorOrNode?: string|any): ComponentRef<C>;
bootstrap<C>(component: Type<C>, rootSelectorOrNode?: string | any): ComponentRef<C>;
/**
* Bootstrap a component onto the element identified by its selector or, optionally, to a
@ -413,8 +424,10 @@ export class ApplicationRef {
* @deprecated Passing Component factories as the `Application.bootstrap` function argument is
* deprecated. Pass Component Types instead.
*/
bootstrap<C>(componentFactory: ComponentFactory<C>, rootSelectorOrNode?: string|any):
ComponentRef<C>;
bootstrap<C>(
componentFactory: ComponentFactory<C>,
rootSelectorOrNode?: string | any,
): ComponentRef<C>;
/**
* Bootstrap a component onto the element identified by its selector or, optionally, to a
@ -453,19 +466,22 @@ export class ApplicationRef {
*
* {@example core/ts/platform/platform.ts region='domNode'}
*/
bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>, rootSelectorOrNode?: string|any):
ComponentRef<C> {
bootstrap<C>(
componentOrFactory: ComponentFactory<C> | Type<C>,
rootSelectorOrNode?: string | any,
): ComponentRef<C> {
(typeof ngDevMode === 'undefined' || ngDevMode) && this.warnIfDestroyed();
const isComponentFactory = componentOrFactory instanceof ComponentFactory;
const initStatus = this._injector.get(ApplicationInitStatus);
if (!initStatus.done) {
const standalone = !isComponentFactory && isStandalone(componentOrFactory);
const errorMessage = (typeof ngDevMode === 'undefined' || ngDevMode) &&
'Cannot bootstrap as there are still asynchronous initializers running.' +
(standalone ?
'' :
' Bootstrap components in the `ngDoBootstrap` method of the root module.');
const errorMessage =
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'Cannot bootstrap as there are still asynchronous initializers running.' +
(standalone
? ''
: ' Bootstrap components in the `ngDoBootstrap` method of the root module.');
throw new RuntimeError(RuntimeErrorCode.ASYNC_INITIALIZERS_STILL_RUNNING, errorMessage);
}
@ -479,8 +495,9 @@ export class ApplicationRef {
this.componentTypes.push(componentFactory.componentType);
// Create a factory associated with the current module if it's not bound to some other
const ngModule =
isBoundToModule(componentFactory) ? undefined : this._injector.get(NgModuleRef);
const ngModule = isBoundToModule(componentFactory)
? undefined
: this._injector.get(NgModuleRef);
const selectorOrNode = rootSelectorOrNode || componentFactory.selector;
const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);
const nativeElement = compRef.location.nativeElement;
@ -520,8 +537,9 @@ export class ApplicationRef {
(typeof ngDevMode === 'undefined' || ngDevMode) && this.warnIfDestroyed();
if (this._runningTick) {
throw new RuntimeError(
RuntimeErrorCode.RECURSIVE_APPLICATION_REF_TICK,
ngDevMode && 'ApplicationRef.tick is called recursively');
RuntimeErrorCode.RECURSIVE_APPLICATION_REF_TICK,
ngDevMode && 'ApplicationRef.tick is called recursively',
);
}
const prevConsumer = setActiveConsumer(null);
@ -530,7 +548,7 @@ export class ApplicationRef {
this.detectChangesInAttachedViews(refreshViews);
if ((typeof ngDevMode === 'undefined' || ngDevMode)) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
for (let view of this._views) {
view.checkNoChanges();
}
@ -554,7 +572,11 @@ export class ApplicationRef {
this.beforeRender.next(isFirstPass);
for (let {_lView, notifyErrorHandler} of this._views) {
detectChangesInViewIfRequired(
_lView, notifyErrorHandler, isFirstPass, this.zonelessEnabled);
_lView,
notifyErrorHandler,
isFirstPass,
this.zonelessEnabled,
);
}
}
runs++;
@ -562,27 +584,33 @@ export class ApplicationRef {
afterRenderEffectManager.executeInternalCallbacks();
// If we have a newly dirty view after running internal callbacks, recheck the views again
// before running user-provided callbacks
if ([...this.externalTestViews.keys(), ...this._views].some(
({_lView}) => requiresRefreshOrTraversal(_lView))) {
if (
[...this.externalTestViews.keys(), ...this._views].some(({_lView}) =>
requiresRefreshOrTraversal(_lView),
)
) {
continue;
}
afterRenderEffectManager.execute();
// If after running all afterRender callbacks we have no more views that need to be refreshed,
// we can break out of the loop
if (![...this.externalTestViews.keys(), ...this._views].some(
({_lView}) => requiresRefreshOrTraversal(_lView))) {
if (
![...this.externalTestViews.keys(), ...this._views].some(({_lView}) =>
requiresRefreshOrTraversal(_lView),
)
) {
break;
}
}
if ((typeof ngDevMode === 'undefined' || ngDevMode) && runs >= MAXIMUM_REFRESH_RERUNS) {
throw new RuntimeError(
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
ngDevMode &&
'Infinite change detection while refreshing application views. ' +
'Ensure views are not calling `markForCheck` on every template execution or ' +
'that afterRender hooks always mark views for check.',
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
ngDevMode &&
'Infinite change detection while refreshing application views. ' +
'Ensure views are not calling `markForCheck` on every template execution or ' +
'that afterRender hooks always mark views for check.',
);
}
}
@ -594,7 +622,7 @@ export class ApplicationRef {
*/
attachView(viewRef: ViewRef): void {
(typeof ngDevMode === 'undefined' || ngDevMode) && this.warnIfDestroyed();
const view = (viewRef as InternalViewRef<unknown>);
const view = viewRef as InternalViewRef<unknown>;
this._views.push(view);
view.attachToAppRef(this);
}
@ -604,7 +632,7 @@ export class ApplicationRef {
*/
detachView(viewRef: ViewRef): void {
(typeof ngDevMode === 'undefined' || ngDevMode) && this.warnIfDestroyed();
const view = (viewRef as InternalViewRef<unknown>);
const view = viewRef as InternalViewRef<unknown>;
remove(this._views, view);
view.detachFromAppRef();
}
@ -617,11 +645,12 @@ export class ApplicationRef {
const listeners = this._injector.get(APP_BOOTSTRAP_LISTENER, []);
if (ngDevMode && !Array.isArray(listeners)) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_MULTI_PROVIDER,
'Unexpected type of the `APP_BOOTSTRAP_LISTENER` token value ' +
`(expected an array, but got ${typeof listeners}). ` +
'Please check that the `APP_BOOTSTRAP_LISTENER` token is configured as a ' +
'`multi: true` provider.');
RuntimeErrorCode.INVALID_MULTI_PROVIDER,
'Unexpected type of the `APP_BOOTSTRAP_LISTENER` token value ' +
`(expected an array, but got ${typeof listeners}). ` +
'Please check that the `APP_BOOTSTRAP_LISTENER` token is configured as a ' +
'`multi: true` provider.',
);
}
[...this._bootstrapListeners, ...listeners].forEach((listener) => listener(componentRef));
}
@ -632,7 +661,7 @@ export class ApplicationRef {
try {
// Call all the lifecycle hooks.
this._destroyListeners.forEach(listener => listener());
this._destroyListeners.forEach((listener) => listener());
// Destroy all registered views.
this._views.slice().forEach((view) => view.destroy());
@ -667,13 +696,14 @@ export class ApplicationRef {
destroy(): void {
if (this._destroyed) {
throw new RuntimeError(
RuntimeErrorCode.APPLICATION_REF_ALREADY_DESTROYED,
ngDevMode && 'This instance of the `ApplicationRef` has already been destroyed.');
RuntimeErrorCode.APPLICATION_REF_ALREADY_DESTROYED,
ngDevMode && 'This instance of the `ApplicationRef` has already been destroyed.',
);
}
// This is a temporary type to represent an instance of an R3Injector, which can be destroyed.
// The type will be replaced with a different one once destroyable injector type is available.
type DestroyableInjector = Injector&{destroy?: Function, destroyed?: boolean};
type DestroyableInjector = Injector & {destroy?: Function; destroyed?: boolean};
const injector = this._injector as DestroyableInjector;
@ -694,9 +724,12 @@ export class ApplicationRef {
private warnIfDestroyed() {
if ((typeof ngDevMode === 'undefined' || ngDevMode) && this._destroyed) {
console.warn(formatRuntimeError(
console.warn(
formatRuntimeError(
RuntimeErrorCode.APPLICATION_REF_ALREADY_DESTROYED,
'This instance of the `ApplicationRef` has already been destroyed.'));
'This instance of the `ApplicationRef` has already been destroyed.',
),
);
}
}
}
@ -708,7 +741,7 @@ export function remove<T>(list: T[], el: T): void {
}
}
let whenStableStore: WeakMap<ApplicationRef, Promise<void>>|undefined;
let whenStableStore: WeakMap<ApplicationRef, Promise<void>> | undefined;
/**
* Returns a Promise that resolves when the application becomes stable after this method is called
* the first time.
@ -720,8 +753,10 @@ export function whenStable(applicationRef: ApplicationRef): Promise<void> {
return cachedWhenStable;
}
const whenStablePromise =
applicationRef.isStable.pipe(first((isStable) => isStable)).toPromise().then(() => void 0);
const whenStablePromise = applicationRef.isStable
.pipe(first((isStable) => isStable))
.toPromise()
.then(() => void 0);
whenStableStore.set(applicationRef, whenStablePromise);
// Be a good citizen and clean the store `onDestroy` even though we are using `WeakMap`.
@ -730,20 +765,24 @@ export function whenStable(applicationRef: ApplicationRef): Promise<void> {
return whenStablePromise;
}
export function detectChangesInViewIfRequired(
lView: LView, notifyErrorHandler: boolean, isFirstPass: boolean, zonelessEnabled: boolean) {
lView: LView,
notifyErrorHandler: boolean,
isFirstPass: boolean,
zonelessEnabled: boolean,
) {
// When re-checking, only check views which actually need it.
if (!isFirstPass && !requiresRefreshOrTraversal(lView)) {
return;
}
const mode = (isFirstPass && !zonelessEnabled) ?
// The first pass is always in Global mode, which includes `CheckAlways` views.
// When using zoneless, all root views must be explicitly marked for refresh, even if they are
// `CheckAlways`.
ChangeDetectionMode.Global :
// Only refresh views with the `RefreshView` flag or views is a changed signal
ChangeDetectionMode.Targeted;
const mode =
isFirstPass && !zonelessEnabled
? // The first pass is always in Global mode, which includes `CheckAlways` views.
// When using zoneless, all root views must be explicitly marked for refresh, even if they are
// `CheckAlways`.
ChangeDetectionMode.Global
: // Only refresh views with the `RefreshView` flag or views is a changed signal
ChangeDetectionMode.Targeted;
detectChangesInternal(lView, notifyErrorHandler, mode);
}

View file

@ -51,8 +51,9 @@ const DEFAULT_APP_ID = 'ng';
* A function that is executed when a platform is initialized.
* @publicApi
*/
export const PLATFORM_INITIALIZER =
new InjectionToken<ReadonlyArray<() => void>>(ngDevMode ? 'Platform Initializer' : '');
export const PLATFORM_INITIALIZER = new InjectionToken<ReadonlyArray<() => void>>(
ngDevMode ? 'Platform Initializer' : '',
);
/**
* A token that indicates an opaque platform ID.
@ -60,7 +61,7 @@ export const PLATFORM_INITIALIZER =
*/
export const PLATFORM_ID = new InjectionToken<Object>(ngDevMode ? 'Platform ID' : '', {
providedIn: 'platform',
factory: () => 'unknown', // set a default platform name, when none set explicitly
factory: () => 'unknown', // set a default platform name, when none set explicitly
});
/**
@ -69,8 +70,9 @@ export const PLATFORM_ID = new InjectionToken<Object>(ngDevMode ? 'Platform ID'
* @publicApi
* @deprecated
*/
export const PACKAGE_ROOT_URL =
new InjectionToken<string>(ngDevMode ? 'Application Packages Root URL' : '');
export const PACKAGE_ROOT_URL = new InjectionToken<string>(
ngDevMode ? 'Application Packages Root URL' : '',
);
// We keep this token here, rather than the animations package, so that modules that only care
// about which animations module is loaded (e.g. the CDK) can retrieve it without having to
@ -81,8 +83,9 @@ export const PACKAGE_ROOT_URL =
* module has been loaded.
* @publicApi
*/
export const ANIMATION_MODULE_TYPE = new InjectionToken<'NoopAnimations'|'BrowserAnimations'>(
ngDevMode ? 'AnimationModuleType' : '');
export const ANIMATION_MODULE_TYPE = new InjectionToken<'NoopAnimations' | 'BrowserAnimations'>(
ngDevMode ? 'AnimationModuleType' : '',
);
// TODO(crisbeto): link to CSP guide here.
/**
@ -92,7 +95,7 @@ export const ANIMATION_MODULE_TYPE = new InjectionToken<'NoopAnimations'|'Browse
*
* @publicApi
*/
export const CSP_NONCE = new InjectionToken<string|null>(ngDevMode ? 'CSP nonce' : '', {
export const CSP_NONCE = new InjectionToken<string | null>(ngDevMode ? 'CSP nonce' : '', {
providedIn: 'root',
factory: () => {
// Ideally we wouldn't have to use `querySelector` here since we know that the nonce will be on
@ -130,10 +133,10 @@ export const CSP_NONCE = new InjectionToken<string|null>(ngDevMode ? 'CSP nonce'
* @publicApi
*/
export type ImageConfig = {
breakpoints?: number[],
placeholderResolution?: number,
disableImageSizeWarning?: boolean,
disableImageLazyLoadWarning?: boolean,
breakpoints?: number[];
placeholderResolution?: number;
disableImageSizeWarning?: boolean;
disableImageLazyLoadWarning?: boolean;
};
export const IMAGE_CONFIG_DEFAULTS: ImageConfig = {
@ -152,5 +155,7 @@ export const IMAGE_CONFIG_DEFAULTS: ImageConfig = {
* @see {@link ImageConfig}
* @publicApi
*/
export const IMAGE_CONFIG = new InjectionToken<ImageConfig>(
ngDevMode ? 'ImageConfig' : '', {providedIn: 'root', factory: () => IMAGE_CONFIG_DEFAULTS});
export const IMAGE_CONFIG = new InjectionToken<ImageConfig>(ngDevMode ? 'ImageConfig' : '', {
providedIn: 'root',
factory: () => IMAGE_CONFIG_DEFAULTS,
});

View file

@ -41,7 +41,7 @@ import {_callAndReportToErrorHandler, ApplicationRef} from './application_ref';
export function internalCreateApplication(config: {
rootComponent?: Type<unknown>;
appProviders?: Array<Provider|EnvironmentProviders>;
appProviders?: Array<Provider | EnvironmentProviders>;
platformProviders?: Provider[];
}): Promise<ApplicationRef> {
try {
@ -55,14 +55,11 @@ export function internalCreateApplication(config: {
// Create root application injector based on a set of providers configured at the platform
// bootstrap level as well as providers passed to the bootstrap call by a user.
const allAppProviders = [
provideZoneChangeDetection(),
...(appProviders || []),
];
const allAppProviders = [provideZoneChangeDetection(), ...(appProviders || [])];
const adapter = new EnvironmentNgModuleRefAdapter({
providers: allAppProviders,
parent: platformInjector as EnvironmentInjector,
debugName: (typeof ngDevMode === 'undefined' || ngDevMode) ? 'Environment Injector' : '',
debugName: typeof ngDevMode === 'undefined' || ngDevMode ? 'Environment Injector' : '',
// We skip environment initializers because we need to run them inside the NgZone, which
// happens after we get the NgZone instance from the Injector.
runEnvironmentInitializers: false,
@ -72,11 +69,12 @@ export function internalCreateApplication(config: {
return ngZone.run(() => {
envInjector.resolveInjectorInitializers();
const exceptionHandler: ErrorHandler|null = envInjector.get(ErrorHandler, null);
const exceptionHandler: ErrorHandler | null = envInjector.get(ErrorHandler, null);
if ((typeof ngDevMode === 'undefined' || ngDevMode) && !exceptionHandler) {
throw new RuntimeError(
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
'No `ErrorHandler` found in the Dependency Injection tree.');
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
'No `ErrorHandler` found in the Dependency Injection tree.',
);
}
let onErrorSubscription: Subscription;
@ -84,7 +82,7 @@ export function internalCreateApplication(config: {
onErrorSubscription = ngZone.onError.subscribe({
next: (error: any) => {
exceptionHandler!.handleError(error);
}
},
});
});

View file

@ -10,11 +10,21 @@
// https://docs.google.com/document/d/1RXb1wYwsbJotO1KBgSDsAtKpduGmIHod9ADxuXcAvV4/edit?tab=t.0.
export {InputFunction} from './authoring/input/input';
export {InputOptions, InputOptionsWithoutTransform, InputOptionsWithTransform, InputSignal, InputSignalWithTransform, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE} from './authoring/input/input_signal';
export {
InputOptions,
InputOptionsWithoutTransform,
InputOptionsWithTransform,
InputSignal,
InputSignalWithTransform,
ɵINPUT_SIGNAL_BRAND_WRITE_TYPE,
} from './authoring/input/input_signal';
export {ɵUnwrapDirectiveSignalInputs} from './authoring/input/input_type_checking';
export {ModelFunction} from './authoring/model/model';
export {ModelOptions, ModelSignal} from './authoring/model/model_signal';
export {output, OutputOptions} from './authoring/output/output';
export {getOutputDestroyRef as ɵgetOutputDestroyRef, OutputEmitterRef} from './authoring/output/output_emitter_ref';
export {
getOutputDestroyRef as ɵgetOutputDestroyRef,
OutputEmitterRef,
} from './authoring/output/output_emitter_ref';
export {OutputRef, OutputRefSubscription} from './authoring/output/output_ref';
export {ContentChildFunction, ViewChildFunction} from './authoring/queries';

View file

@ -8,18 +8,27 @@
import {assertInInjectionContext} from '../../di';
import {createInputSignal, InputOptions, InputOptionsWithoutTransform, InputOptionsWithTransform, InputSignal, InputSignalWithTransform} from './input_signal';
import {
createInputSignal,
InputOptions,
InputOptionsWithoutTransform,
InputOptionsWithTransform,
InputSignal,
InputSignalWithTransform,
} from './input_signal';
import {REQUIRED_UNSET_VALUE} from './input_signal_node';
export function inputFunction<ReadT, WriteT>(
initialValue?: ReadT,
opts?: InputOptions<ReadT, WriteT>): InputSignalWithTransform<ReadT|undefined, WriteT> {
initialValue?: ReadT,
opts?: InputOptions<ReadT, WriteT>,
): InputSignalWithTransform<ReadT | undefined, WriteT> {
ngDevMode && assertInInjectionContext(input);
return createInputSignal(initialValue, opts);
}
export function inputRequiredFunction<ReadT, WriteT = ReadT>(opts?: InputOptions<ReadT, WriteT>):
InputSignalWithTransform<ReadT, WriteT> {
export function inputRequiredFunction<ReadT, WriteT = ReadT>(
opts?: InputOptions<ReadT, WriteT>,
): InputSignalWithTransform<ReadT, WriteT> {
ngDevMode && assertInInjectionContext(input);
return createInputSignal(REQUIRED_UNSET_VALUE as never, opts);
}
@ -39,7 +48,7 @@ export interface InputFunction {
* Initializes an input of type `T` with an initial value of `undefined`.
* Angular will implicitly use `undefined` as initial value.
*/
<T>(): InputSignal<T|undefined>;
<T>(): InputSignal<T | undefined>;
/** Declares an input of type `T` with an explicit initial value. */
<T>(initialValue: T, opts?: InputOptionsWithoutTransform<T>): InputSignal<T>;
/**
@ -49,8 +58,10 @@ export interface InputFunction {
* The input accepts values of type `TransformT` and the given
* transform function will transform the value to type `T`.
*/
<T, TransformT>(initialValue: T, opts: InputOptionsWithTransform<T, TransformT>):
InputSignalWithTransform<T, TransformT>;
<T, TransformT>(
initialValue: T,
opts: InputOptionsWithTransform<T, TransformT>,
): InputSignalWithTransform<T, TransformT>;
/**
* Initializes a required input.
@ -69,8 +80,9 @@ export interface InputFunction {
* The input accepts values of type `TransformT` and the given
* transform function will transform the value to type `T`.
*/
<T, TransformT>(opts: InputOptionsWithTransform<T, TransformT>):
InputSignalWithTransform<T, TransformT>;
<T, TransformT>(
opts: InputOptionsWithTransform<T, TransformT>,
): InputSignalWithTransform<T, TransformT>;
};
}
@ -127,5 +139,5 @@ export const input: InputFunction = (() => {
// this assignment, unless this `input` constant export is accessed. It's a
// self-contained side effect that is local to the user facing`input` export.
(inputFunction as any).required = inputRequiredFunction;
return inputFunction as (typeof inputFunction&{required: typeof inputRequiredFunction});
return inputFunction as typeof inputFunction & {required: typeof inputRequiredFunction};
})();

View file

@ -40,15 +40,17 @@ export interface InputOptions<T, TransformT> {
* @developerPreview
*/
export type InputOptionsWithoutTransform<T> =
// Note: We still keep a notion of `transform` for auto-completion.
Omit<InputOptions<T, T>, 'transform'>&{transform?: undefined};
// Note: We still keep a notion of `transform` for auto-completion.
Omit<InputOptions<T, T>, 'transform'> & {transform?: undefined};
/**
* Signal input options with the transform option required.
*
* @developerPreview
*/
export type InputOptionsWithTransform<T, TransformT> =
Required<Pick<InputOptions<T, TransformT>, 'transform'>>&InputOptions<T, TransformT>;
export type InputOptionsWithTransform<T, TransformT> = Required<
Pick<InputOptions<T, TransformT>, 'transform'>
> &
InputOptions<T, TransformT>;
export const ɵINPUT_SIGNAL_BRAND_READ_TYPE = /* @__PURE__ */ Symbol();
export const ɵINPUT_SIGNAL_BRAND_WRITE_TYPE = /* @__PURE__ */ Symbol();
@ -104,8 +106,9 @@ export interface InputSignal<T> extends InputSignalWithTransform<T, T> {}
* @param options Additional options for the input. e.g. a transform, or an alias.
*/
export function createInputSignal<T, TransformT>(
initialValue: T,
options?: InputOptions<T, TransformT>): InputSignalWithTransform<T, TransformT> {
initialValue: T,
options?: InputOptions<T, TransformT>,
): InputSignalWithTransform<T, TransformT> {
const node: InputSignalNode<T, TransformT> = Object.create(INPUT_SIGNAL_NODE);
node.value = initialValue;
@ -120,8 +123,9 @@ export function createInputSignal<T, TransformT>(
if (node.value === REQUIRED_UNSET_VALUE) {
throw new RuntimeError(
RuntimeErrorCode.REQUIRED_INPUT_NO_VALUE,
ngDevMode && 'Input is required but no value is available yet.');
RuntimeErrorCode.REQUIRED_INPUT_NO_VALUE,
ngDevMode && 'Input is required but no value is available yet.',
);
}
return node.value;

View file

@ -19,7 +19,7 @@ export interface InputSignalNode<T, TransformT> extends SignalNode<T> {
* User-configured transform that will run whenever a new value is applied
* to the input signal node.
*/
transformFn: ((value: TransformT) => T)|undefined;
transformFn: ((value: TransformT) => T) | undefined;
/**
* Applies a new value to the input signal. Expects transforms to be run
@ -42,6 +42,6 @@ export const INPUT_SIGNAL_NODE: InputSignalNode<unknown, unknown> = /* @__PURE__
applyValueToInputSignal<T, TransformT>(node: InputSignalNode<T, TransformT>, value: T) {
signalSetFn(node, value);
}
},
};
})();

View file

@ -10,12 +10,12 @@ import {InputSignalWithTransform} from './input_signal';
/** Retrieves the write type of an `InputSignal` and `InputSignalWithTransform`. */
export type ɵUnwrapInputSignalWriteType<Field> =
Field extends InputSignalWithTransform<any, infer WriteT>? WriteT : never;
Field extends InputSignalWithTransform<any, infer WriteT> ? WriteT : never;
/**
* Unwraps all `InputSignal`/`InputSignalWithTransform` class fields of
* the given directive.
*/
export type ɵUnwrapDirectiveSignalInputs<Dir, Fields extends keyof Dir> = {
[P in Fields]: ɵUnwrapInputSignalWriteType<Dir[P]>
[P in Fields]: ɵUnwrapInputSignalWriteType<Dir[P]>;
};

View file

@ -11,7 +11,7 @@ import {REQUIRED_UNSET_VALUE} from '../input/input_signal_node';
import {createModelSignal, ModelOptions, ModelSignal} from './model_signal';
export function modelFunction<T>(initialValue?: T): ModelSignal<T|undefined> {
export function modelFunction<T>(initialValue?: T): ModelSignal<T | undefined> {
ngDevMode && assertInInjectionContext(model);
return createModelSignal(initialValue);
@ -39,7 +39,7 @@ export interface ModelFunction {
* Initializes a model of type `T` with an initial value of `undefined`.
* Angular will implicitly use `undefined` as initial value.
*/
<T>(): ModelSignal<T|undefined>;
<T>(): ModelSignal<T | undefined>;
/** Initializes a model of type `T` with the given initial value. */
<T>(initialValue: T, opts?: ModelOptions): ModelSignal<T>;
@ -106,5 +106,5 @@ export const model: ModelFunction = (() => {
// this assignment, unless this `model` constant export is accessed. It's a
// self-contained side effect that is local to the user facing `model` export.
(modelFunction as any).required = modelRequiredFunction;
return modelFunction as (typeof modelFunction&{required: typeof modelRequiredFunction});
return modelFunction as typeof modelFunction & {required: typeof modelRequiredFunction};
})();

View file

@ -10,7 +10,11 @@ import {producerAccessed, SIGNAL, signalSetFn} from '@angular/core/primitives/si
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {Signal} from '../../render3/reactivity/api';
import {signalAsReadonlyFn, WritableSignal, ɵWRITABLE_SIGNAL} from '../../render3/reactivity/signal';
import {
signalAsReadonlyFn,
WritableSignal,
ɵWRITABLE_SIGNAL,
} from '../../render3/reactivity/signal';
import {ɵINPUT_SIGNAL_BRAND_READ_TYPE, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE} from '../input/input_signal';
import {INPUT_SIGNAL_NODE, InputSignalNode, REQUIRED_UNSET_VALUE} from '../input/input_signal_node';
import {OutputEmitterRef} from '../output/output_emitter_ref';
@ -85,17 +89,21 @@ export function createModelSignal<T>(initialValue: T): ModelSignal<T> {
getter.toString = () => `[Model Signal: ${getter()}]`;
}
return getter as (typeof getter&Pick<
ModelSignal<T>,
typeof ɵINPUT_SIGNAL_BRAND_READ_TYPE|typeof ɵINPUT_SIGNAL_BRAND_WRITE_TYPE|
typeof ɵWRITABLE_SIGNAL>);
return getter as typeof getter &
Pick<
ModelSignal<T>,
| typeof ɵINPUT_SIGNAL_BRAND_READ_TYPE
| typeof ɵINPUT_SIGNAL_BRAND_WRITE_TYPE
| typeof ɵWRITABLE_SIGNAL
>;
}
/** Asserts that a model's value is set. */
function assertModelSet(value: unknown): void {
if (value === REQUIRED_UNSET_VALUE) {
throw new RuntimeError(
RuntimeErrorCode.REQUIRED_MODEL_NO_VALUE,
ngDevMode && 'Model is required but no value is available yet.');
RuntimeErrorCode.REQUIRED_MODEL_NO_VALUE,
ngDevMode && 'Model is required but no value is available yet.',
);
}
}

View file

@ -30,7 +30,7 @@ import {OutputRef, OutputRefSubscription} from './output_ref';
*/
export class OutputEmitterRef<T> implements OutputRef<T> {
private destroyed = false;
private listeners: Array<(value: T) => void>|null = null;
private listeners: Array<(value: T) => void> | null = null;
private errorHandler = inject(ErrorHandler, {optional: true});
/** @internal */
@ -47,10 +47,11 @@ export class OutputEmitterRef<T> implements OutputRef<T> {
subscribe(callback: (value: T) => void): OutputRefSubscription {
if (this.destroyed) {
throw new RuntimeError(
RuntimeErrorCode.OUTPUT_REF_DESTROYED,
ngDevMode &&
'Unexpected subscription to destroyed `OutputRef`. ' +
'The owning directive/component is destroyed.');
RuntimeErrorCode.OUTPUT_REF_DESTROYED,
ngDevMode &&
'Unexpected subscription to destroyed `OutputRef`. ' +
'The owning directive/component is destroyed.',
);
}
(this.listeners ??= []).push(callback);
@ -61,7 +62,7 @@ export class OutputEmitterRef<T> implements OutputRef<T> {
if (idx !== undefined && idx !== -1) {
this.listeners?.splice(idx, 1);
}
}
},
};
}
@ -69,10 +70,11 @@ export class OutputEmitterRef<T> implements OutputRef<T> {
emit(value: T): void {
if (this.destroyed) {
throw new RuntimeError(
RuntimeErrorCode.OUTPUT_REF_DESTROYED,
ngDevMode &&
'Unexpected emit for destroyed `OutputRef`. ' +
'The owning directive/component is destroyed.');
RuntimeErrorCode.OUTPUT_REF_DESTROYED,
ngDevMode &&
'Unexpected emit for destroyed `OutputRef`. ' +
'The owning directive/component is destroyed.',
);
}
if (this.listeners === null) {
@ -95,6 +97,6 @@ export class OutputEmitterRef<T> implements OutputRef<T> {
}
/** Gets the owning `DestroyRef` for the given output. */
export function getOutputDestroyRef(ref: OutputRef<unknown>): DestroyRef|undefined {
export function getOutputDestroyRef(ref: OutputRef<unknown>): DestroyRef | undefined {
return ref.destroyRef;
}

View file

@ -46,5 +46,5 @@ export interface OutputRef<T> {
*
* @internal
*/
destroyRef: DestroyRef|undefined;
destroyRef: DestroyRef | undefined;
}

View file

@ -8,18 +8,25 @@
import {assertInInjectionContext} from '../di';
import {ProviderToken} from '../di/provider_token';
import {createMultiResultQuerySignalFn, createSingleResultOptionalQuerySignalFn, createSingleResultRequiredQuerySignalFn} from '../render3/query_reactive';
import {
createMultiResultQuerySignalFn,
createSingleResultOptionalQuerySignalFn,
createSingleResultRequiredQuerySignalFn,
} from '../render3/query_reactive';
import {Signal} from '../render3/reactivity/api';
function viewChildFn<LocatorT, ReadT>(
locator: ProviderToken<LocatorT>|string,
opts?: {read?: ProviderToken<ReadT>}): Signal<ReadT|undefined> {
locator: ProviderToken<LocatorT> | string,
opts?: {read?: ProviderToken<ReadT>},
): Signal<ReadT | undefined> {
ngDevMode && assertInInjectionContext(viewChild);
return createSingleResultOptionalQuerySignalFn<ReadT>();
}
function viewChildRequiredFn<LocatorT, ReadT>(
locator: ProviderToken<LocatorT>|string, opts?: {read?: ProviderToken<ReadT>}): Signal<ReadT> {
locator: ProviderToken<LocatorT> | string,
opts?: {read?: ProviderToken<ReadT>},
): Signal<ReadT> {
ngDevMode && assertInInjectionContext(viewChild);
return createSingleResultRequiredQuerySignalFn<ReadT>();
}
@ -40,9 +47,11 @@ export interface ViewChildFunction {
*
* @developerPreview
*/
<LocatorT>(locator: ProviderToken<LocatorT>|string): Signal<LocatorT|undefined>;
<LocatorT, ReadT>(locator: ProviderToken<LocatorT>|string, opts: {read: ProviderToken<ReadT>}):
Signal<ReadT|undefined>;
<LocatorT>(locator: ProviderToken<LocatorT> | string): Signal<LocatorT | undefined>;
<LocatorT, ReadT>(
locator: ProviderToken<LocatorT> | string,
opts: {read: ProviderToken<ReadT>},
): Signal<ReadT | undefined>;
/**
* Initializes a view child query that is expected to always match an element.
@ -50,10 +59,12 @@ export interface ViewChildFunction {
* @developerPreview
*/
required: {
<LocatorT>(locator: ProviderToken<LocatorT>|string): Signal<LocatorT>;
<LocatorT>(locator: ProviderToken<LocatorT> | string): Signal<LocatorT>;
<LocatorT, ReadT>(locator: ProviderToken<LocatorT>|string, opts: {read: ProviderToken<ReadT>}):
Signal<ReadT>;
<LocatorT, ReadT>(
locator: ProviderToken<LocatorT> | string,
opts: {read: ProviderToken<ReadT>},
): Signal<ReadT>;
};
}
@ -84,14 +95,16 @@ export const viewChild: ViewChildFunction = (() => {
// this assignment, unless this `viewChild` constant export is accessed. It's a
// self-contained side effect that is local to the user facing `viewChild` export.
(viewChildFn as any).required = viewChildRequiredFn;
return viewChildFn as (typeof viewChildFn&{required: typeof viewChildRequiredFn});
return viewChildFn as typeof viewChildFn & {required: typeof viewChildRequiredFn};
})();
export function viewChildren<LocatorT>(locator: ProviderToken<LocatorT>|
string): Signal<ReadonlyArray<LocatorT>>;
export function viewChildren<LocatorT>(
locator: ProviderToken<LocatorT> | string,
): Signal<ReadonlyArray<LocatorT>>;
export function viewChildren<LocatorT, ReadT>(
locator: ProviderToken<LocatorT>|string,
opts: {read: ProviderToken<ReadT>}): Signal<ReadonlyArray<ReadT>>;
locator: ProviderToken<LocatorT> | string,
opts: {read: ProviderToken<ReadT>},
): Signal<ReadonlyArray<ReadT>>;
/**
* Initializes a view children query.
@ -114,22 +127,25 @@ export function viewChildren<LocatorT, ReadT>(
* @developerPreview
*/
export function viewChildren<LocatorT, ReadT>(
locator: ProviderToken<LocatorT>|string,
opts?: {read?: ProviderToken<ReadT>}): Signal<ReadonlyArray<ReadT>> {
locator: ProviderToken<LocatorT> | string,
opts?: {read?: ProviderToken<ReadT>},
): Signal<ReadonlyArray<ReadT>> {
ngDevMode && assertInInjectionContext(viewChildren);
return createMultiResultQuerySignalFn<ReadT>();
}
export function contentChildFn<LocatorT, ReadT>(
locator: ProviderToken<LocatorT>|string,
opts?: {descendants?: boolean, read?: ProviderToken<ReadT>}): Signal<ReadT|undefined> {
locator: ProviderToken<LocatorT> | string,
opts?: {descendants?: boolean; read?: ProviderToken<ReadT>},
): Signal<ReadT | undefined> {
ngDevMode && assertInInjectionContext(contentChild);
return createSingleResultOptionalQuerySignalFn<ReadT>();
}
function contentChildRequiredFn<LocatorT, ReadT>(
locator: ProviderToken<LocatorT>|string,
opts?: {descendants?: boolean, read?: ProviderToken<ReadT>}): Signal<ReadT> {
locator: ProviderToken<LocatorT> | string,
opts?: {descendants?: boolean; read?: ProviderToken<ReadT>},
): Signal<ReadT> {
ngDevMode && assertInInjectionContext(contentChildren);
return createSingleResultRequiredQuerySignalFn<ReadT>();
}
@ -150,27 +166,38 @@ export interface ContentChildFunction {
* Consider using `contentChild.required` for queries that should always match.
* @developerPreview
*/
<LocatorT>(locator: ProviderToken<LocatorT>|string, opts?: {
descendants?: boolean,
read?: undefined
}): Signal<LocatorT|undefined>;
<LocatorT>(
locator: ProviderToken<LocatorT> | string,
opts?: {
descendants?: boolean;
read?: undefined;
},
): Signal<LocatorT | undefined>;
<LocatorT, ReadT>(locator: ProviderToken<LocatorT>|string, opts: {
descendants?: boolean, read: ProviderToken<ReadT>
}): Signal<ReadT|undefined>;
<LocatorT, ReadT>(
locator: ProviderToken<LocatorT> | string,
opts: {
descendants?: boolean;
read: ProviderToken<ReadT>;
},
): Signal<ReadT | undefined>;
/**
* Initializes a content child query that is always expected to match.
*/
required: {
<LocatorT>(locator: ProviderToken<LocatorT>|string, opts?: {
descendants?: boolean,
read?: undefined,
}): Signal<LocatorT>;
<LocatorT>(
locator: ProviderToken<LocatorT> | string,
opts?: {
descendants?: boolean;
read?: undefined;
},
): Signal<LocatorT>;
<LocatorT, ReadT>(
locator: ProviderToken<LocatorT>|string,
opts: {descendants?: boolean, read: ProviderToken<ReadT>}): Signal<ReadT>;
locator: ProviderToken<LocatorT> | string,
opts: {descendants?: boolean; read: ProviderToken<ReadT>},
): Signal<ReadT>;
};
}
@ -200,16 +227,17 @@ export const contentChild: ContentChildFunction = (() => {
// this assignment, unless this `viewChild` constant export is accessed. It's a
// self-contained side effect that is local to the user facing `viewChild` export.
(contentChildFn as any).required = contentChildRequiredFn;
return contentChildFn as (typeof contentChildFn&{required: typeof contentChildRequiredFn});
return contentChildFn as typeof contentChildFn & {required: typeof contentChildRequiredFn};
})();
export function contentChildren<LocatorT>(
locator: ProviderToken<LocatorT>|string,
opts?: {descendants?: boolean, read?: undefined}): Signal<ReadonlyArray<LocatorT>>;
locator: ProviderToken<LocatorT> | string,
opts?: {descendants?: boolean; read?: undefined},
): Signal<ReadonlyArray<LocatorT>>;
export function contentChildren<LocatorT, ReadT>(
locator: ProviderToken<LocatorT>|string,
opts: {descendants?: boolean, read: ProviderToken<ReadT>}): Signal<ReadonlyArray<ReadT>>;
locator: ProviderToken<LocatorT> | string,
opts: {descendants?: boolean; read: ProviderToken<ReadT>},
): Signal<ReadonlyArray<ReadT>>;
/**
* Initializes a content children query.
@ -232,7 +260,8 @@ export function contentChildren<LocatorT, ReadT>(
* @developerPreview
*/
export function contentChildren<LocatorT, ReadT>(
locator: ProviderToken<LocatorT>|string,
opts?: {descendants?: boolean, read?: ProviderToken<ReadT>}): Signal<ReadonlyArray<ReadT>> {
locator: ProviderToken<LocatorT> | string,
opts?: {descendants?: boolean; read?: ProviderToken<ReadT>},
): Signal<ReadonlyArray<ReadT>> {
return createMultiResultQuerySignalFn<ReadT>();
}

View file

@ -20,15 +20,19 @@ import {createEnvironmentInjector} from './render3/ng_module_ref';
* of a certain type.
*/
export class CachedInjectorService implements OnDestroy {
private cachedInjectors = new Map<unknown, EnvironmentInjector|null>();
private cachedInjectors = new Map<unknown, EnvironmentInjector | null>();
getOrCreateInjector(
key: unknown, parentInjector: EnvironmentInjector, providers: Provider[],
debugName?: string) {
key: unknown,
parentInjector: EnvironmentInjector,
providers: Provider[],
debugName?: string,
) {
if (!this.cachedInjectors.has(key)) {
const injector = providers.length > 0 ?
createEnvironmentInjector(providers, parentInjector, debugName) :
null;
const injector =
providers.length > 0
? createEnvironmentInjector(providers, parentInjector, debugName)
: null;
this.cachedInjectors.set(key, injector);
}
return this.cachedInjectors.get(key)!;

View file

@ -12,4 +12,23 @@
* Change detection enables data binding in Angular.
*/
export {ChangeDetectionStrategy, ChangeDetectorRef, DefaultIterableDiffer, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers, NgIterable, PipeTransform, SimpleChange, SimpleChanges, TrackByFunction} from './change_detection/change_detection';
export {
ChangeDetectionStrategy,
ChangeDetectorRef,
DefaultIterableDiffer,
IterableChangeRecord,
IterableChanges,
IterableDiffer,
IterableDifferFactory,
IterableDiffers,
KeyValueChangeRecord,
KeyValueChanges,
KeyValueDiffer,
KeyValueDifferFactory,
KeyValueDiffers,
NgIterable,
PipeTransform,
SimpleChange,
SimpleChanges,
TrackByFunction,
} from './change_detection/change_detection';

View file

@ -15,14 +15,29 @@ export {SimpleChange, SimpleChanges} from '../interface/simple_change';
export {devModeEqual} from '../util/comparison';
export {ChangeDetectorRef} from './change_detector_ref';
export {ChangeDetectionStrategy} from './constants';
export {DefaultIterableDiffer, DefaultIterableDifferFactory} from './differs/default_iterable_differ';
export {
DefaultIterableDiffer,
DefaultIterableDifferFactory,
} from './differs/default_iterable_differ';
export {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
export {IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs';
export {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
export {
IterableChangeRecord,
IterableChanges,
IterableDiffer,
IterableDifferFactory,
IterableDiffers,
NgIterable,
TrackByFunction,
} from './differs/iterable_differs';
export {
KeyValueChangeRecord,
KeyValueChanges,
KeyValueDiffer,
KeyValueDifferFactory,
KeyValueDiffers,
} from './differs/keyvalue_differs';
export {PipeTransform} from './pipe_transform';
/**
* Structural diffing for `Object`s and `Map`s.
*/

View file

@ -127,13 +127,13 @@ export abstract class ChangeDetectorRef {
static __NG_ELEMENT_ID__: (flags: InjectFlags) => ChangeDetectorRef = injectChangeDetectorRef;
}
/** Returns a ChangeDetectorRef (a.k.a. a ViewRef) */
export function injectChangeDetectorRef(flags: InjectFlags): ChangeDetectorRef {
return createViewRef(
getCurrentTNode()!, getLView(),
(flags & InternalInjectFlags.ForPipe) === InternalInjectFlags.ForPipe);
getCurrentTNode()!,
getLView(),
(flags & InternalInjectFlags.ForPipe) === InternalInjectFlags.ForPipe,
);
}
/**
@ -148,12 +148,12 @@ function createViewRef(tNode: TNode, lView: LView, isPipe: boolean): ChangeDetec
if (isComponentHost(tNode) && !isPipe) {
// The LView represents the location where the component is declared.
// Instead we want the LView for the component View and so we need to look it up.
const componentView = getComponentLViewByIndex(tNode.index, lView); // look down
const componentView = getComponentLViewByIndex(tNode.index, lView); // look down
return new ViewRef(componentView, componentView);
} else if (tNode.type & (TNodeType.AnyRNode | TNodeType.AnyContainer | TNodeType.Icu)) {
// The LView represents the location where the injection is requested from.
// We need to locate the containing LView (in case where the `lView` is an embedded view)
const hostComponentView = lView[DECLARATION_COMPONENT_VIEW]; // look up
const hostComponentView = lView[DECLARATION_COMPONENT_VIEW]; // look up
return new ViewRef(hostComponentView, lView);
}
return null!;

View file

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
/**
* The strategy that the default change detector uses to detect changes.
* When set, takes effect the next time change detection is triggered.

View file

@ -11,12 +11,18 @@ import {Writable} from '../../interface/type';
import {isListLikeIterable, iterateListLike} from '../../util/iterable';
import {stringify} from '../../util/stringify';
import {IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, NgIterable, TrackByFunction} from './iterable_differs';
import {
IterableChangeRecord,
IterableChanges,
IterableDiffer,
IterableDifferFactory,
NgIterable,
TrackByFunction,
} from './iterable_differs';
export class DefaultIterableDifferFactory implements IterableDifferFactory {
constructor() {}
supports(obj: Object|null|undefined): boolean {
supports(obj: Object | null | undefined): boolean {
return isListLikeIterable(obj);
}
@ -34,23 +40,23 @@ const trackByIdentity = (index: number, item: any) => item;
export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChanges<V> {
public readonly length: number = 0;
// TODO: confirm the usage of `collection` as it's unused, readonly and on a non public API.
public readonly collection!: V[]|Iterable<V>|null;
public readonly collection!: V[] | Iterable<V> | null;
// Keeps track of the used records at any point in time (during & across `_check()` calls)
private _linkedRecords: _DuplicateMap<V>|null = null;
private _linkedRecords: _DuplicateMap<V> | null = null;
// Keeps track of the removed records at any point in time during `_check()` calls.
private _unlinkedRecords: _DuplicateMap<V>|null = null;
private _previousItHead: IterableChangeRecord_<V>|null = null;
private _itHead: IterableChangeRecord_<V>|null = null;
private _itTail: IterableChangeRecord_<V>|null = null;
private _additionsHead: IterableChangeRecord_<V>|null = null;
private _additionsTail: IterableChangeRecord_<V>|null = null;
private _movesHead: IterableChangeRecord_<V>|null = null;
private _movesTail: IterableChangeRecord_<V>|null = null;
private _removalsHead: IterableChangeRecord_<V>|null = null;
private _removalsTail: IterableChangeRecord_<V>|null = null;
private _unlinkedRecords: _DuplicateMap<V> | null = null;
private _previousItHead: IterableChangeRecord_<V> | null = null;
private _itHead: IterableChangeRecord_<V> | null = null;
private _itTail: IterableChangeRecord_<V> | null = null;
private _additionsHead: IterableChangeRecord_<V> | null = null;
private _additionsTail: IterableChangeRecord_<V> | null = null;
private _movesHead: IterableChangeRecord_<V> | null = null;
private _movesTail: IterableChangeRecord_<V> | null = null;
private _removalsHead: IterableChangeRecord_<V> | null = null;
private _removalsTail: IterableChangeRecord_<V> | null = null;
// Keeps track of records where custom track by is the same, but item identity has changed
private _identityChangesHead: IterableChangeRecord_<V>|null = null;
private _identityChangesTail: IterableChangeRecord_<V>|null = null;
private _identityChangesHead: IterableChangeRecord_<V> | null = null;
private _identityChangesTail: IterableChangeRecord_<V> | null = null;
private _trackByFn: TrackByFunction<V>;
constructor(trackByFn?: TrackByFunction<V>) {
@ -58,28 +64,32 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
}
forEachItem(fn: (record: IterableChangeRecord_<V>) => void) {
let record: IterableChangeRecord_<V>|null;
let record: IterableChangeRecord_<V> | null;
for (record = this._itHead; record !== null; record = record._next) {
fn(record);
}
}
forEachOperation(
fn: (item: IterableChangeRecord<V>, previousIndex: number|null, currentIndex: number|null) =>
void) {
fn: (
item: IterableChangeRecord<V>,
previousIndex: number | null,
currentIndex: number | null,
) => void,
) {
let nextIt = this._itHead;
let nextRemove = this._removalsHead;
let addRemoveOffset = 0;
let moveOffsets: number[]|null = null;
let moveOffsets: number[] | null = null;
while (nextIt || nextRemove) {
// Figure out which is the next record to process
// Order: remove, add, move
const record: IterableChangeRecord<V> = !nextRemove ||
nextIt &&
nextIt.currentIndex! <
getPreviousIndex(nextRemove, addRemoveOffset, moveOffsets) ?
nextIt! :
nextRemove;
const record: IterableChangeRecord<V> =
!nextRemove ||
(nextIt &&
nextIt.currentIndex! < getPreviousIndex(nextRemove, addRemoveOffset, moveOffsets))
? nextIt!
: nextRemove;
const adjPreviousIndex = getPreviousIndex(record, addRemoveOffset, moveOffsets);
const currentIndex = record.currentIndex;
@ -117,48 +127,48 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
}
forEachPreviousItem(fn: (record: IterableChangeRecord_<V>) => void) {
let record: IterableChangeRecord_<V>|null;
let record: IterableChangeRecord_<V> | null;
for (record = this._previousItHead; record !== null; record = record._nextPrevious) {
fn(record);
}
}
forEachAddedItem(fn: (record: IterableChangeRecord_<V>) => void) {
let record: IterableChangeRecord_<V>|null;
let record: IterableChangeRecord_<V> | null;
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
fn(record);
}
}
forEachMovedItem(fn: (record: IterableChangeRecord_<V>) => void) {
let record: IterableChangeRecord_<V>|null;
let record: IterableChangeRecord_<V> | null;
for (record = this._movesHead; record !== null; record = record._nextMoved) {
fn(record);
}
}
forEachRemovedItem(fn: (record: IterableChangeRecord_<V>) => void) {
let record: IterableChangeRecord_<V>|null;
let record: IterableChangeRecord_<V> | null;
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
fn(record);
}
}
forEachIdentityChange(fn: (record: IterableChangeRecord_<V>) => void) {
let record: IterableChangeRecord_<V>|null;
let record: IterableChangeRecord_<V> | null;
for (record = this._identityChangesHead; record !== null; record = record._nextIdentityChange) {
fn(record);
}
}
diff(collection: NgIterable<V>|null|undefined): DefaultIterableDiffer<V>|null {
diff(collection: NgIterable<V> | null | undefined): DefaultIterableDiffer<V> | null {
if (collection == null) collection = [];
if (!isListLikeIterable(collection)) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_DIFFER_INPUT,
ngDevMode &&
`Error trying to diff '${
stringify(collection)}'. Only arrays and iterables are allowed`);
RuntimeErrorCode.INVALID_DIFFER_INPUT,
ngDevMode &&
`Error trying to diff '${stringify(collection)}'. Only arrays and iterables are allowed`,
);
}
if (this.check(collection)) {
@ -173,7 +183,7 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
check(collection: NgIterable<V>): boolean {
this._reset();
let record: IterableChangeRecord_<V>|null = this._itHead;
let record: IterableChangeRecord_<V> | null = this._itHead;
let mayBeDirty: boolean = false;
let index: number;
let item: V;
@ -226,8 +236,12 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
* changes.
*/
get isDirty(): boolean {
return this._additionsHead !== null || this._movesHead !== null ||
this._removalsHead !== null || this._identityChangesHead !== null;
return (
this._additionsHead !== null ||
this._movesHead !== null ||
this._removalsHead !== null ||
this._identityChangesHead !== null
);
}
/**
@ -240,7 +254,7 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
*/
_reset() {
if (this.isDirty) {
let record: IterableChangeRecord_<V>|null;
let record: IterableChangeRecord_<V> | null;
for (record = this._previousItHead = this._itHead; record !== null; record = record._next) {
record._nextPrevious = record._next;
@ -273,10 +287,14 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
*
* @internal
*/
_mismatch(record: IterableChangeRecord_<V>|null, item: V, itemTrackBy: any, index: number):
IterableChangeRecord_<V> {
_mismatch(
record: IterableChangeRecord_<V> | null,
item: V,
itemTrackBy: any,
index: number,
): IterableChangeRecord_<V> {
// The previous record after which we will append the current one.
let previousRecord: IterableChangeRecord_<V>|null;
let previousRecord: IterableChangeRecord_<V> | null;
if (record === null) {
previousRecord = this._itTail;
@ -306,8 +324,11 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
this._moveAfter(record, previousRecord, index);
} else {
// It is a new item: add it.
record =
this._addAfter(new IterableChangeRecord_<V>(item, itemTrackBy), previousRecord, index);
record = this._addAfter(
new IterableChangeRecord_<V>(item, itemTrackBy),
previousRecord,
index,
);
}
}
return record;
@ -340,10 +361,14 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
*
* @internal
*/
_verifyReinsertion(record: IterableChangeRecord_<V>, item: V, itemTrackBy: any, index: number):
IterableChangeRecord_<V> {
let reinsertRecord: IterableChangeRecord_<V>|null =
this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy, null);
_verifyReinsertion(
record: IterableChangeRecord_<V>,
item: V,
itemTrackBy: any,
index: number,
): IterableChangeRecord_<V> {
let reinsertRecord: IterableChangeRecord_<V> | null =
this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy, null);
if (reinsertRecord !== null) {
record = this._reinsertAfter(reinsertRecord, record._prev!, index);
} else if (record.currentIndex != index) {
@ -360,10 +385,10 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
*
* @internal
*/
_truncate(record: IterableChangeRecord_<V>|null) {
_truncate(record: IterableChangeRecord_<V> | null) {
// Anything after that needs to be removed;
while (record !== null) {
const nextRecord: IterableChangeRecord_<V>|null = record._next;
const nextRecord: IterableChangeRecord_<V> | null = record._next;
this._addToRemovals(this._unlink(record));
record = nextRecord;
}
@ -390,8 +415,10 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
/** @internal */
_reinsertAfter(
record: IterableChangeRecord_<V>, prevRecord: IterableChangeRecord_<V>|null,
index: number): IterableChangeRecord_<V> {
record: IterableChangeRecord_<V>,
prevRecord: IterableChangeRecord_<V> | null,
index: number,
): IterableChangeRecord_<V> {
if (this._unlinkedRecords !== null) {
this._unlinkedRecords.remove(record);
}
@ -416,8 +443,10 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
/** @internal */
_moveAfter(
record: IterableChangeRecord_<V>, prevRecord: IterableChangeRecord_<V>|null,
index: number): IterableChangeRecord_<V> {
record: IterableChangeRecord_<V>,
prevRecord: IterableChangeRecord_<V> | null,
index: number,
): IterableChangeRecord_<V> {
this._unlink(record);
this._insertAfter(record, prevRecord, index);
this._addToMoves(record, index);
@ -426,8 +455,10 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
/** @internal */
_addAfter(
record: IterableChangeRecord_<V>, prevRecord: IterableChangeRecord_<V>|null,
index: number): IterableChangeRecord_<V> {
record: IterableChangeRecord_<V>,
prevRecord: IterableChangeRecord_<V> | null,
index: number,
): IterableChangeRecord_<V> {
this._insertAfter(record, prevRecord, index);
if (this._additionsTail === null) {
@ -445,15 +476,17 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
/** @internal */
_insertAfter(
record: IterableChangeRecord_<V>, prevRecord: IterableChangeRecord_<V>|null,
index: number): IterableChangeRecord_<V> {
record: IterableChangeRecord_<V>,
prevRecord: IterableChangeRecord_<V> | null,
index: number,
): IterableChangeRecord_<V> {
// TODO(vicb):
// assert(record != prevRecord);
// assert(record._next === null);
// assert(record._prev === null);
const next: IterableChangeRecord_<V>|null =
prevRecord === null ? this._itHead : prevRecord._next;
const next: IterableChangeRecord_<V> | null =
prevRecord === null ? this._itHead : prevRecord._next;
// TODO(vicb):
// assert(next != record);
// assert(prevRecord != record);
@ -569,40 +602,42 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
}
export class IterableChangeRecord_<V> implements IterableChangeRecord<V> {
currentIndex: number|null = null;
previousIndex: number|null = null;
currentIndex: number | null = null;
previousIndex: number | null = null;
/** @internal */
_nextPrevious: IterableChangeRecord_<V>|null = null;
_nextPrevious: IterableChangeRecord_<V> | null = null;
/** @internal */
_prev: IterableChangeRecord_<V>|null = null;
_prev: IterableChangeRecord_<V> | null = null;
/** @internal */
_next: IterableChangeRecord_<V>|null = null;
_next: IterableChangeRecord_<V> | null = null;
/** @internal */
_prevDup: IterableChangeRecord_<V>|null = null;
_prevDup: IterableChangeRecord_<V> | null = null;
/** @internal */
_nextDup: IterableChangeRecord_<V>|null = null;
_nextDup: IterableChangeRecord_<V> | null = null;
/** @internal */
_prevRemoved: IterableChangeRecord_<V>|null = null;
_prevRemoved: IterableChangeRecord_<V> | null = null;
/** @internal */
_nextRemoved: IterableChangeRecord_<V>|null = null;
_nextRemoved: IterableChangeRecord_<V> | null = null;
/** @internal */
_nextAdded: IterableChangeRecord_<V>|null = null;
_nextAdded: IterableChangeRecord_<V> | null = null;
/** @internal */
_nextMoved: IterableChangeRecord_<V>|null = null;
_nextMoved: IterableChangeRecord_<V> | null = null;
/** @internal */
_nextIdentityChange: IterableChangeRecord_<V>|null = null;
_nextIdentityChange: IterableChangeRecord_<V> | null = null;
constructor(public item: V, public trackById: any) {}
constructor(
public item: V,
public trackById: any,
) {}
}
// A linked list of IterableChangeRecords with the same IterableChangeRecord_.item
class _DuplicateItemRecordList<V> {
/** @internal */
_head: IterableChangeRecord_<V>|null = null;
_head: IterableChangeRecord_<V> | null = null;
/** @internal */
_tail: IterableChangeRecord_<V>|null = null;
_tail: IterableChangeRecord_<V> | null = null;
/**
* Append the record to the list of duplicates.
@ -627,11 +662,13 @@ class _DuplicateItemRecordList<V> {
// Returns a IterableChangeRecord_ having IterableChangeRecord_.trackById == trackById and
// IterableChangeRecord_.currentIndex >= atOrAfterIndex
get(trackById: any, atOrAfterIndex: number|null): IterableChangeRecord_<V>|null {
let record: IterableChangeRecord_<V>|null;
get(trackById: any, atOrAfterIndex: number | null): IterableChangeRecord_<V> | null {
let record: IterableChangeRecord_<V> | null;
for (record = this._head; record !== null; record = record._nextDup) {
if ((atOrAfterIndex === null || atOrAfterIndex <= record.currentIndex!) &&
Object.is(record.trackById, trackById)) {
if (
(atOrAfterIndex === null || atOrAfterIndex <= record.currentIndex!) &&
Object.is(record.trackById, trackById)
) {
return record;
}
}
@ -653,8 +690,8 @@ class _DuplicateItemRecordList<V> {
// return false;
//});
const prev: IterableChangeRecord_<V>|null = record._prevDup;
const next: IterableChangeRecord_<V>|null = record._nextDup;
const prev: IterableChangeRecord_<V> | null = record._prevDup;
const next: IterableChangeRecord_<V> | null = record._nextDup;
if (prev === null) {
this._head = next;
} else {
@ -690,7 +727,7 @@ class _DuplicateMap<V> {
* Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a` then asking if we
* have any more `a`s needs to return the second `a`.
*/
get(trackById: any, atOrAfterIndex: number|null): IterableChangeRecord_<V>|null {
get(trackById: any, atOrAfterIndex: number | null): IterableChangeRecord_<V> | null {
const key = trackById;
const recordList = this.map.get(key);
return recordList ? recordList.get(trackById, atOrAfterIndex) : null;
@ -720,7 +757,11 @@ class _DuplicateMap<V> {
}
}
function getPreviousIndex(item: any, addRemoveOffset: number, moveOffsets: number[]|null): number {
function getPreviousIndex(
item: any,
addRemoveOffset: number,
moveOffsets: number[] | null,
): number {
const previousIndex = item.previousIndex;
if (previousIndex === null) return previousIndex;
let moveOffset = 0;

View file

@ -10,8 +10,12 @@ import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {isJsObject} from '../../util/iterable';
import {stringify} from '../../util/stringify';
import {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory} from './keyvalue_differs';
import {
KeyValueChangeRecord,
KeyValueChanges,
KeyValueDiffer,
KeyValueDifferFactory,
} from './keyvalue_differs';
export class DefaultKeyValueDifferFactory<K, V> implements KeyValueDifferFactory {
constructor() {}
@ -26,65 +30,66 @@ export class DefaultKeyValueDifferFactory<K, V> implements KeyValueDifferFactory
export class DefaultKeyValueDiffer<K, V> implements KeyValueDiffer<K, V>, KeyValueChanges<K, V> {
private _records = new Map<K, KeyValueChangeRecord_<K, V>>();
private _mapHead: KeyValueChangeRecord_<K, V>|null = null;
private _mapHead: KeyValueChangeRecord_<K, V> | null = null;
// _appendAfter is used in the check loop
private _appendAfter: KeyValueChangeRecord_<K, V>|null = null;
private _previousMapHead: KeyValueChangeRecord_<K, V>|null = null;
private _changesHead: KeyValueChangeRecord_<K, V>|null = null;
private _changesTail: KeyValueChangeRecord_<K, V>|null = null;
private _additionsHead: KeyValueChangeRecord_<K, V>|null = null;
private _additionsTail: KeyValueChangeRecord_<K, V>|null = null;
private _removalsHead: KeyValueChangeRecord_<K, V>|null = null;
private _removalsTail: KeyValueChangeRecord_<K, V>|null = null;
private _appendAfter: KeyValueChangeRecord_<K, V> | null = null;
private _previousMapHead: KeyValueChangeRecord_<K, V> | null = null;
private _changesHead: KeyValueChangeRecord_<K, V> | null = null;
private _changesTail: KeyValueChangeRecord_<K, V> | null = null;
private _additionsHead: KeyValueChangeRecord_<K, V> | null = null;
private _additionsTail: KeyValueChangeRecord_<K, V> | null = null;
private _removalsHead: KeyValueChangeRecord_<K, V> | null = null;
private _removalsTail: KeyValueChangeRecord_<K, V> | null = null;
get isDirty(): boolean {
return this._additionsHead !== null || this._changesHead !== null ||
this._removalsHead !== null;
return (
this._additionsHead !== null || this._changesHead !== null || this._removalsHead !== null
);
}
forEachItem(fn: (r: KeyValueChangeRecord<K, V>) => void) {
let record: KeyValueChangeRecord_<K, V>|null;
let record: KeyValueChangeRecord_<K, V> | null;
for (record = this._mapHead; record !== null; record = record._next) {
fn(record);
}
}
forEachPreviousItem(fn: (r: KeyValueChangeRecord<K, V>) => void) {
let record: KeyValueChangeRecord_<K, V>|null;
let record: KeyValueChangeRecord_<K, V> | null;
for (record = this._previousMapHead; record !== null; record = record._nextPrevious) {
fn(record);
}
}
forEachChangedItem(fn: (r: KeyValueChangeRecord<K, V>) => void) {
let record: KeyValueChangeRecord_<K, V>|null;
let record: KeyValueChangeRecord_<K, V> | null;
for (record = this._changesHead; record !== null; record = record._nextChanged) {
fn(record);
}
}
forEachAddedItem(fn: (r: KeyValueChangeRecord<K, V>) => void) {
let record: KeyValueChangeRecord_<K, V>|null;
let record: KeyValueChangeRecord_<K, V> | null;
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
fn(record);
}
}
forEachRemovedItem(fn: (r: KeyValueChangeRecord<K, V>) => void) {
let record: KeyValueChangeRecord_<K, V>|null;
let record: KeyValueChangeRecord_<K, V> | null;
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
fn(record);
}
}
diff(map?: Map<any, any>|{[k: string]: any}|null): any {
diff(map?: Map<any, any> | {[k: string]: any} | null): any {
if (!map) {
map = new Map();
} else if (!(map instanceof Map || isJsObject(map))) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_DIFFER_INPUT,
ngDevMode &&
`Error trying to diff '${stringify(map)}'. Only maps and objects are allowed`);
RuntimeErrorCode.INVALID_DIFFER_INPUT,
ngDevMode && `Error trying to diff '${stringify(map)}'. Only maps and objects are allowed`,
);
}
return this.check(map) ? this : null;
@ -96,7 +101,7 @@ export class DefaultKeyValueDiffer<K, V> implements KeyValueDiffer<K, V>, KeyVal
* Check the current state of the map vs the previous.
* The algorithm is optimised for when the keys do no change.
*/
check(map: Map<any, any>|{[k: string]: any}): boolean {
check(map: Map<any, any> | {[k: string]: any}): boolean {
this._reset();
let insertBefore = this._mapHead;
@ -121,8 +126,11 @@ export class DefaultKeyValueDiffer<K, V> implements KeyValueDiffer<K, V>, KeyVal
this._removalsHead = insertBefore;
for (let record: KeyValueChangeRecord_<K, V>|null = insertBefore; record !== null;
record = record._nextRemoved) {
for (
let record: KeyValueChangeRecord_<K, V> | null = insertBefore;
record !== null;
record = record._nextRemoved
) {
if (record === this._mapHead) {
this._mapHead = null;
}
@ -151,8 +159,9 @@ export class DefaultKeyValueDiffer<K, V> implements KeyValueDiffer<K, V>, KeyVal
* - The return value is the new value for the insertion pointer.
*/
private _insertBeforeOrAppend(
before: KeyValueChangeRecord_<K, V>|null,
record: KeyValueChangeRecord_<K, V>): KeyValueChangeRecord_<K, V>|null {
before: KeyValueChangeRecord_<K, V> | null,
record: KeyValueChangeRecord_<K, V>,
): KeyValueChangeRecord_<K, V> | null {
if (before) {
const prev = before._prev;
record._next = before;
@ -208,7 +217,7 @@ export class DefaultKeyValueDiffer<K, V> implements KeyValueDiffer<K, V>, KeyVal
/** @internal */
_reset() {
if (this.isDirty) {
let record: KeyValueChangeRecord_<K, V>|null;
let record: KeyValueChangeRecord_<K, V> | null;
// let `_previousMapHead` contain the state of the map before the changes
this._previousMapHead = this._mapHead;
for (record = this._previousMapHead; record !== null; record = record._next) {
@ -258,31 +267,31 @@ export class DefaultKeyValueDiffer<K, V> implements KeyValueDiffer<K, V>, KeyVal
}
/** @internal */
private _forEach<K, V>(obj: Map<K, V>|{[k: string]: V}, fn: (v: V, k: any) => void) {
private _forEach<K, V>(obj: Map<K, V> | {[k: string]: V}, fn: (v: V, k: any) => void) {
if (obj instanceof Map) {
obj.forEach(fn);
} else {
Object.keys(obj).forEach(k => fn(obj[k], k));
Object.keys(obj).forEach((k) => fn(obj[k], k));
}
}
}
class KeyValueChangeRecord_<K, V> implements KeyValueChangeRecord<K, V> {
previousValue: V|null = null;
currentValue: V|null = null;
previousValue: V | null = null;
currentValue: V | null = null;
/** @internal */
_nextPrevious: KeyValueChangeRecord_<K, V>|null = null;
_nextPrevious: KeyValueChangeRecord_<K, V> | null = null;
/** @internal */
_next: KeyValueChangeRecord_<K, V>|null = null;
_next: KeyValueChangeRecord_<K, V> | null = null;
/** @internal */
_prev: KeyValueChangeRecord_<K, V>|null = null;
_prev: KeyValueChangeRecord_<K, V> | null = null;
/** @internal */
_nextAdded: KeyValueChangeRecord_<K, V>|null = null;
_nextAdded: KeyValueChangeRecord_<K, V> | null = null;
/** @internal */
_nextRemoved: KeyValueChangeRecord_<K, V>|null = null;
_nextRemoved: KeyValueChangeRecord_<K, V> | null = null;
/** @internal */
_nextChanged: KeyValueChangeRecord_<K, V>|null = null;
_nextChanged: KeyValueChangeRecord_<K, V> | null = null;
constructor(public key: K) {}
}

View file

@ -12,14 +12,12 @@ import {Optional, SkipSelf} from '../../di/metadata';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {DefaultIterableDifferFactory} from '../differs/default_iterable_differ';
/**
* A type describing supported iterable types.
*
* @publicApi
*/
export type NgIterable<T> = Array<T>|Iterable<T>;
export type NgIterable<T> = Array<T> | Iterable<T>;
/**
* A strategy for tracking changes over time to an iterable. Used by {@link NgForOf} to
@ -35,7 +33,7 @@ export interface IterableDiffer<V> {
* @returns an object describing the difference. The return value is only valid until the next
* `diff()` invocation.
*/
diff(object: NgIterable<V>|undefined|null): IterableChanges<V>|null;
diff(object: NgIterable<V> | undefined | null): IterableChanges<V> | null;
}
/**
@ -68,9 +66,12 @@ export interface IterableChanges<V> {
* of the item, after applying the operations up to this point.
*/
forEachOperation(
fn:
(record: IterableChangeRecord<V>, previousIndex: number|null,
currentIndex: number|null) => void): void;
fn: (
record: IterableChangeRecord<V>,
previousIndex: number | null,
currentIndex: number | null,
) => void,
): void;
/**
* Iterate over changes in the order of original `Iterable` showing where the original items
@ -101,10 +102,10 @@ export interface IterableChanges<V> {
*/
export interface IterableChangeRecord<V> {
/** Current index of the item in `Iterable` or null if removed. */
readonly currentIndex: number|null;
readonly currentIndex: number | null;
/** Previous index of the item in `Iterable` or null if added. */
readonly previousIndex: number|null;
readonly previousIndex: number | null;
/** The item. */
readonly item: V;
@ -168,7 +169,7 @@ export interface TrackByFunction<T> {
* @param index The index of the item within the iterable.
* @param item The item in the iterable.
*/
<U extends T>(index: number, item: T&U): any;
<U extends T>(index: number, item: T & U): any;
}
/**
@ -192,8 +193,11 @@ export function defaultIterableDiffersFactory() {
*/
export class IterableDiffers {
/** @nocollapse */
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable(
{token: IterableDiffers, providedIn: 'root', factory: defaultIterableDiffersFactory});
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
token: IterableDiffers,
providedIn: 'root',
factory: defaultIterableDiffersFactory,
});
constructor(private factories: IterableDifferFactory[]) {}
@ -229,27 +233,29 @@ export class IterableDiffers {
static extend(factories: IterableDifferFactory[]): StaticProvider {
return {
provide: IterableDiffers,
useFactory: (parent: IterableDiffers|null) => {
useFactory: (parent: IterableDiffers | null) => {
// if parent is null, it means that we are in the root injector and we have just overridden
// the default injection mechanism for IterableDiffers, in such a case just assume
// `defaultIterableDiffersFactory`.
return IterableDiffers.create(factories, parent || defaultIterableDiffersFactory());
},
// Dependency technically isn't optional, but we can provide a better error message this way.
deps: [[IterableDiffers, new SkipSelf(), new Optional()]]
deps: [[IterableDiffers, new SkipSelf(), new Optional()]],
};
}
find(iterable: any): IterableDifferFactory {
const factory = this.factories.find(f => f.supports(iterable));
const factory = this.factories.find((f) => f.supports(iterable));
if (factory != null) {
return factory;
} else {
throw new RuntimeError(
RuntimeErrorCode.NO_SUPPORTING_DIFFER_FACTORY,
ngDevMode &&
`Cannot find a differ supporting object '${iterable}' of type '${
getTypeNameForDebugging(iterable)}'`);
RuntimeErrorCode.NO_SUPPORTING_DIFFER_FACTORY,
ngDevMode &&
`Cannot find a differ supporting object '${iterable}' of type '${getTypeNameForDebugging(
iterable,
)}'`,
);
}
}
}

View file

@ -11,7 +11,6 @@ import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {DefaultKeyValueDifferFactory} from './default_keyvalue_differ';
/**
* A differ that tracks changes made to an object over time.
*
@ -25,7 +24,7 @@ export interface KeyValueDiffer<K, V> {
* @returns an object describing the difference. The return value is only valid until the next
* `diff()` invocation.
*/
diff(object: Map<K, V>): KeyValueChanges<K, V>|null;
diff(object: Map<K, V>): KeyValueChanges<K, V> | null;
/**
* Compute a difference between the previous state and the new `object` state.
@ -34,7 +33,7 @@ export interface KeyValueDiffer<K, V> {
* @returns an object describing the difference. The return value is only valid until the next
* `diff()` invocation.
*/
diff(object: {[key: string]: V}): KeyValueChanges<string, V>|null;
diff(object: {[key: string]: V}): KeyValueChanges<string, V> | null;
// TODO(TS2.1): diff<KP extends string>(this: KeyValueDiffer<KP, V>, object: Record<KP, V>):
// KeyValueDiffer<KP, V>;
}
@ -88,12 +87,12 @@ export interface KeyValueChangeRecord<K, V> {
/**
* Current value for the key or `null` if removed.
*/
readonly currentValue: V|null;
readonly currentValue: V | null;
/**
* Previous value for the key or `null` if added.
*/
readonly previousValue: V|null;
readonly previousValue: V | null;
}
/**
@ -124,8 +123,11 @@ export function defaultKeyValueDiffersFactory() {
*/
export class KeyValueDiffers {
/** @nocollapse */
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable(
{token: KeyValueDiffers, providedIn: 'root', factory: defaultKeyValueDiffersFactory});
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
token: KeyValueDiffers,
providedIn: 'root',
factory: defaultKeyValueDiffersFactory,
});
/**
* @deprecated v4.0.0 - Should be private.
@ -174,17 +176,18 @@ export class KeyValueDiffers {
return KeyValueDiffers.create(factories, parent || defaultKeyValueDiffersFactory());
},
// Dependency technically isn't optional, but we can provide a better error message this way.
deps: [[KeyValueDiffers, new SkipSelf(), new Optional()]]
deps: [[KeyValueDiffers, new SkipSelf(), new Optional()]],
};
}
find(kv: any): KeyValueDifferFactory {
const factory = this.factories.find(f => f.supports(kv));
const factory = this.factories.find((f) => f.supports(kv));
if (factory) {
return factory;
}
throw new RuntimeError(
RuntimeErrorCode.NO_SUPPORTING_DIFFER_FACTORY,
ngDevMode && `Cannot find a differ supporting object '${kv}'`);
RuntimeErrorCode.NO_SUPPORTING_DIFFER_FACTORY,
ngDevMode && `Cannot find a differ supporting object '${kv}'`,
);
}
}

View file

@ -9,7 +9,15 @@
import {Subscription} from 'rxjs';
import {ApplicationRef} from '../../application/application_ref';
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, Injectable, InjectionToken, makeEnvironmentProviders, StaticProvider} from '../../di';
import {
ENVIRONMENT_INITIALIZER,
EnvironmentProviders,
inject,
Injectable,
InjectionToken,
makeEnvironmentProviders,
StaticProvider,
} from '../../di';
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {PendingTasks} from '../../pending_tasks';
@ -45,7 +53,7 @@ export class NgZoneChangeDetectionScheduler {
this.zone.run(() => {
this.applicationRef.tick();
});
}
},
});
}
@ -54,31 +62,39 @@ export class NgZoneChangeDetectionScheduler {
}
}
/**
* Internal token used to verify that `provideZoneChangeDetection` is not used
* with the bootstrapModule API.
*/
export const PROVIDED_NG_ZONE = new InjectionToken<boolean>(
(typeof ngDevMode === 'undefined' || ngDevMode) ? 'provideZoneChangeDetection token' : '');
typeof ngDevMode === 'undefined' || ngDevMode ? 'provideZoneChangeDetection token' : '',
);
export function internalProvideZoneChangeDetection(
{ngZoneFactory, ignoreChangesOutsideZone}:
{ngZoneFactory: () => NgZone, ignoreChangesOutsideZone?: boolean}): StaticProvider[] {
export function internalProvideZoneChangeDetection({
ngZoneFactory,
ignoreChangesOutsideZone,
}: {
ngZoneFactory: () => NgZone;
ignoreChangesOutsideZone?: boolean;
}): StaticProvider[] {
return [
{provide: NgZone, useFactory: ngZoneFactory},
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useFactory: () => {
const ngZoneChangeDetectionScheduler =
inject(NgZoneChangeDetectionScheduler, {optional: true});
if ((typeof ngDevMode === 'undefined' || ngDevMode) &&
ngZoneChangeDetectionScheduler === null) {
const ngZoneChangeDetectionScheduler = inject(NgZoneChangeDetectionScheduler, {
optional: true,
});
if (
(typeof ngDevMode === 'undefined' || ngDevMode) &&
ngZoneChangeDetectionScheduler === null
) {
throw new RuntimeError(
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
`A required Injectable was not found in the dependency injection tree. ` +
'If you are bootstrapping an NgModule, make sure that the `BrowserModule` is imported.');
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
`A required Injectable was not found in the dependency injection tree. ` +
'If you are bootstrapping an NgModule, make sure that the `BrowserModule` is imported.',
);
}
return () => ngZoneChangeDetectionScheduler!.initialize();
},
@ -91,7 +107,7 @@ export function internalProvideZoneChangeDetection(
return () => {
service.initialize();
};
}
},
},
{provide: INTERNAL_APPLICATION_ERROR_HANDLER, useFactory: ngZoneApplicationErrorHandlerFactory},
// Always disable scheduler whenever explicitly disabled, even if another place called
@ -99,9 +115,9 @@ export function internalProvideZoneChangeDetection(
ignoreChangesOutsideZone === true ? {provide: ZONELESS_SCHEDULER_DISABLED, useValue: true} : [],
// TODO(atscott): This should move to the same places that zone change detection is provided by
// default instead of being in the zone scheduling providers.
alwaysProvideZonelessScheduler || ignoreChangesOutsideZone === false ?
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl} :
[],
alwaysProvideZonelessScheduler || ignoreChangesOutsideZone === false
? {provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl}
: [],
];
}
@ -141,11 +157,12 @@ export function provideZoneChangeDetection(options?: NgZoneOptions): Environment
}
return new NgZone(ngZoneOptions);
},
ignoreChangesOutsideZone
ignoreChangesOutsideZone,
});
return makeEnvironmentProviders([
(typeof ngDevMode === 'undefined' || ngDevMode) ? {provide: PROVIDED_NG_ZONE, useValue: true} :
[],
typeof ngDevMode === 'undefined' || ngDevMode
? {provide: PROVIDED_NG_ZONE, useValue: true}
: [],
zoneProviders,
]);
}
@ -240,33 +257,40 @@ export class ZoneStablePendingTask {
}
this.initialized = true;
let task: number|null = null;
let task: number | null = null;
if (!this.zone.isStable && !this.zone.hasPendingMacrotasks && !this.zone.hasPendingMicrotasks) {
task = this.pendingTasks.add();
}
this.zone.runOutsideAngular(() => {
this.subscription.add(this.zone.onStable.subscribe(() => {
NgZone.assertNotInAngularZone();
this.subscription.add(
this.zone.onStable.subscribe(() => {
NgZone.assertNotInAngularZone();
// Check whether there are no pending macro/micro tasks in the next tick
// to allow for NgZone to update the state.
queueMicrotask(() => {
if (task !== null && !this.zone.hasPendingMacrotasks && !this.zone.hasPendingMicrotasks) {
this.pendingTasks.remove(task);
task = null;
}
});
}));
// Check whether there are no pending macro/micro tasks in the next tick
// to allow for NgZone to update the state.
queueMicrotask(() => {
if (
task !== null &&
!this.zone.hasPendingMacrotasks &&
!this.zone.hasPendingMicrotasks
) {
this.pendingTasks.remove(task);
task = null;
}
});
}),
);
});
this.subscription.add(this.zone.onUnstable.subscribe(() => {
NgZone.assertInAngularZone();
task ??= this.pendingTasks.add();
}));
this.subscription.add(
this.zone.onUnstable.subscribe(() => {
NgZone.assertInAngularZone();
task ??= this.pendingTasks.add();
}),
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}

View file

@ -8,8 +8,6 @@
import {InjectionToken} from '../../di/injection_token';
export const enum NotificationSource {
// Change detection needs to run in order to synchronize application state
// with the DOM when the following notifications are received:
@ -60,8 +58,10 @@ export abstract class ChangeDetectionScheduler {
/** Token used to indicate if zoneless was enabled via provideZonelessChangeDetection(). */
export const ZONELESS_ENABLED = new InjectionToken<boolean>(
typeof ngDevMode === 'undefined' || ngDevMode ? 'Zoneless enabled' : '',
{providedIn: 'root', factory: () => false});
typeof ngDevMode === 'undefined' || ngDevMode ? 'Zoneless enabled' : '',
{providedIn: 'root', factory: () => false},
);
export const ZONELESS_SCHEDULER_DISABLED = new InjectionToken<boolean>(
typeof ngDevMode === 'undefined' || ngDevMode ? 'scheduler disabled' : '');
typeof ngDevMode === 'undefined' || ngDevMode ? 'scheduler disabled' : '',
);

View file

@ -15,11 +15,19 @@ import {EnvironmentProviders} from '../../di/interface/provider';
import {makeEnvironmentProviders} from '../../di/provider_collection';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {PendingTasks} from '../../pending_tasks';
import {scheduleCallbackWithMicrotask, scheduleCallbackWithRafRace} from '../../util/callback_scheduler';
import {
scheduleCallbackWithMicrotask,
scheduleCallbackWithRafRace,
} from '../../util/callback_scheduler';
import {performanceMarkFeature} from '../../util/performance';
import {NgZone, NoopNgZone} from '../../zone/ng_zone';
import {ChangeDetectionScheduler, NotificationSource, ZONELESS_ENABLED, ZONELESS_SCHEDULER_DISABLED} from './zoneless_scheduling';
import {
ChangeDetectionScheduler,
NotificationSource,
ZONELESS_ENABLED,
ZONELESS_SCHEDULER_DISABLED,
} from './zoneless_scheduling';
const CONSECUTIVE_MICROTASK_NOTIFICATION_LIMIT = 100;
let consecutiveMicrotaskNotifications = 0;
@ -36,10 +44,11 @@ function trackMicrotaskNotificationForDebugging() {
if (consecutiveMicrotaskNotifications === CONSECUTIVE_MICROTASK_NOTIFICATION_LIMIT) {
throw new RuntimeError(
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
'Angular could not stabilize because there were endless change notifications within the browser event loop. ' +
'The stack from the last several notifications: \n' +
stackFromLastFewNotifications.join('\n'));
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
'Angular could not stabilize because there were endless change notifications within the browser event loop. ' +
'The stack from the last several notifications: \n' +
stackFromLastFewNotifications.join('\n'),
);
}
}
@ -50,42 +59,47 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
private readonly ngZone = inject(NgZone);
private readonly zonelessEnabled = inject(ZONELESS_ENABLED);
private readonly disableScheduling =
inject(ZONELESS_SCHEDULER_DISABLED, {optional: true}) ?? false;
inject(ZONELESS_SCHEDULER_DISABLED, {optional: true}) ?? false;
private readonly zoneIsDefined = typeof Zone !== 'undefined' && !!Zone.root.run;
private readonly schedulerTickApplyArgs = [{data: {'__scheduler_tick__': true}}];
private readonly subscriptions = new Subscription();
private cancelScheduledCallback: null|(() => void) = null;
private cancelScheduledCallback: null | (() => void) = null;
private shouldRefreshViews = false;
private pendingRenderTaskId: number|null = null;
private pendingRenderTaskId: number | null = null;
private useMicrotaskScheduler = false;
runningTick = false;
constructor() {
this.subscriptions.add(this.appRef.afterTick.subscribe(() => {
// If the scheduler isn't running a tick but the application ticked, that means
// someone called ApplicationRef.tick manually. In this case, we should cancel
// any change detections that had been scheduled so we don't run an extra one.
if (!this.runningTick) {
this.cleanup();
}
}));
this.subscriptions.add(this.ngZone.onUnstable.subscribe(() => {
// If the zone becomes unstable when we're not running tick (this happens from the zone.run),
// we should cancel any scheduled change detection here because at this point we
// know that the zone will stabilize at some point and run change detection itself.
if (!this.runningTick) {
this.cleanup();
}
}));
this.subscriptions.add(
this.appRef.afterTick.subscribe(() => {
// If the scheduler isn't running a tick but the application ticked, that means
// someone called ApplicationRef.tick manually. In this case, we should cancel
// any change detections that had been scheduled so we don't run an extra one.
if (!this.runningTick) {
this.cleanup();
}
}),
);
this.subscriptions.add(
this.ngZone.onUnstable.subscribe(() => {
// If the zone becomes unstable when we're not running tick (this happens from the zone.run),
// we should cancel any scheduled change detection here because at this point we
// know that the zone will stabilize at some point and run change detection itself.
if (!this.runningTick) {
this.cleanup();
}
}),
);
// TODO(atscott): These conditions will need to change when zoneless is the default
// Instead, they should flip to checking if ZoneJS scheduling is provided
this.disableScheduling ||= !this.zonelessEnabled &&
// NoopNgZone without enabling zoneless means no scheduling whatsoever
(this.ngZone instanceof NoopNgZone ||
// The same goes for the lack of Zone without enabling zoneless scheduling
!this.zoneIsDefined);
this.disableScheduling ||=
!this.zonelessEnabled &&
// NoopNgZone without enabling zoneless means no scheduling whatsoever
(this.ngZone instanceof NoopNgZone ||
// The same goes for the lack of Zone without enabling zoneless scheduling
!this.zoneIsDefined);
}
notify(source: NotificationSource): void {
@ -126,7 +140,7 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
return;
}
if ((typeof ngDevMode === 'undefined' || ngDevMode)) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (this.useMicrotaskScheduler) {
trackMicrotaskNotificationForDebugging();
} else {
@ -135,8 +149,9 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
}
}
const scheduleCallback =
this.useMicrotaskScheduler ? scheduleCallbackWithMicrotask : scheduleCallbackWithRafRace;
const scheduleCallback = this.useMicrotaskScheduler
? scheduleCallbackWithMicrotask
: scheduleCallbackWithRafRace;
this.pendingRenderTaskId = this.taskService.add();
if (this.zoneIsDefined) {
Zone.root.run(() => {
@ -187,10 +202,14 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
const task = this.taskService.add();
try {
this.ngZone.run(() => {
this.runningTick = true;
this.appRef._tick(shouldRefreshViews);
}, undefined, this.schedulerTickApplyArgs);
this.ngZone.run(
() => {
this.runningTick = true;
this.appRef._tick(shouldRefreshViews);
},
undefined,
this.schedulerTickApplyArgs,
);
} catch (e: unknown) {
this.taskService.remove(task);
throw e;
@ -234,7 +253,6 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
}
}
/**
* Provides change detection without ZoneJS for the application bootstrapped using
* `bootstrapApplication`.

View file

@ -16,7 +16,7 @@ export const enum JitCompilerUsage {
interface JitCompilerUsageRequest {
usage: JitCompilerUsage;
kind: 'directive'|'component'|'pipe'|'injectable'|'NgModule';
kind: 'directive' | 'component' | 'pipe' | 'injectable' | 'NgModule';
type: Type;
}
@ -31,24 +31,17 @@ export function getCompilerFacade(request: JitCompilerUsageRequest): CompilerFac
// console.
console.error(`JIT compilation failed for ${request.kind}`, request.type);
let message = `The ${request.kind} '${
request
.type.name}' needs to be compiled using the JIT compiler, but '@angular/compiler' is not available.\n\n`;
let message = `The ${request.kind} '${request.type.name}' needs to be compiled using the JIT compiler, but '@angular/compiler' is not available.\n\n`;
if (request.usage === JitCompilerUsage.PartialDeclaration) {
message += `The ${request.kind} is part of a library that has been partially compiled.\n`;
message +=
`However, the Angular Linker has not processed the library such that JIT compilation is used as fallback.\n`;
message += `However, the Angular Linker has not processed the library such that JIT compilation is used as fallback.\n`;
message += '\n';
message +=
`Ideally, the library is processed using the Angular Linker to become fully AOT compiled.\n`;
message += `Ideally, the library is processed using the Angular Linker to become fully AOT compiled.\n`;
} else {
message +=
`JIT compilation is discouraged for production use-cases! Consider using AOT mode instead.\n`;
message += `JIT compilation is discouraged for production use-cases! Consider using AOT mode instead.\n`;
}
message +=
`Alternatively, the JIT compiler should be loaded by bootstrapping using '@angular/platform-browser-dynamic' or '@angular/platform-server',\n`;
message +=
`or manually provide the compiler with 'import "@angular/compiler";' before bootstrapping.`;
message += `Alternatively, the JIT compiler should be loaded by bootstrapping using '@angular/platform-browser-dynamic' or '@angular/platform-server',\n`;
message += `or manually provide the compiler with 'import "@angular/compiler";' before bootstrapping.`;
throw new Error(message);
} else {
throw new Error('JIT compiler unavailable');

View file

@ -26,45 +26,83 @@ export interface ExportedCompilerFacade {
}
export interface CompilerFacade {
compilePipe(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3PipeMetadataFacade):
any;
compilePipe(
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
meta: R3PipeMetadataFacade,
): any;
compilePipeDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, declaration: R3DeclarePipeFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
declaration: R3DeclarePipeFacade,
): any;
compileInjectable(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3InjectableMetadataFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
meta: R3InjectableMetadataFacade,
): any;
compileInjectableDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3DeclareInjectableFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
meta: R3DeclareInjectableFacade,
): any;
compileInjector(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3InjectorMetadataFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
meta: R3InjectorMetadataFacade,
): any;
compileInjectorDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
declaration: R3DeclareInjectorFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
declaration: R3DeclareInjectorFacade,
): any;
compileNgModule(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3NgModuleMetadataFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
meta: R3NgModuleMetadataFacade,
): any;
compileNgModuleDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
declaration: R3DeclareNgModuleFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
declaration: R3DeclareNgModuleFacade,
): any;
compileDirective(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3DirectiveMetadataFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
meta: R3DirectiveMetadataFacade,
): any;
compileDirectiveDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
declaration: R3DeclareDirectiveFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
declaration: R3DeclareDirectiveFacade,
): any;
compileComponent(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3ComponentMetadataFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
meta: R3ComponentMetadataFacade,
): any;
compileComponentDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
declaration: R3DeclareComponentFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
declaration: R3DeclareComponentFacade,
): any;
compileFactory(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3FactoryDefMetadataFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
meta: R3FactoryDefMetadataFacade,
): any;
compileFactoryDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3DeclareFactoryFacade): any;
angularCoreEnv: CoreEnvironment,
sourceMapUrl: string,
meta: R3DeclareFactoryFacade,
): any;
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
FactoryTarget: typeof FactoryTarget;
// Note that we do not use `{new(): ResourceLoader}` here because
// the resource loader class is abstract and not constructable.
ResourceLoader: Function&{prototype: ResourceLoader};
ResourceLoader: Function & {prototype: ResourceLoader};
}
export interface CoreEnvironment {
@ -72,7 +110,7 @@ export interface CoreEnvironment {
}
export type ResourceLoader = {
get(url: string): Promise<string>|string;
get(url: string): Promise<string> | string;
};
export type Provider = unknown;
@ -89,7 +127,7 @@ export enum FactoryTarget {
export interface R3DependencyMetadataFacade {
token: OpaqueValue;
attribute: string|null;
attribute: string | null;
host: boolean;
optional: boolean;
self: boolean;
@ -117,7 +155,7 @@ export interface R3InjectableMetadataFacade {
name: string;
type: Type;
typeArgumentCount: number;
providedIn?: Type|'root'|'platform'|'any'|null;
providedIn?: Type | 'root' | 'platform' | 'any' | null;
useClass?: OpaqueValue;
useFactory?: OpaqueValue;
useExisting?: OpaqueValue;
@ -131,8 +169,8 @@ export interface R3NgModuleMetadataFacade {
declarations: Function[];
imports: Function[];
exports: Function[];
schemas: {name: string}[]|null;
id: string|null;
schemas: {name: string}[] | null;
id: string | null;
}
export interface R3InjectorMetadataFacade {
@ -152,30 +190,30 @@ export interface R3DirectiveMetadataFacade {
name: string;
type: Type;
typeSourceSpan: ParseSourceSpan;
selector: string|null;
selector: string | null;
queries: R3QueryMetadataFacade[];
host: {[key: string]: string};
propMetadata: {[key: string]: OpaqueValue[]};
lifecycle: {usesOnChanges: boolean;};
inputs: (string|{name: string, alias?: string, required?: boolean})[];
lifecycle: {usesOnChanges: boolean};
inputs: (string | {name: string; alias?: string; required?: boolean})[];
outputs: string[];
usesInheritance: boolean;
exportAs: string[]|null;
providers: Provider[]|null;
exportAs: string[] | null;
providers: Provider[] | null;
viewQueries: R3QueryMetadataFacade[];
isStandalone: boolean;
isSignal: boolean;
hostDirectives: R3HostDirectiveMetadataFacade[]|null;
hostDirectives: R3HostDirectiveMetadataFacade[] | null;
}
export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade {
template: string;
preserveWhitespaces: boolean;
animations: OpaqueValue[]|undefined;
animations: OpaqueValue[] | undefined;
declarations: R3TemplateDependencyFacade[];
styles: string[];
encapsulation: ViewEncapsulation;
viewProviders: Provider[]|null;
viewProviders: Provider[] | null;
interpolation?: [string, string];
changeDetection?: ChangeDetectionStrategy;
}
@ -183,19 +221,22 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade {
// TODO(legacy-partial-output-inputs): Remove in v18.
// https://github.com/angular/angular/blob/d4b423690210872b5c32a322a6090beda30b05a3/packages/core/src/compiler/compiler_facade_interface.ts#L197-L199
export type LegacyInputPartialMapping =
string|[bindingPropertyName: string, classPropertyName: string, transformFunction?: Function];
| string
| [bindingPropertyName: string, classPropertyName: string, transformFunction?: Function];
export interface R3DeclareDirectiveFacade {
selector?: string;
type: Type;
inputs?: {
[fieldName: string]: {
classPropertyName: string,
publicName: string,
isSignal: boolean,
isRequired: boolean,
transformFunction: Function|null,
}|LegacyInputPartialMapping;
[fieldName: string]:
| {
classPropertyName: string;
publicName: string;
isSignal: boolean;
isRequired: boolean;
transformFunction: Function | null;
}
| LegacyInputPartialMapping;
};
outputs?: {[classPropertyName: string]: string};
host?: {
@ -212,7 +253,7 @@ export interface R3DeclareDirectiveFacade {
usesInheritance?: boolean;
usesOnChanges?: boolean;
isStandalone?: boolean;
hostDirectives?: R3HostDirectiveMetadataFacade[]|null;
hostDirectives?: R3HostDirectiveMetadataFacade[] | null;
isSignal?: boolean;
}
@ -227,10 +268,9 @@ export interface R3DeclareComponentFacade extends R3DeclareDirectiveFacade {
// Pre-standalone libraries have separate component/directive/pipe fields:
components?: R3DeclareDirectiveDependencyFacade[];
directives?: R3DeclareDirectiveDependencyFacade[];
pipes?: {[pipeName: string]: OpaqueValue|(() => OpaqueValue)};
pipes?: {[pipeName: string]: OpaqueValue | (() => OpaqueValue)};
deferBlockDependencies?: (() => Promise<Type>| null)[];
deferBlockDependencies?: (() => Promise<Type> | null)[];
viewProviders?: OpaqueValue;
animations?: OpaqueValue;
changeDetection?: ChangeDetectionStrategy;
@ -240,14 +280,17 @@ export interface R3DeclareComponentFacade extends R3DeclareDirectiveFacade {
}
export type R3DeclareTemplateDependencyFacade = {
kind: string
}&(R3DeclareDirectiveDependencyFacade|R3DeclarePipeDependencyFacade|
R3DeclareNgModuleDependencyFacade);
kind: string;
} & (
| R3DeclareDirectiveDependencyFacade
| R3DeclarePipeDependencyFacade
| R3DeclareNgModuleDependencyFacade
);
export interface R3DeclareDirectiveDependencyFacade {
kind?: 'directive'|'component';
kind?: 'directive' | 'component';
selector: string;
type: OpaqueValue|(() => OpaqueValue);
type: OpaqueValue | (() => OpaqueValue);
inputs?: string[];
outputs?: string[];
exportAs?: string[];
@ -256,12 +299,12 @@ export interface R3DeclareDirectiveDependencyFacade {
export interface R3DeclarePipeDependencyFacade {
kind?: 'pipe';
name: string;
type: OpaqueValue|(() => OpaqueValue);
type: OpaqueValue | (() => OpaqueValue);
}
export interface R3DeclareNgModuleDependencyFacade {
kind: 'ngmodule';
type: OpaqueValue|(() => OpaqueValue);
type: OpaqueValue | (() => OpaqueValue);
}
export enum R3TemplateDependencyKind {
@ -272,25 +315,25 @@ export enum R3TemplateDependencyKind {
export interface R3TemplateDependencyFacade {
kind: R3TemplateDependencyKind;
type: OpaqueValue|(() => OpaqueValue);
type: OpaqueValue | (() => OpaqueValue);
}
export interface R3FactoryDefMetadataFacade {
name: string;
type: Type;
typeArgumentCount: number;
deps: R3DependencyMetadataFacade[]|null;
deps: R3DependencyMetadataFacade[] | null;
target: FactoryTarget;
}
export interface R3DeclareFactoryFacade {
type: Type;
deps: R3DeclareDependencyMetadataFacade[]|'invalid'|null;
deps: R3DeclareDependencyMetadataFacade[] | 'invalid' | null;
target: FactoryTarget;
}
export interface R3DeclareInjectableFacade {
type: Type;
providedIn?: Type|'root'|'platform'|'any'|null;
providedIn?: Type | 'root' | 'platform' | 'any' | null;
useClass?: OpaqueValue;
useFactory?: OpaqueValue;
useExisting?: OpaqueValue;
@ -302,7 +345,7 @@ export enum ViewEncapsulation {
Emulated = 0,
// Historically the 1 value was for `Native` encapsulation which has been removed as of v11.
None = 2,
ShadowDom = 3
ShadowDom = 3,
}
export type ChangeDetectionStrategy = number;
@ -310,10 +353,10 @@ export type ChangeDetectionStrategy = number;
export interface R3QueryMetadataFacade {
propertyName: string;
first: boolean;
predicate: OpaqueValue|string[];
predicate: OpaqueValue | string[];
descendants: boolean;
emitDistinctChangesOnly: boolean;
read: OpaqueValue|null;
read: OpaqueValue | null;
static: boolean;
isSignal: boolean;
}
@ -321,7 +364,7 @@ export interface R3QueryMetadataFacade {
export interface R3DeclareQueryMetadataFacade {
propertyName: string;
first?: boolean;
predicate: OpaqueValue|string[];
predicate: OpaqueValue | string[];
descendants?: boolean;
read?: OpaqueValue;
static?: boolean;
@ -337,10 +380,10 @@ export interface R3DeclareInjectorFacade {
export interface R3DeclareNgModuleFacade {
type: Type;
bootstrap?: OpaqueValue[]|(() => OpaqueValue[]);
declarations?: OpaqueValue[]|(() => OpaqueValue[]);
imports?: OpaqueValue[]|(() => OpaqueValue[]);
exports?: OpaqueValue[]|(() => OpaqueValue[]);
bootstrap?: OpaqueValue[] | (() => OpaqueValue[]);
declarations?: OpaqueValue[] | (() => OpaqueValue[]);
imports?: OpaqueValue[] | (() => OpaqueValue[]);
exports?: OpaqueValue[] | (() => OpaqueValue[]);
schemas?: OpaqueValue[];
id?: OpaqueValue;
}

View file

@ -24,24 +24,63 @@ export * from './metadata';
export * from './version';
export {TypeDecorator} from './util/decorators';
export * from './di';
export {BootstrapOptions, ApplicationRef, NgProbeToken, APP_BOOTSTRAP_LISTENER} from './application/application_ref';
export {
BootstrapOptions,
ApplicationRef,
NgProbeToken,
APP_BOOTSTRAP_LISTENER,
} from './application/application_ref';
export {PlatformRef} from './platform/platform_ref';
export {createPlatform, createPlatformFactory, assertPlatform, destroyPlatform, getPlatform} from './platform/platform';
export {provideZoneChangeDetection, NgZoneOptions} from './change_detection/scheduling/ng_zone_scheduling';
export {
createPlatform,
createPlatformFactory,
assertPlatform,
destroyPlatform,
getPlatform,
} from './platform/platform';
export {
provideZoneChangeDetection,
NgZoneOptions,
} from './change_detection/scheduling/ng_zone_scheduling';
export {provideExperimentalZonelessChangeDetection} from './change_detection/scheduling/zoneless_scheduling_impl';
export {ExperimentalPendingTasks} from './pending_tasks';
export {enableProdMode, isDevMode} from './util/is_dev_mode';
export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, PLATFORM_ID, ANIMATION_MODULE_TYPE, CSP_NONCE} from './application/application_tokens';
export {
APP_ID,
PACKAGE_ROOT_URL,
PLATFORM_INITIALIZER,
PLATFORM_ID,
ANIMATION_MODULE_TYPE,
CSP_NONCE,
} from './application/application_tokens';
export {APP_INITIALIZER, ApplicationInitStatus} from './application/application_init';
export * from './zone';
export * from './render';
export * from './linker';
export * from './linker/ng_module_factory_loader_impl';
export {DebugElement, DebugEventListener, DebugNode, asNativeElements, getDebugNode, Predicate} from './debug/debug_node';
export {GetTestability, Testability, TestabilityRegistry, setTestabilityGetter} from './testability/testability';
export {
DebugElement,
DebugEventListener,
DebugNode,
asNativeElements,
getDebugNode,
Predicate,
} from './debug/debug_node';
export {
GetTestability,
Testability,
TestabilityRegistry,
setTestabilityGetter,
} from './testability/testability';
export * from './change_detection';
export * from './platform/platform_core_providers';
export {TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID, DEFAULT_CURRENCY_CODE, MissingTranslationStrategy} from './i18n/tokens';
export {
TRANSLATIONS,
TRANSLATIONS_FORMAT,
LOCALE_ID,
DEFAULT_CURRENCY_CODE,
MissingTranslationStrategy,
} from './i18n/tokens';
export {ApplicationModule} from './application/application_module';
export {AbstractType, Type} from './interface/type';
export {EventEmitter} from './event_emitter';
@ -51,10 +90,20 @@ export * from './core_render3_private_export';
export * from './core_reactivity_export';
export {SecurityContext} from './sanitization/security';
export {Sanitizer} from './sanitization/sanitizer';
export {createNgModule, createNgModuleRef, createEnvironmentInjector} from './render3/ng_module_ref';
export {
createNgModule,
createNgModuleRef,
createEnvironmentInjector,
} from './render3/ng_module_ref';
export {createComponent, reflectComponentType, ComponentMirror} from './render3/component';
export {isStandalone} from './render3/definition';
export {AfterRenderRef, AfterRenderOptions, AfterRenderPhase, afterRender, afterNextRender} from './render3/after_render_hooks';
export {
AfterRenderRef,
AfterRenderOptions,
AfterRenderPhase,
afterRender,
afterNextRender,
} from './render3/after_render_hooks';
export {ApplicationConfig, mergeApplicationConfig} from './application/application_config';
export {makeStateKey, StateKey, TransferState} from './transfer_state';
export {booleanAttribute, numberAttribute} from './util/coercion';
@ -64,12 +113,13 @@ if (typeof ngDevMode !== 'undefined' && ngDevMode) {
// This helper is to give a reasonable error message to people upgrading to v9 that have not yet
// installed `@angular/localize` in their app.
// tslint:disable-next-line: no-toplevel-property-access
global.$localize ??= function() {
global.$localize ??= function () {
throw new Error(
'It looks like your application or one of its dependencies is using i18n.\n' +
'It looks like your application or one of its dependencies is using i18n.\n' +
'Angular 9 introduced a global `$localize()` function that needs to be loaded.\n' +
'Please run `ng add @angular/localize` from the Angular CLI.\n' +
'(For non-CLI projects, add `import \'@angular/localize/init\';` to your `polyfills.ts` file.\n' +
'For server-side rendering applications add the import to your `main.server.ts` file.)');
"(For non-CLI projects, add `import '@angular/localize/init';` to your `polyfills.ts` file.\n" +
'For server-side rendering applications add the import to your `main.server.ts` file.)',
);
};
}

View file

@ -7,41 +7,119 @@
*/
export {setAlternateWeakRefImpl as ɵsetAlternateWeakRefImpl} from '../primitives/signals';
export {detectChangesInViewIfRequired as ɵdetectChangesInViewIfRequired, whenStable as ɵwhenStable} from './application/application_ref';
export {IMAGE_CONFIG as ɵIMAGE_CONFIG, IMAGE_CONFIG_DEFAULTS as ɵIMAGE_CONFIG_DEFAULTS, ImageConfig as ɵImageConfig} from './application/application_tokens';
export {
detectChangesInViewIfRequired as ɵdetectChangesInViewIfRequired,
whenStable as ɵwhenStable,
} from './application/application_ref';
export {
IMAGE_CONFIG as ɵIMAGE_CONFIG,
IMAGE_CONFIG_DEFAULTS as ɵIMAGE_CONFIG_DEFAULTS,
ImageConfig as ɵImageConfig,
} from './application/application_tokens';
export {internalCreateApplication as ɵinternalCreateApplication} from './application/create_application';
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
export {ChangeDetectionScheduler as ɵChangeDetectionScheduler, NotificationSource as ɵNotificationSource, ZONELESS_ENABLED as ɵZONELESS_ENABLED} from './change_detection/scheduling/zoneless_scheduling';
export {
defaultIterableDiffers as ɵdefaultIterableDiffers,
defaultKeyValueDiffers as ɵdefaultKeyValueDiffers,
} from './change_detection/change_detection';
export {
ChangeDetectionScheduler as ɵChangeDetectionScheduler,
NotificationSource as ɵNotificationSource,
ZONELESS_ENABLED as ɵZONELESS_ENABLED,
} from './change_detection/scheduling/zoneless_scheduling';
export {Console as ɵConsole} from './console';
export {DeferBlockDetails as ɵDeferBlockDetails, getDeferBlocks as ɵgetDeferBlocks} from './defer/discovery';
export {renderDeferBlockState as ɵrenderDeferBlockState, triggerResourceLoading as ɵtriggerResourceLoading} from './defer/instructions';
export {DeferBlockBehavior as ɵDeferBlockBehavior, DeferBlockConfig as ɵDeferBlockConfig, DeferBlockState as ɵDeferBlockState} from './defer/interfaces';
export {convertToBitFlags as ɵconvertToBitFlags, setCurrentInjector as ɵsetCurrentInjector} from './di/injector_compatibility';
export {getInjectableDef as ɵgetInjectableDef, ɵɵInjectableDeclaration, ɵɵInjectorDef} from './di/interface/defs';
export {InternalEnvironmentProviders as ɵInternalEnvironmentProviders, isEnvironmentProviders as ɵisEnvironmentProviders} from './di/interface/provider';
export {
DeferBlockDetails as ɵDeferBlockDetails,
getDeferBlocks as ɵgetDeferBlocks,
} from './defer/discovery';
export {
renderDeferBlockState as ɵrenderDeferBlockState,
triggerResourceLoading as ɵtriggerResourceLoading,
} from './defer/instructions';
export {
DeferBlockBehavior as ɵDeferBlockBehavior,
DeferBlockConfig as ɵDeferBlockConfig,
DeferBlockState as ɵDeferBlockState,
} from './defer/interfaces';
export {
convertToBitFlags as ɵconvertToBitFlags,
setCurrentInjector as ɵsetCurrentInjector,
} from './di/injector_compatibility';
export {
getInjectableDef as ɵgetInjectableDef,
ɵɵInjectableDeclaration,
ɵɵInjectorDef,
} from './di/interface/defs';
export {
InternalEnvironmentProviders as ɵInternalEnvironmentProviders,
isEnvironmentProviders as ɵisEnvironmentProviders,
} from './di/interface/provider';
export {INJECTOR_SCOPE as ɵINJECTOR_SCOPE} from './di/scope';
export {XSS_SECURITY_URL as ɵXSS_SECURITY_URL} from './error_details_base_url';
export {formatRuntimeError as ɵformatRuntimeError, RuntimeError as ɵRuntimeError, RuntimeErrorCode as ɵRuntimeErrorCode} from './errors';
export {
formatRuntimeError as ɵformatRuntimeError,
RuntimeError as ɵRuntimeError,
RuntimeErrorCode as ɵRuntimeErrorCode,
} from './errors';
export {annotateForHydration as ɵannotateForHydration} from './hydration/annotate';
export {withDomHydration as ɵwithDomHydration, withI18nSupport as ɵwithI18nSupport} from './hydration/api';
export {
withDomHydration as ɵwithDomHydration,
withI18nSupport as ɵwithI18nSupport,
} from './hydration/api';
export {withEventReplay as ɵwithEventReplay} from './hydration/event_replay';
export {IS_HYDRATION_DOM_REUSE_ENABLED as ɵIS_HYDRATION_DOM_REUSE_ENABLED} from './hydration/tokens';
export {HydratedNode as ɵHydratedNode, HydrationInfo as ɵHydrationInfo, readHydrationInfo as ɵreadHydrationInfo, SSR_CONTENT_INTEGRITY_MARKER as ɵSSR_CONTENT_INTEGRITY_MARKER} from './hydration/utils';
export {CurrencyIndex as ɵCurrencyIndex, ExtraLocaleDataIndex as ɵExtraLocaleDataIndex, findLocaleData as ɵfindLocaleData, getLocaleCurrencyCode as ɵgetLocaleCurrencyCode, getLocalePluralCase as ɵgetLocalePluralCase, LocaleDataIndex as ɵLocaleDataIndex, registerLocaleData as ɵregisterLocaleData, unregisterAllLocaleData as ɵunregisterLocaleData} from './i18n/locale_data_api';
export {
HydratedNode as ɵHydratedNode,
HydrationInfo as ɵHydrationInfo,
readHydrationInfo as ɵreadHydrationInfo,
SSR_CONTENT_INTEGRITY_MARKER as ɵSSR_CONTENT_INTEGRITY_MARKER,
} from './hydration/utils';
export {
CurrencyIndex as ɵCurrencyIndex,
ExtraLocaleDataIndex as ɵExtraLocaleDataIndex,
findLocaleData as ɵfindLocaleData,
getLocaleCurrencyCode as ɵgetLocaleCurrencyCode,
getLocalePluralCase as ɵgetLocalePluralCase,
LocaleDataIndex as ɵLocaleDataIndex,
registerLocaleData as ɵregisterLocaleData,
unregisterAllLocaleData as ɵunregisterLocaleData,
} from './i18n/locale_data_api';
export {DEFAULT_LOCALE_ID as ɵDEFAULT_LOCALE_ID} from './i18n/localization';
export {Writable as ɵWritable} from './interface/type';
export {ComponentFactory as ɵComponentFactory} from './linker/component_factory';
export {clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution as ɵisComponentDefPendingResolution, resolveComponentResources as ɵresolveComponentResources, restoreComponentResolutionQueue as ɵrestoreComponentResolutionQueue} from './metadata/resource_loading';
export {
clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue,
isComponentDefPendingResolution as ɵisComponentDefPendingResolution,
resolveComponentResources as ɵresolveComponentResources,
restoreComponentResolutionQueue as ɵrestoreComponentResolutionQueue,
} from './metadata/resource_loading';
export {PendingTasks as ɵPendingTasks} from './pending_tasks';
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS} from './platform/platform';
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';
export {AnimationRendererType as ɵAnimationRendererType} from './render/api';
export {InjectorProfilerContext as ɵInjectorProfilerContext, ProviderRecord as ɵProviderRecord, setInjectorProfilerContext as ɵsetInjectorProfilerContext} from './render3/debug/injector_profiler';
export {
InjectorProfilerContext as ɵInjectorProfilerContext,
ProviderRecord as ɵProviderRecord,
setInjectorProfilerContext as ɵsetInjectorProfilerContext,
} from './render3/debug/injector_profiler';
export {queueStateUpdate as ɵqueueStateUpdate} from './render3/queue_state_update';
export {allowSanitizationBypassAndThrow as ɵallowSanitizationBypassAndThrow, BypassType as ɵBypassType, getSanitizationBypassType as ɵgetSanitizationBypassType, SafeHtml as ɵSafeHtml, SafeResourceUrl as ɵSafeResourceUrl, SafeScript as ɵSafeScript, SafeStyle as ɵSafeStyle, SafeUrl as ɵSafeUrl, SafeValue as ɵSafeValue, unwrapSafeValue as ɵunwrapSafeValue} from './sanitization/bypass';
export {
allowSanitizationBypassAndThrow as ɵallowSanitizationBypassAndThrow,
BypassType as ɵBypassType,
getSanitizationBypassType as ɵgetSanitizationBypassType,
SafeHtml as ɵSafeHtml,
SafeResourceUrl as ɵSafeResourceUrl,
SafeScript as ɵSafeScript,
SafeStyle as ɵSafeStyle,
SafeUrl as ɵSafeUrl,
SafeValue as ɵSafeValue,
unwrapSafeValue as ɵunwrapSafeValue,
} from './sanitization/bypass';
export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer';
export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer';
export {TESTABILITY as ɵTESTABILITY, TESTABILITY_GETTER as ɵTESTABILITY_GETTER} from './testability/testability';
export {
TESTABILITY as ɵTESTABILITY,
TESTABILITY_GETTER as ɵTESTABILITY_GETTER,
} from './testability/testability';
export {booleanAttribute, numberAttribute} from './util/coercion';
export {devModeEqual as ɵdevModeEqual} from './util/comparison';
export {global as ɵglobal} from './util/global';

View file

@ -7,24 +7,15 @@
*/
// clang-format off
export {
isSignal,
Signal,
ValueEqualityFn,
} from './render3/reactivity/api';
export {
computed,
CreateComputedOptions,
} from './render3/reactivity/computed';
export {isSignal, Signal, ValueEqualityFn} from './render3/reactivity/api';
export {computed, CreateComputedOptions} from './render3/reactivity/computed';
export {
CreateSignalOptions,
signal,
WritableSignal,
ɵunwrapWritableSignal,
} from './render3/reactivity/signal';
export {
untracked,
} from './render3/reactivity/untracked';
export {untracked} from './render3/reactivity/untracked';
export {
CreateEffectOptions,
effect,
@ -33,7 +24,5 @@ export {
EffectCleanupRegisterFn,
EffectScheduler as ɵEffectScheduler,
} from './render3/reactivity/effect';
export {
assertNotInReactiveContext,
} from './render3/reactivity/asserts';
export {assertNotInReactiveContext} from './render3/reactivity/asserts';
// clang-format on

View file

@ -11,18 +11,10 @@
// performed by rollup while it's creating fesm files.
//
// no code actually imports these symbols from the @angular/core entry point
export {
isBoundToModule as ɵisBoundToModule
} from './application/application_ref';
export {
compileNgModuleFactory as ɵcompileNgModuleFactory,
} from './application/application_ngmodule_factory_compiler';
export {
injectChangeDetectorRef as ɵinjectChangeDetectorRef,
} from './change_detection/change_detector_ref';
export {
getDebugNode as ɵgetDebugNode,
} from './debug/debug_node';
export {isBoundToModule as ɵisBoundToModule} from './application/application_ref';
export {compileNgModuleFactory as ɵcompileNgModuleFactory} from './application/application_ngmodule_factory_compiler';
export {injectChangeDetectorRef as ɵinjectChangeDetectorRef} from './change_detection/change_detector_ref';
export {getDebugNode as ɵgetDebugNode} from './debug/debug_node';
export {
NG_INJ_DEF as ɵNG_INJ_DEF,
NG_PROV_DEF as ɵNG_PROV_DEF,
@ -37,9 +29,7 @@ export {
NgModuleDef as ɵNgModuleDef,
NgModuleTransitiveScopes as ɵNgModuleTransitiveScopes,
} from './metadata/ng_module_def';
export {
getLContext as ɵgetLContext
} from './render3/context_discovery';
export {getLContext as ɵgetLContext} from './render3/context_discovery';
export {
NG_COMP_DEF as ɵNG_COMP_DEF,
NG_DIR_DEF as ɵNG_DIR_DEF,
@ -180,12 +170,10 @@ export {
ɵɵresolveDocument,
ɵɵresolveWindow,
ɵɵrestoreView,
ɵɵrepeater,
ɵɵrepeaterCreate,
ɵɵrepeaterTrackByIdentity,
ɵɵrepeaterTrackByIndex,
ɵɵsetComponentScope,
ɵɵsetNgModuleScope,
ɵɵgetComponentDepsFactory,
@ -249,24 +237,16 @@ export {
ɵgetUnknownElementStrictMode,
ɵsetUnknownElementStrictMode,
ɵgetUnknownPropertyStrictMode,
ɵsetUnknownPropertyStrictMode
ɵsetUnknownPropertyStrictMode,
} from './render3/index';
export {
CONTAINER_HEADER_OFFSET as ɵCONTAINER_HEADER_OFFSET,
} from './render3/interfaces/container';
export {
LContext as ɵLContext,
} from './render3/interfaces/context';
export {
setDocument as ɵsetDocument
} from './render3/interfaces/document';
export {CONTAINER_HEADER_OFFSET as ɵCONTAINER_HEADER_OFFSET} from './render3/interfaces/container';
export {LContext as ɵLContext} from './render3/interfaces/context';
export {setDocument as ɵsetDocument} from './render3/interfaces/document';
export {
compileComponent as ɵcompileComponent,
compileDirective as ɵcompileDirective,
} from './render3/jit/directive';
export {
resetJitOptions as ɵresetJitOptions,
} from './render3/jit/jit_options';
export {resetJitOptions as ɵresetJitOptions} from './render3/jit/jit_options';
export {
compileNgModule as ɵcompileNgModule,
compileNgModuleDefs as ɵcompileNgModuleDefs,
@ -287,14 +267,10 @@ export {
ɵɵngDeclareNgModule,
ɵɵngDeclarePipe,
} from './render3/jit/partial';
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';
export { GlobalDevModeUtils as ɵGlobalDevModeUtils } from './render3/util/global_utils';
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';
export {GlobalDevModeUtils as ɵGlobalDevModeUtils} from './render3/util/global_utils';
export {ViewRef as ɵViewRef} from './render3/view_ref';
export {
bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml,
@ -313,14 +289,16 @@ export {
ɵɵtrustConstantHtml,
ɵɵtrustConstantResourceUrl,
} from './sanitization/sanitization';
export {ɵɵvalidateIframeAttribute} from './sanitization/iframe_attrs_validation';
export {noSideEffects as ɵnoSideEffects} from './util/closure';
export {
ɵɵvalidateIframeAttribute,
} from './sanitization/iframe_attrs_validation';
AfterRenderEventManager as ɵAfterRenderEventManager,
internalAfterNextRender as ɵinternalAfterNextRender,
} from './render3/after_render_hooks';
export {
noSideEffects as ɵnoSideEffects,
} from './util/closure';
export { AfterRenderEventManager as ɵAfterRenderEventManager, internalAfterNextRender as ɵinternalAfterNextRender } from './render3/after_render_hooks';
export {depsTracker as ɵdepsTracker, USE_RUNTIME_DEPS_TRACKER_FOR_JIT as ɵUSE_RUNTIME_DEPS_TRACKER_FOR_JIT} from './render3/deps_tracker/deps_tracker';
depsTracker as ɵdepsTracker,
USE_RUNTIME_DEPS_TRACKER_FOR_JIT as ɵUSE_RUNTIME_DEPS_TRACKER_FOR_JIT,
} from './render3/deps_tracker/deps_tracker';
export {generateStandaloneInDeclarationsError as ɵgenerateStandaloneInDeclarationsError} from './render3/jit/module';
export {getAsyncClassMetadataFn as ɵgetAsyncClassMetadataFn} from './render3/metadata';

View file

@ -12,8 +12,23 @@ import {getLContext} from '../render3/context_discovery';
import {CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from '../render3/interfaces/container';
import {TElementNode, TNode, TNodeFlags, TNodeType} from '../render3/interfaces/node';
import {isComponentHost, isLContainer} from '../render3/interfaces/type_checks';
import {DECLARATION_COMPONENT_VIEW, LView, PARENT, T_HOST, TData, TVIEW} from '../render3/interfaces/view';
import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, getOwningComponent} from '../render3/util/discovery_utils';
import {
DECLARATION_COMPONENT_VIEW,
LView,
PARENT,
T_HOST,
TData,
TVIEW,
} from '../render3/interfaces/view';
import {
getComponent,
getContext,
getInjectionTokens,
getInjector,
getListeners,
getLocalRefs,
getOwningComponent,
} from '../render3/util/discovery_utils';
import {INTERPOLATION_DELIMITER} from '../render3/util/misc_utils';
import {renderStringify} from '../render3/util/stringify_utils';
import {getComponentLViewByIndex, getNativeByTNodeOrNull} from '../render3/util/view_utils';
@ -23,7 +38,10 @@ import {assertDomNode} from '../util/assert';
* @publicApi
*/
export class DebugEventListener {
constructor(public name: string, public callback: Function) {}
constructor(
public name: string,
public callback: Function,
) {}
}
/**
@ -49,7 +67,7 @@ export class DebugNode {
/**
* The `DebugElement` parent. Will be `null` if this is the root element.
*/
get parent(): DebugElement|null {
get parent(): DebugElement | null {
const parent = this.nativeNode.parentNode as Element;
return parent ? new DebugElement(parent) : null;
}
@ -66,8 +84,9 @@ export class DebugNode {
*/
get componentInstance(): any {
const nativeElement = this.nativeNode;
return nativeElement &&
(getComponent(nativeElement as Element) || getOwningComponent(nativeElement));
return (
nativeElement && (getComponent(nativeElement as Element) || getOwningComponent(nativeElement))
);
}
/**
@ -87,7 +106,7 @@ export class DebugNode {
* properties.
*/
get listeners(): DebugEventListener[] {
return getListeners(this.nativeNode as Element).filter(listener => listener.type === 'dom');
return getListeners(this.nativeNode as Element).filter((listener) => listener.type === 'dom');
}
/**
@ -124,7 +143,7 @@ export class DebugElement extends DebugNode {
* The underlying DOM element at the root of the component.
*/
get nativeElement(): any {
return this.nativeNode.nodeType == Node.ELEMENT_NODE ? this.nativeNode as Element : null;
return this.nativeNode.nodeType == Node.ELEMENT_NODE ? (this.nativeNode as Element) : null;
}
/**
@ -155,7 +174,7 @@ export class DebugElement extends DebugNode {
* - input property bindings (e.g. `[myCustomInput]="value"`)
* - attribute bindings (e.g. `[attr.role]="menu"`)
*/
get properties(): {[key: string]: any;} {
get properties(): {[key: string]: any} {
const context = getLContext(this.nativeNode)!;
const lView = context ? context.lView : null;
@ -179,8 +198,8 @@ export class DebugElement extends DebugNode {
* A map of attribute names to attribute values for an element.
*/
// TODO: replace null by undefined in the return type
get attributes(): {[key: string]: string|null} {
const attributes: {[key: string]: string|null} = {};
get attributes(): {[key: string]: string | null} {
const attributes: {[key: string]: string | null} = {};
const element = this.nativeElement as Element | undefined;
if (!element) {
@ -236,7 +255,7 @@ export class DebugElement extends DebugNode {
* The inline styles of the DOM element.
*/
// TODO: replace null by undefined in the return type
get styles(): {[key: string]: string|null} {
get styles(): {[key: string]: string | null} {
const element = this.nativeElement as HTMLElement | null;
return (element?.style ?? {}) as {[key: string]: string | null};
}
@ -258,9 +277,9 @@ export class DebugElement extends DebugNode {
// SVG elements return an `SVGAnimatedString` instead of a plain string for the `className`.
const className = element.className as string | SVGAnimatedString;
const classes =
typeof className !== 'string' ? className.baseVal.split(' ') : className.split(' ');
typeof className !== 'string' ? className.baseVal.split(' ') : className.split(' ');
classes.forEach((value: string) => result[value] = true);
classes.forEach((value: string) => (result[value] = true));
return result;
}
@ -337,7 +356,7 @@ export class DebugElement extends DebugNode {
const node = this.nativeNode as any;
const invokedListeners: Function[] = [];
this.listeners.forEach(listener => {
this.listeners.forEach((listener) => {
if (listener.name === eventName) {
const callback = listener.callback;
callback.call(node, eventObj);
@ -360,15 +379,17 @@ export class DebugElement extends DebugNode {
// strip the name, turning the condition in to ("" === "") and always returning true.
if (listener.toString().indexOf('__ngUnwrap__') !== -1) {
const unwrappedListener = listener('__ngUnwrap__');
return invokedListeners.indexOf(unwrappedListener) === -1 &&
unwrappedListener.call(node, eventObj);
return (
invokedListeners.indexOf(unwrappedListener) === -1 &&
unwrappedListener.call(node, eventObj)
);
}
});
}
}
}
function copyDomProperties(element: Element|null, properties: {[name: string]: string}): void {
function copyDomProperties(element: Element | null, properties: {[name: string]: string}): void {
if (element) {
// Skip own properties (as those are patched)
let obj = Object.getPrototypeOf(element);
@ -392,8 +413,12 @@ function copyDomProperties(element: Element|null, properties: {[name: string]: s
}
function isPrimitiveValue(value: any): boolean {
return typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number' ||
value === null;
return (
typeof value === 'string' ||
typeof value === 'boolean' ||
typeof value === 'number' ||
value === null
);
}
/**
@ -405,20 +430,35 @@ function isPrimitiveValue(value: any): boolean {
* @param elementsOnly whether only elements should be searched
*/
function _queryAll(
parentElement: DebugElement, predicate: Predicate<DebugElement>, matches: DebugElement[],
elementsOnly: true): void;
parentElement: DebugElement,
predicate: Predicate<DebugElement>,
matches: DebugElement[],
elementsOnly: true,
): void;
function _queryAll(
parentElement: DebugElement, predicate: Predicate<DebugNode>, matches: DebugNode[],
elementsOnly: false): void;
parentElement: DebugElement,
predicate: Predicate<DebugNode>,
matches: DebugNode[],
elementsOnly: false,
): void;
function _queryAll(
parentElement: DebugElement, predicate: Predicate<DebugElement>|Predicate<DebugNode>,
matches: DebugElement[]|DebugNode[], elementsOnly: boolean) {
parentElement: DebugElement,
predicate: Predicate<DebugElement> | Predicate<DebugNode>,
matches: DebugElement[] | DebugNode[],
elementsOnly: boolean,
) {
const context = getLContext(parentElement.nativeNode)!;
const lView = context ? context.lView : null;
if (lView !== null) {
const parentTNode = lView[TVIEW].data[context.nodeIndex] as TNode;
_queryNodeChildren(
parentTNode, lView, predicate, matches, elementsOnly, parentElement.nativeNode);
parentTNode,
lView,
predicate,
matches,
elementsOnly,
parentElement.nativeNode,
);
} else {
// If the context is null, then `parentElement` was either created with Renderer2 or native DOM
// APIs.
@ -437,8 +477,13 @@ function _queryAll(
* @param rootNativeNode the root native node on which predicate should not be matched
*/
function _queryNodeChildren(
tNode: TNode, lView: LView, predicate: Predicate<DebugElement>|Predicate<DebugNode>,
matches: DebugElement[]|DebugNode[], elementsOnly: boolean, rootNativeNode: any) {
tNode: TNode,
lView: LView,
predicate: Predicate<DebugElement> | Predicate<DebugNode>,
matches: DebugElement[] | DebugNode[],
elementsOnly: boolean,
rootNativeNode: any,
) {
ngDevMode && assertTNodeForLView(tNode, lView);
const nativeNode = getNativeByTNodeOrNull(tNode, lView);
// For each type of TNode, specific logic is executed.
@ -452,8 +497,13 @@ function _queryNodeChildren(
const componentView = getComponentLViewByIndex(tNode.index, lView);
if (componentView && componentView[TVIEW].firstChild) {
_queryNodeChildren(
componentView[TVIEW].firstChild!, componentView, predicate, matches, elementsOnly,
rootNativeNode);
componentView[TVIEW].firstChild!,
componentView,
predicate,
matches,
elementsOnly,
rootNativeNode,
);
}
} else {
if (tNode.child) {
@ -475,7 +525,12 @@ function _queryNodeChildren(
const nodeOrContainer = lView[tNode.index];
if (isLContainer(nodeOrContainer)) {
_queryNodeChildrenInContainer(
nodeOrContainer, predicate, matches, elementsOnly, rootNativeNode);
nodeOrContainer,
predicate,
matches,
elementsOnly,
rootNativeNode,
);
}
} else if (tNode.type & TNodeType.Container) {
// Case 2: the TNode is a container
@ -489,8 +544,9 @@ function _queryNodeChildren(
// The nodes projected at this location all need to be processed.
const componentView = lView![DECLARATION_COMPONENT_VIEW];
const componentHost = componentView[T_HOST] as TElementNode;
const head: TNode|null =
(componentHost.projection as (TNode | null)[])[tNode.projection as number];
const head: TNode | null = (componentHost.projection as (TNode | null)[])[
tNode.projection as number
];
if (Array.isArray(head)) {
for (let nativeNode of head) {
@ -510,7 +566,7 @@ function _queryNodeChildren(
if (rootNativeNode !== nativeNode) {
// To determine the next node to be processed, we need to use the next or the projectionNext
// link, depending on whether the current node has been projected.
const nextTNode = (tNode.flags & TNodeFlags.isProjected) ? tNode.projectionNext : tNode.next;
const nextTNode = tNode.flags & TNodeFlags.isProjected ? tNode.projectionNext : tNode.next;
if (nextTNode) {
_queryNodeChildren(nextTNode, lView, predicate, matches, elementsOnly, rootNativeNode);
}
@ -527,8 +583,12 @@ function _queryNodeChildren(
* @param rootNativeNode the root native node on which predicate should not be matched
*/
function _queryNodeChildrenInContainer(
lContainer: LContainer, predicate: Predicate<DebugElement>|Predicate<DebugNode>,
matches: DebugElement[]|DebugNode[], elementsOnly: boolean, rootNativeNode: any) {
lContainer: LContainer,
predicate: Predicate<DebugElement> | Predicate<DebugNode>,
matches: DebugElement[] | DebugNode[],
elementsOnly: boolean,
rootNativeNode: any,
) {
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
const childView = lContainer[i] as LView;
const firstChild = childView[TVIEW].firstChild;
@ -548,8 +608,12 @@ function _queryNodeChildrenInContainer(
* @param rootNativeNode the root native node on which predicate should not be matched
*/
function _addQueryMatch(
nativeNode: any, predicate: Predicate<DebugElement>|Predicate<DebugNode>,
matches: DebugElement[]|DebugNode[], elementsOnly: boolean, rootNativeNode: any) {
nativeNode: any,
predicate: Predicate<DebugElement> | Predicate<DebugNode>,
matches: DebugElement[] | DebugNode[],
elementsOnly: boolean,
rootNativeNode: any,
) {
if (rootNativeNode !== nativeNode) {
const debugNode = getDebugNode(nativeNode);
if (!debugNode) {
@ -558,12 +622,18 @@ function _addQueryMatch(
// Type of the "predicate and "matches" array are set based on the value of
// the "elementsOnly" parameter. TypeScript is not able to properly infer these
// types with generics, so we manually cast the parameters accordingly.
if (elementsOnly && (debugNode instanceof DebugElement) && predicate(debugNode) &&
matches.indexOf(debugNode) === -1) {
if (
elementsOnly &&
debugNode instanceof DebugElement &&
predicate(debugNode) &&
matches.indexOf(debugNode) === -1
) {
matches.push(debugNode);
} else if (
!elementsOnly && (predicate as Predicate<DebugNode>)(debugNode) &&
(matches as DebugNode[]).indexOf(debugNode) === -1) {
!elementsOnly &&
(predicate as Predicate<DebugNode>)(debugNode) &&
(matches as DebugNode[]).indexOf(debugNode) === -1
) {
(matches as DebugNode[]).push(debugNode);
}
}
@ -578,8 +648,11 @@ function _addQueryMatch(
* @param elementsOnly whether only elements should be searched
*/
function _queryNativeNodeDescendants(
parentNode: any, predicate: Predicate<DebugElement>|Predicate<DebugNode>,
matches: DebugElement[]|DebugNode[], elementsOnly: boolean) {
parentNode: any,
predicate: Predicate<DebugElement> | Predicate<DebugNode>,
matches: DebugElement[] | DebugNode[],
elementsOnly: boolean,
) {
const nodes = parentNode.childNodes;
const length = nodes.length;
@ -588,12 +661,18 @@ function _queryNativeNodeDescendants(
const debugNode = getDebugNode(node);
if (debugNode) {
if (elementsOnly && (debugNode instanceof DebugElement) && predicate(debugNode) &&
matches.indexOf(debugNode) === -1) {
if (
elementsOnly &&
debugNode instanceof DebugElement &&
predicate(debugNode) &&
matches.indexOf(debugNode) === -1
) {
matches.push(debugNode);
} else if (
!elementsOnly && (predicate as Predicate<DebugNode>)(debugNode) &&
(matches as DebugNode[]).indexOf(debugNode) === -1) {
!elementsOnly &&
(predicate as Predicate<DebugNode>)(debugNode) &&
(matches as DebugNode[]).indexOf(debugNode) === -1
) {
(matches as DebugNode[]).push(debugNode);
}
@ -608,7 +687,11 @@ function _queryNativeNodeDescendants(
* defined in templates, not in host bindings.
*/
function collectPropertyBindings(
properties: {[key: string]: string}, tNode: TNode, lView: LView, tData: TData): void {
properties: {[key: string]: string},
tNode: TNode,
lView: LView,
tData: TData,
): void {
let bindingIndexes = tNode.propertyBindings;
if (bindingIndexes !== null) {
@ -630,7 +713,6 @@ function collectPropertyBindings(
}
}
// Need to keep the nodes in a global Map so that multiple angular apps are supported.
const _nativeNodeToDebugNode = new Map<any, DebugNode>();
@ -639,12 +721,13 @@ const NG_DEBUG_PROPERTY = '__ng_debug__';
/**
* @publicApi
*/
export function getDebugNode(nativeNode: any): DebugNode|null {
export function getDebugNode(nativeNode: any): DebugNode | null {
if (nativeNode instanceof Node) {
if (!(nativeNode.hasOwnProperty(NG_DEBUG_PROPERTY))) {
(nativeNode as any)[NG_DEBUG_PROPERTY] = nativeNode.nodeType == Node.ELEMENT_NODE ?
new DebugElement(nativeNode as Element) :
new DebugNode(nativeNode);
if (!nativeNode.hasOwnProperty(NG_DEBUG_PROPERTY)) {
(nativeNode as any)[NG_DEBUG_PROPERTY] =
nativeNode.nodeType == Node.ELEMENT_NODE
? new DebugElement(nativeNode as Element)
: new DebugNode(nativeNode);
}
return (nativeNode as any)[NG_DEBUG_PROPERTY];
}

View file

@ -6,14 +6,22 @@
* found in the LICENSE file at https://angular.io/license
*/
import {LDeferBlockDetails, PREFETCH_TRIGGER_CLEANUP_FNS, TRIGGER_CLEANUP_FNS, TriggerType} from './interfaces';
import {
LDeferBlockDetails,
PREFETCH_TRIGGER_CLEANUP_FNS,
TRIGGER_CLEANUP_FNS,
TriggerType,
} from './interfaces';
/**
* Registers a cleanup function associated with a prefetching trigger
* or a regular trigger of a defer block.
*/
export function storeTriggerCleanupFn(
type: TriggerType, lDetails: LDeferBlockDetails, cleanupFn: VoidFunction) {
type: TriggerType,
lDetails: LDeferBlockDetails,
cleanupFn: VoidFunction,
) {
const key = type === TriggerType.Prefetch ? PREFETCH_TRIGGER_CLEANUP_FNS : TRIGGER_CLEANUP_FNS;
if (lDetails[key] === null) {
lDetails[key] = [];

View file

@ -13,18 +13,28 @@ import {CONTAINER_HEADER_OFFSET} from '../render3/interfaces/container';
import {TNode} from '../render3/interfaces/node';
import {isDestroyed} from '../render3/interfaces/type_checks';
import {HEADER_OFFSET, INJECTOR, LView} from '../render3/interfaces/view';
import {getNativeByIndex, removeLViewOnDestroy, storeLViewOnDestroy, walkUpViews} from '../render3/util/view_utils';
import {
getNativeByIndex,
removeLViewOnDestroy,
storeLViewOnDestroy,
walkUpViews,
} from '../render3/util/view_utils';
import {assertElement, assertEqual} from '../util/assert';
import {NgZone} from '../zone';
import {storeTriggerCleanupFn} from './cleanup';
import {DEFER_BLOCK_STATE, DeferBlockInternalState, DeferBlockState, TriggerType} from './interfaces';
import {
DEFER_BLOCK_STATE,
DeferBlockInternalState,
DeferBlockState,
TriggerType,
} from './interfaces';
import {getLDeferBlockDetails} from './utils';
/** Configuration object used to register passive and capturing events. */
const eventListenerOptions: AddEventListenerOptions = {
passive: true,
capture: true
capture: true,
};
/** Keeps track of the currently-registered `on hover` triggers. */
@ -43,7 +53,7 @@ const interactionEventNames = ['click', 'keydown'] as const;
const hoverEventNames = ['mouseenter', 'focusin'] as const;
/** `IntersectionObserver` used to observe `viewport` triggers. */
let intersectionObserver: IntersectionObserver|null = null;
let intersectionObserver: IntersectionObserver | null = null;
/** Number of elements currently observed with `viewport` triggers. */
let observedViewportElements = 0;
@ -56,7 +66,7 @@ class DeferEventEntry {
for (const callback of this.callbacks) {
callback();
}
}
};
}
/**
@ -144,20 +154,25 @@ export function onHover(trigger: Element, callback: VoidFunction): VoidFunction
* @param injector Injector that can be used by the trigger to resolve DI tokens.
*/
export function onViewport(
trigger: Element, callback: VoidFunction, injector: Injector): VoidFunction {
trigger: Element,
callback: VoidFunction,
injector: Injector,
): VoidFunction {
const ngZone = injector.get(NgZone);
let entry = viewportTriggers.get(trigger);
intersectionObserver = intersectionObserver || ngZone.runOutsideAngular(() => {
return new IntersectionObserver(entries => {
for (const current of entries) {
// Only invoke the callbacks if the specific element is intersecting.
if (current.isIntersecting && viewportTriggers.has(current.target)) {
ngZone.run(viewportTriggers.get(current.target)!.listener);
intersectionObserver =
intersectionObserver ||
ngZone.runOutsideAngular(() => {
return new IntersectionObserver((entries) => {
for (const current of entries) {
// Only invoke the callbacks if the specific element is intersecting.
if (current.isIntersecting && viewportTriggers.has(current.target)) {
ngZone.run(viewportTriggers.get(current.target)!.listener);
}
}
}
});
});
});
if (!entry) {
entry = new DeferEventEntry();
@ -198,7 +213,10 @@ export function onViewport(
* value means that the trigger is in the same LView as the deferred block.
*/
export function getTriggerLView(
deferredHostLView: LView, deferredTNode: TNode, walkUpTimes: number|undefined): LView|null {
deferredHostLView: LView,
deferredTNode: TNode,
walkUpTimes: number | undefined,
): LView | null {
// The trigger is in the same view, we don't need to traverse.
if (walkUpTimes == null) {
return deferredHostLView;
@ -219,8 +237,10 @@ export function getTriggerLView(
const lDetails = getLDeferBlockDetails(deferredHostLView, deferredTNode);
const renderedState = lDetails[DEFER_BLOCK_STATE];
assertEqual(
renderedState, DeferBlockState.Placeholder,
'Expected a placeholder to be rendered in this defer block.');
renderedState,
DeferBlockState.Placeholder,
'Expected a placeholder to be rendered in this defer block.',
);
assertLView(triggerLView);
}
@ -250,9 +270,14 @@ export function getTriggerElement(triggerLView: LView, triggerIndex: number): El
* @param type Trigger type to distinguish between regular and prefetch triggers.
*/
export function registerDomTrigger(
initialLView: LView, tNode: TNode, triggerIndex: number, walkUpTimes: number|undefined,
registerFn: (element: Element, callback: VoidFunction, injector: Injector) => VoidFunction,
callback: VoidFunction, type: TriggerType) {
initialLView: LView,
tNode: TNode,
triggerIndex: number,
walkUpTimes: number | undefined,
registerFn: (element: Element, callback: VoidFunction, injector: Injector) => VoidFunction,
callback: VoidFunction,
type: TriggerType,
) {
const injector = initialLView[INJECTOR]!;
function pollDomTrigger() {
// If the initial view was destroyed, we don't need to do anything.
@ -264,8 +289,10 @@ export function registerDomTrigger(
const renderedState = lDetails[DEFER_BLOCK_STATE];
// If the block was loaded before the trigger was resolved, we don't need to do anything.
if (renderedState !== DeferBlockInternalState.Initial &&
renderedState !== DeferBlockState.Placeholder) {
if (
renderedState !== DeferBlockInternalState.Initial &&
renderedState !== DeferBlockState.Placeholder
) {
return;
}
@ -283,12 +310,16 @@ export function registerDomTrigger(
}
const element = getTriggerElement(triggerLView, triggerIndex);
const cleanup = registerFn(element, () => {
if (initialLView !== triggerLView) {
removeLViewOnDestroy(triggerLView, cleanup);
}
callback();
}, injector);
const cleanup = registerFn(
element,
() => {
if (initialLView !== triggerLView) {
removeLViewOnDestroy(triggerLView, cleanup);
}
callback();
},
injector,
);
// The trigger and deferred block might be in different LViews.
// For the main LView the cleanup would happen as a part of

View file

@ -32,9 +32,9 @@ export function onIdle(callback: VoidFunction, lView: LView) {
* overridden/mocked in test environment and picked up by the runtime code.
*/
const _requestIdleCallback = () =>
typeof requestIdleCallback !== 'undefined' ? requestIdleCallback : setTimeout;
typeof requestIdleCallback !== 'undefined' ? requestIdleCallback : setTimeout;
const _cancelIdleCallback = () =>
typeof requestIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout;
typeof requestIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout;
/**
* Helper service to schedule `requestIdleCallback`s for batches of defer blocks,
@ -46,7 +46,7 @@ export class IdleScheduler {
executingCallbacks = false;
// Currently scheduled idle callback id.
idleId: number|null = null;
idleId: number | null = null;
// Set of callbacks to be invoked next.
current = new Set<VoidFunction>();

View file

@ -29,19 +29,67 @@ import {DirectiveDefList, PipeDefList} from '../render3/interfaces/definition';
import {TContainerNode, TNode} from '../render3/interfaces/node';
import {isDestroyed} from '../render3/interfaces/type_checks';
import {HEADER_OFFSET, INJECTOR, LView, PARENT, TVIEW, TView} from '../render3/interfaces/view';
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../render3/state';
import {
getCurrentTNode,
getLView,
getSelectedTNode,
getTView,
nextBindingIndex,
} from '../render3/state';
import {isPlatformBrowser} from '../render3/util/misc_utils';
import {getConstant, getTNode, removeLViewOnDestroy, storeLViewOnDestroy} from '../render3/util/view_utils';
import {addLViewToLContainer, createAndRenderEmbeddedLView, removeLViewFromLContainer, shouldAddViewToDom} from '../render3/view_manipulation';
import {
getConstant,
getTNode,
removeLViewOnDestroy,
storeLViewOnDestroy,
} from '../render3/util/view_utils';
import {
addLViewToLContainer,
createAndRenderEmbeddedLView,
removeLViewFromLContainer,
shouldAddViewToDom,
} from '../render3/view_manipulation';
import {assertDefined, throwError} from '../util/assert';
import {performanceMarkFeature} from '../util/performance';
import {invokeAllTriggerCleanupFns, invokeTriggerCleanupFns, storeTriggerCleanupFn} from './cleanup';
import {
invokeAllTriggerCleanupFns,
invokeTriggerCleanupFns,
storeTriggerCleanupFn,
} from './cleanup';
import {onHover, onInteraction, onViewport, registerDomTrigger} from './dom_triggers';
import {onIdle} from './idle_scheduler';
import {DEFER_BLOCK_STATE, DeferBlockBehavior, DeferBlockConfig, DeferBlockDependencyInterceptor, DeferBlockInternalState, DeferBlockState, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, LOADING_AFTER_CLEANUP_FN, NEXT_DEFER_BLOCK_STATE, STATE_IS_FROZEN_UNTIL, TDeferBlockDetails, TriggerType} from './interfaces';
import {
DEFER_BLOCK_STATE,
DeferBlockBehavior,
DeferBlockConfig,
DeferBlockDependencyInterceptor,
DeferBlockInternalState,
DeferBlockState,
DeferDependenciesLoadingState,
DeferredLoadingBlockConfig,
DeferredPlaceholderBlockConfig,
DependencyResolverFn,
LDeferBlockDetails,
LOADING_AFTER_CLEANUP_FN,
NEXT_DEFER_BLOCK_STATE,
STATE_IS_FROZEN_UNTIL,
TDeferBlockDetails,
TriggerType,
} from './interfaces';
import {onTimer, scheduleTimerTrigger} from './timer_scheduler';
import {addDepsToRegistry, assertDeferredDependenciesLoaded, getLDeferBlockDetails, getLoadingBlockAfter, getMinimumDurationForState, getPrimaryBlockTNode, getTDeferBlockDetails, getTemplateIndexForState, setLDeferBlockDetails, setTDeferBlockDetails} from './utils';
import {
addDepsToRegistry,
assertDeferredDependenciesLoaded,
getLDeferBlockDetails,
getLoadingBlockAfter,
getMinimumDurationForState,
getPrimaryBlockTNode,
getTDeferBlockDetails,
getTemplateIndexForState,
setLDeferBlockDetails,
setTDeferBlockDetails,
} from './utils';
/**
* **INTERNAL**, avoid referencing it in application code.
@ -52,13 +100,14 @@ import {addDepsToRegistry, assertDeferredDependenciesLoaded, getLDeferBlockDetai
* This token is only injected in devMode
*/
export const DEFER_BLOCK_DEPENDENCY_INTERCEPTOR =
new InjectionToken<DeferBlockDependencyInterceptor>('DEFER_BLOCK_DEPENDENCY_INTERCEPTOR');
new InjectionToken<DeferBlockDependencyInterceptor>('DEFER_BLOCK_DEPENDENCY_INTERCEPTOR');
/**
* **INTERNAL**, token used for configuring defer block behavior.
*/
export const DEFER_BLOCK_CONFIG =
new InjectionToken<DeferBlockConfig>(ngDevMode ? 'DEFER_BLOCK_CONFIG' : '');
export const DEFER_BLOCK_CONFIG = new InjectionToken<DeferBlockConfig>(
ngDevMode ? 'DEFER_BLOCK_CONFIG' : '',
);
/**
* Returns whether defer blocks should be triggered.
@ -81,23 +130,30 @@ function shouldTriggerDeferBlock(injector: Injector): boolean {
* argument for the `ɵɵdefer` instruction, which references a timer-based
* implementation.
*/
let applyDeferBlockStateWithSchedulingImpl: (typeof applyDeferBlockState)|null = null;
let applyDeferBlockStateWithSchedulingImpl: typeof applyDeferBlockState | null = null;
/**
* Enables timer-related scheduling if `after` or `minimum` parameters are setup
* on the `@loading` or `@placeholder` blocks.
*/
export function ɵɵdeferEnableTimerScheduling(
tView: TView, tDetails: TDeferBlockDetails, placeholderConfigIndex?: number|null,
loadingConfigIndex?: number|null) {
tView: TView,
tDetails: TDeferBlockDetails,
placeholderConfigIndex?: number | null,
loadingConfigIndex?: number | null,
) {
const tViewConsts = tView.consts;
if (placeholderConfigIndex != null) {
tDetails.placeholderBlockConfig =
getConstant<DeferredPlaceholderBlockConfig>(tViewConsts, placeholderConfigIndex);
tDetails.placeholderBlockConfig = getConstant<DeferredPlaceholderBlockConfig>(
tViewConsts,
placeholderConfigIndex,
);
}
if (loadingConfigIndex != null) {
tDetails.loadingBlockConfig =
getConstant<DeferredLoadingBlockConfig>(tViewConsts, loadingConfigIndex);
tDetails.loadingBlockConfig = getConstant<DeferredLoadingBlockConfig>(
tViewConsts,
loadingConfigIndex,
);
}
// Enable implementation that supports timer-based scheduling.
@ -125,11 +181,16 @@ export function ɵɵdeferEnableTimerScheduling(
* @codeGenApi
*/
export function ɵɵdefer(
index: number, primaryTmplIndex: number, dependencyResolverFn?: DependencyResolverFn|null,
loadingTmplIndex?: number|null, placeholderTmplIndex?: number|null,
errorTmplIndex?: number|null, loadingConfigIndex?: number|null,
placeholderConfigIndex?: number|null,
enableTimerScheduling?: typeof ɵɵdeferEnableTimerScheduling) {
index: number,
primaryTmplIndex: number,
dependencyResolverFn?: DependencyResolverFn | null,
loadingTmplIndex?: number | null,
placeholderTmplIndex?: number | null,
errorTmplIndex?: number | null,
loadingConfigIndex?: number | null,
placeholderConfigIndex?: number | null,
enableTimerScheduling?: typeof ɵɵdeferEnableTimerScheduling,
) {
const lView = getLView();
const tView = getTView();
const adjustedIndex = index + HEADER_OFFSET;
@ -163,20 +224,21 @@ export function ɵɵdefer(
// Init instance-specific defer details and store it.
const lDetails: LDeferBlockDetails = [
null, // NEXT_DEFER_BLOCK_STATE
DeferBlockInternalState.Initial, // DEFER_BLOCK_STATE
null, // STATE_IS_FROZEN_UNTIL
null, // LOADING_AFTER_CLEANUP_FN
null, // TRIGGER_CLEANUP_FNS
null // PREFETCH_TRIGGER_CLEANUP_FNS
null, // NEXT_DEFER_BLOCK_STATE
DeferBlockInternalState.Initial, // DEFER_BLOCK_STATE
null, // STATE_IS_FROZEN_UNTIL
null, // LOADING_AFTER_CLEANUP_FN
null, // TRIGGER_CLEANUP_FNS
null, // PREFETCH_TRIGGER_CLEANUP_FNS
];
setLDeferBlockDetails(lView, adjustedIndex, lDetails);
const cleanupTriggersFn = () => invokeAllTriggerCleanupFns(lDetails);
// When defer block is triggered - unsubscribe from LView destroy cleanup.
storeTriggerCleanupFn(
TriggerType.Regular, lDetails, () => removeLViewOnDestroy(lView, cleanupTriggersFn));
storeTriggerCleanupFn(TriggerType.Regular, lDetails, () =>
removeLViewOnDestroy(lView, cleanupTriggersFn),
);
storeLViewOnDestroy(lView, cleanupTriggersFn);
}
@ -190,7 +252,7 @@ export function ɵɵdeferWhen(rawValue: unknown) {
if (bindingUpdated(lView, bindingIndex, rawValue)) {
const prevConsumer = setActiveConsumer(null);
try {
const value = Boolean(rawValue); // handle truthy or falsy values
const value = Boolean(rawValue); // handle truthy or falsy values
const tNode = getSelectedTNode();
const lDetails = getLDeferBlockDetails(lView, tNode);
const renderedState = lDetails[DEFER_BLOCK_STATE];
@ -198,9 +260,10 @@ export function ɵɵdeferWhen(rawValue: unknown) {
// If nothing is rendered yet, render a placeholder (if defined).
renderPlaceholder(lView, tNode);
} else if (
value === true &&
(renderedState === DeferBlockInternalState.Initial ||
renderedState === DeferBlockState.Placeholder)) {
value === true &&
(renderedState === DeferBlockInternalState.Initial ||
renderedState === DeferBlockState.Placeholder)
) {
// The `when` condition has changed to `true`, trigger defer block loading
// if the block is either in initial (nothing is rendered) or a placeholder
// state.
@ -223,7 +286,7 @@ export function ɵɵdeferPrefetchWhen(rawValue: unknown) {
if (bindingUpdated(lView, bindingIndex, rawValue)) {
const prevConsumer = setActiveConsumer(null);
try {
const value = Boolean(rawValue); // handle truthy or falsy values
const value = Boolean(rawValue); // handle truthy or falsy values
const tView = lView[TVIEW];
const tNode = getSelectedTNode();
const tDetails = getTDeferBlockDetails(tView, tNode);
@ -273,7 +336,6 @@ export function ɵɵdeferOnImmediate() {
triggerDeferBlock(lView, tNode);
}
/**
* Sets up logic to handle the `prefetch on immediate` deferred trigger.
* @codeGenApi
@ -319,8 +381,14 @@ export function ɵɵdeferOnHover(triggerIndex: number, walkUpTimes?: number) {
renderPlaceholder(lView, tNode);
registerDomTrigger(
lView, tNode, triggerIndex, walkUpTimes, onHover, () => triggerDeferBlock(lView, tNode),
TriggerType.Regular);
lView,
tNode,
triggerIndex,
walkUpTimes,
onHover,
() => triggerDeferBlock(lView, tNode),
TriggerType.Regular,
);
}
/**
@ -337,8 +405,14 @@ export function ɵɵdeferPrefetchOnHover(triggerIndex: number, walkUpTimes?: num
if (tDetails.loadingState === DeferDependenciesLoadingState.NOT_STARTED) {
registerDomTrigger(
lView, tNode, triggerIndex, walkUpTimes, onHover,
() => triggerPrefetching(tDetails, lView, tNode), TriggerType.Prefetch);
lView,
tNode,
triggerIndex,
walkUpTimes,
onHover,
() => triggerPrefetching(tDetails, lView, tNode),
TriggerType.Prefetch,
);
}
}
@ -354,8 +428,14 @@ export function ɵɵdeferOnInteraction(triggerIndex: number, walkUpTimes?: numbe
renderPlaceholder(lView, tNode);
registerDomTrigger(
lView, tNode, triggerIndex, walkUpTimes, onInteraction, () => triggerDeferBlock(lView, tNode),
TriggerType.Regular);
lView,
tNode,
triggerIndex,
walkUpTimes,
onInteraction,
() => triggerDeferBlock(lView, tNode),
TriggerType.Regular,
);
}
/**
@ -372,8 +452,14 @@ export function ɵɵdeferPrefetchOnInteraction(triggerIndex: number, walkUpTimes
if (tDetails.loadingState === DeferDependenciesLoadingState.NOT_STARTED) {
registerDomTrigger(
lView, tNode, triggerIndex, walkUpTimes, onInteraction,
() => triggerPrefetching(tDetails, lView, tNode), TriggerType.Prefetch);
lView,
tNode,
triggerIndex,
walkUpTimes,
onInteraction,
() => triggerPrefetching(tDetails, lView, tNode),
TriggerType.Prefetch,
);
}
}
@ -389,8 +475,14 @@ export function ɵɵdeferOnViewport(triggerIndex: number, walkUpTimes?: number)
renderPlaceholder(lView, tNode);
registerDomTrigger(
lView, tNode, triggerIndex, walkUpTimes, onViewport, () => triggerDeferBlock(lView, tNode),
TriggerType.Regular);
lView,
tNode,
triggerIndex,
walkUpTimes,
onViewport,
() => triggerDeferBlock(lView, tNode),
TriggerType.Regular,
);
}
/**
@ -407,8 +499,14 @@ export function ɵɵdeferPrefetchOnViewport(triggerIndex: number, walkUpTimes?:
if (tDetails.loadingState === DeferDependenciesLoadingState.NOT_STARTED) {
registerDomTrigger(
lView, tNode, triggerIndex, walkUpTimes, onViewport,
() => triggerPrefetching(tDetails, lView, tNode), TriggerType.Prefetch);
lView,
tNode,
triggerIndex,
walkUpTimes,
onViewport,
() => triggerPrefetching(tDetails, lView, tNode),
TriggerType.Prefetch,
);
}
}
@ -418,7 +516,8 @@ export function ɵɵdeferPrefetchOnViewport(triggerIndex: number, walkUpTimes?:
* Schedules triggering of a defer block for `on idle` and `on timer` conditions.
*/
function scheduleDelayedTrigger(
scheduleFn: (callback: VoidFunction, lView: LView) => VoidFunction) {
scheduleFn: (callback: VoidFunction, lView: LView) => VoidFunction,
) {
const lView = getLView();
const tNode = getCurrentTNode()!;
@ -434,7 +533,8 @@ function scheduleDelayedTrigger(
* @param scheduleFn A function that does the scheduling.
*/
function scheduleDelayedPrefetching(
scheduleFn: (callback: VoidFunction, lView: LView) => VoidFunction) {
scheduleFn: (callback: VoidFunction, lView: LView) => VoidFunction,
) {
const lView = getLView();
const tNode = getCurrentTNode()!;
const tView = lView[TVIEW];
@ -461,8 +561,11 @@ function scheduleDelayedPrefetching(
* between states via `DeferFixture.render` method.
*/
export function renderDeferBlockState(
newState: DeferBlockState, tNode: TNode, lContainer: LContainer,
skipTimerScheduling = false): void {
newState: DeferBlockState,
tNode: TNode,
lContainer: LContainer,
skipTimerScheduling = false,
): void {
const hostLView = lContainer[PARENT];
const hostTView = hostLView[TVIEW];
@ -479,23 +582,30 @@ export function renderDeferBlockState(
const currentState = lDetails[DEFER_BLOCK_STATE];
if (isValidStateChange(currentState, newState) &&
isValidStateChange(lDetails[NEXT_DEFER_BLOCK_STATE] ?? -1, newState)) {
if (
isValidStateChange(currentState, newState) &&
isValidStateChange(lDetails[NEXT_DEFER_BLOCK_STATE] ?? -1, newState)
) {
const injector = hostLView[INJECTOR]!;
const tDetails = getTDeferBlockDetails(hostTView, tNode);
// Skips scheduling on the server since it can delay the server response.
const needsScheduling = !skipTimerScheduling && isPlatformBrowser(injector) &&
(getLoadingBlockAfter(tDetails) !== null ||
getMinimumDurationForState(tDetails, DeferBlockState.Loading) !== null ||
getMinimumDurationForState(tDetails, DeferBlockState.Placeholder));
const needsScheduling =
!skipTimerScheduling &&
isPlatformBrowser(injector) &&
(getLoadingBlockAfter(tDetails) !== null ||
getMinimumDurationForState(tDetails, DeferBlockState.Loading) !== null ||
getMinimumDurationForState(tDetails, DeferBlockState.Placeholder));
if (ngDevMode && needsScheduling) {
assertDefined(
applyDeferBlockStateWithSchedulingImpl, 'Expected scheduling function to be defined');
applyDeferBlockStateWithSchedulingImpl,
'Expected scheduling function to be defined',
);
}
const applyStateFn =
needsScheduling ? applyDeferBlockStateWithSchedulingImpl! : applyDeferBlockState;
const applyStateFn = needsScheduling
? applyDeferBlockStateWithSchedulingImpl!
: applyDeferBlockState;
try {
applyStateFn(newState, lDetails, lContainer, tNode, hostLView);
} catch (error: unknown) {
@ -509,8 +619,10 @@ export function renderDeferBlockState(
* created based on the `OutletInjector`.
*/
export function isRouterOutletInjector(currentInjector: Injector): boolean {
return (currentInjector instanceof ChainedInjector) &&
(typeof (currentInjector.injector as any).__ngOutletInjector === 'function');
return (
currentInjector instanceof ChainedInjector &&
typeof (currentInjector.injector as any).__ngOutletInjector === 'function'
);
}
/**
@ -523,18 +635,23 @@ export function isRouterOutletInjector(currentInjector: Injector): boolean {
* for a newly created `OutletInjector` instance.
*/
function createRouterOutletInjector(
parentOutletInjector: ChainedInjector, parentInjector: Injector) {
parentOutletInjector: ChainedInjector,
parentInjector: Injector,
) {
const outletInjector = parentOutletInjector.injector as any;
return outletInjector.__ngOutletInjector(parentInjector);
}
/**
* Applies changes to the DOM to reflect a given state.
*/
function applyDeferBlockState(
newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer, tNode: TNode,
hostLView: LView<unknown>) {
newState: DeferBlockState,
lDetails: LDeferBlockDetails,
lContainer: LContainer,
tNode: TNode,
hostLView: LView<unknown>,
) {
const stateTmplIndex = getTemplateIndexForState(newState, hostLView, tNode);
if (stateTmplIndex !== null) {
@ -549,7 +666,7 @@ function applyDeferBlockState(
removeLViewFromLContainer(lContainer, viewIndex);
let injector: Injector|undefined;
let injector: Injector | undefined;
if (newState === DeferBlockState.Complete) {
// When we render a defer block in completed state, there might be
// newly loaded standalone components used within the block, which may
@ -568,13 +685,18 @@ function applyDeferBlockState(
// with the `EnvironmentInjector` in Router's code, this special
// handling can be removed.
const isParentOutletInjector = isRouterOutletInjector(parentInjector);
const parentEnvInjector =
isParentOutletInjector ? parentInjector : parentInjector.get(EnvironmentInjector);
const parentEnvInjector = isParentOutletInjector
? parentInjector
: parentInjector.get(EnvironmentInjector);
injector = parentEnvInjector.get(CachedInjectorService)
.getOrCreateInjector(
tDetails, parentEnvInjector as EnvironmentInjector, providers,
ngDevMode ? 'DeferBlock Injector' : '');
injector = parentEnvInjector
.get(CachedInjectorService)
.getOrCreateInjector(
tDetails,
parentEnvInjector as EnvironmentInjector,
providers,
ngDevMode ? 'DeferBlock Injector' : '',
);
// Note: this is a continuation of the special case for Router's `OutletInjector`.
// Since the `OutletInjector` handles `ActivatedRoute` and `ChildrenOutletContexts`
@ -587,10 +709,16 @@ function applyDeferBlockState(
}
}
const dehydratedView = findMatchingDehydratedView(lContainer, activeBlockTNode.tView!.ssrId);
const embeddedLView =
createAndRenderEmbeddedLView(hostLView, activeBlockTNode, null, {dehydratedView, injector});
const embeddedLView = createAndRenderEmbeddedLView(hostLView, activeBlockTNode, null, {
dehydratedView,
injector,
});
addLViewToLContainer(
lContainer, embeddedLView, viewIndex, shouldAddViewToDom(activeBlockTNode, dehydratedView));
lContainer,
embeddedLView,
viewIndex,
shouldAddViewToDom(activeBlockTNode, dehydratedView),
);
markViewDirty(embeddedLView, NotificationSource.DeferBlockStateUpdate);
}
}
@ -602,8 +730,12 @@ function applyDeferBlockState(
* `@placeholder` blocks.
*/
function applyDeferBlockStateWithScheduling(
newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer, tNode: TNode,
hostLView: LView<unknown>) {
newState: DeferBlockState,
lDetails: LDeferBlockDetails,
lContainer: LContainer,
tNode: TNode,
hostLView: LView<unknown>,
) {
const now = Date.now();
const hostTView = hostLView[TVIEW];
const tDetails = getTDeferBlockDetails(hostTView, tNode);
@ -617,8 +749,13 @@ function applyDeferBlockStateWithScheduling(
// Trying to render loading, but it has an `after` config,
// so schedule an update action after a timeout.
lDetails[NEXT_DEFER_BLOCK_STATE] = newState;
const cleanupFn =
scheduleDeferBlockUpdate(loadingAfter, lDetails, tNode, lContainer, hostLView);
const cleanupFn = scheduleDeferBlockUpdate(
loadingAfter,
lDetails,
tNode,
lContainer,
hostLView,
);
lDetails[LOADING_AFTER_CLEANUP_FN] = cleanupFn;
} else {
// If we transition to a complete or an error state and there is a pending
@ -650,8 +787,12 @@ function applyDeferBlockStateWithScheduling(
* Schedules an update operation after a specified timeout.
*/
function scheduleDeferBlockUpdate(
timeout: number, lDetails: LDeferBlockDetails, tNode: TNode, lContainer: LContainer,
hostLView: LView<unknown>): VoidFunction {
timeout: number,
lDetails: LDeferBlockDetails,
tNode: TNode,
lContainer: LContainer,
hostLView: LView<unknown>,
): VoidFunction {
const callback = () => {
const nextState = lDetails[NEXT_DEFER_BLOCK_STATE];
lDetails[STATE_IS_FROZEN_UNTIL] = null;
@ -673,7 +814,9 @@ function scheduleDeferBlockUpdate(
* or an error state (represented as `3`).
*/
function isValidStateChange(
currentState: DeferBlockState|DeferBlockInternalState, newState: DeferBlockState): boolean {
currentState: DeferBlockState | DeferBlockInternalState,
newState: DeferBlockState,
): boolean {
return currentState < newState;
}
@ -696,7 +839,10 @@ export function triggerPrefetching(tDetails: TDeferBlockDetails, lView: LView, t
* @param lView LView of a host view.
*/
export function triggerResourceLoading(
tDetails: TDeferBlockDetails, lView: LView, tNode: TNode): Promise<unknown> {
tDetails: TDeferBlockDetails,
lView: LView,
tNode: TNode,
): Promise<unknown> {
const injector = lView[INJECTOR]!;
const tView = lView[TVIEW];
@ -720,8 +866,9 @@ export function triggerResourceLoading(
if (ngDevMode) {
// Check if dependency function interceptor is configured.
const deferDependencyInterceptor =
injector.get(DEFER_BLOCK_DEPENDENCY_INTERCEPTOR, null, {optional: true});
const deferDependencyInterceptor = injector.get(DEFER_BLOCK_DEPENDENCY_INTERCEPTOR, null, {
optional: true,
});
if (deferDependencyInterceptor) {
dependenciesFn = deferDependencyInterceptor.intercept(dependenciesFn);
@ -745,7 +892,7 @@ export function triggerResourceLoading(
}
// Start downloading of defer block dependencies.
tDetails.loadingPromise = Promise.allSettled(dependenciesFn()).then(results => {
tDetails.loadingPromise = Promise.allSettled(dependenciesFn()).then((results) => {
let failed = false;
const directiveDefs: DirectiveDefList = [];
const pipeDefs: PipeDefList = [];
@ -779,11 +926,12 @@ export function triggerResourceLoading(
if (tDetails.errorTmplIndex === null) {
const templateLocation = getTemplateLocationDetails(lView);
const error = new RuntimeError(
RuntimeErrorCode.DEFER_LOADING_FAILED,
ngDevMode &&
'Loading dependencies for `@defer` block failed, ' +
`but no \`@error\` block was configured${templateLocation}. ` +
'Consider using the `@error` block to render an error state.');
RuntimeErrorCode.DEFER_LOADING_FAILED,
ngDevMode &&
'Loading dependencies for `@defer` block failed, ' +
`but no \`@error\` block was configured${templateLocation}. ` +
'Consider using the `@error` block to render an error state.',
);
handleError(lView, error);
}
} else {
@ -792,18 +940,22 @@ export function triggerResourceLoading(
// Update directive and pipe registries to add newly downloaded dependencies.
const primaryBlockTView = primaryBlockTNode.tView!;
if (directiveDefs.length > 0) {
primaryBlockTView.directiveRegistry =
addDepsToRegistry<DirectiveDefList>(primaryBlockTView.directiveRegistry, directiveDefs);
primaryBlockTView.directiveRegistry = addDepsToRegistry<DirectiveDefList>(
primaryBlockTView.directiveRegistry,
directiveDefs,
);
// Extract providers from all NgModules imported by standalone components
// used within this defer block.
const directiveTypes = directiveDefs.map(def => def.type);
const directiveTypes = directiveDefs.map((def) => def.type);
const providers = internalImportProvidersFrom(false, ...directiveTypes);
tDetails.providers = providers;
}
if (pipeDefs.length > 0) {
primaryBlockTView.pipeRegistry =
addDepsToRegistry<PipeDefList>(primaryBlockTView.pipeRegistry, pipeDefs);
primaryBlockTView.pipeRegistry = addDepsToRegistry<PipeDefList>(
primaryBlockTView.pipeRegistry,
pipeDefs,
);
}
}
});
@ -826,10 +978,12 @@ function renderPlaceholder(lView: LView, tNode: TNode) {
* @param tNode Represents defer block info shared across all instances.
*/
function renderDeferStateAfterResourceLoading(
tDetails: TDeferBlockDetails, tNode: TNode, lContainer: LContainer) {
tDetails: TDeferBlockDetails,
tNode: TNode,
lContainer: LContainer,
) {
ngDevMode &&
assertDefined(
tDetails.loadingPromise, 'Expected loading Promise to exist on this defer block');
assertDefined(tDetails.loadingPromise, 'Expected loading Promise to exist on this defer block');
tDetails.loadingPromise!.then(() => {
if (tDetails.loadingState === DeferDependenciesLoadingState.COMPLETE) {
@ -837,7 +991,6 @@ function renderDeferStateAfterResourceLoading(
// Everything is loaded, show the primary block content
renderDeferBlockState(DeferBlockState.Complete, tNode, lContainer);
} else if (tDetails.loadingState === DeferDependenciesLoadingState.FAILED) {
renderDeferBlockState(DeferBlockState.Error, tNode, lContainer);
}
@ -869,8 +1022,10 @@ function triggerDeferBlock(lView: LView, tNode: TNode) {
triggerResourceLoading(tDetails, lView, tNode);
// The `loadingState` might have changed to "loading".
if ((tDetails.loadingState as DeferDependenciesLoadingState) ===
DeferDependenciesLoadingState.IN_PROGRESS) {
if (
(tDetails.loadingState as DeferDependenciesLoadingState) ===
DeferDependenciesLoadingState.IN_PROGRESS
) {
renderDeferStateAfterResourceLoading(tDetails, tNode, lContainer);
}
break;

View file

@ -54,10 +54,10 @@ export const MINIMUM_SLOT = 0;
export const LOADING_AFTER_SLOT = 1;
/** Configuration object for a loading block as it is stored in the component constants. */
export type DeferredLoadingBlockConfig = [minimumTime: number|null, afterTime: number|null];
export type DeferredLoadingBlockConfig = [minimumTime: number | null, afterTime: number | null];
/** Configuration object for a placeholder block as it is stored in the component constants. */
export type DeferredPlaceholderBlockConfig = [minimumTime: number|null];
export type DeferredPlaceholderBlockConfig = [minimumTime: number | null];
/**
* Describes the data shared across all instances of a defer block.
@ -72,32 +72,32 @@ export interface TDeferBlockDetails {
/**
* Index in an LView and TData arrays where a template for the loading block can be found.
*/
loadingTmplIndex: number|null;
loadingTmplIndex: number | null;
/**
* Extra configuration parameters (such as `after` and `minimum`) for the loading block.
*/
loadingBlockConfig: DeferredLoadingBlockConfig|null;
loadingBlockConfig: DeferredLoadingBlockConfig | null;
/**
* Index in an LView and TData arrays where a template for the placeholder block can be found.
*/
placeholderTmplIndex: number|null;
placeholderTmplIndex: number | null;
/**
* Extra configuration parameters (such as `after` and `minimum`) for the placeholder block.
*/
placeholderBlockConfig: DeferredPlaceholderBlockConfig|null;
placeholderBlockConfig: DeferredPlaceholderBlockConfig | null;
/**
* Index in an LView and TData arrays where a template for the error block can be found.
*/
errorTmplIndex: number|null;
errorTmplIndex: number | null;
/**
* Compiler-generated function that loads all dependencies for a defer block.
*/
dependencyResolverFn: DependencyResolverFn|null;
dependencyResolverFn: DependencyResolverFn | null;
/**
* Keeps track of the current loading state of defer block dependencies.
@ -109,13 +109,13 @@ export interface TDeferBlockDetails {
* are multiple instances of a defer block (e.g. if it was used inside of an *ngFor),
* which all await the same set of dependencies.
*/
loadingPromise: Promise<unknown>|null;
loadingPromise: Promise<unknown> | null;
/**
* List of providers collected from all NgModules that were imported by
* standalone components used within this defer block.
*/
providers: Provider[]|null;
providers: Provider[] | null;
}
/**
@ -171,35 +171,35 @@ export interface LDeferBlockDetails extends Array<unknown> {
/**
* Currently rendered block state.
*/
[DEFER_BLOCK_STATE]: DeferBlockState|DeferBlockInternalState;
[DEFER_BLOCK_STATE]: DeferBlockState | DeferBlockInternalState;
/**
* Block state that was requested when another state was rendered.
*/
[NEXT_DEFER_BLOCK_STATE]: DeferBlockState|null;
[NEXT_DEFER_BLOCK_STATE]: DeferBlockState | null;
/**
* Timestamp indicating when the current state can be switched to
* the next one, in case teh current state has `minimum` parameter.
*/
[STATE_IS_FROZEN_UNTIL]: number|null;
[STATE_IS_FROZEN_UNTIL]: number | null;
/**
* Contains a reference to a cleanup function which cancels a timeout
* when Angular waits before rendering loading state. This is used when
* the loading block has the `after` parameter configured.
*/
[LOADING_AFTER_CLEANUP_FN]: VoidFunction|null;
[LOADING_AFTER_CLEANUP_FN]: VoidFunction | null;
/**
* List of cleanup functions for regular triggers.
*/
[TRIGGER_CLEANUP_FNS]: VoidFunction[]|null;
[TRIGGER_CLEANUP_FNS]: VoidFunction[] | null;
/**
* List of cleanup functions for prefetch triggers.
*/
[PREFETCH_TRIGGER_CLEANUP_FNS]: VoidFunction[]|null;
[PREFETCH_TRIGGER_CLEANUP_FNS]: VoidFunction[] | null;
}
/**
@ -240,7 +240,7 @@ export interface DeferBlockDependencyInterceptor {
/**
* Invoked for each defer block when dependency loading function is accessed.
*/
intercept(dependencyFn: DependencyResolverFn|null): DependencyResolverFn|null;
intercept(dependencyFn: DependencyResolverFn | null): DependencyResolverFn | null;
/**
* Allows to configure an interceptor function.

View file

@ -43,23 +43,23 @@ export class TimerScheduler {
executingCallbacks = false;
// Currently scheduled `setTimeout` id.
timeoutId: number|null = null;
timeoutId: number | null = null;
// When currently scheduled timer would fire.
invokeTimerAt: number|null = null;
invokeTimerAt: number | null = null;
// List of callbacks to be invoked.
// For each callback we also store a timestamp on when the callback
// should be invoked. We store timestamps and callback functions
// in a flat array to avoid creating new objects for each entry.
// [timestamp1, callback1, timestamp2, callback2, ...]
current: Array<number|VoidFunction> = [];
current: Array<number | VoidFunction> = [];
// List of callbacks collected while invoking current set of callbacks.
// Those callbacks are added to the "current" queue at the end of
// the current callback invocation. The shape of this list is the same
// as the shape of the `current` list.
deferred: Array<number|VoidFunction> = [];
deferred: Array<number | VoidFunction> = [];
add(delay: number, callback: VoidFunction) {
const target = this.executingCallbacks ? this.deferred : this.current;
@ -81,7 +81,11 @@ export class TimerScheduler {
}
}
private addToQueue(target: Array<number|VoidFunction>, invokeAt: number, callback: VoidFunction) {
private addToQueue(
target: Array<number | VoidFunction>,
invokeAt: number,
callback: VoidFunction,
) {
let insertAtIndex = target.length;
for (let i = 0; i < target.length; i += 2) {
const invokeQueuedCallbackAt = target[i] as number;
@ -97,7 +101,7 @@ export class TimerScheduler {
arrayInsert2(target, insertAtIndex, invokeAt, callback);
}
private removeFromQueue(target: Array<number|VoidFunction>, callback: VoidFunction) {
private removeFromQueue(target: Array<number | VoidFunction>, callback: VoidFunction) {
let index = -1;
for (let i = 0; i < target.length; i += 2) {
const queuedCallback = target[i + 1];
@ -174,18 +178,20 @@ export class TimerScheduler {
// average frame duration. This is needed for better
// batching and to avoid kicking off excessive change
// detection cycles.
const FRAME_DURATION_MS = 16; // 1000ms / 60fps
const FRAME_DURATION_MS = 16; // 1000ms / 60fps
if (this.current.length > 0) {
const now = Date.now();
// First element in the queue points at the timestamp
// of the first (earliest) event.
const invokeAt = this.current[0] as number;
if (this.timeoutId === null ||
// Reschedule a timer in case a queue contains an item with
// an earlier timestamp and the delta is more than an average
// frame duration.
(this.invokeTimerAt && (this.invokeTimerAt - invokeAt > FRAME_DURATION_MS))) {
if (
this.timeoutId === null ||
// Reschedule a timer in case a queue contains an item with
// an earlier timestamp and the delta is more than an average
// frame duration.
(this.invokeTimerAt && this.invokeTimerAt - invokeAt > FRAME_DURATION_MS)
) {
// There was a timeout already, but an earlier event was added
// into the queue. In this case we drop an old timer and setup
// a new one with an updated (smaller) timeout.

Some files were not shown because too many files have changed in this diff Show more