Разработка

Rome: что умеет новый универсальный инструмент JavaScript

Rome: что умеет новый универсальный инструмент JavaScript главное изображение

Создатель Yarn и Babel Себастиан Маккензи (Sebastian McKenzie) долго работал над универсальным инструментом для JavaScript-разработчиков. 26 февраля он представил проект Rome. «Все дороги ведут в Рим», — таков девиз создателей инструмента.

Что такое 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. Мнение автора оригинальной публикации может не совпадать с мнением администрации Хекслета.

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Javascript, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →