/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/core/src/render3/hmr.ts
409 строк
16 KB
RonGamzu
docs: fix typos in source code comments
27 май 2026, 21:18
27 май 2026, 21:18
6f56202
Код
Авторство
О чём код?
/*! * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {Type} from '../interface/type'; import {assertDefined, assertEqual, assertNotEqual} from '../util/assert'; import {assertLView} from './assert'; import {getComponentDef} from './def_getters'; import {assertComponentDef} from './errors'; import {refreshView} from './instructions/change_detection'; import {renderView} from './instructions/render'; import {CONTAINER_HEADER_OFFSET} from './interfaces/container'; import {ComponentDef} from './interfaces/definition'; import {getTrackedLViews} from './interfaces/lview_tracking'; import {isTNodeShape, TElementNode, TNodeFlags, TNodeType} from './interfaces/node'; import {isLContainer, isLView, isRootView} from './interfaces/type_checks'; import { CHILD_HEAD, CHILD_TAIL, CONTEXT, ENVIRONMENT, HEADER_OFFSET, HOST, INJECTOR, LView, NEXT, PARENT, RENDERER, T_HOST, TVIEW, } from './interfaces/view'; import {assertTNodeType} from './node_assert'; import {cleanupLView as cleanupDehydratedLView} from '../hydration/cleanup'; import {destroyLView, removeViewFromDOM} from './node_manipulation'; import {RendererFactory} from './interfaces/renderer'; import {NgZone} from '../zone'; import {ViewEncapsulation} from '../metadata/view'; import {NG_COMP_DEF} from './fields'; import { createLView, getInitialLViewFlagsFromDef, getOrCreateComponentTView, } from './view/construction'; /** Represents `import.meta` plus some information that's not in the built-in types. */ type ImportMetaExtended = ImportMeta & { hot?: { send?: (name: string, payload: unknown) => void; }; }; /** * Gets the URL from which the client will fetch a new version of a component's metadata so it * can be replaced during hot module reloading. * @param id Unique ID for the component, generated during compile time. * @param timestamp Time at which the request happened. * @param base Base URL against which to resolve relative paths. * @codeGenApi */ export function ɵɵgetReplaceMetadataURL(id: string, timestamp: string, base: string): string { const url = `./@ng/component?c=${id}&t=${encodeURIComponent(timestamp)}`; return new URL(url, base).href; } /** * Replaces the metadata of a component type and re-renders all live instances of the component. * @param type Class whose metadata will be replaced. * @param applyMetadata Callback that will apply a new set of metadata on the `type` when invoked. * @param environment Synthetic namespace imports that need to be passed along to the callback. * @param locals Local symbols from the source location that have to be exposed to the callback. * @param importMeta `import.meta` from the call site of the replacement function. Optional since * it isn't used internally. * @param id ID to the class being replaced. **Not** the same as the component definition ID. * Optional since the ID might not be available internally. * @codeGenApi */ export function ɵɵreplaceMetadata( type: Type<unknown>, applyMetadata: (...args: [Type<unknown>, unknown[], ...unknown[]]) => void, namespaces: unknown[], locals: unknown[], importMeta: ImportMetaExtended | null = null, id: string | null = null, ) { ngDevMode && assertComponentDef(type); const currentDef = getComponentDef(type)!; // The reason `applyMetadata` is a callback that is invoked (almost) immediately is because // the compiler usually produces more code than just the component definition, e.g. there // can be functions for embedded views, the variables for the constant pool and `setClassMetadata` // calls. The callback allows us to keep them isolate from the rest of the app and to invoke // them at the right time. applyMetadata.apply(null, [type, namespaces, ...locals]); const {newDef, oldDef} = mergeWithExistingDefinition(currentDef, getComponentDef(type)!); // TODO(crisbeto): the `applyMetadata` call above will replace the definition on the type. // Ideally we should adjust the compiler output so the metadata is returned, however that'll // require some internal changes. We re-add the metadata here manually. (type as any)[NG_COMP_DEF] = newDef; // If a `tView` hasn't been created yet, it means that this component hasn't been instantianted // before. In this case there's nothing left for us to do aside from patching it in. if (oldDef.tView) { const trackedViews = getTrackedLViews().values(); for (const root of trackedViews) { // Note: we have the additional check, because `IsRoot` can also indicate // a component created through something like `createComponent`. if (isRootView(root) && root[PARENT] === null) { recreateMatchingLViews(importMeta, id, newDef, oldDef, root); } } } } /** * Merges two component definitions while preseving the original one in place. * @param currentDef Definition that should receive the new metadata. * @param newDef Source of the new metadata. */ function mergeWithExistingDefinition( currentDef: ComponentDef<unknown>, newDef: ComponentDef<unknown>, ) { // Clone the current definition since we reference its original data further // down in the replacement process (e.g. when destroying the renderer). const clone = {...currentDef}; // Assign the new metadata in place while preserving the object literal. It's important to // Keep the object in place, because there can be references to it, for example in the // `directiveDefs` of another definition. const replacement = Object.assign(currentDef, newDef, { // We need to keep the existing directive and pipe defs, because they can get patched on // by a call to `setComponentScope` from a module file. That call won't make it into the // HMR replacement function, because it lives in an entirely different file. directiveDefs: clone.directiveDefs, pipeDefs: clone.pipeDefs, // Preserve the old `setInput` function, because it has some state. // This is fine, because the component instance is preserved as well. setInput: clone.setInput, // Externally this is redundant since we redeclare the definition using the original type. // Internally we may receive a definition with an alternate, but identical, type so we have // to ensure that the original one is preserved. type: clone.type, }); ngDevMode && assertEqual(replacement, currentDef, 'Expected definition to be merged in place'); return {newDef: replacement, oldDef: clone}; } /** * Finds all LViews matching a specific component definition and recreates them. * @param importMeta `import.meta` information. * @param id HMR ID of the component. * @param oldDef Component definition to search for. * @param rootLView View from which to start the search. */ function recreateMatchingLViews( importMeta: ImportMetaExtended | null, id: string | null, newDef: ComponentDef<unknown>, oldDef: ComponentDef<unknown>, rootLView: LView, ): void { ngDevMode && assertDefined( oldDef.tView, 'Expected a component definition that has been instantiated at least once', ); const tView = rootLView[TVIEW]; // Use `tView` to match the LView since `instanceof` can // produce false positives when using inheritance. if (tView === oldDef.tView) { ngDevMode && assertComponentDef(oldDef.type); recreateLView(importMeta, id, newDef, oldDef, rootLView); return; } for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) { const current = rootLView[i]; if (isLContainer(current)) { // The host can be an LView if a component is injecting `ViewContainerRef`. if (isLView(current[HOST])) { recreateMatchingLViews(importMeta, id, newDef, oldDef, current[HOST]); } for (let j = CONTAINER_HEADER_OFFSET; j < current.length; j++) { recreateMatchingLViews(importMeta, id, newDef, oldDef, current[j]); } } else if (isLView(current)) { recreateMatchingLViews(importMeta, id, newDef, oldDef, current); } } } /** * Removes any cached renderers from the factory for the provided type. * This is currently used by the HMR logic to ensure Renderers are kept * synchronized with any definition metadata updates. * @param factory A RendererFactory2 instance. * @param def A ComponentDef instance. */ function clearRendererCache(factory: RendererFactory, def: ComponentDef<unknown>) { // Cast to read a private field. // NOTE: This must be kept synchronized with the renderer factory implementation in // platform-browser and platform-browser/animations. (factory as {componentReplaced?: (id: string) => void}).componentReplaced?.(def.id); } /** * Recreates an LView in-place from a new component definition. * @param importMeta `import.meta` information. * @param id HMR ID for the component. * @param newDef Definition from which to recreate the view. * @param oldDef Previous component definition being swapped out. * @param lView View to be recreated. */ function recreateLView( importMeta: ImportMetaExtended | null, id: string | null, newDef: ComponentDef<unknown>, oldDef: ComponentDef<unknown>, lView: LView<unknown>, ): void { const instance = lView[CONTEXT]; let host = lView[HOST]! as HTMLElement; // In theory the parent can also be an LContainer, but it appears like that's // only the case for embedded views which we won't be replacing here. const parentLView = lView[PARENT] as LView; ngDevMode && assertLView(parentLView); const tNode = lView[T_HOST] as TElementNode; ngDevMode && assertTNodeType(tNode, TNodeType.Element); ngDevMode && assertNotEqual(newDef, oldDef, 'Expected different component definition'); const zone = lView[INJECTOR].get(NgZone, null); const recreate = () => { // If we're recreating a component with shadow DOM encapsulation, it will have attached a // shadow root. The browser will throw if we attempt to attach another one and there's no way // to detach it. Our only option is to make a clone only of the root node, replace the node // with the clone and use it for the newly-created LView. if ( oldDef.encapsulation === ViewEncapsulation.ShadowDom || oldDef.encapsulation === ViewEncapsulation.ExperimentalIsolatedShadowDom ) { const newHost = host.cloneNode(false) as HTMLElement; host.replaceWith(newHost); host = newHost; } // Recreate the TView since the template might've changed. const newTView = getOrCreateComponentTView(newDef); // Create a new LView from the new TView, but reusing the existing TNode and DOM node. const newLView = createLView( parentLView, newTView, instance, getInitialLViewFlagsFromDef(newDef), host, tNode, null, null, // The renderer will be created a bit further down once the old one is destroyed. null, null, null, ); // Detach the LView from its current place in the tree so we don't // start traversing any siblings and modifying their structure. replaceLViewInTree(parentLView, lView, newLView, tNode.index); // Destroy the detached LView. destroyLView(lView[TVIEW], lView); // Clean up any dehydrated views left over from SSR hydration. // Neither destroyLView nor removeViewFromDOM handle DOM nodes // stored in LContainer[DEHYDRATED_VIEWS], which causes duplicated // content when the view is re-rendered during HMR. cleanupDehydratedLView(lView); // Always force the creation of a new renderer to ensure state captured during construction // stays consistent with the new component definition by clearing any old ached factories. const rendererFactory = lView[ENVIRONMENT].rendererFactory; clearRendererCache(rendererFactory, oldDef); // Patch a brand-new renderer onto the new view only after the old // view is destroyed so that the runtime doesn't try to reuse it. newLView[RENDERER] = rendererFactory.createRenderer(host, newDef); // Remove the nodes associated with the destroyed LView. This removes the // descendants, but not the host which we want to stay in place. removeViewFromDOM(lView[TVIEW], lView); // Reset the content projection state of the TNode before the first render. // Note that this has to happen after the LView has been destroyed or we // risk some projected nodes not being removed correctly. resetProjectionState(tNode); // Creation pass for the new view. renderView(newTView, newLView, instance); // Update pass for the new view. refreshView(newTView, newLView, newTView.template, instance); }; // The callback isn't guaranteed to be inside the Zone so we need to bring it in ourselves. if (zone === null) { executeWithInvalidateFallback(importMeta, id, recreate); } else { zone.run(() => executeWithInvalidateFallback(importMeta, id, recreate)); } } /** * Runs an HMR-related function and falls back to * invalidating the HMR data if it throws an error. */ function executeWithInvalidateFallback( importMeta: ImportMetaExtended | null, id: string | null, callback: () => void, ) { try { callback(); } catch (e) { const error = e as {message?: string; stack?: string}; // If we have all the necessary information and APIs to send off the invalidation // request, send it before rethrowing so the dev server can decide what to do. if (id !== null && error.message) { const toLog = error.message + (error.stack ? '\n' + error.stack : ''); importMeta?.hot?.send?.('angular:invalidate', {id, message: toLog, error: true}); } // Throw the error in case the page doesn't get refreshed. throw e; } } /** * Replaces one LView in the tree with another one. * @param parentLView Parent of the LView being replaced. * @param oldLView LView being replaced. * @param newLView Replacement LView to be inserted. * @param index Index at which the LView should be inserted. */ function replaceLViewInTree( parentLView: LView, oldLView: LView, newLView: LView, index: number, ): void { // Update the sibling whose `NEXT` pointer refers to the old view. for (let i = HEADER_OFFSET; i < parentLView[TVIEW].bindingStartIndex; i++) { const current = parentLView[i]; if ((isLView(current) || isLContainer(current)) && current[NEXT] === oldLView) { current[NEXT] = newLView; break; } } // Set the new view as the head, if the old view was first. if (parentLView[CHILD_HEAD] === oldLView) { parentLView[CHILD_HEAD] = newLView; } // Set the new view as the tail, if the old view was last. if (parentLView[CHILD_TAIL] === oldLView) { parentLView[CHILD_TAIL] = newLView; } // Update the `NEXT` pointer to the same as the old view. newLView[NEXT] = oldLView[NEXT]; // Clear out the `NEXT` of the old view. oldLView[NEXT] = null; // Insert the new LView at the correct index. parentLView[index] = newLView; } /** * Child nodes mutate the `projection` state of their parent node as they're being projected. * This function resets the `project` back to its initial state. * @param tNode */ function resetProjectionState(tNode: TElementNode): void { // The `projection` is mutated by child nodes as they're being projected. We need to // reset it to the initial state so projection works after the template is swapped out. if (tNode.projection !== null) { for (const current of tNode.projection) { if (isTNodeShape(current)) { // Reset `projectionNext` since it can affect the traversal order during projection. current.projectionNext = null; current.flags &= ~TNodeFlags.isProjected; } } tNode.projection = null; } }