Создатель Yarn и Babel Себастиан Маккензи (Sebastian McKenzie) долго работал над универсальным инструментом для JavaScript-разработчиков. 26 февраля он представил проект Rome. «Все дороги ведут в Рим», — таков девиз создателей инструмента.
- Что такое Rome
- Как это выглядит на практике
- Почему Rome — перспективный и удобный инструмент?
- Как устроена сборка в Rome
- Что у Rome с качеством сборки
- Перспективы Rome в больших проектах
- Сокращение размеров сборки в Rome
- Дальнейшая оптимизация
- Разделение кода в Rome
- Использование Rome в CLI
Что такое Rome
Rome — это универсальный набор инструментов для разработки на JavaScript. Он компилирует и собирает JavaScript-проекты, выполняет линтинг и тайп-чекинг, запускает тесты, а также форматирует код.
Как это выглядит на практике
Создатели представили Rome несколько дней назад. Но в CLI уже есть полезная информация об использовании инструмента. Вот некоторые команды:
-
rome bundle
— собирает модули в проекте JavaScript; -
rome compile
— компилирует единый файл; -
rome develop
— запускает локальный сервер; -
rome parse
— парсит единый файл и выводит абстрактное синтаксическое дерево; -
rome resolve
— резолвит файл; -
rome analyzeDependencies
— анализирует и выводит зависимости.
Подробнее об использовании Rome в CLI пойдёт речь ниже.
Почему Rome — перспективный и удобный инструмент?
Rome можно назвать новым подходом к использованию инструментов для разработки на JavaScript. Он больше похож на универсальные решения, которые применяют в крупных компаниях, чем на опенсорсные инструменты, каждый из которых решает какую-то одну задачу. То есть при использовании Rome разработчик использует только его, а не пропускает код через несколько разных инструментов.
Это решает одну из проблем, с которыми сталкиваются разработчики при использовании популярных сборщиков типа Webpack или Rollup. Она заключается в том, что анализ и оптимизация программы становятся слишком «дорогими», так как каждый инструмент независимо от других парсит код и строит своё абстрактное синтаксическое дерево (AST).
Как устроена сборка в Rome
Архитектуру Rome можно назвать уникальной. Компиляция выполняется помодульно. Это позволяет обрабатывать каждый модуль в пуле рабочих потоков. Это хорошо работает на уровне обработки модулей, но создаёт проблемы на уровне сборки. Чтобы избежать необходимости повторного парсинга модулей, необходимо создавать единое пространство имён. То есть все модули в проекте находятся в одной области видимости.
Чтобы сделать сборку возможной с учётом помодульной обработки, Rome добавляет префиксы всем переменным в области видимости. Эти префиксы генерируются из названия файлов. Например, переменная foo
в модуле test.js
превращается в test_js_foo
.
Этот подход применяется к импортируемым и экспортируемым идентификаторам на уровне модулей. Это значит, что при экспорте можно использовать только название файла и идентификатор экспортируемой сущности.
Например, если имя файла — test.js
, экспорт — export const foo = 1;
, на выходе получаем const ___R$test_js$foo = 1;
. Ещё один пример с именем файла index.js
. Импорт выглядит так:
import { foo } from './test.js';
console.log(foo);
На выходе получаем: console.log(___R$test_js$foo);
.
Что у Rome с качеством сборки
В современной веб-разработке инструменты определяют качество и размер приложения. Это значит, что разработчики должны обращать внимание на содержание сборок Rome. Автор оригинальной публикации всегда проверяет, будет ли инструмент объединять модули в единое замыкание как Rollup, или сохранит границы модулей с помощью замыканий и рантайм-загрузчиков как Webpack.
Rome создаёт сборки с единым замыканием. Это похоже на то, как работает Rollup. Примеры ниже.
Модуль:
function hello() {
return 'Hello World';
}
console.log(hello());
Сборка:
(function(global) {
'use strict';
// input.ts
const ___R$rome$input_ts = {};
function ___R$$priv$rome$input_ts$hello() {
return 'Hello World';
}
console.log(___R$$priv$rome$input_ts$hello());
return ___R$rome$input_ts;
})(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this);
Пока Rome не предлагает инструментов для уменьшения размеров сборки. Однако дополнительное использование плагина Terser улучшает результат.
!function(o) {
"use strict";
console.log("Hello World");
}("undefined" != typeof global ? global : "undefined" != typeof window && window);
Примеры показывают, что сборки Rome можно серьёзно оптимизировать. В идеале сборщик должен знать режим работы и поддерживать замыкания и директиву strict mode
при работе с модулями ES. Он также мог бы поднимать глобальные декларации на уровень модуля.
Перспективы Rome в больших проектах
Посмотрим на более сложный пример с двумя модулями, в которых есть общие зависимости.
entry.tsx
:
import React from './react';
import title from './other';
// Note: dynamic import doesn't yet work in Rome
// const title = import('./other').then(m => m.default);
async function App(props: any) {
return <div id="app">{await title()}</div>
}
App({}).then(console.log);
other.tsx
:
import React from './react';
export default () => <h1>Hello World</h1>;
react.tsx
:
type VNode = {
type: string;
props: any;
children: Array<VNode|string>
};
function createElement(
type: string,
props: any,
...children: Array<VNode|string>
): VNode {
return { type, props, children };
}
export default { createElement };
Сборка с помощью rome bundle entry.tsx out
генерирует директорию с файлом index.js
.
(function(global) {
'use strict';
// rome/react.tsx
function ___R$$priv$rome$react_tsx$createElement(
type, props, ...children
) {
return {type: type, props: props, children: children};
}
const ___R$rome$react_tsx$default = {
createElement: ___R$$priv$rome$react_tsx$createElement
};
// rome/other.tsx
const ___R$rome$other_tsx$default = () =>
___R$rome$react_tsx$default.createElement(
'h1', null, 'Hello World'
);
// rome/test.tsx
const ___R$rome$test_tsx = {};
async function ___R$$priv$rome$test_tsx$App(props) {
return ___R$rome$react_tsx$default.createElement(
'div', { id: 'app'},
(await ___R$rome$other_tsx$default())
);
}
___R$$priv$rome$test_tsx$App({}).then(console.log);
return ___R$rome$test_tsx;
})(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this);
Пример немного сложнее предыдущего, но структура в нём сохраняется.
Без реализации модулей и «мёртвого кода» три исходных модуля превращаются в одно замыкание.
(function(global) {
'use strict';
// rome/react.tsx
const ___R$rome$react_tsx$default = /* snip */;
// rome/other.tsx
const ___R$rome$other_tsx$default = /* snip */;
// rome/entry.tsx
___R$$priv$rome$entry_tsx$App({}).then(console.log);
})(window);
Сокращение размеров сборки в Rome
Как сказано выше, в данный момент в Rome нет инструментов для уменьшения размеров сборки. Можно пропускать полученный результат через Terser. Если сделать это с кодом из примера выше, на выходе получаем следующее (код отформатирован для удобства чтения):
! function(e) {
const n = {
createElement: function(e, n, ...t) {
return {
type: e,
props: n,
children: t
}
}
};
(async function(e) {
return n.createElement("div", {
id: "app"
}, await n.createElement("h1", null, "Hello World"))
})().then(console.log)
}("undefined" != typeof global ? global : "undefined" != typeof window && window);
После минимизации результат выглядит довольно хорошо. Надо учитывать, что это очень простое приложение. Результаты работы с реальным приложением могут быть другими.
Дальнейшая оптимизация
Автор оригинальной публикации работает над инструментом, который оптимизирует сборки JavaScript. В качестве эксперимента он пропустил созданную с помощью Rome сборку через этот инструмент до обработки с помощью Terser. В итоге получился близкий к идеальному результат: без «мёртвого кода» и функций-обёрток, а также с использованием преимуществ современного синтаксиса JavaScript.
const e = {
createElement: (e, n, ...t) =>
({ type: e, props: n, children: t })
};
(async () =>
e.createElement("div", { id: "app" },
await e.createElement("h1", null, "Hello World")
)
)().then(console.log);
Этот пример кажется перспективным.
Разделение кода в Rome
Похоже, Rome не поддерживает динамический импорт и разделение кода. Использование import()
позволяет импортировать модуль, но он ведёт себя как при статическом импорте. Исходный оператор import()
не меняется при сборке, что приводит к ошибке.
Пока неизвестно, как разделение и фрагментирование кода повлияет на качество сборки. Эффективность этих действий зависит от доступа к переменным, которые попадают в одну сборку из другой.
Использование Rome в CLI
Ниже приводится вывод хелпа Rome в CLI.
$ rome --help
Usage: rome [command] [flags]
Options
--benchmark
--benchmark-iterations <num>
--collect-markers
--cwd <input>
--focus <input>
--grep <input>
--inverse-grep
--log-path <input>
--logs
--log-workers
--markers-path <input>
--max-diagnostics <num>
--no-profile-workers
--no-show-all-diagnostics
--profile
--profile-path <input>
--profile-sampling <num>
--profile-timeout <num>
--rage
--rage-path <input>
--resolver-mocks
--resolver-scale <num>
--silent
--temporary-daemon
--verbose
--verbose-diagnostics
--watch
Code Quality Commands
ci install dependencies, run lint and tests
lint run lint against a set of files
test run tests
--no-coverage
--show-all-coverage
--update-snapshots
Internal Commands
evict evict a file from the memory cache
logs
rage
Process Management Commands
restart restart daemon
start start daemon (if none running)
status get the current daemon status
stop stop a running daemon if one exists
web
Project Management Commands
config
publish TODO
run TODO
Source Code Commands
analyzeDependencies analyze and dump the dependencies of a file
--compact
--focus-source <input>
bundle build a standalone js bundle for a package
compile compile a single file
--bundle
develop start a web server
--port <num>
parse parse a single file and dump its ast
--no-compact
--show-despite-diagnostics
resolve resolve a file
Адаптированный перевод статьи Rome, a new JavaScript Toolchain by Jason Miller. Мнение автора оригинальной публикации может не совпадать с мнением администрации Хекслета.