Як Deno 2.x тихо вирішив проблеми сумісності з Node та втому від інструментів
У цій статті розглядаються версії Deno від 2.0 до 2.9, де показано, як сумісність з npm, набори дозволів та вбудовані інструменти усунули перешкоди, які раніше змушували розробників відмовлятися від нього.
Цей пробний запуск відбувся п’ять років тому.
Минулого місяця для швидкого побічного проекту знадобився невеликий HTTP-сервер. Створено нову директорію, виконано команду deno init, і через десять хвилин вже був готовий працюючий сервер із TypeScript, тестами, форматуванням та інструментами для перевірки коду. Не потрібно було писати файл tsconfig, налаштовувати prettier чи eslint, створювати файл jest.config.ts — також не було жодного file package.json.
Під час перевірки версії виявилося 2.9.3.
Виявилося, що з жовтня 2024 року по липень 2026 року Deno випустив десять мінорних оновлень, кожне з яких тихо вирішувало одну з проблем, що спричинили початкове відмовлення від цього інструменту. Нічого з цього на той момент не було помічено, адже інструмент вже був у свідомості класифікований як „цікава ідея, але ще не готова до реальної роботи“, і не було причин знову до нього звертатися.
Ось огляд того, що насправді змінилося, по одній старій скарзі за раз.
Скарга 1: пакети npm були другосортними
Це була проблема для багатьох розробників, включаючи мене. У версії Deno 1.x все, що не було опубліковано на deno.land/x або не мало належних URL для ES-модулів, просто не функціонувало. Розділення між світом Deno та світом npm здавалося незмінним.
Deno 2.0, випущений у жовтні 2024 року, повністю подолав цю прогалину. Тепер ви можете безпосередньо завантажувати будь-який пакет з каталогу npm, який налічує понад два мільйони пакетів, використовуючи специфікатор npm::
import express from "npm:express@5";
import { PrismaClient } from "npm:@prisma/client";
Якщо ви волієте користуватися package.json, це також підтримується. Deno аналізує його, отримує залежності з реєстру npm та навіть може створити локальну папку node_modules, якщо увімкнути відповідне налаштування. Приватні реєстри також працюють через .npmrc, точно так само, як у Node.
Статистика, яка справді переконала всіх: понад 75 відсотків власних тестових наборів Node тепер успішно запускаються під Deno. Це не якийсь поверхневий рішення — це свідчить про справжню, підтверджену сумісність.
Момент, який остаточно все вирішив, — це спроба запустити Deno у вже існуючому проекті Express. Не довелося змінювати жодної інструкції імпорту. Було додано файл deno.json із значенням "nodeModulesDir": "auto", виконано команду deno install, і час запуску склав близько 900 мс, на відміну від понад 3 секунд у npm за тих самих умов (тестування проводилося з чистою кеш-системою у проекті, що використовує React, Vite, Babel та ESLint). Сервер працював коректно вже при першій спробі.
Скарга 2: система дозволів була клопіткою на практиці
Концепція моделі дозволів здавалася логічною. На практиці це означало необхідність щоразу переписувати щось подібне під час запуску:
deno run --allow-read=./data --allow-write=./data --allow-net=api.example.com --allow-env main.ts
Якщо забути один флаг, процес завершується. Якщо додати нову залежність, яка читає змінну середовища, процес знову завершується. Це більше нагадувало налаштування правил брандмауера, ніж створення додаткового проекту.
Deno 2.5, випущений у вересні 2025 року, запровадив набори дозволів, визначені безпосередньо у файлі конфігурації. Ви оголошуєте названі набори під ключем "permissions" у файлі deno.json, а потім використовуєте їх за допомогою флага -P (скорочення від --permission-set):
{
"permissions": {
"default": {
"read": ["./src", "./data"],
"write": ["./data"],
"net": ["api.example.com", "0.0.0.0:3000"],
"env": true
},
"test": {
"read": true,
"write": ["./tmp"],
"net": false
}
}
}
Після цього виконання команди deno run -P main.ts автоматично завантажує „стандартний“ набір дозволів з файлу deno.json, розташованого у робочій директорії, або можна виконати команду deno test -P=test, щоб використати окремий набір дозволів лише для тестів. Код тестів та код для продакшну отримують різні обмеження щодо дозволів без необхідності змінювати жодних параметрів командного рядка. Основні гарантії безпеки залишилися незмінними — просто зникла складність їх використання.
Також існує змінна середовища DENO_AUDIT_PERMISSIONS, яка створює журнал у форматі JSONL, що фіксує кожну перевірку дозволів під час роботи програми. Це дозволяє точно бачити, до чого намагаються отримати доступ ваші залежності, не переглядаючи їхній вихідний код.
Скарга 3: Мені все одно знадобилася купа додаткових інструментів
Перевагою та водночас тягарем Node є те, що майже кожна обов’язок передається окремому пакету. Потрібна форматування? Використовуйте prettier. Перевірка на помилки? ESLint та кілька плагінів. Тестування? Jest або Vitest, плюс файл конфігурації для обробки трансформацій TypeScript. Перевірка типів? tsc, налаштовується окремо від того, що запускає ваш код. Збирання проекту? Виберіть будь-який з півдюжини інструментів для збирання, кожен з яких має власний діалект конфігурації.
Deno об’єднує все це у вбудовані підкоманди. Вони існували ще до випуску 2.0:
deno fmt # formats TS, JS, JSON, HTML, CSS, YAML, SQL
deno lint # built-in linter with quick fixes
deno test # test runner with coverage, snapshots, sharding
deno check # type checking
deno compile # single binary, cross-platform, code signing
deno bench # benchmarking
deno doc # documentation generation
Починаючи з версії 2.8, з’явилося ще шість підкоманд:
deno audit # security audit of dependencies
deno audit fix # auto-upgrade vulnerable packages to nearest patched version
deno why # explain why a package is installed (traces dependency paths)
deno transpile # strip types, emit .d.ts declarations
deno pack # build an npm-publishable tarball from JSR/Deno code
deno ci # reproducible install for CI (errors without lockfile)
А з версією 2.9 з’явилося ще більше:
deno desktop # build native desktop apps via webview (experimental)
deno list # show dependency tree (like npm ls)
deno link/unlink # local package linking for development
Найбільш поширеним інструментом є deno compile. Створення невеликого інструменту командного рядка та виконання команди deno compile --target x86_64-unknown-linux-gnu main.ts генерує самодостатній бінарний файл. На цільовій машині не потрібно встановлювати нічого іншого — достатньо просто скопіювати його на сервер та запустити.
Увага: отриманий бінарний файл містить V8 та весь середовище виконання Deno, тому розмір типових додатків становить приблизно 60–100 МБ. Якщо важливий розмір, команда
deno compile --bundle(яка досі є нестабільною у версії 2.8) застосовує інтенсивне скорочення коду та може значно зменшити розмір вихідного файлу для простих скриптів. На власному блогу Deno було продемонстровано, що скрипт „hello world“ з бібліотекою lodash має розмір 1,5 МБ при використанні параметрів--bundle --minify.
Скарга 4: міграція реального проекту здавалась недосяжною
Найбільша перешкода для зміни часу виконання зазвичай не пов’язана з самим часом виконання. Це файл блокування, структура node_modules, від якої залежать ваші існуючі інструменти, та розкидані по кодовій базі виклики require().
У Deno 2.9 було введено функцію створення файлу блокування. Припустимо, проект вже відстежує свої залежності за допомогою одного з поширених файлів блокування менеджерів пакетів — наприклад, package-lock.json від npm, pnpm-lock.yaml, yarn.lock або bun.lock — але ніколи не створював deno.lock. Виконання команди deno install у такому проекті створює відсутній deno.lock безпосередньо на основі того з цих файлів, який було знайдено. Версії, визначені у файлах, збігаються. Хеші цілісності також збігаються. Не існує проблем з розбіжностями під час визначення версій.
У ситуаціях, де дійсно потрібен фізичний каталог node_modules — для нативних додатків чи інструментів, які безпосередньо сканують файлову систему, встановлення "nodeModulesDir": "auto" примушує Deno створити його. Також існує опція hoisted-layout ("nodeModulesLinker": "hoisted" у файлі deno.json, доступна з версії 2.8) для старіших інструментів, створених на основі плоскої структури каталогів npm замість символічних посилань у стилі pnpm.
Шим для node — це чудова деталь: як тільки ви встановлюєте сам Deno, він автоматично встановлює замінний бінарник node у ваш PATH без необхідності ручної налаштування. Якщо ніщо інше ще не надає виконуваного файлу node, цей шим перехоплює виклики Node CLI, перетворює аргументи та передає їх Deno. Це означає, що скрипти CI, які все ще використовують node dist/server.js, продовжуватимуть працювати без жодних змін. Ви можете вимкнути цю функцію за допомогою DENO_DISABLE_NODE_SHIM=1, якщо не бажаєте, щоб вона діяла автоматично.
Імпорти з прямим вказівником — написання import fs from „fs“, що призводить до розрішення як node:fs — були додані у версії 2.0 та стали повністю стабільними, працюючи без будь-яких флагів чи налаштувань з версії 2.9. Немає потреби повертатися назад та переписувати свої оператори імпорту для міграції.
Спроба міграції
Розглянемо невеликий проект API Hono (Hono — це легкий HTTP-фреймворк, схожий на Express) із приблизно 15 маршрутами, базою даних Postgres, до якої підключаються через Drizzle ORM, та процесором фонових завдань. Він працював на Node 22 з використанням pnpm. Усі експерименти проводилися за допомогою Deno 2.9.3.
Ось як відбувалася конвертація:
- Запустіть команду
deno installз кореневого каталогу проекту. Вона створює файлdeno.lockбезпосередньо з існуючого файлуpnpm-lock.yamlпротягом менше ніж двох секунд. - Додайте значення
"nodeModulesDir": "auto"у щойно створений файлdeno.json. - Запустіть додаток за допомогою команди
deno run -A src/server.ts.
Все працює без проблем. Кожен із 15 маршрутів реагує правильно, запити на основі Drizzle виконуються за очікуваним чином, а працівник черги продовжує обробляти завдання на фоні.
Однак зламалися три речі:
- Один файл для тестування використовував
jest.mock(), якого немає у вбудованому засобі тестування Deno. Заміна на ручний мок займає менше п’яти хвилин. - Одна з залежностей посилалася на
__dirnameу файлі CommonJS, але Deno завантажив його як ES-модуль. Для виправлення довелося додати"type": "commonjs"до локальних налаштувань цього конкретного пакета.
process.env.NODE_ENV без явного імпорту модуля process. Ця частина насправді працювала нормально, адже з версії 2.0 Deno робить process глобальним об’єктом — проте прапорці дозволів для цього конкретного скрипта не включали env: true, тож це потрібно було додати.Усе перетворення зайняло приблизно 25 хвилин від початку до кінця. Час запуску знизився з приблизно 620 мс до 320 мс, за даними вимірювань з hyperfine протягом 50 запусків. Використання пам’яті у стані очікування (RSS) зменшилося з 142 МБ до 64 МБ. Крім того, можна було видалити чотири окремі файли конфігурації: prettier, eslint, jest та tsconfig.
Залишкові проблеми
У заголовку цієї статті згадується „усе, що я ненавидів“, а раніше перелічені скарги є справжніми, поширеними проблемами серед розробників Node. Проте існує кілька новіших зауважень, які варто врахувати:
- Покриття Node API не є повним. 75 відсотків успішних тестів у наборі тестів Node означає, що чверть з них все ще провалюється. Ця проблема може не впливати на типові завдання, але ті, хто використовує незвичайні аспекти
node:vm, програмне використанняnode:inspectorабо більш глибокі функціїnode:cluster, повинні заздалегідь перевірити панель сумісності на node-test-viewer.deno.dev.
node_modules. Будь-які засоби, що використовують C++ — такі як sharp, bcrypt, sqlite3 та подібні пакети — вимагають як налаштування nodeModulesDir, так і флага --allow-ffi. Це працює, але це додатковий крок налаштування, який легко можна випадково пропустити.postinstall — наприклад, кроки компіляції через node-gyp або команда prisma generate — потребують прямого дозволу за допомогою параметра --allow-scripts=npm:package-name. Це свідомий крок з міркувань безпеки, але він може застати команду зненацька під час міграції.process.versions.node та змінюють свою поведінку відповідно, або ж закодовують шляхи до файлів як /node_modules/.cache. Схема модулів Deno у стилі pnpm із символічними посиланнями ускладнює роботу деяких з цих інструментів. Існує опція режиму hoisted-mode для обходу цієї проблеми, але це лише тимчасове рішення, а не справжнє.Жодна з цих проблем не була настільки серйозною, щоб завадити виконанню описаної вище міграції. Однак у інших проектах вони можуть стати проблемою, тому варто перевірити їх заздалегідь, а не виявляти під час міграції.
Чому ці виправлення залишилися непоміченими
Протягом 21 місяця вийшло десять версій з незначними оновленнями, кожна з яких поступово вирішувала проблеми, пов’язані з трьома–п’ятьма аспектами. Не було жодного масштабного переписування коду чи спеціальної події з представленням „Deno 3.0“. Команда впровадила деталізовані набори дозволів у версії 2.5, досягла швидкості встановлення на рівні npm у версії 2.8 та додала функцію створення файлу lockfile у версії 2.9, розглядаючи кожне з цих оновлень як звичайний процес підтримки, а не як гучну новину.
На противагу цьому, зазвичай переходи на нові фреймворки просуваються через блог-пости, доповіді на конференціях, посібники з міграції, хвилю коментарів у соціальних мережах, екстремальні думки та заперечення проти них. Deno обійшов увесь цей цикл публічних дискусій та просто випустив необхідні виправлення безпосередньо.
Розробники, які рано відкинули цей середовище виконання, часто робили це тому, що сформували свою думку один раз та більше не переглядали її у міру розвитку інструментів. Це ознака недбалості, а не недолік самого проекту.
Для тих, хто сформував своє уявлення про Deno до версії 2.0, інструмент, який тоді тестувався, фактично зник. Те, що існує сьогодні, більше нагадує „Node без усього зайвого обтяження“, ніж „цікавий движок виконання TypeScript, який не може працювати з npm“. Розчарування того часу були обґрунтованими. Їх вже вирішили. Варто ще раз на це подивитися.
Пов’язана література
- Заміна Jest на вбудований тестовий запускач Node у Node 24 — приклад з реального життя показує, як вбудований тестовий запускач Node 24 та підтримка TypeScript скорочують час виконання тестів у середовищі CI, водночас усуваючи чотири залежності.