Как настроить псевдонимы путей в проектах внешнего интерфейса нативным способом

Как настроить псевдонимы путей в проектах внешнего интерфейса нативным способом

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:

Чтобы использовать настроенный псевдоним, импорт можно записать следующим образом:

Начиная с этапа настройки, мы сталкиваемся с первым ограничением: записи в поле imports должны начинаться с символа #. Это гарантирует, что они отличаются от спецификаторов пакетов, таких как @.

Я считаю, что это ограничение полезно, потому что оно позволяет разработчикам быстро определить, когда псевдоним пути используется при импорте, и где можно найти конфигурации псевдонимов.

Чтобы добавить дополнительные псевдонимы путей для часто используемых модулей, поле imports можно изменить следующим образом:

Идеально было бы завершить статью фразой «все остальное будет работать из коробки». Однако в действительности, если вы планируете использовать поле 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 и приводит к двум новым требованиям, которые необходимо выполнить:

  1. Необходимо указать полный путь к файлу, включая расширение файла.

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 имеет следующие проблемы с псевдонимами путей:

  1. 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 есть отдельный набор проблем, связанных с поддержкой псевдонимов путей:

  1. Редактор строго соблюдает ограничения, налагаемые 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 без дополнительных шагов сборки. Чтобы использовать его, выполните следующие действия:

  1. Настройте поле imports в файле package.json. В этом случае достаточно самой простой конфигурации.

2. Чтобы навигация по коду работала в редакторах кода, необходимо настроить псевдонимы путей в файле jsconfig.json.

// jsconfig.json
{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "#*": ["./src/*"]
        }
    }
}

// package.json
{
    "name": "my-awesome-project",
    "imports": {
        "#*": "./src/*"
    }
}

Создание кода с использованием TypeScript

Эту конфигурацию следует использовать для проектов, исходный код которых написан на TypeScript и собран с помощью компилятора tsc. В этой конфигурации важно настроить следующее:

  1. Поле 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, касающиеся указания расширений файлов при импорте.

Важно настроить следующее:

  1. Настройте поле 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 не принесет значительных преимуществ.

Надеюсь, вы узнали что-то новое из этой статьи. Спасибо за прочтение!

Полезные ссылки

  1. RFC для реализации экспорта и импорта
  2. Набор тестов для лучшего понимания возможностей поля импорта
  3. Документация по полю импорта в Node.js
  4. Ограничения Node.js на пути импорта в модулях ES

Также опубликовано здесь< /эм>


Оригинал