Как настроить псевдонимы путей в проектах внешнего интерфейса нативным способом
5 мая 2023 г.О псевдонимах пути
Проекты часто превращаются в сложные вложенные структуры каталогов. В результате пути импорта могут стать длиннее и запутаннее, что может негативно сказаться на внешнем виде кода и затруднить понимание источника импортируемого кода.
Использование псевдонимов пути может решить проблему, разрешив определение импорта, относящегося к предопределенным каталогам. Такой подход не только решает проблемы с пониманием путей импорта, но и упрощает процесс перемещения кода при рефакторинге.
// 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';
Существует несколько библиотек для настройки псевдонимов путей в Node.js, таких как alias-hq и tsconfig-paths. Однако, просматривая документацию Node.js, я обнаружил способ настроить псевдонимы путей, не полагаясь на сторонние библиотеки.
Более того, этот подход позволяет использовать псевдонимы без этапа сборки.
В этой статье мы обсудим Импорт подпутей Node.js и как с его помощью настроить псевдонимы пути. Мы также изучим их поддержку в экосистеме внешнего интерфейса.
Поле импорта
Начиная с Node.js версии 12.19.0, разработчики могут использовать импорт подпути для объявления псевдонимов пути внутри npm-пакет. Это можно сделать с помощью поля imports в файле package.json. Пакет не обязательно публиковать на npm. 
Достаточно создать файл package.json в любом каталоге. Следовательно, этот метод подходит и для частных проектов.
:::подсказка
Вот интересный факт: Node.js представил поддержку поля imports еще в 2020 году через RFC под названием «my-awesome-project
├── src/
│   ├── entities/
│   │    └── product/
│   │        └── components/
│   │            └── ProductView.js
│   ├── features/
│   │    └── add-to-cart/
│   │        └── actions/
│   │            └── index.js
│   └── shared/
│       └── api/
│            └── index.js
└── package.json
Чтобы настроить псевдонимы пути, вы можете добавить несколько строк в package.json, как описано в документации. Например, если вы хотите разрешить импорт относительно каталога src, добавьте следующее поле imports в package.json:
{
      "name": "my-awesome-project",
      "imports": {
            "#*": "./src/*"
      }
}
Чтобы использовать настроенный псевдоним, импорт можно записать следующим образом:
import { apiClient } from '#shared/api';
import { ProductView } from '#entities/product/components/ProductView';
import { addProductToCart } from '#features/add-to-cart/actions';
Начиная с этапа настройки, мы сталкиваемся с первым ограничением: записи в поле imports должны начинаться с символа #. Это гарантирует, что они отличаются от спецификаторов пакетов, таких как @. 
Я считаю, что это ограничение полезно, потому что оно позволяет разработчикам быстро определить, когда псевдоним пути используется при импорте, и где можно найти конфигурации псевдонимов.
Чтобы добавить дополнительные псевдонимы путей для часто используемых модулей, поле imports можно изменить следующим образом:
{
    "name": "my-awesome-project",
    "imports": {
        "#modules/*": "./path/to/modules/*",
        "#logger": "./src/shared/lib/logger.js",
        "#*": "./src/*"
    }
}
Идеально было бы завершить статью фразой «все остальное будет работать из коробки». Однако в действительности, если вы планируете использовать поле imports, вы можете столкнуться с некоторыми трудностями.
Ограничения Node.js
Если вы планируете использовать псевдонимы путей с модулями CommonJS, у меня для вас плохие новости: следующий код не будет работа.
const { apiClient } = require('#shared/api');
const { ProductView } = require('#entities/product/components/ProductView');
const { addProductToCart } = require('#features/add-to-cart/actions');
При использовании псевдонимов путей в Node.js вы должны следовать правилам разрешения модулей из мира ESM. Это относится как к модулям ES, так и к модулям CommonJS и приводит к двум новым требованиям, которые необходимо выполнить:
- Необходимо указать полный путь к файлу, включая расширение файла.
 
2. Нельзя указывать путь к каталогу и ожидать импорт файла index.js. Вместо этого необходимо указать полный путь к файлу index.js.
Чтобы Node.js правильно разрешал модули, необходимо исправить импорт следующим образом:
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');
Эти ограничения могут привести к проблемам при настройке поля imports в проекте с большим количеством модулей CommonJS. Однако, если вы уже используете модули ES, ваш код соответствует всем требованиям. 
Кроме того, если вы создаете код с помощью сборщика, вы можете обойти эти ограничения. Ниже мы обсудим, как это сделать.
Поддержка импорта вложенных путей в TypeScript
Чтобы правильно разрешать импортированные модули для проверки типов, TypeScript должен поддерживать поле imports. Эта функция поддерживается, начиная с версии 4.8.1, но только при соблюдении перечисленных выше ограничений Node.js. 
Чтобы использовать поле imports для разрешения модуля, необходимо настроить несколько параметров в файле 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"
    }
}
Эта конфигурация позволяет полю imports работать так же, как в Node.js. Это означает, что если вы забудете включить расширение файла в импорт модуля, TypeScript сгенерирует сообщение об ошибке, предупреждающее вас об этом.
// 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';
Я не хотел переписывать весь импорт, так как в большинстве моих проектов для сборки кода используется сборщик, и я никогда не добавляю расширения файлов при импорте модулей. Чтобы обойти это ограничение, я нашел способ настроить проект следующим образом:
{
    "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"
        ]
    }
}
Эта конфигурация позволяет импортировать модули обычным способом без указания расширений. Это работает даже тогда, когда путь импорта указывает на каталог.
// 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';
У нас осталась одна проблема, связанная с импортом с использованием относительного пути. Эта проблема не связана с псевдонимами путей. TypeScript выдает ошибку, потому что мы настроили разрешение модуля для использования режима nodenext. 
К счастью, в недавнем выпуске TypeScript 5.0 был добавлен новый режим разрешения модуля. это устраняет необходимость указывать полный путь внутри импорта. Чтобы включить этот режим, необходимо настроить несколько параметров в файле 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"
    }
}
После завершения настройки импорт относительных путей будет работать как обычно.
// 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';
Теперь мы можем полностью использовать псевдонимы путей через поле imports без каких-либо дополнительных ограничений на то, как писать пути импорта.
Создание кода с помощью TypeScript
При сборке исходного кода с помощью компилятора tsc может потребоваться дополнительная настройка. Одним из ограничений TypeScript является то, что код нельзя построить в формате модуля CommonJS при использовании поля imports. 
Следовательно, код должен быть скомпилирован в формате ESM, а поле type должно быть добавлено в package.json для запуска скомпилированного кода в Node.js.
{
    "name": "my-awesome-project",
    "type": "module",
    "imports": {
        "#*": "./src/*"
    }
}
Если ваш код скомпилирован в отдельный каталог, например build/, Node.js может не найти модуль, поскольку псевдоним пути будет указывать на исходное местоположение, например src/ код>. Чтобы решить эту проблему, можно использовать условные пути импорта в файле package.json. 
Это позволяет импортировать уже созданный код из каталога build/ вместо каталога src/.
{
    "name": "my-awesome-project",
    "type": "module",
    "imports": {
        "#*": {
            "default": "./src/*",
            "production": "./build/*"
        }
    }
}
Чтобы использовать определенное условие импорта, Node.js следует запускать с флагом --conditions.
node --conditions=production build/index.js
Поддержка импорта вложенных путей в пакетах кода
Сборщики кода обычно используют собственную реализацию разрешения модулей, а не встроенную в Node.js. Поэтому для них важно реализовать поддержку поля imports. 
Я протестировал псевдонимы путей с помощью Webpack, Rollup и Vite в своих проектах и готов поделиться своими выводами.
Вот конфигурация псевдонима пути, которую я использовал для тестирования упаковщиков. Я использовал тот же трюк, что и для TypeScript, чтобы не указывать полный путь к файлам внутри импорта.
{
    "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 поддерживает начало поля imports начиная с версии 5.0. Псевдонимы пути работают без дополнительной настройки. Вот конфигурация Webpack, которую я использовал для создания тестового проекта с 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;
Посетить
Поддержка поля imports была добавлена в Vite версии 4.2.0. Однако в версии 4.3.3 была исправлена важная ошибка, поэтому рекомендуется использовать как минимум эту версию. В Vite псевдонимы путей работают без необходимости дополнительной настройки как в режимах dev, так и в режимах build. 
Поэтому я собрал тестовый проект с абсолютно пустой конфигурацией.
Сводка
Несмотря на то, что Rollup используется внутри Vite, поле imports не работает по умолчанию. Чтобы включить его, вам необходимо установить подключаемый модуль @rollup/plugin-node-resolve версии 11.1.0 или выше. Вот пример конфигурации:
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'],
            }),
        ],
    },
];
К сожалению, в этой конфигурации псевдонимы путей работают только в рамках ограничений Node.js. Это означает, что вы должны указать полный путь к файлу, включая расширение. Указание массива в поле imports не обойдет это ограничение, так как Rollup использует только первый путь в массиве.
Я считаю, что эту проблему можно решить с помощью плагинов Rollup, но я не пробовал этого делать, потому что в основном использую Rollup для небольших библиотек. В моем случае было проще переписать пути импорта по всему проекту.
Поддержка импорта вложенных путей в средствах запуска тестов
Средства запуска тестов — это еще одна группа инструментов разработки, которые сильно зависят от механизма разрешения модулей. Они часто используют собственную реализацию разрешения модулей, аналогичную сборщикам кода. В результате есть вероятность, что поле imports может работать не так, как ожидалось.
К счастью, инструменты, которые я тестировал, работают хорошо. Я тестировал псевдонимы путей с помощью Jest v29.5.0 и Vite v0.30.1. В обоих случаях псевдонимы пути работали без каких-либо дополнительных настроек или ограничений. Jest поддерживает поле imports, начиная с версии v29.4.0. . 
Уровень поддержки в Vitest зависит исключительно от версии Vite, которая должна быть не ниже 4.2.0.
Поддержка импорта вложенных путей в редакторах кода
Поле imports в популярных библиотеках в настоящее время хорошо поддерживается. Однако как насчет редакторов кода? Я протестировал навигацию по коду, в частности функцию «Перейти к определению», в проекте, в котором используются псевдонимы путей. Оказывается, поддержка этой функции в редакторах кода имеет некоторые проблемы.
Код VS
Когда дело доходит до VS Code, версия TypeScript имеет решающее значение. Языковой сервер TypeScript отвечает за анализ и навигацию по коду JavaScript и TypeScript.
В зависимости от ваших настроек VS Code будет использовать либо встроенную версию TypeScript, либо версию, установленную в вашем проекте.
Я протестировал поддержку поля imports в VS Code v1.77.3 в сочетании с TypeScript v5.0.4.
VS Code имеет следующие проблемы с псевдонимами путей:
- TypeScript не использует поле 
imports, пока для параметра разрешения модуля не установлено значениеnodenextилиbundler. Поэтому, чтобы использовать его в VS Code, вам нужно указать разрешение модуля в вашем проекте. 
2. В настоящее время IntelliSense не поддерживает предложение путей импорта с использованием поля imports. Для этой проблемы существует открытая проблема.
Чтобы обойти обе проблемы, вы можете реплицировать конфигурацию псевдонима пути в файле tsconfig.json. Если вы не используете TypeScript, вы можете сделать то же самое в jsconfig.json.
// tsconfig.json OR jsconfig.json
{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "#*": ["./src/*"]
        }
    }
}
// package.json
{
    "name": "my-awesome-project",
    "imports": {
        "#*": "./src/*"
    }
}
Вебсторм
Начиная с версии 2021.3 (я тестировал версию 2022.3.4), WebStorm  поддерживает поле imports. Эта функция работает независимо от версии TypeScript, поскольку WebStorm использует собственный анализатор кода. Однако у WebStorm есть отдельный набор проблем, связанных с поддержкой псевдонимов путей:
- Редактор строго соблюдает ограничения, налагаемые Node.js на использование псевдонимов путей. Навигация по коду не будет работать, если расширение файла не указано явно. То же самое относится к импорту каталогов с файлом 
index.js. 
2. В WebStorm есть ошибка, не позволяющая использовать массив путей в поле imports. В этом случае навигация по коду перестает работать полностью.
{
    "name": "my-awesome-project",
    // OK
    "imports": {
        "#*": "./src/*"
    },
    // This breaks code navigation
    "imports": {
        "#*": ["./src/*", "./src/*.ts", "./src/*.tsx"]
    }
}
К счастью, мы можем использовать тот же трюк, который решает все проблемы в VS Code. В частности, мы можем реплицировать конфигурацию псевдонима пути в файле tsconfig.json или jsconfig.json. Это позволяет использовать псевдонимы путей без каких-либо ограничений.
Рекомендуемая конфигурация
Основываясь на своих экспериментах и опыте использования поля imports в различных проектах, я определил лучшие конфигурации псевдонимов пути для различных типов проектов.
Без TypeScript или Bundler
Эта конфигурация предназначена для проектов, в которых исходный код выполняется в Node.js без дополнительных шагов сборки. Чтобы использовать его, выполните следующие действия:
- Настройте поле 
importsв файлеpackage.json. В этом случае достаточно самой простой конфигурации. 
2. Чтобы навигация по коду работала в редакторах кода, необходимо настроить псевдонимы путей в файле jsconfig.json.
// jsconfig.json
{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "#*": ["./src/*"]
        }
    }
}
// package.json
{
    "name": "my-awesome-project",
    "imports": {
        "#*": "./src/*"
    }
}
Создание кода с использованием TypeScript
Эту конфигурацию следует использовать для проектов, исходный код которых написан на TypeScript и собран с помощью компилятора tsc. В этой конфигурации важно настроить следующее:
- Поле 
importsв файлеpackage.json. В этом случае необходимо добавить псевдонимы условного пути, чтобы убедиться, что Node.js правильно разрешает скомпилированный код. 
2. Включение формата пакета ESM в файле package.json необходимо, поскольку TypeScript может компилировать код в формате ESM только при использовании поля imports.
3. В файле tsconfig.json задайте формат модуля ESM и moduleResolution. Это позволит TypeScript предлагать забытые расширения файлов при импорте. Если расширение файла не указано, код не будет работать в Node.js после компиляции.
4. Чтобы исправить навигацию по коду в редакторах кода, псевдонимы путей должны повторяться в файле 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/*"
        }
    }
}
Создание кода с помощью сборщика
Эта конфигурация предназначена для проектов, исходный код которых входит в комплект. В этом случае TypeScript не требуется. Если его нет, все настройки можно задать в файле jsconfig.json. 
Основная особенность этой конфигурации заключается в том, что она позволяет обойти ограничения Node.js, касающиеся указания расширений файлов при импорте.
Важно настроить следующее:
- Настройте поле 
importsв файлеpackage.json. В этом случае вам нужно добавить массив путей к каждому псевдониму. Это позволит сборщику найти импортированный модуль, не требуя указания расширения файла. 
2. Чтобы исправить навигацию по коду в редакторах кода, необходимо повторить псевдонимы путей в файле tsconfig.json или 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"
        ]
    }
}
Заключение
Настройка псевдонимов пути через поле imports имеет как плюсы, так и минусы по сравнению с настройкой с помощью сторонних библиотек. Хотя этот подход поддерживается распространенными инструментами разработки (по состоянию на апрель 2023 г.), он также имеет ограничения.
Этот метод предлагает следующие преимущества:
- Возможность использовать псевдонимы путей без необходимости компилировать или транспилировать код "на лету".
 
* Большинство популярных средств разработки поддерживают псевдонимы путей без какой-либо дополнительной настройки. Это было подтверждено в Webpack, Vite, Jest и Vitest.
* Этот подход способствует настройке псевдонимов путей в одном предсказуемом месте (файл package.json).
* Настройка псевдонимов пути не требует установки сторонних библиотек.
Однако существуют временные недостатки, которые будут устранены по мере развития средств разработки:
* Даже популярные редакторы кода имеют проблемы с поддержкой поля imports. Чтобы избежать этих проблем, вы можете использовать файл jsconfig.json. Однако это приводит к дублированию конфигурации псевдонима пути в двух файлах.
* Некоторые средства разработки могут не работать с полем imports из коробки. Например, для Rollup требуется установка дополнительных подключаемых модулей.
* Использование поля imports в Node.js добавляет новые ограничения на пути импорта. Эти ограничения такие же, как и для модулей ES, но они могут затруднить начало использования поля imports.
* Ограничения Node.js могут привести к различиям в реализации между Node.js и другими инструментами разработки. Например, сборщики кода могут игнорировать ограничения Node.js. Эти различия могут иногда усложнять настройку, особенно при настройке TypeScript.
Итак, стоит ли использовать поле imports для настройки псевдонимов пути? Я считаю, что для новых проектов да, этот метод стоит использовать вместо сторонних библиотек. 
Поле imports имеет хорошие шансы стать стандартным способом настройки псевдонимов путей для многих разработчиков в ближайшие годы, поскольку оно предлагает значительные преимущества по сравнению с традиционными методами настройки. 
Однако если у вас уже есть проект с настроенными псевдонимами путей, переключение на поле imports не принесет значительных преимуществ.
Надеюсь, вы узнали что-то новое из этой статьи. Спасибо за прочтение!
Полезные ссылки
- RFC для реализации экспорта и импорта
 - Набор тестов для лучшего понимания возможностей поля импорта
 - Документация по полю импорта в Node.js
 - Ограничения Node.js на пути импорта в модулях ES
 
Также опубликовано здесь< /эм>
Оригинал