nx is a tool for working with microfrontends.

Nx is a front-end tool that makes it easy for multiple teams to collaborate on a project.

Nx allows you to generate code, automate build processes, test, and manage dependencies efficiently. In addition, nx has many plugins that allow you to use popular frameworks out of the box in your project: Angular, React, Vue.js.

The code modularity that nx offers improves code reusability and also allows different microfronts to reuse repetitive code.

nx project types:

  • package-based (package-based): each project in the workspace is a separate independent package. Each package has its own set of dependencies, scripts. They are published as separate packages.

  • integrated (integrated): Multiple projects are combined into one workspace and interact with each other. Projects in the workspace can depend on each other, share code, resources and settings, their dependencies are managed centrally, in the package.json file of the entire repository.

  • Standalone (isolated): This is a project that can function and be used independently of other projects or components in the system. Such projects are most often created in order to try the nx tool, but there are no microfrontends. We only have one application. Following nx best practices when developing applications, we have the option in the future to move to integrated project.

When creating new libraries, nx creates paths aliases in the tsconfig.base.json file, this allows the use of absolute imports. Following this approach, we will correctly work out automatic imports in webstorm. And also eslint @nx/enforce-module-boundaries. If use npm workspacesthen there will be problems.

nx.json file

Some projects may depend on other projects, where changes in one project will affect other projects. For example, Standard Tasks Runner nx allows you to cache certain operations in projects.

It is specified in the nx.json file at the root of the repository:

{
  ...
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "storybook"],
        "parallel": 5
      }
    }
  }
  ...
}

Thanks to the dependency graph, we have projects (A) that depend on others (B) are affected projects. For example, if the cache is updated in projects B, then operations will be performed again in projects A.

This behavior is configured globally via the nx.json file:

{
  ...
  "namedInputs": {
    "default": ["{projectRoot}/**/*"],
    "production": [
      "default",
      "!{projectRoot}/.eslintrc.json",
      "!{projectRoot}/.stylelintrc.json",
      "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
      "!{projectRoot}/tsconfig.spec.json",
      "!{projectRoot}/jest.config.[jt]s",
      "!{projectRoot}/storybook/**/*",
      "!{projectRoot}/**/*.md",
      "!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)"
    ]
  },
  "targetDefaults": {
    "build": {
      "inputs": ["default", "^production"]
    },
    "stylelint": {
      "inputs": [
        "{projectRoot}/**/*.styles.ts",
        "{workspaceRoot}/.stylelintrc.json",
        "{workspaceRoot}/.stylelintignore",
        "{projectRoot}/.stylelintrc.json"
      ]
    },
    "test": {
      "inputs": [
        "default",
        "^production",
        "{workspaceRoot}/jest.preset.js",
        "{workspaceRoot}/jest.config.js",
        "{projectRoot}/jest.config.ts"
      ]
    }
  }
  ...
}

projectRoot – directories of the project from which the task is executed.

workspaceRoot – the directory of the entire repository.

namedInputs

namedInputs is the named input referenced in targetDefaults.

where default is the base input.

production – data for “target”, if it is necessary to update the cache in a “truncated” form. In the example above, we exclude config files, tests, and storybook files. Exclusion from the selection is indicated by an exclamation point “!”.

The names default and production can be anything, it’s just that here they reflect the business logic more.

targetDefaults

targetDefaults use these named “inputs”. For example, the build of any project will only consider its own cache and the cache of dependent projects with production inputs. The “^” sign just means dependent projects. Dependent projects can be calculated when directly imported in the code, and when specified in the project.json file of the project, the property is implicitDependencies.

project.json file

project.json is the project file that nx picks up and takes into account in the dependency graph. The targets property specifies the tasks that can be performed via nx commands.

{
  "name": "my-main",
  ...
  "targets": {
    "build": {
      "executor": "my-plugin:webpack",
      "outputs": ["{options.outputPath}"],
      "defaultConfiguration": "production",
      "options": {
        "outputPath": "dist/apps/my-main"
      },
      "configurations": {
        "development": {},
        "production": {}
      }
    },
    "static": {
      "executor": "my-plugin:static",
      "defaultConfiguration": "production",
      "options": {
        "buildTarget": "my-main:build"
      },
      "configurations": {
        "development": {},
        "production": {}
      }
    }
  }
  ...
}

configurations must be specified, even if we do not have parameters for them, otherwise defaultConfiguration will be taken, even if we explicitly specify which configuration we start the task with: nx build my-main -c=development

name in the file points to the name of the project, which is referred to in the command above.

“my” – is npm scope, and it is intended to ensure that their packages have a unique name, because. there may be packages with similar names in the npm registry.

outputs are the files that will be cached.

executor is the executor, which in this example is taken from my-plugin. The colon is followed by the name executor.

nx workspace

The key concept in the monorepository is the workspace (workspace), it brings together many projects. This space provides unified dependency management, common settings, and tools. Which ultimately allows different teams of microfronts to have a common environment. In principle, all the pros and cons that apply to a monorepository apply to workspace, it’s just the term that refers to the “space” in which our application resides.

What is nx made of?

What is nx made of?

  • nx console – extensions for interactive launch of various commands for VSCode, IntelliJ, VIM

  • nx cloud – basically a distributed cache, as well as distributed execution of tasks on different “machines”. Allows you to speed up the execution of tasks on the developer’s device, as well as speed up CI / CD processes through the use of a cache.

  • plugins – various plugins that make it easier to work with a mono-repository, add additional logic to the application environment. Plugins can contain in turn:

    • generators – allow you to create something or change in the project. For example, a new application, a library in a project. Allows you to simplify the creation of a new microfront using a prepared template.

    • executors – “executable” scripts that are used in libraries, applications, to run a specific task in the application. For example, building, testing, checking code quality, running a microfront in development mode.

    • code migrations – scripts to automate migration from one version of the library to a new one. Also, migrations can change the source code of the application so that when the library is updated, the application does not break.

  • devkit (@nx/devkit) – A “devkit” package for working with nx, useful for writing your own plugins.

  • nx is the nx library itself, which provides the following features:

    • Task Running – execution of commands (tasks). An application or library can run different commands, either directly or using executors.

    • Distribution – associated with nx cloud, allows you to execute remote commands.

    • Workspace analysis – in the nx project, it generates a graph of dependencies, for faster launch of commands and for cache flush analysis. Allows you to visualize project dependencies through a command nx graph

      After all, if microfrontends use their own library in a monorep, we want to have an up-to-date version of microfrontends, and not get it from the cache. Because the cache is no longer relevant, due to the fact that the dependency, represented by the library, has changed.

    • Caching – local caching executors, the result of output to the console and output files are cached.

    • Code generation – includes both plugin generators, and update generator (which is called after nx is updated).

      Through code generation, we launch both our own generators, which we will “write” for our needs, and third-party developers.

    • Automatic migration – allows you to update the “ecosystem” nx. Starts updating packages, calling update generators if needed.

nx allows you to run commands either in the very root of the project, then we need to specify the name of the application (library), or in the project directory itself without specifying the name: nx build app or in the app directory: nx build

When developing your own plugins or to clarify some of the features of using nx tools, do not be afraid to poke around in sources.

The sources are useful to understand exactly how you can create your plugin with executors and/or generators.

CI/CD

When writing deployment scripts, you need to use nx affected commands so that when libraries are changed, applications that depend on them: checked for code quality, passed tests, deployed, etc. Examples of ci files can be found Here.

Microfrontends at nx

In the webpack.config.js file, we can call enable ModuleFederationPlugin webpack itself through the call to the withModuleFederation function, the developers make certain settings for this plugin for us. But this function is only available for react and angular. If they are not satisfied with their function, then you can directly interact withModuleFederationPlugin in the configuration file.

Also, the nx developers provided for updating the dependency graph on the file module-federation.config.js the project itself. You can see the full example here.

module.exports = {
  name: 'shell',
  remotes: ['shop', 'cart', 'about'],
};

For example, this command launches the shell in development mode, as well as cart and shop. If you do not specify devRemotes, then the rest of the microfronts will be given static, i.e. running microfronts will not react to changes in their code, except for the shell.

Shell in this case acts as an application host for shop, cart, about.

nx serve shell --devRemotes=cart,shop

This is how the static launch of module federation works for us.

There is also a dynamic launch, then the remotes of the application host must be empty. This is done for various reasons, one of them is the dynamic address of the microfront, then you will have to describe the module loading logic in the webpack.config file. Another reason is to load microfrontends on demand (i.e. not to launch all applications at once at application start and load all modules at once). In this case, you can refuse to use the withModuleFederation function, and refer directly to ModuleFederationPlugin.

But on the other hand, the nx developers themselves argue that it is better to leave remotes “empty”, because this speeds up application deployment.

Keeping the applications independent allows them to be deployed on different cadences, which is the whole point of MFEs.

Environment Variables (env)

Working with environment variables is the same as in any webpack application.

To load environment variables use two approaches:

Use files in the environments folder

For each environment, we create our own configuration file, for example, environment.prod.ts. environment.ts. Accordingly, the file for the product and test stands. Replacing the file, if necessary, will be carried out at the time of assembly / development.

The replacement logic is described in the package.json file of the project:

"fileReplacements": [
  {
    "replace": "apps/products/src/environments/environment.ts",
    "with": "apps/products/src/environments/environment.prod.ts"
  }
]

For this method of using environment variables in the application code, it will be simple – a simple import of the environment.ts file

Using .env files, or loading from OS environment variables.

To download and use in the application, you must use the plugin webpack.DefinePlugin.

Application development guidelines

Microfrontends are best divided by business value, into independent parts that can be given to different teams for independent development. Otherwise, we will slide into hundreds and thousands of microfronts if we put each component as a separate application.

If we have a common logic for microfrontends, we put them in a library. To reuse painlessly between microfrontends.

The creation of microfrontend applications should come from the business, but with the assistance of developers.

According to the nx team rule: 20% of the code is in applications, the remaining 80% is in libraries. This can be somewhat difficult to rebuild to this approach, but it allows us to reuse code and reduce the amount of coupling within the application where the code is used, plus it simplifies test coverage. And since we have a lot of small libraries, our caching efficiency increases and, accordingly, the build speed due to the fact that we have going small projectsand not one big one.

To reduce the size of the application, it is desirable to use only one library for working with the frontend: angular or react or something else. Although microfrontends allow this, it is considered bad practice in monorep.

In the next article, I will discuss how to create your own plugin, executor and generator.

If there are additions or corrections to this article, I am ready to discuss.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *