Главная » Статьи » Rome, новый набор инструментов JavaScript

Rome, новый набор инструментов JavaScript

Rome, новый набор инструментов JavaScript

От автора: Себастьян МакКензи, первоначальный создатель Yarn и Babel и член команды Facebook React Native, работает над решением «все в одном» для разработки JavaScript и TypeScript. Проект Rome, отсылка ко «все дороги ведут в Рим», был представлен общественности 26 фев 2020.

Что такое Rome?

Rome — это реализация полного набора инструментов JavaScript с нуля. Он компилирует и связывает проекты JavaScript, линты и проверки типов, запускает тесты, а также может форматировать код.

На что это похоже?

Хотя Rome все еще находится на ранней стадии, CLI предоставляет некоторую полезную информацию о его использовании:

rome bundle — создать отдельный набор JS для пакета

rome compile — скомпилировать один файл

rome develop — запустить веб-сервер

rome parse — парсировать один файл и вывести его

rome resolve — разрешить файл

rome analyzeDependencies — проанализировать и вывести зависимости файла

Для получения полной информации см. Использование CLI.

Почему это может быть хорошей идеей?

Rome использует отличный от существующих стеков с открытым исходным кодом подход к инструментам JavaScript, и, возможно, больше похож на внутренний инструмент на основе монорепо, который можно найти в очень крупных компаниях. Вместо того, чтобы строить конвейер сборки, передавая исходный код через несколько разрозненных инструментов для различных задач, Rome выполняет все этапы сборки и компиляции самостоятельно.

Это помогает решить одну из проблем, с которыми сталкиваются популярные упаковщики, такие как Webpack и Rollup, а именно то, что анализ и оптимизация всей программы оказываются очень сложными или затратными, поскольку каждый инструмент должен анализировать и создавать свой собственный AST.

Пакетирование

Архитектура Rome относительно уникальна: вся компиляция происходит отдельно для каждого модуля, что позволяет обрабатывать каждый модуль в пуле потоков workers. Это хорошо подходит для преобразований для каждого модуля, но представляет собой сложную задачу для объединения: во избежание необходимости повторного анализа каждого модуля, созданного workers, модули должны быть предварительно разделены именами, чтобы все они могли совместно использовать одну область.

Чтобы сделать возможным связывание, несмотря на то, что компиляция выполняется для каждого файла, 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 стоит обратить внимание. В частности, мне всегда интересно узнать, сворачивают ли пакеты, сгенерированные инструментом, модули в общее закрытие, как 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);

В настоящее время не предусмотрено способа минимизации выходных данных в пакете, чего можно было ожидать с учетом ранней стадии проекта. Однако выполнение вышеуказанного результата с использованием Terser дает очень неплохой вывод:

!function(o) { "use strict"; console.log("Hello World");
}("undefined" != typeof global ? global : "undefined" != typeof window && window);

Как вы можете видеть, здесь есть небольшое количество оптимизационных вещей, даже в таком очень простом пакете. В идеале сборщик может быть осведомлен о предполагаемом режиме, и если известно, что он компилируется для цели ES-модулей, он может опустить директиву закрытия и строгого режима. Он также может поднять объявление «global» в область видимости модуля, что в приведенном выше случае позволит Terser полностью устранить код.

В более крупных проектах

Давайте посмотрим на немного более сложную демонстрацию, включающую два модуля с общей зависимостью:

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);

Это немного сложнее, но мы видим ту же структуру, что и в примере с одним модулем. После удаления из вывода Rome реализации модулей и мертвый код взаимодействия CommonJS, наши три исходных модуля будут встроены в одно закрывающее завершение:

(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 не предусмотрена минимизация в производстве, хотя его дизайн хорошо подходит для минимизации на уровне сборки. Мы можем пропустить приведенный выше вывод через 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 с теми же настройками, что и выше. Я рад сказать, что это привело к чему-то близкому к идеальному выводу: нет оборачивающих функций, нет мертвого кода, и он использует преимущества размера современного синтаксиса:

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 пока не поддерживает динамический импорт или разделение кода. Использование в коде операторов import() действительно обнаруживает импортированный модуль, но он встроен в пакет, как если бы это был статический импорт. Исходный оператор import() остается неизменным в сгенерированном выводе, что вызывает ошибку.

Еще неизвестно, как разделение кода и разбиение на фрагменты повлияют на качество вывода, поскольку оба зависят от доступа к переменным, заключенным в один пакет из другого. Я еще недостаточно знаком с Rome, чтобы даже предполагать, как это может выглядеть.

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

Если вы просто хотите взглянуть на то, что предлагает CLI Rome, вот вывод —help, который вы получите, не создавая его самостоятельно (хотя его очень быстро собрать!):

$ 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

Автор: Jason Miller

Источник: https://jasonformat.com

Редакция: Команда webformyself.