lenec ru

← все посты

SyntaxError: Unexpected token 'export' — что не так с настройкой

11K

Запускаешь Node-скрипт или Jest-тест и получаешь:

SyntaxError: Unexpected token 'export'

Парсер увидел export, но не понял, что это. export — синтаксис ESM-модулей, и он работает только в одном из двух режимов: либо файл сам ESM, либо есть транспайлер, который переводит ESM в CommonJS. Если ни того, ни другого — Node воспринимает это как неожиданный токен и обрывает выполнение.

В моей практике эта ошибка прилетает в трёх контекстах: при работе с Node-скриптами, в Jest-тестах, и при запуске TypeScript-файлов напрямую через Node. Ситуации схожие, но лекарства слегка разные.

Сценарий 1: запускаю Node-скрипт

// scripts/seed.js
export function seedDatabase() { /* ... */ }
seedDatabase();
$ node scripts/seed.js
SyntaxError: Unexpected token 'export'

Node по умолчанию для .js считает файл CommonJS и не понимает export. Лекарство — одно из:

  • Переименовать в .mjs: mv scripts/seed.js scripts/seed.mjs.
  • Добавить в package.json: "type": "module" — тогда все .js станут ESM.
  • Переписать на CommonJS: module.exports = { seedDatabase }.

Выбор зависит от проекта. Для нового кода я предпочитаю ESM. Если работаю в старой кодовой базе на CommonJS — не ломаю, просто пишу в её стиле.

Сценарий 2: TypeScript через node

$ node scripts/migrate.ts
SyntaxError: Unexpected token 'export'

Node не выполняет TypeScript. Если TS-файл написан в ESM-стиле, парсер видит export и валится в первой же строке.

Лекарство — раннер, который понимает TS:

pnpm add -D tsx
pnpm tsx scripts/migrate.ts

Через флаг --import:

node --import tsx scripts/migrate.ts

tsx делает on-the-fly компиляцию TypeScript в JS и подсовывает Node. Никакого отдельного tsc-этапа. У меня все скрипты для миграций и сидеров работают именно так.

Сценарий 3: Jest и ESM-зависимости

Самый раздражающий сценарий. Jest по умолчанию умеет CommonJS, и пакеты, которые перешли на ESM (а это сейчас почти все современные), валятся.

SyntaxError: Unexpected token 'export'
  > 1 | export { something } from './internal';

Jest до сих пор не запускает ESM нативно по умолчанию. Решений несколько:

Подход 1: transformIgnorePatterns

Проблемный пакет нужно прогнать через transformer (например, babel-jest), чтобы он скомпилировался в CommonJS перед запуском. По умолчанию Jest пропускает node_modules через transformer.

// jest.config.js
module.exports = {
  transformIgnorePatterns: [
    '/node_modules/(?!(uuid|some-esm-package|@scope/.+))',
  ],
};

Регулярка читается как «игнорируй node_modules, кроме перечисленных». Все упомянутые пакеты будут проходить через transformer.

Минус: при каждом запуске тестов transformer прогоняет лишний код, тесты замедляются.

Подход 2: переключиться на Vitest

На новых проектах я не борюсь с Jest и сразу беру Vitest. У него ESM из коробки, никаких transformIgnorePatterns с регулярками. API почти полностью совместимое — миграция занимает день для среднего проекта.

pnpm add -D vitest
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'node',
  },
});

Подход 3: Jest с экспериментальным ESM

Можно включить нативный ESM в Jest, но это всё ещё experimental:

node --experimental-vm-modules ./node_modules/.bin/jest

В package.json:

{
  "scripts": {
    "test": "NODE_OPTIONS=--experimental-vm-modules jest"
  }
}

Работает, но с подводными камнями: моки, динамические импорты, jest globals — всё ведёт себя чуть иначе.

Сценарий 4: TypeScript с tsconfig для CommonJS

В tsconfig.json у тебя "module": "commonjs". После сборки получается код вида exports.foo = .... Это работает в Node. Но если ты в каком-то месте проекта забыл, что компиляция в CommonJS, и в исходнике написал export — после сборки Node всё равно увидит CommonJS, не ESM.

Если же "module": "esnext" или "node16", скомпилированный код использует export. Тогда Node должен запускать его как ESM — то есть с "type": "module" в package.json или с расширением .mjs.

Логика простая: модуль-формат на исходниках, в tsconfig и в Node должен совпадать. Если хоть в одном месте рассинхрон — прилетит SyntaxError.

Алгоритм диагностики

  • Какой файл выполняется — .js, .mjs, .cjs, .ts?
  • Что говорит package.json в поле type?
  • Если это TypeScript — кто его запускает: Node напрямую (нельзя), tsx (можно), ts-node, jest, vitest?
  • Если Jest — это твой код или пакет из node_modules? Если пакет — добавляй его в transformIgnorePatterns.
  • Если в проекте есть собранный код — какой формат используется? Совпадает ли с настройкой Node-окружения?

Маленький чек-лист «когда писать ESM, а когда CJS»

  • Новый проект — ESM ("type": "module", "module": "esnext" или "node16").
  • Миграция старой кодовой базы — оставляй CJS, не путай команду.
  • Скрипты для разовых задач — TS-файл + tsx.
  • Тесты — Vitest на новых, Jest с transformIgnorePatterns на старых.

Unexpected token 'export' звучит страшно, но на деле это сигнал «у нас рассинхрон по модульному формату». Понимаешь, кто кого запускает и в каком режиме — и сразу видно, какой винтик подкрутить.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.