Injecting variables into webpack - javascript

I'm trying to inject a variable into each module within my webpack bundle in order to have debugging information for JS errors per file. I've enabled
node: {
__filename: true
}
Current file path in webpack
in my webpack.config, but I would like to inject something like
var filename = 'My filename is: ' + __filename;
into each module before compilation. I've seen the Banner Plugin with the raw option but it seems this would only inject the banner outside of the webpack closure rather than my desired result of injecting script into each module.

I use variables to resolve a couple of variables in my webpack.config.js file:
plugins: [
new webpack.DefinePlugin({
ENVIRONMENT: JSON.stringify(process.env.NODE_ENV || 'development'),
VERSION: JSON.stringify(require('./package.json').version)
})
]
It might not be dynamic enough, but if it is, then you might be able to bypass that loader solution.

Write your own loader:
my_project/my_loaders/filename-loader.js:
module.exports = function(source) {
var injection = 'var __filename = "' + this.resourcePath + '";\n';
return injection + source;
};
Add it to your pipeline and make sure to add also the configuration:
resolveLoader: {
modulesDirectories: ["my_loaders", "node_modules"]
}
See the documentation on how to write a loader.

Related

Can't resolve css url from webpack asset librarymodule

I was able to output an assets library and many other libraries that work as remote federated modules and as deep import libraries in case I am not connecting to a remote or I am not using webpack in the consumer end.
The issue is now that all my assets exports a module, that either have the raw data as uri or the string that points to the right asset. Eg: the bird.svg is outputed to dust with it's hash plus the modules that resolves to the file bird-[hash].svg.
The above is great from javascript but not so much for css. Now I can't rewrite the url() to point to the right remote path which would be sg like:
//since I don't know when the assets will change names I can't refer to it directly. So I would need to first read the string from the bird.js module. Append the publicPath and then rewrite the url.
.someClass {
background-image: url('/assets/bird.js')
}
//the above won't work for obvious reasons.
Só, the question is how can I solve this? Or is there any loader for this? I checked the resolve url loader but it does not seem to be what need.
Ok I resolved this issue, by passing additional data as variable to sass-loader. That way I can evaluate the actual name of the files, and put it as a sass map before and handle it from sass.
//I am using glob to return an object with all the assets.
//This can probably be automated better. That would be an easier way.
//But this way works for me in all 3 scenarios, node, browser and federated module.
//Also has caching ootb for the assets.
const assetsPaths = {
...glob.sync('../assets/dist/img/**.node.js').reduce(function (obj, el) {
obj['img/' + path.parse(el).name] = '../../assets/dist/' + require(el).default;
return obj
}, {}), ...glob.sync('../assets/dist/fonts/**.node.js').reduce(function (obj, el) {
obj['fonts/' + path.parse(el).name] = '../../assets/dist/' + require(el).default;
return obj
}, {})
};
//...
{
loader: 'sass-loader',
options: {
additionalData: "$assets: '" + assetsMap + "';",
sourceMap: true,
sassOptions: {
outputStyle: "compressed",
},
}
},
//...
you also need to disable url rewriting
{
loader: 'css-loader',
options: {
url: false,
}
},
then you can use assets map in your sass files:
#font-face {
font-family: 'Some Font';
src: local('Some Font');
src: url("#{map-get($assets, SomeFont)}");
}
You will need probably have your project setup sort like a mono repo and you also need to build those assets library with two bundles.
One for node so you can use the string path to your actual assets when bundling you sass/whatever.
And another for normally loading it from the browser.
update:
Instead of doing all this I just used the manifest generated from 'webpack-manifest-plugin' to build the $assets map to be used in sass.
const assetsManifest = JSON.parse(fs.readFileSync('../assets/dist/manifest.json'));
const assetsMapFn = asset => `'${asset[0]}':'${asset[1]}'`;
const assetsMap = `(
${Object.entries(assetsManifest).map(assetsMapFn).join(',')}
); `;
If anyone knows a better way to do this please reply or comment.

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)!

How to use variables output by webpack?

I am trying to display git commit hash in my react application using https://www.npmjs.com/package/git-revision-webpack-plugin this webpack plugin that supposedly exposes COMMITHASH variable
In my jsx I included:
<p>{process.COMMITHASH}</p>
and installed plugin in production webpack config as described:
plugins: [
new GitRevisionPlugin()
]
yet generated html returns <p></p>
If you want to access the COMMITHASH variable inside your code, you need to use the Define plugin, just as it says in the documentation here: https://www.npmjs.com/package/git-revision-webpack-plugin#plugin-api
var GitRevisionPlugin = require('git-revision-webpack-plugin');
var webpack = require('webpack');
var gitRevisionPlugin = new GitRevisionPlugin()
module.exports = {
plugins: [
new webpack.DefinePlugin({
'VERSION': JSON.stringify(gitRevisionPlugin.version()),
'COMMITHASH': JSON.stringify(gitRevisionPlugin.commithash()),
})
]
};
Then every occurrence of COMMITHASH "constant" in your code should be replaced by webpack when you build the bundle.

Webpack: Exposing global variable without using ProvidePlugin and expose-loader

I'm working in this ReactJS project and I have a requirement to read subfolder package.json, install them all into the node_modules and after, all dependencies installed add them to the global variable so they can be used anywhere in the code.
The problem being is that I don't have access to the jsons on expose-loader due to the syntax from webpack.config.js (I need to add them dynamically), so instead I created a loader that adding as test the package.json, gets the dependencies and tries to replicate expose-loader behaviour.
This is
var toCamelCase = function(str) {
return str.toLowerCase()
.replace( /[-_]+/g, ' ')
.replace( /[^\w\s]/g, '')
.replace( / (.)/g, function($1) { return $1.toUpperCase(); })
.replace( / /g, '' );
}
var returning_string = function(dependencies_object){
var final_string = "";
Object.keys(dependencies_object).map(function(dependency){
var location = require.resolve(dependency);
var export_dependency = 'module.exports = global["'+toCamelCase(dependency)+'"] = require("-!'+ location+'");';
final_string += export_dependency;
})
return final_string;
};
module.exports = function() {};
module.exports.pitch = function(e){
if(this.cacheable) {this.cacheable();}
var dependencies = require(e).dependencies;
return returning_string(dependencies);
};
The problem is that for some reason even though the output is exactly the same, it is not adding the library to the global context while using the expose loader it does work. When doing both things I manually added the dependency to provide plugin which I'll need to replicate later somehow anyway.
Is there any better way to do this? Or I am doing right but I am missing something?
After a research I found out the following in webpack 2.x (I am using webpack 1.x but I guess the phylosophy is valid for my version) documentation about configuration says:
write and execute function to generate a part of the configuration
So my approach to this problem is not to use a new plugin but reuse the ones that should work. Basically I wrote a new javascript file that creates all loaders that I need in a webpack.config way.
This is the file:
dependencies_loader.js
https://gist.github.com/abenitoc/b4bdc02d3c7cf287de2c92793d0a0b43
And this is aproximately the way I call it:
var webpack = require('webpack');
var dependency_loader = require('./webpack_plugins/dependencies_loader.js');
module.exports = {
devtool: 'source-map',
entry: {/* Preloading */ },
module: {preLoaders: [/*Preloading*/],
loaders: [/* Call all the loaders */].concat(dependency_loader.getExposeString()),
plugins: [
new webpack.ContextReplacementPlugin(/package\.json$/, "./plugins/"),
new webpack.HotModuleReplacementPlugin(),
new webpack.ProvidePlugin(Object.assign({
'$': 'jquery',
'jQuery': 'jquery',
'window.jQuery': 'jquery'
}, dependency_loader.getPluginProvider())), // Wraps module with variable and injects wherever it's needed
new ZipBundlePlugin() // Compile automatically zips
]
Notice that I concat the array of loaders adding the following loaders with getExposeString() that I need and reassign the object with the new global elements in pluginProvider with getPluginProvider.
Also because I use jsHint I exclude global names that's why the other method.
This only solves for node_modules dependencies, there is a different approach if you need a local library.

How to get original file path in the script with webpack?

An example code:
//in the file app.module.js
module.exports = framework.module("app", [
require('./api/api.module').name
])
//in the file app/api/api.module.js
module.exports = framework.module("app.api", [
])
Here are two dependent modules named 'app' and 'api'.
Module name is always same as file path to the module file (except module.js part, e.g. for file at app/api/api.module.js module name is 'app.api').
Is it possible to make webpack provide a filename of the included file during compilation, so following can be done?
//in the file app.module.js
module.exports = framework.module(__filename, [
require('./api/api.module').name
])
//in the file app/api/api.module.js
module.exports = framework.module(__filename, [
])
Where __filename is an actual path to the file folder.
It does not really matter what's format of name of the module, but it should be unique (for framework reasons) and lead to the module location (for debug reasons).
Update:
I've solved it for myself - this can be done by custom webpack loader which substitutes a certain placeholder with file path string. But anyway question is still open.
I know you said you resolved this yourself, yet, here's my take on it.
Your solution includes using a custom loader, however, maybe you could have solved it in a different way.
First step, in your webpack.config add these in the config object:
context: __dirname, //set the context of your app to be the project directory
node: {
__dirname: true //Allow use of __dirname in modules, based on context
},
Then, add this in your list of plugins:
new webpack.DefinePlugin({
SEPARATOR: JSON.stringify(path.sep)
})
This will replace all SEPARATOR instances in your modules with whatever the correct path separator is, for the system you are working on (you will have to require('path') in your webpack.config for this to work)
And finally in whatever module you want you can now get its name by doing
var moduleName = __dirname.replace(new RegExp(SEPARATOR, 'g'), '.');
So, for example
//in the file app.module.js
var moduleName = __dirname.replace(new RegExp(SEPARATOR, 'g'), '.');
module.exports = framework.module(moduleName, [
require('./api/api.module').name
])
//in the file app/api/api.module.js
var moduleName = __dirname.replace(new RegExp(SEPARATOR, 'g'), '.');
module.exports = framework.module(moduleName, [
])

Categories