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'.

Относительный и неотносительный импорт модулей

Импорт модуля разрешается разными способами в зависимости от того, является ли ссылка относительной или неотносительной.

Относительный импорт начинается с /, ./ или ../. Примеры:

Любой другой импорт считается неотносительным. Примеры:

Относительный импорт разрешается относительно импортируемого файла и не может разрешиться объявлением внешнего модуля. Относительный импорт лучше использовать для своих модулей, которые во время выполнения программы гарантированно находятся в указанном месте.

Неотносительный импорт может быть разрешен относительно baseUrl или с помощью сопоставления путей, которое будет описано ниже. Он также может разрешаться объявлениями внешних модулей. Используйте неотносительные пути при импорте любых внешних зависимостей.

Стратегии разрешения модулей

Существует две стратегии разрешения модулей: Node и Classic. Для указания выбранной стратегии вы можете использовать флаг --moduleResolution. По умолчанию используется стратегия Node.

Classic

Эта стратегия раньше была принята в TypeScript's по умолчанию. Но теперь она сохранена лишь для обратной совместимости.

Относительный импорт будет разрешен относительно импортируемого файла. Таким образом, import { b } from "./moduleB" в исходном файле /root/src/folder/A.ts приведет к поиску следующих файлов:

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts

При неотносительном импорте модулей, компилятор, пытаясь найти подходящий файл определений, пройдет по дереву каталогов, начиная с директории, содержащей импортирующий файл.

Например:

Неотносительный импорт из moduleB, такой как import { b } from "moduleB", расположенный в файле с исходным кодом /root/src/folder/A.ts, приведет к поиску "moduleB" в следующих местах:

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts
  3. /root/src/moduleB.ts
  4. /root/src/moduleB.d.ts
  5. /root/moduleB.ts
  6. /root/moduleB.d.ts
  7. /moduleB.ts
  8. /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 разрешает этот импорт в таком порядке:

  1. Как файл с именем /root/src/moduleB.js, если он существует.

  2. Как каталог /root/src/moduleB, если в нём есть файл package.json, который определяет модуль "main". В нашем примере, если Node.js нашла файл /root/src/moduleB/package.json, содержащий { "main": "lib/mainModule.js" }, тогда она сошлётся на /root/src/moduleB/lib/mainModule.js.

  3. Если каталог /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 в один из следующих путей и остановится на первом подходящем.

  1. /root/src/node_modules/moduleB.js
  2. /root/src/node_modules/moduleB/package.json (если он определяет свойство "main")
  3. /root/src/node_modules/moduleB/index.js

  4. /root/node_modules/moduleB.js
  5. /root/node_modules/moduleB/package.json (если он определяет свойство "main")
  6. /root/node_modules/moduleB/index.js

  7. /node_modules/moduleB.js
  8. /node_modules/moduleB/package.json (если он определяет свойство "main")
  9. /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" в следующих местах:

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (если он определяет свойство "typings")
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

Напомним, что Node.js пыталась найти файл moduleB.js, затем подходящий package.json, а после index.js.

Неотносительный импорт будет следовать логике разрешения модулей Node.js, сначала пытаясь найти файл, а затем подходящую директорию. Таким образом, import { b } from "moduleB" в файле с исходным кодом /src/moduleA.ts приведёт к поиску в следующих местах:

  1. /root/src/node_modules/moduleB.ts
  2. /root/src/node_modules/moduleB.tsx
  3. /root/src/node_modules/moduleB.d.ts
  4. /root/src/node_modules/moduleB/package.json (если он определяет свойство "typings")
  5. /root/src/node_modules/moduleB/index.ts
  6. /root/src/node_modules/moduleB/index.tsx
  7. /root/src/node_modules/moduleB/index.d.ts

  8. /root/node_modules/moduleB.ts
  9. /root/node_modules/moduleB.tsx
  10. /root/node_modules/moduleB.d.ts
  11. /root/node_modules/moduleB/package.json (если он определяет свойство "typings")
  12. /root/node_modules/moduleB/index.ts
  13. /root/node_modules/moduleB/index.tsx
  14. /root/node_modules/moduleB/index.d.ts

  15. /node_modules/moduleB.ts
  16. /node_modules/moduleB.tsx
  17. /node_modules/moduleB.d.ts
  18. /node_modules/moduleB/package.json (если он определяет свойство "typings")
  19. /node_modules/moduleB/index.ts
  20. /node_modules/moduleB/index.tsx
  21. /node_modules/moduleB/index.d.ts

Не пугайтесь большого количества пунктов - TypeScript также перешёл на уровень вверх лишь дважды: на шагах (8) и (15). На самом деле это не сложнее того, что делает Node.js.

Дополнительные флаги системы разрешения модулей

Исходная структура проекта не всегда соответствует тому, что получается на выходе. Обычно для достижения результата нужно несколько шагов. Это и компиляция файлов .ts в .js, и копирование зависимостей из различных источников в один выходной файл. В итоге получается, что модули в процессе выполнения могут иметь имена, отличные от имен исходных файлов с их определениями. Пути модулей в итоговом выводе также могут отличаться от соответствующих первоначальных путей на этапе компиляции.

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

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

Base URL

baseUrl часто используется в приложениях, использующих загрузчик модулей AMD, где модули динамически "разворачиваются" в одном каталоге. Исходные файлы этих модулей могут находиться в разных местах, но скрипт сборки поместит их все в одну директорию.

Установка baseUrl сообщает компилятору о том, где искать модули. Все команды импорта модулей с неотносительными именами считаются относительными baseUrl.

Значение baseUrl определяется как одно из:

Заметьте, что установка 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/*"
                ]
            }
    }
}

Таким образом мы сообщаем компилятору, что для каждого модуля, инструкция импорта которого соответствует шаблону "*" (то есть любые значения), он должен выполнить поиск в двух местах:

  1. "*": означающее то же самое имя без изменений, поэтому сопоставляем <moduleName> => <baseUrl>\<moduleName>
  2. "generated\*" означающее имя модуля с добавленным префиксом "generated", поэтому сопоставляем <moduleName> => <baseUrl>\generated\<moduleName>

Следуя этой логике, компилятор попытается разрешить указанные инструкции импорта следующим образом:

  1. есть соответствие шаблону '*', под который подпадает имя модуля целиком;
  2. пробуем первую замену по списку: '*' -> folder1/file2;
  3. результатом замены является относительное имя, соединяем его с baseUrl -> projectRoot/folder1/file2.ts;
  4. Файл существует. Готово.
  1. есть соответствие шаблону '*', под который подпадает имя модуля целиком;
  2. пробуем первую замену по списку: '*' -> folder2/file3
  3. результатом замены является относительное имя, соединяем его с baseUrl -> projectRoot/folder2/file3.ts.
  4. Файл не существует, переходим к следующей замене
  5. вторая замена 'generated/*' -> generated/folder2/file3
  6. результатом замены является относительное имя, соединяем его с baseUrl -> projectRoot/generated/folder2/file3.ts.
  7. Файл существует. Готово.

Виртуальные каталоги с 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'.

'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 приведет к следующим результатам:

Общие вопросы

Почему модуль, находящийся в списке исключенных, тем не менее используется компилятором?

tsconfig.json преобразует каталог в “проект”. Без указания пунктов “exclude” или “files” в сборку включаются все файлы в каталоге, содержащем tsconfig.json, а также в его подкаталогах. Для исключения некоторых файлов, используйте “exclude”. Используйте “files”, если удобнее явно указать все файлы, вместо того чтобы давать возможность компилятору искать их самостоятельно.

Здесь мы говорили об автоматическом включении с tsconfig.json. Согласно обсуждавшемуся выше, это правило не охватывает разрешение модулей. Если компилятор определит, что какой-либо файл является целевым для импорта модуля, этот файл будет включен в сборку независимо от того, был ли он исключен на предыдущих шагах.

Таким образом, чтобы исключить файл из сборки, необходимо исключить его самого и все файлы, в которых есть команды import или /// <reference path="..." />, ссылающиеся на него.

Источник







Поддержите перевод документации:



Поддерживатель | Github Репозиторий


Documentation generated by mdoc.
Молния! Обновления, новости и статьи Typescript.