modular build [options] [packages...]
Search workspaces based on their name
field in the package.json
and build:
- Modular packages them according to their respective
modular.type
. - Non-Modular packages (i.e. packages without a
modular
configuration) only if they have abuild
script, by runningyarn build
on the package’s workspace.
Packages are always built in order of dependency (e.g. if a package a
depends on a package b
, b
is built first). Packages may be built concurrently when possible. The maximum concurrency level defaults to the number of CPUs available on the machine, but can be set by the user (see the --concurrencyLevel
option).
The output directory for built artifacts is dist/
, which has a flat structure of modular package names. Each built app/view/package is added to the dist/
as its own folder.
When packages
is empty and no selective options have been specified (for example when running yarn modular build
), all packages in the monorepo will be built. When packages
contains one or more non-existing package name, the non-existing packages will be ignored without an error. If any package or selective option have been defined but the final set of regular expressions is empty, Modular will write a message to stdout
and exit with code 0
.
For views and packages, package names are transformed to Param case
(e.g. this-is-param-case) in dist/
(i.e. modular build @scoped/package-a
will output built artifacts into dist/scoped-package-a
)
Options:
--private
: Allows the building of private packages.
--preserve-modules
: Preserve module structure in generated modules.
--changed
: Build only the packages whose workspaces contain files that have changed. Files that have changed are calculated comparing the current state of the repository with the branch specified by compareBranch
or, if compareBranch
is not set, with the default git branch.
--compareBranch <branchName>
: Specify the comparison branch used to determine which files have changed when using the changed
option. If this option is used without changed
, the command will fail.
--descendants
: Build the packages specified by the [packages...]
argument and/or the --changed
option and additionally build all their descendants (i.e. the packages they directly or indirectly depend on) in dependency order.
--ancestors
: Build the packages specified by the [packages...]
argument and/or the --changed
option and additionally build all their ancestors (i.e. the packages that have a direct or indirect dependency on them) in dependency order.
--concurrencyLevel <level>
: Limit the concurrency of build processes that are executed in different processes within build batches. 0 or 1 means no concurrency. If not specified, this option defaults to the number of logical CPUs available on the machine, or 1 if that information is not available.
Dependency selection and build order examples
We’ll be using this package manifests in our Modular monorepo for the following examples:
{
"name": "a",
"dependencies": {
"b": "*",
"c": "*",
"react": ">16.8.0",
// ...
}
}
{
"name": "b",
"dependencies": {
"c": "*",
"react": ">16.8.0",
// ...
}
}
{
"name": "c",
"dependencies": {
"d": "*",
// ...
}
}
{
"name": "d",
"dependencies": {}
}
{
"name": "e",
"dependencies": {
"a": "*",
// ...
}
}
Which internally are filtered into this set of WorkspaceDependencyObject
s:
{
a: { workspaceDependencies: ['b', 'c'] },
b: { workspaceDependencies: ['c'] },
c: { workspaceDependencies: ['d'] },
d: { workspaceDependencies: undefined },
e: { workspaceDependencies: ['a'] }
}
Example: local workflow with descendants
Let’s say we just pulled an update to our monorepo and we want to work on workspace b
. To be able to work with the last modifications we pulled, we want to build b
and all the other workspaces that b
depends on (descendants), either directly or indirectly. We can tell Modular that we want to build b
and its all descendants by using this command:
modular build b --descendants
Modular will first select all the descendants of b
(according to the previous graph: c
because it’a direct dependency and d
because it’s a dependency of c
), then build them in the correct build order, where workspaces depended on are built before workspaces that depend on them, recursively. In this example:
d
gets built first, because it has no dependenciesc
can now get built, because it only depends ond
, that got built in the previous step.b
can now get built, because it only depends onc
, that got built in the previous step.
Example: incremental builds with ancestors
Let’s suppose we’re building a PR of our monorepository on a CI pipeline, and we want to incrementally build the workspaces that have code changes compared to the base branch. Since those workspaces will generate new, different build artefacts, we can’t just build them and call it a day; we also need to re-build all the workspaces that depend on the changed workspaces, and those who depend on them, and so on. In other words, we need a way to tell Modular to build the ancestors of the changed workspaces. This command:
modular build --changed --ancestors
will identify all the workspaces that have changed compared to the --compareBranch
(which is the repository’s base branch by default), then identify all the workspaces which directly or indirectly depend on them (ancestors) and build the resulting set of packages in the correct build order, where workspaces depended on are built before workspaces that depend on them, recursively. If we suppose that workspaces b
and c
have changed, Modular will:
- Select all ancestors of
b
andc
, which area
(because it depends on both) ande
(because it depends ona
). - Build
c
first, because it doesn’t depend on any package that has changed (it only depends ond
, which is not in the changed set). - Build
b
, because it only depends onc
, that got built in the previous step. - Build
a
, because it depends onb
andc
, that got built in the previous steps. - Build
e
, because it only depends ona
, that got built in the previous step.
Non-Modular packages
Packages without a modular
configuration are built only if they have a build
script in their package.json
. For example, if you have a Modular package named “app” of type app
that imports a simple non-Modular package called “non-modular-buildable” that is able to build itself using tsc
:
packages/non-modular-buildable/package.json
{
"name": "non-modular-buildable",
"private": false,
"scripts": {
"build": "tsc --skipLibCheck"
},
"files": ["dist", "src"],
"main": "./dist/index.js",
"version": "1.0.0"
}
packages/non-modular-buildable/src/index.ts
export default function add(a: number, b: number): number {
return a + b;
}
packages/non-modular-buildable/tsconfig.json
{
"include": ["src"],
"compilerOptions": {
"target": "es2018",
"outDir": "dist",
"lib": ["dom", "esnext"],
"declaration": true,
"moduleResolution": "node",
"sourceMap": true,
"strict": true,
"esModuleInterop": true
}
}
packages/app/src/App.tsx
import * as React from 'react';
import sum from 'non-modular-buildable';
import logo from './logo.svg';
import './App.css';
function App(): JSX.Element {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>This is the sum:</p>
<p>
<code>7 + 7 = {sum(7, 7)}</code>
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
yarn modular build
will, in this order:
- Build
non-modular-buildable
by callingyarn workspace non-modular-buildable build
and waiting for the spawned process to terminate successfully - Build
app
using Modular’s build scripts and configuration, bundling the previously builtnon-modular-buildable
dependency
Please note that Modular merely works as a task runner when building non-modular packages: it’s your responsibility to ensure that the build
script works and that your package.json
is correctly configured to export the right functions in the right format.