Share modules between apps - architecture

So, I recently just solved this issue at the company I work for using custom npm packages and the solution itself is actually very elegant and clean… it just takes time to get there. :stuck_out_tongue:

We built our shared component library using ng-packagr and use it across multiple projects as well as using semantic versioning to control what updates get applied to projects (version locking). I really highly recommend it as it has been the best development experience that I’ve had so far with npm packages. We initially tried to go the npm link route but as you pointed out above, it doesn’t work very well and has quite a few issues.

I’ll try to give a quick write up of what I did and how I did it and got it working and hopefully it will help you get up and running quickly. Keep in mind this is how we set it up to work for us, so feel free to tweak it for your own personal needs.

ng new my-component-library

Then in the package.json file, change the name of the package to be whatever you want. We used npm scope for our name to keep it specific to our company, ie. @company or @company/ionic but you can use whatever you want to, scope or not.

Next, cd into your project directory and install the following packages.

npm install ng-packagr --save-dev
npm install standard-version --save-dev

I recommend standard-version for version control, but you don’t need to use it or could use something else of your choice.

Update package.json to add the following lines to the scripts section.

"packagr": "ng-packagr -p ng-package.json",
"release": "standard-version",
"publish:latest": "npm run release && npm run packagr && cd dist && npm pack && cd .. && npm publish dist && git push --follow-tags origin master",

as well, add a new key to the package.json with the following code:

"standard-version": {},

and finally remove "private": true from the file if you plan on publishing to a registry.

Next you’ll need to create two files at the root directory,
ng-package.json

{
    "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
    "dest": "dist",
    "workingDirectory": ".ng_build",
    "lib": {
        "entryFile": "public_api.ts"
    }
}

public_api.ts

export { MyLibraryModule } from './library/public_api';

You’ll notice that the public api file is exporting from a file that doesn’t exist. We’ll want to create that file. The reason why we developed our library this way is to support multiple entry points. Similar to how with angular projects, you can import @angular/core or @angular/common but both are in the same project, with this structure you can have @company/http or @company/forms. The project structure should look like this (shortened):

library/
|- src/
|  |- library.module.ts
|- public_api.ts
|- package.json
|- README.md
src/
|- Angular project files
ng-package.json
package.json
public_api.ts
README.md

The library/package.json file should look like this:

{
    "ngPackage": {
        "lib": {
            "entryFile": "./public_api.ts"
        }
    }
}

The library/public_api.ts file should export anything you want to make available to import in other projects.
The angular project files src directory can be used like a normal Angular project that can import the different component modules from the library for testing and development purposes.

At this point, you can actually be done. Run npm run packagr which will generate the dist folder, cd into the dist folder and then run npm pack to create a tarball (.tgz). This tarball can then be distributed and anyone can install it into any project using npm install ./path/to/tarball-0.0.0.tgz. then to use it, you would import files from company/library. For every folder that you create at the top level that matches the structure of the library directory, it should create an entry point that you can use.

If you want to continue with publishing to a registry, you can use the npm run publish:stable command, or modify that to fit your needs. If you don’t want to publish to the public registry, then you can either use npm private registries, or host your own (we use verdaccio to host our own private registry that only our projects have access to).

Hopefully this helps and gets you up and running with a simpler solution for sharing modules between projects.

Edit: One thing I just remembered, best practice is to list dependencies for your library as peerDependencies in your package.json file rather than as dependencies so that you avoid the node_modules conflict that you mentioned above. This means that it is up to the project installing the library to provide the needed dependencies, like @angular/core or ionic-angular.

8 Likes

Thanks @dallastjames for the detailed description of your way.

But do I understand it right: You are also not able to actually develop a module as part of an app using it? Because then also my first try above could be used, the issues I had was just due to npm link I tried to use, which you say also doesn’t work in your case?

Hi,

We’ve been maintaining multi-app repository since last spring, and I’d like to share what we do at my company. Our solution is based on https://medium.com/@blewpri/sharing-code-in-angular2-ionic2-apps-simply-without-npm-5203048ec1e1 .

Here is the directory structure:

ionic-pro-build/ionic-pro-push.js
  # This script converts the directory structure for Ionic Pro,
  # and commits to a temporary branch and push to Ionic Pro git remote.
ionic-pro-build/package.json

modules/XXXXXXX-common  # project specific models and components
modules/XXXXXXX-common/index.ts  # CommonModule
modules/XXXXXXX-utils  # general purpose providers and directives
modules/XXXXXXX-utils/index.ts  # UtilsModule

packages/AAAAAAA-app
packages/AAAAAAA-app/ionic.config.json
packages/AAAAAAA-app/config/watch.config.js  # adds ../../modules/** to srcFiles/paths
packages/AAAAAAA-app/config/webpack.config.js  # adds ../../modules to resolve/modules
packages/AAAAAAA-app/package.json
  # `"ionic:push": "ionic-pro-push AAAAAAA-app"` in scripts
  # `"ionic-pro-build": "file:../../ionic-pro-build"` in devDependencies
packages/AAAAAAA-app/src/foo/bar.ts  # you can `import { Baz } from "XXXXXXX-utils/baz";`
packages/AAAAAAA-app/src/... 

packages/BBBBBBB-app
packages/BBBBBBB-app/...

Firstly, the reason we share modules is that we have two audiences to whom we release one app each in the same release cycle. The apps work together and much of the models and services are shared.

This is an important point to note because if the apps were more independent, we might have decided to separate the repositories. Our approach is specifically designed for maintaining two apps and their shared logic/components simultaneously in an agile environment.

I also would like to mention Ionic Pro because we’ve finally converted from Ionic Cloud to Ionic Pro in December. This too is a concern when multiple apps are in one repository in whatever way. Our strategy was to commit a converted directory structure and push it to Ionic Pro remote. Unfortunately, the revision on Ionic Pro does not correspond to the one on github, but we are committing prod/dev environment switch instead while converting and it’s nice to have that freedom.

If you are interested in any part of our solution, please let me know. I’ve got a permission from my boss to make a demonstration repo based on our code base at my spare time. Not sure how helpful this approach might be to you though. The balance between how tightly or loosely apps share the modules should depend on your context. We opted to make them as tight as possible.

1 Like

@Rasioc I think I better understand your question now. It’s not just about being able to share the code across multiple projects but also being able to develop the shared library simultaneously alongside the projects, is that right?

So, that’s not a requirement for my library, we have a demo project setup in the main src directory that we use to run tests and make sure components and directives all work correctly before we publish the package out to our projects. However, I did some digging and was able to get npm link working correctly with the library setup that I posted above with an ionic project. Here is what I did to get that working.

In the library, once you have the basics of your library setup (at least the module that you will import), compile the library with npm run packagr. This will create your dist folder. Once that is created, cd into the dist folder and run npm link. This will make your entire library available to be linked.

Once your library is linked, go to your ionic project and run npm link my-awesome-library (or whatever it’s called) to link the library to your project. Once linked, open up the tsconfig.json file at the root of your project. You’ll want to add the baseUrl and the paths options to the compiler options. It should look like this:

{
    "compilerOptions": {
        ...(shortened)...
        "baseUrl": "",
        "paths": { },
        ....
    },
}

These settings will be left up to your discretion but here is what I like to do. The base url will set the base path for all of your imports in your application. By defining this, you could do something like import { AppComponent } from 'app/app.component' from anywhere within your project (assuming that the component lives at /src/app/app.component.ts). You can set it to whatever, but you need to know how to reference the root of your project from that baseUrl.

Assuming that you use "baseUrl": "src", you would then need to add each of the shared dependencies between the library and your app to the paths array. What does that mean? Well, in this example, the library above, and a new ionic project share 3 main dependencies, @angular/*, rxjs, and zone.js. In order to resolve those dependencies correctly and not get those strange npm link errors, you would tell the compiler where to look for those dependencies (thereby preventing any conflict from occurring between the node_modules of the two packages). It should look like this:

"paths": {
    "@angular/*": [
        "../node_modules/@angular/*"
    ],
    "rxjs/*": [
        "../node_modules/rxjs/*"
    ],
    "zone.js/*": [
        "../node_modules/zone.js/*"
    ]
},

Take note that each of the paths for the dependencies, reference the node_modules based on what the baseURL is set as. So, had you chosen the root directory (./) rather than src, your paths would be "./node_modules/@angular/*", etc. You must make sure that you specify this for each shared dependency between your library and your projects. So, chances are you would be sharing the ionic_angular library, so you would just add another line:

"ionic-angular/*": [
    "../node_modules/ionic-angular/*"
]

Once this is done, you can then serve your ionic application and it should run with the linked library correctly. To make changes to the library, you would just need to make sure to rebuild it after each change (could be achieved by adding a watcher that runs npm run packagr after each change or just manually running it). Once rebuilt, it should auto-update in your ionic project.

That should get you up and running! (NPM link with angular-cli reference)

2 Likes

How were you able to use this structure to build Ionic-based components? I’m looking to update my monorepo, which used variations of Ionic Native’s scripts, to something much more manageable. I just get build issues saying that ionic-angular cannot be found.

I’ve created a demo repository for my setup that supports multiple Ionic 4 apps in single Angular workspace - https://github.com/sneat-opensource/ionic-ng-workspace

Took me couple of days to figure out.

https://devdactic.com/ionic-multi-app-shared-library/

Angular out of the box