Трансформация динамического import()

Динамический import() в JavaScript представляет собой выражение, возвращающее Promise, который резолвится в модульное пространство (module namespace object). В отличие от статического import, эта форма выполняется во время исполнения и позволяет:

  • ленивую загрузку кода;
  • разбиение приложения на чанки;
  • условную загрузку модулей;
  • асинхронную инициализацию зависимостей.

Базовая форма:

const module = await import(&

Возвращаемый объект содержит все экспортированные сущности:

module.add(2, 3);
module.default;

Поведение динамического import() на уровне AST

SWC оперирует абстрактным синтаксическим деревом (AST) и рассматривает import() как CallExpression с Import в качестве callee-типа:

import('./module.js')

В AST:

  • type: CallExpression
  • callee: Import
  • arguments: [StringLiteral]

Ключевой момент: в отличие от require, это не синхронный вызов, а отдельная семантическая конструкция языка, требующая сохранения асинхронного контекста.


Режимы обработки модулей в SWC

Поведение динамического import() в SWC напрямую зависит от конфигурации модуля:

  • module.type = “es6” — сохранение import() без изменений
  • module.type = “commonjs” — трансформация в совместимую форму
  • module.type = “umd” — аналогично CommonJS с дополнительной обёрткой
  • module.type = “nodenext” — гибридное поведение, близкое к Node.js ESM

Конфигурация SWC:

{
  "jsc": {
    "parser": {
      "syntax": "ecmascript"
    },
    "transform": {},
    "target": "es2020"
  },
  "module": {
    "type": "commonjs"
  }
}

Трансформация import() в CommonJS

Наиболее важная трансформация происходит при генерации CommonJS-кода.

Исходный код:

async function load() {
  const mod = await import('./utils.js');
  return mod.sum(2, 2);
}

SWC преобразует динамический import() в Promise-обёртку над require:

function load() {
  return Promise.resolve().then(function () {
    return require('./utils.js');
  }).then(function (mod) {
    return mod.sum(2, 2);
  });
}

Смысл трансформации

CommonJS не поддерживает асинхронную загрузку модулей как часть языка. Поэтому SWC эмулирует поведение:

  • require() выполняется синхронно;
  • Promise.resolve().then() создаёт асинхронный контекст;
  • результат приводится к интерфейсу ESM namespace object.

Обработка default и named exports

При трансформации важно сохранить совместимость структуры экспортов.

Исходный ESM:

// utils.js
export default function sum(a, b) {
  return a + b;
}

export const version = 1;

Использование:

const mod = await import('./utils.js');

После трансформации в CommonJS:

function load() {
  return Promise.resolve().then(function () {
    return require('./utils.js');
  }).then(function (mod) {
    return mod;
  });
}

Доступ:

mod.default;
mod.version;

SWC не изменяет структуру экспортов, но обеспечивает корректную упаковку результата require.


Ленивые зависимости и chunk-поведение

SWC сам по себе не является бандлером, поэтому:

  • он не создаёт чанки;
  • он не выполняет code splitting;
  • он трансформирует синтаксис на уровне файла.

Однако при использовании SWC внутри сборщиков (Rspack, Webpack loaders, Next.js pipeline) динамический import() становится точкой разбиения на чанки.

Пример поведения в связке со сборщиком:

button.oncl ick = () => import('./chart.js');

На уровне SWC:

button.oncl ick = function () {
  return Promise.resolve().then(function () {
    return require('./chart.js');
  });
};

На уровне бандлера:

  • создаётся отдельный chunk chart.js;
  • require заменяется на runtime chunk loader;
  • добавляется mapping модулей.

Влияние target на трансформацию

SWC учитывает целевой уровень ECMAScript.

target = ES2020 и выше

{
  "jsc": {
    "target": "es2020"
  }
}

Результат:

  • import() остаётся без изменений;
  • трансформация не применяется.

target ниже ES2020 (при CJS)

Если включён CommonJS:

  • происходит понижение до Promise + require.

Особенности Node.js интероперабельности

В Node.js существуют две модели модулей:

  • ESM (import/export)
  • CommonJS (require/module.exports)

SWC при трансформации динамического import() в CommonJS должен учитывать:

  • синхронность require;
  • кеширование модулей Node;
  • различие между module.exports и export default.

Пример потенциального конфликта:

const mod = await import('./cjs-module.js');

Если модуль CommonJS:

module.exports = function () {};

Результат SWC:

mod.default; // обёртка может отсутствовать

В зависимости от окружения может потребоваться interop-обёртка, но SWC базово не эмулирует полноценный ESM-CJS interop runtime, оставляя это сборщику.


Ленивые цепочки и последовательное импортирование

Динамический import() часто используется в цепочках:

async function bootstrap() {
  const router = await import('./router.js');
  const store = await import('./store.js');
  const app = await import('./app.js');

  app.init(store, router);
}

После SWC-трансформации (CJS):

function bootstrap() {
  return Promise.resolve()
    .then(function () {
      return require('./router.js');
    })
    .then(function (router) {
      return require('./store.js').then(function (store) {
        return { router: router, store: store };
      });
    })
    .then(function (data) {
      return require('./app.js').then(function (app) {
        return app.init(data.store, data.router);
      });
    });
}

Структура Promise сохраняется, но последовательность становится вложенной при транспиляции.


Ошибки загрузки и обработка исключений

Динамический import() в оригинале:

try {
  const mod = await import('./missing.js');
} catch (e) {
  console.error(e);
}

SWC-трансформация сохраняет поведение через rejection Promise:

function load() {
  return Promise.resolve()
    .then(function () {
      return require('./missing.js');
    })
    .catch(function (e) {
      console.error(e);
    });
}

Любая ошибка require превращается в rejection цепочки.


Ограничения трансформации SWC

При обработке import() существуют системные ограничения:

  • отсутствует полноценная симуляция ESM runtime;
  • нет автоматического code splitting;
  • не моделируется асинхронная загрузка файлов;
  • interop между ESM и CJS частично зависит от окружения;
  • поведение может отличаться от webpack/Vite/Rollup.

SWC выполняет именно синтаксическую трансформацию, а не семантический bundling.


Встраивание в пайплайн трансформации

В типичной архитектуре SWC используется как этап компиляции:

TypeScript/ESNext
        ↓
       SWC
        ↓
   JavaScript (ESM/CJS)
        ↓
   Bundler / Runtime (Rspack, Webpack, Node)

На этапе SWC:

  • анализируется ImportExpression;
  • определяется target module system;
  • генерируется Promise-обёртка или сохраняется ESM.

Оптимизационные аспекты

Динамический import() часто рассматривается как сигнал для ленивой загрузки. Однако SWC не оптимизирует:

  • порядок загрузки;
  • объединение чанков;
  • дедупликацию модулей;
  • предзагрузку зависимостей.

Оптимизация возможна только на уровне bundler/runtime.


Совместимость с TypeScript

TypeScript поддерживает:

const mod = await import('./utils');

SWC при трансформации TypeScript:

  • удаляет типы;
  • сохраняет import() как синтаксическую конструкцию;
  • применяет module transform по конфигурации.

Типовая аннотация:

type Utils = typeof import('./utils');

SWC не влияет на типовую систему, но сохраняет корректность runtime-структуры.


Итоговая модель поведения

Поведение динамического import() в SWC можно свести к трём сценариям:

  • ESM output — конструкция сохраняется без изменений
  • CommonJS outputimport() превращается в Promise-обёртку над require()
  • Bundler pipeline — SWC оставляет сигнальную конструкцию, а разбиение выполняется выше уровнем

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