I'm trying to implement Module Federation with Angular and NgRX, but i'm facing a problem and don't know how to fix it.
The problem is: when X application lazy loads a module from Y
application that uses Firebase, angular does not recognize the fire
auth provider.
I have 2 apps: Auth and Dashboard.
My Auth application uses firebase to do user login.
The firebase request login is made by a NgRX effect:
import {AngularFireAuth} from '#angular/fire/auth';
#Injectable()
export class AuthEffects {
userLogin$: Observable<Action> = createEffect(() => {
/* effect implementation */
});
constructor(private fireAuth: AngularFireAuth){}
}
The AuthModule imports:
imports: [
/* ...other imports */
StoreModule.forFeature('authState', authReducer),
EffectsModule.forFeature([AuthEffects]),
AngularFireModule.initializeApp(firebaseConfig),
AngularFireAuthModule
]
The Dashboard AppRoutingModule imports:
const routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('auth/AuthModule').then(m => m.AuthModule)
}
];
#NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [RouterModule]
})
When I start only the Auth application, everything works fine and I can do login.
But when I try to use the Auth application remotely, inside Dashboard application, I get this error:
NullInjectorError: StaticInjectorError(AppModule)[InjectionToken angularfire2.app.options]:
StaticInjectorError(Platform: core)[InjectionToken angularfire2.app.options]:
NullInjectorError: No provider for InjectionToken angularfire2.app.options!
Partial Auth - webpack.config.js
plugins: [
new ModuleFederationPlugin({
name: 'auth',
library: {type: 'var', name: 'auth'},
filename: 'remoteEntry.js',
exposes: {
'./AuthModule': './src/app/login/auth.module.ts',
'./AuthComponent': './src/app/login/auth.component.ts',
'./AuthActions': './src/app/store/actions/auth.actions.ts',
'./AuthReducer': './src/app/store/reducers/auth.reducer.ts',
'./AuthEffects': './src/app/store/effects/auth.effects'
},
shared: {
...dependencies,
'#angular/core': {
requiredVersion: dependencies['#angular/core'],
singleton: true,
},
'#angular/common': {
requiredVersion: dependencies['#angular/common'],
singleton: true,
},
'#angular/router': {
requiredVersion: dependencies['#angular/router'],
singleton: true,
}
}
})
]
Parial Dashboard - webpack.config.js
plugins: [
new ModuleFederationPlugin({
name: 'dashboard',
library: {type: 'var', name: 'dashboard'},
filename: 'remoteEntry.js',
remotes: {
auth: 'auth',
},
shared: {
...dependencies,
'#angular/core': {
requiredVersion: dependencies['#angular/core'],
singleton: true,
},
'#angular/common': {
requiredVersion: dependencies['#angular/common'],
singleton: true,
},
'#angular/router': {
requiredVersion: dependencies['#angular/router'],
singleton: true,
}
}
})
]
I tried to resolve it by myself, but Module Federation is a new thing and we have little posts about.
Can someone help me? If you came until here, thank you very much! :D
Solved!
Thanks Zack Jackson, I could solve the problem.
Solution:
https://github.com/module-federation/module-federation.github.io/issues/14#issuecomment-672647713
Related
I have a vite SPA and use dynamic imports to load my json files. Since I need to load numerous very large json files, the index.js file in my /dist becomes too big in my production build.
What is the best way to import these json files dynamically but still keep small chunks? Can I import somehow the json files dynamically as own chunks, similar to images and videos with hashes?
Here my vite.config.js
import path from 'path'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
import { createHtmlPlugin } from 'vite-plugin-html'
import svgr from 'vite-plugin-svgr'
import legacy from '#vitejs/plugin-legacy'
import react from '#vitejs/plugin-react'
import { name } from './package.json'
export default defineConfig({
base: '/widgets/' + name + '/',
server: {
open: true, // Define a BROWSER in your .env-File to specify which browser. Defaults to Chrome. https://vitejs.dev/config/#server-open
port: 3000,
},
resolve: {
alias: {
'#Assets': path.resolve(__dirname, 'src/assets'),
'#Components': path.resolve(__dirname, 'src/components'),
'#Examples': path.resolve(__dirname, 'src/examples'),
'#Scripts': path.resolve(__dirname, 'src/scripts'),
'#Styles': path.resolve(__dirname, 'src/assets/styles'),
'#Cms': path.resolve(__dirname, 'src/assets/styles/cms'),
},
},
css: {
devSourcemap: true, // needed for css imported in cms template
},
define: {
__DATA_PATH__: JSON.stringify(process.env.npm_package_config_dataPath),
},
build: {
rollupOptions: {
output: {
entryFileNames: `[name].js`,
chunkFileNames: `[name].js`,
assetFileNames: `[name].[ext]`,
},
},
},
plugins: [
legacy({
polyfills: true,
}),
react(),
createHtmlPlugin({
minify: true,
inject: {
data: {
title: name,
id: name,
},
},
}),
cssInjectedByJsPlugin(),
svgr(),
],
})
You can try this configuration:
// vite.config.ts
build: {
chunkSizeWarningLimit: 500, // default 500 KB
},
https://vitejs.dev/config/build-options.html#build-chunksizewarninglimit
I have a stupid simple question that I basically failed to find a good answer for.
I have a nuxt2 project that has a #nuxtjs/router module on it. I have added the module on the buildModules on nuxt.config.js and created router.js on the src folder.
this is my nuxt.config.js file :
ssr: true, // tauri
target: 'static', // tauri
server : {
host:"127.0.0.1",
port: 8001
},
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
...
},
env:{
MEDIA_API:process.env.VUE_APP_MEDIA_API,
API_URL: process.env.API_URL
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [
...
],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
'#nuxtjs/router'
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
...
'#nuxtjs/router',
...
],
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
extractCSS: true,
plugins: [ // to import jQuery :"
new webpack.ProvidePlugin({
jQuery: 'jquery',
$: 'jquery',
'window.jQuery': 'jquery',
'window.$': 'jquery',
}),
],
standalone: true
},
router: {
middleware: ['auth']
},
auth: {
...
}
and here is my router.js file :
import { parseHTML } from 'jquery';
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
// this is just a function to help me not writing the full path of each page
const page = (path) => () => import(`~/pages/${path}`).then(m => m.default || m)
const routes = [
{path: '/', name: 'home', component: page('index.vue')},
{path: '/login', name: 'login', component: page('login.vue')},
{path: '/players', name: 'allPlayers', component: page('players/index.vue')},
{path: '/players/:id', name: 'singlePlayer', component: page('players/view.vue')},
{path: '/plans', name: 'allPlans', component: page('plans/index.vue')},
{path: '/plans/new', name: 'newPlan', component: page('plans/new.vue')},
{path: '/activities', name : 'allActs', component: page ('activities/index.vue')},
{path: '/activities/new', name: 'newAct', component: page('activities/new.vue')},
{path: '/activityPlayer/:id', name: 'viewActivityPlayer', component: page('activities/viewActivityPlayer')},
{path: '/auth/login', name: 'auth.login', component: page('auth/login')},
{path: '/superAdmin/', name: 'superAdmin', component: page('superAdmin/index.vue')},
{path: '/superAdmin/viewAll', name: 'viewAdmins', component: page('superAdmin/viewAdmins.vue')},
];
export function createRouter() {
return new Router({
routes,
mode: 'history'
})
}
I want to generate full static build to deploy my nuxt app on a tauri build. I was able to successfully deploy a nuxt app that does NOT has that router.js file. The build generate just generate all routes by default in the dist folder.
How can I generate the routes ?
I've tried with the /login path and it's working great as shown in this commit, there was a typo in the path tho (I did tried only for that one since it was an obvious one for me).
I also removed the useless package-lock.json since you use yarn in your project (could be vice-versa of course) and since you should not use both at the same time. Added a few explicit keys in the nuxt.config.js file too.
Commented #nuxtjs/auth-next, ran yarn generate && yarn start and I have successfully access to the given path.
The generated route files are maybe not that friendly (because of their hash) but they are still available in the dist directory. There is a way to make them prettier, you could search for that on Stackoverflow/Google.
Update: it works with the auth middleware too actually.
I have a legacy app were we are trying to export some components with WP5 module-federation.
The plugin configuration is currently like this:
output: {
filename: "[name].bundle.js",
},
plugins: [
new ModuleFederationPlugin({
name: "remote",
filename: "remoteEntry.js",
remotes: {},
exposes: {
"./TestExport": path.resolve(__dirname, "../src/TestExport"),
},
shared: {
"react#17": {
singleton: true,
requiredVersion: deps.react,
},
},
}
),
]
the webpack config file is in a config dir in the root folder, this is why we are exposing the component with path.resolve.
When compiling the code, the remoteEntry.js file is not generated and we can't import it in another application using module-federation.
I've tried creating an app and exporting something with module-federation and consuming it inside the legacy app and it worked, but the opposite doesn't work.
I feel like this should be resolved simply, but after several attempts, I'm just not coming to a resolution.
Here is the error I've received:
Uncaught ReferenceError: process is not defined
38509 # [PathToProject]\node_modules\util\util.js:109
This is getting triggered when I instantiate web3 into a clean/new site (there's two other 'test' components, one link one button)
I've searched and found numerous bits of information suggesting that
process is a server side 'node' var, and I can set this to be available client-side by adding to my webpack.config.js, which I have done.
I might be able to resolve by just declaring a global angular var in my app.component.ts, but it seems this dependency project .js file is not accessing it.
I've also tried just updating the dependency project directly, but even with a compile, it seems that my changes are not being distributed into the webpack build /dist/ result.
I think this probably has a blatantly simple solution, but I'm just overlooking it. I'm just spinning my tires here and could use a little help, but I'm the first in my circle to venture into web3 and don't have a close friend to bounce this off of. Can someone here provide some insight or an alternative resolve this issue?
Relevant bits of code:
webpack.config.js
var webpack = require('webpack');
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.(sass|less|css|ts)$/,
use: [
'ts-loader'
],
}
],
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': 'develop',
})
],
entry: './src/main.ts',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
resolve: {
extensions: [ '.js', '.ts', '.html' ],
modules: [
path.resolve(__dirname, 'node_modules/'),
path.resolve("", "src")
],
alias: {
Environments: path.resolve(__dirname, 'src/environments/'),
},
fallback: {
"fs": false,
"tls": false,
"net": false,
"path": false,
"zlib": false,
"http": require.resolve("stream-http"),
"https": require.resolve("https-browserify"),
"stream": false,
"crypto": require.resolve("crypto-browserify"),
"crypto-browserify": require.resolve('crypto-browserify'),
},
}
}
global-constants.ts
export class GlobalConstants {
public static process: any = {
env: {
NODE_ENV: 'development'
}
}
}
app.component.ts
import { Component } from '#angular/core';
import{ GlobalConstants } from './common/global-constants';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'Cool Title';
process = GlobalConstants.process;
}
Relevant bit of utils/util.js (line 106-116)
var debugs = {};
var debugEnvRegex = /^$/;
if (process.env.NODE_DEBUG) {
var debugEnv = process.env.NODE_DEBUG;
debugEnv = debugEnv.replace(/[|\\{}()[\]^$+?.]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/,/g, '$|^')
.toUpperCase();
debugEnvRegex = new RegExp('^' + debugEnv + '$', 'i');
}
Add the following to polyfills.ts (usually inside src directory):
(window as any).global = window;
global.Buffer = global.Buffer || require('buffer').Buffer;
global.process = require('process');
We want to divide our large frontend projects into multiple separately deployed projects which are easier to work with. I am trying to include a bundled ngModule to handle a route from within another app. The apps must be ignorant of each other's configuration. The bundles will share some large dependencies(like Angular) via globals. We don't need to shake across the bundles and we may just have to accept some duplicate dependencies.
The root router complains that
Error: No NgModule metadata found for 'TestsetModule'.
which leads me to believe the child module is not being angular compiled on load, or is not registering its module for some reason. I think it may be necessary to manually compile the module, but I'm not sure how to use this https://angular.io/api/core/Compiler#compileModuleAndAllComponentsAsync
The root app loads the child via a route:
import { ModuleWithProviders } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
const load = require("little-loader");
const routes: Routes = [
{ path: ``, loadChildren: () => new Promise(function (resolve) {
load('http://localhost:3100/testset-module-bundle.js',(err: any) => {
console.log('global loaded bundle is: ', (<any>global).TestsetModule )
resolve((<any>global).TestsetModule)
}
)
})}
];
export const HostRouting: ModuleWithProviders = RouterModule.forRoot(routes);
I also tried using angular router's string resolution syntax rather than this weird global thing you see but I had similar issues.
Here is the module which is being loaded, very standard except for the global export:
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { HttpModule } from '#angular/http';
//import { MaterialModule } from '#angular/material';
import { FlexLayoutModule } from '#angular/flex-layout';
import { FormsModule } from '#angular/forms';
import { LoggerModule, Level } from '#churro/ngx-log';
import { FeatureLoggerConfig } from './features/logger/services/feature-logger-config';
import { TestsetComponent } from './features/testset/testset.component';
import { TestsetRouting } from './testset.routing';
#NgModule({
imports: [
CommonModule,
//MaterialModule,
FlexLayoutModule,
HttpModule,
FormsModule,
LoggerModule.forChild({
moduleName: 'Testset',
minLevel: Level.INFO
}),
TestsetRouting,
],
declarations: [TestsetComponent],
providers: [
/* TODO: Providers go here */
]
})
class TestsetModule { }
(<any>global).TestsetModule = TestsetModule
export {TestsetModule as default, TestsetModule};
Here is the webpack configuration of the root bundle. Note the global exports via the poorly named "ProvidePlugin".
const webpack = require('webpack');
const AotPlugin = require('#ngtools/webpack').AotPlugin;
const path = require('path');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const IgnorePlugin = require('webpack/lib/IgnorePlugin');
const PolyfillsPlugin = require('webpack-polyfills-plugin');
const WebpackSystemRegister = require('webpack-system-register');
module.exports = (envOptions) => {
envOptions = envOptions || {};
const config = {
entry: {
'bundle': './root.ts'
},
output: {
libraryTarget: 'umd',
filename: '[name].js',//"bundle.[hash].js",
chunkFilename: '[name]-chunk.js',
path: __dirname
},
externals: {
},
resolve: {
extensions: ['.ts', '.js', '.html'],
},
module: {
rules: [
{ test: /\.html$/, loader: 'raw-loader' },
{ test: /\.css$/, loader: 'raw-loader' },
]
},
devtool: '#source-map',
plugins: [
new webpack.ProvidePlugin({
'angular': '#angular/core',
'ngrouter': '#angular/router',
'ngxlog':'#churro/ngx-log'
})
]
};
config.module.rules.push(
{ test: /\.ts$/, loaders: [
'awesome-typescript-loader',
'angular-router-loader',
'angular2-template-loader',
'source-map-loader'
] }
);
}
return config;
};
And here is the webpack configuration of the child bundle. Note the "externals" which look for angular as a global.
module.exports = (envOptions) => {
envOptions = envOptions || {};
const config = {
entry: {
'testset-module-bundle': './src/index.ts'
},
output: {
//library: 'TestsetModule',
libraryTarget: 'umd',
filename: '[name].js',//"bundle.[hash].js",
chunkFilename: '[name]-chunk.js',
path: path.resolve(__dirname, "dist")
},
externals: {
//expect these to come from the app that imported us
// name to be required : name from global
'angular': '#angular/core',
'ngrouter': '#angular/router',
'ngxlog': '#churro/ngx-log'
},
resolve: {
extensions: ['.ts', '.js', '.html'],
},
module: {
rules: [
{ test: /\.html$/, loader: 'raw-loader' },
{ test: /\.css$/, loader: 'raw-loader' },
]
},
devtool: '#source-map',
plugins: [
]
};
config.module.rules.push(
{ test: /\.ts$/, loaders: [
'awesome-typescript-loader',
'angular-router-loader',
'angular2-template-loader',
'source-map-loader'
] }
);
}
return config;
};
And for good measure here is my tsconfig file which 'awesome-typescript-loader' reads.
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true,
"baseUrl": ".",
"rootDir": "src",
"outDir": "app",
"paths": {
"#capone/*": [
"*"
],
"#angular/*": [
"node_modules/#angular/*"
],
"rxjs/*": [
"node_modules/rxjs/*"
]
}
},
"exclude": ["node_modules", "src/node_modules", "compiled", "src/dev_wrapper_app"],
"angularCompilerOptions": {
"genDir": "./compiled",
"skipMetadataEmit": true
}
}
If you're still reading, awesome. I was able to get this working when both bundles are part of the same webpack config and the child module is just a chunk. Angular is designed to do that. But our use case is to have the children and parent be ignorant of each other until runtime.
As you have mentioned
The apps must be ignorant of each other's configuration.
I had a similar problem in Angular2. I solved it by creating a sub-application. A separate sub-main.browser.ts and index.html file. It had its own dependencies, sharing the same node modules. Both main modules bootstrapping different app-component. We were working on Angular without angular-cli.
In webpack config, I added
entry: {
'polyfills': './src/polyfills.browser.ts',
'main' . : './src/main.browser.aot.ts',
'sub-main' : '/src/sub-main.browser.ts'
},
and a more detailed HtmlWebpackPlugin. In the chunks, we load only modules that will be used in both the app. If we see polyfills is common.
new HtmlWebpackPlugin({
template: 'src/index.html',
title: METADATA.title,
chunksSortMode: 'dependency',
metadata: METADATA,
inject: 'head',
chunks: ['polyfills','main']
}),
new HtmlWebpackPlugin({
template: 'src/index2.html',
title: 'Sub app',
chunksSortMode: 'dependency',
metadata: METADATA,
inject: 'head',
filename: './sub-app.html',
chunks: ['polyfills','sub-main']
}),
The next task was to create separate endpoints for both sub apps for dev environment.
devServer: {
port: METADATA.port,
host: METADATA.host,
historyApiFallback: true,
watchOptions: {
aggregateTimeout: 300,
poll: 1000
},
proxy: {
"/sub-app": {
target: "http://localhost:3009",
bypass: function(req, res, proxyOptions) {
return "/index2.html";
}
}
}
},
Now when I build the project two different HTML files are generated. Each with their own javascript bundle dependencies and common assets. They can be deployed on a different server as well.
I was able to finish my POC with lots of trial and error. My suggestion will be to look a step above angular. See how webpack is deploying your current project. And If you can configure it to serve your purpose.