single-spa / single-spa-angular

Helpers for building single-spa applications which use Angular
Apache License 2.0
198 stars 78 forks source link

How to share single angular library with all apps? #303

Closed 3gwebtrain closed 2 years ago

3gwebtrain commented 3 years ago

I am using angular 8.x with "single-spa-angular": "3.1.0". I have 4 separate sub apps. all it's assembled with container app. all works fine. I have created a angular library with generic components. it's installed in all 4 apps and container app. I really find it's unnecessary.

So how can all this 5 apps can use a single angular-library with them? what is the correct approach to implement this? now all 5 apps having their own copy of angular-library. I want to avoid this.

All my apps created using angular-cli

Please advice. Thank you. Note: If I find a solution, there is a idea to remove all angular common modules from "node_module" bundle from other than wrapper app.

arturovt commented 3 years ago

https://single-spa.js.org/docs/recommended-setup/#cross-microfrontend-imports

3gwebtrain commented 3 years ago

@arturovt thanks for your replay. Is there any demo repo we have? if so can you share me please?

3gwebtrain commented 3 years ago

Hi there, as a trial, I tried to remove my local "single-spa" and added the same in "import-map". But still I am getting following error:


 ERROR in src/ambient.d.ts:1:30 - error TS2307: Cannot find module 'single-spa'.

1 import { ParcelConfig } from 'single-spa';
                               ~~~~~~~~~~~~
src/main.ts:3:28 - error TS2307: Cannot find module 'single-spa'.

3 import * as singleSpa from 'single-spa';
                             ~~~~~~~~~~~~
src/services/single-spa.service.ts:2:43 - error TS2307: Cannot find module 'single-spa'.

2 import { Parcel, mountRootParcel,  } from 'single-spa';
                                            ~~~~~~~~~~~~

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
i 「wdm」: Failed to compile.

I understood that, I am missing some configuration. can you please point out the configuration i need do here?

Thanks in advance.

here is my import:

{
  "imports": {
    "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/5.4.0/umd/single-spa.min.js",
    "content": "https://amxanedevaoscmsui01-as-spa.azurewebsites.net/main-es2015.js",
  }
}
filoxo commented 3 years ago

This is a TypeScript error. single-spa is published with types included, so you'll need to install it as a devDependency it to have those types available to the compiler, eg npm install --save-dev single-spa.

3gwebtrain commented 3 years ago

How to add that with, imports? my intension is to import that in wrapper application. both wrapper and child applications to use. sorry for inconvenience. please help me. As a simple I am trying to convert "single-spa" as browser module instead of locally installed. reason to share all child apps. If I am wrong please show me right direction.

filoxo commented 3 years ago

How to add that with, imports?

Yes. Both installed locally (only because you're using TypeScript!) as well as in the import map. You should install the module (OR just the types) so that TypeScript can do its thing and analyze the code with types. So for this reason, you would install the module locally. For example, npm install --save-dev single-spa to install the library or npm install --save-dev @types/some-library to install just the types that are published on DefinitelyTyped. But, assuming your Webpack config is configured correctly, that module would not be bundled into your application. SystemJS serves the purpose of enabling in-browser module resolution so you need to then ensure that the library is in your import map. There isn't an example of doing all of this yet (though some issues, like https://github.com/single-spa/single-spa-angular/issues/288, might be a good reference) that I know of. You'll need to pioneer the rest of this setup and architecture on your own, and it would be a great example or case study to share with the single-spa community!

3gwebtrain commented 3 years ago

As per your suggestion I have installed the single spa with "--dev" flag. it works. as well I added a link with index.html as like below:

<script type="systemjs-importmap">
      {
        "imports": {
          "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.4.0/lib/system/single-spa.min.js"
        }
      }
    </script>

But I not insure that, it downloads in local. assuming it's only required for bundling purpose, I need to update the webpack.config.js, I see that there is number of example for handle the extra-webpack.config.js which connected with single-spa-angular . But my case I am using single-spa so in this case how can i write the webpack-config? can you help me? at preset my map import-map-dev look like this:

{ "imports": { "content": "https://content-as-spa.azurewebsites.net/main-es2015.js", "user": "https://management-as.azurewebsites.net/main-es2015.js", "status": "https://message-as.azurewebsites.net/main-es2015.js", } }

here is my index.html: ( need a way to test the import)

<script type="systemjs-importmap">
      {
        "imports": {
          "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.4.0/lib/system/single-spa.min.js"
        }
      }
    </script>

I am appending the import-map with index.html like this:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import * as singleSpa from 'single-spa';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}
const script = document.createElement('script');
script.src = `/assets/json-map/import-map-${environment.type}.json`;
script.type = 'systemjs-importmap';
document.head.appendChild(script);
singleSpa.start();

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

Now the pending thing is, how to write the webpack-config without single-spa-angular installed?. please help me to complete my integration.

Basically, I am using the approach like here : https://github.com/matt-gold/single-spa-angular-cli

Thanks for help.

daniloesk commented 3 years ago

I didn't understand anything past the initial request, but here is the summary to have shared dependencies. That's a guide. Check your versions and file and project types.

Root config index.ejs (on sharing block)

<script type="systemjs-importmap">
  {
    "imports": {
      "rxjs": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs@6.6.3-fix.0/system/es5/rxjs.min.js",
      "rxjs/operators": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs@6.6.3-fix.0/system/es5/rxjs-operators.min.js",
      "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.8.2/lib/system/single-spa.min.js",
      "single-spa-angular": "https://cdn.jsdelivr.net/npm/single-spa-angular@4.9.0/bundles/single-spa-angular.umd.min.js",
      "single-spa-angular/internals": "https://cdn.jsdelivr.net/npm/single-spa-angular@4.9.0/bundles/single-spa-angular-internals.umd.min.js",
      "@angular/core": "https://cdn.jsdelivr.net/npm/@angular/core@10.2.3/bundles/core.umd.js",
      "@angular/common": "https://cdn.jsdelivr.net/npm/@angular/common@10.2.3/bundles/common.umd.min.js",
      "@angular/compiler": "https://cdn.jsdelivr.net/npm/@angular/compiler@10.2.3/bundles/compiler.umd.js",
      "@angular/forms": "https://cdn.jsdelivr.net/npm/@angular/forms@10.2.3/bundles/forms.umd.min.js",
      "@angular/platform-browser": "https://cdn.jsdelivr.net/npm/@angular/platform-browser@10.2.3/bundles/platform-browser.umd.min.js",
      "@angular/platform-browser-dynamic": "https://cdn.jsdelivr.net/npm/@angular/platform-browser-dynamic@10.2.3/bundles/platform-browser-dynamic.umd.min.js",
      "@angular/router": "https://cdn.jsdelivr.net/npm/@angular/router@10.2.3/bundles/router.umd.min.js"
    }
  }
</script>

Application main-single-spa.ts

if (environment.production) {
  try {
    enableProdMode();
  } catch (exception) {
    // Prod/dev mode already defined
  }
}

Application extra-webpack.config.js

singleSpaWebpackConfig.externals.push(
  "rxjs",
  "rxjs/operators",
  "@angular/core",
  "@angular/common",
  "@angular/compiler",
  "@angular/platform-browser",
  "@angular/platform-browser-dynamic"
);

Application tsconfig.app.json (Angular 9+)

"angularCompilerOptions": {
  "enableIvy": false
}

Edited: Actually this is for sharing dependencies, not libraries. I misunderstood the issue. But can be helpful for #321 .

3gwebtrain commented 3 years ago

@daniloesk, thanks for your help. in my host application I am not using singleSpaWebpackConfig at all. instead i have the main.ts with following:

import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

import * as singleSpa from "single-spa";

import { AppModule } from "./app/app.module";
import { environment } from "./environments/environment";

if (environment.production) {
  enableProdMode();
}

singleSpa.start(); //starting my app

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch((err) => console.error(err));

so how to handle this? if require I can add the "custom-webpack" with my app. if so how to config? please help me.

daniloesk commented 3 years ago

I guess you need to decide to either follow the recommended setup or understand all the involved components to build your own solution. How can you expect to share dependencies without a custom webpack? Or are you using one and not posting it?

I haven't tried it, but I guess you could manually build a simple root config into your host application.

For your imports, I see no reason for changing package.json dependencies. You need to include such packages on webpack's external for them not to be packed but loaded from import map.

3gwebtrain commented 3 years ago

I am ready to install custom-webpack and do the root config. please help me to add the configuration with singleSpa environment.

daniloesk commented 3 years ago

I guess I misunderstood your issue. You are trying to reuse a library. But from #321 it seems you are struggling with single-spa-angular .

For this one, create a brand new Angular project with npx create-single-spa and see how webpack is configured. Then apply it to your project and follow @arturovt https://single-spa.js.org/docs/recommended-setup/#cross-microfrontend-imports. I guess just running ng add single-spa-angular does not make sense on your host project because... it is the host.

Probably you should try build a test with a couple of brand new empty projects to better understand single-spa instead of looking for a magic copy and paste solution. That's what I am doing =). It is quite hard to just apply something to existing projects.

3gwebtrain commented 3 years ago

sure daniloesk, will try and update in this space, thank for your time with me.

3gwebtrain commented 3 years ago

@daniloesk if do not mind, I have installed the new app using npx create-single-spa but how to load my child app which is running in http://localhost:3000 to created app? it show to load the external app from public path if so what should i do? sorry for silly questions.

joeldenning commented 3 years ago

@arturovt found a way to share Ivy dependencies. You make @angular/core, @angular/router, etc all separate webpack entries, and then compile them with webpack + angular compiler. The resulting output bundles can be put into the systemjs import map.

I have been meaning to create an example of this, but haven't gotten to that yet.

daniloesk commented 3 years ago

I successfully loaded at runtime using Ivy like this:

Loaded commons, platform-browser and core, in this order, then got the error:

ERROR TypeError: Cannot read property 'bindingStartIndex' of null
    at Module.ɵɵelementStart (core.js:14781)

Anyone got past that?

daniloesk commented 3 years ago

It worked after setting runtimeChunk as documented here:

And importing the runtime.js on Root.

daniloesk commented 3 years ago

Now I am trying to make this into a real world usage. The exported bundles using entry are keeping multiple copies of libraries. For example each one of them has core.js. Any ideas?

daniloesk commented 3 years ago

I just remember I saw a dependency somewhere on the docs...

daniloesk commented 3 years ago

dependOn requires webpack 5 :-/

isandeeppatil commented 3 years ago

Please do share once you succeed in this. I have not yet got a chance to look at this myself. Got redirected here from slack thread.

daniloesk commented 3 years ago

OK, I am struggling a little bit with this. I succeeded having runtime dependencies with Ivy on, but the bundles have duplicated dependencies, making the overall very large. Then I have some ideas for this:

  1. Since having a single common bundle is a possibility for me, tried "multi-main entry", but couldn't get it to be loaded.
    entry = [ '@angular/common', '@angular/core', '@angular/platform-browser' ]
  2. splitChunk only (no entries): tried building single ou multiple bundles, but couldn't get them loaded with SystemJS.
    • Always loading with script tags works.
    • Changing code to lazy imports may work, but changing all code base is no good for me.
  3. Individual entry and splitChunk: same as above.
  4. Using webpack 5 dependOn between entries should work, but webpack 5 is a whole new issue. Angular 11 just started experimenting with it and no idea if single-spa will work.

Any suggestions are welcome. Also I am not an expert on webpack / SystemJS / Angular / single-spa, so it is very possible I missed something on any of the ideas I tested. Most importantly, I would love to know how this was handled by @arturovt and @joeldenning . :-)

daniloesk commented 3 years ago

Demo of sharing using entry, but still with duplicated code: https://github.com/daniloesk/single-spa-examples/releases/tag/v20201215-entry

daniloesk commented 3 years ago

Using webpack 5 dependOn seems a no go. It seems both the usual load method with import and providing a custom load are not supported yet: https://github.com/webpack/webpack/blob/10ad4e95b850bbe20019275fd1d364666c4b69ea/lib/javascript/EnableChunkLoadingPlugin.js#L71-L112

daniloesk commented 3 years ago

Although using multiple entry points per page is allowed in webpack, it should be avoided when possible in favor of an entry point with multiple imports: entry: { page: ['./analytics', './app'] }. This results in a better optimization and consistent execution order when using async script tags. https://webpack.js.org/guides/code-splitting/

I can pack but I can't use.

daniloesk commented 3 years ago

splitChunks should work, right? Considering it is used by Angular to load lazy modules. EDIT: Angular lazy loads async imports only

daniloesk commented 3 years ago

Breaktrough on dependOn with webpack 5. There is no need for a @angular/common to lazy load @angular/core if it is loaded directly by the entry. The problem is that when using webpack 4 for the app (not the Angular builder), the load was in the wrong order. With webpack 5 the dependency graph is used and dependencies loaded in correct order.

Webpack 4: System.register(["@angular/common", "@angular/core","@angular/router","@angular/platform-browser"] Webpack 5: System.register(["@angular/core","@angular/router","@angular/platform-browser","@angular/common"]

daniloesk commented 3 years ago

OK, solved the duplication problem by using multiple projects. It is a bit ugly but works really well with Ivy on and webpack 4. Here is the example: https://github.com/daniloesk/single-spa-examples/releases/tag/v20201217-importmap-ivy

@joeldenning , any chance of a project like esm-bundle for that? LOL Does esm-bundle builds automatically whenever let's say rxjs releases?

Can this thread be closed?

daniloesk commented 3 years ago

This also handles #268 ?

3gwebtrain commented 3 years ago

I am trying to run after installation, getting error as:

app11 :

An unhandled exception occurred: Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
 - configuration.entry['main'] should not contain the item 'C:\Users\722333\722333\Import-Map\app11\node_modules\webpack-dev-server\client\index.js?http://0.0.0.0:0&sockPath=/sockjs-node' twice.
   -> A non-empty array of non-empty strings
See "C:\Users\722333\AppData\Local\Temp\ng-SFOWTE\angular-errors.log" for further details.
error Command failed with exit code 127.

builder11 :

An unhandled exception occurred: No projects support the 'serve' target.
See "C:\Users\722333\AppData\Local\Temp\ng-ue5Me2\angular-errors.log" for further details.
error Command failed with exit code 127.

what is the correct way to start the app?

joeldenning commented 3 years ago

@joeldenning , any chance of a project like esm-bundle for that? LOL Does esm-bundle builds automatically whenever let's say rxjs releases?

@daniloesk Yes, I started work on this a long time ago but was doing it with rollup which doesn't work since angular compiler only works with webpack. I hope to switch over the esm-bundle repos for angular to webpack and then have those available that way. If you have interest in helping implement that, let me know. See the following links:

https://github.com/esm-bundle/angular__core https://github.com/esm-bundle/angular__common https://github.com/esm-bundle/angular__platform-browser https://github.com/esm-bundle/angular__router

exaucae commented 3 years ago

any insight on @3gwebtrain 's last question so far?

arturovt commented 2 years ago

Closing the issue since it was implemented, see this guide: https://single-spa.js.org/docs/ecosystem-angular/#shared-angular. All Angular packages (even Cdk + Material) are built & published in SystemJS format (see esm-bundle repo).