Hello, my name is Dmitry Karlovsky, and I ... adore MAM. M AM governs A gnostic M moduli, saving me from the lion's share of the routine.
The Agnostic Module , unlike the traditional one, is not a source file, but a directory within which there can be source codes in various languages: program logic on JS
/ TS
, tests for it on TS
/ JS
, composition of components on view.tree
, styles on CSS
, localization in locale=*.json
, images, etc., and so on. If desired, it is not difficult to fasten support for any other language. For example, Stylus for writing styles, or HTML for describing templates.
Dependencies between modules are tracked automatically by analyzing the sources. If the module is turned on, then it is turned on entirely - each source of the module is transpiled and falls into the corresponding bundle: scripts - separately, styles - separately, tests - separately. For different platforms - their own bundles: for nodes - their own, for the browser - their own.
Full automation, the lack of configuration and boilerplate, the minimum size of bundles, automatic extrusion of dependencies, the development of hundreds of alienable libraries and applications in the same code base without pain and suffering. Wow, what a drug addiction! Remove from the monitors pregnant, faint of heart, children and welcome to the submarine!
MAM is a bold experiment to radically change the way the code is organized and how it is handled. Here are the basic principles:
Conventions instead of configuring. Reasonable, simple and universal agreements allow you to automate the entire routine, while maintaining convenience and uniformity between different projects.
Infrastructure separately, code separately. It is not uncommon to develop dozens or even hundreds of libraries and applications. Do not deploy the same infrastructure assembly, development, deployment and TP for each of them. It is enough to set it once and then rivet applications like patties.
Do not pay for what you do not use. You use some kind of module - it is included in the bundle with all its dependencies. Do not use - does not turn on. The smaller the modules, the greater the granularity and less extra code in the bundle.
Minimum excess code. Breaking code into modules should be as easy as writing all code in one file. Otherwise, the developer will be lazy to break large modules into small ones.
No version conflicts. There is only one version - current. There is no need to spend resources on support of old versions if it is possible to spend them on actualization of the last.
Keep your finger on the pulse. The fastest possible feedback regarding incompatibilities will not allow the code to rot.
The easiest way is the surest one. If the right way requires additional efforts, then be sure that no one will go.
We open the first available project using a modern system of modules: The module is less than 300 lines, 30 of them are imports.
But these are flowers: A function of 9 lines requires 8 imports.
And my favorite: Not a single line of useful code. 20 lines of shifting values ​​from a heap of modules to one, then to import from one module, and not from twenty.
All this is a boilerplate, which leads to the fact that developers are lazy to allocate small pieces of code into separate modules, preferring large modules to small ones. And even if they are not lazy, it turns out either a lot of code for importing small modules, or special modules that import many modules into themselves and export them all in a crowd.
All this leads to low granularity of the code and bloat of unused code sizes, which are lucky to be close to the one used. This problem for JS is at least trying to be solved by complicating the assembly pipeline by adding the so-called "tree-shaking", cutting out the excess from what you imported. This slows down the assembly, but cuts away not everything.
Idea: What if we don’t import, but just take and use, and the assembler already understands what needs to be imported?
Modern IDEs can automatically generate imports for the entities you use. If an IDE can do this, then what's stopping the assembler from doing this? It is enough to have a simple naming and file arrangement that is convenient for the user and understandable to the machine. In PHP, there has long been such a standard convention: PSR-4 . MAM introduces the same for .ts and .jam.js files: names starting with $ are a Fully Qualified Name of a global entity whose code is loaded along the path obtained from the FQN by replacing separators with slashes. A simple example of two modules:
my / alert / alert.ts
const $my_alert = alert // FQN
my / app / app.ts
$my_alert( 'Hello!' ) // , /my/alert/
A whole module from one line - what could be simpler? The result is not long in coming: the simplicity of creating and using modules leads to minimization of their size. As a result - to maximize the granularity. And like a cherry - minimizing the size of bundles without any tree-shaking.
A good example is the JSON / mol / data family of validation modules. If you use the $mol_data_integer
function anywhere in your code, then the /mol/data/integer
and /mol/data/number
modules on which $mol_data_integer
depends will be included in the bundle. But, for example, the /mol/data/email
collector will not even read from the disk, since nobody depends on it.
Since we started to kick Angular, we will not stop. What do you think, where to look for the declaration of the applyStyles
function? You would never guess in /packages/core/src/render3/styling_next/bindings.ts
. The ability to put anything anywhere leads to the fact that in each project we observe a unique file layout system, often incapable of any logic. And if the IDE often saves the "jump to the definition", then viewing the code on the githaba or reviewing the pullrexest is deprived of this possibility.
Idea: What if entity names strictly correspond to their location?
To place the code in the file / /angular/packages/core/src/render3/stylingNext/bindings.ts
. But the names in the code you want to see short and concise, so the developer will try to exclude all unnecessary from the name, leaving only the important: $angular_render3_applyStyles
. And it will be located respectively in /angular/render3/applyStyles/applyStyles.ts
.
Notice how MAM uses the weaknesses of the developers to achieve the desired result: each entity has a short, globally unique name that can be used in any context. For example, in the messages of komitov these names allow you to quickly and accurately catch what they are:
73ebc45e517ffcc3dcce53f5b39b6d06fc95cae1 $mol_vector: range expanding support 3a843b2cb77be19688324eeb72bd090d350a6cc3 $mol_data: allowed transformations 24576f087133a18e0c9f31e0d61052265fd8a31a $mol_data_record: support recursion
Or, say, you want to find all the references to the $ mol_fiber module on the Internet - to make it easier than ever, thanks to FQN.
Let's write in one file 7 lines of simple code:
export class Foo { get bar() { return new Bar(); } } export class Bar extends Foo {} console.log(new Foo().bar);
Despite the cyclical dependency, it works correctly. We divide it into 3 files:
my / foo.js
import { Bar } from './bar.js'; export class Foo { get bar() { return new Bar(); } }
my / bar.js
import { Foo } from './foo.js'; export class Bar extends Foo {}
my / app.js
import { Foo } from './foo.js'; console.log(new Foo().bar);
Opa, ReferenceError: Cannot access 'Foo' before initialization
. What kind of nonsense? To fix this, our app.js
needs to know that foo.js
depends on bar.js
Therefore, we must first import bar.js
, which imports foo.js
After that we can import foo.js
without error:
my / app.js
import './bar.js'; import { Foo } from './foo.js'; console.log(new Foo().bar);
What browsers, what NodeJS, what Webpack, what Parcel - all of them work crookedly with circular dependencies. And it would be okay if they simply forbade them - one could immediately complicate the code so that there were no cycles. But they can work normally, and then bang, and give an incomprehensible error.
Idea: What if during assembly we just stick the files together in the correct order, as if all the code were originally written in one file?
Let's divide the code using the principles of MAM:
my / foo / foo.ts
class $my_foo { get bar() { return new $my_bar(); } }
my / bar / bar.ts
class $my_bar extends $my_foo {}
my / app / app.ts
console.log(new $my_foo().bar);
All the same 7 lines of code that were originally. And they just work without additional shamanism. The thing is that the collector understands that the dependence of my/bar
on my/foo
more rigid than my/foo
on my/bar
. This means that these modules should be included in the bundle in this order: my/foo
, my/bar
, my/app
.
How does a collector understand this? Now the heuristics is simple - according to the number of indents in the line in which the dependency is found. Note that the stronger dependency in our example has zero indent, and the weak one double.
It so happens that for different things we have different languages ​​for these different things sharpened. The most common ones are: JS, TS, CSS, HTML, SVG, SCSS, Less, Stylus. Each has its own system of modules that does not interact with other languages. What to say about 100,500 types of more specific languages. As a result, in order to connect a component, you have to separately connect its scripts, separate styles, separately register templates, separately configure the static files needed for it, etc., and so on.
Webpack thanks to loaders trying to solve this problem. But his entry point is a script that already includes files in other languages. And if we do not need a script? For example, we have a module with beautiful styles for tablets and we want them to have some colors in a light theme, and others in a dark theme:
.dark-theme table { background: black; } .light-theme table { background: white; }
At the same time, if we depend on the topic, then the script should be loaded, which will set the desired topic depending on the time of day. That is, CSS actually depends on JS.
Idea: What if the modular system is language independent?
Since in MAM the modular system is separated from languages, the dependencies can be cross-language. CSS may depend on JS, which may depend on TS, which may depend on another JS. This is achieved due to the fact that source codes show dependencies on modules, and the modules are connected entirely and can contain source codes in any languages. In the case of an example with themes, it looks like this:
/my/table/table.css
/* , /my/theme */ [my_theme="dark"] table { background: black; } [my_theme="light"] table { background: white; }
/my/theme/theme.js
document.documentElement.setAttribute( 'my_theme' , ( new Date().getHours() + 15 ) % 24 < 12 ? 'light' : 'dark' , )
Using this technique, by the way, you can implement your Modernizr , but without 300 checks you don't need, because only those checks that your CSS really depends on will be included in the bundle.
Typically, the entry point for assembling a bundle is some kind of file. In the case of a webpack this is js. If you are developing a lot of alienable libraries and applications, then you also need a lot of bundles. And for each bundle you need to create a separate entry point. In the case of Parcel, the entry point is HTML, which for applications will have to be created anyway. But for libraries it is somehow not very suitable.
Idea: What if any module can be assembled into an independent bundle without prior preparation?
Let's build the latest version of the $ mol_build MAM project builder:
mam mol/build
And now run this collector and let it assemble itself again to make sure that it is still able to assemble itself:
node mol/build/-/node.js mol/build
Although, no, let's along with the assembly, we ask it to also run the tests:
node mol/build/-/node.test.js mol/build
And if everything went well, we will publish the result in NPM:
npm publish mol/build/-
As you can see, when assembling a module, a subdirectory with the name -
created -
and all the artifacts of the assembly are placed there. Let's go through the files that can be found there:
web.dep.json
- all information on the dependency graphweb.js
- browser scriptsweb.js.map
- sorsmapu for himweb.esm.js
- it's in the form of an es-moduleweb.esm.js.map
- and for it sorsmapweb.test.js
- test bundleweb.test.js.map
- and for sorsmapu testsweb.d.ts
- bundle with the types of everything that is in the script bundleweb.css
- style bundleweb.css.map
- and sorsmaps for itweb.test.html
- entry point to run execution tests in the browserweb.view.tree
- declarations of all components included in the bundle view.treeweb.locale=*.json
- bundles with localized texts, for each detected language its own bundlepackage.json
- allows you to immediately publish the assembled module in NPMnode.dep.json
- all information about the dependency graphnode.js
- bandl scripts for nodesnode.js.map
- sorsmapy for itnode.esm.js
- it's in the form of an es-modulenode.esm.js.map
- and for it sorsmapnode.test.js
- the same bundle, but also with testsnode.test.js.map
- and for it sorsmapnode.d.ts
- a bundle with the types of everything in the script bundlenode.view.tree
- declarations of all components included in the bundle view.treenode.locale=*.json
- bundles with localized texts, for each detected language its own bundleStatics are simply copied along with the paths. As an example, take an application that displays its own source code . Its sources are here:
/mol/app/quine/quine.view.tree
/mol/app/quine/quine.view.ts
/mol/app/quine/index.html
/mol/app/quine/quine.locale=ru.json
Unfortunately, in general, the collector cannot know that we will need these files in runtime. But we can tell him this by putting a special file next to it:
/mol/app/quine/quine.meta.tree
deploy \/mol/app/quine/quine.view.tree deploy \/mol/app/quine/quine.view.ts deploy \/mol/app/quine/index.html deploy \/mol/app/quine/quine.locale=ru.json
As a result of the build /mol/app/quine
, they will be copied in the following ways:
/mol/app/quine/-/mol/app/quine/quine.view.tree
/mol/app/quine/-/mol/app/quine/quine.view.ts
/mol/app/quine/-/mol/app/quine/index.html
/mol/app/quine/-/mol/app/quine/quine.locale=ru.json
Now the directory /mol/app/quine/-
can be put on any static hosting and the application will be fully operational.
JS can be executed both on the client and on the server. And how cool it is to write one code and it will work everywhere. However, sometimes the implementation of the same thing on the client and the server is completely different. And I want one implementation to be used for the node, for example, and another for the browser.
Idea: What if the file's purpose is reflected in its name?
MAM uses a tag system in file names. For example, the $mol_state_arg
module provides access to user-defined application parameters. In the browser, these parameters are set via the address bar. And in the node - through the command line arguments. $mol_sate_arg
abstracts the rest of the application from these nuances by implementing both options with a single interface, placing them in files:
Sources not marked with these tags are included regardless of the target platform.
A similar situation is observed with the tests - they want to be stored next to the other sources, but I do not want to include them in the bundle that will go to the end user. Therefore, tests are also marked with a separate tag:
Tags can be parametric. For example, with each module can go texts in various languages ​​and they should be included in the appropriate language bundles. The text file is a regular JSON dictionary named with a locale in the name:
Finally, what if we want to place files side by side, but we want the builder to ignore them and not automatically include them in the bundle? It is enough to add any non-alphanumeric character at the beginning of their name. For example:
First, Google released AngularJS and published it in NPM as angular
. Then he created a completely new framework with a similar name - Angular and published it under the same name, but version 2. Now these two frameworks develop independently. Only one API breaking change occurs between major versions. And the other - between the minor . And since it is impossible to put two versions of the same dependency on the same level, then there can be no question of a smooth transition, when two versions of the library coexist at the same time for some time at the same time.
It seems the Angulyar team has already attacked all possible rakes. And here's another one: the framework code is broken into several large modules. At first they versioned them independently, but very quickly even they themselves began to get confused about which versions of the modules were compatible with each other, what to speak of common developers. Therefore, Angular has switched to pass-through versioning , where the major version of the module can change even without any changes in the code. Supporting multiple versions of multiple modules is a big problem for both the maintainers themselves and the ecosystem as a whole. After all, a lot of resources of all community members are spent on ensuring compatibility with already outdated modules.
The beautiful idea of Semantic Versioning is broken about the harsh reality - you never know if something will break if you change a minor version or even a patch version . Therefore, in many projects they fix a specific version of the dependency. However, such a fixation does not affect transitive dependencies, which can be attracted by the latest version when installed from scratch, but can remain the same if they are already. This confusion leads to the fact that you can never rely on a fixed version and you regularly need to check compatibility with current versions (at least transitive) dependencies.
What about lock files ? If you are developing a library that is installed through dependencies, the lock file will not help you, because it will be ignored by the package manager. For the final application, the lock file will give you a so-called “reproducible build”. But let's be honest. How many times do you need to build the final application from the same source? Exactly once. Receiving at the output, independent of any NPM, an assembly artifact: an executable binary, a docker-container or just an archive with everything you need to run the code. I hope you do not do npm install
on the prod?
Some find the use of lock-files in that the CI server collects exactly what the developer commits. But wait, the developer himself can just collect on his local machine. Moreover, he must do this to make sure that he has not broken anything. Continuous Integration is not only and not so much about the assembly, as about checking the compatibility of what one developer wrote with what someone else wrote. The concept of CI is to detect incompatibilities as soon as possible, and as a result, to start work to eliminate them as soon as possible.
, , . , Angular@4 ( 3). , , " " " ". Angular@4 , Angular@5. Angular@6, . Angular TypeScript . . , 2 , … , business value , , , , .
, , , , 2 . : , — , — . 3 React, 5 jQuery, 7 lodash.
: — ?
. - . , . , . , . , . , . , , . : issue, , workaround, pull request, , . , , . . .
, . , , . . . : , , -. - — . , , - . , , , NPM . , . .
, ? — . mobx
, mobx2
API . — , : , . mobx
mobx2
, API. API, .
. — . , :
var pages_count = $mol_atom2_sync( ()=> $lib_pdfjs.getDocument( uri ).promise ).document().numPages
mol_atom2_sync
lib_pdfjs
, :
npm install mol_atom2_sync@2.1 lib_pdfjs@5.6
, , — , . ? — , *.meta.tree
, :
/.meta.tree
pack node git \https://github.com/nin-jin/pms-node.git pack mol git \https://github.com/eigenmethod/mol.git pack lib git \https://github.com/eigenmethod/mam-lib.git
. .
MAM — NPM . , — . , , NPM .
NPM , $node. , - -:
/my/app/app.ts
$node.portastic.find({ min : 8080 , max : 8100 , retrieve : 1 }).then( ( ports : number[] ) => { $node.express().listen( ports[0] ) })
, . - lib
NPM . , NPM- pdfjs-dist
:
/lib/pdfjs/pdfjs.ts
namespace $ { export let $lib_pdfjs : typeof import( 'pdfjs-dist' ) = require( 'pdfjs-dist/build/pdf.min.js' ) $lib_pdfjs.disableRange = true $lib_pdfjs.GlobalWorkerOptions.workerSrc = '-/node_modules/pdfjs-dist/build/pdf.worker.min.js' }
/lib/pdfjs/pdfjs.meta.tree
deploy \/node_modules/pdfjs-dist/build/pdf.worker.min.js
, .
. create-react-app
angular-cli
, . , , eject
. . , , .
: ?
MAM . .
MAM MAM , :
git clone https://github.com/eigenmethod/mam.git ./mam && cd mam npm install npm start
8080 . , — MAM.
( — acme
) ( — hello
home
):
/acme/acme.meta.tree
pack hello git \https://github.com/acme/hello.git pack home git \https://github.com/acme/home.git
npm start
:
npm start acme/hello acme/home
. — . , , . — : https://t.me/mam_mol
Source: https://habr.com/ru/post/456288/
All Articles