mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor: migrate core to prettier formatting (#55488)
Migrate formatting to prettier for core from clang-format PR Close #55488
This commit is contained in:
parent
be17de53d4
commit
31fdf0fbea
581 changed files with 52758 additions and 41450 deletions
|
|
@ -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}',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe('takeUntilDestroyed', () => {
|
|||
},
|
||||
complete() {
|
||||
completed = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
source$.next(1);
|
||||
|
|
@ -52,7 +52,7 @@ describe('takeUntilDestroyed', () => {
|
|||
},
|
||||
complete() {
|
||||
completed = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
source$.next(1);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(() => {`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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>`',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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: []},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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]>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,5 +46,5 @@ export interface OutputRef<T> {
|
|||
*
|
||||
* @internal
|
||||
*/
|
||||
destroyRef: DestroyRef|undefined;
|
||||
destroyRef: DestroyRef | undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)!;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' : '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.)',
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] = [];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue