- PVSM.RU - https://www.pvsm.ru -
Это и последующие руководства проведут вас через процесс создания решения на основе проекта Discovery.js [1]. Наша цель — создать инспектор NPM-зависимостей, то есть интерфейс для исследования структуры node_modules
.
Примечание: Discovery.js находится на ранней стадии разработки, поэтому со временем что-то будет упрощаться и становиться полезнее. Если у вас есть идеи, как можно что-то улучшить, напишите нам [2].
Ниже вы найдёте обзор ключевых концепций Discovery.js. Изучить весь код руководства можно в репозитории на GitHub [3], или можете попробовать как это работает онлайн [4].
Прежде всего нам нужно выбрать проект для анализа. Это может быть свежесозданный проект или уже существующий, главное, чтобы он содержал node_modules
(объект нашего анализа).
Сначала установим основной пакет discoveryjs
и его консольные инструменты:
npm install @discoveryjs/discovery @discoveryjs/cli
Далее запускаем сервер Discovery.js:
> npx discovery
No config is used
Models are not defined (model free mode is enabled)
Init common routes ... OK
Server listen on http://localhost:8123
Если открыть в браузере http://localhost:8123
, то можем увидеть следующее:
Это режим без модели, то есть режим когда ничего не сконфигурировано. Но уже сейчас с помощью кнопки "Load data" можно выбрать любой JSON-файл, или просто перетащить его на страницу, и начать анализ.
Однако нам нужно нечто конкретное. В частности, нам нужно получить представление структуры node_modules
. Для этого добавим конфигурацию.
Как вы могли заметить, при запуске сервера вывелось сообщение No config is used
. Давайте создадим конфигурационный файл .discoveryrc.js
с таким содержимым:
module.exports = {
name: 'Node modules structure',
data() {
return { hello: 'world' };
}
};
Примечание: если вы создаете файл в текущей рабочей директории (то есть в корне проекта), то больше ничего не требуется. В противном случае нужно передать путь к файлу конфигурации с помощью опции --config
, либо задать путь в package.json
:
{
...
"discovery": "path/to/discovery/config.js",
...
}
Перезапустим сервер, чтобы конфигурация была применена:
> npx discovery
Load config from .discoveryrc.js
Init single model
default
Define default routes ... OK
Cache: DISABLED
Init common routes ... OK
Server listen on http://localhost:8123
Как видите, теперь созданный нами файл используется. И применяется описанная нами модель по умолчанию (Discovery может работать в режиме множества моделей, об этой возможности мы расскажем в следующих руководствах). Давайте посмотрим, что у нас изменилось в браузере:
Что тут можно увидеть:
name
используется в качестве заголовка страницы;data
отображается как основное содержимое страницы.Примечание: метод
data
должен возвращать данные или Promise, который разрешается в данные.
Основные настройки сделаны, можно двигаться дальше.
Давайте посмотрим на страницу произвольного отчёта (кликнинте Make report
):
На первый взгляд, это не сильно отличается от стартовой страницы… Но здесь можно всё менять! Например, мы можем легко воссоздать вид стартовой страницы:
Обратите внимание, как определяется заголовок: "h1:#.name"
. Это заголовок первого уровня с содержимым #.name
, которое является запросом Jora [5]. #
ссылается на контекст запроса. Чтобы посмотреть его содержимое, просто введите #
в редакторе запроса и воспользуйтесь отображением по умолчанию:
Теперь вы знаете, как можно получить ID текущей страницы, её параметры и другие полезные значения.
Сейчас мы используем в проекте заглушку вместо реальных данные, а нам нужны настоящие. Для этого создадим модуль и изменим значение data
в конфигурации (кстати, после этих изменений перезапускать сервер не обязательно):
module.exports = {
name: 'Node modules structure',
data: require('./collect-node-modules-data')
};
Содержимое collect-node-modules-data.js
:
const path = require('path');
const scanFs = require('@discoveryjs/scan-fs');
module.exports = function() {
const packages = [];
return scanFs({
include: ['node_modules'],
rules: [{
test: //package.json$/,
extract: (file, content) => {
const pkg = JSON.parse(content);
if (pkg.name && pkg.version) {
packages.push({
name: pkg.name,
version: pkg.version,
path: path.dirname(file.filename),
dependencies: pkg.dependencies
});
}
}
}]
}).then(() => packages);
};
Я использовал пакет @discoveryjs/scan-fs
, который упрощает сканирование файловой системы. Пример использования пакета описан в его readme, я взял этот пример за основу и доработал как нужно. Теперь у нас есть некоторая информация о содержимом node_modules
:
То что надо! И несмотря на то, что это обычный JSON, мы уже можем его проанализировать и сделать какие-то выводы. Например, с помощью попапа структуры данных можно выяснить количество пакетов и узнать, сколько из них имеют больше одного физического экземпляра (из-за разницы версий или проблем с их дедупликацией).
Не смотря на то, что у нас уже есть некоторые данные, нам нужно больше деталей. Например, хорошо бы знать, в какой физический экземпляр разрешается каждая из объявленных зависимостей определенного модуля. Однако работа над улучшением извлечения данных выходит за рамки этого руководства. Поэтому мы сделаем замену на пакет @discoveryjs/node-modules
(который также построен на основе @discoveryjs/scan-fs
) для извлечения данных и получим необходимые подробности относительно пакетов. В результате collect-node-modules-data.js
значительно упрощается:
const fetchNodeModules = require('@discoveryjs/node-modules');
module.exports = function() {
return fetchNodeModules();
};
Теперь информация о node_modules
выглядит так:
Как вы могли заметить, некоторые объекты описывающие пакеты содержат deps
— список зависимостей. У каждой зависимости есть поле resolved
, значение которого является ссылкой на физический экземпляр пакета. Такая ссылка — это значение path
одного из пакетов, оно является уникальным. Для разрешения ссылки на пакет нужно использовать дополнительный код (например, #.data.pick(<path=resolved>)
). И конечно же, было бы гораздо удобнее, если бы такие ссылки были бы уже разрешены в ссылки на объекты.
К сожалению, на этапе сбора данных мы не можем разрешать ссылки, поскольку это приведёт к циклическим связям, что создаст проблему передачи таких данных в виде JSON. Однако решение есть: это специальный скрипт prepare
. Он определяется в конфигурации и вызывается каждый раз при назначении новых данных для экземпляра Discovery. Начнём с конфигурации:
module.exports = {
...
prepare: __dirname + '/prepare.js', // Важно: значение это строка, путь к модулю
...
};
Определим prepare.js
:
discovery.setPrepare(function(data) {
// делаем что-то с data и/или с экземпляром discovery
});
В этом модуле мы задали функцию prepare
для экземпляра Discovery. Эта функция вызывается каждый раз перед применением данных к экземпляру Discovery. Это хорошее место для того, чтобы разрешить значения в ссылки на объекты:
discovery.setPrepare(function(data) {
const packageIndex = data.reduce((map, pkg) => map.set(pkg.path, pkg), new Map());
data.forEach(pkg =>
pkg.deps.forEach(dep =>
dep.resolved = packageIndex.get(dep.resolved)
)
);
});
Здесь мы создали индекс пакетов, в котором ключом является значение path
пакета (уникальное). Затем мы проходим по всем пакетам и их зависимостям, и заменяем в зависимостях значение resolved
ссылкой на объект пакета. Результат:
Теперь гораздо легче делать запросы к графу зависимостей. Вот так можно получить кластер зависимостей (то есть зависимости, зависимости зависимостей и т.д.) для конкретного пакета:
Неожиданная история успеха: изучая данные в ходе написания руководства, я обнаружил проблему в
@discoveryjs/cli
(с помощью запроса.[deps.[not resolved]]
), у которого была опечатка в peerDependencies. Проблема немедленно была исправлена [6]. Случай является наглядным примером, как помогают подобные инструменты.
Пожалуй настало время, чтобы показать на стартовой странице несколько чисел и пакеты с дублями.
Для начала нам нужно создать модуль страницы, например, pages/default.js
. Мы используем default
, поскольку это идентификатор для стартовой страницы, который мы можем переопределить (в Discovery.js можно переопределить очень многое). Начнём с чего-нибудь простого, например:
discovery.page.define('default', [
'h1:#.name',
'text:"Hello world!"'
]);
Теперь в конфигурации нужно подключить модуль страницы:
module.exports = {
name: 'Node modules structure',
data: require('./collect-node-modules-data'),
view: {
assets: [
'pages/default.js' // ссылка на модуль страницы
]
}
};
Проверяем в браузере:
Работает!
Теперь давайте выведем какие-нибудь счётчики. Для этого внесём изменения в pages/default.js
:
discovery.page.define('default', [
'h1:#.name',
{
view: 'inline-list',
item: 'indicator',
data: `[
{ label: 'Package entries', value: size() },
{ label: 'Unique packages', value: name.size() },
{ label: 'Dup packages', value: group(<name>).[value.size() > 1].size() }
]`
}
]);
Здесь мы определяем инлайновый список индикаторов. В качестве значения data
стоит запрос Jora, создающий массив записей. В качестве основы для запросов используется список пакетов (корень данных), так мы получили длину списка (size()
), количество уникальных имен пакетов (name.size()
) и количество имен пакетов, которые имеют дубли (group(<name>).[value.size() > 1].size()
).
Неплохо. Тем не менее, было бы лучше кроме чисел иметь ссылки на соответствующие выборки:
discovery.page.define('default', [
'h1:#.name',
{
view: 'inline-list',
data: [
{ label: 'Package entries', value: '' },
{ label: 'Unique packages', value: 'name' },
{ label: 'Dup packages', value: 'group(<name>).[value.size() > 1]' }
],
item: `indicator:{
label,
value: value.query(#.data, #).size(),
href: pageLink('report', { query: value, title: label })
}`
}
]);
В первую очередь мы изменили значение data
, теперь это обычный массив с некоторыми объектами. Также из запросов значений (value) убран метод size()
.
Помимо этого в представление indicator
добавлен подзапрос. Такого вида запросы создают новый объект для каждого элемента, в котором вычисляются value
и href
. Для value
выполняется запрос с использованием метода query()
, в который данные передаются из контекста, а потом к результату запроса применяется метод size()
. Для href
используется метод pageLink()
, который генерирует ссылку на страницу отчёта с конкретным запросом и заголовком. После всех этих изменений индикаторы стали кликабельными (обратите внимание, что их значения стали синего цвета) и более функциональными.
Чтобы сделать стартовую страницу более полезной, добавим таблицу с пакетами имеющие дубли.
discovery.page.define('default', [
// ... то же что и раньше
'h2:"Packages with more than one physical instance"',
{
view: 'table',
data: `
group(<name>)
.[value.size() > 1]
.sort(<value.size()>)
.reverse()
`,
cols: [
{ header: 'Name', content: 'text:key' },
{ header: 'Version & Location', content: {
view: 'list',
data: 'value.sort(<version>)',
item: [
'badge:version',
'text:path'
]
} }
]
}
]);
Для таблицы используются те же данные, что и для индикатора Dup packages
. Список пакетов был отсортирован по размеру группы в обратном порядке. Остальная настройка связана с колонками (кстати, обычно их не нужно настраивать). Для колонки Version & Location
мы определили вложенный список (отсортированный по версии), в котором каждый элемент представляет собой пару из номера версии и пути к экземпляру.
Сейчас у нас есть лишь общий обзор пакетов. Но было бы полезно иметь страницу с подробностями о конкретном пакете. Для этого создадим новый модуль pages/package.js
и определим новую страницу:
discovery.page.define('package', {
view: 'context',
data: `{
name: #.id,
instances: .[name = #.id]
}`,
content: [
'h1:name',
'table:instances'
]
});
В этом модуле мы определили страницу с идентификатором package
. В качестве исходного представления использовался компонент context
. Это невизуальный компонент, помогающий определять данные для вложенных отображений. Обратите внимание, что мы использовали #.id
для получения названия пакета, которое извлекается из URL вроде такого http://localhost:8123/#package:{id}
.
Не забываем включить новый модуль в конфигурацию:
module.exports = {
...
view: {
assets: [
'pages/default.js',
'pages/package.js' // вот так
]
}
};
Результат в браузере:
Не слишком впечатляет, но пока пойдёт. Более сложные отображения мы будем создавать в последующих руководствах.
Раз у нас уже есть страница пакета, хорошо бы иметь список всех пакетов. Для этого можно определить специальное представление – sidebar
, которое отображается в том случае, если его определить (по умолчанию не определено). Создадим новый модуль views/sidebar.js
:
discovery.view.define('sidebar', {
view: 'list',
data: 'name.sort()',
item: 'link:{ text: $, href: pageLink("package") }'
});
Теперь у нас есть список всех пакетов:
Выглядит хорошо. Но с фильтром было бы ещё лучше. Расширим определение sidebar
:
discovery.view.define('sidebar', {
view: 'content-filter',
content: {
view: 'list',
data: 'name.[no #.filter or $~=#.filter].sort()',
item: {
view: 'link',
data: '{ text: $, href: pageLink("package"), match: #.filter }',
content: 'text-match'
}
}
});
Здесь мы обернули список в компонент content-filter
, которое преобразует вводимое значение в поле ввода в регулярное выражения (или null
, если поле пустое), и сохраняет его в качестве значения filter
в контексте (название можно менять опцией name
). Также для фильтрации данных для списка мы применили #.filter
. Наконец, мы применили отображение ссылок, чтобы подсвечивать совпадающие части с помощью text-match
. Результат:
В случае, если вам не нравится оформление по умолчанию, можете настроить стили по своему желанию. Допустим, вы хотите поменять ширину боковой панели, для этого нужно создать файл стиля (скажем, views/sidebar.css
):
.discovery-sidebar {
width: 300px;
}
И добавить в конфигурацию ссылку на этот файл, так же как на JavaScript модули:
module.exports = {
...
view: {
assets: [
...
'views/sidebar.css', // в assets можно задавать и *.css тоже
'views/sidebar.js'
]
}
};
Последняя глава этого руководства посвящена ссылкам. Ранее, с помощью метода pageLink()
мы делали ссылку на страницу пакета. Но помимо ссылки нужно еще задавать и текст ссылки. Но как бы нам делать это проще?
Чтобы упростить работу ссылками, нам нужно определить правило генерации ссылок. Это лучше сделать в скрипте prepare
:
discovery.setPrepare(function(data) {
...
const packageIndex = data.reduce(
(map, item) => map
.set(item, item) // key is item itself
.set(item.name, item), // and `name` value
new Map()
);
discovery.addEntityResolver(value => {
value = packageIndex.get(value) || packageIndex.get(value.name);
if (value) {
return {
type: 'package',
id: value.name,
name: value.name
};
}
});
});
Мы добавили новую карту (индекс) пакетов и использовали её для ресолвера сущностей. Ресолвер сущностей пытается, по возможности, преобразовать переданное ему значение в дескриптор сущности. Дескриптор содержит:
type
— тип сущностиid
— уникальная ссылка на экземпляр сущности, используемая в ссылках в качестве IDname
— используется в качестве текста ссылкиНаконец, нужно присвоить этот тип определённой странице (ссылка ведь должна куда-то вести, не так ли?).
discovery.page.define('package', {
...
}, {
resolveLink: 'package' // привязываем тип сущности `package` к этой странице
});
Первое следствие этих изменений заключается в том, что некоторые значения в представлении struct
теперь помечаются ссылкой на страницу пакета:
И теперь также можно применить компонент auto-link
к объекту или имени пакета:
И, как пример, можно немного переработать боковую панель:
// было раньше
item: {
view: 'link',
data: '{ text: $, href: pageLink("package"), match: #.filter }',
content: 'text-match'
},
// с использованием `auto-link`
item: {
view: 'auto-link',
content: 'text-match:{ text, match: #.filter }'
}
Теперь у вас есть базовое представление о ключевых концепциях Discovery.js [1]. В следующих руководствах мы подробнее рассмотрим затронутые темы.
Посмотреть весь исходный код руководства можно в репозитории на GitHub [3] или попробовать как это работает онлайн [4].
Подписывайтесь на твиттер проекта @js_discovery [7], чтобы быть в курсе новостей проекта!
Автор: Константин Лебедев
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/331377
Ссылки в тексте:
[1] Discovery.js: https://github.com/discoveryjs
[2] напишите нам: https://github.com/discoveryjs/discovery/issues
[3] в репозитории на GitHub: https://github.com/discoveryjs/quick-start-tutorial
[4] как это работает онлайн: https://discoveryjs.github.io/quick-start-tutorial/
[5] Jora: https://github.com/discoveryjs/jora
[6] была исправлена: https://github.com/discoveryjs/discovery-cli/commit/3f300cedb7e8217d339cb0ecd7aa57e3e53ecd91
[7] @js_discovery: https://twitter.com/js_discovery
[8] Источник: https://habr.com/ru/post/469115/?utm_source=habrahabr&utm_medium=rss&utm_campaign=469115
Нажмите здесь для печати.