Discovery.js Guide: Quick Start

This and the following guides will guide you through the process of creating a solution based on the Discovery.js project. Our goal is to create an NPM dependency inspector, that is, an interface for examining the structure of node_modules .



Note: Discovery.js is at an early stage of development, so over time, something will simplify and become more useful. If you have ideas on how to improve something, write to us .

annotation


Below you will find an overview of the key concepts of Discovery.js. You can learn all the code in the manual in the repository on GitHub , or you can try how it works online .


Initial conditions


First of all, we need to choose a project for analysis. This can be a freshly created project or an existing one, the main thing is that it contains node_modules (the object of our analysis).


First, install the discoveryjs core package and its console tools:


 npm install @discoveryjs/discovery @discoveryjs/cli 

Next, launch the Discovery.js server:


 > 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 

If you open http://localhost:8123 in your browser, you can see the following:


Discovery without configuration


This is a mode without a model, that is, a mode when nothing is configured. But now, using the "Load data" button, you can select any JSON file, or simply drag it onto the page and start the analysis.


However, we need something specific. In particular, we need to get a view of the node_modules structure. To do this, add the configuration.


Add configuration


As you may have noticed, the message No config is used displayed when the server started. Let's create a .discoveryrc.js configuration file with the following contents:


 module.exports = { name: 'Node modules structure', data() { return { hello: 'world' }; } }; 

Note: if you create a file in the current working directory (that is, in the root of the project), then nothing else is required. Otherwise, you need to pass the path to the configuration file using the --config option, or set the path in package.json :


 { ... "discovery": "path/to/discovery/config.js", ... } 

Restart the server so that the configuration is applied:


 > 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 

As you can see, now the file we created is used. And the default model described by us is applied (Discovery can work in the mode of many models, we will talk about this feature in the following manuals). Let's see what has changed in the browser:


With basic configuration


What can be seen here:



Note: the data method must return data or Promise, which resolves to data.

Basic settings are made, you can move on.


Context


Let's look at a custom report page (click Make report ):


Report Page


At first glance, this is not very different from the start page ... But here you can change everything! For example, we can easily recreate the appearance of the start page:


Recreating the start page


Notice how the header is defined: "h1:#.name" . This is the first level header with the contents of #.name , which is a Jora request. # refers to the request context. To view its contents, simply enter # in the query editor and use the default display:


Context values


Now you know how you can get the ID of the current page, its parameters and other useful values.


Data collection


Now we use a stub in the project instead of real data, but we need real data. To do this, create a module and change the data value in the configuration (by the way, after these changes it is not necessary to restart the server):


 module.exports = { name: 'Node modules structure', data: require('./collect-node-modules-data') }; 

The contents of 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); }; 

I used the @discoveryjs/scan-fs package, which simplifies file system scanning. An example of using the package is described in its readme, I took this example as a basis and finalized as needed. Now we have some information about the contents of node_modules :


Data collected


What you need! And despite the fact that this is ordinary JSON, we can already analyze it and draw some conclusions. For example, using the popup of the data structure, you can find out the number of packets and find out how many of them have more than one physical instance (due to the difference in versions or problems with their deduplication).


Data structure research


Despite the fact that we already have some data, we need more details. For example, it would be nice to know which physical instance resolves each of the declared dependencies of a particular module. However, work on improving data extraction is beyond the scope of this guide. Therefore, we will replace it with the @discoveryjs/node-modules package (which is also based on @discoveryjs/scan-fs ) to retrieve the data and get the necessary details about the packages. As a result, collect-node-modules-data.js greatly simplified:


 const fetchNodeModules = require('@discoveryjs/node-modules'); module.exports = function() { return fetchNodeModules(); }; 

Now the information about node_modules looks like this:


New data structure


Preparation script


As you may have noticed, some objects describing packages contain deps - a list of dependencies. Each dependency has a resolved field, the value of which is a reference to a physical instance of the package. Such a link is the path value of one of the packages, it is unique. To resolve the link to the package, you need to use additional code (for example, #.data.pick(<path=resolved>) ). And of course, it would be much more convenient if such links were already resolved into object references.


Unfortunately, at the stage of data collection, we cannot resolve the links, as this will lead to circular connections, which will create the problem of transferring such data in the form of JSON. However, there is a solution: this is a special prepare script. It is defined in the configuration and called each time a new data is assigned to the Discovery instance. Let's start with the configuration:


 module.exports = { ... prepare: __dirname + '/prepare.js', // :   ,    ... }; 

Define prepare.js :


 discovery.setPrepare(function(data) { //  -  data /   discovery }); 

In this module, we defined the prepare function for the Discovery instance. This function is called each time before applying data to the Discovery instance. This is a good place to allow values ​​in object references:


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

Here we have created a package index in which the key is the package path value (unique). Then we go through all the packages and their dependencies, and in the dependencies we replace the resolved value with a reference to the package object. Result:


Converted deps.resolved


Now it is much easier to make dependency graph queries. This is how you can get a cluster of dependencies (i.e. dependencies, dependency dependencies, etc.) for a specific package:


Dependency Cluster Example


An unexpected success story: while studying the data during the writing of the manual, I found a problem in @discoveryjs/cli (using the query .[deps.[not resolved]] ), which had a typo in peerDependencies. The problem was fixed immediately. The case is a good example of how such tools help.

Perhaps the time has come to show on the start page several numbers and packages with takes.


Customize Start Page


First we need to create a page module, for example, pages/default.js . We use default , because this is the identifier for the start page, which we can override (in Discovery.js, you can override a lot). Let's start with something simple, for example:


 discovery.page.define('default', [ 'h1:#.name', 'text:"Hello world!"' ]); 

Now in the configuration you need to connect the page module:


 module.exports = { name: 'Node modules structure', data: require('./collect-node-modules-data'), view: { assets: [ 'pages/default.js' //     ] } }; 

Check in the browser:


Overridden start page


Works!


Now let's get some counters. To do this, make changes to 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() } ]` } ]); 

Here we define an inline list of indicators. The data value is a Jora query that creates an array of records. The list of packages (data root) is used as the basis for queries, so we get the list length ( size() ), the number of unique package names ( name.size() ) and the number of package names that have duplicates ( group(<name>).[value.size() > 1].size() ).


Add indicators to the start page


Not bad. Nevertheless, it would be better to have, in addition to numbers, links to the corresponding samples:


 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 }) }` } ]); 

First of all, we changed the value of data , now it is a regular array with some objects. Also, the size() method has been removed from value requests.


In addition, a subquery has been added to the indicator view. These types of queries create a new object for each element in which value and href are calculated. For value , a query is executed using the query() method, into which data is transferred from the context, and then the size() method is applied to the query result. For href , the pageLink() method is used, which generates a link to the report page with a specific request and header. After all these changes, the indicators became clickable (note that their values ​​have turned blue) and more functional.


Clickable indicators


To make the start page more useful, add a table with packages that have duplicates.


 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' ] } } ] } ]); 

The table uses the same data as the Dup packages indicator. The list of packages was sorted by group size in reverse order. The rest of the setup is related to the columns (by the way, usually they do not need to be tuned). For the Version & Location column, we defined a nested list (sorted by version), in which each element is a pair of the version number and the path to the instance.


Added a table with packages that have more than one physical instance


Package Page


Now we have only a general overview of packages. But it would be useful to have a page with details about a particular package. To do this, create a new module pages/package.js and define a new page:


 discovery.page.define('package', { view: 'context', data: `{ name: #.id, instances: .[name = #.id] }`, content: [ 'h1:name', 'table:instances' ] }); 

In this module, we defined the page with the identifier package . The context component was used as the initial representation. This is a non-visual component that helps you define data for nested mappings. Note that we used #.id to get the name of the package, which is retrieved from a URL like this http://localhost:8123/#package:{id} .


Do not forget to include the new module in the configuration:


 module.exports = { ... view: { assets: [ 'pages/default.js', 'pages/package.js' //   ] } }; 

Result in the browser:


Package Page Example


Not too impressive, but for now. We will create more complex mappings in subsequent manuals.


Side panel


Since we already have a package page, it would be nice to have a list of all packages. To do this, you can define a special view - sidebar , which is displayed if defined (not defined by default). Create a new module views/sidebar.js :


 discovery.view.define('sidebar', { view: 'list', data: 'name.sort()', item: 'link:{ text: $, href: pageLink("package") }' }); 

Now we have a list of all the packages:


Added sidebar


Looks good. But with a filter it would be even better. We expand the definition of 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' } } }); 

Here we wrapped the list in a content-filter component that converts the input value in the input field to regular expressions (or null if the field is empty) and saves it as a filter value in the context (the name can be changed with the name option). We also used #.filter to filter the data for the list. Finally, we used link mapping to highlight matching parts using text-match . Result:


Filter list


In case you do not like the default design, you can customize the styles as you wish. Let's say you want to change the width of the sidebar, for this you need to create a style file (say, views/sidebar.css ):


 .discovery-sidebar { width: 300px; } 

And add a link to this file in the configuration, as well as to JavaScript modules:


 module.exports = { ... view: { assets: [ ... 'views/sidebar.css', //  assets    *.css  'views/sidebar.js' ] } }; 

AutoLinks


The final chapter of this guide is devoted to links. Earlier, using the pageLink() method, we made a link to the package page. But in addition to the link, you must also set the link text. But how would we make it easier?


To simplify the work of links, we need to define a rule for generating links. This is best done in the prepare script:


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

We added a new map (index) of packages and used it for entity resolver. The entity resolver tries, if possible, to convert the value passed to it into the entity descriptor. The descriptor contains:



Finally, you need to assign this type to a specific page (the link should lead somewhere, right?).


 discovery.page.define('package', { ... }, { resolveLink: 'package' //    `package`    }); 

The first consequence of these changes is that some values ​​in the struct view are now marked with a link to the package page:


AutoLinks in struct


And now you can also apply the auto-link component to an object or package name:


Using auto-link


And, as an example, you can slightly rework the sidebar:


  //   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 }' } 

Conclusion


You now have a basic understanding of the key concepts of Discovery.js . In the following guides we will take a closer look at the topics covered.


You can view the entire source code of the manual in the repository on GitHub or try how it works online .


Follow @js_discovery on Twitter to keep up with the latest news!



Source: https://habr.com/ru/post/469115/


All Articles