gestión de dependencias - Prevención de importaciones inapropiadas y aplicación de la jerarquía del proyecto en Typescript
En un proyecto de TS me gustaría que lo siguiente estuviera bloqueado:
- Un archivo de la carpeta
commonimportando desde la carpetaprojectA - Un archivo de la carpeta
projectBimportando desde la carpetaprojectA
Me gustaría que se permitiera lo siguiente:
- Un archivo de la carpeta
projectAimportando desde la carpetacommon.
Estoy al tanto de las Referencias. Sin embargo, según entiendo, requieren una compilación para la verificación de tipos (si se hace tal separación, primero se debe compilar para crear los archivos d.ts), lo cual preferiría evitar.
¿Qué opciones tengo? ¿Es posible lograrlo simplemente mediante archivos tsconfig separados para cada uno de esos proyectos/carpetas?
Respuesta #1
TLDR; Realmente deberías usar Referencias. Es exactamente para lo que sirven.
Pero primero abordemos algunos de tus pensamientos específicos:
¿Es posible lograrlo simplemente mediante archivos tsconfig separados para cada uno de esos proyectos/carpetas?
Sí, pero no es muy flexible. Podrías aislar common configurando su
rootDira.. Entonces obtendrías un error'/path/to/projectA' no está bajo 'rootDir'si intentaras importar projectA en common. Pero para poder importar common en projectA, surootDirtendría que ser más global, pero entonces eso permitiría que importaras projectB.No solo eso, según la documentación de Referencias del Proyecto:
Anteriormente, esta estructura era bastante incómoda de trabajar si usabas un solo archivo tsconfig:
- Era posible que los archivos de implementación importaran los archivos de prueba
- No era posible construir
testysrcal mismo tiempo sin quesrcapareciera en el nombre de la carpeta de salida, lo que probablemente no quieras - Cambiar solo los internals en los archivos de implementación requería typechecking de las pruebas nuevamente, aunque esto nunca causaría nuevos errores
Cambiar solo las pruebas requería verificar el tipo de la implementación nuevamente, incluso si nada había cambiado
- No hay comprobación actualizada incorporada, así que terminas ejecutando
tscdos veces - Invocar
tscdos veces incurre en más sobrecarga de tiempo de inicio tsc -wno puede ejecutarse en múltiples archivos de configuración a la vezEstoy al tanto de las Referencias. Sin embargo, según entiendo, requieren una compilación para la verificación de tipos (Si se hace tal separación, primero se debe compilar para crear archivos d.ts) lo cual preferiría evitar.
¿Cuál es la razón de esta aversión?
Si se trata del costo inicial de construir un clon de un proyecto nuevo, eso se compensará ampliamente con los tiempos de compilación mejorados (ver argumentos a favor a continuación). Los beneficios de esto último para la productividad del desarrollador superarán con creces los costos de lo primero.
¡Irónicamente, cuanto más te preocupes por el costo inicial, mayor será el beneficio de los tiempos de construcción mejorados!
Si es que quieres poder navegar un clon reciente en un editor consciente de tipos y enlaces como VS Code o WebStorm sin tener que compilar, puedes lograr esto incluyendo los archivos
.d.tsen el control de versiones.
Podrías usar múltiples archivos tsconfig para resolver algunos de esos problemas, pero aparecerían nuevos:
Esto es lo que los documentos dicen específicamente:
Debido a que los proyectos dependientes hacen uso de archivos
.d.tsque se generan a partir de sus dependencias, tendrás que registrar ciertos resultados de la compilación o compilar un proyecto después de clonarlo antes de poder navegar por el proyecto en un editor sin ver errores espurios. Estamos trabajando en un proceso de generación de .d.ts detrás de escena que debería ser capaz de mitigar esto, pero por ahora recomendamos informar a los desarrolladores que deben compilar después de clonar.
El argumento a favor de las referencias de proyecto
De los documentos:
puedes mejorar mucho los tiempos de construcción
Una característica muy esperada son las compilaciones incrementales inteligentes para proyectos de TypeScript. En la versión 3.0 puedes usar la bandera
--buildcontsc. Esto es efectivamente un nuevo punto de entrada paratscque se comporta más como un orquestador de compilaciones que como un simple compilador.Ejecutar
tsc --build(tsc -bpara abreviar) hará lo siguiente:- Encontrar todos los proyectos referenciados
- Detectar si están actualizados
Construir proyectos desactualizados en el orden correcto
No te preocupes por el orden de los archivos que pases en la línea de comandos: tsc los reorganizará si es necesario para que las dependencias siempre se construyan primero.
hacer cumplir la separación lógica entre componentes
organiza tu código de maneras nuevas y mejores.
Hay algunos beneficios/características más útiles en el documento de Referencias del Proyecto.
Configuración de ejemplo
src/tsconfig.jsonIncluso si no tienes código en la raíz, este tsconfig puede ser donde vayan todas las configuraciones comunes (los demás heredarán de él), y permitirá un simple
tsc --build srcpara construir todo el proyecto (y con--forcepara construirlo desde cero).{ "compilerOptions": { "outDir": ".", // prevents this tsconfig from compiling any files // we want subprojects to inherit these options: "target": "ES2019", "module": "es2020", "strict": true, ... }, // building this project will build all of the following: "references": [ { "path": "./common" } { "path": "./projectA" } { "path": "./projectB" } ] }src/common/tsconfig.jsonDebido a que common no tiene referencias, las importaciones se limitan a objetivos dentro de su directorio y
npm_modules. Incluso podrías restringir este último, creo, dándole su propiopackage.json.{ "extends": "../tsconfig.json", //inherit from root tsconfig "compilerOptions": { "rootDir": ".", "outDir": "../../build/common", "composite": true } }
src/projectA/tsconfig.jsonprojectA puede importar common debido a la referencia declarada.
{ "extends": "../tsconfig.json", //inherit from root tsconfig "compilerOptions": { "rootDir": ".", "outDir": "../../build/projectA", "composite": true }, "references": [ { "path": "../common" } ] }src/projectB/tsconfig.jsonprojectB puede importar common Y projectA debido a las referencias declaradas.
{ "extends": "../tsconfig.json", //inherit from root tsconfig "compilerOptions": { "rootDir": ".", "outDir": "../../build/projectB", "composite": true }, "references": [ { "path": "../common" } { "path": "../projectA" } ] }
Construye
Estos son solo algunos ejemplos. Uso las formas abreviadas de los interruptores de tsc a continuación, por ejemplo, -b en lugar de --build. Todos los comandos se ejecutan desde la raíz del repositorio.
tsc -b src - construye todo el árbol.
tsc -p src/projectA/ compila solo projectA.
tsc -b src/projectA/ compila projectA y cualquier dependencia que esté desactualizada.
tsc -b -w src - compilar y observar todo el árbol.
tsc -b --clean src - elimina la salida de todo el árbol.
tsc -b -f src - forzar una reconstrucción de todo el árbol.
Usa el conmutador -d o -dry para obtener una vista previa de lo que hará tsc -b.
Respuesta #2
Sugiero usar un linter para ese trabajo, no es necesario ajustar el paso de compilación o usar Referencias de Proyecto.
eslint-plugin-import es un plugin de ESLint bastante popular, compatible con TS y puede hacer lo que quieras. Después de haber configurado typescript-eslint (si aún no lo has hecho), puedes experimentar con estas reglas:
- import/no-restricted-paths - Restringir qué archivos se pueden importar en una carpeta determinada
- import/no-relative-parent-imports - Evitar importaciones a carpetas en rutas relativas de padres
- import/no-internal-modules - Evitar importar los submódulos de otros módulos
Vamos a intentar con la siguiente estructura de proyecto:
| .eslintrc.js
| package.json
| tsconfig.json
\---src
+---common
| common.ts
|
+---projectA
| a.ts
|
\---projectB
b.ts
.eslintrc.js:
module.exports = {
extends: ["plugin:import/typescript"],
parser: "@typescript-eslint/parser",
parserOptions: {
sourceType: "module",
project: "./tsconfig.json",
},
plugins: ["@typescript-eslint", "import"],
rules: {
"import/no-restricted-paths": [
"error",
{
basePath: "./src",
zones: [
// disallow import from projectB in common
{ target: "./common", from: "./projectB" },
// disallow import from projectB in projectA
{ target: "./projectA", from: "./projectB" },
],
},
],
"import/no-relative-parent-imports": "error",
},
};
Cada zona consiste en la ruta de destino y una ruta origen. La ruta de destino es la ruta donde se deben aplicar las importaciones restringidas. La ruta origen define la carpeta que no se permite usar en una importación.
Examinando el archivo ./src/common/common.ts:
import { a } from "../projectA/a"; // works
// Error: Unexpected path "../projectB/b" imported in restricted zone.
import { b } from "../projectB/b";
La regla import/no-relative-parent-imports también se queja por ambas importaciones, como para a.ts:
No se permiten importaciones relativas desde directorios superiores. Por favor, pase lo que está importando en tiempo de ejecución (inyección de dependencias), mueva
common.tsal mismo directorio que../projectA/ao considere convertir../projectA/aen un paquete.
La tercera regla import/no-internal-modules no se utilizó, pero también la menciono aquí, ya que puede ser muy útil para restringir el acceso a carpetas/módulos hijos y emular (al menos) algún tipo de modificador interno de paquete en TS.
