Add comprehensive library expansion with new components and demos

- Add new libraries: ui-accessibility, ui-animations, ui-backgrounds, ui-code-display, ui-data-utils, ui-font-manager, hcl-studio
- Add extensive layout components: gallery-grid, infinite-scroll-container, kanban-board, masonry, split-view, sticky-layout
- Add comprehensive demo components for all new features
- Update project configuration and dependencies
- Expand component exports and routing structure
- Add UI landing pages planning document

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
skyai_dev
2025-09-05 05:37:37 +10:00
parent 876eb301a0
commit 5346d6d0c9
5476 changed files with 350855 additions and 10 deletions

21
projects/ui-code-display/node_modules/@angular/core/LICENSE generated vendored Executable file
View File

@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2010-2025 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,8 @@
Angular
=======
The sources for this package are in the main [Angular](https://github.com/angular/angular) repo. Please file issues and pull requests against that repo.
Usage information and reference details can be found in [Angular documentation](https://angular.dev/overview).
License: MIT

View File

@@ -0,0 +1 @@
(()=>{function p(t,n,r,o,e,i,f,m){return{eventType:t,event:n,targetElement:r,eic:o,timeStamp:e,eia:i,eirp:f,eiack:m}}function u(t){let n=[],r=e=>{n.push(e)};return{c:t,q:n,et:[],etc:[],d:r,h:e=>{r(p(e.type,e,e.target,t,Date.now()))}}}function s(t,n,r){for(let o=0;o<n.length;o++){let e=n[o];(r?t.etc:t.et).push(e),t.c.addEventListener(e,t.h,r)}}function c(t,n,r,o,e=window){let i=u(t);e._ejsas||(e._ejsas={}),e._ejsas[n]=i,s(i,r),s(i,o,!0)}window.__jsaction_bootstrap=c;})();

View File

@@ -0,0 +1,395 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
declare global {
/**
* Values of ngDevMode
* Depending on the current state of the application, ngDevMode may have one of several values.
*
* For convenience, the “truthy” value which enables dev mode is also an object which contains
* Angulars performance counters. This is not necessary, but cuts down on boilerplate for the
* perf counters.
*
* ngDevMode may also be set to false. This can happen in one of a few ways:
* - The user explicitly sets `window.ngDevMode = false` somewhere in their app.
* - The user calls `enableProdMode()`.
* - The URL contains a `ngDevMode=false` text.
* Finally, ngDevMode may not have been defined at all.
*/
const ngDevMode: null | NgDevModePerfCounters;
interface NgDevModePerfCounters {
namedConstructors: boolean;
firstCreatePass: number;
tNode: number;
tView: number;
rendererCreateTextNode: number;
rendererSetText: number;
rendererCreateElement: number;
rendererAddEventListener: number;
rendererSetAttribute: number;
rendererRemoveAttribute: number;
rendererSetProperty: number;
rendererSetClassName: number;
rendererAddClass: number;
rendererRemoveClass: number;
rendererSetStyle: number;
rendererRemoveStyle: number;
rendererDestroy: number;
rendererDestroyNode: number;
rendererMoveNode: number;
rendererRemoveNode: number;
rendererAppendChild: number;
rendererInsertBefore: number;
rendererCreateComment: number;
hydratedNodes: number;
hydratedComponents: number;
dehydratedViewsRemoved: number;
dehydratedViewsCleanupRuns: number;
componentsSkippedHydration: number;
deferBlocksWithIncrementalHydration: number;
}
}
/**
* Records information about the action that should handle a given `Event`.
*/
interface ActionInfo {
name: string;
element: Element;
}
type ActionInfoInternal = [name: string, element: Element];
/**
* Records information for later handling of events. This type is
* shared, and instances of it are passed, between the eventcontract
* and the dispatcher jsbinary. Therefore, the fields of this type are
* referenced by string literals rather than property literals
* throughout the code.
*
* 'targetElement' is the element the action occurred on, 'actionElement'
* is the element that has the jsaction handler.
*
* A null 'actionElement' identifies an EventInfo instance that didn't match a
* jsaction attribute. This allows us to execute global event handlers with the
* appropriate event type (including a11y clicks and custom events).
* The declare portion of this interface creates a set of externs that make sure
* renaming doesn't happen for EventInfo. This is important since EventInfo
* is shared across multiple binaries.
*/
declare interface EventInfo {
eventType: string;
event: Event;
targetElement: Element;
/** The element that is the container for this Event. */
eic: Element;
timeStamp: number;
/**
* The action parsed from the JSAction element.
*/
eia?: ActionInfoInternal;
/**
* Whether this `Event` is a replay event, meaning no dispatcher was
* installed when this `Event` was originally dispatched.
*/
eirp?: boolean;
/**
* Whether this `Event` represents a `keydown` event that should be processed
* as a `click`. Only used when a11y click events is on.
*/
eiack?: boolean;
/** Whether action resolution has already run on this `EventInfo`. */
eir?: boolean;
}
/**
* Utility class around an `EventInfo`.
*
* This should be used in compilation units that are less sensitive to code
* size.
*/
declare class EventInfoWrapper {
readonly eventInfo: EventInfo;
constructor(eventInfo: EventInfo);
getEventType(): string;
setEventType(eventType: string): void;
getEvent(): Event;
setEvent(event: Event): void;
getTargetElement(): Element;
setTargetElement(targetElement: Element): void;
getContainer(): Element;
setContainer(container: Element): void;
getTimestamp(): number;
setTimestamp(timestamp: number): void;
getAction(): {
name: string;
element: Element;
} | undefined;
setAction(action: ActionInfo | undefined): void;
getIsReplay(): boolean | undefined;
setIsReplay(replay: boolean): void;
getResolved(): boolean | undefined;
setResolved(resolved: boolean): void;
clone(): EventInfoWrapper;
}
declare interface EarlyJsactionDataContainer {
_ejsa?: EarlyJsactionData;
_ejsas?: {
[appId: string]: EarlyJsactionData | undefined;
};
}
declare global {
interface Window {
_ejsa?: EarlyJsactionData;
_ejsas?: {
[appId: string]: EarlyJsactionData | undefined;
};
}
}
/**
* Defines the early jsaction data types.
*/
declare interface EarlyJsactionData {
/** List used to keep track of the early JSAction event types. */
et: string[];
/** List used to keep track of the early JSAction capture event types. */
etc: string[];
/** Early JSAction handler for all events. */
h: (event: Event) => void;
/** Dispatcher handler. Initializes to populating `q`. */
d: (eventInfo: EventInfo) => void;
/** List used to push `EventInfo` objects if the dispatcher is not registered. */
q: EventInfo[];
/** Container for listening to events. */
c: HTMLElement;
}
/**
* An `EventContractContainerManager` provides the common interface for managing
* containers.
*/
interface EventContractContainerManager {
addEventListener(eventType: string, getHandler: (element: Element) => (event: Event) => void, passive?: boolean): void;
cleanUp(): void;
}
/**
* A class representing a container node and all the event handlers
* installed on it. Used so that handlers can be cleaned up if the
* container is removed from the contract.
*/
declare class EventContractContainer implements EventContractContainerManager {
readonly element: Element;
/**
* Array of event handlers and their corresponding event types that are
* installed on this container.
*
*/
private handlerInfos;
/**
* @param element The container Element.
*/
constructor(element: Element);
/**
* Installs the provided installer on the element owned by this container,
* and maintains a reference to resulting handler in order to remove it
* later if desired.
*/
addEventListener(eventType: string, getHandler: (element: Element) => (event: Event) => void, passive?: boolean): void;
/**
* Removes all the handlers installed on this container.
*/
cleanUp(): void;
}
/**
* @fileoverview An enum to control who can call certain jsaction APIs.
*/
declare enum Restriction {
I_AM_THE_JSACTION_FRAMEWORK = 0
}
/**
* @fileoverview Implements the local event handling contract. This
* allows DOM objects in a container that enters into this contract to
* define event handlers which are executed in a local context.
*
* One EventContract instance can manage the contract for multiple
* containers, which are added using the addContainer() method.
*
* Events can be registered using the addEvent() method.
*
* A Dispatcher is added using the registerDispatcher() method. Until there is
* a dispatcher, events are queued. The idea is that the EventContract
* class is inlined in the HTML of the top level page and instantiated
* right after the start of <body>. The Dispatcher class is contained
* in the external deferred js, and instantiated and registered with
* EventContract when the external javascript in the page loads. The
* external javascript will also register the jsaction handlers, which
* then pick up the queued events at the time of registration.
*
* Since this class is meant to be inlined in the main page HTML, the
* size of the binary compiled from this file MUST be kept as small as
* possible and thus its dependencies to a minimum.
*/
/**
* The API of an EventContract that is safe to call from any compilation unit.
*/
declare interface UnrenamedEventContract {
ecrd(dispatcher: Dispatcher, restriction: Restriction): void;
}
/** A function that is called to handle events captured by the EventContract. */
type Dispatcher = (eventInfo: EventInfo, globalDispatch?: boolean) => void;
/**
* A function that handles an event dispatched from the browser.
*
* eventType: May differ from `event.type` if JSAction uses a
* short-hand name or is patching over an non-bubbling event with a bubbling
* variant.
* event: The native browser event.
* container: The container for this dispatch.
*/
type EventHandler = (eventType: string, event: Event, container: Element) => void;
/**
* EventContract intercepts events in the bubbling phase at the
* boundary of a container element, and maps them to generic actions
* which are specified using the custom jsaction attribute in
* HTML. Behavior of the application is then specified in terms of
* handler for such actions, cf. jsaction.Dispatcher in dispatcher.js.
*
* This has several benefits: (1) No DOM event handlers need to be
* registered on the specific elements in the UI. (2) The set of
* events that the application has to handle can be specified in terms
* of the semantics of the application, rather than in terms of DOM
* events. (3) Invocation of handlers can be delayed and handlers can
* be delay loaded in a generic way.
*/
declare class EventContract implements UnrenamedEventContract {
static MOUSE_SPECIAL_SUPPORT: boolean;
private containerManager;
/**
* The DOM events which this contract covers. Used to prevent double
* registration of event types. The value of the map is the
* internally created DOM event handler function that handles the
* DOM events. See addEvent().
*
*/
private eventHandlers;
private browserEventTypeToExtraEventTypes;
/**
* The dispatcher function. Events are passed to this function for
* handling once it was set using the registerDispatcher() method. This is
* done because the function is passed from another jsbinary, so passing the
* instance and invoking the method here would require to leave the method
* unobfuscated.
*/
private dispatcher;
/**
* The list of suspended `EventInfo` that will be dispatched
* as soon as the `Dispatcher` is registered.
*/
private queuedEventInfos;
constructor(containerManager: EventContractContainerManager);
private handleEvent;
/**
* Handle an `EventInfo`.
*/
private handleEventInfo;
/**
* Enables jsaction handlers to be called for the event type given by
* name.
*
* If the event is already registered, this does nothing.
*
* @param prefixedEventType If supplied, this event is used in
* the actual browser event registration instead of the name that is
* exposed to jsaction. Use this if you e.g. want users to be able
* to subscribe to jsaction="transitionEnd:foo" while the underlying
* event is webkitTransitionEnd in one browser and mozTransitionEnd
* in another.
*
* @param passive A boolean value that, if `true`, indicates that the event
* handler will never call `preventDefault()`.
*/
addEvent(eventType: string, prefixedEventType?: string, passive?: boolean): void;
/**
* Gets the queued early events and replay them using the appropriate handler
* in the provided event contract. Once all the events are replayed, it cleans
* up the early contract.
*/
replayEarlyEvents(earlyJsactionData?: EarlyJsactionData | undefined): void;
/**
* Replays all the early `EventInfo` objects, dispatching them through the normal
* `EventContract` flow.
*/
replayEarlyEventInfos(earlyEventInfos: EventInfo[]): void;
/**
* Returns all JSAction event types that have been registered for a given
* browser event type.
*/
private getEventTypesForBrowserEventType;
/**
* Returns the event handler function for a given event type.
*/
handler(eventType: string): EventHandler | undefined;
/**
* Cleans up the event contract. This resets all of the `EventContract`'s
* internal state. Users are responsible for not using this `EventContract`
* after it has been cleaned up.
*/
cleanUp(): void;
/**
* Register a dispatcher function. Event info of each event mapped to
* a jsaction is passed for handling to this callback. The queued
* events are passed as well to the dispatcher for later replaying
* once the dispatcher is registered. Clears the event queue to null.
*
* @param dispatcher The dispatcher function.
* @param restriction
*/
registerDispatcher(dispatcher: Dispatcher, restriction: Restriction): void;
/**
* Unrenamed alias for registerDispatcher. Necessary for any codebases that
* split the `EventContract` and `Dispatcher` code into different compilation
* units.
*/
ecrd(dispatcher: Dispatcher, restriction: Restriction): void;
}
/** An internal symbol used to indicate whether propagation should be stopped or not. */
declare const PROPAGATION_STOPPED_SYMBOL: unique symbol;
/** Extra event phases beyond what the browser provides. */
declare const EventPhase: {
REPLAY: number;
};
declare global {
interface Event {
[PROPAGATION_STOPPED_SYMBOL]?: boolean;
}
}
/**
* A dispatcher that uses browser-based `Event` semantics, for example bubbling, `stopPropagation`,
* `currentTarget`, etc.
*/
declare class EventDispatcher {
private readonly dispatchDelegate;
private readonly clickModSupport;
private readonly actionResolver;
private readonly dispatcher;
constructor(dispatchDelegate: (event: Event, actionName: string) => void, clickModSupport?: boolean);
/**
* The entrypoint for the `EventContract` dispatch.
*/
dispatch(eventInfo: EventInfo): void;
/** Internal method that does basic disaptching. */
private dispatchToDelegate;
}
/**
* Registers deferred functionality for an EventContract and a Jsaction
* Dispatcher.
*/
declare function registerDispatcher(eventContract: UnrenamedEventContract, dispatcher: EventDispatcher): void;
export { EventContract, EventContractContainer, EventDispatcher, EventInfoWrapper, EventPhase, Restriction, registerDispatcher };
export type { EarlyJsactionDataContainer, EventInfo };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,45 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
/**
* Current injector value used by `inject`.
* - `undefined`: it is an error to call `inject`
* - `null`: `inject` can be called but there is no injector (limp-mode).
* - Injector instance: Use the injector for resolution.
*/
let _currentInjector = undefined;
function getCurrentInjector() {
return _currentInjector;
}
function setCurrentInjector(injector) {
const former = _currentInjector;
_currentInjector = injector;
return former;
}
/**
* Value returned if the key-value pair couldn't be found in the context
* hierarchy.
*/
const NOT_FOUND = Symbol('NotFound');
/**
* Error thrown when the key-value pair couldn't be found in the context
* hierarchy. Context can be attached below.
*/
class NotFoundError extends Error {
constructor(message) {
super(message);
}
}
/**
* Type guard for checking if an unknown value is a NotFound.
*/
function isNotFound(e) {
return e === NOT_FOUND || e instanceof NotFoundError;
}
export { NOT_FOUND, NotFoundError, getCurrentInjector, isNotFound, setCurrentInjector };
//# sourceMappingURL=di.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"di.mjs","sources":["../../../../../../darwin_arm64-fastbuild-ST-2d99d9656325/bin/packages/core/primitives/di/src/injector.ts","../../../../../../darwin_arm64-fastbuild-ST-2d99d9656325/bin/packages/core/primitives/di/src/not_found.ts"],"sourcesContent":["/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {InjectionToken} from './injection_token';\nimport {NotFound} from './not_found';\n\nexport interface Injector {\n retrieve<T>(token: InjectionToken<T>, options?: unknown): T | NotFound;\n}\n\n/**\n * Current injector value used by `inject`.\n * - `undefined`: it is an error to call `inject`\n * - `null`: `inject` can be called but there is no injector (limp-mode).\n * - Injector instance: Use the injector for resolution.\n */\nlet _currentInjector: Injector | undefined | null = undefined;\n\nexport function getCurrentInjector(): Injector | undefined | null {\n return _currentInjector;\n}\n\nexport function setCurrentInjector(\n injector: Injector | null | undefined,\n): Injector | undefined | null {\n const former = _currentInjector;\n _currentInjector = injector;\n return former;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\n/**\n * Value returned if the key-value pair couldn't be found in the context\n * hierarchy.\n */\nexport const NOT_FOUND: unique symbol = Symbol('NotFound');\n\n/**\n * Error thrown when the key-value pair couldn't be found in the context\n * hierarchy. Context can be attached below.\n */\nexport class NotFoundError extends Error {\n constructor(message: string) {\n super(message);\n }\n}\n\n/**\n * Type guard for checking if an unknown value is a NotFound.\n */\nexport function isNotFound(e: unknown): e is NotFound {\n return e === NOT_FOUND || e instanceof NotFoundError;\n}\n\n/**\n * Type union of NotFound and NotFoundError.\n */\nexport type NotFound = typeof NOT_FOUND | NotFoundError;\n"],"names":[],"mappings":";;;;;;AAeA;;;;;AAKG;AACH,IAAI,gBAAgB,GAAgC,SAAS;SAE7C,kBAAkB,GAAA;AAChC,IAAA,OAAO,gBAAgB;AACzB;AAEM,SAAU,kBAAkB,CAChC,QAAqC,EAAA;IAErC,MAAM,MAAM,GAAG,gBAAgB;IAC/B,gBAAgB,GAAG,QAAQ;AAC3B,IAAA,OAAO,MAAM;AACf;;ACzBA;;;AAGG;MACU,SAAS,GAAkB,MAAM,CAAC,UAAU;AAEzD;;;AAGG;AACG,MAAO,aAAc,SAAQ,KAAK,CAAA;AACtC,IAAA,WAAA,CAAY,OAAe,EAAA;QACzB,KAAK,CAAC,OAAO,CAAC;;AAEjB;AAED;;AAEG;AACG,SAAU,UAAU,CAAC,CAAU,EAAA;AACnC,IAAA,OAAO,CAAC,KAAK,SAAS,IAAI,CAAC,YAAY,aAAa;AACtD;;;;"}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,85 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
import { consumerMarkDirty, SIGNAL, consumerDestroy, isInNotificationPhase, consumerPollProducersForChange, consumerBeforeComputation, consumerAfterComputation, REACTIVE_NODE } from '../untracked-BKcld_ew.mjs';
export { SIGNAL_NODE, createComputed, createLinkedSignal, createSignal, defaultEquals, getActiveConsumer, isReactive, linkedSignalSetFn, linkedSignalUpdateFn, producerAccessed, producerIncrementEpoch, producerMarkClean, producerNotifyConsumers, producerUpdateValueVersion, producerUpdatesAllowed, runPostSignalSetFn, setActiveConsumer, setAlternateWeakRefImpl, setPostSignalSetFn, setThrowInvalidWriteToSignalError, signalSetFn, signalUpdateFn, untracked } from '../untracked-BKcld_ew.mjs';
function createWatch(fn, schedule, allowSignalWrites) {
const node = Object.create(WATCH_NODE);
if (allowSignalWrites) {
node.consumerAllowSignalWrites = true;
}
node.fn = fn;
node.schedule = schedule;
const registerOnCleanup = (cleanupFn) => {
node.cleanupFn = cleanupFn;
};
function isWatchNodeDestroyed(node) {
return node.fn === null && node.schedule === null;
}
function destroyWatchNode(node) {
if (!isWatchNodeDestroyed(node)) {
consumerDestroy(node); // disconnect watcher from the reactive graph
node.cleanupFn();
// nullify references to the integration functions to mark node as destroyed
node.fn = null;
node.schedule = null;
node.cleanupFn = NOOP_CLEANUP_FN;
}
}
const run = () => {
if (node.fn === null) {
// trying to run a destroyed watch is noop
return;
}
if (isInNotificationPhase()) {
throw new Error(`Schedulers cannot synchronously execute watches while scheduling.`);
}
node.dirty = false;
if (node.hasRun && !consumerPollProducersForChange(node)) {
return;
}
node.hasRun = true;
const prevConsumer = consumerBeforeComputation(node);
try {
node.cleanupFn();
node.cleanupFn = NOOP_CLEANUP_FN;
node.fn(registerOnCleanup);
}
finally {
consumerAfterComputation(node, prevConsumer);
}
};
node.ref = {
notify: () => consumerMarkDirty(node),
run,
cleanup: () => node.cleanupFn(),
destroy: () => destroyWatchNode(node),
[SIGNAL]: node,
};
return node.ref;
}
const NOOP_CLEANUP_FN = () => { };
// Note: Using an IIFE here to ensure that the spread assignment is not considered
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
const WATCH_NODE = /* @__PURE__ */ (() => {
return {
...REACTIVE_NODE,
consumerIsAlwaysLive: true,
consumerAllowSignalWrites: false,
consumerMarkedDirty: (node) => {
if (node.schedule !== null) {
node.schedule(node.ref);
}
},
hasRun: false,
cleanupFn: NOOP_CLEANUP_FN,
};
})();
export { REACTIVE_NODE, SIGNAL, consumerAfterComputation, consumerBeforeComputation, consumerDestroy, consumerMarkDirty, consumerPollProducersForChange, createWatch, isInNotificationPhase };
//# sourceMappingURL=signals.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,351 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
import { assertInInjectionContext, inject, DestroyRef, ɵRuntimeError as _RuntimeError, ɵgetOutputDestroyRef as _getOutputDestroyRef, Injector, effect, untracked, ɵmicrotaskEffect as _microtaskEffect, assertNotInReactiveContext, signal, computed, PendingTasks, resource } from '@angular/core';
import { Observable, ReplaySubject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
/**
* Operator which completes the Observable when the calling context (component, directive, service,
* etc) is destroyed.
*
* @param destroyRef optionally, the `DestroyRef` representing the current context. This can be
* passed explicitly to use `takeUntilDestroyed` outside of an [injection
* context](guide/di/dependency-injection-context). Otherwise, the current `DestroyRef` is injected.
*
* @publicApi
*/
function takeUntilDestroyed(destroyRef) {
if (!destroyRef) {
assertInInjectionContext(takeUntilDestroyed);
destroyRef = inject(DestroyRef);
}
const destroyed$ = new Observable((observer) => {
const unregisterFn = destroyRef.onDestroy(observer.next.bind(observer));
return unregisterFn;
});
return (source) => {
return source.pipe(takeUntil(destroyed$));
};
}
/**
* Implementation of `OutputRef` that emits values from
* an RxJS observable source.
*
* @internal
*/
class OutputFromObservableRef {
source;
destroyed = false;
destroyRef = inject(DestroyRef);
constructor(source) {
this.source = source;
this.destroyRef.onDestroy(() => {
this.destroyed = true;
});
}
subscribe(callbackFn) {
if (this.destroyed) {
throw new _RuntimeError(953 /* ɵ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),
});
return {
unsubscribe: () => subscription.unsubscribe(),
};
}
}
/**
* Declares an Angular output that is using an RxJS observable as a source
* for events dispatched to parent subscribers.
*
* The behavior for an observable as source is defined as followed:
* 1. New values are forwarded to the Angular output (next notifications).
* 2. Errors notifications are not handled by Angular. You need to handle these manually.
* For example by using `catchError`.
* 3. Completion notifications stop the output from emitting new values.
*
* @usageNotes
* Initialize an output in your directive by declaring a
* class field and initializing it with the `outputFromObservable()` function.
*
* ```ts
* @Directive({..})
* export class MyDir {
* nameChange$ = <some-observable>;
* nameChange = outputFromObservable(this.nameChange$);
* }
* ```
*
* @publicApi
*/
function outputFromObservable(observable, opts) {
ngDevMode && assertInInjectionContext(outputFromObservable);
return new OutputFromObservableRef(observable);
}
/**
* Converts an Angular output declared via `output()` or `outputFromObservable()`
* to an observable.
*
* You can subscribe to the output via `Observable.subscribe` then.
*
* @publicApi
*/
function outputToObservable(ref) {
const destroyRef = _getOutputDestroyRef(ref);
return new Observable((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));
return () => subscription.unsubscribe();
});
}
/**
* Exposes the value of an Angular `Signal` as an RxJS `Observable`.
*
* The signal's value will be propagated into the `Observable`'s subscribers using an `effect`.
*
* `toObservable` must be called in an injection context unless an injector is provided via options.
*
* @developerPreview
*/
function toObservable(source, options) {
!options?.injector && assertInInjectionContext(toObservable);
const injector = options?.injector ?? inject(Injector);
const subject = new ReplaySubject(1);
const watcher = effect(() => {
let value;
try {
value = source();
}
catch (err) {
untracked(() => subject.error(err));
return;
}
untracked(() => subject.next(value));
}, { injector, manualCleanup: true });
injector.get(DestroyRef).onDestroy(() => {
watcher.destroy();
subject.complete();
});
return subject.asObservable();
}
function toObservableMicrotask(source, options) {
!options?.injector && assertInInjectionContext(toObservable);
const injector = options?.injector ?? inject(Injector);
const subject = new ReplaySubject(1);
const watcher = _microtaskEffect(() => {
let value;
try {
value = source();
}
catch (err) {
untracked(() => subject.error(err));
return;
}
untracked(() => subject.next(value));
}, { injector, manualCleanup: true });
injector.get(DestroyRef).onDestroy(() => {
watcher.destroy();
subject.complete();
});
return subject.asObservable();
}
/**
* Get the current value of an `Observable` as a reactive `Signal`.
*
* `toSignal` returns a `Signal` which provides synchronous reactive access to values produced
* by the given `Observable`, by subscribing to that `Observable`. The returned `Signal` will always
* have the most recent value emitted by the subscription, and will throw an error if the
* `Observable` errors.
*
* With `requireSync` set to `true`, `toSignal` will assert that the `Observable` produces a value
* immediately upon subscription. No `initialValue` is needed in this case, and the returned signal
* does not include an `undefined` type.
*
* By default, the subscription will be automatically cleaned up when the current [injection
* context](guide/di/dependency-injection-context) is destroyed. For example, when `toSignal` is
* called during the construction of a component, the subscription will be cleaned up when the
* component is destroyed. If an injection context is not available, an explicit `Injector` can be
* passed instead.
*
* If the subscription should persist until the `Observable` itself completes, the `manualCleanup`
* option can be specified instead, which disables the automatic subscription teardown. No injection
* context is needed in this configuration as well.
*
* @developerPreview
*/
function toSignal(source, options) {
typeof ngDevMode !== 'undefined' &&
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.');
const requiresCleanup = !options?.manualCleanup;
requiresCleanup && !options?.injector && assertInInjectionContext(toSignal);
const cleanupRef = requiresCleanup
? (options?.injector?.get(DestroyRef) ?? inject(DestroyRef))
: null;
const equal = makeToSignalEqual(options?.equal);
// 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;
if (options?.requireSync) {
// Initially the signal is in a `NoValue` state.
state = signal({ kind: 0 /* StateKind.NoValue */ }, { equal });
}
else {
// If an initial value was passed, use it. Otherwise, use `undefined` as the initial value.
state = signal({ kind: 1 /* StateKind.Value */, value: options?.initialValue }, { equal });
}
let destroyUnregisterFn;
// Note: This code cannot run inside a reactive context (see assertion above). If we'd support
// this, we would subscribe to the observable outside of the current reactive context, avoiding
// that side-effect signal reads/writes are attribute to the current consumer. The current
// consumer only needs to be notified when the `state` signal changes through the observable
// subscription. Additional context (related to async pipe):
// https://github.com/angular/angular/pull/50522.
const sub = source.subscribe({
next: (value) => state.set({ kind: 1 /* 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.
throw error;
}
state.set({ kind: 2 /* StateKind.Error */, error });
},
complete: () => {
destroyUnregisterFn?.();
},
// Completion of the Observable is meaningless to the signal. Signals don't have a concept of
// "complete".
});
if (options?.requireSync && state().kind === 0 /* StateKind.NoValue */) {
throw new _RuntimeError(601 /* ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT */, (typeof ngDevMode === 'undefined' || ngDevMode) &&
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.');
}
// Unsubscribe when the current context is destroyed, if requested.
destroyUnregisterFn = cleanupRef?.onDestroy(sub.unsubscribe.bind(sub));
// The actual returned signal is a `computed` of the `State` signal, which maps the various states
// to either values or errors.
return computed(() => {
const current = state();
switch (current.kind) {
case 1 /* StateKind.Value */:
return current.value;
case 2 /* StateKind.Error */:
throw current.error;
case 0 /* StateKind.NoValue */:
// This shouldn't really happen because the error is thrown on creation.
throw new _RuntimeError(601 /* ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT */, (typeof ngDevMode === 'undefined' || ngDevMode) &&
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.');
}
}, { equal: options?.equal });
}
function makeToSignalEqual(userEquality = Object.is) {
return (a, b) => a.kind === 1 /* StateKind.Value */ && b.kind === 1 /* StateKind.Value */ && userEquality(a.value, b.value);
}
/**
* Operator which makes the application unstable until the observable emits, completes, errors, or is unsubscribed.
*
* Use this operator in observables whose subscriptions are important for rendering and should be included in SSR serialization.
*
* @param injector The `Injector` to use during creation. If this is not provided, the current injection context will be used instead (via `inject`).
*
* @experimental
*/
function pendingUntilEvent(injector) {
if (injector === undefined) {
assertInInjectionContext(pendingUntilEvent);
injector = inject(Injector);
}
const taskService = injector.get(PendingTasks);
return (sourceObservable) => {
return new Observable((originalSubscriber) => {
// create a new task on subscription
const removeTask = taskService.add();
let cleanedUp = false;
function cleanupTask() {
if (cleanedUp) {
return;
}
removeTask();
cleanedUp = true;
}
const innerSubscription = sourceObservable.subscribe({
next: (v) => {
originalSubscriber.next(v);
cleanupTask();
},
complete: () => {
originalSubscriber.complete();
cleanupTask();
},
error: (e) => {
originalSubscriber.error(e);
cleanupTask();
},
});
innerSubscription.add(() => {
originalSubscriber.unsubscribe();
cleanupTask();
});
return innerSubscription;
});
};
}
function rxResource(opts) {
opts?.injector || assertInInjectionContext(rxResource);
return resource({
...opts,
loader: undefined,
stream: (params) => {
let sub;
// Track the abort listener so it can be removed if the Observable completes (as a memory
// optimization).
const onAbort = () => sub.unsubscribe();
params.abortSignal.addEventListener('abort', onAbort);
// Start off stream as undefined.
const stream = signal({ value: undefined });
let resolve;
const promise = new Promise((r) => (resolve = r));
function send(value) {
stream.set(value);
resolve?.(stream);
resolve = undefined;
}
sub = opts.loader(params).subscribe({
next: (value) => send({ value }),
error: (error) => {
send({ error });
params.abortSignal.removeEventListener('abort', onAbort);
},
complete: () => {
if (resolve) {
send({ error: new Error('Resource completed before producing a value') });
}
params.abortSignal.removeEventListener('abort', onAbort);
},
});
return promise;
},
});
}
export { outputFromObservable, outputToObservable, pendingUntilEvent, rxResource, takeUntilDestroyed, toObservable, toSignal, toObservableMicrotask as ɵtoObservableMicrotask };
//# sourceMappingURL=rxjs-interop.mjs.map

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,605 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
/**
* The default equality function used for `signal` and `computed`, which uses referential equality.
*/
function defaultEquals(a, b) {
return Object.is(a, b);
}
/**
* The currently active consumer `ReactiveNode`, if running code in a reactive context.
*
* Change this via `setActiveConsumer`.
*/
let activeConsumer = null;
let inNotificationPhase = false;
/**
* Global epoch counter. Incremented whenever a source signal is set.
*/
let epoch = 1;
/**
* Symbol used to tell `Signal`s apart from other functions.
*
* This can be used to auto-unwrap signals in various cases, or to auto-wrap non-signal values.
*/
const SIGNAL = /* @__PURE__ */ Symbol('SIGNAL');
function setActiveConsumer(consumer) {
const prev = activeConsumer;
activeConsumer = consumer;
return prev;
}
function getActiveConsumer() {
return activeConsumer;
}
function isInNotificationPhase() {
return inNotificationPhase;
}
function isReactive(value) {
return value[SIGNAL] !== undefined;
}
const REACTIVE_NODE = {
version: 0,
lastCleanEpoch: 0,
dirty: false,
producerNode: undefined,
producerLastReadVersion: undefined,
producerIndexOfThis: undefined,
nextProducerIndex: 0,
liveConsumerNode: undefined,
liveConsumerIndexOfThis: undefined,
consumerAllowSignalWrites: false,
consumerIsAlwaysLive: false,
kind: 'unknown',
producerMustRecompute: () => false,
producerRecomputeValue: () => { },
consumerMarkedDirty: () => { },
consumerOnSignalRead: () => { },
};
/**
* Called by implementations when a producer's signal is read.
*/
function producerAccessed(node) {
if (inNotificationPhase) {
throw new Error(typeof ngDevMode !== 'undefined' && ngDevMode
? `Assertion error: signal read during notification phase`
: '');
}
if (activeConsumer === null) {
// Accessed outside of a reactive context, so nothing to record.
return;
}
activeConsumer.consumerOnSignalRead(node);
// This producer is the `idx`th dependency of `activeConsumer`.
const idx = activeConsumer.nextProducerIndex++;
assertConsumerNode(activeConsumer);
if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) {
// There's been a change in producers since the last execution of `activeConsumer`.
// `activeConsumer.producerNode[idx]` holds a stale dependency which will be be removed and
// replaced with `this`.
//
// If `activeConsumer` isn't live, then this is a no-op, since we can replace the producer in
// `activeConsumer.producerNode` directly. However, if `activeConsumer` is live, then we need
// to remove it from the stale producer's `liveConsumer`s.
if (consumerIsLive(activeConsumer)) {
const staleProducer = activeConsumer.producerNode[idx];
producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]);
// At this point, the only record of `staleProducer` is the reference at
// `activeConsumer.producerNode[idx]` which will be overwritten below.
}
}
if (activeConsumer.producerNode[idx] !== node) {
// We're a new dependency of the consumer (at `idx`).
activeConsumer.producerNode[idx] = node;
// If the active consumer is live, then add it as a live consumer. If not, then use 0 as a
// placeholder value.
activeConsumer.producerIndexOfThis[idx] = consumerIsLive(activeConsumer)
? producerAddLiveConsumer(node, activeConsumer, idx)
: 0;
}
activeConsumer.producerLastReadVersion[idx] = node.version;
}
/**
* Increment the global epoch counter.
*
* Called by source producers (that is, not computeds) whenever their values change.
*/
function producerIncrementEpoch() {
epoch++;
}
/**
* Ensure this producer's `version` is up-to-date.
*/
function producerUpdateValueVersion(node) {
if (consumerIsLive(node) && !node.dirty) {
// A live consumer will be marked dirty by producers, so a clean state means that its version
// is guaranteed to be up-to-date.
return;
}
if (!node.dirty && node.lastCleanEpoch === epoch) {
// Even non-live consumers can skip polling if they previously found themselves to be clean at
// the current epoch, since their dependencies could not possibly have changed (such a change
// would've increased the epoch).
return;
}
if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) {
// None of our producers report a change since the last time they were read, so no
// recomputation of our value is necessary, and we can consider ourselves clean.
producerMarkClean(node);
return;
}
node.producerRecomputeValue(node);
// After recomputing the value, we're no longer dirty.
producerMarkClean(node);
}
/**
* Propagate a dirty notification to live consumers of this producer.
*/
function producerNotifyConsumers(node) {
if (node.liveConsumerNode === undefined) {
return;
}
// Prevent signal reads when we're updating the graph
const prev = inNotificationPhase;
inNotificationPhase = true;
try {
for (const consumer of node.liveConsumerNode) {
if (!consumer.dirty) {
consumerMarkDirty(consumer);
}
}
}
finally {
inNotificationPhase = prev;
}
}
/**
* Whether this `ReactiveNode` in its producer capacity is currently allowed to initiate updates,
* based on the current consumer context.
*/
function producerUpdatesAllowed() {
return activeConsumer?.consumerAllowSignalWrites !== false;
}
function consumerMarkDirty(node) {
node.dirty = true;
producerNotifyConsumers(node);
node.consumerMarkedDirty?.(node);
}
function producerMarkClean(node) {
node.dirty = false;
node.lastCleanEpoch = epoch;
}
/**
* Prepare this consumer to run a computation in its reactive context.
*
* Must be called by subclasses which represent reactive computations, before those computations
* begin.
*/
function consumerBeforeComputation(node) {
node && (node.nextProducerIndex = 0);
return setActiveConsumer(node);
}
/**
* Finalize this consumer's state after a reactive computation has run.
*
* Must be called by subclasses which represent reactive computations, after those computations
* have finished.
*/
function consumerAfterComputation(node, prevConsumer) {
setActiveConsumer(prevConsumer);
if (!node ||
node.producerNode === undefined ||
node.producerIndexOfThis === undefined ||
node.producerLastReadVersion === undefined) {
return;
}
if (consumerIsLive(node)) {
// For live consumers, we need to remove the producer -> consumer edge for any stale producers
// which weren't dependencies after the recomputation.
for (let i = node.nextProducerIndex; i < node.producerNode.length; i++) {
producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
}
}
// Truncate the producer tracking arrays.
// Perf note: this is essentially truncating the length to `node.nextProducerIndex`, but
// benchmarking has shown that individual pop operations are faster.
while (node.producerNode.length > node.nextProducerIndex) {
node.producerNode.pop();
node.producerLastReadVersion.pop();
node.producerIndexOfThis.pop();
}
}
/**
* Determine whether this consumer has any dependencies which have changed since the last time
* they were read.
*/
function consumerPollProducersForChange(node) {
assertConsumerNode(node);
// Poll producers for change.
for (let i = 0; i < node.producerNode.length; i++) {
const producer = node.producerNode[i];
const seenVersion = node.producerLastReadVersion[i];
// First check the versions. A mismatch means that the producer's value is known to have
// changed since the last time we read it.
if (seenVersion !== producer.version) {
return true;
}
// The producer's version is the same as the last time we read it, but it might itself be
// stale. Force the producer to recompute its version (calculating a new value if necessary).
producerUpdateValueVersion(producer);
// Now when we do this check, `producer.version` is guaranteed to be up to date, so if the
// versions still match then it has not changed since the last time we read it.
if (seenVersion !== producer.version) {
return true;
}
}
return false;
}
/**
* Disconnect this consumer from the graph.
*/
function consumerDestroy(node) {
assertConsumerNode(node);
if (consumerIsLive(node)) {
// Drop all connections from the graph to this node.
for (let i = 0; i < node.producerNode.length; i++) {
producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
}
}
// Truncate all the arrays to drop all connection from this node to the graph.
node.producerNode.length =
node.producerLastReadVersion.length =
node.producerIndexOfThis.length =
0;
if (node.liveConsumerNode) {
node.liveConsumerNode.length = node.liveConsumerIndexOfThis.length = 0;
}
}
/**
* Add `consumer` as a live consumer of this node.
*
* Note that this operation is potentially transitive. If this node becomes live, then it becomes
* a live consumer of all of its current producers.
*/
function producerAddLiveConsumer(node, consumer, indexOfThis) {
assertProducerNode(node);
if (node.liveConsumerNode.length === 0 && isConsumerNode(node)) {
// When going from 0 to 1 live consumers, we become a live consumer to our producers.
for (let i = 0; i < node.producerNode.length; i++) {
node.producerIndexOfThis[i] = producerAddLiveConsumer(node.producerNode[i], node, i);
}
}
node.liveConsumerIndexOfThis.push(indexOfThis);
return node.liveConsumerNode.push(consumer) - 1;
}
/**
* Remove the live consumer at `idx`.
*/
function producerRemoveLiveConsumerAtIndex(node, idx) {
assertProducerNode(node);
if (typeof ngDevMode !== 'undefined' && ngDevMode && idx >= node.liveConsumerNode.length) {
throw new Error(`Assertion error: active consumer index ${idx} is out of bounds of ${node.liveConsumerNode.length} consumers)`);
}
if (node.liveConsumerNode.length === 1 && isConsumerNode(node)) {
// When removing the last live consumer, we will no longer be live. We need to remove
// ourselves from our producers' tracking (which may cause consumer-producers to lose
// liveness as well).
for (let i = 0; i < node.producerNode.length; i++) {
producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
}
}
// Move the last value of `liveConsumers` into `idx`. Note that if there's only a single
// live consumer, this is a no-op.
const lastIdx = node.liveConsumerNode.length - 1;
node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx];
node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx];
// Truncate the array.
node.liveConsumerNode.length--;
node.liveConsumerIndexOfThis.length--;
// If the index is still valid, then we need to fix the index pointer from the producer to this
// consumer, and update it from `lastIdx` to `idx` (accounting for the move above).
if (idx < node.liveConsumerNode.length) {
const idxProducer = node.liveConsumerIndexOfThis[idx];
const consumer = node.liveConsumerNode[idx];
assertConsumerNode(consumer);
consumer.producerIndexOfThis[idxProducer] = idx;
}
}
function consumerIsLive(node) {
return node.consumerIsAlwaysLive || (node?.liveConsumerNode?.length ?? 0) > 0;
}
function assertConsumerNode(node) {
node.producerNode ??= [];
node.producerIndexOfThis ??= [];
node.producerLastReadVersion ??= [];
}
function assertProducerNode(node) {
node.liveConsumerNode ??= [];
node.liveConsumerIndexOfThis ??= [];
}
function isConsumerNode(node) {
return node.producerNode !== undefined;
}
/**
* Create a computed signal which derives a reactive value from an expression.
*/
function createComputed(computation, equal) {
const node = Object.create(COMPUTED_NODE);
node.computation = computation;
if (equal !== undefined) {
node.equal = equal;
}
const computed = () => {
// Check if the value needs updating before returning it.
producerUpdateValueVersion(node);
// Record that someone looked at this signal.
producerAccessed(node);
if (node.value === ERRORED) {
throw node.error;
}
return node.value;
};
computed[SIGNAL] = node;
return computed;
}
/**
* A dedicated symbol used before a computed value has been calculated for the first time.
* Explicitly typed as `any` so we can use it as signal's value.
*/
const UNSET = /* @__PURE__ */ Symbol('UNSET');
/**
* A dedicated symbol used in place of a computed signal value to indicate that a given computation
* is in progress. Used to detect cycles in computation chains.
* Explicitly typed as `any` so we can use it as signal's value.
*/
const COMPUTING = /* @__PURE__ */ Symbol('COMPUTING');
/**
* A dedicated symbol used in place of a computed signal value to indicate that a given computation
* failed. The thrown error is cached until the computation gets dirty again.
* Explicitly typed as `any` so we can use it as signal's value.
*/
const ERRORED = /* @__PURE__ */ Symbol('ERRORED');
// Note: Using an IIFE here to ensure that the spread assignment is not considered
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
const COMPUTED_NODE = /* @__PURE__ */ (() => {
return {
...REACTIVE_NODE,
value: UNSET,
dirty: true,
error: null,
equal: defaultEquals,
kind: 'computed',
producerMustRecompute(node) {
// Force a recomputation if there's no current value, or if the current value is in the
// process of being calculated (which should throw an error).
return node.value === UNSET || node.value === COMPUTING;
},
producerRecomputeValue(node) {
if (node.value === COMPUTING) {
// Our computation somehow led to a cyclic read of itself.
throw new Error('Detected cycle in computations.');
}
const oldValue = node.value;
node.value = COMPUTING;
const prevConsumer = consumerBeforeComputation(node);
let newValue;
let wasEqual = false;
try {
newValue = node.computation();
// We want to mark this node as errored if calling `equal` throws; however, we don't want
// to track any reactive reads inside `equal`.
setActiveConsumer(null);
wasEqual =
oldValue !== UNSET &&
oldValue !== ERRORED &&
newValue !== ERRORED &&
node.equal(oldValue, newValue);
}
catch (err) {
newValue = ERRORED;
node.error = err;
}
finally {
consumerAfterComputation(node, prevConsumer);
}
if (wasEqual) {
// No change to `valueVersion` - old and new values are
// semantically equivalent.
node.value = oldValue;
return;
}
node.value = newValue;
node.version++;
},
};
})();
function defaultThrowError() {
throw new Error();
}
let throwInvalidWriteToSignalErrorFn = defaultThrowError;
function throwInvalidWriteToSignalError(node) {
throwInvalidWriteToSignalErrorFn(node);
}
function setThrowInvalidWriteToSignalError(fn) {
throwInvalidWriteToSignalErrorFn = fn;
}
/**
* If set, called after `WritableSignal`s are updated.
*
* This hook can be used to achieve various effects, such as running effects synchronously as part
* of setting a signal.
*/
let postSignalSetFn = null;
/**
* Create a `Signal` that can be set or updated directly.
*/
function createSignal(initialValue, equal) {
const node = Object.create(SIGNAL_NODE);
node.value = initialValue;
if (equal !== undefined) {
node.equal = equal;
}
const getter = (() => {
producerAccessed(node);
return node.value;
});
getter[SIGNAL] = node;
return getter;
}
function setPostSignalSetFn(fn) {
const prev = postSignalSetFn;
postSignalSetFn = fn;
return prev;
}
function signalSetFn(node, newValue) {
if (!producerUpdatesAllowed()) {
throwInvalidWriteToSignalError(node);
}
if (!node.equal(node.value, newValue)) {
node.value = newValue;
signalValueChanged(node);
}
}
function signalUpdateFn(node, updater) {
if (!producerUpdatesAllowed()) {
throwInvalidWriteToSignalError(node);
}
signalSetFn(node, updater(node.value));
}
function runPostSignalSetFn() {
postSignalSetFn?.();
}
// Note: Using an IIFE here to ensure that the spread assignment is not considered
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
const SIGNAL_NODE = /* @__PURE__ */ (() => {
return {
...REACTIVE_NODE,
equal: defaultEquals,
value: undefined,
kind: 'signal',
};
})();
function signalValueChanged(node) {
node.version++;
producerIncrementEpoch();
producerNotifyConsumers(node);
postSignalSetFn?.();
}
function createLinkedSignal(sourceFn, computationFn, equalityFn) {
const node = Object.create(LINKED_SIGNAL_NODE);
node.source = sourceFn;
node.computation = computationFn;
if (equalityFn != undefined) {
node.equal = equalityFn;
}
const linkedSignalGetter = () => {
// Check if the value needs updating before returning it.
producerUpdateValueVersion(node);
// Record that someone looked at this signal.
producerAccessed(node);
if (node.value === ERRORED) {
throw node.error;
}
return node.value;
};
const getter = linkedSignalGetter;
getter[SIGNAL] = node;
return getter;
}
function linkedSignalSetFn(node, newValue) {
producerUpdateValueVersion(node);
signalSetFn(node, newValue);
producerMarkClean(node);
}
function linkedSignalUpdateFn(node, updater) {
producerUpdateValueVersion(node);
signalUpdateFn(node, updater);
producerMarkClean(node);
}
// Note: Using an IIFE here to ensure that the spread assignment is not considered
// a side-effect, ending up preserving `LINKED_SIGNAL_NODE` and `REACTIVE_NODE`.
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
const LINKED_SIGNAL_NODE = /* @__PURE__ */ (() => {
return {
...REACTIVE_NODE,
value: UNSET,
dirty: true,
error: null,
equal: defaultEquals,
kind: 'linkedSignal',
producerMustRecompute(node) {
// Force a recomputation if there's no current value, or if the current value is in the
// process of being calculated (which should throw an error).
return node.value === UNSET || node.value === COMPUTING;
},
producerRecomputeValue(node) {
if (node.value === COMPUTING) {
// Our computation somehow led to a cyclic read of itself.
throw new Error('Detected cycle in computations.');
}
const oldValue = node.value;
node.value = COMPUTING;
const prevConsumer = consumerBeforeComputation(node);
let newValue;
try {
const newSourceValue = node.source();
const prev = oldValue === UNSET || oldValue === ERRORED
? undefined
: {
source: node.sourceValue,
value: oldValue,
};
newValue = node.computation(newSourceValue, prev);
node.sourceValue = newSourceValue;
}
catch (err) {
newValue = ERRORED;
node.error = err;
}
finally {
consumerAfterComputation(node, prevConsumer);
}
if (oldValue !== UNSET && newValue !== ERRORED && node.equal(oldValue, newValue)) {
// No change to `valueVersion` - old and new values are
// semantically equivalent.
node.value = oldValue;
return;
}
node.value = newValue;
node.version++;
},
};
})();
function setAlternateWeakRefImpl(impl) {
// TODO: remove this function
}
/**
* Execute an arbitrary function in a non-reactive (non-tracking) context. The executed function
* can, optionally, return a value.
*/
function untracked(nonReactiveReadsFn) {
const prevConsumer = setActiveConsumer(null);
// We are not trying to catch any particular errors here, just making sure that the consumers
// stack is restored in case of errors.
try {
return nonReactiveReadsFn();
}
finally {
setActiveConsumer(prevConsumer);
}
}
export { REACTIVE_NODE, SIGNAL, SIGNAL_NODE, consumerAfterComputation, consumerBeforeComputation, consumerDestroy, consumerMarkDirty, consumerPollProducersForChange, createComputed, createLinkedSignal, createSignal, defaultEquals, getActiveConsumer, isInNotificationPhase, isReactive, linkedSignalSetFn, linkedSignalUpdateFn, producerAccessed, producerIncrementEpoch, producerMarkClean, producerNotifyConsumers, producerUpdateValueVersion, producerUpdatesAllowed, runPostSignalSetFn, setActiveConsumer, setAlternateWeakRefImpl, setPostSignalSetFn, setThrowInvalidWriteToSignalError, signalSetFn, signalUpdateFn, untracked };
//# sourceMappingURL=untracked-BKcld_ew.mjs.map

File diff suppressed because one or more lines are too long

18722
projects/ui-code-display/node_modules/@angular/core/index.d.ts generated vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,122 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
interface NavigationEventMap {
navigate: NavigateEvent;
navigatesuccess: Event;
navigateerror: ErrorEvent;
currententrychange: NavigationCurrentEntryChangeEvent;
}
interface NavigationResult {
committed: Promise<NavigationHistoryEntry>;
finished: Promise<NavigationHistoryEntry>;
}
declare class Navigation extends EventTarget {
entries(): NavigationHistoryEntry[];
readonly currentEntry: NavigationHistoryEntry | null;
updateCurrentEntry(options: NavigationUpdateCurrentEntryOptions): void;
readonly transition: NavigationTransition | null;
readonly canGoBack: boolean;
readonly canGoForward: boolean;
navigate(url: string, options?: NavigationNavigateOptions): NavigationResult;
reload(options?: NavigationReloadOptions): NavigationResult;
traverseTo(key: string, options?: NavigationOptions): NavigationResult;
back(options?: NavigationOptions): NavigationResult;
forward(options?: NavigationOptions): NavigationResult;
onnavigate: ((this: Navigation, ev: NavigateEvent) => any) | null;
onnavigatesuccess: ((this: Navigation, ev: Event) => any) | null;
onnavigateerror: ((this: Navigation, ev: ErrorEvent) => any) | null;
oncurrententrychange: ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any) | null;
addEventListener<K extends keyof NavigationEventMap>(type: K, listener: (this: Navigation, ev: NavigationEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof NavigationEventMap>(type: K, listener: (this: Navigation, ev: NavigationEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
declare class NavigationTransition {
readonly navigationType: NavigationTypeString;
readonly from: NavigationHistoryEntry;
readonly finished: Promise<void>;
}
interface NavigationHistoryEntryEventMap {
dispose: Event;
}
declare class NavigationHistoryEntry extends EventTarget {
readonly key: string;
readonly id: string;
readonly url: string | null;
readonly index: number;
readonly sameDocument: boolean;
getState(): unknown;
ondispose: ((this: NavigationHistoryEntry, ev: Event) => any) | null;
addEventListener<K extends keyof NavigationHistoryEntryEventMap>(type: K, listener: (this: NavigationHistoryEntry, ev: NavigationHistoryEntryEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof NavigationHistoryEntryEventMap>(type: K, listener: (this: NavigationHistoryEntry, ev: NavigationHistoryEntryEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
type NavigationTypeString = 'reload' | 'push' | 'replace' | 'traverse';
interface NavigationUpdateCurrentEntryOptions {
state: unknown;
}
interface NavigationOptions {
info?: unknown;
}
interface NavigationNavigateOptions extends NavigationOptions {
state?: unknown;
history?: 'auto' | 'push' | 'replace';
}
interface NavigationReloadOptions extends NavigationOptions {
state?: unknown;
}
declare class NavigationCurrentEntryChangeEvent extends Event {
constructor(type: string, eventInit?: NavigationCurrentEntryChangeEventInit);
readonly navigationType: NavigationTypeString | null;
readonly from: NavigationHistoryEntry;
}
interface NavigationCurrentEntryChangeEventInit extends EventInit {
navigationType?: NavigationTypeString | null;
from: NavigationHistoryEntry;
}
declare class NavigateEvent extends Event {
constructor(type: string, eventInit?: NavigateEventInit);
readonly navigationType: NavigationTypeString;
readonly canIntercept: boolean;
readonly userInitiated: boolean;
readonly hashChange: boolean;
readonly destination: NavigationDestination;
readonly signal: AbortSignal;
readonly formData: FormData | null;
readonly downloadRequest: string | null;
readonly info?: unknown;
intercept(options?: NavigationInterceptOptions): void;
scroll(): void;
}
interface NavigateEventInit extends EventInit {
navigationType?: NavigationTypeString;
canIntercept?: boolean;
userInitiated?: boolean;
hashChange?: boolean;
destination: NavigationDestination;
signal: AbortSignal;
formData?: FormData | null;
downloadRequest?: string | null;
info?: unknown;
}
interface NavigationInterceptOptions {
handler?: () => Promise<void>;
focusReset?: 'after-transition' | 'manual';
scroll?: 'after-transition' | 'manual';
}
declare class NavigationDestination {
readonly url: string;
readonly key: string | null;
readonly id: string | null;
readonly index: number;
readonly sameDocument: boolean;
getState(): unknown;
}
export { NavigateEvent, Navigation, NavigationCurrentEntryChangeEvent, NavigationDestination, NavigationHistoryEntry, NavigationTransition };
export type { NavigationInterceptOptions, NavigationNavigateOptions, NavigationOptions, NavigationReloadOptions, NavigationResult, NavigationTypeString, NavigationUpdateCurrentEntryOptions };

View File

@@ -0,0 +1,83 @@
{
"name": "@angular/core",
"version": "19.2.14",
"description": "Angular - the core framework",
"author": "angular",
"license": "MIT",
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
},
"exports": {
"./schematics/*": {
"default": "./schematics/*.js"
},
"./event-dispatch-contract.min.js": {
"default": "./event-dispatch-contract.min.js"
},
"./package.json": {
"default": "./package.json"
},
".": {
"types": "./index.d.ts",
"default": "./fesm2022/core.mjs"
},
"./primitives/di": {
"types": "./primitives/di/index.d.ts",
"default": "./fesm2022/primitives/di.mjs"
},
"./primitives/event-dispatch": {
"types": "./primitives/event-dispatch/index.d.ts",
"default": "./fesm2022/primitives/event-dispatch.mjs"
},
"./primitives/signals": {
"types": "./primitives/signals/index.d.ts",
"default": "./fesm2022/primitives/signals.mjs"
},
"./rxjs-interop": {
"types": "./rxjs-interop/index.d.ts",
"default": "./fesm2022/rxjs-interop.mjs"
},
"./testing": {
"types": "./testing/index.d.ts",
"default": "./fesm2022/testing.mjs"
}
},
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.15.0"
},
"repository": {
"type": "git",
"url": "https://github.com/angular/angular.git",
"directory": "packages/core"
},
"ng-update": {
"migrations": "./schematics/migrations.json",
"packageGroup": [
"@angular/core",
"@angular/bazel",
"@angular/common",
"@angular/compiler",
"@angular/compiler-cli",
"@angular/animations",
"@angular/elements",
"@angular/platform-browser",
"@angular/platform-browser-dynamic",
"@angular/forms",
"@angular/platform-server",
"@angular/upgrade",
"@angular/router",
"@angular/language-service",
"@angular/localize",
"@angular/service-worker"
]
},
"schematics": "./schematics/collection.json",
"sideEffects": false,
"module": "./fesm2022/core.mjs",
"typings": "./index.d.ts",
"type": "module"
}

View File

@@ -0,0 +1,106 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
/**
* @description
*
* Represents a type that a Component or other object is instances of.
*
* An example of a `Type` is `MyCustomComponent` class, which in JavaScript is represented by
* the `MyCustomComponent` constructor function.
*
* @publicApi
*/
declare const Type: FunctionConstructor;
interface Type<T> extends Function {
new (...args: any[]): T;
}
/**
* Information about how a type or `InjectionToken` interfaces with the DI
* system. This describes:
*
* 1. *How* the type is provided
* The declaration must specify only one of the following:
* - A `value` which is a predefined instance of the type.
* - A `factory` which defines how to create the given type `T`, possibly
* requesting injection of other types if necessary.
* - Neither, in which case the type is expected to already be present in the
* injector hierarchy. This is used for internal use cases.
*
* 2. *Where* the type is stored (if it is stored)
* - The `providedIn` parameter specifies which injector the type belongs to.
* - The `token` is used as the key to store the type in the injector.
*/
interface ɵɵInjectableDeclaration<T> {
/**
* Specifies that the given type belongs to a particular `Injector`,
* `NgModule`, or a special scope (e.g. `'root'`).
*
* `any` is deprecated and will be removed soon.
*
* A value of `null` indicates that the injectable does not belong to any
* scope, and won't be stored in any injector. For declarations with a
* factory, this will create a new instance of the type each time it is
* requested.
*/
providedIn: Type<any> | 'root' | 'platform' | 'any' | null;
/**
* The token to which this definition belongs.
*
* Note that this may not be the same as the type that the `factory` will create.
*/
token: unknown;
/**
* Factory method to execute to create an instance of the injectable.
*/
factory?: (t?: Type<any>) => T;
/**
* In a case of no explicit injector, a location where the instance of the injectable is stored.
*/
value?: T;
}
/**
* A `Type` which has a `ɵprov: ɵɵInjectableDeclaration` static field.
*
* `InjectableType`s contain their own Dependency Injection metadata and are usable in an
* `InjectorDef`-based `StaticInjector`.
*
* @publicApi
*/
interface InjectionToken<T> extends Type<T> {
ɵprov: ɵɵInjectableDeclaration<T>;
}
/**
* Value returned if the key-value pair couldn't be found in the context
* hierarchy.
*/
declare const NOT_FOUND: unique symbol;
/**
* Error thrown when the key-value pair couldn't be found in the context
* hierarchy. Context can be attached below.
*/
declare class NotFoundError extends Error {
constructor(message: string);
}
/**
* Type guard for checking if an unknown value is a NotFound.
*/
declare function isNotFound(e: unknown): e is NotFound;
/**
* Type union of NotFound and NotFoundError.
*/
type NotFound = typeof NOT_FOUND | NotFoundError;
interface Injector {
retrieve<T>(token: InjectionToken<T>, options?: unknown): T | NotFound;
}
declare function getCurrentInjector(): Injector | undefined | null;
declare function setCurrentInjector(injector: Injector | null | undefined): Injector | undefined | null;
export { NOT_FOUND, NotFoundError, getCurrentInjector, isNotFound, setCurrentInjector };
export type { InjectionToken, Injector, NotFound, ɵɵInjectableDeclaration };

View File

@@ -0,0 +1,62 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
import { EarlyJsactionDataContainer, EventInfo, Restriction } from '../../event_dispatcher.d-K56StcHr.js';
export { EventContract, EventContractContainer, EventDispatcher, EventInfoWrapper, EventPhase, registerDispatcher } from '../../event_dispatcher.d-K56StcHr.js';
declare const Attribute: {
/**
* The jsaction attribute defines a mapping of a DOM event to a
* generic event (aka jsaction), to which the actual event handlers
* that implement the behavior of the application are bound. The
* value is a semicolon separated list of colon separated pairs of
* an optional DOM event name and a jsaction name. If the optional
* DOM event name is omitted, 'click' is assumed. The jsaction names
* are dot separated pairs of a namespace and a simple jsaction
* name.
*
* See grammar in README.md for expected syntax in the attribute value.
*/
JSACTION: "jsaction";
};
/**
* Reads the jsaction parser cache for the given DOM element. If no cache is yet present,
* creates an empty one.
*/
declare function getDefaulted(element: Element): {
[key: string]: string | undefined;
};
/**
* Whether or not an event type should be registered in the capture phase.
* @param eventType
* @returns bool
*/
declare const isCaptureEventType: (eventType: string) => boolean;
/**
* Whether or not an event type is registered in the early contract.
*/
declare const isEarlyEventType: (eventType: string) => boolean;
/**
* Creates an `EarlyJsactionData`, adds events to it, and populates it on a nested object on
* the window.
*/
declare function bootstrapAppScopedEarlyEventContract(container: HTMLElement, appId: string, bubbleEventTypes: string[], captureEventTypes: string[], dataContainer?: EarlyJsactionDataContainer): void;
/** Get the queued `EventInfo` objects that were dispatched before a dispatcher was registered. */
declare function getAppScopedQueuedEventInfos(appId: string, dataContainer?: EarlyJsactionDataContainer): EventInfo[];
/**
* Registers a dispatcher function on the `EarlyJsactionData` present on the nested object on the
* window.
*/
declare function registerAppScopedDispatcher(restriction: Restriction, appId: string, dispatcher: (eventInfo: EventInfo) => void, dataContainer?: EarlyJsactionDataContainer): void;
/** Removes all event listener handlers. */
declare function removeAllAppScopedEventListeners(appId: string, dataContainer?: EarlyJsactionDataContainer): void;
/** Clear the early event contract. */
declare function clearAppScopedEarlyEventContract(appId: string, dataContainer?: EarlyJsactionDataContainer): void;
export { Attribute, EarlyJsactionDataContainer, bootstrapAppScopedEarlyEventContract, clearAppScopedEarlyEventContract, getDefaulted as getActionCache, getAppScopedQueuedEventInfos, isCaptureEventType, isEarlyEventType, registerAppScopedDispatcher, removeAllAppScopedEventListeners };

View File

@@ -0,0 +1,121 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
import { ReactiveNode, ValueEqualityFn, SIGNAL, SignalNode } from '../../weak_ref.d-DWHPG08n.js';
export { REACTIVE_NODE, Reactive, SIGNAL_NODE, SignalGetter, consumerAfterComputation, consumerBeforeComputation, consumerDestroy, consumerMarkDirty, consumerPollProducersForChange, createSignal, defaultEquals, getActiveConsumer, isInNotificationPhase, isReactive, producerAccessed, producerIncrementEpoch, producerMarkClean, producerNotifyConsumers, producerUpdateValueVersion, producerUpdatesAllowed, runPostSignalSetFn, setActiveConsumer, setAlternateWeakRefImpl, setPostSignalSetFn, signalSetFn, signalUpdateFn } from '../../weak_ref.d-DWHPG08n.js';
/**
* A computation, which derives a value from a declarative reactive expression.
*
* `Computed`s are both producers and consumers of reactivity.
*/
interface ComputedNode<T> extends ReactiveNode {
/**
* Current value of the computation, or one of the sentinel values above (`UNSET`, `COMPUTING`,
* `ERROR`).
*/
value: T;
/**
* If `value` is `ERRORED`, the error caught from the last computation attempt which will
* be re-thrown.
*/
error: unknown;
/**
* The computation function which will produce a new value.
*/
computation: () => T;
equal: ValueEqualityFn<T>;
}
type ComputedGetter<T> = (() => T) & {
[SIGNAL]: ComputedNode<T>;
};
/**
* Create a computed signal which derives a reactive value from an expression.
*/
declare function createComputed<T>(computation: () => T, equal?: ValueEqualityFn<T>): ComputedGetter<T>;
type ComputationFn<S, D> = (source: S, previous?: {
source: S;
value: D;
}) => D;
interface LinkedSignalNode<S, D> extends ReactiveNode {
/**
* Value of the source signal that was used to derive the computed value.
*/
sourceValue: S;
/**
* Current state value, or one of the sentinel values (`UNSET`, `COMPUTING`,
* `ERROR`).
*/
value: D;
/**
* If `value` is `ERRORED`, the error caught from the last computation attempt which will
* be re-thrown.
*/
error: unknown;
/**
* The source function represents reactive dependency based on which the linked state is reset.
*/
source: () => S;
/**
* The computation function which will produce a new value based on the source and, optionally - previous values.
*/
computation: ComputationFn<S, D>;
equal: ValueEqualityFn<D>;
}
type LinkedSignalGetter<S, D> = (() => D) & {
[SIGNAL]: LinkedSignalNode<S, D>;
};
declare function createLinkedSignal<S, D>(sourceFn: () => S, computationFn: ComputationFn<S, D>, equalityFn?: ValueEqualityFn<D>): LinkedSignalGetter<S, D>;
declare function linkedSignalSetFn<S, D>(node: LinkedSignalNode<S, D>, newValue: D): void;
declare function linkedSignalUpdateFn<S, D>(node: LinkedSignalNode<S, D>, updater: (value: D) => D): void;
declare function setThrowInvalidWriteToSignalError(fn: <T>(node: SignalNode<T>) => never): void;
/**
* A cleanup function that can be optionally registered from the watch logic. If registered, the
* cleanup logic runs before the next watch execution.
*/
type WatchCleanupFn = () => void;
/**
* A callback passed to the watch function that makes it possible to register cleanup logic.
*/
type WatchCleanupRegisterFn = (cleanupFn: WatchCleanupFn) => void;
interface Watch {
notify(): void;
/**
* Execute the reactive expression in the context of this `Watch` consumer.
*
* Should be called by the user scheduling algorithm when the provided
* `schedule` hook is called by `Watch`.
*/
run(): void;
cleanup(): void;
/**
* Destroy the watcher:
* - disconnect it from the reactive graph;
* - mark it as destroyed so subsequent run and notify operations are noop.
*/
destroy(): void;
[SIGNAL]: WatchNode;
}
interface WatchNode extends ReactiveNode {
hasRun: boolean;
fn: ((onCleanup: WatchCleanupRegisterFn) => void) | null;
schedule: ((watch: Watch) => void) | null;
cleanupFn: WatchCleanupFn;
ref: Watch;
}
declare function createWatch(fn: (onCleanup: WatchCleanupRegisterFn) => void, schedule: (watch: Watch) => void, allowSignalWrites: boolean): Watch;
/**
* Execute an arbitrary function in a non-reactive (non-tracking) context. The executed function
* can, optionally, return a value.
*/
declare function untracked<T>(nonReactiveReadsFn: () => T): T;
export { ReactiveNode, SIGNAL, SignalNode, ValueEqualityFn, createComputed, createLinkedSignal, createWatch, linkedSignalSetFn, linkedSignalUpdateFn, setThrowInvalidWriteToSignalError, untracked };
export type { ComputationFn, ComputedNode, LinkedSignalGetter, LinkedSignalNode, Watch, WatchCleanupFn, WatchCleanupRegisterFn };

View File

@@ -0,0 +1,192 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
import { OutputOptions, OutputRef, DestroyRef, Signal, Injector, BaseResourceOptions, ResourceLoaderParams, ResourceRef } from '@angular/core';
import { Observable, MonoTypeOperatorFunction, Subscribable } from 'rxjs';
import { ValueEqualityFn } from '@angular/core/primitives/signals';
/**
* Declares an Angular output that is using an RxJS observable as a source
* for events dispatched to parent subscribers.
*
* The behavior for an observable as source is defined as followed:
* 1. New values are forwarded to the Angular output (next notifications).
* 2. Errors notifications are not handled by Angular. You need to handle these manually.
* For example by using `catchError`.
* 3. Completion notifications stop the output from emitting new values.
*
* @usageNotes
* Initialize an output in your directive by declaring a
* class field and initializing it with the `outputFromObservable()` function.
*
* ```ts
* @Directive({..})
* export class MyDir {
* nameChange$ = <some-observable>;
* nameChange = outputFromObservable(this.nameChange$);
* }
* ```
*
* @publicApi
*/
declare function outputFromObservable<T>(observable: Observable<T>, opts?: OutputOptions): OutputRef<T>;
/**
* Converts an Angular output declared via `output()` or `outputFromObservable()`
* to an observable.
*
* You can subscribe to the output via `Observable.subscribe` then.
*
* @publicApi
*/
declare function outputToObservable<T>(ref: OutputRef<T>): Observable<T>;
/**
* Operator which completes the Observable when the calling context (component, directive, service,
* etc) is destroyed.
*
* @param destroyRef optionally, the `DestroyRef` representing the current context. This can be
* passed explicitly to use `takeUntilDestroyed` outside of an [injection
* context](guide/di/dependency-injection-context). Otherwise, the current `DestroyRef` is injected.
*
* @publicApi
*/
declare function takeUntilDestroyed<T>(destroyRef?: DestroyRef): MonoTypeOperatorFunction<T>;
/**
* Options for `toObservable`.
*
* @developerPreview
*/
interface ToObservableOptions {
/**
* The `Injector` to use when creating the underlying `effect` which watches the signal.
*
* If this isn't specified, the current [injection context](guide/di/dependency-injection-context)
* will be used.
*/
injector?: Injector;
}
/**
* Exposes the value of an Angular `Signal` as an RxJS `Observable`.
*
* The signal's value will be propagated into the `Observable`'s subscribers using an `effect`.
*
* `toObservable` must be called in an injection context unless an injector is provided via options.
*
* @developerPreview
*/
declare function toObservable<T>(source: Signal<T>, options?: ToObservableOptions): Observable<T>;
declare function toObservableMicrotask<T>(source: Signal<T>, options?: ToObservableOptions): Observable<T>;
/**
* Options for `toSignal`.
*
* @publicApi
*/
interface ToSignalOptions<T> {
/**
* Initial value for the signal produced by `toSignal`.
*
* This will be the value of the signal until the observable emits its first value.
*/
initialValue?: unknown;
/**
* Whether to require that the observable emits synchronously when `toSignal` subscribes.
*
* If this is `true`, `toSignal` will assert that the observable produces a value immediately upon
* subscription. Setting this option removes the need to either deal with `undefined` in the
* signal type or provide an `initialValue`, at the cost of a runtime error if this requirement is
* not met.
*/
requireSync?: boolean;
/**
* `Injector` which will provide the `DestroyRef` used to clean up the Observable subscription.
*
* If this is not provided, a `DestroyRef` will be retrieved from the current [injection
* context](guide/di/dependency-injection-context), unless manual cleanup is requested.
*/
injector?: Injector;
/**
* Whether the subscription should be automatically cleaned up (via `DestroyRef`) when
* `toSignal`'s creation context is destroyed.
*
* If manual cleanup is enabled, then `DestroyRef` is not used, and the subscription will persist
* until the `Observable` itself completes.
*/
manualCleanup?: boolean;
/**
* Whether `toSignal` should throw errors from the Observable error channel back to RxJS, where
* they'll be processed as uncaught exceptions.
*
* In practice, this means that the signal returned by `toSignal` will keep returning the last
* good value forever, as Observables which error produce no further values. This option emulates
* the behavior of the `async` pipe.
*/
rejectErrors?: boolean;
/**
* A comparison function which defines equality for values emitted by the observable.
*
* Equality comparisons are executed against the initial value if one is provided.
*/
equal?: ValueEqualityFn<T>;
}
declare function toSignal<T>(source: Observable<T> | Subscribable<T>): Signal<T | undefined>;
declare function toSignal<T>(source: Observable<T> | Subscribable<T>, options: NoInfer<ToSignalOptions<T | undefined>> & {
initialValue?: undefined;
requireSync?: false;
}): Signal<T | undefined>;
declare function toSignal<T>(source: Observable<T> | Subscribable<T>, options: NoInfer<ToSignalOptions<T | null>> & {
initialValue?: null;
requireSync?: false;
}): Signal<T | null>;
declare function toSignal<T>(source: Observable<T> | Subscribable<T>, options: NoInfer<ToSignalOptions<T>> & {
initialValue?: undefined;
requireSync: true;
}): Signal<T>;
declare function toSignal<T, const U extends T>(source: Observable<T> | Subscribable<T>, options: NoInfer<ToSignalOptions<T | U>> & {
initialValue: U;
requireSync?: false;
}): Signal<T | U>;
/**
* Operator which makes the application unstable until the observable emits, completes, errors, or is unsubscribed.
*
* Use this operator in observables whose subscriptions are important for rendering and should be included in SSR serialization.
*
* @param injector The `Injector` to use during creation. If this is not provided, the current injection context will be used instead (via `inject`).
*
* @experimental
*/
declare function pendingUntilEvent<T>(injector?: Injector): MonoTypeOperatorFunction<T>;
/**
* Like `ResourceOptions` but uses an RxJS-based `loader`.
*
* @experimental
*/
interface RxResourceOptions<T, R> extends BaseResourceOptions<T, R> {
loader: (params: ResourceLoaderParams<R>) => Observable<T>;
}
/**
* Like `resource` but uses an RxJS based `loader` which maps the request to an `Observable` of the
* resource's value.
*
* @experimental
*/
declare function rxResource<T, R>(opts: RxResourceOptions<T, R> & {
defaultValue: NoInfer<T>;
}): ResourceRef<T>;
/**
* Like `resource` but uses an RxJS based `loader` which maps the request to an `Observable` of the
* resource's value.
*
* @experimental
*/
declare function rxResource<T, R>(opts: RxResourceOptions<T, R>): ResourceRef<T | undefined>;
export { outputFromObservable, outputToObservable, pendingUntilEvent, rxResource, takeUntilDestroyed, toObservable, toSignal, toObservableMicrotask as ɵtoObservableMicrotask };
export type { RxResourceOptions, ToObservableOptions, ToSignalOptions };

View File

@@ -0,0 +1,67 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var checker = require('./checker-BwV9MjSQ.cjs');
var project_paths = require('./project_paths-DY3SIODd.cjs');
/**
* Applies import manager changes, and writes them as replacements the
* given result array.
*/
function applyImportManagerChanges(importManager, replacements, sourceFiles, info) {
const { newImports, updatedImports, deletedImports } = importManager.finalize();
const printer = ts.createPrinter({});
const pathToFile = new Map(sourceFiles.map((s) => [s.fileName, s]));
// Capture new imports
newImports.forEach((newImports, fileName) => {
newImports.forEach((newImport) => {
const printedImport = printer.printNode(ts.EmitHint.Unspecified, newImport, pathToFile.get(fileName));
replacements.push(new project_paths.Replacement(project_paths.projectFile(checker.absoluteFrom(fileName), info), new project_paths.TextUpdate({ position: 0, end: 0, toInsert: `${printedImport}\n` })));
});
});
// Capture updated imports
for (const [oldBindings, newBindings] of updatedImports.entries()) {
// The import will be generated as multi-line if it already is multi-line,
// or if the number of elements significantly increased and it previously
// consisted of very few specifiers.
const isMultiline = oldBindings.getText().includes('\n') ||
(newBindings.elements.length >= 6 && oldBindings.elements.length <= 3);
const hasSpaceBetweenBraces = oldBindings.getText().startsWith('{ ');
let formatFlags = ts.ListFormat.NamedImportsOrExportsElements |
ts.ListFormat.Indented |
ts.ListFormat.Braces |
ts.ListFormat.PreserveLines |
(isMultiline ? ts.ListFormat.MultiLine : ts.ListFormat.SingleLine);
if (hasSpaceBetweenBraces) {
formatFlags |= ts.ListFormat.SpaceBetweenBraces;
}
else {
formatFlags &= ~ts.ListFormat.SpaceBetweenBraces;
}
const printedBindings = printer.printList(formatFlags, newBindings.elements, oldBindings.getSourceFile());
replacements.push(new project_paths.Replacement(project_paths.projectFile(oldBindings.getSourceFile(), info), new project_paths.TextUpdate({
position: oldBindings.getStart(),
end: oldBindings.getEnd(),
// TS uses four spaces as indent. We migrate to two spaces as we
// assume this to be more common.
toInsert: printedBindings.replace(/^ {4}/gm, ' '),
})));
}
// Update removed imports
for (const removedImport of deletedImports) {
replacements.push(new project_paths.Replacement(project_paths.projectFile(removedImport.getSourceFile(), info), new project_paths.TextUpdate({
position: removedImport.getStart(),
end: removedImport.getEnd(),
toInsert: '',
})));
}
}
exports.applyImportManagerChanges = applyImportManagerChanges;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,310 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
require('@angular-devkit/core');
require('node:path/posix');
var project_paths = require('./project_paths-DY3SIODd.cjs');
var ts = require('typescript');
require('os');
var checker = require('./checker-BwV9MjSQ.cjs');
var index = require('./index-BnJH1Hc7.cjs');
require('path');
var apply_import_manager = require('./apply_import_manager-DF0BUe6N.cjs');
var leading_space = require('./leading_space-D9nQ8UQC.cjs');
require('@angular-devkit/schematics');
require('./project_tsconfig_paths-CDVxT6Ov.cjs');
require('fs');
require('module');
require('url');
/** Migration that cleans up unused imports from a project. */
class UnusedImportsMigration extends project_paths.TsurgeFunnelMigration {
printer = ts.createPrinter();
createProgram(tsconfigAbsPath, fs) {
return super.createProgram(tsconfigAbsPath, fs, {
extendedDiagnostics: {
checks: {
// Ensure that the diagnostic is enabled.
unusedStandaloneImports: index.DiagnosticCategoryLabel.Warning,
},
},
});
}
async analyze(info) {
const nodePositions = new Map();
const replacements = [];
let removedImports = 0;
let changedFiles = 0;
info.ngCompiler?.getDiagnostics().forEach((diag) => {
if (diag.file !== undefined &&
diag.start !== undefined &&
diag.length !== undefined &&
diag.code === checker.ngErrorCode(checker.ErrorCode.UNUSED_STANDALONE_IMPORTS)) {
// Skip files that aren't owned by this compilation unit.
if (!info.sourceFiles.includes(diag.file)) {
return;
}
if (!nodePositions.has(diag.file)) {
nodePositions.set(diag.file, new Set());
}
nodePositions.get(diag.file).add(this.getNodeKey(diag.start, diag.length));
}
});
nodePositions.forEach((locations, sourceFile) => {
const resolvedLocations = this.resolveRemovalLocations(sourceFile, locations);
const usageAnalysis = this.analyzeUsages(sourceFile, resolvedLocations);
if (resolvedLocations.allRemovedIdentifiers.size > 0) {
removedImports += resolvedLocations.allRemovedIdentifiers.size;
changedFiles++;
}
this.generateReplacements(sourceFile, resolvedLocations, usageAnalysis, info, replacements);
});
return project_paths.confirmAsSerializable({ replacements, removedImports, changedFiles });
}
async migrate(globalData) {
return project_paths.confirmAsSerializable(globalData);
}
async combine(unitA, unitB) {
return project_paths.confirmAsSerializable({
replacements: [...unitA.replacements, ...unitB.replacements],
removedImports: unitA.removedImports + unitB.removedImports,
changedFiles: unitA.changedFiles + unitB.changedFiles,
});
}
async globalMeta(combinedData) {
return project_paths.confirmAsSerializable(combinedData);
}
async stats(globalMetadata) {
return {
counters: {
removedImports: globalMetadata.removedImports,
changedFiles: globalMetadata.changedFiles,
},
};
}
/** Gets a key that can be used to look up a node based on its location. */
getNodeKey(start, length) {
return `${start}/${length}`;
}
/**
* Resolves a set of node locations to the actual AST nodes that need to be migrated.
* @param sourceFile File in which to resolve the locations.
* @param locations Location keys that should be resolved.
*/
resolveRemovalLocations(sourceFile, locations) {
const result = {
fullRemovals: new Set(),
partialRemovals: new Map(),
allRemovedIdentifiers: new Set(),
};
const walk = (node) => {
if (!ts.isIdentifier(node)) {
node.forEachChild(walk);
return;
}
// The TS typings don't reflect that the parent can be undefined.
const parent = node.parent;
if (!parent) {
return;
}
if (locations.has(this.getNodeKey(node.getStart(), node.getWidth()))) {
// When the entire array needs to be cleared, the diagnostic is
// reported on the property assignment, rather than an array element.
if (ts.isPropertyAssignment(parent) &&
parent.name === node &&
ts.isArrayLiteralExpression(parent.initializer)) {
result.fullRemovals.add(parent.initializer);
parent.initializer.elements.forEach((element) => {
if (ts.isIdentifier(element)) {
result.allRemovedIdentifiers.add(element.text);
}
});
}
else if (ts.isArrayLiteralExpression(parent)) {
if (!result.partialRemovals.has(parent)) {
result.partialRemovals.set(parent, new Set());
}
result.partialRemovals.get(parent).add(node);
result.allRemovedIdentifiers.add(node.text);
}
}
};
walk(sourceFile);
return result;
}
/**
* Analyzes how identifiers are used across a file.
* @param sourceFile File to be analyzed.
* @param locations Locations that will be changed as a part of this migration.
*/
analyzeUsages(sourceFile, locations) {
const { partialRemovals, fullRemovals } = locations;
const result = {
importedSymbols: new Map(),
identifierCounts: new Map(),
};
const walk = (node) => {
if (ts.isIdentifier(node) &&
node.parent &&
// Don't track individual identifiers marked for removal.
(!ts.isArrayLiteralExpression(node.parent) ||
!partialRemovals.has(node.parent) ||
!partialRemovals.get(node.parent).has(node))) {
result.identifierCounts.set(node.text, (result.identifierCounts.get(node.text) ?? 0) + 1);
}
// Don't track identifiers in array literals that are about to be removed.
if (ts.isArrayLiteralExpression(node) && fullRemovals.has(node)) {
return;
}
if (ts.isImportDeclaration(node)) {
const namedBindings = node.importClause?.namedBindings;
const moduleName = ts.isStringLiteral(node.moduleSpecifier)
? node.moduleSpecifier.text
: null;
if (namedBindings && ts.isNamedImports(namedBindings) && moduleName !== null) {
namedBindings.elements.forEach((imp) => {
if (!result.importedSymbols.has(moduleName)) {
result.importedSymbols.set(moduleName, new Map());
}
const symbolName = (imp.propertyName || imp.name).text;
const localName = imp.name.text;
result.importedSymbols.get(moduleName).set(localName, symbolName);
});
}
// Don't track identifiers in imports.
return;
}
// Track identifiers in all other node kinds.
node.forEachChild(walk);
};
walk(sourceFile);
return result;
}
/**
* Generates text replacements based on the data produced by the migration.
* @param sourceFile File being migrated.
* @param removalLocations Data about nodes being removed.
* @param usages Data about identifier usage.
* @param info Information about the current program.
* @param replacements Array tracking all text replacements.
*/
generateReplacements(sourceFile, removalLocations, usages, info, replacements) {
const { fullRemovals, partialRemovals, allRemovedIdentifiers } = removalLocations;
const { importedSymbols, identifierCounts } = usages;
const importManager = new checker.ImportManager();
const sourceText = sourceFile.getFullText();
// Replace full arrays with empty ones. This allows preserves more of the user's formatting.
fullRemovals.forEach((node) => {
replacements.push(new project_paths.Replacement(project_paths.projectFile(sourceFile, info), new project_paths.TextUpdate({
position: node.getStart(),
end: node.getEnd(),
toInsert: '[]',
})));
});
// Filter out the unused identifiers from an array.
partialRemovals.forEach((toRemove, parent) => {
toRemove.forEach((node) => {
replacements.push(new project_paths.Replacement(project_paths.projectFile(sourceFile, info), getArrayElementRemovalUpdate(node, parent, sourceText)));
});
});
// Attempt to clean up unused import declarations. Note that this isn't foolproof, because we
// do the matching based on identifier text, rather than going through the type checker which
// can be expensive. This should be enough for the vast majority of cases in this schematic
// since we're dealing exclusively with directive/pipe class names which tend to be very
// specific. In the worst case we may end up not removing an import declaration which would
// still be valid code that the user can clean up themselves.
importedSymbols.forEach((names, moduleName) => {
names.forEach((symbolName, localName) => {
// Note that in the `identifierCounts` lookup both zero and undefined
// are valid and mean that the identifiers isn't being used anymore.
if (allRemovedIdentifiers.has(localName) && !identifierCounts.get(localName)) {
importManager.removeImport(sourceFile, symbolName, moduleName);
}
});
});
apply_import_manager.applyImportManagerChanges(importManager, replacements, [sourceFile], info);
}
}
/** Generates a `TextUpdate` for the removal of an array element. */
function getArrayElementRemovalUpdate(node, parent, sourceText) {
let position = node.getStart();
let end = node.getEnd();
let toInsert = '';
const whitespaceOrLineFeed = /\s/;
// Usually the way we'd remove the nodes would be to recreate the `parent` while excluding
// the nodes that should be removed. The problem with this is that it'll strip out comments
// inside the array which can have special meaning internally. We work around it by removing
// only the node's own offsets. This comes with another problem in that it won't remove the commas
// that separate array elements which in turn can look weird if left in place (e.g.
// `[One, Two, Three, Four]` can turn into `[One,,Four]`). To account for them, we start with the
// node's end offset and then expand it to include trailing commas, whitespace and line breaks.
for (let i = end; i < sourceText.length; i++) {
if (sourceText[i] === ',' || whitespaceOrLineFeed.test(sourceText[i])) {
end++;
}
else {
break;
}
}
// If we're removing the last element in the array, adjust the starting offset so that
// it includes the previous comma on the same line. This avoids turning something like
// `[One, Two, Three]` into `[One,]`. We only do this within the same like, because
// trailing comma at the end of the line is fine.
if (parent.elements[parent.elements.length - 1] === node) {
for (let i = position - 1; i >= 0; i--) {
if (sourceText[i] === ',' || sourceText[i] === ' ') {
position--;
}
else {
break;
}
}
// Replace the node with its leading whitespace to preserve the formatting.
toInsert = leading_space.getLeadingLineWhitespaceOfNode(node);
}
return new project_paths.TextUpdate({ position, end, toInsert });
}
function migrate() {
return async (tree, context) => {
await project_paths.runMigrationInDevkit({
getMigration: () => new UnusedImportsMigration(),
tree,
beforeProgramCreation: (tsconfigPath, stage) => {
if (stage === project_paths.MigrationStage.Analysis) {
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
}
else {
context.logger.info(`Running migration for: ${tsconfigPath}...`);
}
},
beforeUnitAnalysis: (tsconfigPath) => {
context.logger.info(`Scanning for unused imports using ${tsconfigPath}`);
},
afterAnalysisFailure: () => {
context.logger.error('Schematic failed unexpectedly with no analysis data');
},
whenDone: (stats) => {
const { removedImports, changedFiles } = stats.counters;
let statsMessage;
if (removedImports === 0) {
statsMessage = 'Schematic could not find unused imports in the project';
}
else {
statsMessage =
`Removed ${removedImports} import${removedImports !== 1 ? 's' : ''} ` +
`in ${changedFiles} file${changedFiles !== 1 ? 's' : ''}`;
}
context.logger.info('');
context.logger.info(statsMessage);
},
});
};
}
exports.migrate = migrate;

View File

@@ -0,0 +1,319 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
var checker = require('./checker-BwV9MjSQ.cjs');
require('os');
var p = require('path');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var p__namespace = /*#__PURE__*/_interopNamespaceDefault(p);
/** Tracks changes that have to be made for specific files. */
class ChangeTracker {
_printer;
_importRemapper;
_changes = new Map();
_importManager;
_quotesCache = new WeakMap();
constructor(_printer, _importRemapper) {
this._printer = _printer;
this._importRemapper = _importRemapper;
this._importManager = new checker.ImportManager({
shouldUseSingleQuotes: (file) => this._getQuoteKind(file) === 0 /* QuoteKind.SINGLE */,
});
}
/**
* Tracks the insertion of some text.
* @param sourceFile File in which the text is being inserted.
* @param start Index at which the text is insert.
* @param text Text to be inserted.
*/
insertText(sourceFile, index, text) {
this._trackChange(sourceFile, { start: index, text });
}
/**
* Replaces text within a file.
* @param sourceFile File in which to replace the text.
* @param start Index from which to replace the text.
* @param removeLength Length of the text being replaced.
* @param text Text to be inserted instead of the old one.
*/
replaceText(sourceFile, start, removeLength, text) {
this._trackChange(sourceFile, { start, removeLength, text });
}
/**
* Replaces the text of an AST node with a new one.
* @param oldNode Node to be replaced.
* @param newNode New node to be inserted.
* @param emitHint Hint when formatting the text of the new node.
* @param sourceFileWhenPrinting File to use when printing out the new node. This is important
* when copying nodes from one file to another, because TypeScript might not output literal nodes
* without it.
*/
replaceNode(oldNode, newNode, emitHint = ts.EmitHint.Unspecified, sourceFileWhenPrinting) {
const sourceFile = oldNode.getSourceFile();
this.replaceText(sourceFile, oldNode.getStart(), oldNode.getWidth(), this._printer.printNode(emitHint, newNode, sourceFileWhenPrinting || sourceFile));
}
/**
* Removes the text of an AST node from a file.
* @param node Node whose text should be removed.
* @param useFullOffsets Whether to remove the node using its full offset (e.g. `getFullStart`
* rather than `fullStart`). This has the advantage of removing any comments that may be tied
* to the node, but can lead to too much code being deleted.
*/
removeNode(node, useFullOffsets = false) {
this._trackChange(node.getSourceFile(), {
start: useFullOffsets ? node.getFullStart() : node.getStart(),
removeLength: useFullOffsets ? node.getFullWidth() : node.getWidth(),
text: '',
});
}
/**
* Adds an import to a file.
* @param sourceFile File to which to add the import.
* @param symbolName Symbol being imported.
* @param moduleName Module from which the symbol is imported.
* @param alias Alias to use for the import.
*/
addImport(sourceFile, symbolName, moduleName, alias) {
if (this._importRemapper) {
moduleName = this._importRemapper(moduleName, sourceFile.fileName);
}
// It's common for paths to be manipulated with Node's `path` utilties which
// can yield a path with back slashes. Normalize them since outputting such
// paths will also cause TS to escape the forward slashes.
moduleName = normalizePath(moduleName);
if (!this._changes.has(sourceFile)) {
this._changes.set(sourceFile, []);
}
return this._importManager.addImport({
requestedFile: sourceFile,
exportSymbolName: symbolName,
exportModuleSpecifier: moduleName,
unsafeAliasOverride: alias,
});
}
/**
* Removes an import from a file.
* @param sourceFile File from which to remove the import.
* @param symbolName Original name of the symbol to be removed. Used even if the import is aliased.
* @param moduleName Module from which the symbol is imported.
*/
removeImport(sourceFile, symbolName, moduleName) {
// It's common for paths to be manipulated with Node's `path` utilties which
// can yield a path with back slashes. Normalize them since outputting such
// paths will also cause TS to escape the forward slashes.
moduleName = normalizePath(moduleName);
if (!this._changes.has(sourceFile)) {
this._changes.set(sourceFile, []);
}
this._importManager.removeImport(sourceFile, symbolName, moduleName);
}
/**
* Gets the changes that should be applied to all the files in the migration.
* The changes are sorted in the order in which they should be applied.
*/
recordChanges() {
this._recordImports();
return this._changes;
}
/**
* Clear the tracked changes
*/
clearChanges() {
this._changes.clear();
}
/**
* Adds a change to a `ChangesByFile` map.
* @param file File that the change is associated with.
* @param change Change to be added.
*/
_trackChange(file, change) {
const changes = this._changes.get(file);
if (changes) {
// 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);
if (insertIndex === -1) {
changes.push(change);
}
else {
changes.splice(insertIndex, 0, change);
}
}
else {
this._changes.set(file, [change]);
}
}
/** Determines what kind of quotes to use for a specific file. */
_getQuoteKind(sourceFile) {
if (this._quotesCache.has(sourceFile)) {
return this._quotesCache.get(sourceFile);
}
let kind = 0 /* QuoteKind.SINGLE */;
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
kind = statement.moduleSpecifier.getText()[0] === '"' ? 1 /* QuoteKind.DOUBLE */ : 0 /* QuoteKind.SINGLE */;
this._quotesCache.set(sourceFile, kind);
break;
}
}
return kind;
}
/** Records the pending import changes from the import manager. */
_recordImports() {
const { newImports, updatedImports, deletedImports } = this._importManager.finalize();
for (const [original, replacement] of updatedImports) {
this.replaceNode(original, replacement);
}
for (const node of deletedImports) {
this.removeNode(node);
}
for (const [sourceFile] of this._changes) {
const importsToAdd = newImports.get(sourceFile.fileName);
if (!importsToAdd) {
continue;
}
const importLines = [];
let lastImport = null;
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement)) {
lastImport = statement;
}
}
for (const decl of importsToAdd) {
importLines.push(this._printer.printNode(ts.EmitHint.Unspecified, decl, sourceFile));
}
this.insertText(sourceFile, lastImport ? lastImport.getEnd() : 0, (lastImport ? '\n' : '') + importLines.join('\n'));
}
}
}
/** Normalizes a path to use posix separators. */
function normalizePath(path) {
return path.replace(/\\/g, '/');
}
function parseTsconfigFile(tsconfigPath, basePath) {
const { config } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
const parseConfigHost = {
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
fileExists: ts.sys.fileExists,
readDirectory: ts.sys.readDirectory,
readFile: ts.sys.readFile,
};
// Throw if incorrect arguments are passed to this function. Passing relative base paths
// results in root directories not being resolved and in later type checking runtime errors.
// More details can be found here: https://github.com/microsoft/TypeScript/issues/37731.
if (!p__namespace.isAbsolute(basePath)) {
throw Error('Unexpected relative base path has been specified.');
}
return ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, {});
}
/**
* Creates a TypeScript program instance for a TypeScript project within
* the virtual file system tree.
* @param tree Virtual file system tree that contains the source files.
* @param tsconfigPath Virtual file system path that resolves to the TypeScript project.
* @param basePath Base path for the virtual file system tree.
* @param fakeFileRead Optional file reader function. Can be used to overwrite files in
* the TypeScript program, or to add in-memory files (e.g. to add global types).
* @param additionalFiles Additional file paths that should be added to the program.
*/
function createMigrationProgram(tree, tsconfigPath, basePath, fakeFileRead, additionalFiles) {
const { rootNames, options, host } = createProgramOptions(tree, tsconfigPath, basePath, fakeFileRead);
return ts.createProgram(rootNames, options, host);
}
/**
* Creates the options necessary to instantiate a TypeScript program.
* @param tree Virtual file system tree that contains the source files.
* @param tsconfigPath Virtual file system path that resolves to the TypeScript project.
* @param basePath Base path for the virtual file system tree.
* @param fakeFileRead Optional file reader function. Can be used to overwrite files in
* the TypeScript program, or to add in-memory files (e.g. to add global types).
* @param additionalFiles Additional file paths that should be added to the program.
* @param optionOverrides Overrides of the parsed compiler options.
*/
function createProgramOptions(tree, tsconfigPath, basePath, fakeFileRead, additionalFiles, optionOverrides) {
// 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.
tsconfigPath = p.resolve(basePath, tsconfigPath);
const parsed = parseTsconfigFile(tsconfigPath, p.dirname(tsconfigPath));
const options = optionOverrides ? { ...parsed.options, ...optionOverrides } : parsed.options;
const host = createMigrationCompilerHost(tree, options, basePath, fakeFileRead);
return { rootNames: parsed.fileNames.concat([]), options, host };
}
function createMigrationCompilerHost(tree, options, basePath, fakeRead) {
const host = ts.createCompilerHost(options, true);
const defaultReadFile = host.readFile;
// We need to overwrite the host "readFile" method, as we want the TypeScript
// 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) => {
const treeRelativePath = p.relative(basePath, fileName);
let result = 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();
}
// Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset,
// which breaks the CLI UpdateRecorder.
// See: https://github.com/angular/angular/pull/30719
return typeof result === 'string' ? result.replace(/^\uFEFF/, '') : undefined;
};
return host;
}
/**
* Checks whether a file can be migrate by our automated migrations.
* @param basePath Absolute path to the project.
* @param sourceFile File being checked.
* @param program Program that includes the source file.
*/
function canMigrateFile(basePath, sourceFile, program) {
// 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)) {
return false;
}
// Our migrations are set up to create a `Program` from the project's tsconfig and to migrate all
// the files within the program. This can include files that are outside of the Angular CLI
// project. We can't migrate files outside of the project, because our file system interactions
// go through the CLI's `Tree` which assumes that all files are within the project. See:
// https://github.com/angular/angular-cli/blob/0b0961c9c233a825b6e4bb59ab7f0790f9b14676/packages/angular_devkit/schematics/src/tree/host-tree.ts#L131
return !p.relative(basePath, sourceFile.fileName).startsWith('..');
}
exports.ChangeTracker = ChangeTracker;
exports.canMigrateFile = canMigrateFile;
exports.createMigrationProgram = createMigrationProgram;
exports.createProgramOptions = createProgramOptions;
exports.normalizePath = normalizePath;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var p = require('path');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
var compiler_host = require('./compiler_host-C55Cczah.cjs');
var ts = require('typescript');
var imports = require('./imports-CIX-JgAN.cjs');
require('@angular-devkit/core');
require('./checker-BwV9MjSQ.cjs');
require('os');
require('fs');
require('module');
require('url');
const CORE = '@angular/core';
const DIRECTIVE = 'Directive';
const COMPONENT = 'Component';
const PIPE = 'Pipe';
function migrateFile(sourceFile, rewriteFn) {
const changeTracker = new compiler_host.ChangeTracker(ts.createPrinter());
// Check if there are any imports of the `AfterRenderPhase` enum.
const coreImports = imports.getNamedImports(sourceFile, CORE);
if (!coreImports) {
return;
}
const directive = imports.getImportSpecifier(sourceFile, CORE, DIRECTIVE);
const component = imports.getImportSpecifier(sourceFile, CORE, COMPONENT);
const pipe = imports.getImportSpecifier(sourceFile, CORE, PIPE);
if (!directive && !component && !pipe) {
return;
}
ts.forEachChild(sourceFile, function visit(node) {
ts.forEachChild(node, visit);
// First we need to check for class declarations
// Decorators will come after
if (!ts.isClassDeclaration(node)) {
return;
}
ts.getDecorators(node)?.forEach((decorator) => {
if (!ts.isDecorator(decorator)) {
return;
}
const callExpression = decorator.expression;
if (!ts.isCallExpression(callExpression)) {
return;
}
const decoratorIdentifier = callExpression.expression;
if (!ts.isIdentifier(decoratorIdentifier)) {
return;
}
// Checking the identifier of the decorator by comparing to the import specifier
switch (decoratorIdentifier.text) {
case directive?.name.text:
case component?.name.text:
case pipe?.name.text:
break;
default:
// It's not a decorator to migrate
return;
}
const [decoratorArgument] = callExpression.arguments;
if (!decoratorArgument || !ts.isObjectLiteralExpression(decoratorArgument)) {
return;
}
const properties = decoratorArgument.properties;
const standaloneProp = getStandaloneProperty(properties);
const hasImports = decoratorHasImports(decoratorArgument);
// We'll use the presence of imports to keep the migration idempotent
// We need to take care of 3 cases
// - standalone: true => remove the property if we have imports
// - standalone: false => nothing
// - No standalone property => add a standalone: false property if there are no imports
let newProperties;
if (!standaloneProp) {
if (!hasImports) {
const standaloneFalseProperty = ts.factory.createPropertyAssignment('standalone', ts.factory.createFalse());
newProperties = [...properties, standaloneFalseProperty];
}
}
else if (standaloneProp.value === ts.SyntaxKind.TrueKeyword && hasImports) {
// To keep the migration idempotent, we'll only remove the standalone prop when there are imports
newProperties = properties.filter((p) => p !== standaloneProp.property);
}
if (newProperties) {
// At this point we know that we need to add standalone: false or
// remove an existing standalone: true property.
const newPropsArr = ts.factory.createNodeArray(newProperties);
const newFirstArg = ts.factory.createObjectLiteralExpression(newPropsArr, true);
changeTracker.replaceNode(decoratorArgument, newFirstArg);
}
});
});
// Write the changes.
for (const changesInFile of changeTracker.recordChanges().values()) {
for (const change of changesInFile) {
rewriteFn(change.start, change.removeLength ?? 0, change.text);
}
}
}
function getStandaloneProperty(properties) {
for (const prop of properties) {
if (ts.isShorthandPropertyAssignment(prop) && prop.name.text) {
return { property: prop, value: prop.objectAssignmentInitializer };
}
if (isStandaloneProperty(prop)) {
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
return { property: prop, value: prop.initializer.kind };
}
else {
return { property: prop, value: prop.initializer };
}
}
}
return undefined;
}
function isStandaloneProperty(prop) {
return (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'standalone');
}
function decoratorHasImports(decoratorArgument) {
for (const prop of decoratorArgument.properties) {
if (ts.isPropertyAssignment(prop) &&
ts.isIdentifier(prop.name) &&
prop.name.text === 'imports') {
if (prop.initializer.kind === ts.SyntaxKind.ArrayLiteralExpression ||
prop.initializer.kind === ts.SyntaxKind.Identifier) {
return true;
}
}
}
return false;
}
function migrate() {
return async (tree) => {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];
if (!allPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the explicit-standalone-flag migration.');
}
for (const tsconfigPath of allPaths) {
runMigration(tree, tsconfigPath, basePath);
}
};
}
function runMigration(tree, tsconfigPath, basePath) {
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => compiler_host.canMigrateFile(basePath, sourceFile, program));
for (const sourceFile of sourceFiles) {
let update = null;
const rewriter = (startPos, width, text) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
}
update.remove(startPos, width);
if (text !== null) {
update.insertLeft(startPos, text);
}
};
migrateFile(sourceFile, rewriter);
if (update !== null) {
tree.commitUpdate(update);
}
}
}
exports.migrate = migrate;

View File

@@ -0,0 +1,105 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
/** Gets import information about the specified identifier by using the Type checker. */
function getImportOfIdentifier(typeChecker, node) {
const symbol = typeChecker.getSymbolAtLocation(node);
if (!symbol || symbol.declarations === undefined || !symbol.declarations.length) {
return null;
}
const decl = symbol.declarations[0];
if (!ts.isImportSpecifier(decl)) {
return null;
}
const importDecl = decl.parent.parent.parent;
if (!ts.isImportDeclaration(importDecl) || !ts.isStringLiteral(importDecl.moduleSpecifier)) {
return null;
}
return {
// 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,
};
}
/**
* 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:
*
* ```ts
* import { Component, Directive } from '@angular/core';
* import { Foo } from './foo';
* ```
*
* Calling `getImportSpecifier(sourceFile, '@angular/core', 'Directive')` will yield the node
* referring to `Directive` in the top import.
*
* @param sourceFile File in which to look for imports.
* @param moduleName Name of the import's module.
* @param specifierName Original name of the specifier to look for. Aliases will be resolved to
* their original name.
*/
function getImportSpecifier(sourceFile, moduleName, specifierName) {
return getImportSpecifiers(sourceFile, moduleName, specifierName)[0] ?? null;
}
function getImportSpecifiers(sourceFile, moduleName, specifierOrSpecifiers) {
const matches = [];
for (const node of sourceFile.statements) {
if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) {
continue;
}
const namedBindings = node.importClause?.namedBindings;
const isMatch = typeof moduleName === 'string'
? node.moduleSpecifier.text === moduleName
: moduleName.test(node.moduleSpecifier.text);
if (!isMatch || !namedBindings || !ts.isNamedImports(namedBindings)) {
continue;
}
if (typeof specifierOrSpecifiers === 'string') {
const match = findImportSpecifier(namedBindings.elements, specifierOrSpecifiers);
if (match) {
matches.push(match);
}
}
else {
for (const specifierName of specifierOrSpecifiers) {
const match = findImportSpecifier(namedBindings.elements, specifierName);
if (match) {
matches.push(match);
}
}
}
}
return matches;
}
function getNamedImports(sourceFile, moduleName) {
for (const node of sourceFile.statements) {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const isMatch = node.moduleSpecifier.text === moduleName
;
const namedBindings = node.importClause?.namedBindings;
if (isMatch && namedBindings && ts.isNamedImports(namedBindings)) {
return namedBindings;
}
}
}
return null;
}
/** Finds an import specifier with a particular name. */
function findImportSpecifier(nodes, specifierName) {
return nodes.find((element) => {
const { name, propertyName } = element;
return propertyName ? propertyName.text === specifierName : name.text === specifierName;
});
}
exports.getImportOfIdentifier = getImportOfIdentifier;
exports.getImportSpecifier = getImportSpecifier;
exports.getNamedImports = getNamedImports;

View File

@@ -0,0 +1,997 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var checker = require('./checker-BwV9MjSQ.cjs');
var index = require('./index-BnJH1Hc7.cjs');
require('path');
var project_paths = require('./project_paths-DY3SIODd.cjs');
function getMemberName(member) {
if (member.name === undefined) {
return null;
}
if (ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name)) {
return member.name.text;
}
if (ts.isPrivateIdentifier(member.name)) {
return `#${member.name.text}`;
}
return null;
}
/** Checks whether the given node can be an `@Input()` declaration node. */
function isInputContainerNode(node) {
return (((ts.isAccessor(node) && ts.isClassDeclaration(node.parent)) ||
ts.isPropertyDeclaration(node)) &&
getMemberName(node) !== null);
}
/**
* Detects `query(By.directive(T)).componentInstance` patterns and enhances
* them with information of `T`. This is important because `.componentInstance`
* is currently typed as `any` and may cause runtime test failures after input
* migrations then.
*
* The reference resolution pass leverages information from this pattern
* recognizer.
*/
class DebugElementComponentInstance {
checker;
cache = new WeakMap();
constructor(checker) {
this.checker = checker;
}
detect(node) {
if (this.cache.has(node)) {
return this.cache.get(node);
}
if (!ts.isPropertyAccessExpression(node)) {
return null;
}
// Check for `<>.componentInstance`.
if (!ts.isIdentifier(node.name) || node.name.text !== 'componentInstance') {
return null;
}
// Check for `<>.query(..).<>`.
if (!ts.isCallExpression(node.expression) ||
!ts.isPropertyAccessExpression(node.expression.expression) ||
!ts.isIdentifier(node.expression.expression.name) ||
node.expression.expression.name.text !== 'query') {
return null;
}
const queryCall = node.expression;
if (queryCall.arguments.length !== 1) {
return null;
}
const queryArg = queryCall.arguments[0];
let typeExpr;
if (ts.isCallExpression(queryArg) &&
queryArg.arguments.length === 1 &&
ts.isIdentifier(queryArg.arguments[0])) {
// Detect references, like: `query(By.directive(T))`.
typeExpr = queryArg.arguments[0];
}
else if (ts.isIdentifier(queryArg)) {
// Detect references, like: `harness.query(T)`.
typeExpr = queryArg;
}
else {
return null;
}
const symbol = this.checker.getSymbolAtLocation(typeExpr);
if (symbol?.valueDeclaration === undefined ||
!ts.isClassDeclaration(symbol?.valueDeclaration)) {
// Cache this as we use the expensive type checker.
this.cache.set(node, null);
return null;
}
const type = this.checker.getTypeAtLocation(symbol.valueDeclaration);
this.cache.set(node, type);
return type;
}
}
/**
* Recognizes `Partial<T>` instances in Catalyst tests. Those type queries
* are likely used for typing property initialization values for the given class `T`
* and we have a few scenarios:
*
* 1. The API does not unwrap signal inputs. In which case, the values are likely no
* longer assignable to an `InputSignal`.
* 2. The API does unwrap signal inputs, in which case we need to unwrap the `Partial`
* because the values are raw initial values, like they were before.
*
* We can enable this heuristic when we detect Catalyst as we know it supports unwrapping.
*/
class PartialDirectiveTypeInCatalystTests {
checker;
knownFields;
constructor(checker, knownFields) {
this.checker = checker;
this.knownFields = knownFields;
}
detect(node) {
// Detect `Partial<...>`
if (!ts.isTypeReferenceNode(node) ||
!ts.isIdentifier(node.typeName) ||
node.typeName.text !== 'Partial') {
return null;
}
// Ignore if the source file doesn't reference Catalyst.
if (!node.getSourceFile().text.includes('angular2/testing/catalyst')) {
return null;
}
// Extract T of `Partial<T>`.
const cmpTypeArg = node.typeArguments?.[0];
if (!cmpTypeArg ||
!ts.isTypeReferenceNode(cmpTypeArg) ||
!ts.isIdentifier(cmpTypeArg.typeName)) {
return null;
}
const cmpType = cmpTypeArg.typeName;
const symbol = this.checker.getSymbolAtLocation(cmpType);
// Note: Technically the class might be derived of an input-containing class,
// but this is out of scope for now. We can expand if we see it's a common case.
if (symbol?.valueDeclaration === undefined ||
!ts.isClassDeclaration(symbol.valueDeclaration) ||
!this.knownFields.shouldTrackClassReference(symbol.valueDeclaration)) {
return null;
}
return { referenceNode: node, targetClass: symbol.valueDeclaration };
}
}
/**
* Attempts to look up the given property access chain using
* the type checker.
*
* Notably this is not as safe as using the type checker directly to
* retrieve symbols of a given identifier, but in some cases this is
* a necessary approach to compensate e.g. for a lack of TCB information
* when processing Angular templates.
*
* The path is a list of properties to be accessed sequentially on the
* given type.
*/
function lookupPropertyAccess(checker, type, path, options = {}) {
let symbol = null;
for (const propName of path) {
// Note: We support assuming `NonNullable` for the pathl This is necessary
// in some situations as otherwise the lookups would fail to resolve the target
// symbol just because of e.g. a ternary. This is used in the signal input migration
// for host bindings.
type = options.ignoreNullability ? type.getNonNullableType() : type;
const propSymbol = type.getProperty(propName);
if (propSymbol === undefined) {
return null;
}
symbol = propSymbol;
type = checker.getTypeOfSymbol(propSymbol);
}
if (symbol === null) {
return null;
}
return { symbol, type };
}
/**
* AST visitor that iterates through a template and finds all
* input references.
*
* This resolution is important to be able to migrate references to inputs
* that will be migrated to signal inputs.
*/
class TemplateReferenceVisitor extends checker.RecursiveVisitor$1 {
result = [];
/**
* Whether we are currently descending into HTML AST nodes
* where all bound attributes are considered potentially narrowing.
*
* Keeps track of all referenced inputs in such attribute expressions.
*/
templateAttributeReferencedFields = null;
expressionVisitor;
seenKnownFieldsCount = new Map();
constructor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup) {
super();
this.expressionVisitor = new TemplateExpressionReferenceVisitor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup);
}
checkExpressionForReferencedFields(activeNode, expressionNode) {
const referencedFields = this.expressionVisitor.checkTemplateExpression(activeNode, expressionNode);
// Add all references to the overall visitor result.
this.result.push(...referencedFields);
// Count usages of seen input references. We'll use this to make decisions
// based on whether inputs are potentially narrowed or not.
for (const input of referencedFields) {
this.seenKnownFieldsCount.set(input.targetField.key, (this.seenKnownFieldsCount.get(input.targetField.key) ?? 0) + 1);
}
return referencedFields;
}
descendAndCheckForNarrowedSimilarReferences(potentiallyNarrowedInputs, descend) {
const inputs = potentiallyNarrowedInputs.map((i) => ({
ref: i,
key: i.targetField.key,
pastCount: this.seenKnownFieldsCount.get(i.targetField.key) ?? 0,
}));
descend();
for (const input of inputs) {
// Input was referenced inside a narrowable spot, and is used in child nodes.
// This is a sign for the input to be narrowed. Mark it as such.
if ((this.seenKnownFieldsCount.get(input.key) ?? 0) > input.pastCount) {
input.ref.isLikelyNarrowed = true;
}
}
}
visitTemplate(template) {
// Note: We assume all bound expressions for templates may be subject
// to TCB narrowing. This is relevant for now until we support narrowing
// of signal calls in templates.
// TODO: Remove with: https://github.com/angular/angular/pull/55456.
this.templateAttributeReferencedFields = [];
checker.visitAll$1(this, template.attributes);
checker.visitAll$1(this, template.templateAttrs);
// If we are dealing with a microsyntax template, do not check
// inputs and outputs as those are already passed to the children.
// Template attributes may contain relevant expressions though.
if (template.tagName === 'ng-template') {
checker.visitAll$1(this, template.inputs);
checker.visitAll$1(this, template.outputs);
}
const referencedInputs = this.templateAttributeReferencedFields;
this.templateAttributeReferencedFields = null;
this.descendAndCheckForNarrowedSimilarReferences(referencedInputs, () => {
checker.visitAll$1(this, template.children);
checker.visitAll$1(this, template.references);
checker.visitAll$1(this, template.variables);
});
}
visitIfBlockBranch(block) {
if (block.expression) {
const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
super.visitIfBlockBranch(block);
});
}
else {
super.visitIfBlockBranch(block);
}
}
visitForLoopBlock(block) {
this.checkExpressionForReferencedFields(block, block.expression);
this.checkExpressionForReferencedFields(block, block.trackBy);
super.visitForLoopBlock(block);
}
visitSwitchBlock(block) {
const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
super.visitSwitchBlock(block);
});
}
visitSwitchBlockCase(block) {
if (block.expression) {
const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
super.visitSwitchBlockCase(block);
});
}
else {
super.visitSwitchBlockCase(block);
}
}
visitDeferredBlock(deferred) {
if (deferred.triggers.when) {
this.checkExpressionForReferencedFields(deferred, deferred.triggers.when.value);
}
if (deferred.prefetchTriggers.when) {
this.checkExpressionForReferencedFields(deferred, deferred.prefetchTriggers.when.value);
}
super.visitDeferredBlock(deferred);
}
visitBoundText(text) {
this.checkExpressionForReferencedFields(text, text.value);
}
visitBoundEvent(attribute) {
this.checkExpressionForReferencedFields(attribute, attribute.handler);
}
visitBoundAttribute(attribute) {
const referencedFields = this.checkExpressionForReferencedFields(attribute, attribute.value);
// Attributes inside templates are potentially "narrowed" and hence we
// keep track of all referenced inputs to see if they actually are.
if (this.templateAttributeReferencedFields !== null) {
this.templateAttributeReferencedFields.push(...referencedFields);
}
}
}
/**
* Expression AST visitor that checks whether a given expression references
* a known `@Input()`.
*
* This resolution is important to be able to migrate references to inputs
* that will be migrated to signal inputs.
*/
class TemplateExpressionReferenceVisitor extends checker.RecursiveAstVisitor {
typeChecker;
templateTypeChecker;
componentClass;
knownFields;
fieldNamesToConsiderForReferenceLookup;
activeTmplAstNode = null;
detectedInputReferences = [];
isInsideObjectShorthandExpression = false;
insideConditionalExpressionsWithReads = [];
constructor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup) {
super();
this.typeChecker = typeChecker;
this.templateTypeChecker = templateTypeChecker;
this.componentClass = componentClass;
this.knownFields = knownFields;
this.fieldNamesToConsiderForReferenceLookup = fieldNamesToConsiderForReferenceLookup;
}
/** Checks the given AST expression. */
checkTemplateExpression(activeNode, expressionNode) {
this.detectedInputReferences = [];
this.activeTmplAstNode = activeNode;
expressionNode.visit(this, []);
return this.detectedInputReferences;
}
visit(ast, context) {
super.visit(ast, [...context, ast]);
}
// Keep track when we are inside an object shorthand expression. This is
// necessary as we need to expand the shorthand to invoke a potential new signal.
// E.g. `{bla}` may be transformed to `{bla: bla()}`.
visitLiteralMap(ast, context) {
for (const [idx, key] of ast.keys.entries()) {
this.isInsideObjectShorthandExpression = !!key.isShorthandInitialized;
ast.values[idx].visit(this, context);
this.isInsideObjectShorthandExpression = false;
}
}
visitPropertyRead(ast, context) {
this._inspectPropertyAccess(ast, context);
super.visitPropertyRead(ast, context);
}
visitSafePropertyRead(ast, context) {
this._inspectPropertyAccess(ast, context);
super.visitPropertyRead(ast, context);
}
visitPropertyWrite(ast, context) {
this._inspectPropertyAccess(ast, context);
super.visitPropertyWrite(ast, context);
}
visitConditional(ast, context) {
this.visit(ast.condition, context);
this.insideConditionalExpressionsWithReads.push(ast.condition);
this.visit(ast.trueExp, context);
this.visit(ast.falseExp, context);
this.insideConditionalExpressionsWithReads.pop();
}
/**
* Inspects the property access and attempts to resolve whether they access
* a known field. If so, the result is captured.
*/
_inspectPropertyAccess(ast, astPath) {
if (this.fieldNamesToConsiderForReferenceLookup !== null &&
!this.fieldNamesToConsiderForReferenceLookup.has(ast.name)) {
return;
}
const isWrite = !!(ast instanceof checker.PropertyWrite ||
(this.activeTmplAstNode && isTwoWayBindingNode(this.activeTmplAstNode)));
this._checkAccessViaTemplateTypeCheckBlock(ast, isWrite, astPath) ||
this._checkAccessViaOwningComponentClassType(ast, isWrite, astPath);
}
/**
* Checks whether the node refers to an input using the TCB information.
* Type check block may not exist for e.g. test components, so this can return `null`.
*/
_checkAccessViaTemplateTypeCheckBlock(ast, isWrite, astPath) {
// There might be no template type checker. E.g. if we check host bindings.
if (this.templateTypeChecker === null) {
return false;
}
const symbol = this.templateTypeChecker.getSymbolOfNode(ast, this.componentClass);
if (symbol?.kind !== checker.SymbolKind.Expression || symbol.tsSymbol === null) {
return false;
}
// Dangerous: Type checking symbol retrieval is a totally different `ts.Program`,
// than the one where we analyzed `knownInputs`.
// --> Find the input via its input id.
const targetInput = this.knownFields.attemptRetrieveDescriptorFromSymbol(symbol.tsSymbol);
if (targetInput === null) {
return false;
}
this.detectedInputReferences.push({
targetNode: targetInput.node,
targetField: targetInput,
read: ast,
readAstPath: astPath,
context: this.activeTmplAstNode,
isLikelyNarrowed: this._isPartOfNarrowingTernary(ast),
isObjectShorthandExpression: this.isInsideObjectShorthandExpression,
isWrite,
});
return true;
}
/**
* Simple resolution checking whether the given AST refers to a known input.
* This is a fallback for when there is no type checking information (e.g. in host bindings).
*
* It attempts to resolve references by traversing accesses of the "component class" type.
* e.g. `this.bla` is resolved via `CompType#bla` and further.
*/
_checkAccessViaOwningComponentClassType(ast, isWrite, astPath) {
// We might check host bindings, which can never point to template variables or local refs.
const expressionTemplateTarget = this.templateTypeChecker === null
? null
: this.templateTypeChecker.getExpressionTarget(ast, this.componentClass);
// Skip checking if:
// - the reference resolves to a template variable or local ref. No way to resolve without TCB.
// - the owning component does not have a name (should not happen technically).
if (expressionTemplateTarget !== null || this.componentClass.name === undefined) {
return;
}
const property = traverseReceiverAndLookupSymbol(ast, this.componentClass, this.typeChecker);
if (property === null) {
return;
}
const matchingTarget = this.knownFields.attemptRetrieveDescriptorFromSymbol(property);
if (matchingTarget === null) {
return;
}
this.detectedInputReferences.push({
targetNode: matchingTarget.node,
targetField: matchingTarget,
read: ast,
readAstPath: astPath,
context: this.activeTmplAstNode,
isLikelyNarrowed: this._isPartOfNarrowingTernary(ast),
isObjectShorthandExpression: this.isInsideObjectShorthandExpression,
isWrite,
});
}
_isPartOfNarrowingTernary(read) {
// Note: We do not safe check that the reads are fully matching 1:1. This is acceptable
// as worst case we just skip an input from being migrated. This is very unlikely too.
return this.insideConditionalExpressionsWithReads.some((r) => (r instanceof checker.PropertyRead ||
r instanceof checker.PropertyWrite ||
r instanceof checker.SafePropertyRead) &&
r.name === read.name);
}
}
/**
* Emulates an access to a given field using the TypeScript `ts.Type`
* of the given class. The resolved symbol of the access is returned.
*/
function traverseReceiverAndLookupSymbol(readOrWrite, componentClass, checker$1) {
const path = [readOrWrite.name];
let node = readOrWrite;
while (node.receiver instanceof checker.PropertyRead || node.receiver instanceof checker.PropertyWrite) {
node = node.receiver;
path.unshift(node.name);
}
if (!(node.receiver instanceof checker.ImplicitReceiver || node.receiver instanceof checker.ThisReceiver)) {
return null;
}
const classType = checker$1.getTypeAtLocation(componentClass.name);
return (lookupPropertyAccess(checker$1, classType, path, {
// Necessary to avoid breaking the resolution if there is
// some narrowing involved. E.g. `myClass ? myClass.input`.
ignoreNullability: true,
})?.symbol ?? null);
}
/** Whether the given node refers to a two-way binding AST node. */
function isTwoWayBindingNode(node) {
return ((node instanceof checker.BoundAttribute && node.type === checker.BindingType.TwoWay) ||
(node instanceof checker.BoundEvent && node.type === checker.ParsedEventType.TwoWay));
}
/** Possible types of references to known fields detected. */
exports.ReferenceKind = void 0;
(function (ReferenceKind) {
ReferenceKind[ReferenceKind["InTemplate"] = 0] = "InTemplate";
ReferenceKind[ReferenceKind["InHostBinding"] = 1] = "InHostBinding";
ReferenceKind[ReferenceKind["TsReference"] = 2] = "TsReference";
ReferenceKind[ReferenceKind["TsClassTypeReference"] = 3] = "TsClassTypeReference";
})(exports.ReferenceKind || (exports.ReferenceKind = {}));
/** Whether the given reference is a TypeScript reference. */
function isTsReference(ref) {
return ref.kind === exports.ReferenceKind.TsReference;
}
/** Whether the given reference is a template reference. */
function isTemplateReference(ref) {
return ref.kind === exports.ReferenceKind.InTemplate;
}
/** Whether the given reference is a host binding reference. */
function isHostBindingReference(ref) {
return ref.kind === exports.ReferenceKind.InHostBinding;
}
/**
* Whether the given reference is a TypeScript `ts.Type` reference
* to a class containing known fields.
*/
function isTsClassTypeReference(ref) {
return ref.kind === exports.ReferenceKind.TsClassTypeReference;
}
/**
* Checks host bindings of the given class and tracks all
* references to inputs within bindings.
*/
function identifyHostBindingReferences(node, programInfo, checker$1, reflector, result, knownFields, fieldNamesToConsiderForReferenceLookup) {
if (node.name === undefined) {
return;
}
const decorators = reflector.getDecoratorsOfDeclaration(node);
if (decorators === null) {
return;
}
const angularDecorators = checker.getAngularDecorators(decorators, ['Directive', 'Component'],
/* isAngularCore */ false);
if (angularDecorators.length === 0) {
return;
}
// Assume only one Angular decorator per class.
const ngDecorator = angularDecorators[0];
if (ngDecorator.args?.length !== 1) {
return;
}
const metadataNode = checker.unwrapExpression(ngDecorator.args[0]);
if (!ts.isObjectLiteralExpression(metadataNode)) {
return;
}
const metadata = checker.reflectObjectLiteral(metadataNode);
if (!metadata.has('host')) {
return;
}
let hostField = checker.unwrapExpression(metadata.get('host'));
// Special-case in case host bindings are shared via a variable.
// e.g. Material button shares host bindings as a constant in the same target.
if (ts.isIdentifier(hostField)) {
let symbol = checker$1.getSymbolAtLocation(hostField);
// Plain identifier references can point to alias symbols (e.g. imports).
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
symbol = checker$1.getAliasedSymbol(symbol);
}
if (symbol !== undefined &&
symbol.valueDeclaration !== undefined &&
ts.isVariableDeclaration(symbol.valueDeclaration)) {
hostField = symbol?.valueDeclaration.initializer;
}
}
if (hostField === undefined || !ts.isObjectLiteralExpression(hostField)) {
return;
}
const hostMap = checker.reflectObjectLiteral(hostField);
const expressionResult = [];
const expressionVisitor = new TemplateExpressionReferenceVisitor(checker$1, null, node, knownFields, fieldNamesToConsiderForReferenceLookup);
for (const [rawName, expression] of hostMap.entries()) {
if (!ts.isStringLiteralLike(expression)) {
continue;
}
const isEventBinding = rawName.startsWith('(');
const isPropertyBinding = rawName.startsWith('[');
// Only migrate property or event bindings.
if (!isPropertyBinding && !isEventBinding) {
continue;
}
const parser = checker.makeBindingParser();
const sourceSpan = new checker.ParseSourceSpan(
// Fake source span to keep parsing offsets zero-based.
// We then later combine these with the expression TS node offsets.
new checker.ParseLocation({ content: '', url: '' }, 0, 0, 0), new checker.ParseLocation({ content: '', url: '' }, 0, 0, 0));
const name = rawName.substring(1, rawName.length - 1);
let parsed = undefined;
if (isEventBinding) {
const result = [];
parser.parseEvent(name.substring(1, name.length - 1), expression.text, false, sourceSpan, sourceSpan, [], result, sourceSpan);
parsed = result[0].handler;
}
else {
const result = [];
parser.parsePropertyBinding(name, expression.text, true,
/* isTwoWayBinding */ false, sourceSpan, 0, sourceSpan, [], result, sourceSpan);
parsed = result[0].expression;
}
if (parsed != null) {
expressionResult.push(...expressionVisitor.checkTemplateExpression(expression, parsed));
}
}
for (const ref of expressionResult) {
result.references.push({
kind: exports.ReferenceKind.InHostBinding,
from: {
read: ref.read,
readAstPath: ref.readAstPath,
isObjectShorthandExpression: ref.isObjectShorthandExpression,
isWrite: ref.isWrite,
file: project_paths.projectFile(ref.context.getSourceFile(), programInfo),
hostPropertyNode: ref.context,
},
target: ref.targetField,
});
}
}
/**
* Attempts to extract the `TemplateDefinition` for the given
* class, if possible.
*
* The definition can then be used with the Angular compiler to
* load/parse the given template.
*/
function attemptExtractTemplateDefinition(node, checker$1, reflector, resourceLoader) {
const classDecorators = reflector.getDecoratorsOfDeclaration(node);
const evaluator = new index.PartialEvaluator(reflector, checker$1, null);
const ngDecorators = classDecorators !== null
? checker.getAngularDecorators(classDecorators, ['Component'], /* isAngularCore */ false)
: [];
if (ngDecorators.length === 0 ||
ngDecorators[0].args === null ||
ngDecorators[0].args.length === 0 ||
!ts.isObjectLiteralExpression(ngDecorators[0].args[0])) {
return null;
}
const properties = checker.reflectObjectLiteral(ngDecorators[0].args[0]);
const templateProp = properties.get('template');
const templateUrlProp = properties.get('templateUrl');
const containingFile = node.getSourceFile().fileName;
// inline template.
if (templateProp !== undefined) {
const templateStr = evaluator.evaluate(templateProp);
if (typeof templateStr === 'string') {
return {
isInline: true,
expression: templateProp,
interpolationConfig: checker.DEFAULT_INTERPOLATION_CONFIG,
preserveWhitespaces: false,
resolvedTemplateUrl: containingFile,
templateUrl: containingFile,
};
}
}
try {
// external template.
if (templateUrlProp !== undefined) {
const templateUrl = evaluator.evaluate(templateUrlProp);
if (typeof templateUrl === 'string') {
return {
isInline: false,
interpolationConfig: checker.DEFAULT_INTERPOLATION_CONFIG,
preserveWhitespaces: false,
templateUrlExpression: templateUrlProp,
templateUrl,
resolvedTemplateUrl: resourceLoader.resolve(templateUrl, containingFile),
};
}
}
}
catch (e) {
console.error(`Could not parse external template: ${e}`);
}
return null;
}
/**
* Checks whether the given class has an Angular template, and resolves
* all of the references to inputs.
*/
function identifyTemplateReferences(programInfo, node, reflector, checker$1, evaluator, templateTypeChecker, resourceLoader, options, result, knownFields, fieldNamesToConsiderForReferenceLookup) {
const template = templateTypeChecker.getTemplate(node, checker.OptimizeFor.WholeProgram) ??
// If there is no template registered in the TCB or compiler, the template may
// be skipped due to an explicit `jit: true` setting. We try to detect this case
// and parse the template manually.
extractTemplateWithoutCompilerAnalysis(node, checker$1, reflector, resourceLoader, evaluator, options);
if (template !== null) {
const visitor = new TemplateReferenceVisitor(checker$1, templateTypeChecker, node, knownFields, fieldNamesToConsiderForReferenceLookup);
template.forEach((node) => node.visit(visitor));
for (const res of visitor.result) {
const templateFilePath = res.context.sourceSpan.start.file.url;
// Templates without an URL are non-mappable artifacts of e.g.
// string concatenated templates. See the `indirect` template
// source mapping concept in the compiler. We skip such references
// as those cannot be migrated, but print an error for now.
if (templateFilePath === '') {
// TODO: Incorporate a TODO potentially.
console.error(`Found reference to field ${res.targetField.key} that cannot be ` +
`migrated because the template cannot be parsed with source map information ` +
`(in file: ${node.getSourceFile().fileName}).`);
continue;
}
result.references.push({
kind: exports.ReferenceKind.InTemplate,
from: {
read: res.read,
readAstPath: res.readAstPath,
node: res.context,
isObjectShorthandExpression: res.isObjectShorthandExpression,
originatingTsFile: project_paths.projectFile(node.getSourceFile(), programInfo),
templateFile: project_paths.projectFile(checker.absoluteFrom(templateFilePath), programInfo),
isLikelyPartOfNarrowing: res.isLikelyNarrowed,
isWrite: res.isWrite,
},
target: res.targetField,
});
}
}
}
/**
* Attempts to extract a `@Component` template from the given class,
* without relying on the `NgCompiler` program analysis.
*
* This is useful for JIT components using `jit: true` which were not
* processed by the Angular compiler, but may still have templates that
* contain references to inputs that we can resolve via the fallback
* reference resolutions (that does not use the type check block).
*/
function extractTemplateWithoutCompilerAnalysis(node, checker$1, reflector, resourceLoader, evaluator, options) {
if (node.name === undefined) {
return null;
}
const tmplDef = attemptExtractTemplateDefinition(node, checker$1, reflector, resourceLoader);
if (tmplDef === null) {
return null;
}
return index.extractTemplate(node, tmplDef, evaluator, null, resourceLoader, {
enableBlockSyntax: true,
enableLetSyntax: true,
usePoisonedData: true,
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat !== false,
i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs === true,
}, checker.CompilationMode.FULL).nodes;
}
/** Gets the pattern and property name for a given binding element. */
function resolveBindingElement(node) {
const name = node.propertyName ?? node.name;
// If we are discovering a non-analyzable element in the path, abort.
if (!ts.isStringLiteralLike(name) && !ts.isIdentifier(name)) {
return null;
}
return {
pattern: node.parent,
propertyName: name.text,
};
}
/** Gets the declaration node of the given binding element. */
function getBindingElementDeclaration(node) {
while (true) {
if (ts.isBindingElement(node.parent.parent)) {
node = node.parent.parent;
}
else {
return node.parent.parent;
}
}
}
/**
* Expands the given reference to its containing expression, capturing
* the full context.
*
* E.g. `traverseAccess(ref<`bla`>)` may return `this.bla`
* or `traverseAccess(ref<`bla`>)` may return `this.someObj.a.b.c.bla`.
*
* This helper is useful as we will replace the full access with a temporary
* variable for narrowing. Replacing just the identifier is wrong.
*/
function traverseAccess(access) {
if (ts.isPropertyAccessExpression(access.parent) && access.parent.name === access) {
return access.parent;
}
else if (ts.isElementAccessExpression(access.parent) &&
access.parent.argumentExpression === access) {
return access.parent;
}
return access;
}
/**
* Unwraps the parent of the given node, if it's a
* parenthesized expression or `as` expression.
*/
function unwrapParent(node) {
if (ts.isParenthesizedExpression(node.parent)) {
return unwrapParent(node.parent);
}
else if (ts.isAsExpression(node.parent)) {
return unwrapParent(node.parent);
}
return node;
}
/**
* List of binary operators that indicate a write operation.
*
* Useful for figuring out whether an expression assigns to
* something or not.
*/
const writeBinaryOperators = [
ts.SyntaxKind.EqualsToken,
ts.SyntaxKind.BarBarEqualsToken,
ts.SyntaxKind.BarEqualsToken,
ts.SyntaxKind.AmpersandEqualsToken,
ts.SyntaxKind.AmpersandAmpersandEqualsToken,
ts.SyntaxKind.SlashEqualsToken,
ts.SyntaxKind.MinusEqualsToken,
ts.SyntaxKind.PlusEqualsToken,
ts.SyntaxKind.CaretEqualsToken,
ts.SyntaxKind.PercentEqualsToken,
ts.SyntaxKind.AsteriskEqualsToken,
ts.SyntaxKind.ExclamationEqualsToken,
];
/**
* Checks whether given TypeScript reference refers to an Angular input, and captures
* the reference if possible.
*
* @param fieldNamesToConsiderForReferenceLookup List of field names that should be
* respected when expensively looking up references to known fields.
* May be null if all identifiers should be inspected.
*/
function identifyPotentialTypeScriptReference(node, programInfo, checker, knownFields, result, fieldNamesToConsiderForReferenceLookup, advisors) {
// Skip all identifiers that never can point to a migrated field.
// TODO: Capture these assumptions and performance optimizations in the design doc.
if (fieldNamesToConsiderForReferenceLookup !== null &&
!fieldNamesToConsiderForReferenceLookup.has(node.text)) {
return;
}
let target = undefined;
try {
// Resolve binding elements to their declaration symbol.
// Commonly inputs are accessed via object expansion. e.g. `const {input} = this;`.
if (ts.isBindingElement(node.parent)) {
// Skip binding elements that are using spread.
if (node.parent.dotDotDotToken !== undefined) {
return;
}
const bindingInfo = resolveBindingElement(node.parent);
if (bindingInfo === null) {
// The declaration could not be resolved. Skip analyzing this.
return;
}
const bindingType = checker.getTypeAtLocation(bindingInfo.pattern);
const resolved = lookupPropertyAccess(checker, bindingType, [bindingInfo.propertyName]);
target = resolved?.symbol;
}
else {
target = checker.getSymbolAtLocation(node);
}
}
catch (e) {
console.error('Unexpected error while trying to resolve identifier reference:');
console.error(e);
// Gracefully skip analyzing. This can happen when e.g. a reference is named similar
// to an input, but is dependant on `.d.ts` that is not necessarily available (clutz dts).
return;
}
noTargetSymbolCheck: if (target === undefined) {
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
const propAccessSymbol = checker.getSymbolAtLocation(node.parent.expression);
if (propAccessSymbol !== undefined &&
propAccessSymbol.valueDeclaration !== undefined &&
ts.isVariableDeclaration(propAccessSymbol.valueDeclaration) &&
propAccessSymbol.valueDeclaration.initializer !== undefined) {
target = advisors.debugElComponentInstanceTracker
.detect(propAccessSymbol.valueDeclaration.initializer)
?.getProperty(node.text);
// We found a target in the fallback path. Break out.
if (target !== undefined) {
break noTargetSymbolCheck;
}
}
}
return;
}
let targetInput = knownFields.attemptRetrieveDescriptorFromSymbol(target);
if (targetInput === null) {
return;
}
const access = unwrapParent(traverseAccess(node));
const accessParent = access.parent;
const isWriteReference = ts.isBinaryExpression(accessParent) &&
accessParent.left === access &&
writeBinaryOperators.includes(accessParent.operatorToken.kind);
// track accesses from source files to known fields.
result.references.push({
kind: exports.ReferenceKind.TsReference,
from: {
node,
file: project_paths.projectFile(node.getSourceFile(), programInfo),
isWrite: isWriteReference,
isPartOfElementBinding: ts.isBindingElement(node.parent),
},
target: targetInput,
});
}
/**
* Phase where we iterate through all source file references and
* detect references to known fields (e.g. commonly inputs).
*
* This is useful, for example in the signal input migration whe
* references need to be migrated to unwrap signals, given that
* their target properties is no longer holding a raw value, but
* instead an `InputSignal`.
*
* This phase detects references in all types of locations:
* - TS source files
* - Angular templates (inline or external)
* - Host binding expressions.
*/
function createFindAllSourceFileReferencesVisitor(programInfo, checker, reflector, resourceLoader, evaluator, templateTypeChecker, knownFields, fieldNamesToConsiderForReferenceLookup, result) {
const debugElComponentInstanceTracker = new DebugElementComponentInstance(checker);
const partialDirectiveCatalystTracker = new PartialDirectiveTypeInCatalystTests(checker, knownFields);
const perfCounters = {
template: 0,
hostBindings: 0,
tsReferences: 0,
tsTypes: 0,
};
// Schematic NodeJS execution may not have `global.performance` defined.
const currentTimeInMs = () => typeof global.performance !== 'undefined' ? global.performance.now() : Date.now();
const visitor = (node) => {
let lastTime = currentTimeInMs();
// Note: If there is no template type checker and resource loader, we aren't processing
// an Angular program, and can skip template detection.
if (ts.isClassDeclaration(node) && templateTypeChecker !== null && resourceLoader !== null) {
identifyTemplateReferences(programInfo, node, reflector, checker, evaluator, templateTypeChecker, resourceLoader, programInfo.userOptions, result, knownFields, fieldNamesToConsiderForReferenceLookup);
perfCounters.template += (currentTimeInMs() - lastTime) / 1000;
lastTime = currentTimeInMs();
identifyHostBindingReferences(node, programInfo, checker, reflector, result, knownFields, fieldNamesToConsiderForReferenceLookup);
perfCounters.hostBindings += (currentTimeInMs() - lastTime) / 1000;
lastTime = currentTimeInMs();
}
lastTime = currentTimeInMs();
// find references, but do not capture input declarations itself.
if (ts.isIdentifier(node) &&
!(isInputContainerNode(node.parent) && node.parent.name === node)) {
identifyPotentialTypeScriptReference(node, programInfo, checker, knownFields, result, fieldNamesToConsiderForReferenceLookup, {
debugElComponentInstanceTracker,
});
}
perfCounters.tsReferences += (currentTimeInMs() - lastTime) / 1000;
lastTime = currentTimeInMs();
// Detect `Partial<T>` references.
// Those are relevant to be tracked as they may be updated in Catalyst to
// unwrap signal inputs. Commonly people use `Partial` in Catalyst to type
// some "component initial values".
const partialDirectiveInCatalyst = partialDirectiveCatalystTracker.detect(node);
if (partialDirectiveInCatalyst !== null) {
result.references.push({
kind: exports.ReferenceKind.TsClassTypeReference,
from: {
file: project_paths.projectFile(partialDirectiveInCatalyst.referenceNode.getSourceFile(), programInfo),
node: partialDirectiveInCatalyst.referenceNode,
},
isPartialReference: true,
isPartOfCatalystFile: true,
target: partialDirectiveInCatalyst.targetClass,
});
}
perfCounters.tsTypes += (currentTimeInMs() - lastTime) / 1000;
};
return {
visitor,
debugPrintMetrics: () => {
console.info('Source file analysis performance', perfCounters);
},
};
}
exports.createFindAllSourceFileReferencesVisitor = createFindAllSourceFileReferencesVisitor;
exports.getBindingElementDeclaration = getBindingElementDeclaration;
exports.getMemberName = getMemberName;
exports.isHostBindingReference = isHostBindingReference;
exports.isInputContainerNode = isInputContainerNode;
exports.isTemplateReference = isTemplateReference;
exports.isTsClassTypeReference = isTsClassTypeReference;
exports.isTsReference = isTsReference;
exports.traverseAccess = traverseAccess;
exports.unwrapParent = unwrapParent;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
/**
* Gets the leading line whitespace of a given node.
*
* Useful for inserting e.g. TODOs without breaking indentation.
*/
function getLeadingLineWhitespaceOfNode(node) {
const fullText = node.getFullText().substring(0, node.getStart() - node.getFullStart());
let result = '';
for (let i = fullText.length - 1; i > -1; i--) {
// Note: LF line endings are `\n` while CRLF are `\r\n`. This logic should cover both, because
// we start from the beginning of the node and go backwards so will always hit `\n` first.
if (fullText[i] !== '\n') {
result = fullText[i] + result;
}
else {
break;
}
}
return result;
}
exports.getLeadingLineWhitespaceOfNode = getLeadingLineWhitespaceOfNode;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
var imports = require('./imports-CIX-JgAN.cjs');
function getCallDecoratorImport(typeChecker, decorator) {
// 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)) {
return null;
}
const identifier = decorator.expression.expression;
return imports.getImportOfIdentifier(typeChecker, identifier);
}
/**
* Gets all decorators which are imported from an Angular package (e.g. "@angular/core")
* from a list of decorators.
*/
function getAngularDecorators(typeChecker, decorators) {
return decorators
.map((node) => ({ node, importData: getCallDecoratorImport(typeChecker, node) }))
.filter(({ importData }) => importData && importData.importModule.startsWith('@angular/'))
.map(({ node, importData }) => ({
node: node,
name: importData.name,
moduleName: importData.importModule,
importNode: importData.node,
}));
}
exports.getAngularDecorators = getAngularDecorators;

View File

@@ -0,0 +1,23 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
/** Find the closest parent node of a particular kind. */
function closestNode(node, predicate) {
let current = node.parent;
while (current && !ts.isSourceFile(current)) {
if (predicate(current)) {
return current;
}
current = current.parent;
}
return null;
}
exports.closestNode = closestNode;

View File

@@ -0,0 +1,608 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var checker = require('./checker-BwV9MjSQ.cjs');
var index$1 = require('./index-BnJH1Hc7.cjs');
require('path');
var project_paths = require('./project_paths-DY3SIODd.cjs');
var apply_import_manager = require('./apply_import_manager-DF0BUe6N.cjs');
var index = require('./index-B6p5mHIY.cjs');
require('@angular-devkit/core');
require('node:path/posix');
require('fs');
require('module');
require('url');
require('@angular-devkit/schematics');
require('./project_tsconfig_paths-CDVxT6Ov.cjs');
function isOutputDeclarationEligibleForMigration(node) {
return (node.initializer !== undefined &&
ts.isNewExpression(node.initializer) &&
ts.isIdentifier(node.initializer.expression) &&
node.initializer.expression.text === 'EventEmitter');
}
function isPotentialOutputCallUsage(node, name) {
if (ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
ts.isIdentifier(node.expression.name)) {
return node.expression?.name.text === name;
}
else {
return false;
}
}
function isPotentialPipeCallUsage(node) {
return isPotentialOutputCallUsage(node, 'pipe');
}
function isPotentialNextCallUsage(node) {
return isPotentialOutputCallUsage(node, 'next');
}
function isPotentialCompleteCallUsage(node) {
return isPotentialOutputCallUsage(node, 'complete');
}
function isTargetOutputDeclaration(node, checker, reflector, dtsReader) {
const targetSymbol = checker.getSymbolAtLocation(node);
if (targetSymbol !== undefined) {
const propertyDeclaration = getTargetPropertyDeclaration(targetSymbol);
if (propertyDeclaration !== null &&
isOutputDeclaration(propertyDeclaration, reflector, dtsReader)) {
return propertyDeclaration;
}
}
return null;
}
/** Gets whether the given property is an Angular `@Output`. */
function isOutputDeclaration(node, reflector, dtsReader) {
// `.d.ts` file, so we check the `static ecmp` metadata on the `declare class`.
if (node.getSourceFile().isDeclarationFile) {
if (!ts.isIdentifier(node.name) ||
!ts.isClassDeclaration(node.parent) ||
node.parent.name === undefined) {
return false;
}
const ref = new checker.Reference(node.parent);
const directiveMeta = dtsReader.getDirectiveMetadata(ref);
return !!directiveMeta?.outputs.getByClassPropertyName(node.name.text);
}
// `.ts` file, so we check for the `@Output()` decorator.
return getOutputDecorator(node, reflector) !== null;
}
function getTargetPropertyDeclaration(targetSymbol) {
const valDeclaration = targetSymbol.valueDeclaration;
if (valDeclaration !== undefined && ts.isPropertyDeclaration(valDeclaration)) {
return valDeclaration;
}
return null;
}
/** Returns Angular `@Output` decorator or null when a given property declaration is not an @Output */
function getOutputDecorator(node, reflector) {
const decorators = reflector.getDecoratorsOfDeclaration(node);
const ngDecorators = decorators !== null ? checker.getAngularDecorators(decorators, ['Output'], /* isCore */ false) : [];
return ngDecorators.length > 0 ? ngDecorators[0] : null;
}
// THINK: this utility + type is not specific to @Output, really, maybe move it to tsurge?
/** Computes an unique ID for a given Angular `@Output` property. */
function getUniqueIdForProperty(info, prop) {
const { id } = project_paths.projectFile(prop.getSourceFile(), info);
id.replace(/\.d\.ts$/, '.ts');
return `${id}@@${prop.parent.name ?? 'unknown-class'}@@${prop.name.getText()}`;
}
function isTestRunnerImport(node) {
if (ts.isImportDeclaration(node)) {
const moduleSpecifier = node.moduleSpecifier.getText();
return moduleSpecifier.includes('jasmine') || moduleSpecifier.includes('catalyst');
}
return false;
}
// TODO: code duplication with signals migration - sort it out
/**
* Gets whether the given read is used to access
* the specified field.
*
* E.g. whether `<my-read>.toArray` is detected.
*/
function checkNonTsReferenceAccessesField(ref, fieldName) {
const readFromPath = ref.from.readAstPath.at(-1);
const parentRead = ref.from.readAstPath.at(-2);
if (ref.from.read !== readFromPath) {
return null;
}
if (!(parentRead instanceof checker.PropertyRead) || parentRead.name !== fieldName) {
return null;
}
return parentRead;
}
/**
* Gets whether the given reference is accessed to call the
* specified function on it.
*
* E.g. whether `<my-read>.toArray()` is detected.
*/
function checkNonTsReferenceCallsField(ref, fieldName) {
const propertyAccess = checkNonTsReferenceAccessesField(ref, fieldName);
if (propertyAccess === null) {
return null;
}
const accessIdx = ref.from.readAstPath.indexOf(propertyAccess);
if (accessIdx === -1) {
return null;
}
const potentialRead = ref.from.readAstPath[accessIdx];
if (potentialRead === undefined) {
return null;
}
return potentialRead;
}
const printer = ts.createPrinter();
function calculateDeclarationReplacement(info, node, aliasParam) {
const sf = node.getSourceFile();
let payloadTypes;
if (node.initializer && ts.isNewExpression(node.initializer) && node.initializer.typeArguments) {
payloadTypes = node.initializer.typeArguments;
}
else if (node.type && ts.isTypeReferenceNode(node.type) && node.type.typeArguments) {
payloadTypes = ts.factory.createNodeArray(node.type.typeArguments);
}
const outputCall = ts.factory.createCallExpression(ts.factory.createIdentifier('output'), payloadTypes, aliasParam !== undefined
? [
ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment('alias', ts.factory.createStringLiteral(aliasParam, true)),
], false),
]
: []);
const existingModifiers = (node.modifiers ?? []).filter((modifier) => !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.ReadonlyKeyword);
const updatedOutputDeclaration = ts.factory.createPropertyDeclaration(
// Think: this logic of dealing with modifiers is applicable to all signal-based migrations
ts.factory.createNodeArray([
...existingModifiers,
ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword),
]), node.name, undefined, undefined, outputCall);
return prepareTextReplacementForNode(info, node, printer.printNode(ts.EmitHint.Unspecified, updatedOutputDeclaration, sf));
}
function calculateImportReplacements(info, sourceFiles) {
const importReplacements = {};
for (const sf of sourceFiles) {
const importManager = new checker.ImportManager();
const addOnly = [];
const addRemove = [];
const file = project_paths.projectFile(sf, info);
importManager.addImport({
requestedFile: sf,
exportModuleSpecifier: '@angular/core',
exportSymbolName: 'output',
});
apply_import_manager.applyImportManagerChanges(importManager, addOnly, [sf], info);
importManager.removeImport(sf, 'Output', '@angular/core');
importManager.removeImport(sf, 'EventEmitter', '@angular/core');
apply_import_manager.applyImportManagerChanges(importManager, addRemove, [sf], info);
importReplacements[file.id] = {
add: addOnly,
addAndRemove: addRemove,
};
}
return importReplacements;
}
function calculateNextFnReplacement(info, node) {
return prepareTextReplacementForNode(info, node, 'emit');
}
function calculateNextFnReplacementInTemplate(file, span) {
return prepareTextReplacement(file, 'emit', span.start, span.end);
}
function calculateNextFnReplacementInHostBinding(file, offset, span) {
return prepareTextReplacement(file, 'emit', offset + span.start, offset + span.end);
}
function calculateCompleteCallReplacement(info, node) {
return prepareTextReplacementForNode(info, node, '', node.getFullStart());
}
function calculatePipeCallReplacement(info, node) {
if (ts.isPropertyAccessExpression(node.expression)) {
const sf = node.getSourceFile();
const importManager = new checker.ImportManager();
const outputToObservableIdent = importManager.addImport({
requestedFile: sf,
exportModuleSpecifier: '@angular/core/rxjs-interop',
exportSymbolName: 'outputToObservable',
});
const toObsCallExp = ts.factory.createCallExpression(outputToObservableIdent, undefined, [
node.expression.expression,
]);
const pipePropAccessExp = ts.factory.updatePropertyAccessExpression(node.expression, toObsCallExp, node.expression.name);
const pipeCallExp = ts.factory.updateCallExpression(node, pipePropAccessExp, [], node.arguments);
const replacements = [
prepareTextReplacementForNode(info, node, printer.printNode(ts.EmitHint.Unspecified, pipeCallExp, sf)),
];
apply_import_manager.applyImportManagerChanges(importManager, replacements, [sf], info);
return replacements;
}
else {
// TODO: assert instead?
throw new Error(`Unexpected call expression for .pipe - expected a property access but got "${node.getText()}"`);
}
}
function prepareTextReplacementForNode(info, node, replacement, start) {
const sf = node.getSourceFile();
return new project_paths.Replacement(project_paths.projectFile(sf, info), new project_paths.TextUpdate({
position: start ?? node.getStart(),
end: node.getEnd(),
toInsert: replacement,
}));
}
function prepareTextReplacement(file, replacement, start, end) {
return new project_paths.Replacement(file, new project_paths.TextUpdate({
position: start,
end: end,
toInsert: replacement,
}));
}
class OutputMigration extends project_paths.TsurgeFunnelMigration {
config;
constructor(config = {}) {
super();
this.config = config;
}
async analyze(info) {
const { sourceFiles, program } = info;
const outputFieldReplacements = {};
const problematicUsages = {};
let problematicDeclarationCount = 0;
const filesWithOutputDeclarations = new Set();
const checker$1 = program.getTypeChecker();
const reflector = new checker.TypeScriptReflectionHost(checker$1);
const dtsReader = new index$1.DtsMetadataReader(checker$1, reflector);
const evaluator = new index$1.PartialEvaluator(reflector, checker$1, null);
const resourceLoader = info.ngCompiler?.['resourceManager'] ?? null;
// Pre-analyze the program and get access to the template type checker.
// If we are processing a non-Angular target, there is no template info.
const { templateTypeChecker } = info.ngCompiler?.['ensureAnalyzed']() ?? {
templateTypeChecker: null,
};
const knownFields = {
// Note: We don't support cross-target migration of `Partial<T>` usages.
// This is an acceptable limitation for performance reasons.
shouldTrackClassReference: () => false,
attemptRetrieveDescriptorFromSymbol: (s) => {
const propDeclaration = getTargetPropertyDeclaration(s);
if (propDeclaration !== null) {
const classFieldID = getUniqueIdForProperty(info, propDeclaration);
if (classFieldID !== null) {
return {
node: propDeclaration,
key: classFieldID,
};
}
}
return null;
},
};
let isTestFile = false;
const outputMigrationVisitor = (node) => {
// detect output declarations
if (ts.isPropertyDeclaration(node)) {
const outputDecorator = getOutputDecorator(node, reflector);
if (outputDecorator !== null) {
if (isOutputDeclarationEligibleForMigration(node)) {
const outputDef = {
id: getUniqueIdForProperty(info, node),
aliasParam: outputDecorator.args?.at(0),
};
const outputFile = project_paths.projectFile(node.getSourceFile(), info);
if (this.config.shouldMigrate === undefined ||
this.config.shouldMigrate({
key: outputDef.id,
node: node,
}, outputFile)) {
const aliasParam = outputDef.aliasParam;
const aliasOptionValue = aliasParam ? evaluator.evaluate(aliasParam) : undefined;
if (aliasOptionValue == undefined || typeof aliasOptionValue === 'string') {
filesWithOutputDeclarations.add(node.getSourceFile());
addOutputReplacement(outputFieldReplacements, outputDef.id, outputFile, calculateDeclarationReplacement(info, node, aliasOptionValue?.toString()));
}
else {
problematicUsages[outputDef.id] = true;
problematicDeclarationCount++;
}
}
}
else {
problematicDeclarationCount++;
}
}
}
// detect .next usages that should be migrated to .emit
if (isPotentialNextCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
if (propertyDeclaration !== null) {
const id = getUniqueIdForProperty(info, propertyDeclaration);
const outputFile = project_paths.projectFile(node.getSourceFile(), info);
addOutputReplacement(outputFieldReplacements, id, outputFile, calculateNextFnReplacement(info, node.expression.name));
}
}
// detect .complete usages that should be removed
if (isPotentialCompleteCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
if (propertyDeclaration !== null) {
const id = getUniqueIdForProperty(info, propertyDeclaration);
const outputFile = project_paths.projectFile(node.getSourceFile(), info);
if (ts.isExpressionStatement(node.parent)) {
addOutputReplacement(outputFieldReplacements, id, outputFile, calculateCompleteCallReplacement(info, node.parent));
}
else {
problematicUsages[id] = true;
}
}
}
addCommentForEmptyEmit(node, info, checker$1, reflector, dtsReader, outputFieldReplacements);
// detect imports of test runners
if (isTestRunnerImport(node)) {
isTestFile = true;
}
// detect unsafe access of the output property
if (isPotentialPipeCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
if (propertyDeclaration !== null) {
const id = getUniqueIdForProperty(info, propertyDeclaration);
if (isTestFile) {
const outputFile = project_paths.projectFile(node.getSourceFile(), info);
addOutputReplacement(outputFieldReplacements, id, outputFile, ...calculatePipeCallReplacement(info, node));
}
else {
problematicUsages[id] = true;
}
}
}
ts.forEachChild(node, outputMigrationVisitor);
};
// calculate output migration replacements
for (const sf of sourceFiles) {
isTestFile = false;
ts.forEachChild(sf, outputMigrationVisitor);
}
// take care of the references in templates and host bindings
const referenceResult = { references: [] };
const { visitor: templateHostRefVisitor } = index.createFindAllSourceFileReferencesVisitor(info, checker$1, reflector, resourceLoader, evaluator, templateTypeChecker, knownFields, null, // TODO: capture known output names as an optimization
referenceResult);
// calculate template / host binding replacements
for (const sf of sourceFiles) {
ts.forEachChild(sf, templateHostRefVisitor);
}
for (const ref of referenceResult.references) {
// detect .next usages that should be migrated to .emit in template and host binding expressions
if (ref.kind === index.ReferenceKind.InTemplate) {
const callExpr = checkNonTsReferenceCallsField(ref, 'next');
// TODO: here and below for host bindings, we should ideally filter in the global meta stage
// (instead of using the `outputFieldReplacements` map)
// as technically, the call expression could refer to an output
// from a whole different compilation unit (e.g. tsconfig.json).
if (callExpr !== null && outputFieldReplacements[ref.target.key] !== undefined) {
addOutputReplacement(outputFieldReplacements, ref.target.key, ref.from.templateFile, calculateNextFnReplacementInTemplate(ref.from.templateFile, callExpr.nameSpan));
}
}
else if (ref.kind === index.ReferenceKind.InHostBinding) {
const callExpr = checkNonTsReferenceCallsField(ref, 'next');
if (callExpr !== null && outputFieldReplacements[ref.target.key] !== undefined) {
addOutputReplacement(outputFieldReplacements, ref.target.key, ref.from.file, calculateNextFnReplacementInHostBinding(ref.from.file, ref.from.hostPropertyNode.getStart() + 1, callExpr.nameSpan));
}
}
}
// calculate import replacements but do so only for files that have output declarations
const importReplacements = calculateImportReplacements(info, filesWithOutputDeclarations);
return project_paths.confirmAsSerializable({
problematicDeclarationCount,
outputFields: outputFieldReplacements,
importReplacements,
problematicUsages,
});
}
async combine(unitA, unitB) {
const outputFields = {};
const importReplacements = {};
const problematicUsages = {};
let problematicDeclarationCount = 0;
for (const unit of [unitA, unitB]) {
for (const declIdStr of Object.keys(unit.outputFields)) {
const declId = declIdStr;
// THINK: detect clash? Should we have an utility to merge data based on unique IDs?
outputFields[declId] = unit.outputFields[declId];
}
for (const fileIDStr of Object.keys(unit.importReplacements)) {
const fileID = fileIDStr;
importReplacements[fileID] = unit.importReplacements[fileID];
}
problematicDeclarationCount += unit.problematicDeclarationCount;
}
for (const unit of [unitA, unitB]) {
for (const declIdStr of Object.keys(unit.problematicUsages)) {
const declId = declIdStr;
problematicUsages[declId] = unit.problematicUsages[declId];
}
}
return project_paths.confirmAsSerializable({
problematicDeclarationCount,
outputFields,
importReplacements,
problematicUsages,
});
}
async globalMeta(combinedData) {
const globalMeta = {
importReplacements: combinedData.importReplacements,
outputFields: combinedData.outputFields,
problematicDeclarationCount: combinedData.problematicDeclarationCount,
problematicUsages: {},
};
for (const keyStr of Object.keys(combinedData.problematicUsages)) {
const key = keyStr;
// it might happen that a problematic usage is detected but we didn't see the declaration - skipping those
if (globalMeta.outputFields[key] !== undefined) {
globalMeta.problematicUsages[key] = true;
}
}
// Noop here as we don't have any form of special global metadata.
return project_paths.confirmAsSerializable(combinedData);
}
async stats(globalMetadata) {
const detectedOutputs = new Set(Object.keys(globalMetadata.outputFields)).size +
globalMetadata.problematicDeclarationCount;
const problematicOutputs = new Set(Object.keys(globalMetadata.problematicUsages)).size +
globalMetadata.problematicDeclarationCount;
const successRate = detectedOutputs > 0 ? (detectedOutputs - problematicOutputs) / detectedOutputs : 1;
return {
counters: {
detectedOutputs,
problematicOutputs,
successRate,
},
};
}
async migrate(globalData) {
const migratedFiles = new Set();
const problematicFiles = new Set();
const replacements = [];
for (const declIdStr of Object.keys(globalData.outputFields)) {
const declId = declIdStr;
const outputField = globalData.outputFields[declId];
if (!globalData.problematicUsages[declId]) {
replacements.push(...outputField.replacements);
migratedFiles.add(outputField.file.id);
}
else {
problematicFiles.add(outputField.file.id);
}
}
for (const fileIDStr of Object.keys(globalData.importReplacements)) {
const fileID = fileIDStr;
if (migratedFiles.has(fileID)) {
const importReplacements = globalData.importReplacements[fileID];
if (problematicFiles.has(fileID)) {
replacements.push(...importReplacements.add);
}
else {
replacements.push(...importReplacements.addAndRemove);
}
}
}
return { replacements };
}
}
function addOutputReplacement(outputFieldReplacements, outputId, file, ...replacements) {
let existingReplacements = outputFieldReplacements[outputId];
if (existingReplacements === undefined) {
outputFieldReplacements[outputId] = existingReplacements = {
file: file,
replacements: [],
};
}
existingReplacements.replacements.push(...replacements);
}
function addCommentForEmptyEmit(node, info, checker, reflector, dtsReader, outputFieldReplacements) {
if (!isEmptyEmitCall(node))
return;
const propertyAccess = getPropertyAccess(node);
if (!propertyAccess)
return;
const symbol = checker.getSymbolAtLocation(propertyAccess.name);
if (!symbol || !symbol.declarations?.length)
return;
const propertyDeclaration = isTargetOutputDeclaration(propertyAccess, checker, reflector, dtsReader);
if (!propertyDeclaration)
return;
const eventEmitterType = getEventEmitterArgumentType(propertyDeclaration);
if (!eventEmitterType)
return;
const id = getUniqueIdForProperty(info, propertyDeclaration);
const file = project_paths.projectFile(node.getSourceFile(), info);
const formatter = getFormatterText(node);
const todoReplacement = new project_paths.TextUpdate({
toInsert: `${formatter.indent}// TODO: The 'emit' function requires a mandatory ${eventEmitterType} argument\n`,
end: formatter.lineStartPos,
position: formatter.lineStartPos,
});
addOutputReplacement(outputFieldReplacements, id, file, new project_paths.Replacement(file, todoReplacement));
}
function isEmptyEmitCall(node) {
return (ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'emit' &&
node.arguments.length === 0);
}
function getPropertyAccess(node) {
const propertyAccessExpression = node.expression.expression;
return ts.isPropertyAccessExpression(propertyAccessExpression) ? propertyAccessExpression : null;
}
function getEventEmitterArgumentType(propertyDeclaration) {
const initializer = propertyDeclaration.initializer;
if (!initializer || !ts.isNewExpression(initializer))
return null;
const isEventEmitter = ts.isIdentifier(initializer.expression) && initializer.expression.getText() === 'EventEmitter';
if (!isEventEmitter)
return null;
const [typeArg] = initializer.typeArguments ?? [];
return typeArg ? typeArg.getText() : null;
}
function getFormatterText(node) {
const sourceFile = node.getSourceFile();
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
const lineStartPos = sourceFile.getPositionOfLineAndCharacter(line, 0);
const indent = sourceFile.text.slice(lineStartPos, node.getStart());
return { indent, lineStartPos };
}
function migrate(options) {
return async (tree, context) => {
await project_paths.runMigrationInDevkit({
tree,
getMigration: (fs) => new OutputMigration({
shouldMigrate: (_, file) => {
return (file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
!/(^|\/)node_modules\//.test(file.rootRelativePath));
},
}),
beforeProgramCreation: (tsconfigPath, stage) => {
if (stage === project_paths.MigrationStage.Analysis) {
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
}
else {
context.logger.info(`Running migration for: ${tsconfigPath}...`);
}
},
afterProgramCreation: (info, fs) => {
const analysisPath = fs.resolve(options.analysisDir);
// Support restricting the analysis to subfolders for larger projects.
if (analysisPath !== '/') {
info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
}
},
beforeUnitAnalysis: (tsconfigPath) => {
context.logger.info(`Scanning for outputs: ${tsconfigPath}...`);
},
afterAllAnalyzed: () => {
context.logger.info(``);
context.logger.info(`Processing analysis data between targets...`);
context.logger.info(``);
},
afterAnalysisFailure: () => {
context.logger.error('Migration failed unexpectedly with no analysis data');
},
whenDone: ({ counters }) => {
const { detectedOutputs, problematicOutputs, successRate } = counters;
const migratedOutputs = detectedOutputs - problematicOutputs;
const successRatePercent = (successRate * 100).toFixed(2);
context.logger.info('');
context.logger.info(`Successfully migrated to outputs as functions 🎉`);
context.logger.info(` -> Migrated ${migratedOutputs} out of ${detectedOutputs} detected outputs (${successRatePercent} %).`);
},
});
};
}
exports.migrate = migrate;

View File

@@ -0,0 +1,97 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var p = require('path');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
var compiler_host = require('./compiler_host-C55Cczah.cjs');
var ts = require('typescript');
var imports = require('./imports-CIX-JgAN.cjs');
require('@angular-devkit/core');
require('./checker-BwV9MjSQ.cjs');
require('os');
require('fs');
require('module');
require('url');
const CORE = '@angular/core';
const EXPERIMENTAL_PENDING_TASKS = 'ExperimentalPendingTasks';
function migrateFile(sourceFile, typeChecker, rewriteFn) {
const changeTracker = new compiler_host.ChangeTracker(ts.createPrinter());
// Check if there are any imports of the `AfterRenderPhase` enum.
const coreImports = imports.getNamedImports(sourceFile, CORE);
if (!coreImports) {
return;
}
const importSpecifier = imports.getImportSpecifier(sourceFile, CORE, EXPERIMENTAL_PENDING_TASKS);
if (!importSpecifier) {
return;
}
const nodeToReplace = importSpecifier.propertyName ?? importSpecifier.name;
if (!ts.isIdentifier(nodeToReplace)) {
return;
}
changeTracker.replaceNode(nodeToReplace, ts.factory.createIdentifier('PendingTasks'));
ts.forEachChild(sourceFile, function visit(node) {
// import handled above
if (ts.isImportDeclaration(node)) {
return;
}
if (ts.isIdentifier(node) &&
node.text === EXPERIMENTAL_PENDING_TASKS &&
imports.getImportOfIdentifier(typeChecker, node)?.name === EXPERIMENTAL_PENDING_TASKS) {
changeTracker.replaceNode(node, ts.factory.createIdentifier('PendingTasks'));
}
ts.forEachChild(node, visit);
});
// Write the changes.
for (const changesInFile of changeTracker.recordChanges().values()) {
for (const change of changesInFile) {
rewriteFn(change.start, change.removeLength ?? 0, change.text);
}
}
}
function migrate() {
return async (tree) => {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];
if (!allPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the afterRender phase migration.');
}
for (const tsconfigPath of allPaths) {
runMigration(tree, tsconfigPath, basePath);
}
};
}
function runMigration(tree, tsconfigPath, basePath) {
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => compiler_host.canMigrateFile(basePath, sourceFile, program));
for (const sourceFile of sourceFiles) {
let update = null;
const rewriter = (startPos, width, text) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
}
update.remove(startPos, width);
if (text !== null) {
update.insertLeft(startPos, text);
}
};
migrateFile(sourceFile, program.getTypeChecker(), rewriter);
if (update !== null) {
tree.commitUpdate(update);
}
}
}
exports.migrate = migrate;

View File

@@ -0,0 +1,784 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var index = require('./index-BnJH1Hc7.cjs');
var schematics = require('@angular-devkit/schematics');
var core = require('@angular-devkit/core');
var posixPath = require('node:path/posix');
var os = require('os');
var ts = require('typescript');
var checker = require('./checker-BwV9MjSQ.cjs');
require('path');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var posixPath__namespace = /*#__PURE__*/_interopNamespaceDefault(posixPath);
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
/// <reference types="node" />
class NgtscCompilerHost {
fs;
options;
constructor(fs, options = {}) {
this.fs = fs;
this.options = options;
}
getSourceFile(fileName, languageVersion) {
const text = this.readFile(fileName);
return text !== undefined
? ts.createSourceFile(fileName, text, languageVersion, true)
: undefined;
}
getDefaultLibFileName(options) {
return this.fs.join(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options));
}
getDefaultLibLocation() {
return this.fs.getDefaultLibLocation();
}
writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles) {
const path = checker.absoluteFrom(fileName);
this.fs.ensureDir(this.fs.dirname(path));
this.fs.writeFile(path, data);
}
getCurrentDirectory() {
return this.fs.pwd();
}
getCanonicalFileName(fileName) {
return this.useCaseSensitiveFileNames() ? fileName : fileName.toLowerCase();
}
useCaseSensitiveFileNames() {
return this.fs.isCaseSensitive();
}
getNewLine() {
switch (this.options.newLine) {
case ts.NewLineKind.CarriageReturnLineFeed:
return '\r\n';
case ts.NewLineKind.LineFeed:
return '\n';
default:
return os__namespace.EOL;
}
}
fileExists(fileName) {
const absPath = this.fs.resolve(fileName);
return this.fs.exists(absPath) && this.fs.stat(absPath).isFile();
}
readFile(fileName) {
const absPath = this.fs.resolve(fileName);
if (!this.fileExists(absPath)) {
return undefined;
}
return this.fs.readFile(absPath);
}
realpath(path) {
return this.fs.realpath(this.fs.resolve(path));
}
}
// We use TypeScript's native `ts.matchFiles` utility for the virtual file systems
// and their TypeScript compiler host `readDirectory` implementation. TypeScript's
// function implements complex logic for matching files with respect to root
// directory, extensions, excludes, includes etc. The function is currently
// internal but we can use it as the API most likely will not change any time soon,
// nor does it seem like this is being made public any time soon.
// Related issue for tracking: https://github.com/microsoft/TypeScript/issues/13793.
/**
* Creates a {@link ts.CompilerHost#readDirectory} implementation function,
* that leverages the specified file system (that may be e.g. virtual).
*/
function createFileSystemTsReadDirectoryFn(fs) {
if (ts.matchFiles === undefined) {
throw Error('Unable to read directory in configured file system. This means that ' +
'TypeScript changed its file matching internals.\n\nPlease consider downgrading your ' +
'TypeScript version, and report an issue in the Angular framework repository.');
}
const matchFilesFn = ts.matchFiles.bind(ts);
return (rootDir, extensions, excludes, includes, depth) => {
const directoryExists = (p) => {
const resolvedPath = fs.resolve(p);
return fs.exists(resolvedPath) && fs.stat(resolvedPath).isDirectory();
};
return matchFilesFn(rootDir, extensions, excludes, includes, fs.isCaseSensitive(), fs.pwd(), depth, (p) => {
const resolvedPath = fs.resolve(p);
// TS also gracefully returns an empty file set.
if (!directoryExists(resolvedPath)) {
return { directories: [], files: [] };
}
const children = fs.readdir(resolvedPath);
const files = [];
const directories = [];
for (const child of children) {
if (fs.stat(fs.join(resolvedPath, child))?.isDirectory()) {
directories.push(child);
}
else {
files.push(child);
}
}
return { files, directories };
}, (p) => fs.resolve(p), (p) => directoryExists(p));
};
}
function calcProjectFileAndBasePath(project, host = checker.getFileSystem()) {
const absProject = host.resolve(project);
const projectIsDir = host.lstat(absProject).isDirectory();
const projectFile = projectIsDir ? host.join(absProject, 'tsconfig.json') : absProject;
const projectDir = projectIsDir ? absProject : host.dirname(absProject);
const basePath = host.resolve(projectDir);
return { projectFile, basePath };
}
function readConfiguration(project, existingOptions, host = checker.getFileSystem()) {
try {
const fs = checker.getFileSystem();
const readConfigFile = (configFile) => ts.readConfigFile(configFile, (file) => host.readFile(host.resolve(file)));
const readAngularCompilerOptions = (configFile, parentOptions = {}) => {
const { config, error } = readConfigFile(configFile);
if (error) {
// Errors are handled later on by 'parseJsonConfigFileContent'
return parentOptions;
}
// Note: In Google, `angularCompilerOptions` are stored in `bazelOptions`.
// This function typically doesn't run for actual Angular compilations, but
// tooling like Tsurge, or schematics may leverage this helper, so we account
// for this here.
const angularCompilerOptions = config.angularCompilerOptions ?? config.bazelOptions?.angularCompilerOptions;
// we are only interested into merging 'angularCompilerOptions' as
// other options like 'compilerOptions' are merged by TS
let existingNgCompilerOptions = { ...angularCompilerOptions, ...parentOptions };
if (!config.extends) {
return existingNgCompilerOptions;
}
const extendsPaths = typeof config.extends === 'string' ? [config.extends] : config.extends;
// Call readAngularCompilerOptions recursively to merge NG Compiler options
// Reverse the array so the overrides happen from right to left.
return [...extendsPaths].reverse().reduce((prevOptions, extendsPath) => {
const extendedConfigPath = getExtendedConfigPath(configFile, extendsPath, host, fs);
return extendedConfigPath === null
? prevOptions
: readAngularCompilerOptions(extendedConfigPath, prevOptions);
}, existingNgCompilerOptions);
};
const { projectFile, basePath } = calcProjectFileAndBasePath(project, host);
const configFileName = host.resolve(host.pwd(), projectFile);
const { config, error } = readConfigFile(projectFile);
if (error) {
return {
project,
errors: [error],
rootNames: [],
options: {},
emitFlags: index.EmitFlags.Default,
};
}
const existingCompilerOptions = {
genDir: basePath,
basePath,
...readAngularCompilerOptions(configFileName),
...existingOptions,
};
const parseConfigHost = createParseConfigHost(host, fs);
const { options, errors, fileNames: rootNames, projectReferences, } = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingCompilerOptions, configFileName);
let emitFlags = index.EmitFlags.Default;
if (!(options['skipMetadataEmit'] || options['flatModuleOutFile'])) {
emitFlags |= index.EmitFlags.Metadata;
}
if (options['skipTemplateCodegen']) {
emitFlags = emitFlags & ~index.EmitFlags.Codegen;
}
return { project: projectFile, rootNames, projectReferences, options, errors, emitFlags };
}
catch (e) {
const errors = [
{
category: ts.DiagnosticCategory.Error,
messageText: e.stack ?? e.message,
file: undefined,
start: undefined,
length: undefined,
source: 'angular',
code: index.UNKNOWN_ERROR_CODE,
},
];
return { project: '', errors, rootNames: [], options: {}, emitFlags: index.EmitFlags.Default };
}
}
function createParseConfigHost(host, fs = checker.getFileSystem()) {
return {
fileExists: host.exists.bind(host),
readDirectory: createFileSystemTsReadDirectoryFn(fs),
readFile: host.readFile.bind(host),
useCaseSensitiveFileNames: fs.isCaseSensitive(),
};
}
function getExtendedConfigPath(configFile, extendsValue, host, fs) {
const result = getExtendedConfigPathWorker(configFile, extendsValue, host, fs);
if (result !== null) {
return result;
}
// Try to resolve the paths with a json extension append a json extension to the file in case if
// it is missing and the resolution failed. This is to replicate TypeScript behaviour, see:
// https://github.com/microsoft/TypeScript/blob/294a5a7d784a5a95a8048ee990400979a6bc3a1c/src/compiler/commandLineParser.ts#L2806
return getExtendedConfigPathWorker(configFile, `${extendsValue}.json`, host, fs);
}
function getExtendedConfigPathWorker(configFile, extendsValue, host, fs) {
if (extendsValue.startsWith('.') || fs.isRooted(extendsValue)) {
const extendedConfigPath = host.resolve(host.dirname(configFile), extendsValue);
if (host.exists(extendedConfigPath)) {
return extendedConfigPath;
}
}
else {
const parseConfigHost = createParseConfigHost(host, fs);
// Path isn't a rooted or relative path, resolve like a module.
const { resolvedModule } = ts.nodeModuleNameResolver(extendsValue, configFile, { moduleResolution: ts.ModuleResolutionKind.Node10, resolveJsonModule: true }, parseConfigHost);
if (resolvedModule) {
return checker.absoluteFrom(resolvedModule.resolvedFileName);
}
}
return null;
}
/**
* Angular compiler file system implementation that leverages an
* CLI schematic virtual file tree.
*/
class DevkitMigrationFilesystem {
tree;
constructor(tree) {
this.tree = tree;
}
extname(path) {
return core.extname(path);
}
isRoot(path) {
return path === core.normalize('/');
}
isRooted(path) {
return this.normalize(path).startsWith('/');
}
dirname(file) {
return this.normalize(core.dirname(file));
}
join(basePath, ...paths) {
return this.normalize(core.join(basePath, ...paths));
}
relative(from, to) {
return this.normalize(core.relative(from, to));
}
basename(filePath, extension) {
return posixPath__namespace.basename(filePath, extension);
}
normalize(path) {
return core.normalize(path);
}
resolve(...paths) {
const normalizedPaths = paths.map((p) => core.normalize(p));
// In dev-kit, the NodeJS working directory should never be
// considered, so `/` is the last resort over `cwd`.
return this.normalize(posixPath__namespace.resolve(core.normalize('/'), ...normalizedPaths));
}
pwd() {
return '/';
}
isCaseSensitive() {
return true;
}
exists(path) {
return statPath(this.tree, path) !== null;
}
readFile(path) {
return this.tree.readText(path);
}
readFileBuffer(path) {
const buffer = this.tree.read(path);
if (buffer === null) {
throw new Error(`File does not exist: ${path}`);
}
return buffer;
}
readdir(path) {
const dir = this.tree.getDir(path);
return [
...dir.subdirs,
...dir.subfiles,
];
}
lstat(path) {
const stat = statPath(this.tree, path);
if (stat === null) {
throw new Error(`File does not exist for "lstat": ${path}`);
}
return stat;
}
stat(path) {
const stat = statPath(this.tree, path);
if (stat === null) {
throw new Error(`File does not exist for "stat": ${path}`);
}
return stat;
}
realpath(filePath) {
return filePath;
}
getDefaultLibLocation() {
return 'node_modules/typescript/lib';
}
ensureDir(path) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#ensureDir is not supported.');
}
writeFile(path, data) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#writeFile is not supported.');
}
removeFile(path) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#removeFile is not supported.');
}
copyFile(from, to) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#copyFile is not supported.');
}
moveFile(from, to) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#moveFile is not supported.');
}
removeDeep(path) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#removeDeep is not supported.');
}
chdir(_path) {
throw new Error('FileSystem#chdir is not supported.');
}
symlink() {
throw new Error('FileSystem#symlink is not supported.');
}
}
/** Stats the given path in the virtual tree. */
function statPath(tree, path) {
let fileInfo = null;
let dirInfo = null;
try {
fileInfo = tree.get(path);
}
catch (e) {
if (e.constructor.name === 'PathIsDirectoryException') {
dirInfo = tree.getDir(path);
}
else {
throw e;
}
}
if (fileInfo !== null || dirInfo !== null) {
return {
isDirectory: () => dirInfo !== null,
isFile: () => fileInfo !== null,
isSymbolicLink: () => false,
};
}
return null;
}
/**
* Groups the given replacements per project relative
* file path.
*
* This allows for simple execution of the replacements
* against a given file. E.g. via {@link applyTextUpdates}.
*/
function groupReplacementsByFile(replacements) {
const result = new Map();
for (const { projectFile, update } of replacements) {
if (!result.has(projectFile.rootRelativePath)) {
result.set(projectFile.rootRelativePath, []);
}
result.get(projectFile.rootRelativePath).push(update);
}
return result;
}
/**
* Synchronously combines unit data for the given migration.
*
* Note: This helper is useful for testing and execution of
* Tsurge migrations in non-batchable environments. In general,
* prefer parallel execution of combining via e.g. Beam combiners.
*/
async function synchronouslyCombineUnitData(migration, unitDatas) {
if (unitDatas.length === 0) {
return null;
}
if (unitDatas.length === 1) {
return unitDatas[0];
}
let combined = unitDatas[0];
for (let i = 1; i < unitDatas.length; i++) {
const other = unitDatas[i];
combined = await migration.combine(combined, other);
}
return combined;
}
/**
* By default, Tsurge will always create an Angular compiler program
* for projects analyzed and migrated. This works perfectly fine in
* third-party where Tsurge migrations run in Angular CLI projects.
*
* In first party, when running against full Google3, creating an Angular
* program for e.g. plain `ts_library` targets is overly expensive and
* can result in out of memory issues for large TS targets. In 1P we can
* reliably distinguish between TS and Angular targets via the `angularCompilerOptions`.
*/
function google3UsePlainTsProgramIfNoKnownAngularOption() {
return process.env['GOOGLE3_TSURGE'] === '1';
}
/** Options that are good defaults for Tsurge migrations. */
const defaultMigrationTsOptions = {
// Avoid checking libraries to speed up migrations.
skipLibCheck: true,
skipDefaultLibCheck: true,
noEmit: true,
// Does not apply to g3 and externally is enforced when the app is built by the compiler.
disableTypeScriptVersionCheck: true,
};
/**
* Creates an instance of a TypeScript program for the given project.
*/
function createPlainTsProgram(tsHost, tsconfig, optionOverrides) {
const program = ts.createProgram({
rootNames: tsconfig.rootNames,
options: {
...tsconfig.options,
...defaultMigrationTsOptions,
...optionOverrides,
},
});
return {
ngCompiler: null,
program,
userOptions: tsconfig.options,
__programAbsoluteRootFileNames: tsconfig.rootNames,
host: tsHost,
};
}
/**
* Parses the configuration of the given TypeScript project and creates
* an instance of the Angular compiler for the project.
*/
function createNgtscProgram(tsHost, tsconfig, optionOverrides) {
const ngtscProgram = new index.NgtscProgram(tsconfig.rootNames, {
...tsconfig.options,
...defaultMigrationTsOptions,
...optionOverrides,
}, tsHost);
// Expose an easy way to debug-print ng semantic diagnostics.
if (process.env['DEBUG_NG_SEMANTIC_DIAGNOSTICS'] === '1') {
console.error(ts.formatDiagnosticsWithColorAndContext(ngtscProgram.getNgSemanticDiagnostics(), tsHost));
}
return {
ngCompiler: ngtscProgram.compiler,
program: ngtscProgram.getTsProgram(),
userOptions: tsconfig.options,
__programAbsoluteRootFileNames: tsconfig.rootNames,
host: tsHost,
};
}
/** Code of the error raised by TypeScript when a tsconfig doesn't match any files. */
const NO_INPUTS_ERROR_CODE = 18003;
/** Parses the given tsconfig file, supporting Angular compiler options. */
function parseTsconfigOrDie(absoluteTsconfigPath, fs) {
const tsconfig = readConfiguration(absoluteTsconfigPath, {}, fs);
// Skip the "No inputs found..." error since we don't want to interrupt the migration if a
// tsconfig doesn't match a file. This will result in an empty `Program` which is still valid.
const errors = tsconfig.errors.filter((diag) => diag.code !== NO_INPUTS_ERROR_CODE);
if (errors.length) {
throw new Error(`Tsconfig could not be parsed or is invalid:\n\n` + `${errors.map((e) => e.messageText)}`);
}
return tsconfig;
}
/** Creates the base program info for the given tsconfig path. */
function createBaseProgramInfo(absoluteTsconfigPath, fs, optionOverrides = {}) {
if (fs === undefined) {
fs = new checker.NodeJSFileSystem();
checker.setFileSystem(fs);
}
const tsconfig = parseTsconfigOrDie(absoluteTsconfigPath, fs);
const tsHost = new NgtscCompilerHost(fs, tsconfig.options);
// When enabled, use a plain TS program if we are sure it's not
// an Angular project based on the `tsconfig.json`.
if (google3UsePlainTsProgramIfNoKnownAngularOption() &&
tsconfig.options['_useHostForImportGeneration'] === undefined) {
return createPlainTsProgram(tsHost, tsconfig, optionOverrides);
}
return createNgtscProgram(tsHost, tsconfig, optionOverrides);
}
/**
* Creates the {@link ProgramInfo} from the given base information.
*
* This function purely exists to support custom programs that are
* intended to be injected into Tsurge migrations. e.g. for language
* service refactorings.
*/
function getProgramInfoFromBaseInfo(baseInfo) {
const fullProgramSourceFiles = [...baseInfo.program.getSourceFiles()];
const sourceFiles = fullProgramSourceFiles.filter((f) => !f.isDeclarationFile &&
// Note `isShim` will work for the initial program, but for TCB programs, the shims are no longer annotated.
!checker.isShim(f) &&
!f.fileName.endsWith('.ngtypecheck.ts'));
// Sort it by length in reverse order (longest first). This speeds up lookups,
// since there's no need to keep going through the array once a match is found.
const sortedRootDirs = checker.getRootDirs(baseInfo.host, baseInfo.userOptions).sort((a, b) => b.length - a.length);
// TODO: Consider also following TS's logic here, finding the common source root.
// See: Program#getCommonSourceDirectory.
const primaryRoot = checker.absoluteFrom(baseInfo.userOptions.rootDir ?? sortedRootDirs.at(-1) ?? baseInfo.program.getCurrentDirectory());
return {
...baseInfo,
sourceFiles,
fullProgramSourceFiles,
sortedRootDirs,
projectRoot: primaryRoot,
};
}
/**
* @private
*
* Base class for the possible Tsurge migration variants.
*
* For example, this class exposes methods to conveniently create
* TypeScript programs, while also allowing migration authors to override.
*/
class TsurgeBaseMigration {
/**
* Creates the TypeScript program for a given compilation unit.
*
* By default:
* - In 3P: Ngtsc programs are being created.
* - In 1P: Ngtsc or TS programs are created based on the Blaze target.
*/
createProgram(tsconfigAbsPath, fs, optionsOverride) {
return getProgramInfoFromBaseInfo(createBaseProgramInfo(tsconfigAbsPath, fs, optionsOverride));
}
}
/**
* A simpler variant of a {@link TsurgeComplexMigration} that does not
* fan-out into multiple workers per compilation unit to compute
* the final migration replacements.
*
* This is faster and less resource intensive as workers and TS programs
* are only ever created once.
*
* This is commonly the case when migrations are refactored to eagerly
* compute replacements in the analyze stage, and then leverage the
* global unit data to filter replacements that turned out to be "invalid".
*/
class TsurgeFunnelMigration extends TsurgeBaseMigration {
}
/**
* Complex variant of a `Tsurge` migration.
*
* For example, every analyze worker may contribute to a list of TS
* references that are later combined. The migrate phase can then compute actual
* file updates for all individual compilation units, leveraging the global metadata
* to e.g. see if there are any references from other compilation units that may be
* problematic and prevent migration of a given file.
*/
class TsurgeComplexMigration extends TsurgeBaseMigration {
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
exports.MigrationStage = void 0;
(function (MigrationStage) {
/** The migration is analyzing an entrypoint */
MigrationStage[MigrationStage["Analysis"] = 0] = "Analysis";
/** The migration is about to migrate an entrypoint */
MigrationStage[MigrationStage["Migrate"] = 1] = "Migrate";
})(exports.MigrationStage || (exports.MigrationStage = {}));
/** Runs a Tsurge within an Angular Devkit context. */
async function runMigrationInDevkit(config) {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(config.tree);
if (!buildPaths.length && !testPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the migration.');
}
const tsconfigPaths = [...buildPaths, ...testPaths];
const fs = new DevkitMigrationFilesystem(config.tree);
checker.setFileSystem(fs);
const migration = config.getMigration(fs);
const unitResults = [];
const isFunnelMigration = migration instanceof TsurgeFunnelMigration;
const compilationUnitAssignments = new Map();
for (const tsconfigPath of tsconfigPaths) {
config.beforeProgramCreation?.(tsconfigPath, exports.MigrationStage.Analysis);
const info = migration.createProgram(tsconfigPath, fs);
modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments);
config.afterProgramCreation?.(info, fs, exports.MigrationStage.Analysis);
config.beforeUnitAnalysis?.(tsconfigPath);
unitResults.push(await migration.analyze(info));
}
config.afterAllAnalyzed?.();
const combined = await synchronouslyCombineUnitData(migration, unitResults);
if (combined === null) {
config.afterAnalysisFailure?.();
return;
}
const globalMeta = await migration.globalMeta(combined);
let replacements;
if (isFunnelMigration) {
replacements = (await migration.migrate(globalMeta)).replacements;
}
else {
replacements = [];
for (const tsconfigPath of tsconfigPaths) {
config.beforeProgramCreation?.(tsconfigPath, exports.MigrationStage.Migrate);
const info = migration.createProgram(tsconfigPath, fs);
modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments);
config.afterProgramCreation?.(info, fs, exports.MigrationStage.Migrate);
const result = await migration.migrate(globalMeta, info);
replacements.push(...result.replacements);
}
}
const replacementsPerFile = new Map();
const changesPerFile = groupReplacementsByFile(replacements);
for (const [file, changes] of changesPerFile) {
if (!replacementsPerFile.has(file)) {
replacementsPerFile.set(file, changes);
}
}
for (const [file, changes] of replacementsPerFile) {
const recorder = config.tree.beginUpdate(file);
for (const c of changes) {
recorder
.remove(c.data.position, c.data.end - c.data.position)
.insertRight(c.data.position, c.data.toInsert);
}
config.tree.commitUpdate(recorder);
}
config.whenDone?.(await migration.stats(globalMeta));
}
/**
* Special logic for devkit migrations. In the Angular CLI, or in 3P precisely,
* projects can have tsconfigs with overlapping source files. i.e. two tsconfigs
* like e.g. build or test include the same `ts.SourceFile` (`.ts`). Migrations
* should never have 2+ compilation units with overlapping source files as this
* can result in duplicated replacements or analysis— hence we only ever assign a
* source file to a compilation unit *once*.
*
* Note that this is fine as we expect Tsurge migrations to work together as
* isolated compilation units— so it shouldn't matter if worst case a `.ts`
* file ends up in the e.g. test program.
*/
function modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments) {
const sourceFiles = [];
for (const sf of info.sourceFiles) {
const assignment = compilationUnitAssignments.get(sf.fileName);
// File is already assigned to a different compilation unit.
if (assignment !== undefined && assignment !== tsconfigPath) {
continue;
}
compilationUnitAssignments.set(sf.fileName, tsconfigPath);
sourceFiles.push(sf);
}
info.sourceFiles = sourceFiles;
}
/** A text replacement for the given file. */
class Replacement {
projectFile;
update;
constructor(projectFile, update) {
this.projectFile = projectFile;
this.update = update;
}
}
/** An isolated text update that may be applied to a file. */
class TextUpdate {
data;
constructor(data) {
this.data = data;
}
}
/** Confirms that the given data `T` is serializable. */
function confirmAsSerializable(data) {
return data;
}
/**
* Gets a project file instance for the given file.
*
* Use this helper for dealing with project paths throughout your
* migration. The return type is serializable.
*
* See {@link ProjectFile}.
*/
function projectFile(file, { sortedRootDirs, projectRoot }) {
const fs = checker.getFileSystem();
const filePath = fs.resolve(typeof file === 'string' ? file : file.fileName);
// Sorted root directories are sorted longest to shortest. First match
// is the appropriate root directory for ID computation.
for (const rootDir of sortedRootDirs) {
if (!isWithinBasePath(fs, rootDir, filePath)) {
continue;
}
return {
id: fs.relative(rootDir, filePath),
rootRelativePath: fs.relative(projectRoot, filePath),
};
}
// E.g. project directory may be `src/`, but files may be looked up
// from `node_modules/`. This is fine, but in those cases, no root
// directory matches.
const rootRelativePath = fs.relative(projectRoot, filePath);
return {
id: rootRelativePath,
rootRelativePath: rootRelativePath,
};
}
/**
* Whether `path` is a descendant of the `base`?
* E.g. `a/b/c` is within `a/b` but not within `a/x`.
*/
function isWithinBasePath(fs, base, path) {
return checker.isLocalRelativePath(fs.relative(base, path));
}
exports.Replacement = Replacement;
exports.TextUpdate = TextUpdate;
exports.TsurgeComplexMigration = TsurgeComplexMigration;
exports.TsurgeFunnelMigration = TsurgeFunnelMigration;
exports.confirmAsSerializable = confirmAsSerializable;
exports.projectFile = projectFile;
exports.runMigrationInDevkit = runMigrationInDevkit;

View File

@@ -0,0 +1,90 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var core = require('@angular-devkit/core');
/**
* Gets all tsconfig paths from a CLI project by reading the workspace configuration
* and looking for common tsconfig locations.
*/
async function getProjectTsConfigPaths(tree) {
// 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();
const testPaths = new Set();
const workspace = await getWorkspace(tree);
for (const [, project] of workspace.projects) {
for (const [name, target] of project.targets) {
if (name !== 'build' && name !== 'test') {
continue;
}
for (const [, options] of allTargetOptions(target)) {
const tsConfig = options['tsConfig'];
// Filter out tsconfig files that don't exist in the CLI project.
if (typeof tsConfig !== 'string' || !tree.exists(tsConfig)) {
continue;
}
if (name === 'build') {
buildPaths.add(core.normalize(tsConfig));
}
else {
testPaths.add(core.normalize(tsConfig));
}
}
}
}
return {
buildPaths: [...buildPaths],
testPaths: [...testPaths],
};
}
/** Get options for all configurations for the passed builder target. */
function* allTargetOptions(target) {
if (target.options) {
yield [undefined, target.options];
}
if (!target.configurations) {
return;
}
for (const [name, options] of Object.entries(target.configurations)) {
if (options) {
yield [name, options];
}
}
}
function createHost(tree) {
return {
async readFile(path) {
const data = tree.read(path);
if (!data) {
throw new Error('File not found.');
}
return core.virtualFs.fileBufferToString(data);
},
async writeFile(path, data) {
return tree.overwrite(path, data);
},
async isDirectory(path) {
// Approximate a directory check.
// We don't need to consider empty directories and hence this is a good enough approach.
// This is also per documentation, see:
// https://angular.dev/tools/cli/schematics-for-libraries#get-the-project-configuration
return !tree.exists(path) && tree.getDir(path).subfiles.length > 0;
},
async isFile(path) {
return tree.exists(path);
},
};
}
async function getWorkspace(tree) {
const host = createHost(tree);
const { workspace } = await core.workspaces.readWorkspace('/', host);
return workspace;
}
exports.getProjectTsConfigPaths = getProjectTsConfigPaths;

View File

@@ -0,0 +1,27 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
/**
* Gets the text of the given property name. Returns null if the property
* name couldn't be determined statically.
*/
function getPropertyNameText(node) {
if (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) {
return node.text;
}
return null;
}
/** Finds a property with a specific name in an object literal expression. */
function findLiteralProperty(literal, name) {
return literal.properties.find((prop) => prop.name && ts.isIdentifier(prop.name) && prop.name.text === name);
}
exports.findLiteralProperty = findLiteralProperty;
exports.getPropertyNameText = getPropertyNameText;

View File

@@ -0,0 +1,180 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var p = require('path');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
var compiler_host = require('./compiler_host-C55Cczah.cjs');
var ts = require('typescript');
var imports = require('./imports-CIX-JgAN.cjs');
require('@angular-devkit/core');
require('./checker-BwV9MjSQ.cjs');
require('os');
require('fs');
require('module');
require('url');
function migrateFile(sourceFile, rewriteFn) {
const changeTracker = new compiler_host.ChangeTracker(ts.createPrinter());
const visitNode = (node) => {
const provider = tryParseProviderExpression(node);
if (provider) {
replaceProviderWithNewApi({
sourceFile,
node,
provider,
changeTracker,
});
return;
}
ts.forEachChild(node, visitNode);
};
ts.forEachChild(sourceFile, visitNode);
for (const change of changeTracker.recordChanges().get(sourceFile)?.values() ?? []) {
rewriteFn(change.start, change.removeLength ?? 0, change.text);
}
}
function replaceProviderWithNewApi({ sourceFile, node, provider, changeTracker, }) {
const { initializerCode, importInject, provideInitializerFunctionName, initializerToken } = provider;
const initializerTokenSpecifier = imports.getImportSpecifier(sourceFile, angularCoreModule, initializerToken);
// The token doesn't come from `@angular/core`.
if (!initializerTokenSpecifier) {
return;
}
// Replace the provider with the new provide function.
changeTracker.replaceText(sourceFile, node.getStart(), node.getWidth(), `${provideInitializerFunctionName}(${initializerCode})`);
// Remove the `*_INITIALIZER` token from imports.
changeTracker.removeImport(sourceFile, initializerToken, angularCoreModule);
// Add the `inject` function to imports if needed.
if (importInject) {
changeTracker.addImport(sourceFile, 'inject', angularCoreModule);
}
// Add the `provide*Initializer` function to imports.
changeTracker.addImport(sourceFile, provideInitializerFunctionName, angularCoreModule);
}
function tryParseProviderExpression(node) {
if (!ts.isObjectLiteralExpression(node)) {
return;
}
let deps = [];
let initializerToken;
let useExisting;
let useFactoryCode;
let useValue;
let multi = false;
for (const property of node.properties) {
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
switch (property.name.text) {
case 'deps':
if (ts.isArrayLiteralExpression(property.initializer)) {
deps = property.initializer.elements.map((el) => el.getText());
}
break;
case 'provide':
initializerToken = property.initializer.getText();
break;
case 'useExisting':
useExisting = property.initializer;
break;
case 'useFactory':
useFactoryCode = property.initializer.getText();
break;
case 'useValue':
useValue = property.initializer;
break;
case 'multi':
multi = property.initializer.kind === ts.SyntaxKind.TrueKeyword;
break;
}
}
// Handle the `useFactory() {}` shorthand case.
if (ts.isMethodDeclaration(property) && property.name.getText() === 'useFactory') {
const params = property.parameters.map((param) => param.getText()).join(', ');
useFactoryCode = `(${params}) => ${property.body?.getText()}`;
}
}
if (!initializerToken || !multi) {
return;
}
const provideInitializerFunctionName = initializerTokenToFunctionMap.get(initializerToken);
if (!provideInitializerFunctionName) {
return;
}
const info = {
initializerToken,
provideInitializerFunctionName,
importInject: false,
};
if (useExisting) {
return {
...info,
importInject: true,
initializerCode: `() => inject(${useExisting.getText()})()`,
};
}
if (useFactoryCode) {
const args = deps.map((dep) => `inject(${dep})`);
return {
...info,
importInject: deps.length > 0,
initializerCode: `() => {
const initializerFn = (${useFactoryCode})(${args.join(', ')});
return initializerFn();
}`,
};
}
if (useValue) {
return { ...info, initializerCode: useValue.getText() };
}
return;
}
const angularCoreModule = '@angular/core';
const initializerTokenToFunctionMap = new Map([
['APP_INITIALIZER', 'provideAppInitializer'],
['ENVIRONMENT_INITIALIZER', 'provideEnvironmentInitializer'],
['PLATFORM_INITIALIZER', 'providePlatformInitializer'],
]);
function migrate() {
return async (tree) => {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];
if (!allPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the provide initializer migration.');
}
for (const tsconfigPath of allPaths) {
runMigration(tree, tsconfigPath, basePath);
}
};
}
function runMigration(tree, tsconfigPath, basePath) {
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => compiler_host.canMigrateFile(basePath, sourceFile, program));
for (const sourceFile of sourceFiles) {
let update = null;
const rewriter = (startPos, width, text) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
}
update.remove(startPos, width);
if (text !== null) {
update.insertLeft(startPos, text);
}
};
migrateFile(sourceFile, rewriter);
if (update !== null) {
tree.commitUpdate(update);
}
}
}
exports.migrate = migrate;

View File

@@ -0,0 +1,411 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var fs = require('fs');
var p = require('path');
var compiler_host = require('./compiler_host-C55Cczah.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
var ts = require('typescript');
var checker = require('./checker-BwV9MjSQ.cjs');
var property_name = require('./property_name-BBwFuqMe.cjs');
require('os');
require('@angular-devkit/core');
require('module');
require('url');
/**
* Finds the class declaration that is being referred to by a node.
* @param reference Node referring to a class declaration.
* @param typeChecker
*/
function findClassDeclaration(reference, typeChecker) {
return (typeChecker
.getTypeAtLocation(reference)
.getSymbol()
?.declarations?.find(ts.isClassDeclaration) || null);
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Checks whether a component is standalone.
* @param node Class being checked.
* @param reflector The reflection host to use.
*/
function isStandaloneComponent(node, reflector) {
const decorators = reflector.getDecoratorsOfDeclaration(node);
if (decorators === null) {
return false;
}
const decorator = checker.findAngularDecorator(decorators, 'Component', false);
if (decorator === undefined || decorator.args === null || decorator.args.length !== 1) {
return false;
}
const arg = decorator.args[0];
if (ts.isObjectLiteralExpression(arg)) {
const property = property_name.findLiteralProperty(arg, 'standalone');
if (property) {
return property.initializer.getText() === 'true';
}
else {
return true; // standalone is true by default in v19
}
}
return false;
}
/**
* Checks whether a node is variable declaration of type Routes or Route[] and comes from @angular/router
* @param node Variable declaration being checked.
* @param typeChecker
*/
function isAngularRoutesArray(node, typeChecker) {
if (ts.isVariableDeclaration(node)) {
const type = typeChecker.getTypeAtLocation(node);
if (type && typeChecker.isArrayType(type)) {
// Route[] is an array type
const typeArguments = typeChecker.getTypeArguments(type);
const symbol = typeArguments[0]?.getSymbol();
return (symbol?.name === 'Route' &&
symbol?.declarations?.some((decl) => {
return decl.getSourceFile().fileName.includes('@angular/router');
}));
}
}
return false;
}
/**
* Checks whether a node is a call expression to a router module method.
* Examples:
* - RouterModule.forRoot(routes)
* - RouterModule.forChild(routes)
*/
function isRouterModuleCallExpression(node, typeChecker) {
if (ts.isPropertyAccessExpression(node.expression)) {
const propAccess = node.expression;
const moduleSymbol = typeChecker.getSymbolAtLocation(propAccess.expression);
return (moduleSymbol?.name === 'RouterModule' &&
(propAccess.name.text === 'forRoot' || propAccess.name.text === 'forChild'));
}
return false;
}
/**
* Checks whether a node is a call expression to a router method.
* Example: this.router.resetConfig(routes)
*/
function isRouterCallExpression(node, typeChecker) {
if (ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'resetConfig') {
const calleeExpression = node.expression.expression;
const symbol = typeChecker.getSymbolAtLocation(calleeExpression);
if (symbol) {
const type = typeChecker.getTypeOfSymbolAtLocation(symbol, calleeExpression);
// if type of router is Router, then it is a router call expression
return type.aliasSymbol?.escapedName === 'Router';
}
}
return false;
}
/**
* Checks whether a node is a call expression to router provide function.
* Example: provideRoutes(routes)
*/
function isRouterProviderCallExpression(node, typeChecker) {
if (ts.isIdentifier(node.expression)) {
const moduleSymbol = typeChecker.getSymbolAtLocation(node.expression);
return moduleSymbol && moduleSymbol.name === 'provideRoutes';
}
return false;
}
/**
* Checks whether a node is a call expression to provideRouter function.
* Example: provideRouter(routes)
*/
function isProvideRoutesCallExpression(node, typeChecker) {
if (ts.isIdentifier(node.expression)) {
const moduleSymbol = typeChecker.getSymbolAtLocation(node.expression);
return moduleSymbol && moduleSymbol.name === 'provideRouter';
}
return false;
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Converts all application routes that are using standalone components to be lazy loaded.
* @param sourceFile File that should be migrated.
* @param program
*/
function migrateFileToLazyRoutes(sourceFile, program) {
const typeChecker = program.getTypeChecker();
const reflector = new checker.TypeScriptReflectionHost(typeChecker);
const printer = ts.createPrinter();
const tracker = new compiler_host.ChangeTracker(printer);
const routeArraysToMigrate = findRoutesArrayToMigrate(sourceFile, typeChecker);
if (routeArraysToMigrate.length === 0) {
return { pendingChanges: [], skippedRoutes: [], migratedRoutes: [] };
}
const { skippedRoutes, migratedRoutes } = migrateRoutesArray(routeArraysToMigrate, typeChecker, reflector, tracker);
return {
pendingChanges: tracker.recordChanges().get(sourceFile) || [],
skippedRoutes,
migratedRoutes,
};
}
/** Finds route object that can be migrated */
function findRoutesArrayToMigrate(sourceFile, typeChecker) {
const routesArrays = [];
sourceFile.forEachChild(function walk(node) {
if (ts.isCallExpression(node)) {
if (isRouterModuleCallExpression(node, typeChecker) ||
isRouterProviderCallExpression(node, typeChecker) ||
isRouterCallExpression(node, typeChecker) ||
isProvideRoutesCallExpression(node, typeChecker)) {
const arg = node.arguments[0]; // ex: RouterModule.forRoot(routes) or provideRouter(routes)
const routeFileImports = sourceFile.statements.filter(ts.isImportDeclaration);
if (ts.isArrayLiteralExpression(arg) && arg.elements.length > 0) {
// ex: inline routes array: RouterModule.forRoot([{ path: 'test', component: TestComponent }])
routesArrays.push({
routeFilePath: sourceFile.fileName,
array: arg,
routeFileImports,
});
}
else if (ts.isIdentifier(arg)) {
// ex: reference to routes array: RouterModule.forRoot(routes)
// RouterModule.forRoot(routes), provideRouter(routes), provideRoutes(routes)
const symbol = typeChecker.getSymbolAtLocation(arg);
if (!symbol?.declarations)
return;
for (const declaration of symbol.declarations) {
if (ts.isVariableDeclaration(declaration)) {
const initializer = declaration.initializer;
if (initializer && ts.isArrayLiteralExpression(initializer)) {
// ex: const routes = [{ path: 'test', component: TestComponent }];
routesArrays.push({
routeFilePath: sourceFile.fileName,
array: initializer,
routeFileImports,
});
}
}
}
}
}
}
if (ts.isVariableDeclaration(node)) {
if (isAngularRoutesArray(node, typeChecker)) {
const initializer = node.initializer;
if (initializer &&
ts.isArrayLiteralExpression(initializer) &&
initializer.elements.length > 0) {
// ex: const routes: Routes = [{ path: 'test', component: TestComponent }];
if (routesArrays.find((x) => x.array === initializer)) {
// already exists
return;
}
routesArrays.push({
routeFilePath: sourceFile.fileName,
array: initializer,
routeFileImports: sourceFile.statements.filter(ts.isImportDeclaration),
});
}
}
}
node.forEachChild(walk);
});
return routesArrays;
}
/** Migrate a routes object standalone components to be lazy loaded. */
function migrateRoutesArray(routesArray, typeChecker, reflector, tracker) {
const migratedRoutes = [];
const skippedRoutes = [];
const importsToRemove = [];
for (const route of routesArray) {
route.array.elements.forEach((element) => {
if (ts.isObjectLiteralExpression(element)) {
const { migratedRoutes: migrated, skippedRoutes: toBeSkipped, importsToRemove: toBeRemoved, } = migrateRoute(element, route, typeChecker, reflector, tracker);
migratedRoutes.push(...migrated);
skippedRoutes.push(...toBeSkipped);
importsToRemove.push(...toBeRemoved);
}
});
}
for (const importToRemove of importsToRemove) {
tracker.removeNode(importToRemove);
}
return { migratedRoutes, skippedRoutes };
}
/**
* Migrates a single route object and returns the results of the migration
* It recursively migrates the children routes if they exist
*/
function migrateRoute(element, route, typeChecker, reflector, tracker) {
const skippedRoutes = [];
const migratedRoutes = [];
const importsToRemove = [];
const component = property_name.findLiteralProperty(element, 'component');
// this can be empty string or a variable that is not a string, or not present at all
const routePath = property_name.findLiteralProperty(element, 'path')?.getText() ?? '';
const children = property_name.findLiteralProperty(element, 'children');
// recursively migrate children routes first if they exist
if (children && ts.isArrayLiteralExpression(children.initializer)) {
for (const childRoute of children.initializer.elements) {
if (ts.isObjectLiteralExpression(childRoute)) {
const { migratedRoutes: migrated, skippedRoutes: toBeSkipped, importsToRemove: toBeRemoved, } = migrateRoute(childRoute, route, typeChecker, reflector, tracker);
migratedRoutes.push(...migrated);
skippedRoutes.push(...toBeSkipped);
importsToRemove.push(...toBeRemoved);
}
}
}
const routeMigrationResults = { migratedRoutes, skippedRoutes, importsToRemove };
if (!component) {
return routeMigrationResults;
}
const componentDeclaration = findClassDeclaration(component, typeChecker);
if (!componentDeclaration) {
return routeMigrationResults;
}
// if component is not a standalone component, skip it
if (!isStandaloneComponent(componentDeclaration, reflector)) {
skippedRoutes.push({ path: routePath, file: route.routeFilePath });
return routeMigrationResults;
}
const componentClassName = componentDeclaration.name && ts.isIdentifier(componentDeclaration.name)
? componentDeclaration.name.text
: null;
if (!componentClassName) {
return routeMigrationResults;
}
// if component is in the same file as the routes array, skip it
if (componentDeclaration.getSourceFile().fileName === route.routeFilePath) {
return routeMigrationResults;
}
const componentImport = route.routeFileImports.find((importDecl) => importDecl.importClause?.getText().includes(componentClassName));
// remove single and double quotes from the import path
let componentImportPath = ts.isStringLiteral(componentImport?.moduleSpecifier)
? componentImport.moduleSpecifier.text
: null;
// if the import path is not a string literal, skip it
if (!componentImportPath) {
skippedRoutes.push({ path: routePath, file: route.routeFilePath });
return routeMigrationResults;
}
const isDefaultExport = componentDeclaration.modifiers?.some((x) => x.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
const loadComponent = createLoadComponentPropertyAssignment(componentImportPath, componentClassName, isDefaultExport);
tracker.replaceNode(component, loadComponent);
// Add the import statement for the standalone component
if (!importsToRemove.includes(componentImport)) {
importsToRemove.push(componentImport);
}
migratedRoutes.push({ path: routePath, file: route.routeFilePath });
// the component was migrated, so we return the results
return routeMigrationResults;
}
/**
* Generates the loadComponent property assignment for a given component.
*
* Example:
* loadComponent: () => import('./path').then(m => m.componentName)
* or
* loadComponent: () => import('./path') // when isDefaultExport is true
*/
function createLoadComponentPropertyAssignment(componentImportPath, componentDeclarationName, isDefaultExport) {
return ts.factory.createPropertyAssignment('loadComponent', ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), isDefaultExport
? createImportCallExpression(componentImportPath) // will generate import('./path) and will skip the then() call
: ts.factory.createCallExpression(
// will generate import('./path).then(m => m.componentName)
ts.factory.createPropertyAccessExpression(createImportCallExpression(componentImportPath), 'then'), undefined, [createImportThenCallExpression(componentDeclarationName)])));
}
// import('./path)
const createImportCallExpression = (componentImportPath) => ts.factory.createCallExpression(ts.factory.createIdentifier('import'), undefined, [
ts.factory.createStringLiteral(componentImportPath, true),
]);
// m => m.componentName
const createImportThenCallExpression = (componentDeclarationName) => ts.factory.createArrowFunction(undefined, undefined, [ts.factory.createParameterDeclaration(undefined, undefined, 'm', undefined, undefined)], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('m'), componentDeclarationName));
function migrate(options) {
return async (tree, context) => {
const { buildPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
const basePath = process.cwd();
// TS and Schematic use paths in POSIX format even on Windows. This is needed as otherwise
// string matching such as `sourceFile.fileName.startsWith(pathToMigrate)` might not work.
const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
if (!buildPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the route lazy loading migration.');
}
let migratedRoutes = [];
let skippedRoutes = [];
for (const tsconfigPath of buildPaths) {
const { migratedRoutes: migrated, skippedRoutes: skipped } = standaloneRoutesMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
migratedRoutes.push(...migrated);
skippedRoutes.push(...skipped);
}
if (migratedRoutes.length === 0 && skippedRoutes.length === 0) {
throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}.`);
}
context.logger.info('🎉 Automated migration step has finished! 🎉');
context.logger.info(`Number of updated routes: ${migratedRoutes.length}`);
context.logger.info(`Number of skipped routes: ${skippedRoutes.length}`);
if (skippedRoutes.length > 0) {
context.logger.info(`Note: this migration was unable to optimize the following routes, since they use components declared in NgModules:`);
for (const route of skippedRoutes) {
context.logger.info(`- \`${route.path}\` path at \`${route.file}\``);
}
context.logger.info(`Consider making those components standalone and run this migration again. More information about standalone migration can be found at https://angular.dev/reference/migrations/standalone`);
}
context.logger.info('IMPORTANT! Please verify manually that your application builds and behaves as expected.');
context.logger.info(`See https://angular.dev/reference/migrations/route-lazy-loading for more information.`);
};
}
function standaloneRoutesMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
if (schematicOptions.path.startsWith('..')) {
throw new schematics.SchematicsException('Cannot run route lazy loading migration outside of the current project.');
}
if (fs.existsSync(pathToMigrate) && !fs.statSync(pathToMigrate).isDirectory()) {
throw new schematics.SchematicsException(`Migration path ${pathToMigrate} has to be a directory. Cannot run the route lazy loading migration.`);
}
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
compiler_host.canMigrateFile(basePath, sourceFile, program));
const migratedRoutes = [];
const skippedRoutes = [];
if (sourceFiles.length === 0) {
return { migratedRoutes, skippedRoutes };
}
for (const sourceFile of sourceFiles) {
const { pendingChanges, skippedRoutes: skipped, migratedRoutes: migrated, } = migrateFileToLazyRoutes(sourceFile, program);
skippedRoutes.push(...skipped);
migratedRoutes.push(...migrated);
const update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
pendingChanges.forEach((change) => {
if (change.removeLength != null) {
update.remove(change.start, change.removeLength);
}
update.insertRight(change.start, change.text);
});
tree.commitUpdate(update);
}
return { migratedRoutes, skippedRoutes };
}
exports.migrate = migrate;

View File

@@ -0,0 +1,419 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var checker = require('./checker-BwV9MjSQ.cjs');
require('./index-BnJH1Hc7.cjs');
require('path');
var project_paths = require('./project_paths-DY3SIODd.cjs');
var ng_decorators = require('./ng_decorators-B5HCqr20.cjs');
var property_name = require('./property_name-BBwFuqMe.cjs');
require('@angular-devkit/core');
require('node:path/posix');
require('fs');
require('module');
require('url');
require('@angular-devkit/schematics');
require('./project_tsconfig_paths-CDVxT6Ov.cjs');
require('./imports-CIX-JgAN.cjs');
/**
* Unwraps a given expression TypeScript node. Expressions can be wrapped within multiple
* parentheses or as expression. e.g. "(((({exp}))))()". The function should return the
* TypeScript node referring to the inner expression. e.g "exp".
*/
function unwrapExpression(node) {
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node)) {
return unwrapExpression(node.expression);
}
else {
return node;
}
}
/** Extracts `@Directive` or `@Component` metadata from the given class. */
function extractAngularClassMetadata(typeChecker, node) {
const decorators = ts.getDecorators(node);
if (!decorators || !decorators.length) {
return null;
}
const ngDecorators = ng_decorators.getAngularDecorators(typeChecker, decorators);
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.
if (!decorator) {
return null;
}
const decoratorCall = decorator.node.expression;
// In case the decorator call is not valid, skip this class declaration.
if (decoratorCall.arguments.length !== 1) {
return null;
}
const metadata = unwrapExpression(decoratorCall.arguments[0]);
// Ensure that the metadata is an object literal expression.
if (!ts.isObjectLiteralExpression(metadata)) {
return null;
}
return {
type: componentDecorator ? 'component' : 'directive',
node: metadata,
};
}
const LF_CHAR = 10;
const CR_CHAR = 13;
const LINE_SEP_CHAR = 8232;
const PARAGRAPH_CHAR = 8233;
/** Gets the line and character for the given position from the line starts map. */
function getLineAndCharacterFromPosition(lineStartsMap, position) {
const lineIndex = findClosestLineStartPosition(lineStartsMap, position);
return { character: position - lineStartsMap[lineIndex], line: lineIndex };
}
/**
* Computes the line start map of the given text. This can be used in order to
* retrieve the line and character of a given text position index.
*/
function computeLineStartsMap(text) {
const result = [0];
let pos = 0;
while (pos < text.length) {
const char = text.charCodeAt(pos++);
// Handles the "CRLF" line break. In that case we peek the character
// after the "CR" and check if it is a line feed.
if (char === CR_CHAR) {
if (text.charCodeAt(pos) === LF_CHAR) {
pos++;
}
result.push(pos);
}
else if (char === LF_CHAR || char === LINE_SEP_CHAR || char === PARAGRAPH_CHAR) {
result.push(pos);
}
}
result.push(pos);
return result;
}
/** Finds the closest line start for the given position. */
function findClosestLineStartPosition(linesMap, position, low = 0, high = linesMap.length - 1) {
while (low <= high) {
const pivotIdx = Math.floor((low + high) / 2);
const pivotEl = linesMap[pivotIdx];
if (pivotEl === position) {
return pivotIdx;
}
else if (position > pivotEl) {
low = pivotIdx + 1;
}
else {
high = pivotIdx - 1;
}
}
// In case there was no exact match, return the closest "lower" line index. We also
// subtract the index by one because want the index of the previous line start.
return low - 1;
}
/**
* Visitor that can be used to determine Angular templates referenced within given
* TypeScript source files (inline templates or external referenced templates)
*/
class NgComponentTemplateVisitor {
typeChecker;
resolvedTemplates = [];
fs = checker.getFileSystem();
constructor(typeChecker) {
this.typeChecker = typeChecker;
}
visitNode(node) {
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
this.visitClassDeclaration(node);
}
ts.forEachChild(node, (n) => this.visitNode(n));
}
visitClassDeclaration(node) {
const metadata = extractAngularClassMetadata(this.typeChecker, node);
if (metadata === null || metadata.type !== 'component') {
return;
}
const sourceFile = node.getSourceFile();
const sourceFileName = sourceFile.fileName;
// Walk through all component metadata properties and determine the referenced
// HTML templates (either external or inline)
metadata.node.properties.forEach((property) => {
if (!ts.isPropertyAssignment(property)) {
return;
}
const propertyName = property_name.getPropertyNameText(property.name);
// In case there is an inline template specified, ensure that the value is statically
// analyzable by checking if the initializer is a string literal-like node.
if (propertyName === 'template' && ts.isStringLiteralLike(property.initializer)) {
// Need to add an offset of one to the start because the template quotes are
// not part of the template content.
// The `getText()` method gives us the original raw text.
// We could have used the `text` property, but if the template is defined as a backtick
// string then the `text` property contains a "cooked" version of the string. Such cooked
// strings will have converted CRLF characters to only LF. This messes up string
// replacements in template migrations.
// The raw text returned by `getText()` includes the enclosing quotes so we change the
// `content` and `start` values accordingly.
const content = property.initializer.getText().slice(1, -1);
const start = property.initializer.getStart() + 1;
this.resolvedTemplates.push({
filePath: sourceFileName,
container: node,
content,
inline: true,
start: start,
getCharacterAndLineOfPosition: (pos) => ts.getLineAndCharacterOfPosition(sourceFile, pos + start),
});
}
if (propertyName === 'templateUrl' && ts.isStringLiteralLike(property.initializer)) {
const absolutePath = this.fs.resolve(this.fs.dirname(sourceFileName), property.initializer.text);
if (!this.fs.exists(absolutePath)) {
return;
}
const fileContent = this.fs.readFile(absolutePath);
const lineStartsMap = computeLineStartsMap(fileContent);
this.resolvedTemplates.push({
filePath: absolutePath,
container: node,
content: fileContent,
inline: false,
start: 0,
getCharacterAndLineOfPosition: (pos) => getLineAndCharacterFromPosition(lineStartsMap, pos),
});
}
});
}
}
function parseTemplate(template) {
let parsed;
try {
// Note: we use the HtmlParser here, instead of the `parseTemplate` function, because the
// latter returns an Ivy AST, not an HTML AST. The HTML AST has the advantage of preserving
// interpolated text as text nodes containing a mixture of interpolation tokens and text tokens,
// rather than turning them into `BoundText` nodes like the Ivy AST does. This allows us to
// easily get the text-only ranges without having to reconstruct the original text.
parsed = new checker.HtmlParser().parse(template, '', {
// Allows for ICUs to be parsed.
tokenizeExpansionForms: true,
// Explicitly disable blocks so that their characters are treated as plain text.
tokenizeBlocks: true,
preserveLineEndings: true,
});
// Don't migrate invalid templates.
if (parsed.errors && parsed.errors.length > 0) {
const errors = parsed.errors.map((e) => ({ type: 'parse', error: e }));
return { tree: undefined, errors };
}
}
catch (e) {
return { tree: undefined, errors: [{ type: 'parse', error: e }] };
}
return { tree: parsed, errors: [] };
}
function migrateTemplateToSelfClosingTags(template) {
let parsed = parseTemplate(template);
if (parsed.tree === undefined) {
return { migrated: template, changed: false, replacementCount: 0 };
}
const visitor = new AngularElementCollector();
checker.visitAll(visitor, parsed.tree.rootNodes);
let newTemplate = template;
let changedOffset = 0;
let replacementCount = 0;
for (let element of visitor.elements) {
const { start, end, tagName } = element;
const currentLength = newTemplate.length;
const templatePart = newTemplate.slice(start + changedOffset, end + changedOffset);
const convertedTemplate = replaceWithSelfClosingTag(templatePart, tagName);
// if the template has changed, replace the original template with the new one
if (convertedTemplate.length !== templatePart.length) {
newTemplate = replaceTemplate(newTemplate, convertedTemplate, start, end, changedOffset);
changedOffset += newTemplate.length - currentLength;
replacementCount++;
}
}
return { migrated: newTemplate, changed: changedOffset !== 0, replacementCount };
}
function replaceWithSelfClosingTag(html, tagName) {
const pattern = new RegExp(`<\\s*${tagName}\\s*([^>]*?(?:"[^"]*"|'[^']*'|[^'">])*)\\s*>([\\s\\S]*?)<\\s*/\\s*${tagName}\\s*>`, 'gi');
return html.replace(pattern, (_, content) => `<${tagName}${content ? ` ${content}` : ''} />`);
}
/**
* Replace the value in the template with the new value based on the start and end position + offset
*/
function replaceTemplate(template, replaceValue, start, end, offset) {
return template.slice(0, start + offset) + replaceValue + template.slice(end + offset);
}
const ALL_HTML_TAGS = new checker.DomElementSchemaRegistry().allKnownElementNames();
class AngularElementCollector extends checker.RecursiveVisitor {
elements = [];
constructor() {
super();
}
visitElement(element) {
const isHtmlTag = ALL_HTML_TAGS.includes(element.name);
if (isHtmlTag) {
return;
}
const hasNoContent = this.elementHasNoContent(element);
const hasNoClosingTag = this.elementHasNoClosingTag(element);
if (hasNoContent && !hasNoClosingTag) {
this.elements.push({
tagName: element.name,
start: element.sourceSpan.start.offset,
end: element.sourceSpan.end.offset,
});
}
return super.visitElement(element, null);
}
elementHasNoContent(element) {
if (!element.children?.length) {
return true;
}
if (element.children.length === 1) {
const child = element.children[0];
return child instanceof checker.Text && /^\s*$/.test(child.value);
}
return false;
}
elementHasNoClosingTag(element) {
const { startSourceSpan, endSourceSpan } = element;
if (!endSourceSpan) {
return true;
}
return (startSourceSpan.start.offset === endSourceSpan.start.offset &&
startSourceSpan.end.offset === endSourceSpan.end.offset);
}
}
class SelfClosingTagsMigration extends project_paths.TsurgeFunnelMigration {
config;
constructor(config = {}) {
super();
this.config = config;
}
async analyze(info) {
const { sourceFiles, program } = info;
const typeChecker = program.getTypeChecker();
const tagReplacements = [];
for (const sf of sourceFiles) {
ts.forEachChild(sf, (node) => {
// Skipping any non component declarations
if (!ts.isClassDeclaration(node)) {
return;
}
const file = project_paths.projectFile(node.getSourceFile(), info);
if (this.config.shouldMigrate && this.config.shouldMigrate(file) === false) {
return;
}
const templateVisitor = new NgComponentTemplateVisitor(typeChecker);
templateVisitor.visitNode(node);
templateVisitor.resolvedTemplates.forEach((template) => {
const { migrated, changed, replacementCount } = migrateTemplateToSelfClosingTags(template.content);
if (!changed) {
return;
}
const fileToMigrate = template.inline
? file
: project_paths.projectFile(template.filePath, info);
const end = template.start + template.content.length;
const replacements = [
prepareTextReplacement(fileToMigrate, migrated, template.start, end),
];
const fileReplacements = tagReplacements.find((tagReplacement) => tagReplacement.file === file);
if (fileReplacements) {
fileReplacements.replacements.push(...replacements);
fileReplacements.replacementCount += replacementCount;
}
else {
tagReplacements.push({ file, replacements, replacementCount });
}
});
});
}
return project_paths.confirmAsSerializable({ tagReplacements });
}
async combine(unitA, unitB) {
return project_paths.confirmAsSerializable({
tagReplacements: [...unitA.tagReplacements, ...unitB.tagReplacements],
});
}
async globalMeta(combinedData) {
const globalMeta = {
tagReplacements: combinedData.tagReplacements,
};
return project_paths.confirmAsSerializable(globalMeta);
}
async stats(globalMetadata) {
const touchedFilesCount = globalMetadata.tagReplacements.length;
const replacementCount = globalMetadata.tagReplacements.reduce((acc, cur) => acc + cur.replacementCount, 0);
return {
counters: {
touchedFilesCount,
replacementCount,
},
};
}
async migrate(globalData) {
return { replacements: globalData.tagReplacements.flatMap(({ replacements }) => replacements) };
}
}
function prepareTextReplacement(file, replacement, start, end) {
return new project_paths.Replacement(file, new project_paths.TextUpdate({
position: start,
end: end,
toInsert: replacement,
}));
}
function migrate(options) {
return async (tree, context) => {
await project_paths.runMigrationInDevkit({
tree,
getMigration: (fs) => new SelfClosingTagsMigration({
shouldMigrate: (file) => {
return (file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
!/(^|\/)node_modules\//.test(file.rootRelativePath));
},
}),
beforeProgramCreation: (tsconfigPath, stage) => {
if (stage === project_paths.MigrationStage.Analysis) {
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
}
else {
context.logger.info(`Running migration for: ${tsconfigPath}...`);
}
},
beforeUnitAnalysis: (tsconfigPath) => {
context.logger.info(`Scanning for component tags: ${tsconfigPath}...`);
},
afterAllAnalyzed: () => {
context.logger.info(``);
context.logger.info(`Processing analysis data between targets...`);
context.logger.info(``);
},
afterAnalysisFailure: () => {
context.logger.error('Migration failed unexpectedly with no analysis data');
},
whenDone: ({ counters }) => {
const { touchedFilesCount, replacementCount } = counters;
context.logger.info('');
context.logger.info(`Successfully migrated to self-closing tags 🎉`);
context.logger.info(` -> Migrated ${replacementCount} components to self-closing tags in ${touchedFilesCount} component files.`);
},
});
};
}
exports.migrate = migrate;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
'use strict';
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var signalQueriesMigration = require('./signal-queries-migration.cjs');
var signalInputMigration = require('./signal-input-migration.cjs');
var outputMigration = require('./output-migration.cjs');
require('./checker-BwV9MjSQ.cjs');
require('typescript');
require('os');
require('fs');
require('module');
require('path');
require('url');
require('./index-BnJH1Hc7.cjs');
require('./project_paths-DY3SIODd.cjs');
require('@angular-devkit/core');
require('node:path/posix');
require('./project_tsconfig_paths-CDVxT6Ov.cjs');
require('./apply_import_manager-DF0BUe6N.cjs');
require('./migrate_ts_type_references-DQe6JtwN.cjs');
require('assert');
require('./index-B6p5mHIY.cjs');
require('./leading_space-D9nQ8UQC.cjs');
function migrate(options) {
// The migrations are independent so we can run them in any order, but we sort them here
// alphabetically so we get a consistent execution order in case of issue reports.
const migrations = options.migrations.slice().sort();
const rules = [];
for (const migration of migrations) {
switch (migration) {
case "inputs" /* SupportedMigrations.inputs */:
rules.push(signalInputMigration.migrate(options));
break;
case "outputs" /* SupportedMigrations.outputs */:
rules.push(outputMigration.migrate(options));
break;
case "queries" /* SupportedMigrations.queries */:
rules.push(signalQueriesMigration.migrate(options));
break;
default:
throw new schematics.SchematicsException(`Unsupported migration "${migration}"`);
}
}
return schematics.chain(rules);
}
exports.migrate = migrate;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
{
"schematics": {
"standalone-migration": {
"description": "Converts the entire application or a part of it to standalone",
"factory": "./bundles/standalone-migration.cjs#migrate",
"schema": "./ng-generate/standalone-migration/schema.json",
"aliases": ["standalone"]
},
"control-flow-migration": {
"description": "Converts the entire application to block control flow syntax",
"factory": "./bundles/control-flow-migration.cjs#migrate",
"schema": "./ng-generate/control-flow-migration/schema.json",
"aliases": ["control-flow"]
},
"inject-migration": {
"description": "Converts usages of constructor-based injection to the inject() function",
"factory": "./bundles/inject-migration.cjs#migrate",
"schema": "./ng-generate/inject-migration/schema.json",
"aliases": ["inject"]
},
"route-lazy-loading-migration": {
"description": "Updates route definitions to use lazy-loading of components instead of eagerly referencing them",
"factory": "./bundles/route-lazy-loading.cjs#migrate",
"schema": "./ng-generate/route-lazy-loading/schema.json",
"aliases": ["route-lazy-loading"]
},
"signal-input-migration": {
"description": "Updates `@Input` declarations to signal inputs, while also migrating all relevant references.",
"factory": "./bundles/signal-input-migration.cjs#migrate",
"schema": "./ng-generate/signal-input-migration/schema.json",
"aliases": ["signal-inputs", "signal-input"]
},
"signal-queries-migration": {
"description": "Updates query declarations to signal queries, while also migrating all relevant references.",
"factory": "./bundles/signal-queries-migration.cjs#migrate",
"schema": "./ng-generate/signal-queries-migration/schema.json",
"aliases": ["signal-queries", "signal-query", "signal-query-migration"]
},
"output-migration": {
"description": "Updates @output declarations to the functional equivalent, while also migrating all relevant references.",
"factory": "./bundles/output-migration.cjs#migrate",
"schema": "./ng-generate/output-migration/schema.json",
"aliases": ["outputs"]
},
"signals": {
"description": "Combines all signals-related migrations into a single migration",
"factory": "./bundles/signals.cjs#migrate",
"schema": "./ng-generate/signals/schema.json"
},
"cleanup-unused-imports": {
"description": "Removes unused imports from standalone components.",
"factory": "./bundles/cleanup-unused-imports.cjs#migrate",
"schema": "./ng-generate/cleanup-unused-imports/schema.json"
},
"self-closing-tags-migration": {
"description": "Updates the components templates to use self-closing tags where possible",
"factory": "./bundles/self-closing-tags-migration.cjs#migrate",
"schema": "./ng-generate/self-closing-tags-migration/schema.json",
"aliases": ["self-closing-tag"]
}
}
}

View File

@@ -0,0 +1,20 @@
{
"schematics": {
"explicit-standalone-flag": {
"version": "19.0.0",
"description": "Updates non-standalone Directives, Component and Pipes to 'standalone:false' and removes 'standalone:true' from those who are standalone",
"factory": "./bundles/explicit-standalone-flag.cjs#migrate"
},
"pending-tasks": {
"version": "19.0.0",
"description": "Updates ExperimentalPendingTasks to PendingTasks",
"factory": "./bundles/pending-tasks.cjs#migrate"
},
"provide-initializer": {
"version": "19.0.0",
"description": "Replaces `APP_INITIALIZER`, `ENVIRONMENT_INITIALIZER` & `PLATFORM_INITIALIZER` respectively with `provideAppInitializer`, `provideEnvironmentInitializer` & `providePlatformInitializer`.",
"factory": "./bundles/provide-initializer.cjs#migrate",
"optional": true
}
}
}

View File

@@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularCleanupUnusedImportsMigration",
"title": "Angular Cleanup Unused Imports Schema",
"type": "object",
"properties": {}
}

View File

@@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularControlFlowMigration",
"title": "Angular Control Flow Migration Schema",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path relative to the project root which should be migrated",
"x-prompt": "Which path in your project should be migrated?",
"default": "./"
},
"format": {
"type": "boolean",
"description": "Enables reformatting of your templates",
"x-prompt": "Should the migration reformat your templates?",
"default": "true"
}
}
}

View File

@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularInjectMigration",
"title": "Angular Inject Migration Schema",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path relative to the project root which should be migrated",
"x-prompt": "Which path in your project should be migrated?",
"default": "./"
},
"migrateAbstractClasses": {
"type": "boolean",
"description": "Whether abstract classes should be migrated",
"x-prompt": "Do you want to migrate abstract classes? Abstract classes are not migrated by default, because their parameters aren't guaranteed to be injectable",
"default": false
},
"backwardsCompatibleConstructors": {
"type": "boolean",
"description": "Whether to clean up constructors or keep their signatures backwards compatible",
"x-prompt": "Do you want to clean up all constructors or keep them backwards compatible? Enabling this option will include an additional signature of `constructor(...args: unknown[]);` that will avoid errors for sub-classes, but will increase the amount of generated code by the migration",
"default": false
},
"nonNullableOptional": {
"type": "boolean",
"description": "Whether to cast the optional inject sites to be non-nullable",
"x-prompt": "Do you want optional inject calls to be non-nullable? Enable this option if you want the return type to be identical to @Optional(), at the expense of worse type safety",
"default": false
}
}
}

View File

@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularOutputMigration",
"title": "Angular Output migration",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory where all outputs should be migrated.",
"x-prompt": "Which directory do you want to migrate?",
"default": "./"
},
"analysisDir": {
"type": "string",
"description": "Path to the directory that should be analyzed. References to migrated outputs are migrated based on this folder. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.",
"default": "./"
}
}
}

View File

@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularStandaloneRoutesMigration",
"title": "Angular Standalone Routes Migration Schema",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path relative to the project root which should be migrated",
"x-prompt": "Which path in your project should be migrated?",
"default": "./"
}
}
}

View File

@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularSelfClosingTagMigration",
"title": "Angular Self Closing Tag Migration Schema",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory where all templates should be migrated.",
"x-prompt": "Which directory do you want to migrate?",
"default": "./"
}
}
}

View File

@@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularSignalInputMigration",
"title": "Angular Signal Input migration",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory where all inputs should be migrated.",
"x-prompt": "Which directory do you want to migrate?",
"default": "./"
},
"analysisDir": {
"type": "string",
"description": "Path to the directory that should be analyzed. References to migrated inputs are migrated based on this folder. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.",
"default": "./"
},
"bestEffortMode": {
"type": "boolean",
"description": "Whether to eagerly migrate as much as possible, ignoring problematic patterns that would otherwise prevent migration.",
"x-prompt": "Do you want to migrate as much as possible, even if it may break your build?",
"default": false
},
"insertTodos": {
"type": "boolean",
"description": "Whether the migration should add TODOs for inputs that could not be migrated",
"default": false
}
}
}

View File

@@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularSignalQueriesMigration",
"title": "Angular Signal Queries migration",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory where all queries should be migrated.",
"x-prompt": "Which directory do you want to migrate?",
"default": "./"
},
"analysisDir": {
"type": "string",
"description": "Path to the directory that should be analyzed. References to migrated queries are migrated based on this folder. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.",
"default": "./"
},
"bestEffortMode": {
"type": "boolean",
"description": "Whether to eagerly migrate as much as possible, ignoring problematic patterns that would otherwise prevent migration.",
"x-prompt": "Do you want to migrate as much as possible, even if it may break your build?",
"default": false
},
"insertTodos": {
"type": "boolean",
"description": "Whether the migration should add TODOs for queries that could not be migrated",
"default": false
}
}
}

View File

@@ -0,0 +1,66 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularSignalMigration",
"title": "Angular Signals migration",
"type": "object",
"properties": {
"migrations": {
"type": "array",
"default": [
"inputs",
"outputs",
"queries"
],
"items": {
"type": "string",
"enum": [
"inputs",
"outputs",
"queries"
]
},
"description": "Signals-related migrations that should be run",
"x-prompt": {
"message": "Which migrations do you want to run?",
"type": "list",
"multiselect": true,
"items": [
{
"value": "inputs",
"label": "Convert `@Input` to the signal-based `input`"
},
{
"value": "outputs",
"label": "Convert `@Output` to the new `output` function"
},
{
"value": "queries",
"label": "Convert `@ViewChild`/`@ViewChildren` and `@ContentChild`/`@ContentChildren` to the signal-based `viewChild`/`viewChildren` and `contentChild`/`contentChildren`"
}
]
}
},
"path": {
"type": "string",
"description": "Path to the directory that should be migrated.",
"x-prompt": "Which directory do you want to migrate?",
"default": "./"
},
"analysisDir": {
"type": "string",
"description": "Path to the directory that should be analyzed. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.",
"default": "./"
},
"bestEffortMode": {
"type": "boolean",
"description": "Whether to eagerly migrate as much as possible, ignoring problematic patterns that would otherwise prevent migration.",
"x-prompt": "Do you want to migrate as much as possible, even if it may break your build?",
"default": false
},
"insertTodos": {
"type": "boolean",
"description": "Whether the migration should add TODOs for code that could not be migrated",
"default": false
}
}
}

View File

@@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularStandaloneMigration",
"title": "Angular Standalone Migration Schema",
"type": "object",
"properties": {
"mode": {
"description": "Operation that should be performed by the migrator",
"type": "string",
"enum": ["convert-to-standalone", "prune-ng-modules", "standalone-bootstrap"],
"default": "convert-to-standalone",
"x-prompt": {
"message": "Choose the type of migration:",
"type": "list",
"items": [
{
"value": "convert-to-standalone",
"label": "Convert all components, directives and pipes to standalone"
},
{
"value": "prune-ng-modules",
"label": "Remove unnecessary NgModule classes"
},
{
"value": "standalone-bootstrap",
"label": "Bootstrap the application using standalone APIs"
}
]
}
},
"path": {
"type": "string",
"description": "Path relative to the project root which should be migrated",
"x-prompt": "Which path in your project should be migrated?",
"default": "./"
}
}
}

View File

@@ -0,0 +1,704 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
import * as _angular_core from '@angular/core';
import { ɵDeferBlockDetails as _DeferBlockDetails, ɵDeferBlockState as _DeferBlockState, ComponentRef, DebugElement, ElementRef, ChangeDetectorRef, NgZone, SchemaMetadata, ɵDeferBlockBehavior as _DeferBlockBehavior, InjectionToken, PlatformRef, Type, ProviderToken, InjectOptions, InjectFlags, NgModule, Component, Directive, Pipe } from '@angular/core';
export { ɵDeferBlockBehavior as DeferBlockBehavior, ɵDeferBlockState as DeferBlockState } from '@angular/core';
import { Navigation, NavigationHistoryEntry, NavigationNavigateOptions, NavigationResult, NavigationOptions, NavigateEvent, NavigationCurrentEntryChangeEvent, NavigationTransition, NavigationUpdateCurrentEntryOptions, NavigationReloadOptions } from '../navigation_types.d-fAxd92YV.js';
/**
* Wraps a test function in an asynchronous test zone. The test will automatically
* complete when all asynchronous calls within this zone are done. Can be used
* to wrap an {@link inject} call.
*
* Example:
*
* ```ts
* it('...', waitForAsync(inject([AClass], (object) => {
* object.doSomething.then(() => {
* expect(...);
* })
* })));
* ```
*
* @publicApi
*/
declare function waitForAsync(fn: Function): (done: any) => any;
/**
* Represents an individual defer block for testing purposes.
*
* @publicApi
*/
declare class DeferBlockFixture {
private block;
private componentFixture;
/** @docs-private */
constructor(block: _DeferBlockDetails, componentFixture: ComponentFixture<unknown>);
/**
* Renders the specified state of the defer fixture.
* @param state the defer state to render
*/
render(state: _DeferBlockState): Promise<void>;
/**
* Retrieves all nested child defer block fixtures
* in a given defer block.
*/
getDeferBlocks(): Promise<DeferBlockFixture[]>;
}
/**
* Fixture for debugging and testing a component.
*
* @publicApi
*/
declare class ComponentFixture<T> {
componentRef: ComponentRef<T>;
/**
* The DebugElement associated with the root element of this component.
*/
debugElement: DebugElement;
/**
* The instance of the root component class.
*/
componentInstance: T;
/**
* The native element at the root of the component.
*/
nativeElement: any;
/**
* The ElementRef for the element at the root of the component.
*/
elementRef: ElementRef;
/**
* The ChangeDetectorRef for the component
*/
changeDetectorRef: ChangeDetectorRef;
private _renderer;
private _isDestroyed;
private readonly _testAppRef;
private readonly pendingTasks;
private readonly appErrorHandler;
private readonly zonelessEnabled;
private readonly scheduler;
private readonly rootEffectScheduler;
private readonly microtaskEffectScheduler;
private readonly autoDetectDefault;
private autoDetect;
private subscriptions;
ngZone: NgZone | null;
/** @docs-private */
constructor(componentRef: ComponentRef<T>);
/**
* Trigger a change detection cycle for the component.
*/
detectChanges(checkNoChanges?: boolean): void;
/**
* Do a change detection run to make sure there were no changes.
*/
checkNoChanges(): void;
/**
* Set whether the fixture should autodetect changes.
*
* Also runs detectChanges once so that any existing change is detected.
*
* @param autoDetect Whether to autodetect changes. By default, `true`.
*/
autoDetectChanges(autoDetect?: boolean): void;
/**
* Return whether the fixture is currently stable or has async tasks that have not been completed
* yet.
*/
isStable(): boolean;
/**
* Get a promise that resolves when the fixture is stable.
*
* This can be used to resume testing after events have triggered asynchronous activity or
* asynchronous change detection.
*/
whenStable(): Promise<any>;
/**
* Retrieves all defer block fixtures in the component fixture.
*/
getDeferBlocks(): Promise<DeferBlockFixture[]>;
private _getRenderer;
/**
* Get a promise that resolves when the ui state is stable following animations.
*/
whenRenderingDone(): Promise<any>;
/**
* Trigger component destruction.
*/
destroy(): void;
}
/**
* Clears out the shared fake async zone for a test.
* To be called in a global `beforeEach`.
*
* @publicApi
*/
declare function resetFakeAsyncZone(): void;
/**
* Wraps a function to be executed in the `fakeAsync` zone:
* - Microtasks are manually executed by calling `flushMicrotasks()`.
* - Timers are synchronous; `tick()` simulates the asynchronous passage of time.
*
* Can be used to wrap `inject()` calls.
*
* @param fn The function that you want to wrap in the `fakeAsync` zone.
* @param options
* - flush: When true, will drain the macrotask queue after the test function completes.
* When false, will throw an exception at the end of the function if there are pending timers.
*
* @usageNotes
* ### Example
*
* {@example core/testing/ts/fake_async.ts region='basic'}
*
*
* @returns The function wrapped to be executed in the `fakeAsync` zone.
* Any arguments passed when calling this returned function will be passed through to the `fn`
* function in the parameters when it is called.
*
* @publicApi
*/
declare function fakeAsync(fn: Function, options?: {
flush?: boolean;
}): (...args: any[]) => any;
/**
* Simulates the asynchronous passage of time for the timers in the `fakeAsync` zone.
*
* The microtasks queue is drained at the very start of this function and after any timer callback
* has been executed.
*
* @param millis The number of milliseconds to advance the virtual timer.
* @param tickOptions The options to pass to the `tick()` function.
*
* @usageNotes
*
* The `tick()` option is a flag called `processNewMacroTasksSynchronously`,
* which determines whether or not to invoke new macroTasks.
*
* If you provide a `tickOptions` object, but do not specify a
* `processNewMacroTasksSynchronously` property (`tick(100, {})`),
* then `processNewMacroTasksSynchronously` defaults to true.
*
* If you omit the `tickOptions` parameter (`tick(100))`), then
* `tickOptions` defaults to `{processNewMacroTasksSynchronously: true}`.
*
* ### Example
*
* {@example core/testing/ts/fake_async.ts region='basic'}
*
* The following example includes a nested timeout (new macroTask), and
* the `tickOptions` parameter is allowed to default. In this case,
* `processNewMacroTasksSynchronously` defaults to true, and the nested
* function is executed on each tick.
*
* ```ts
* it ('test with nested setTimeout', fakeAsync(() => {
* let nestedTimeoutInvoked = false;
* function funcWithNestedTimeout() {
* setTimeout(() => {
* nestedTimeoutInvoked = true;
* });
* };
* setTimeout(funcWithNestedTimeout);
* tick();
* expect(nestedTimeoutInvoked).toBe(true);
* }));
* ```
*
* In the following case, `processNewMacroTasksSynchronously` is explicitly
* set to false, so the nested timeout function is not invoked.
*
* ```ts
* it ('test with nested setTimeout', fakeAsync(() => {
* let nestedTimeoutInvoked = false;
* function funcWithNestedTimeout() {
* setTimeout(() => {
* nestedTimeoutInvoked = true;
* });
* };
* setTimeout(funcWithNestedTimeout);
* tick(0, {processNewMacroTasksSynchronously: false});
* expect(nestedTimeoutInvoked).toBe(false);
* }));
* ```
*
*
* @publicApi
*/
declare function tick(millis?: number, tickOptions?: {
processNewMacroTasksSynchronously: boolean;
}): void;
/**
* Flushes any pending microtasks and simulates the asynchronous passage of time for the timers in
* the `fakeAsync` zone by
* draining the macrotask queue until it is empty.
*
* @param maxTurns The maximum number of times the scheduler attempts to clear its queue before
* throwing an error.
* @returns The simulated time elapsed, in milliseconds.
*
* @publicApi
*/
declare function flush(maxTurns?: number): number;
/**
* Discard all remaining periodic tasks.
*
* @publicApi
*/
declare function discardPeriodicTasks(): void;
/**
* Flush any pending microtasks.
*
* @publicApi
*/
declare function flushMicrotasks(): void;
/**
* Type used for modifications to metadata
*
* @publicApi
*/
type MetadataOverride<T> = {
add?: Partial<T>;
remove?: Partial<T>;
set?: Partial<T>;
};
/**
* An abstract class for inserting the root test component element in a platform independent way.
*
* @publicApi
*/
declare class TestComponentRenderer {
insertRootElement(rootElementId: string): void;
removeAllRootElements?(): void;
}
/**
* @publicApi
*/
declare const ComponentFixtureAutoDetect: InjectionToken<boolean>;
/**
* @publicApi
*/
declare const ComponentFixtureNoNgZone: InjectionToken<boolean>;
/**
* @publicApi
*/
interface TestModuleMetadata {
providers?: any[];
declarations?: any[];
imports?: any[];
schemas?: Array<SchemaMetadata | any[]>;
teardown?: ModuleTeardownOptions;
/**
* Whether NG0304 runtime errors should be thrown when unknown elements are present in component's
* template. Defaults to `false`, where the error is simply logged. If set to `true`, the error is
* thrown.
* @see [NG8001](/errors/NG8001) for the description of the problem and how to fix it
*/
errorOnUnknownElements?: boolean;
/**
* Whether errors should be thrown when unknown properties are present in component's template.
* Defaults to `false`, where the error is simply logged.
* If set to `true`, the error is thrown.
* @see [NG8002](/errors/NG8002) for the description of the error and how to fix it
*/
errorOnUnknownProperties?: boolean;
/**
* Whether errors that happen during application change detection should be rethrown.
*
* When `true`, errors that are caught during application change detection will
* be reported to the `ErrorHandler` and rethrown to prevent them from going
* unnoticed in tests.
*
* When `false`, errors are only forwarded to the `ErrorHandler`, which by default
* simply logs them to the console.
*
* Defaults to `true`.
*/
rethrowApplicationErrors?: boolean;
/**
* Whether defer blocks should behave with manual triggering or play through normally.
* Defaults to `manual`.
*/
deferBlockBehavior?: _DeferBlockBehavior;
}
/**
* @publicApi
*/
interface TestEnvironmentOptions {
/**
* Configures the test module teardown behavior in `TestBed`.
*/
teardown?: ModuleTeardownOptions;
/**
* Whether errors should be thrown when unknown elements are present in component's template.
* Defaults to `false`, where the error is simply logged.
* If set to `true`, the error is thrown.
* @see [NG8001](/errors/NG8001) for the description of the error and how to fix it
*/
errorOnUnknownElements?: boolean;
/**
* Whether errors should be thrown when unknown properties are present in component's template.
* Defaults to `false`, where the error is simply logged.
* If set to `true`, the error is thrown.
* @see [NG8002](/errors/NG8002) for the description of the error and how to fix it
*/
errorOnUnknownProperties?: boolean;
}
/**
* Configures the test module teardown behavior in `TestBed`.
* @publicApi
*/
interface ModuleTeardownOptions {
/** Whether the test module should be destroyed after every test. Defaults to `true`. */
destroyAfterEach: boolean;
/** Whether errors during test module destruction should be re-thrown. Defaults to `true`. */
rethrowErrors?: boolean;
}
/**
* Static methods implemented by the `TestBed`.
*
* @publicApi
*/
interface TestBedStatic extends TestBed {
new (...args: any[]): TestBed;
}
/**
* Returns a singleton of the `TestBed` class.
*
* @publicApi
*/
declare function getTestBed(): TestBed;
/**
* @publicApi
*/
interface TestBed {
get platform(): PlatformRef;
get ngModule(): Type<any> | Type<any>[];
/**
* Initialize the environment for testing with a compiler factory, a PlatformRef, and an
* angular module. These are common to every test in the suite.
*
* This may only be called once, to set up the common providers for the current test
* suite on the current platform. If you absolutely need to change the providers,
* first use `resetTestEnvironment`.
*
* Test modules and platforms for individual platforms are available from
* '@angular/<platform_name>/testing'.
*/
initTestEnvironment(ngModule: Type<any> | Type<any>[], platform: PlatformRef, options?: TestEnvironmentOptions): void;
/**
* Reset the providers for the test injector.
*/
resetTestEnvironment(): void;
resetTestingModule(): TestBed;
configureCompiler(config: {
providers?: any[];
useJit?: boolean;
}): void;
configureTestingModule(moduleDef: TestModuleMetadata): TestBed;
compileComponents(): Promise<any>;
inject<T>(token: ProviderToken<T>, notFoundValue: undefined, options: InjectOptions & {
optional?: false;
}): T;
inject<T>(token: ProviderToken<T>, notFoundValue: null | undefined, options: InjectOptions): T | null;
inject<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions): T;
/** @deprecated use object-based flags (`InjectOptions`) instead. */
inject<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
/** @deprecated use object-based flags (`InjectOptions`) instead. */
inject<T>(token: ProviderToken<T>, notFoundValue: null, flags?: InjectFlags): T | null;
/** @deprecated from v9.0.0 use TestBed.inject */
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): any;
/** @deprecated from v9.0.0 use TestBed.inject */
get(token: any, notFoundValue?: any): any;
/**
* Runs the given function in the `EnvironmentInjector` context of `TestBed`.
*
* @see {@link EnvironmentInjector#runInContext}
*/
runInInjectionContext<T>(fn: () => T): T;
execute(tokens: any[], fn: Function, context?: any): any;
overrideModule(ngModule: Type<any>, override: MetadataOverride<NgModule>): TestBed;
overrideComponent(component: Type<any>, override: MetadataOverride<Component>): TestBed;
overrideDirective(directive: Type<any>, override: MetadataOverride<Directive>): TestBed;
overridePipe(pipe: Type<any>, override: MetadataOverride<Pipe>): TestBed;
overrideTemplate(component: Type<any>, template: string): TestBed;
/**
* Overwrites all providers for the given token with the given provider definition.
*/
overrideProvider(token: any, provider: {
useFactory: Function;
deps: any[];
multi?: boolean;
}): TestBed;
overrideProvider(token: any, provider: {
useValue: any;
multi?: boolean;
}): TestBed;
overrideProvider(token: any, provider: {
useFactory?: Function;
useValue?: any;
deps?: any[];
multi?: boolean;
}): TestBed;
overrideTemplateUsingTestingModule(component: Type<any>, template: string): TestBed;
createComponent<T>(component: Type<T>): ComponentFixture<T>;
/**
* Execute any pending effects.
*
* @developerPreview
*/
flushEffects(): void;
}
/**
* @description
* Configures and initializes environment for unit testing and provides methods for
* creating components and services in unit tests.
*
* `TestBed` is the primary api for writing unit tests for Angular applications and libraries.
*
* @publicApi
*/
declare const TestBed: TestBedStatic;
/**
* Allows injecting dependencies in `beforeEach()` and `it()`. Note: this function
* (imported from the `@angular/core/testing` package) can **only** be used to inject dependencies
* in tests. To inject dependencies in your application code, use the [`inject`](api/core/inject)
* function from the `@angular/core` package instead.
*
* Example:
*
* ```ts
* beforeEach(inject([Dependency, AClass], (dep, object) => {
* // some code that uses `dep` and `object`
* // ...
* }));
*
* it('...', inject([AClass], (object) => {
* object.doSomething();
* expect(...);
* })
* ```
*
* @publicApi
*/
declare function inject(tokens: any[], fn: Function): () => any;
/**
* @publicApi
*/
declare class InjectSetupWrapper {
private _moduleDef;
constructor(_moduleDef: () => TestModuleMetadata);
private _addModule;
inject(tokens: any[], fn: Function): () => any;
}
/**
* @publicApi
*/
declare function withModule(moduleDef: TestModuleMetadata): InjectSetupWrapper;
declare function withModule(moduleDef: TestModuleMetadata, fn: Function): () => any;
/**
* This API should be removed. But doing so seems to break `google3` and so it requires a bit of
* investigation.
*
* A work around is to mark it as `@codeGenApi` for now and investigate later.
*
* @codeGenApi
*/
declare const __core_private_testing_placeholder__ = "";
declare class MetadataOverrider {
private _references;
/**
* Creates a new instance for the given metadata class
* based on an old instance and overrides.
*/
overrideMetadata<C extends T, T>(metadataClass: {
new (options: T): C;
}, oldMetadata: C, override: MetadataOverride<T>): C;
}
/**
* Fake implementation of user agent history and navigation behavior. This is a
* high-fidelity implementation of browser behavior that attempts to emulate
* things like traversal delay.
*/
declare class FakeNavigation implements Navigation {
/**
* The fake implementation of an entries array. Only same-document entries
* allowed.
*/
private readonly entriesArr;
/**
* The current active entry index into `entriesArr`.
*/
private currentEntryIndex;
/**
* A Map of pending traversals, so that traversals to the same entry can be
* re-used.
*/
private readonly traversalQueue;
/**
* A Promise that resolves when the previous traversals have finished. Used to
* simulate the cross-process communication necessary for traversals.
*/
private nextTraversal;
/**
* A prospective current active entry index, which includes unresolved
* traversals. Used by `go` to determine where navigations are intended to go.
*/
private prospectiveEntryIndex;
/**
* A test-only option to make traversals synchronous, rather than emulate
* cross-process communication.
*/
private synchronousTraversals;
/** Whether to allow a call to setInitialEntryForTesting. */
private canSetInitialEntry;
/** The next unique id for created entries. Replace recreates this id. */
private nextId;
/** The next unique key for created entries. Replace inherits this id. */
private nextKey;
/** Whether this fake is disposed. */
private disposed;
/** Equivalent to `navigation.currentEntry`. */
get currentEntry(): FakeNavigationHistoryEntry;
get canGoBack(): boolean;
get canGoForward(): boolean;
private readonly createEventTarget;
private readonly _window;
get window(): Pick<Window, 'addEventListener' | 'removeEventListener'>;
constructor(doc: Document, startURL: `http${string}`);
/**
* Sets the initial entry.
*/
setInitialEntryForTesting(url: `http${string}`, options?: {
historyState: unknown;
state?: unknown;
}): void;
/** Returns whether the initial entry is still eligible to be set. */
canSetInitialEntryForTesting(): boolean;
/**
* Sets whether to emulate traversals as synchronous rather than
* asynchronous.
*/
setSynchronousTraversalsForTesting(synchronousTraversals: boolean): void;
/** Equivalent to `navigation.entries()`. */
entries(): FakeNavigationHistoryEntry[];
/** Equivalent to `navigation.navigate()`. */
navigate(url: string, options?: NavigationNavigateOptions): FakeNavigationResult;
/** Equivalent to `history.pushState()`. */
pushState(data: unknown, title: string, url?: string): void;
/** Equivalent to `history.replaceState()`. */
replaceState(data: unknown, title: string, url?: string): void;
private pushOrReplaceState;
/** Equivalent to `navigation.traverseTo()`. */
traverseTo(key: string, options?: NavigationOptions): FakeNavigationResult;
/** Equivalent to `navigation.back()`. */
back(options?: NavigationOptions): FakeNavigationResult;
/** Equivalent to `navigation.forward()`. */
forward(options?: NavigationOptions): FakeNavigationResult;
/**
* Equivalent to `history.go()`.
* Note that this method does not actually work precisely to how Chrome
* does, instead choosing a simpler model with less unexpected behavior.
* Chrome has a few edge case optimizations, for instance with repeated
* `back(); forward()` chains it collapses certain traversals.
*/
go(direction: number): void;
/** Runs a traversal synchronously or asynchronously */
private runTraversal;
/** Equivalent to `navigation.addEventListener()`. */
addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: AddEventListenerOptions | boolean): void;
/** Equivalent to `navigation.removeEventListener()`. */
removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: EventListenerOptions | boolean): void;
/** Equivalent to `navigation.dispatchEvent()` */
dispatchEvent(event: Event): boolean;
/** Cleans up resources. */
dispose(): void;
/** Returns whether this fake is disposed. */
isDisposed(): boolean;
/**
* Implementation for all navigations and traversals.
* @returns true if the event was intercepted, otherwise false
*/
private userAgentNavigate;
/** Utility method for finding entries with the given `key`. */
private findEntry;
set onnavigate(_handler: ((this: Navigation, ev: NavigateEvent) => any) | null);
get onnavigate(): ((this: Navigation, ev: NavigateEvent) => any) | null;
set oncurrententrychange(_handler: // tslint:disable-next-line:no-any
((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any) | null);
get oncurrententrychange(): // tslint:disable-next-line:no-any
((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any) | null;
set onnavigatesuccess(_handler: ((this: Navigation, ev: Event) => any) | null);
get onnavigatesuccess(): ((this: Navigation, ev: Event) => any) | null;
set onnavigateerror(_handler: ((this: Navigation, ev: ErrorEvent) => any) | null);
get onnavigateerror(): ((this: Navigation, ev: ErrorEvent) => any) | null;
private _transition;
get transition(): NavigationTransition | null;
updateCurrentEntry(_options: NavigationUpdateCurrentEntryOptions): void;
reload(_options?: NavigationReloadOptions): NavigationResult;
}
/**
* Fake equivalent of the `NavigationResult` interface with
* `FakeNavigationHistoryEntry`.
*/
interface FakeNavigationResult extends NavigationResult {
readonly committed: Promise<FakeNavigationHistoryEntry>;
readonly finished: Promise<FakeNavigationHistoryEntry>;
}
/**
* Fake equivalent of `NavigationHistoryEntry`.
*/
declare class FakeNavigationHistoryEntry implements NavigationHistoryEntry {
private eventTarget;
readonly url: string | null;
readonly sameDocument: boolean;
readonly id: string;
readonly key: string;
readonly index: number;
private readonly state;
private readonly historyState;
ondispose: ((this: NavigationHistoryEntry, ev: Event) => any) | null;
constructor(eventTarget: EventTarget, url: string | null, { id, key, index, sameDocument, state, historyState, }: {
id: string;
key: string;
index: number;
sameDocument: boolean;
historyState: unknown;
state?: unknown;
});
getState(): unknown;
getHistoryState(): unknown;
addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: AddEventListenerOptions | boolean): void;
removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: EventListenerOptions | boolean): void;
dispatchEvent(event: Event): boolean;
/** internal */
dispose(): void;
}
declare class Log<T = string> {
logItems: T[];
constructor();
add(value: T): void;
fn(value: T): () => void;
clear(): void;
result(): string;
static ɵfac: _angular_core.ɵɵFactoryDeclaration<Log<any>, never>;
static ɵprov: _angular_core.ɵɵInjectableDeclaration<Log<any>>;
}
export { ComponentFixture, ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, DeferBlockFixture, InjectSetupWrapper, TestBed, TestComponentRenderer, __core_private_testing_placeholder__, discardPeriodicTasks, fakeAsync, flush, flushMicrotasks, getTestBed, inject, resetFakeAsyncZone, tick, waitForAsync, withModule, FakeNavigation as ɵFakeNavigation, Log as ɵLog, MetadataOverrider as ɵMetadataOverrider };
export type { MetadataOverride, ModuleTeardownOptions, TestBedStatic, TestEnvironmentOptions, TestModuleMetadata };

View File

@@ -0,0 +1,214 @@
/**
* @license Angular v19.2.14
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
/**
* A comparison function which can determine if two values are equal.
*/
type ValueEqualityFn<T> = (a: T, b: T) => boolean;
/**
* The default equality function used for `signal` and `computed`, which uses referential equality.
*/
declare function defaultEquals<T>(a: T, b: T): boolean;
type Version = number & {
__brand: 'Version';
};
/**
* Symbol used to tell `Signal`s apart from other functions.
*
* This can be used to auto-unwrap signals in various cases, or to auto-wrap non-signal values.
*/
declare const SIGNAL: unique symbol;
declare function setActiveConsumer(consumer: ReactiveNode | null): ReactiveNode | null;
declare function getActiveConsumer(): ReactiveNode | null;
declare function isInNotificationPhase(): boolean;
interface Reactive {
[SIGNAL]: ReactiveNode;
}
declare function isReactive(value: unknown): value is Reactive;
declare const REACTIVE_NODE: ReactiveNode;
/**
* A producer and/or consumer which participates in the reactive graph.
*
* Producer `ReactiveNode`s which are accessed when a consumer `ReactiveNode` is the
* `activeConsumer` are tracked as dependencies of that consumer.
*
* Certain consumers are also tracked as "live" consumers and create edges in the other direction,
* from producer to consumer. These edges are used to propagate change notifications when a
* producer's value is updated.
*
* A `ReactiveNode` may be both a producer and consumer.
*/
interface ReactiveNode {
/**
* Version of the value that this node produces.
*
* This is incremented whenever a new value is produced by this node which is not equal to the
* previous value (by whatever definition of equality is in use).
*/
version: Version;
/**
* Epoch at which this node is verified to be clean.
*
* This allows skipping of some polling operations in the case where no signals have been set
* since this node was last read.
*/
lastCleanEpoch: Version;
/**
* Whether this node (in its consumer capacity) is dirty.
*
* Only live consumers become dirty, when receiving a change notification from a dependency
* producer.
*/
dirty: boolean;
/**
* Producers which are dependencies of this consumer.
*
* Uses the same indices as the `producerLastReadVersion` and `producerIndexOfThis` arrays.
*/
producerNode: ReactiveNode[] | undefined;
/**
* `Version` of the value last read by a given producer.
*
* Uses the same indices as the `producerNode` and `producerIndexOfThis` arrays.
*/
producerLastReadVersion: Version[] | undefined;
/**
* Index of `this` (consumer) in each producer's `liveConsumers` array.
*
* This value is only meaningful if this node is live (`liveConsumers.length > 0`). Otherwise
* these indices are stale.
*
* Uses the same indices as the `producerNode` and `producerLastReadVersion` arrays.
*/
producerIndexOfThis: number[] | undefined;
/**
* Index into the producer arrays that the next dependency of this node as a consumer will use.
*
* This index is zeroed before this node as a consumer begins executing. When a producer is read,
* it gets inserted into the producers arrays at this index. There may be an existing dependency
* in this location which may or may not match the incoming producer, depending on whether the
* same producers were read in the same order as the last computation.
*/
nextProducerIndex: number;
/**
* Array of consumers of this producer that are "live" (they require push notifications).
*
* `liveConsumerNode.length` is effectively our reference count for this node.
*/
liveConsumerNode: ReactiveNode[] | undefined;
/**
* Index of `this` (producer) in each consumer's `producerNode` array.
*
* Uses the same indices as the `liveConsumerNode` array.
*/
liveConsumerIndexOfThis: number[] | undefined;
/**
* Whether writes to signals are allowed when this consumer is the `activeConsumer`.
*
* This is used to enforce guardrails such as preventing writes to writable signals in the
* computation function of computed signals, which is supposed to be pure.
*/
consumerAllowSignalWrites: boolean;
readonly consumerIsAlwaysLive: boolean;
/**
* Tracks whether producers need to recompute their value independently of the reactive graph (for
* example, if no initial value has been computed).
*/
producerMustRecompute(node: unknown): boolean;
producerRecomputeValue(node: unknown): void;
consumerMarkedDirty(node: unknown): void;
/**
* Called when a signal is read within this consumer.
*/
consumerOnSignalRead(node: unknown): void;
/**
* A debug name for the reactive node. Used in Angular DevTools to identify the node.
*/
debugName?: string;
/**
* Kind of node. Example: 'signal', 'computed', 'input', 'effect'.
*
* ReactiveNode has this as 'unknown' by default, but derived node types should override this to
* make available the kind of signal that particular instance of a ReactiveNode represents.
*
* Used in Angular DevTools to identify the kind of signal.
*/
kind: string;
}
/**
* Called by implementations when a producer's signal is read.
*/
declare function producerAccessed(node: ReactiveNode): void;
/**
* Increment the global epoch counter.
*
* Called by source producers (that is, not computeds) whenever their values change.
*/
declare function producerIncrementEpoch(): void;
/**
* Ensure this producer's `version` is up-to-date.
*/
declare function producerUpdateValueVersion(node: ReactiveNode): void;
/**
* Propagate a dirty notification to live consumers of this producer.
*/
declare function producerNotifyConsumers(node: ReactiveNode): void;
/**
* Whether this `ReactiveNode` in its producer capacity is currently allowed to initiate updates,
* based on the current consumer context.
*/
declare function producerUpdatesAllowed(): boolean;
declare function consumerMarkDirty(node: ReactiveNode): void;
declare function producerMarkClean(node: ReactiveNode): void;
/**
* Prepare this consumer to run a computation in its reactive context.
*
* Must be called by subclasses which represent reactive computations, before those computations
* begin.
*/
declare function consumerBeforeComputation(node: ReactiveNode | null): ReactiveNode | null;
/**
* Finalize this consumer's state after a reactive computation has run.
*
* Must be called by subclasses which represent reactive computations, after those computations
* have finished.
*/
declare function consumerAfterComputation(node: ReactiveNode | null, prevConsumer: ReactiveNode | null): void;
/**
* Determine whether this consumer has any dependencies which have changed since the last time
* they were read.
*/
declare function consumerPollProducersForChange(node: ReactiveNode): boolean;
/**
* Disconnect this consumer from the graph.
*/
declare function consumerDestroy(node: ReactiveNode): void;
interface SignalNode<T> extends ReactiveNode {
value: T;
equal: ValueEqualityFn<T>;
}
type SignalBaseGetter<T> = (() => T) & {
readonly [SIGNAL]: unknown;
};
interface SignalGetter<T> extends SignalBaseGetter<T> {
readonly [SIGNAL]: SignalNode<T>;
}
/**
* Create a `Signal` that can be set or updated directly.
*/
declare function createSignal<T>(initialValue: T, equal?: ValueEqualityFn<T>): SignalGetter<T>;
declare function setPostSignalSetFn(fn: (() => void) | null): (() => void) | null;
declare function signalSetFn<T>(node: SignalNode<T>, newValue: T): void;
declare function signalUpdateFn<T>(node: SignalNode<T>, updater: (value: T) => T): void;
declare function runPostSignalSetFn(): void;
declare const SIGNAL_NODE: SignalNode<unknown>;
declare function setAlternateWeakRefImpl(impl: unknown): void;
export { REACTIVE_NODE, SIGNAL, SIGNAL_NODE, consumerAfterComputation, consumerBeforeComputation, consumerDestroy, consumerMarkDirty, consumerPollProducersForChange, createSignal, defaultEquals, getActiveConsumer, isInNotificationPhase, isReactive, producerAccessed, producerIncrementEpoch, producerMarkClean, producerNotifyConsumers, producerUpdateValueVersion, producerUpdatesAllowed, runPostSignalSetFn, setActiveConsumer, setAlternateWeakRefImpl, setPostSignalSetFn, signalSetFn, signalUpdateFn };
export type { Reactive, ReactiveNode, SignalGetter, SignalNode, ValueEqualityFn };