Webpack Module Federation changes names of shared libraries to numbers - javascript

With Webpack Module Federation, how do I retain development filenames when building as production?
Currently, it's changing them all to numbers like 3279.js instead of something like src_applications_myApp_jsx.js.
Parts of the Webpack config:
const { dependencies } = require('../package.json');
output: {
chunkFilename: 'vendor/[name].js',
filename: '[name]/app.js',
},
new webpack.container.ModuleFederationPlugin({
shared: dependencies,
}),
The issue is the chunkFilename. Changing it to 'vendor/[id].js' doesn't change anything either.
Webpack's docs say the [name] property will only work if the chunk has a name. So I guess, why is a name not set?

for chunks you can just use
webpackConfig.optimization.chunkIds='named'
It'll keep your chunk names readable, see link for further docu
https://webpack.js.org/configuration/optimization/#optimizationchunkids

Related

Use different output.libraryTarget for a Web Worker

I have this Webpack configuration:
{
output: {
libraryTarget: "system",
...
},
...
}
I'm trying to use a Web Worker. I'm using the standard Webpack 5 syntax:
new Worker(new URL('./MyWorker', import.meta.url));
Now Webpack outputs the Web Worker as a System.js module. How can I change it to something different, like ES module, without affecting the main bundle?
You can use chunkFormat to specify what the chunk formats are, workers are by default array-push, https://webpack.js.org/configuration/output/#outputchunkformat.
You can also create multiple configs with different targets for different entries.
const config = {
//
}
module.exports = (env) => {
if (env.module) {
config.entry = //path/to/index
config.output.libraryTarget = 'module'
} else {
config.entry = //path/to/worker
config.output.libraryTarget = 'umd'
}
return config
}
Then you can separately compile your web workers or chunks different from others. You can also use chunkFileName: () => along with that.
If you want to compile it in a single run, without having to run it twice using different configs, you can also manually invoke the webpack compiler with both configs.
import {Compiler} from 'webpack'
// or import webpack from 'webpack'
compiler.run(config)
compiler.run(config2)
Then you can run both of them at the same time and compile everything.
Another possible option is enabledChunkLoadingTypes, https://webpack.js.org/configuration/output/#outputenabledchunkloadingtypes, which will allow you to specify the available loading types for chunks which webpack will automatically use based on the entry function. I've never used that myself so I don't know if that'll work but it's something you can try.

How to include manual import() in Webpack Bundle

I am quite new to Webpack, so bear with me if thats a stupid question.
My goal is to transform my old, AMD based codebase to a ES6 Module based solution. What I am struggling with is handling dynamic import()s. So my app router works on a module basis, i.e. each route is mapped to a module path and then required. Since I know what modules will be included, I just add those dynamically imported modules to my r.js configuration and am able to build everything in a single file, with all require calls still working.
Now, I am trying to do the same with ES6 modules and Webpack. With my devmode this is no problem as I can just replace require() with import(). However I cannot get this to work with bundling. Either Webpack splits my code (and still fails to load the dynamic module anyways), or - if I use the Array format for the entry config, the dynamic module is included in the bundle but loading still fails: Error: Cannot find module '/src/app/DynClass.js'
This is how my Webpack config looks like:
const webpack = require('webpack');
const path = require('path');
module.exports = {
mode: "development",
entry: ['./main.js', './app/DynClass.js'],
output: {
filename: 'main.js',
path: path.resolve(__dirname, "../client/")
},
resolve: {
alias: {
"/src": path.resolve(__dirname, '')
}
},
module: {
rules: [
{
test: /\.tpl$/i,
use: 'raw-loader',
},
]
}
};
So basically I want to tell Webpack: "hey, there is another module (or more) that is to be loaded dynamically and I want it to be included in the bundle"
How can I do this?
So yeah, after much fiddling there seems to be a light at the end of the tunnel. Still, this is not a 100% solution and it is surely not for the faint of heart, as it is quite ugly and fragile. But still I want to share my approach with you:
1) manual parsing of my routes config
My router uses a config file looking like this:
import StaticClass from "/src/app/StaticClass.js";
export default {
StaticClass: {
match: /^\//,
module: StaticClass
},
DynClass: {
match: /^\//,
module: "/src/app/DynClass.js"
}
};
So as you can see the export is an object, with keys acting as the route id, and an object that contains the matches (regex based) and the module which should be executed by the router if the route matches. I can feed my router with both a Constructor function (or an object) for modules which are available immediatly (i.e. contained in the main chunk) or if the module value is a string, this means that the router has to load this module dynamically by using the path specified in the string.
So as I know what modules could be potentially loaded (but not if and when) I can now parse this file within my build process and transform the route config to something webpack can understand:
const path = require("path");
const fs = require("fs");
let routesSource = fs.readFileSync(path.resolve(__dirname, "app/routes.js"), "utf8");
routesSource = routesSource.substr(routesSource.indexOf("export default"));
routesSource = routesSource.replace(/module:\s*((?!".*").)*$/gm, "module: undefined,");
routesSource = routesSource.replace(/\r?\n|\r/g, "").replace("export default", "var routes = ");
eval(routesSource);
let dummySource = Object.entries(routes).reduce((acc, [routeName, routeConfig]) => {
if (typeof routeConfig.module === "string") {
return acc + `import(/* webpackChunkName: "${routeName}" */"${routeConfig.module}");`;
}
return acc;
}, "") + "export default ''";
(Yeah I know this is quite ugly and also a bit brittle so this surely could be done better)
Essentially I create a new, virtual module where every route entry which demands a dynamic import is translated, so:
DynClass: {
match: /^\//,
module: "/src/app/DynClass.js"
}
becomes:
import(/* webpackChunkName: "DynClass" */"/src/app/DynClass.js");
So the route id simply becomes the name of the chunk!
2) including the virtual module in the build
For this I use the virtual-module-webpack-plugin:
plugins: [
new VirtualModulePlugin({
moduleName: "./app/dummy.js",
contents: dummySource
})
],
Where dummySource is just a string containing the sourcecode of my virtual module I just have generated. Now, this module is pulled in and the "virtual imports" can be processed by webpack. But wait, I still need to import the dummy module, but I do not have any in my development mode (where I use everything natively, so no loaders).
So in my main code I do the following:
let isDev = false;
/** #remove */
isDev = true;
/** #endremove */
if (isDev) { import('./app/dummy.js'); }
Where "dummy.js" is just an empty stub module while I am in development mode. The parts between that special comments are removed while building (using the webpack-loader-clean-pragma loader), so while webpack "sees" the import for dummy.js, this code will not be executed in the build itself since then isDev evaluates to false. And since we already defined a virtual module with the same path, the virtual module is included while building just like I want, and of course all dependencies are resolved as well.
3) Handling the actual loading
For development, this is quite easy:
import routes from './app/routes.js';
Object.entries(routes).forEach(async ([routeId, route]) => {
if (typeof route.module === "function") {
new route.module;
} else {
const result = await import(route.module);
new result.default;
}
});
(Note that this is not the actual router code, just enough to help me with my PoC)
Well, but for the build I need something else, so I added some code specific to the build environment:
/** #remove */
const result = await import(route.module);
new result.default;
/** #endremove */
if (!isDev) {
if (typeof route.module === "string") { await __webpack_require__.e(routeId); }
const result = __webpack_require__(route.module.replace("/src", "."));
new result.default;
}
Now, the loading code for the dev environment is just stripped out, and there is another loading code that uses webpack internally. I also check if the module value is a function or string, and if it is the latter I invoke the internal require.ensure function to load the correct chunk: await __webpack_require__.e(routeId);. Remember that I named my chunks when generating the virtual module? Now thats why I still can find them now!
4) more needs to be done
Another thing I encountered is when several dynamically loaded modules have the same dependencies, webpack tries to generate more chunks with names like module1~module2.bundle.js, breaking my build. To counter this, I needed to make sure that all those shared modules go into a specific named bundle I called "shared":
optimization: {
splitChunks: {
chunks: "all",
name: "shared"
}
}
And when in production mode, I simply load this chunk manually before any dynamic modules depending on it are requested:
if (!isDev) {
await __webpack_require__.e("shared");
}
Again, this code only runs in production mode!
Finally, I have to prevent webpack renaming my modules (and chunks) to something like "1", "2" etc, but rather keep the names I just have defined:
optimization: {
namedChunks: true,
namedModules: true
}
Se yeah, there you have it! As I said this wasn't pretty but seems to work, at least with my simplified test setup. I really hope there aren't any blockers ahead of me when I do all the rest (like ESLint, SCSS etc)!

webpack 2 expose webworker as global

I'm trying to write tests for a react/redux app, and we have a bunch of webworkers which are currently imported via require('worker-loader?inline!downloadTrackWorker')
I've been going in circles trying to figure out how to separate out this code so I can run tests in node.js without having trouble with loading the webworker.
One solution I came up with was to expose the webworker globally in my webpack, which would mean I could define a stub or mock in my tests.
In my webpack config, I've added
module: {
loaders: [...],
rules: [{
test: require.resolve(APP_DIR + '/helpers/downloadTrackWorkerLoader'),
loader: 'expose-loader',
options: 'DownloadTrackWorker'
}]
}
my trackWorkerLoader is simply
const DownloadTrackWorker = require('worker-loader?inline!./downloadTrackWorker.js');
module.export = DownloadTrackWorker;
I've also tried the above without inline, but no luck.
I'm experiencing two problems.
when I look for DownloadTrackWorker in my console, it is undefined
with my updated webpack.config, I get an error that webpack can't parse may need appropriate loader at
ReactDOM.render(
<Provider store={store}>
<Player />
</Provider>,
document.getElementById('root')
);
Any suggestions on what I'm doing wrong? It appears to me the issues I'm seeing are related.
when I look for DownloadTrackWorker in my console, it is undefined
As the expose-loader notes in Readme - Usage, you need to import it in order to be included in the bundle and therefore exposed. The rules are not including anything but are applied to the imports in your app which satisfy the test. Besides that you're also not applying the loader to the correct file. You want to apply the expose-loader to trackWorkerLoader.js, so the correct rule would be:
{
test: require.resolve(APP_DIR + '/helpers/trackWorkerLoader'),
loader: 'expose-loader',
options: 'DownloadTrackWorker'
}
Now you need to import it somewhere in your app with:
require('./path/to/helpers/trackWorkerLoader');
This will correctly expose DownloadTrackWorker as a global variable, but you have a typo in trackWorkerLoader.js instead of module.exports you have module.export. Currently you're not actually exporting anything. It should be:
module.exports = DownloadTrackWorker;
Instead of inlining the worker-loader in the require (not talking about its option) you can also define it as a rule:
{
test: require.resolve(APP_DIR + '/helpers/downloadTrackWorker'),
loader: 'worker-loader',
options: {
inline: true
}
}
And now you can simply require it without needing to specify the loaders in trackWorkerLoader.js:
const DownloadTrackWorker = require('./downloadTrackWorker');
module.exports = DownloadTrackWorker;
with my updated webpack.config, I get an error that webpack can't parse may need appropriate loader
You're defining both module.loaders and module.rules at the same time. Although module.loaders still exists for compatibility reasons, it will be ignored completely if module.rules is present. Hence the loaders you configured before, are not being applied. Simply move all rules to module.rules.

Define global variable with webpack

Is it possible to define a global variable with webpack to result something like this:
var myvar = {};
All of the examples I saw were using external file require("imports?$=jquery!./file.js")
There are several way to approach globals:
1. Put your variables in a module.
Webpack evaluates modules only once, so your instance remains global and carries changes through from module to module. So if you create something like a globals.js and export an object of all your globals then you can import './globals' and read/write to these globals. You can import into one module, make changes to the object from a function and import into another module and read those changes in a function. Also remember the order things happen. Webpack will first take all the imports and load them up in order starting in your entry.js. Then it will execute entry.js. So where you read/write to globals is important. Is it from the root scope of a module or in a function called later?
config.js
export default {
FOO: 'bar'
}
somefile.js
import CONFIG from './config.js'
console.log(`FOO: ${CONFIG.FOO}`)
Note: If you want the instance to be new each time, then use an ES6 class. Traditionally in JS you would capitalize classes (as opposed to the lowercase for objects) like
import FooBar from './foo-bar' // <-- Usage: myFooBar = new FooBar()
2. Use Webpack's ProvidePlugin.
Here's how you can do it using Webpack's ProvidePlugin (which makes a module available as a variable in every module and only those modules where you actually use it). This is useful when you don't want to keep typing import Bar from 'foo' again and again. Or you can bring in a package like jQuery or lodash as global here (although you might take a look at Webpack's Externals).
Step 1. Create any module. For example, a global set of utilities would be handy:
utils.js
export function sayHello () {
console.log('hello')
}
Step 2. Alias the module and add to ProvidePlugin:
webpack.config.js
var webpack = require("webpack");
var path = require("path");
// ...
module.exports = {
// ...
resolve: {
extensions: ['', '.js'],
alias: {
'utils': path.resolve(__dirname, './utils') // <-- When you build or restart dev-server, you'll get an error if the path to your utils.js file is incorrect.
}
},
plugins: [
// ...
new webpack.ProvidePlugin({
'utils': 'utils'
})
]
}
Now just call utils.sayHello() in any js file and it should work. Make sure you restart your dev-server if you are using that with Webpack.
Note: Don't forget to tell your linter about the global, so it won't complain. For example, see my answer for ESLint here.
3. Use Webpack's DefinePlugin.
If you just want to use const with string values for your globals, then you can add this plugin to your list of Webpack plugins:
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify("5fa3b9"),
BROWSER_SUPPORTS_HTML5: true,
TWO: "1+1",
"typeof window": JSON.stringify("object")
})
Use it like:
console.log("Running App version " + VERSION);
if(!BROWSER_SUPPORTS_HTML5) require("html5shiv");
4. Use the global window object (or Node's global).
window.foo = 'bar' // For SPA's, browser environment.
global.foo = 'bar' // Webpack will automatically convert this to window if your project is targeted for web (default), read more here: https://webpack.js.org/configuration/node/
You'll see this commonly used for polyfills, for example: window.Promise = Bluebird
5. Use a package like dotenv.
(For server side projects) The dotenv package will take a local configuration file (which you could add to your .gitignore if there are any keys/credentials) and adds your configuration variables to Node's process.env object.
// As early as possible in your application, require and configure dotenv.
require('dotenv').config()
Create a .env file in the root directory of your project. Add environment-specific variables on new lines in the form of NAME=VALUE. For example:
DB_HOST=localhost
DB_USER=root
DB_PASS=s1mpl3
That's it.
process.env now has the keys and values you defined in your .env file.
var db = require('db')
db.connect({
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASS
})
Notes
Regarding Webpack's Externals, use it if you want to exclude some modules from being included in your built bundle. Webpack will make the module globally available but won't put it in your bundle. This is handy for big libraries like jQuery (because tree shaking external packages doesn't work in Webpack) where you have these loaded on your page already in separate script tags (perhaps from a CDN).
I was about to ask the very same question. After searching a bit further and decyphering part of webpack's documentation I think that what you want is the output.library and output.libraryTarget in the webpack.config.js file.
For example:
js/index.js:
var foo = 3;
var bar = true;
webpack.config.js
module.exports = {
...
entry: './js/index.js',
output: {
path: './www/js/',
filename: 'index.js',
library: 'myLibrary',
libraryTarget: 'var'
...
}
Now if you link the generated www/js/index.js file in a html script tag you can access to myLibrary.foo from anywhere in your other scripts.
Use DefinePlugin.
The DefinePlugin allows you to create global constants which can be
configured at compile time.
new webpack.DefinePlugin(definitions)
Example:
plugins: [
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true)
})
//...
]
Usage:
console.log(`Environment is in production: ${PRODUCTION}`);
You can use define window.myvar = {}.
When you want to use it, you can use like window.myvar = 1
DefinePlugin doesn't actually define anything. What it does is replace variables that exist in your bundle code. If the variable doesn't exist in your code, it will do nothing. So it doesn't create global variables.
In order to create a global variable, write it in your code:
window.MyGlobal = MY_GLOBAL;
And use DefinePlugin to replace MY_GLOBAL with some code:
new webpack.DefinePlugin({
'MY_GLOBAL': `'foo'`,
// or
'MY_GLOBAL': `Math.random()`,
}),
Then your output JS will be like this:
window.MyGlobal = 'foo';
// or
window.MyGlobal = Math.random();
But MY_GLOBAL will never actually exist at runtime, because it is never defined. So that's why DefinePlugin has a misleading name.
I solved this issue by setting the global variables as a static properties on the classes to which they are most relevant. In ES5 it looks like this:
var Foo = function(){...};
Foo.globalVar = {};
You may hit this issue, when triing bundle < script > tag js files in some old project.
Do not use webpack for this, it may be even impossible if joining 50+ libraries like jquery and then figuring out all global variables or if they used nested require. I would advice to simply use uglify js instead , which drops all this problems in 2 commands.
npm install uglify-js -g
uglifyjs --compress --mangle --output bundle.js -- js/jquery.js js/silly.js

Webpack multiple named chunks ignoring names at runtime

I am having trouble with webpacks code splitting functionality. I am trying to have 2 named chunks for two routes in my application which are not often visited. mysite.com/settings and mysite.com/access.
here is my webpack.config.coffee
module.exports =
contentBase: "#{__dirname}/src/"
cache: true
entry:
app: './src/coffee/app'
head: './src/coffee/head'
output:
path: path.join(__dirname, 'build')
publicPath: '/'
filename: '[name].js'
chunkFilename: '[name]-[chunkhash].js'
plugins: []
And here is my router.coffee
access: (slug) ->
_this = #
require.ensure ['../view/page/access-page.coffee'], (require) ->
AccessPage = require '../view/page/access-page.coffee'
accessPage = AccessPage.getInstance()
accessPage.render() unless accessPage.isRendered
_this.showPage accessPage
, 'access'
settings: (slug) ->
_this = #
require.ensure ['../view/page/settings-page.coffee'], (require) ->
SettingsPage = require '../view/page/settings-page.coffee'
settingsPage = SettingsPage.getInstance()
settingsPage.render() unless settingsPage.isRendered
_this.showPage settingsPage
, 'settings'
I am not using the webpack dev-server, instead I am watching simply by using the following cmd-line tool
webpack -d --progress --colors --watch
The problem is that it ignores the names when requiring the files, as you can see the format is '[name]-[hash].js' it generates files with the correct format e.g. settings-2j3nekj2n3ejkn2.js but during development, when I attempt to load the page, the browser complains that '-2j3nekj2n3ejkn2.js' cannot be found, somehow the mapping of the files, ignores the names. If I leave out the names, then it works.
So the question is how can I setup mulitple named chunks correctly. Thanks in advance.
Note I have checked out their examples in the docs at https://github.com/webpack/docs/wiki/code-splitting
and I have followed their optimization docs aswell at
https://github.com/webpack/docs/wiki/optimization
But I am stuck
Well the simple answer is - [name= is not supported in chunkName.
The awesome guys at Webpack have actually heard my cries and implemented it
Here is the commit
https://github.com/webpack/webpack/commit/03c87c11a4219ae6ec6bfe87e570a0dacceac859
As a result of the following issue I made
https://github.com/webpack/webpack/issues/358
It is already available as of Beta ^1.3.2

Categories