Share modules between apps - architecture

We could start a separate topic for people to vote on that one, but I have a feeling it would be considered ‘irrelevant’ to Ionic.

We’ll have to agree to disagree!
Pink Floyd agrees with me though. I have proof

@Rasioc see:

And:

And:

@Rasioc yes and no :wink:

no I don’t use it yet to share components and providers across projects. Like I said, I would be super interesting to know a clean or proper solution and this would only be my spontaneous first thought

yes I do use this way to build and publish the open-source library ionic-swing (https://github.com/peterpeterparker/ionic-swing) and use it in my project (private repo)

@robinyo
Thanks for the links, but unfortunately I think you miss the point here. I am not talking about sharing modules WITHIN an app. I am talking about sharing modules between multiple apps. None of these links cover this topic as far as I can see.

@reedrichards
Ok I will look into it, thanks!

@Rasioc cool, looking forward to the results of your test, hope you gonna find the simpliest cleanest way to share these modules :slight_smile:

@Rasioc I think you may be missing my point, that being it is easier to ‘share code between multiple Ionic Apps’ if each App adopts a standardised project structure (see this guide and this post).

You can ‘share code between multiple Ionic Apps’ by:

  • cutting and pasting (not recommended)
  • using a Cordova plugin
  • using a Node module (packaged using npm) as @reedrichards described (BTW, the npm registry also supports private packages)

You can use links (during development):

  • ln -s EXISTING_FILE_OR_DIRECTORY SYMLINK_NAME
  • cordova plugin add --link ../PLUGIN_NAME
  • npm link

See: Best way to share components etc. between multiple apps?

“Library support — Today the CLI can produce UMDs optimized for the browser, and CommonJS bundles optimized for the server. What if the CLI could help you produce a bundle that could be consumed by other Angular applications?”

See:

I found a few more topic-related posts:

2 Likes

Hi @robinyo,
seems I misunderstood partly - sorry! I already have my apps structured in separate (kind of) standalone modules - which I want to share now by not using copy & paste.
I would prefer not to publish to npm (also not using private repos). But the last links you provided seem like what I was looking for.
Thanks for these hints and I will take a look which seems more suitable to my use case.

@Rasioc I should have included some comments and not just the links, sorry :slight_smile:

If you are working as a part of a team then you can store your projects on your private Git server and reference them in your App’s package.json using URLs: https://docs.npmjs.com/files/package.json#git-urls-as-dependencies

JFrog also has an Npm Registry: JFrog Help Center

1 Like

Ok so let me summarize my first analysis (without trying out yet):

  1. NPM packages:
    Seems the cleanest solution so far and includes versioning, but also contains quite some effort. Using NPM Link sounds good for development, but not sure how to utilize this while I need to be able to checkout the repository of each app on a MAC build pc to build the apple app. I could install from GIT URls, which would help with the multi-platform issue, but I guess using npm LINK at the sime time will not work well?

  2. Symlinks:
    According to Best way to share components etc. between multiple apps? the build system doesn’t play too nice with symlinks. Also I would always need some setup script which adds the symlinks after checkout (e.g. for mac)

  3. Cordova Plugin
    Doesn’t seem too right for me for this

  4. The approach from @onderceylan in Best way to share components etc. between multiple apps? looks interesting but still too hacky and also setup for multiple apps at the sime time would be strange as far as I understand it.

  5. The approach from https://medium.com/@blewpri/sharing-code-in-angular2-ionic2-apps-simply-without-npm-5203048ec1e1 seems the most promising one when you do not need versioning for each component - at least from reading it. But when you look at the comments in the blog, there seem to be unresolved issues with production builds.

Sooo… At the moment I think it would be best to try the npm solution and figure out if I somehow can still use linking while also use github repos for installation on different platform. Would be nice if the CLI would provide an easy way for such requirement in future, but looks like right now there is no better way of doing it.

2 Likes

A realllllllllly dirrrrrrtty way would be to setup a script that copies the files from a common lib to your project lib at specific moments…

like (pseudo)

xcopy /s /e /sharedlib/* /myproject/src/sharedlib

But then you are left with the detection of changes…

Hmm ok I started by trying out this tutorial for npm packages which also covers linking: Sharing Code in Angular2 + Ionic2 Apps | by Martin Pritchard | Medium
But it doesn’t really work well yet:

As the linked module contains his own node_modules folder, I start seeing random errors about type mismatches. For example if I use some class of the shared code, which returns an observable. Then I try calling .pipe() on this observable and passing in first() method as argument:

Error:(152, 38) TS2345: Argument of type ‘UnaryFunction<Observable, Observable>’ is not assignable to parameter of type ‘UnaryFunction<Observable, Observable>’.
Types of parameters ‘source’ and ‘source’ are incompatible.
Type ‘Observable’ is not assignable to type ‘Observable’. Two different types with this name exist, but they are unrelated.
Property ‘source’ is protected but type ‘Observable’ is not a class derived from ‘Observable’.

I guess it’s because the shared code references its own node_modules folder for the Observable, while the app uses the other node_modules folder for the first() reference (import { first } from ‘rxjs/operators’;). Not sure how to solve this yet.

Edit:
Ok it seems I am running into this particular known angular/npm problem, which is (and will probably stay) unresolved: No Directive annotation found on when working with NPM linked modules · Issue #1514 · angular/angular-cli · GitHub
Seems like linking via NPM is NOT a valid solution :frowning:

Edit2:
Still no solution. Only working solution is deleting node_modules and dist/node_modules in the library folder, but this requires another “npm i” whenever you want to rebuild the library, so this is also not a solution :frowning:

Edit3:
Ok seems like this will not be my way. I can’t sacrifize development experience that much, I need to develop the modules as part of the apps.
See also Verify works with linked modules · Issue #635 · ionic-team/ionic-app-scripts · GitHub for an existing github issue from ionic team regarding support of linked modules…

1 Like

As I am running out of time to solve this, I’m taking the shorter path now. I think I got it working with symlinks as follows:

  1. Moved out an exemplary shared module to some shared folder
  2. Created a symlink (windows, admin CMD required): cd …\src\modules && mklink /D shared …\ …\ …\ …\SharedModules\shared (hint: be aware that the forum modifies the target path somehow strangely)
  3. Add ./config/watch.config.js:
const watchConfig = require('@ionic/app-scripts/config/watch.config');

// add symlinked paths manually to enforce them being watched - even though they are in src/ folder they will otherwise be ignored
watchConfig.srcFiles.paths.push('{{SRC}}/modules/shared/**/*.(ts|html|s(c|a)ss)');

module.exports = watchConfig;
  1. Add ./config/webpack.config.js:
const webpackConfig = require('@ionic/app-scripts/config/webpack.config');
// When using symlinks for modules this must be set to FALSE, otherwise compile issues will be shown
// yes this sounds counter intuitive...
webpackConfig.dev.resolve.symlinks = false;
webpackConfig.prod.resolve.symlinks = false;

module.exports = webpackConfig;
  1. Add to package.json to use our new config files
  "config": {
    "ionic_watch": "config/watch.config.js",
    "ionic_webpack": "config/webpack.config.js"
  }

This seems to work, including ionic serve, live reload and production build.
If somebody else has some spare time and can go on with my results to investigate better options, I would be glad…
All I need now is some kind of setup script which initializes the symlinks after checkout on all platforms.

edit: does somebody know a cross-platform solution for adding symlinks, e.g. via npm “postinstall” script which does not require admin privileges on windows?

1 Like

@Rasioc

You should also try posting your questions on Ionic Worldwide: https://ionicworldwide.herokuapp.com/

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

1 Like