Los proyectos a menudo evolucionan hacia estructuras de directorio anidadas y complejas. Como resultado, las rutas de importación pueden volverse más largas y confusas, lo que puede afectar negativamente la apariencia del código y dificultar la comprensión de dónde se origina el código importado.
El uso de alias de ruta puede resolver el problema al permitir la definición de importaciones relativas a directorios predefinidos. Este enfoque no solo resuelve los problemas de comprensión de las rutas de importación, sino que también simplifica el proceso de movimiento del código durante la refactorización.
// Without Aliases import { apiClient } from '../../../../shared/api'; import { ProductView } from '../../../../entities/product/components/ProductView'; import { addProductToCart } from '../../../add-to-cart/actions'; // With Aliases import { apiClient } from '#shared/api'; import { ProductView } from '#entities/product/components/ProductView'; import { addProductToCart } from '#features/add-to-cart/actions';
Hay varias bibliotecas disponibles para configurar alias de ruta en Node.js, como alias-hq y tsconfig-paths . Sin embargo, al revisar la documentación de Node.js, descubrí una manera de configurar alias de ruta sin tener que depender de bibliotecas de terceros.
Además, este enfoque permite el uso de alias sin necesidad del paso de compilación.
En este artículo, analizaremos las importaciones de subruta de Node.js y cómo configurar alias de ruta usándolas. También exploraremos su soporte en el ecosistema frontend.
A partir de Node.js v12.19.0, los desarrolladores pueden usar Importaciones de subruta para declarar alias de ruta dentro de un paquete npm. Esto se puede hacer a través del campo imports
en el archivo package.json
. No es necesario publicar el paquete en npm.
Basta con crear un archivo package.json
en cualquier directorio. Por lo tanto, este método también es adecuado para proyectos privados.
Aquí hay un hecho interesante: Node.js introdujo soporte para el campo imports
en 2020 a través del RFC llamado " Resolución de especificador de módulo desnudo en node.js ". Si bien este RFC se reconoce principalmente para el campo exports
, que permite la declaración de puntos de entrada para paquetes npm, los campos de exports
e imports
abordan tareas completamente diferentes, aunque tienen nombres y sintaxis similares.
El soporte nativo para alias de ruta tiene las siguientes ventajas en teoría:
Traté de configurar alias de ruta en mis proyectos y probé esas declaraciones en la práctica.
Como ejemplo, consideremos un proyecto con la siguiente estructura de directorios:
my-awesome-project ├── src/ │ ├── entities/ │ │ └── product/ │ │ └── components/ │ │ └── ProductView.js │ ├── features/ │ │ └── add-to-cart/ │ │ └── actions/ │ │ └── index.js │ └── shared/ │ └── api/ │ └── index.js └── package.json
Para configurar alias de ruta, puede agregar algunas líneas a package.json
como se describe en la documentación. Por ejemplo, si desea permitir las importaciones relativas al directorio src
, agregue el siguiente campo imports
a package.json
:
{ "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
Para usar el alias configurado, las importaciones se pueden escribir así:
import { apiClient } from '#shared/api'; import { ProductView } from '#entities/product/components/ProductView'; import { addProductToCart } from '#features/add-to-cart/actions';
A partir de la fase de configuración, nos enfrentamos a la primera limitación: las entradas en el campo imports
deben comenzar con el símbolo #
. Esto garantiza que se distingan de los especificadores de paquetes como @
.
Creo que esta limitación es útil porque permite a los desarrolladores determinar rápidamente cuándo se usa un alias de ruta en una importación y dónde se pueden encontrar configuraciones de alias.
Para agregar más alias de ruta para módulos de uso común, el campo imports
se puede modificar de la siguiente manera:
{ "name": "my-awesome-project", "imports": { "#modules/*": "./path/to/modules/*", "#logger": "./src/shared/lib/logger.js", "#*": "./src/*" } }
Sería ideal concluir el artículo con la frase "todo lo demás saldrá de la caja". Sin embargo, en realidad, si planea utilizar el campo imports
, puede enfrentar algunas dificultades.
Si planea usar alias de ruta con módulos CommonJS , tengo malas noticias para usted: el siguiente código no funcionará.
const { apiClient } = require('#shared/api'); const { ProductView } = require('#entities/product/components/ProductView'); const { addProductToCart } = require('#features/add-to-cart/actions');
Cuando utilice alias de ruta en Node.js, debe seguir las reglas de resolución de módulos del mundo ESM. Esto se aplica tanto a los módulos ES como a los módulos CommonJS y da como resultado dos nuevos requisitos que deben cumplirse:
Es necesario especificar la ruta completa a un archivo, incluida la extensión del archivo.
No está permitido especificar una ruta a un directorio y esperar importar un archivo index.js
. En su lugar, se debe especificar la ruta completa a un archivo index.js
.
Para permitir que Node.js resuelva correctamente los módulos, las importaciones deben corregirse de la siguiente manera:
const { apiClient } = require('#shared/api/index.js'); const { ProductView } = require('#entities/product/components/ProductView.js'); const { addProductToCart } = require('#features/add-to-cart/actions/index.js');
Estas limitaciones pueden generar problemas al configurar el campo imports
en un proyecto que tiene muchos módulos de CommonJS. Sin embargo, si ya está usando módulos ES, entonces su código cumple con todos los requisitos.
Además, si está compilando código con un paquete, puede omitir estas limitaciones. Discutiremos cómo hacer esto a continuación.
Para resolver correctamente los módulos importados para la verificación de tipos, TypeScript debe ser compatible con el campo imports
. Esta característica es compatible a partir de la versión 4.8.1, pero solo si se cumplen las limitaciones de Node.js enumeradas anteriormente.
Para usar el campo imports
para la resolución del módulo, se deben configurar algunas opciones en el archivo tsconfig.json
.
{ "compilerOptions": { /* Specify what module code is generated. */ "module": "esnext", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "nodenext" } }
Esta configuración permite que el campo imports
funcione de la misma manera que lo hace en Node.js. Esto significa que si olvida incluir una extensión de archivo en la importación de un módulo, TypeScript generará un error advirtiéndole al respecto.
// OK import { apiClient } from '#shared/api/index.js'; // Error: Cannot find module '#src/shared/api/index' or its corresponding type declarations. import { apiClient } from '#shared/api/index'; // Error: Cannot find module '#src/shared/api' or its corresponding type declarations. import { apiClient } from '#shared/api'; // Error: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './relative.js'? import { foo } from './relative';
No quería volver a escribir todas las importaciones, ya que la mayoría de mis proyectos usan un paquete para compilar código y nunca agrego extensiones de archivo cuando importo módulos. Para evitar esta limitación, encontré una manera de configurar el proyecto de la siguiente manera:
{ "name": "my-awesome-project", "imports": { "#*": [ "./src/*", "./src/*.ts", "./src/*.tsx", "./src/*.js", "./src/*.jsx", "./src/*/index.ts", "./src/*/index.tsx", "./src/*/index.js", "./src/*/index.jsx" ] } }
Esta configuración permite la forma habitual de importar módulos sin necesidad de especificar extensiones. Esto incluso funciona cuando una ruta de importación apunta a un directorio.
// OK import { apiClient } from '#shared/api/index.js'; // OK import { apiClient } from '#shared/api/index'; // OK import { apiClient } from '#shared/api'; // Error: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './relative.js'? import { foo } from './relative';
Tenemos un problema pendiente que se refiere a la importación mediante una ruta relativa. Este problema no está relacionado con los alias de ruta. TypeScript arroja un error porque hemos configurado la resolución del módulo para usar el modo nodenext
.
Afortunadamente, se agregó un nuevo modo de resolución de módulo en la versión reciente de TypeScript 5.0 que elimina la necesidad de especificar la ruta completa dentro de las importaciones. Para habilitar este modo, se deben configurar algunas opciones en el archivo tsconfig.json
.
{ "compilerOptions": { /* Specify what module code is generated. */ "module": "esnext", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "bundler" } }
Después de completar la configuración, las importaciones de rutas relativas funcionarán como de costumbre.
// OK import { apiClient } from '#shared/api/index.js'; // OK import { apiClient } from '#shared/api/index'; // OK import { apiClient } from '#shared/api'; // OK import { foo } from './relative';
Ahora, podemos utilizar completamente los alias de ruta a través del campo imports
sin ninguna limitación adicional sobre cómo escribir rutas de importación.
Al compilar el código fuente con el compilador tsc
, es posible que sea necesaria una configuración adicional. Una limitación de TypeScript es que no se puede crear un código en el formato del módulo CommonJS cuando se usa el campo imports
.
Por lo tanto, el código debe compilarse en formato ESM y el campo type
debe agregarse a package.json
para ejecutar el código compilado en Node.js.
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": "./src/*" } }
Si su código se compila en un directorio separado, como build/
, es posible que Node.js no encuentre el módulo porque el alias de la ruta apuntaría a la ubicación original, como src/
. Para resolver este problema, se pueden usar rutas de importación condicionales en el archivo package.json
.
Esto permite que el código ya creado se importe desde el directorio build/
en lugar del directorio src/
.
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": { "default": "./src/*", "production": "./build/*" } } }
Para usar una condición de importación específica, Node.js debe iniciarse con el indicador --conditions
.
node --conditions=production build/index.js
Los empaquetadores de código suelen utilizar su propia implementación de resolución de módulos, en lugar de la integrada en Node.js. Por lo tanto, es importante que implementen soporte para el campo imports
.
He probado alias de ruta con Webpack, Rollup y Vite en mis proyectos y estoy listo para compartir mis hallazgos.
Esta es la configuración del alias de la ruta que usé para probar los paquetes. Usé el mismo truco que para TypeScript para evitar tener que especificar la ruta completa a los archivos dentro de las importaciones.
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": [ "./src/*", "./src/*.ts", "./src/*.tsx", "./src/*.js", "./src/*.jsx", "./src/*/index.ts", "./src/*/index.tsx", "./src/*/index.js", "./src/*/index.jsx" ] } }
Webpack admite el campo imports
a partir de v5.0. Los alias de ruta funcionan sin ninguna configuración adicional. Aquí está la configuración de Webpack que usé para construir un proyecto de prueba con TypeScript:
const config = { mode: 'development', devtool: false, entry: './src/index.ts', module: { rules: [ { test: /\.tsx?$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-typescript'], }, }, }, ], }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, }; export default config;
Se agregó soporte para el campo imports
en Vite versión 4.2.0. Sin embargo, se solucionó un error importante en la versión 4.3.3, por lo que se recomienda utilizar al menos esta versión. En Vite, los alias de ruta funcionan sin necesidad de una configuración adicional en los modos dev
y build
.
Por lo tanto, construí un proyecto de prueba con una configuración completamente vacía.
Aunque Rollup se usa dentro de Vite, el campo imports
no funciona de forma inmediata. Para habilitarlo, debe instalar el complemento @rollup/plugin-node-resolve
versión 11.1.0 o superior. Aquí hay una configuración de ejemplo:
import { nodeResolve } from '@rollup/plugin-node-resolve'; import { babel } from '@rollup/plugin-babel'; export default [ { input: 'src/index.ts', output: { name: 'mylib', file: 'build.js', format: 'es', }, plugins: [ nodeResolve({ extensions: ['.ts', '.tsx', '.js', '.jsx'], }), babel({ presets: ['@babel/preset-typescript'], extensions: ['.ts', '.tsx', '.js', '.jsx'], }), ], }, ];
Desafortunadamente, con esta configuración, los alias de ruta solo funcionan dentro de las limitaciones de Node.js. Esto significa que debe especificar la ruta completa del archivo, incluida la extensión. Especificar una matriz dentro del campo imports
no evitará esta limitación, ya que Rollup solo usa la primera ruta de la matriz.
Creo que es posible resolver este problema usando los complementos de Rollup, pero no he intentado hacerlo porque principalmente uso Rollup para bibliotecas pequeñas. En mi caso, fue más fácil reescribir las rutas de importación a lo largo del proyecto.
Los ejecutores de pruebas son otro grupo de herramientas de desarrollo que dependen en gran medida del mecanismo de resolución del módulo. A menudo usan su propia implementación de resolución de módulos, similar a los paquetes de código. Como resultado, existe la posibilidad de que el campo imports
no funcione como se esperaba.
Afortunadamente, las herramientas que he probado funcionan bien. Probé los alias de ruta con Jest v29.5.0 y Vite v0.30.1. En ambos casos, los alias de la ruta funcionaron a la perfección sin ninguna configuración o limitación adicional. Jest ha tenido soporte para el campo imports
desde la versión v29.4.0.
El nivel de soporte en Vitest se basa únicamente en la versión de Vite, que debe ser al menos v4.2.0.
El campo imports
en las bibliotecas populares actualmente está bien soportado. Sin embargo, ¿qué pasa con los editores de código? Probé la navegación de código, específicamente la función "Ir a definición", en un proyecto que usa alias de ruta. Resulta que la compatibilidad con esta función en los editores de código tiene algunos problemas.
Cuando se trata de VS Code, la versión de TypeScript es crucial. El servidor de lenguaje TypeScript es responsable de analizar y navegar a través del código JavaScript y TypeScript.
Según su configuración, VS Code utilizará la versión integrada de TypeScript o la instalada en su proyecto.
Probé la compatibilidad con campos imports
en VS Code v1.77.3 en combinación con TypeScript v5.0.4.
VS Code tiene los siguientes problemas con los alias de ruta:
TypeScript no usa el campo imports
hasta que la configuración de resolución del módulo se establece en nodenext
o bundler
. Por lo tanto, para usarlo en VS Code, debe especificar la resolución del módulo en su proyecto.
IntelliSense actualmente no admite la sugerencia de rutas de importación mediante el campo imports
. Hay un problema abierto para este problema.
Para evitar ambos problemas, puede replicar una configuración de alias de ruta en el archivo tsconfig.json
. Si no usa TypeScript, puede hacer lo mismo en jsconfig.json
.
// tsconfig.json OR jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
Desde la versión 2021.3 (probé en 2022.3.4), WebStorm admite el campo imports
. Esta característica funciona independientemente de la versión de TypeScript, ya que WebStorm usa su propio analizador de código. Sin embargo, WebStorm tiene un conjunto separado de problemas con respecto a los alias de ruta compatibles:
El editor sigue estrictamente las restricciones impuestas por Node.js sobre el uso de alias de ruta. La navegación de código no funcionará si la extensión del archivo no se especifica explícitamente. Lo mismo se aplica a la importación de directorios con un archivo index.js
.
WebStorm tiene un error que impide el uso de una matriz de rutas dentro del campo imports
. En este caso, la navegación por código deja de funcionar por completo.
{ "name": "my-awesome-project", // OK "imports": { "#*": "./src/*" }, // This breaks code navigation "imports": { "#*": ["./src/*", "./src/*.ts", "./src/*.tsx"] } }
Por suerte, podemos usar el mismo truco que resuelve todos los problemas en VS Code. Específicamente, podemos replicar una configuración de alias de ruta en el archivo tsconfig.json
o jsconfig.json
. Esto permite el uso de alias de ruta sin ninguna limitación.
Según mis experimentos y mi experiencia con el campo imports
en varios proyectos, identifiqué las mejores configuraciones de alias de ruta para diferentes tipos de proyectos.
Esta configuración está pensada para proyectos en los que el código fuente se ejecuta en Node.js sin necesidad de pasos de compilación adicionales. Para usarlo, sigue estos pasos:
Configure el campo imports
en un archivo package.json
. Una configuración muy básica es suficiente en este caso.
Para que la navegación de código funcione en los editores de código, es necesario configurar alias de ruta en un archivo jsconfig.json
.
// jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
Esta configuración debe usarse para proyectos donde el código fuente está escrito en TypeScript y construido usando el compilador tsc
. Es importante configurar lo siguiente en esta configuración:
El campo imports
en un archivo package.json
. En este caso, es necesario agregar alias de ruta condicional para garantizar que Node.js resuelva correctamente el código compilado.
Es necesario habilitar el formato de paquete ESM en un archivo package.json
porque TypeScript solo puede compilar código en formato ESM cuando se usa el campo imports
.
En un archivo tsconfig.json
, configure el formato del módulo ESM y moduleResolution
. Esto permitirá que TypeScript sugiera extensiones de archivo olvidadas en las importaciones. Si no se especifica una extensión de archivo, el código no se ejecutará en Node.js después de la compilación.
Para corregir la navegación de código en los editores de código, los alias de ruta deben repetirse en un archivo tsconfig.json
.
// tsconfig.json { "compilerOptions": { "module": "esnext", "moduleResolution": "nodenext", "baseUrl": "./", "paths": { "#*": ["./src/*"] }, "outDir": "./build" } } // package.json { "name": "my-awesome-project", "type": "module", "imports": { "#*": { "default": "./src/*", "production": "./build/*" } } }
Esta configuración está pensada para proyectos en los que se incluye el código fuente. TypeScript no es necesario en este caso. Si no está presente, todas las configuraciones se pueden establecer en un archivo jsconfig.json
.
La característica principal de esta configuración es que le permite eludir las limitaciones de Node.js con respecto a la especificación de extensiones de archivo en las importaciones.
Es importante configurar lo siguiente:
Configure el campo imports
en un archivo package.json
. En este caso, debe agregar una serie de rutas a cada alias. Esto permitirá que un empaquetador encuentre el módulo importado sin necesidad de especificar la extensión del archivo.
Para corregir la navegación de código en los editores de código, debe repetir los alias de ruta en un archivo tsconfig.json
o jsconfig.json
.
// tsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": [ "./src/*", "./src/*.ts", "./src/*.tsx", "./src/*.js", "./src/*.jsx", "./src/*/index.ts", "./src/*/index.tsx", "./src/*/index.js", "./src/*/index.jsx" ] } }
La configuración de alias de ruta a través del campo imports
tiene ventajas y desventajas en comparación con la configuración a través de bibliotecas de terceros. Aunque este enfoque está respaldado por herramientas de desarrollo comunes (a partir de abril de 2023), también tiene limitaciones.
Este método ofrece los siguientes beneficios:
package.json
).
Sin embargo, existen desventajas temporales que se eliminarán a medida que evolucionen las herramientas de desarrollo:
imports
. Para evitar estos problemas, puede usar el archivo jsconfig.json
. Sin embargo, esto conduce a la duplicación de la configuración del alias de ruta en dos archivos.
imports
listo para usar. Por ejemplo, Rollup requiere la instalación de complementos adicionales.
imports
en Node.js agrega nuevas restricciones en las rutas de importación. Estas restricciones son las mismas que para los módulos ES, pero pueden dificultar el uso del campo imports
.
Entonces, ¿vale la pena usar el campo imports
para configurar alias de ruta? Creo que para nuevos proyectos, sí, vale la pena usar este método en lugar de bibliotecas de terceros.
El campo imports
tiene buenas posibilidades de convertirse en una forma estándar de configurar alias de ruta para muchos desarrolladores en los próximos años, ya que ofrece ventajas significativas en comparación con los métodos de configuración tradicionales.
Sin embargo, si ya tiene un proyecto con alias de ruta configurados, cambiar al campo imports
no traerá beneficios significativos.
Espero que hayas aprendido algo nuevo de este artículo. ¡Gracias por leer!
También publicado aquí