angular-architects / module-federation-plugin

MIT License
729 stars 198 forks source link

Native Federation: Workspace tsconfig paths ignored #472

Open nickbelling opened 8 months ago

nickbelling commented 8 months ago

I'm having an issue with the native federation builder that's causing me to tear my hair out a little. I am hoping it is merely a configuration issue on my part but I am beginning to think there may be a bug in the native-federation library.

I want to expose the ability for developers external to my repository to be able to create "plugins" for my app via native federation. The intent is that my main (shell) app provides Angular and some shared functionality in its entirety, and the plugin provides next to nothing other than its own components.

I currently have an example setup like this:

@my-project/
|- dist/
|- main/
| |- src/
| |- federation.config.js (shell app federation config)
| |- package.json
|- shared/
| |- src/
| |- ng-package.json (shared library ng-packagr config)
| |- package.json
|- angular.json (workspace-level Angular config)
|- package.json (workspace-level package.json)
|- tsconfig.json (workspace-level tsconfig)

plugin/
|- dist/
|- src/
|- angular.json
|- federation.config.js
|- package.json

The plugin app is at no point meant to be a fully standalone app - it merely is supposed to provide runtime components to the main app. This currently works as expected. Both apps declare all of the Angular modules as singleton shared modules, and I am able to load components from the plugin into the main app.

@my-project/shared is a library using the Angular Package Format. It is imported by both the main and plugin apps with the intent being that services inside are declared as singletons. This is currently working, however I'm seeing some unexpected behavior that is requiring me to do a bit of manual processing which I'm hoping to avoid if this is indeed a bug.

Because the plugin app is outside of the NPM workspace, I use npm link to link ./@my-project/dist/@my-project/shared to the plugin (in reality, the @my-project/shared package will be published internally, but either way, it's referring to the built output of @my-project/shared).

When I build the plugin app (while not skipping anything for testing), everything works as I'd expect. A file called _my_project_shared-0_0_1.js is created in its dist directory, and the plugin's remoteEntry.json contains the following:

    {
      "packageName": "@my-project/shared",
      "outFileName": "_my_project_shared-0_0_1.js",
      "requiredVersion": "0.0.1",
      "singleton": true,
      "strictVersion": true,
      "version": "0.0.1"
    },

However, when I build my main (shell/host) app, the results are different.

When I build the @my-project/main app, some strange things happen. While the main app seemingly correctly externalizes the shared library, there are some weird quirks.

While all of the Angular shared libraries are exported correctly (e.g. _angular_core-17_2_1.js), the shared project does not have a version number included: _my_project_shared-2QJEZZPT.js.

The main app's remoteEntry.json file also looks like this:

    {
      "packageName": "@my-project/shared",
      "outFileName": "_my_project_shared-2QJEZZPT.js",
      "requiredVersion": "",
      "singleton": true,
      "strictVersion": false,
      "version": ""
    }

At runtime, this causes two separate versions of @my-project/shared to be loaded. If I modify the main app's remoteEntry.json file to properly set the requiredVersion and version fields, the services become magically singleton and everything works as expected.

The first clue as to something being wrong occurs during build of @my-project/main:

 WARN  No entry point found for @my-project/shared
 WARN  If you don't need this package, skip it in your federation.config.js or consider moving it into depDependencies in your package.json
 WARN  No meta data found for shared lib @my-project/shared

I've determined that this is occurring because the native-federation builder is attempting to read the package.json file out of the @my-project/shared source directory rather than the built dist directory. An Angular Package Format project's package.json should not include any main/imports/external references, as these are provided by ng-packagr during build time.

I believe this behavior is incorrect, because while stepping through the build, I can see that the withNativeFederation function creates a config object that correctly references the path to the built library:

// federation.config.js
const config = withNativeFederation({
  name: 'main-app',
    shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto', eager: true }),
    "@my-project/shared": { singleton: true, strictVersion: true, requiredVersion: '0.0,1', version: '0.0.1' }
  },
  skip: [],
  sharedMappings: [
    '@my-project/shared'
  ]
});

console.log(config);
module.exports = config;
// Console output:
{
  name: 'main-app',
  exposes: {},
  shared: {
    '@angular/animations': {
      requiredVersion: '^17.1.0',
      singleton: true,
      strictVersion: true,
      version: '^17.1.0',
      includeSecondaries: undefined
    },
    // ... etc
    '@my-project/shared': {
      requiredVersion: '0.0.1', // Note: this is correct, but will be replaced with '' in remoteEntry.json)
      singleton: true,
      strictVersion: true,
      version: '0.0.1', // Note: same as above
      includeSecondaries: undefined
    }
  },
  sharedMappings: [
    {
      key: '@my-project/shared',
      path: '/path/to/src/@my-project/dist/@my-project/shared'
    }
  ]
}

Note that the path for @my-project/shared is correct at this point. If it attempted to read the package.json file from here, it would work as expected. However, during the build process, once the builder gets into package-info.js, the getPackageInfo() function seems to ignore the mapping path, causing it to look up the (incomplete) package.json file from the workspace root src/@my-project/shared/package.json instead of the built directory src/@my-project/dist/@my-project/shared/package.json.

I'm hoping I've just made a config error that you could point me towards, but if this is indeed a bug I hope all of this information helps.

If you'd like an example repo, I've provided https://github.com/nickbelling/AngularModules/tree/native-federation as a stripped-down example that should hopefully demonstrate the issue.

Like I said, I'm hoping this is simply a configuration issue on my part! Thanks in advance for looking into this.

nickbelling commented 7 months ago

Sorry to bump this, but I am hoping to integrate this library, and am just trying to figure out whether I need to manually modify the remoteEntry.json file post-build. Would it be possible to get a sanity check as to whether I just have a simple config option wrong, or whether this is a real issue that needs fixing at the native-federation level?

nicdenny commented 7 months ago

Can you also share the content of the tsconfig.json files?

nickbelling commented 7 months ago

Can you also share the content of the tsconfig.json files?

They are in the repo I linked above, but here are direct links:

https://github.com/nickbelling/AngularModules/blob/native-federation/src/%40my-project/tsconfig.json https://github.com/nickbelling/AngularModules/blob/native-federation/src/%40my-project/main/tsconfig.app.json https://github.com/nickbelling/AngularModules/blob/native-federation/src/%40my-project/shared/tsconfig.lib.json https://github.com/nickbelling/AngularModules/blob/native-federation/src/plugin/plugin/tsconfig.json https://github.com/nickbelling/AngularModules/blob/native-federation/src/plugin/plugin/tsconfig.app.json

edit: wrong branch

nicdenny commented 7 months ago

Can you also share the content of the tsconfig.json files?

I'm sorry totally missed the repo link thanks!

have you tried to change the paths in tsconfig to aim to the src/@my-project/shared/src/public-api.ts folder instead of the one in the dist folder

from

"paths": {
      "@my-project/shared": [
        "./dist/@my-project/shared"
      ]
    },

to something like:

"paths": {
      "@my-project/shared": [
        "src/@my-project/shared/src/public-api.ts"
      ]
    },
nickbelling commented 7 months ago

Sadly that doesn't seem to change anything. As I pointed out in the initial issue, it's searching for a package.json file. It's pointed at /dist because the package.json file for the shared library doesn't exist until it's built, but the getPackageInfo() method inside the native-federation builder's package-info.js is ignoring the mapping that would allow it to read that package.json''s version value.

nickbelling commented 6 months ago

@manfredsteyer I am so very sorry to tag you directly - but now, several months later, I am beginning to be more and more convinced there is a bug in /libs/native-federation-core/src/lib/utils/package-info.ts where it is ignoring the output of a tsconfig.json-path-redirected sharedMapping.

Note in my example repo (note the native-federation branch, not main), when running ng build @my-project/main from the /src/@my-project directory, the native federation builder fails to acknowledge the redirection for my shared library in tsconfig.json:

image

and produces a non-semver'd file both in the built output and the inputmap.json/remoteEntry.json files.

image

When I console.log the withNativeFederation output where I have passed the library as a sharedMapping, I can see that it successfully resolved the correct path to the shared library:

  sharedMappings: [
    {
      key: '@my-project/shared',
      path: '/Users/nickbelling/Code/AngularModules/src/@my-project/dist/@my-project/shared'
    }
  ]

This path has a built package.json file with a valid entry point.

However, the native federation builder is still outputting this:

 INFO  Building federation artefacts
 WARN  No entry point found for @my-project/shared
 WARN  If you don't need this package, skip it in your federation.config.js or consider moving it into depDependencies in your package.json
 WARN  No meta data found for shared lib @my-project/shared

And when I put a breakpoint in package-info.js where that line is called during the build process, I can see that it instead looked up the actual location of the source library's package.json (as it exists prior to ng-packagr building it) instead of the location provided to it from the withNativeFederation output (as it exists in the /dist folder after being built). I can't modify the source package.json file, as it's then invalid in either the workspace or when building the shared lib.

Again, I apologise for the direct tag. I am hoping you can just point me at a fundamental mistake I have made, however if I have, I cannot see it.