Como funciona el compilador de Angular
Tags: angular, javascript, webdev, spanish
Published: 2022-05-03 23:58:29

Artículo original de [Angular Blog](https://blog.angular.io/) por [Alex Rickabaugh](https://medium.com/@synalx?source=user_popover) en inglés [aquí](https://blog.angular.io/how-the-angular-compiler-works-42111f9d2549):

El Compilador de Angular (al cual llámanos `ngc`) es la herramienta utilizada para compilar aplicaciones y librerías  de Angular. `ngc` se basa en el compilador TypeScript (llamado `tsc`) y amplía el proceso de compilación de código TypeScript para agregar generación de código adicional relacionada con capacidades de Angular.

El compilador de angular sirve como un puente entre la experiencia del desarrollador y el rendimiento del tiempo de ejecución, los usuarios de Angular crean aplicaciones con una API amigable al usuario y basada en decoradores, y `ngc` traduce este código en instrucciones de tiempo de ejecución más eficientes.

Por ejemplo, un componente básico de Angular podría verse de esta manera:


{% gist https://gist.github.com/AntonioCardenas/ccedad8d76f3f0cd981f4be3ef1dfaf3 %}

Después de la compilación a través de `ngc`, este componente en su lugar se ve de esta manera:

{% gist https://gist.github.com/AntonioCardenas/4d8e79f9cf3cc1e2925949b9fd411477 %}


El decorador del `@Component` se remplazó con varias propiedades estáticas (`ɵfac` y `ɵcmp`), que describen este componente en el tiempo de ejecución de Angular e implementan la representación y la detección de cambios para su plantilla.

De esta forma, `ngc` puede considerarse un compilador de TypeScript extendido que también sabe cómo "ejecutar" decoradores Angular, aplicando sus efectos a las clases decoradas en tiempo de compilación (a diferencia del tiempo de ejecución).


## Dentro de ngc
`ngc` tiene varios objetivos importantes:

- Compilar los decoradores Angular, incluidos los componentes y sus plantillas.

- Aplicar las reglas de verificación de tipos de TypeScript a las plantillas de componentes.

- Volver a compilar rápidamente cuando el desarrollador realice cambios.

Examinemos cómo `ngc` gestiona cada uno de estos objetivos.

## Flujo de compilación
El objetivo principal de `ngc` es compilar código TypeScript mientras transforma las clases decoradas de Angular reconocidas en representaciones más eficientes para el tiempo de ejecución. El flujo principal de la compilación Angular procede de la siguiente manera:


1. Crear una instancia del compilador TypeScript, con alguna funcionalidad Angular adicional.

2. Escanear cada archivo en el proyecto en busca de clases decoradas y generar un modelo de qué componentes, directivas, pipes, NgModules, etc. deben compilarse.

3. Hacer conexiones entre clases decoradas (por ejemplo, qué directivas se usan en que plantillas de componentes).

4. Aprovechar TypeScript para verificar expresiones en plantillas de componentes.

5. Compilar todo el programa, incluyendo la generación de código Angular adicional para cada clase decorada.

## Paso 1: Creación del programa TypeScript

En el compilador de TypeScript, un programa que se va a compilar está representado por una instancia de `ts.Program`. Esta instancia combina el conjunto de archivos que se compilarán, escribe la información de las dependencias y el conjunto particular de opciones del compilador que se utilizará.

Identificar el conjunto de archivos y dependencias no es sencillo. A menudo, el usuario especifica un archivo de "punto de entrada" (por ejemplo, `main.ts`), y TypeScript debe mirar las importaciones en ese archivo para descubrir otros archivos que necesitan compilarse. Esos archivos tienen importaciones adicionales, que se expanden a más archivos, y así sucesivamente. Algunas de estas importaciones apuntan a dependencias: referencias a código que no se está compilando, pero que se usa de alguna manera y debe ser conocido por el sistema de tipos de TypeScript. Estas importaciones de dependencia son para archivos `.d.ts`, generalmente en `node_modules` 

En este punto, el compilador de Angular hace algo especial: agrega archivos de entrada adicionales al  `ts.Program`. Para cada archivo escrito por el usuario (por ejemplo, `my.component.ts`), ngc agrega un archivo "sombra" con un sufijo `.ngtypecheck` (por ejemplo, `my.component.ngtypecheck.ts`). Estos archivos se utilizan internamente para verificar el tipo de plantilla (más sobre esto más adelante).

Según las opciones del compilador, `ngc`puede agregar otros archivos al `ts.Program`, como archivos `.ngfactory` para compatibilidad con la arquitectura anterior de View Engine.

## Paso 2: Análisis Individual

En la fase de análisis de la compilación, `ngc` busca clases con decoradores Angular e intenta comprender estáticamente cada decorador. Por ejemplo, si encuentra una clase decorada `@Component `, mira al decorador e intenta determinar la plantilla del componente, su selector, ver la configuración de encapsulación y cualquier otra información sobre el componente que pueda ser necesaria para generar código para él. Esto requiere que el compilador sea capaz de realizar una operación conocida como evaluación parcial: leer expresiones dentro de los metadatos del decorador e intentar interpretar esas expresiones sin ejecutarlas realmente.

## Evaluación parcial
A veces, la información en un decorador angular se oculta detrás de una expresión. Por ejemplo, un selector para un componente se proporciona como una cadena literal, pero también podría ser una constante:

{% gist https://gist.github.com/alxhub/716f5fd50af663922b3834dfd51c229d %}

`ngc` usa las API de TypeScript para navegar por el código para evaluar la expresión `MY_SELECTOR`, rastrearla hasta su declaración y finalmente resolverla en la cadena `'my-cmp'`. El evaluador parcial puede entender constantes simples; objetos y arreglos literales; accesos a las propiedades; importaciones/exportaciones; aritmética y otras operaciones binarias; e incluso evaluar llamadas a funciones simples. Esta función brinda a los desarrolladores de Angular más flexibilidad en la forma en que describen los componentes y otros tipos de Angular al compilador.


## Salida del análisis
Al final de la fase de análisis, el compilador ya tiene una buena idea de qué componentes, directivas, pipes, inyectables y NgModules se encuentran en el programa de entrada. Para cada uno de estos, el compilador construye un objeto de "metadatos" que describe todo lo que aprendió de los decoradores de la clase. En este punto, los componentes tienen sus plantillas y hojas de estilo cargadas desde el disco (si es necesario), y es posible que el compilador ya haya producido errores (conocidos en TypeScript como "diagnósticos") si se detectan errores semánticos en cualquier parte de la entrada hasta el momento.


## Paso 3: Análisis Global
Antes de que pueda verificar el tipo o generar código, el compilador necesita comprender cómo se relacionan entre sí los diversos tipos decorados en el programa. El objetivo principal de este paso es comprender la estructura NgModule del programa.

## NgModules
Para comprobar el tipo y generar código, el compilador necesita saber qué directivas, componentes y conductos se usan en la plantilla de cada componente. Esto no es sencillo porque los componentes de Angular no importan directamente sus dependencias. En cambio, los componentes de Angular describen plantillas utilizando HTML, y las dependencias potenciales se comparan con elementos en esas plantillas utilizando selectores de estilo CSS. Esto habilita una poderosa capa de abstracción: los componentes Angular no necesitan saber exactamente cómo están estructuradas sus dependencias. En cambio, cada componente tiene un conjunto de dependencias potenciales (su "ámbito de compilación de plantilla"), solo un subconjunto del cual terminará haciendo coincidir los elementos de su plantilla.

Esta [indirección](https://es.wikipedia.org/wiki/Indirecci%C3%B3n) se resuelve a través de la abstracción Angular `@NgModule`. NgModules se puede considerar como unidades componibles del ámbito de la plantilla. Un NgModule básico puede verse así:

{% gist https://gist.github.com/alxhub/db47ca300ec077ddd3751bda43697b6d %}

Se puede entender que NgModules declara cada uno dos ámbitos diferentes:

- Un "ámbito de compilación", que representa el conjunto de dependencias potenciales que están disponibles para cualquier componente declarado en el propio NgModule.

- Un "ámbito de exportación", que representa un conjunto de dependencias potenciales que están disponibles en el ámbito de compilación de cualquier NgModules que importe el NgModule determinado.

En el ejemplo anterior, `ImageViewerComponent` es un componente declarado en este NgModule, por lo que sus dependencias potenciales vienen dadas por el ámbito de compilación de NgModule. Este alcance de compilación es la unión de todas las declaraciones y los alcances de exportación de cualquier NgModules que se importe. Debido a esto, es un error en Angular declarar un componente en múltiples NgModules. Además, un componente y su NgModule deben compilarse al mismo tiempo.

En este caso, se importa `CommonModule`, por lo que el ámbito de compilación de `ImageViewerModule` (y, por lo tanto, `ImageViewerComponent`) incluye todas las directivas y canalizaciones exportadas por `CommonModule`- `NgIf`, `NgForOf`, `AsyncPipe`, y media docena más. El alcance de la compilación también incluye ambas directivas declaradas - `ImageViewerComponent` e `ImageResizeDirective`.

Tenga en cuenta que para los componentes, su relación con el NgModule que los declara es bidireccional: el NgModule define el ámbito de la plantilla del componente y hace que ese componente esté disponible en los ámbitos de la plantilla de otros componentes.

El NgModule anterior también declara un "alcance de exportación" que consiste solo en ImageViewerComponent. Otros NgModules que importan este tendrán ImageViewerComponent agregado a sus ámbitos de compilación. De esta manera, NgModule permite la encapsulación de los detalles de implementación de ImageViewerComponent -  internamente, podría usar ImageResizeDirective, pero esta directiva no está disponible para los consumidores de ImageViewerComponent.

Para determinar estos ámbitos, el compilador crea un gráfico de NgModules, sus declaraciones y sus importaciones y exportaciones, utilizando la información que aprendió sobre cada clase individualmente en el paso anterior. También requiere conocimientos sobre dependencias: componentes y NgModules importados de bibliotecas y no declarados en el programa actual. Angular codifica esta información en los archivos .d.ts de esas dependencias.

## metadata .d.ts

Por ejemplo, el `ImageViewerModule` anterior importa `CommonModule` del paquete @angular/common. La evaluación parcial de la lista de importaciones resolverá las clases nombradas en las declaraciones de importaciones dentro de los archivos `.d.ts` de esas dependencias.

El simple hecho de conocer el símbolo de NgModules importados no es suficiente. Para construir su gráfico, el compilador pasa información sobre las declaraciones, importaciones y exportaciones de NgModules a través de los archivos `.d.ts` en un tipo de metadatos especial. Por ejemplo, en el archivo de declaración generado para `CommonModule` de Angular, estos metadatos (simplificados) se ven así:

{% gist https://gist.github.com/alxhub/dd75604e2f2b091c82645f620dc0ae92 %}

Esta declaración de tipo no está destinada a la verificación de tipos por parte de TypeScript, sino que incorpora información (referencias y otros metadatos) sobre la comprensión de Angular de la clase en cuestión en el sistema de tipos. A partir de estos tipos especiales, `ngc` puede determinar el ámbito de exportación de `CommonModule`. Usando las API de TypeScript para resolver las referencias dentro de estos metadatos a esas definiciones de clase, puede extraer metadatos útiles con respecto a las directivas.

{% gist https://gist.github.com/alxhub/044e0f9012661d7d4157964b8a316038 %}

Esto le da a `ngc` suficiente información sobre la estructura del programa para continuar con la compilación.

## Paso 4: Comprobación de tipo de plantilla

`ngc` es capaz de informar errores de tipo dentro de las plantillas de Angular. Por ejemplo, si una plantilla intenta vincular un valor `{{name.first}}` pero el objeto de nombre no tiene una propiedad `first`, `ngc` puede mostrar este problema como un error de tipo. Realizar esta verificación de manera eficiente es un desafío importante para `ngc`.

TypeScript por sí mismo no comprende la sintaxis de la plantilla Angular y no puede verificar el tipo directamente. Para realizar esta verificación, el compilador de Angular convierte las plantillas de Angular en código TypeScript (conocido como "Bloque de verificación de tipo" o TCB) que expresa operaciones equivalentes en el nivel de tipo y alimenta este código a TypeScript para la verificación semántica. Cualquier diagnóstico generado se mapea y se informa al usuario en el contexto de la plantilla original.

Por ejemplo, considere un componente con una plantilla que usa `ngFor`:

{%gist https://gist.github.com/alxhub/b7c93e9d42307cae58cc954ac2b6375d %}

Para esta plantilla, el compilador desea verificar que el acceso a la propiedad `user.name` sea legal. Para hacer esto, primero debe comprender cómo se deriva el tipo de `user` de la variable de bucle a través de `NgFor` a partir de la matriz de entrada de `users`.

El bloque de verificación de tipos que genera el compilador para la plantilla de este componente tiene el siguiente aspecto:

{%gist https://gist.github.com/alxhub/6988c63ae3e692b153ef024a7b0e292b %}

La complejidad aquí parece ser alta, pero fundamentalmente este TCB está realizando una secuencia específica de operaciones:

- Primero deduce el tipo real de la directiva `NgForOf` (que es genérica) a partir de sus enlaces de entrada. Esto se llama `_t1`.

- Valida que la propiedad de usuarios del componente se pueda asignar a la entrada `NgForOf`, a través de la instrucción de asignación `_t1.ngForOf = ctx.users`.

- A continuación, declara un tipo para el contexto de vista incrustado de la plantilla de fila `*ngFor`, denominada `_t2`, con un tipo inicial de cualquier valor.

- Usando un `if` con una llamada de guardia de tipo, usa la [función auxiliar ngTemplateContextGuard](https://docs.angular.lat/guide/structural-directives) de NgForOf para restringir el tipo de `_t2` según cómo funciona `NgForOf`.

- La variable de bucle implícita (usuario en la plantilla) se extrae de este contexto y se le asigna el nombre `_t3`.

- Finalmente, se expresa el acceso `_t3.name`.

Si el acceso `_t3.name` no es legal según las reglas de TypeScript, TypeScript generará un error de diagnóstico para este código. El verificador de tipos de plantilla de Angular puede ver la ubicación de este error en el TCB y usar los comentarios incrustados para mapear el error a la plantilla original antes de mostrárselo al desarrollador.

Dado que las plantillas de Angular contienen referencias a las propiedades de las clases de componentes, tienen tipos del programa del usuario. Por lo tanto, el código de verificación de tipo de plantilla no se puede verificar de forma independiente y debe verificarse dentro del contexto de todo el programa del usuario (en el ejemplo anterior, el tipo de componente se importa del archivo `test.ts` del usuario). `ngc` logra esto agregando los TCB generados al programa del usuario a través de un paso de compilación incremental de TypeScript (generando un nuevo `ts.Program`). Para evitar la [hiperpaginación](https://es.wikipedia.org/wiki/Hiperpaginaci%C3%B3n) de la caché de compilación incremental, se agrega código de verificación de tipo a archivos `.ngtypecheck.ts` separados que el compilador agrega al `ts.Program` en la creación en lugar de directamente a los archivos de usuario.

## Paso 5: Emitir

Cuando comienza este paso, `ngc` ha entendido el programa y validado que no hay errores fatales. Luego se le dice al compilador de TypeScript que genere código JavaScript para el programa. Durante el proceso de generación, los decoradores de Angular se eliminan y, en su lugar, se agregan varios campos estáticos a las clases, con el código Angular generado listo para escribirse en JavaScript.

Si el programa que se compila es una biblioteca, también se generan archivos `.d.ts`. Los archivos contienen metadatos angulares incrustados que describen cómo una compilación futura puede usar esos tipos como dependencias.

## Ser rápido incrementalmente

Si lo anterior suena como mucho trabajo antes de generar el código, es porque lo es. Si bien la lógica de TypeScript y Angular es eficiente, aún puede llevar varios segundos realizar todo el análisis, análisis y síntesis necesarios para producir una salida de JavaScript para el programa de entrada. Por esta razón, tanto TypeScript como Angular admiten un modo de compilación incremental, donde el trabajo realizado anteriormente se reutiliza para actualizar de manera más eficiente un programa compilado cuando se realiza un pequeño cambio en la entrada.
El principal problema de la compilación incremental es: dado un cambio específico en un archivo de entrada, el compilador necesita determinar qué salidas pueden haber cambiado y qué salidas son seguras para reutilizar. El compilador debe ser perfecto y errar por volver a compilar una salida si no puede estar seguro de que no ha cambiado.
Para resolver este problema, el compilador de Angular tiene dos herramientas principales: el gráfico de importación y el gráfico de dependencia semántica.

## Importar gráfico

Como el compilador realiza operaciones de evaluación parcial mientras analiza el programa por primera vez, crea un gráfico de importaciones críticas entre archivos. Esto permite que el compilador comprenda las dependencias entre archivos cuando algo cambia.

Por ejemplo, si el archivo `my.component.ts` tiene un componente y el selector de ese componente está definido por una constante importada de `selector.ts`, el gráfico de importación muestra que `my.component.ts` depende de `selector.ts`. Si `selector.ts` cambia, el compilador puede consultar este gráfico y saber que los resultados del análisis de `my.component.ts` ya no son correctos y deben rehacerse.

El gráfico de importación es importante para comprender lo que podría cambiar, pero tiene dos problemas principales:

- Es demasiado sensible a los cambios no relacionados. Si se cambia `selector.ts`, pero ese cambio solo agrega un comentario, entonces `my.component.ts` realmente no necesita volver a compilarse.

- No todas las dependencias en las aplicaciones de Angular se expresan a través de importaciones. Si el selector de `MyCmp` cambia, otros componentes que usan `MyCmp` en su plantilla pueden verse afectados, aunque nunca importen `MyCmp` directamente.

Ambos problemas se abordan a través de la segunda herramienta incremental del compilador:

## Gráfico de dependencia semántica 
El gráfico de dependencia semántica comienza donde termina el gráfico de importación. Este gráfico captura la semántica real de la compilación: cómo los componentes y las directivas se relacionan entre sí. Su trabajo es saber qué cambios semánticos requerirían que se reprodujera una salida determinada.

Por ejemplo, si se cambia `selector.ts`, pero el selector de `MyCmp` no cambia, entonces el gráfico de profundidad semántica sabrá que nada que afecte semánticamente a `MyCmp` ha cambiado, y la salida anterior de `MyCmp` se puede reutilizar. Por el contrario, si el selector cambia, entonces el conjunto de componentes/directivas utilizadas en otros componentes puede cambiar, y el gráfico semántico sabrá que esos componentes necesitan volver a compilarse.

## Incrementalidad
Por lo tanto, ambos gráficos funcionan juntos para proporcionar una compilación incremental rápida. El gráfico de importación se utiliza para determinar qué análisis se debe volver a realizar y, a continuación, se aplica el gráfico semántico para comprender cómo los cambios en los datos del análisis se propagan a través del programa y requieren que se vuelvan a compilar los resultados. El resultado es un compilador que puede reaccionar eficientemente a los cambios en las entradas y solo hacer la cantidad mínima de trabajo para actualizar correctamente sus salidas en respuesta.

## Resumen
El compilador de Angular aprovecha la flexibilidad de las API del compilador de TypeScript para ofrecer una compilación correcta y eficiente de plantillas y clases de Angular. La compilación de aplicaciones Angular nos permite ofrecer una experiencia de desarrollador deseable en el IDE, brindar comentarios en el tiempo de compilación sobre problemas en el código y transformar ese código durante el proceso de compilación en el JavaScript más eficiente para ejecutar en el navegador.