Astro: Cannot use import statement outside a module — что не так с конфигом
Эта ошибка вылетает чаще всего там, где её не ждёшь: при запуске astro.config.mjs, при попытке подключить какой-нибудь сторонний скрипт в <script> или при вызове Node-команды над файлом, где ты искренне думал, что это ESM.
Сообщение в консоли выглядит обычно так:
SyntaxError: Cannot use import statement outside a moduleВ моём опыте у этой ошибки три типичных причины. Разберу по очереди — и по каждой покажу, что именно чинить.
Причина 1: Node не считает файл ESM-модулем
Node работает в двух режимах — CommonJS (по умолчанию для .js) и ESM. ESM включается одним из трёх способов:
- расширение
.mjs; "type": "module"вpackage.json;- загрузчик/раннер, который сам форсит ESM (например,
tsx).
Astro изначально полагается на ESM. Если ты в проекте написал свой служебный скрипт scripts/seed.js с import внутри, и в package.json нет "type": "module", Node ругнётся именно так.
Самая быстрая починка — переименовать файл в .mjs:
mv scripts/seed.js scripts/seed.mjs
node scripts/seed.mjsЕсли хочется оставить .js, добавляй в package.json:
{
"name": "my-astro-site",
"type": "module",
"scripts": {
"dev": "astro dev"
}
}После этого все .js в проекте автоматически считаются ESM. Если у тебя в репозитории есть старые скрипты на CommonJS, их придётся переименовать в .cjs или переписать.
Причина 2: TS-файл прокидывают в Node без раннера
У меня был кейс: коллега запускал node scripts/migrate.ts и получал ровно эту ошибку. Node не понимает TypeScript, ему всё равно, что в файле import { something } from '...'. Он видит синтаксис ESM, но файл по умолчанию обрабатывается как CommonJS, а сам TS-синтаксис в стандартном Node ещё и не выполняется.
Лечится через tsx:
pnpm add -D tsx
pnpm tsx scripts/migrate.tsИли через --import с tsx:
node --import tsx scripts/migrate.tsВ Astro 5 это особенно заметно, когда пишешь миграции для Drizzle или сидеры. Я обычно держу отдельный scripts/db и запускаю всё через tsx, чтобы не возиться ни с tsc, ни с ts-node.
Причина 3: код для браузера отправили в Node
Менее очевидный сценарий, но я на него тоже наступал. Бывает, что кто-то по ошибке запускает клиентский JS-файл напрямую в Node — например, для отладки. В файле сверху import { Foo } from './foo.js', а Node видит обычный .js без type: module и валится.
Решение зависит от задачи:
- если файл реально клиентский — не запускай его в Node, его задача — попасть в бандл и работать в браузере;
- если хочется погонять логику локально — вынеси её в отдельный
.mjs-скрипт и зови оттуда.
Где это часто всплывает в Astro
Кастомные интеграции и плагины
Когда пишешь свою интеграцию для Astro, файл интеграции — это обычно ESM. Если ты по привычке вынес её в integrations/my-thing.js и забыл про type: module, при сборке прилетит та же ошибка. Лечение — то же.
Скрипты в <script> на странице Astro
В Astro по умолчанию <script> бандлится и работает как модуль. Но если ты добавил is:inline, он вставляется как есть, и браузер по умолчанию считает такой скрипт классическим — не модулем. Тогда import внутри уронит код прямо в браузере с тем же текстом.
<script is:inline>
import { foo } from '/some.js'; // упадёт
</script>Для inline-скриптов либо не используй import, либо явно укажи type="module":
<script type="module" is:inline>
import { foo } from '/some.js';
</script>Быстрый чек-лист, по которому я прохожу, когда вижу эту ошибку
- Какое расширение у файла? Если
.js— есть ли"type": "module"вpackage.json? - Это TS-файл, который кто-то запустил в Node напрямую без
tsx? - Это inline-скрипт в Astro без
type="module"? - Это вообще не серверный файл, а кусок клиентского кода, который ушёл в Node по недоразумению?
Девять из десяти раз ответ находится на этих четырёх вопросах. Если нет — смотри стек-трейс целиком, там обычно видно конкретный .js-файл, на котором споткнулся парсер, и дальше уже идёшь по списку причин выше.