Table of Contents #
> Для лучшего понимания данного раздела документации необходимо знание основ работы с модулями. См. modules для получения более подробной информации.
Разрешение модулей (Module resolution) — это используемый компилятором процесс выяснения того, на что ссылается команда импорта. Рассмотрим инструкцию следующего вида: import { a } from "moduleA"
. Чтобы проверить корректность использования a
, компилятор должен точно знать, что представляет из себя этот элемент, для чего необходимо проверить соответствующее определение - moduleA
.
На данном этапе компилятор должен узнать, какова форма moduleA
. Пока всё кажется просто, но moduleA
может быть определён в одном из файлов .ts
/.tsx
или .d.ts
.
Сначала компилятор попытается найти файл, представляющий импортируемый модуль. Для этого он должен выбрать одну из двух стратегий: Classic или Node. С помощью этих стратегий компилятор определяет, где искать moduleA
.
Если найти файл не удалось, и имя модуля не относительное (как в случае "moduleA"
), тогда компилятор попытается найти объявление внешнего модуля (ambient module declaration). Неотносительный импорт (non-relative imports) описан далее.
В итоге, если компилятор не смог разрешить модуль, он выведет ошибку вида error TS2307: Cannot find module 'moduleA'.
Относительный и неотносительный импорт модулей
Импорт модуля разрешается разными способами в зависимости от того, является ли ссылка относительной или неотносительной.
Относительный импорт начинается с /
, ./
или ../
. Примеры:
import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";
Любой другой импорт считается неотносительным. Примеры:
import * as $ from "jQuery";
import { Component } from "angular2/core";
Относительный импорт разрешается относительно импортируемого файла и не может разрешиться объявлением внешнего модуля. Относительный импорт лучше использовать для своих модулей, которые во время выполнения программы гарантированно находятся в указанном месте.
Неотносительный импорт может быть разрешен относительно baseUrl
или с помощью сопоставления путей, которое будет описано ниже. Он также может разрешаться объявлениями внешних модулей. Используйте неотносительные пути при импорте любых внешних зависимостей.
Стратегии разрешения модулей
Существует две стратегии разрешения модулей: Node и Classic. Для указания выбранной стратегии вы можете использовать флаг --moduleResolution
. По умолчанию используется стратегия Node.
Classic
Эта стратегия раньше была принята в TypeScript's по умолчанию. Но теперь она сохранена лишь для обратной совместимости.
Относительный импорт будет разрешен относительно импортируемого файла. Таким образом, import { b } from "./moduleB"
в исходном файле /root/src/folder/A.ts
приведет к поиску следующих файлов:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
При неотносительном импорте модулей, компилятор, пытаясь найти подходящий файл определений, пройдет по дереву каталогов, начиная с директории, содержащей импортирующий файл.
Например:
Неотносительный импорт из moduleB
, такой как import { b } from "moduleB"
, расположенный в файле с исходным кодом /root/src/folder/A.ts
, приведет к поиску "moduleB"
в следующих местах:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
Node
Эта стратегия копирует поведение работающего динамически механизма разрешения модулей Node.js. См. полное описание алгоритма разрешения Node.js в документации по модулям Node.js.
Как Node.js разрешает модули
Чтобы понять, каким путем пойдет компилятор TS, важно немного разобраться в модулях Node.js. Импорт в Node.js выполняется с помощью вызова функции require
. Node.js будет действовать по-разному в зависимости от того, указан ли в require
относительный или неотносительный путь.
Использование относительных путей обычно не вызывает затруднений. Для примера давайте рассмотрим файл /root/src/moduleA.js
, в котором есть следующая инструкция иморта var x = require("./moduleB");
Node.js разрешает этот импорт в таком порядке:
-
Как файл с именем
/root/src/moduleB.js
, если он существует. -
Как каталог
/root/src/moduleB
, если в нём есть файлpackage.json
, который определяет модуль"main"
. В нашем примере, если Node.js нашла файл/root/src/moduleB/package.json
, содержащий{ "main": "lib/mainModule.js" }
, тогда она сошлётся на/root/src/moduleB/lib/mainModule.js
. -
Если каталог
/root/src/moduleB
содержит файл с именемindex.js
, по умолчанию считается, что он является main-модулем данного каталога.
Вы можете найти дополнительную информацию в документации по Node.js: file modules и folder modules.
Однако, разрешение неотносительных имен модулей выполняется иным способом. Node будет искать ваши модули в специальном каталоге, называемом node_modules
. Он может быть на том же уровне иерархии каталогов, что и текущий файл, или выше. Node пойдет вверх по цепочке каталогов, просматривая каждый node_modules
, пока не найдет модуль, который вы пытались загрузить.
Продолжая рассматривать наш пример, предположим, что в /root/src/moduleA.js
использовался неотносительный путь, и команда импорта выглядела следующим образом: var x = require("moduleB");
. Node попытается разрешить moduleB
в один из следующих путей и остановится на первом подходящем.
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json
(если он определяет свойство"main"
)/root/src/node_modules/moduleB/index.js
/root/node_modules/moduleB.js
/root/node_modules/moduleB/package.json
(если он определяет свойство"main"
)/root/node_modules/moduleB/index.js
/node_modules/moduleB.js
/node_modules/moduleB/package.json
(если он определяет свойство"main"
)/node_modules/moduleB/index.js
Заметьте, что Node.js поднялась на один уровень на шагах (4) и (7).
Вы можете найти дополнительную информацию в документации по Node.js в разделе загрузка модулей из node_modules
.
Как TypeScript разрешает модули
TypeScript копирует стратегию динамического разрешения модулей в Node.js с целью поиска файлов с определениями модулей во время компиляции. С этой целью TypeScript применяет логику Node.js для работы с собственными типами файлов .ts
, .tsx
и .d.ts
. TypeScript также использует поле "typings"
в package.json
, чтобы отразить назначение "main"
- указание компилятору, где находится "основной" файл определений ("main" definition file).
Например, команда импорта import { b } from "./moduleB"
в /root/src/moduleA.ts
приведёт к поиску "./moduleB"
в следующих местах:
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json
(если он определяет свойство"typings"
)/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
Напомним, что Node.js пыталась найти файл moduleB.js
, затем подходящий package.json
, а после index.js
.
Неотносительный импорт будет следовать логике разрешения модулей Node.js, сначала пытаясь найти файл, а затем подходящую директорию. Таким образом, import { b } from "moduleB"
в файле с исходным кодом /src/moduleA.ts
приведёт к поиску в следующих местах:
/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json
(если он определяет свойство"typings"
)/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts
/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json
(если он определяет свойство"typings"
)/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts
/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json
(если он определяет свойство"typings"
)/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts
Не пугайтесь большого количества пунктов - TypeScript также перешёл на уровень вверх лишь дважды: на шагах (8) и (15). На самом деле это не сложнее того, что делает Node.js.
Дополнительные флаги системы разрешения модулей
Исходная структура проекта не всегда соответствует тому, что получается на выходе. Обычно для достижения результата нужно несколько шагов. Это и компиляция файлов .ts
в .js
, и копирование зависимостей из различных источников в один выходной файл. В итоге получается, что модули в процессе выполнения могут иметь имена, отличные от имен исходных файлов с их определениями. Пути модулей в итоговом выводе также могут отличаться от соответствующих первоначальных путей на этапе компиляции.
В TypeScript есть набор дополнительных флагов, с помощью которых можно сообщить компилятору о тех трансформациях, которые должны произойти с исходниками, чтобы сгенерировать итоговый вывод.
Важно отметить, что компилятор не будет выполнять эти трансформации. Он лишь использует полученную информацию, чтобы выполнить процесс разрешения импорта модуля в его файл определения.
Base URL
baseUrl
часто используется в приложениях, использующих загрузчик модулей AMD, где модули динамически "разворачиваются" в одном каталоге. Исходные файлы этих модулей могут находиться в разных местах, но скрипт сборки поместит их все в одну директорию.
Установка baseUrl
сообщает компилятору о том, где искать модули. Все команды импорта модулей с неотносительными именами считаются относительными baseUrl
.
Значение baseUrl определяется как одно из:
- значение аргумента командной строки baseUrl (если передан относительный путь, он рассчитывается относительно текущей директории)
- значение свойства baseUrl в 'tsconfig.json' (если передан относительный путь, он рассчитывается на основе расположения 'tsconfig.json')
Заметьте, что установка baseUrl не влияет на команды относительного импорта модулей, так как они всегда разрешаются относительно импортирующих файлов.
См. дополнительную информацию о baseUrl в документации по RequireJS and SystemJS.
Сопоставление путей
Иногда модули не находятся прямо под baseUrl. Например, команда импорта модуля "jquery"
во время выполнения будет преобразована к "node_modules\jquery\dist\jquery.slim.min.js"
. Загрузчики используют конфигурацию сопоставления путей, чтобы динамически установить соответствие имен модулей и соответствующих файлов, см. документацию по RequireJs и SystemJS.
Компилятор TypeScript поддерживает объявление подобных сопоставлений в свойстве "paths"
файла tsconfig.json
. Вот пример того, как можно указать свойство "paths"
для jquery
.
{ "compilerOptions": { "paths": { "jquery": ["node_modules/jquery/dist/jquery.d.ts"] } }
Свойство "paths"
позволяет использовать более сложные методы сопоставления, включая множественные резервные пути. Давайте рассмотрим конфигурацию, в которой в одном расположении доступны лишь некоторые модули, оставшиеся же находятся в другом. При сборке все эти модули будут помещены в одно место. Схема проекта может выглядеть следующим образом:
projectRoot ├── folder1 │ ├── file1.ts (импортирует 'folder1/file2' и 'folder2/file3') │ └── file2.ts ├── generated │ ├── folder1 │ └── folder2 │ └── file3.ts └── tsconfig.json
Соответствующий tsconfig.json
будет выглядеть следующим образом:
{ "compilerOptions": { "baseUrl": ".", "paths": { "*": [ "*", "generated/*" ] } } }
Таким образом мы сообщаем компилятору, что для каждого модуля, инструкция импорта которого соответствует шаблону "*"
(то есть любые значения), он должен выполнить поиск в двух местах:
"*"
: означающее то же самое имя без изменений, поэтому сопоставляем<moduleName>
=><baseUrl>\<moduleName>
"generated\*"
означающее имя модуля с добавленным префиксом "generated", поэтому сопоставляем<moduleName>
=><baseUrl>\generated\<moduleName>
Следуя этой логике, компилятор попытается разрешить указанные инструкции импорта следующим образом:
- import 'folder1/file2'
- есть соответствие шаблону '*', под который подпадает имя модуля целиком;
- пробуем первую замену по списку: '*' ->
folder1/file2
; - результатом замены является относительное имя, соединяем его с baseUrl ->
projectRoot/folder1/file2.ts
; - Файл существует. Готово.
- import 'folder2/file3'
- есть соответствие шаблону '*', под который подпадает имя модуля целиком;
- пробуем первую замену по списку: '*' ->
folder2/file3
- результатом замены является относительное имя, соединяем его с baseUrl ->
projectRoot/folder2/file3.ts
. - Файл не существует, переходим к следующей замене
- вторая замена 'generated/*' ->
generated/folder2/file3
- результатом замены является относительное имя, соединяем его с baseUrl ->
projectRoot/generated/folder2/file3.ts
. - Файл существует. Готово.
Виртуальные каталоги с rootDirs
Исходные файлы проекта, находящиеся в разных каталогах, иногда объединяются на этапе компиляции, чтобы сгенерировать единственный выходной каталог. Это можно рассматривать как создание из набора исходных каталогов одного "виртуального" каталога.
Используя 'rootDirs', можно сообщить компилятору о корневых каталогах (roots), составляющих этот "виртуальный" каталог, давая возможность компилятору разрешить команды относительного импорта модулей в пределах этих "виртуальных" каталогов, как если бы они были объединены в один каталог.
Для примера давайте рассмотрим следующую структуру проекта:
src └── views └── view1.ts (импортирует './template1') └── view2.ts generated └── templates └── views └── template1.ts (импортирует './view2')
В src/views
находятся файлы с пользовательским кодом для элементов UI. Файлы в generated/templates
содержат код связывания шаблонов пользовательского интерфейса, автоматически сгенерированный генератором шаблонов как часть сборки. На одном из шагов сборки файлы из /src/views
и /generated/templates/views
будут скопированы в такие же директории в выходной структуре проекта. Представление (view) во время выполнения программы ожидает, что её шаблон находится рядом, и его можно импортировать с помощью относительного пути "./template"
.
Чтобы указать компилятору на эту связь, используйте "rootDirs"
. "rootDirs"
определяет список корневых директорий (roots), чьё содержимое необходимо объединить динамически. Продолжая наш пример, файл tsconfig.json
должен выглядеть следующим образом:
{ "compilerOptions": { "rootDirs": [ "src/views", "generated/templates/views" ] } }
Каждый раз, когда компилятор встречает относительный импорт модуля в подкаталоге одного из rootDirs
, он пытается найти этот импорт в записях rootDirs
.
Отслеживание разрешения модулей
Как упоминалось ранее, компилятор имеет возможность выходить за пределы текущей директории при разрешении модулей. Такое поведение может затруднять диагностику причин, по которым модуль не был разрешен или был разрешен неверно. Чтобы получить представление о том, как проходит процесс разрешения модулей, можно воспользоваться ключом компилятора --traceResolution
.
Предположим, что у нас есть простое приложение, использующее модуль typescript
. В app.ts
находится инструкция импорта import * as ts from "typescript"
.
│ tsconfig.json ├───node_modules │ └───typescript │ └───lib │ typescript.d.ts └───src app.ts
Вызываем компилятор с опцией --traceResolution
tsc --traceResolution
Результаты вывода:
======== Resolving module 'typescript' from 'src/app.ts'. ======== Module resolution kind is not specified, using 'NodeJs'. Loading module 'typescript' from 'node_modules' folder. File 'src/node_modules/typescript.ts' does not exist. File 'src/node_modules/typescript.tsx' does not exist. File 'src/node_modules/typescript.d.ts' does not exist. File 'src/node_modules/typescript/package.json' does not exist. File 'node_modules/typescript.ts' does not exist. File 'node_modules/typescript.tsx' does not exist. File 'node_modules/typescript.d.ts' does not exist. Found 'package.json' at 'node_modules/typescript/package.json'. 'package.json' has 'typings' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'. File 'node_modules/typescript/lib/typescript.d.ts' exist - use it as a module resolution result. ======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========
Что искать в трассировке
- Имя и расположение инструкции импорта
======== Resolving module 'typescript' from 'src/app.ts'. ========
- Стратегию, которой придерживается компилятор
Module resolution kind is not specified, using 'NodeJs'.
- Загрузку объявлений типов (typings) из npm-пакетов
'package.json' has 'typings' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
- Конечный результат
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========
Использование --noResolve
Обычно компилятор пытается разрешить все инструкции импорта модулей до начала процесса компиляции. Каждый раз, когда он успешно разрешает import
в файл, этот файл добавляется в набор файлов, который компилятор обработает позже.
Опция --noResolve
говорит компилятору не "добавлять" в компиляцию файлы, которые не были явно указаны в командной строке. Компилятор всё равно попытается разрешить модули в файлы, но не включит в сборку те, которые не были явно указаны.
Например:
app.ts
import * as A from "moduleA" // OK, 'moduleA' был передан в командной строке import * as B from "moduleB" // Ошибка TS2307: Cannot find module 'moduleB'.
tsc app.ts moduleA.ts --noResolve
Компиляция app.ts
с использованием --noResolve
приведет к следующим результатам:
moduleA
будет успешно найдено, поскольку было передано в командной строке.- Поиск
moduleB
завершится ошибкой, так как его не было в командной строке.
Общие вопросы
Почему модуль, находящийся в списке исключенных, тем не менее используется компилятором?
tsconfig.json
преобразует каталог в “проект”. Без указания пунктов “exclude”
или “files”
в сборку включаются все файлы в каталоге, содержащем tsconfig.json
, а также в его подкаталогах. Для исключения некоторых файлов, используйте “exclude”
. Используйте “files”
, если удобнее явно указать все файлы, вместо того чтобы давать возможность компилятору искать их самостоятельно.
Здесь мы говорили об автоматическом включении с tsconfig.json
. Согласно обсуждавшемуся выше, это правило не охватывает разрешение модулей. Если компилятор определит, что какой-либо файл является целевым для импорта модуля, этот файл будет включен в сборку независимо от того, был ли он исключен на предыдущих шагах.
Таким образом, чтобы исключить файл из сборки, необходимо исключить его самого и все файлы, в которых есть команды import
или /// <reference path="..." />
, ссылающиеся на него.
Поддержите перевод документации:
Documentation generated by mdoc.