How to handle dynamic image paths with webpack - javascript

I've managed to process the majority of my icons within a large application but there are still two use cases which don't get caught.
Dynamic paths used within angular templates
<md-icon md-svg-src="{{'/assets/icons/ic-' + $ctrl.icon + '.svg'}}"></md-icon>
Paths used as varibled within components that are passed to angular templates
i.e something along the lines of
class Comp {
this.settingsLinks = [
{name: 'advanced settings', icon: '/assets/icons/ic-settings.svg'}
]
}
and then that gets used within a template like so
<div ng-repeat="setting in $ctrl.settingsLinks">
<md-icon md-svg-src="{{setting.icon}}"></md-icon>
</div>
My webpack config is like so
module.exports = {
module: {
loaders: [
{
test: /\.html$/,
loaders: 'html-loader',
options: {
attrs: ['md-icon:md-svg-src', 'img:ng-src'],
root: path.resolve(__dirname, '../src'),
name: '[name]-[hash].[ext]'
}
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint-loader',
enforce: 'pre'
},
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'file-loader',
options: {
name() {
return '[name]-[hash].[ext]';
}
}
},
{
test: /\.js$/,
exclude: /node_modules/,
loaders: [
'ng-annotate-loader',
'babel-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: conf.path.src('index.html')
}),
new ExtractTextPlugin('index-[contenthash].css'),
],
output: {
path: path.join(process.cwd(), conf.paths.dist),
filename: '[name]-[hash].js'
},
entry: {
app: [`./${conf.path.src('app/app.module.js')}`],
vendor: Object.keys(pkg.dependencies)
},
};
I've been looking at the webpack.ContextReplacementPlugin as a potential way of handling the dynamic paths does anyone have any insight into what I'm trying to achieve? I want to be able to hash names for cache busting purposes but am struggling to work out how to handle dynamic paths in both js and templates.

https://webpack.js.org/guides/dependency-management/
webpack gives access to require.context which allows telling webpack what the dynamic path could/should be, it removed the uncertainty of what is being required and allows you to return the newly hashed icon name from its original name. It does not require them all having an import cost it merely creates a map between the old and new name if I have understood correctly.
for example, this is saying grab all the file names in the icons folder grab the ones starting with ic- as that's our naming scheme for icons, this creates an object at build time I believe of all the possible icons to be used.
const ICONS = require.context('../../../assets/icons', true, /ic-[a-zA-Z0-9.-]/);
whats returned is a function where you can pass the original icon name and get back the hashed version. You can also use ICONS.keys() to get an array of icons.
here is an example usage I've used to provide some icons.
const ICONS = require.context('../../../assets/icons', true, /ic-[a-zA-Z0-9.-]/);
export function getIconFromPath(path) {
return ICONS(path);
}
function getIconsFromPaths(obj) {
Object.keys(obj).forEach(key => Object.assign(obj, {[key]: ICONS(obj[key])}));
return obj;
}
export default getIconsFromPaths({
ARCHIVED: './ic-status-warning.svg',
CANCELLED: './ic-status-cancelled.svg',
CONFLICT: './ic-status-warning.svg',
DRAFT: './ic-status-draft.svg',
EARLIER: './ic-status-published.svg',
ENDED: './ic-status-ended.svg',
ERROR: './ic-status-published-failure.svg',
FAILURE: './ic-status-failure.svg',
INVALID: './ic-status-warning.svg',
IN_PROGRESS: './ic-content-publish-in-progress.svg',
LATEST: './ic-status-published-latest.svg',
LOCKED: './ic-status-locked.svg',
PUBLISHED: './ic-status-published.svg',
PUBLISHING: './ic-content-publish-in-progress.svg',
SCHEDULED: './ic-status-scheduled.svg',
SCHEDULING: './ic-content-publish-in-progress.svg',
UNLOCKED: './ic-status-unlocked.svg',
UPDATED: './ic-webhook-request-success.svg',
UNPUBLISHING: './ic-content-publish-in-progress.svg',
UNSCHEDULING: './ic-content-publish-in-progress.svg',
VALID: './ic-content-valid-tick.svg',
WARNING: './ic-status-warning.svg'
});
Because webpack now knows what could be returned from here it can now hash the name and you can do all sorts of good stuff like optimizing at build time.
so the example class given in my question would be resolved by
import { getIconFromPath } from '../icons/;
class Comp {
this.settingsLinks = [
{
name: 'advanced settings',
icon: getIconFromPath('./ic-settings.svg')
}
]
}

Related

Webpack-compiled CSS file is including Javascript variables and functions

We use a simple application of webpack/mix:
mix.js('resources/js/app.js', 'public/js')
.js('resources/js/cpg.js', 'public/js')
.js('resources/js/editor.js', 'public/js')
.copy('resources/js/ckeditor/dist', 'public/js/editor')
.sass('resources/sass/app.scss', 'public/css')
.sass('resources/sass/cpg.scss', 'public/css')
With webpack.config.js:
module.exports = {
resolve: {
alias: {
'#': path.resolve('resources/js'),
},
},
// https://webpack.js.org/configuration/entry-context/
entry: './resources/js/editor.js',
// https://webpack.js.org/configuration/output/
output: {
path: path.resolve(__dirname, 'public/js/editor'),
filename: 'editor.js'
},
module: {
rules: [
{
test: /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/,
use: ['raw-loader']
},
{
test: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/,
use: [
{
loader: 'style-loader',
options: {
injectType: 'singletonStyleTag',
attributes: {
'data-cke': true
}
}
},
{
loader: 'postcss-loader',
options: styles.getPostCssConfig({
themeImporter: {
themePath: require.resolve('#ckeditor/ckeditor5-theme-lark')
},
minify: true
})
}
]
}
]
},
// Useful for debugging.
devtool: 'source-map',
// By default webpack logs warnings if the bundle is bigger than 200kb.
performance: { hints: false }
}
Prior to the addition of ckeditor, we had no troubles. But now that ckeditor has been added, the following JS now appears in our compiled cpg.css file:
function webpackContext(req) {
var id = webpackContextResolve(req);
return __webpack_require__(id);
}
function webpackContextResolve(req) {
if(!__webpack_require__.o(map, req)) {
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
}
return map[req];
}
webpackContext.keys = function webpackContextKeys() {
return Object.keys(map);
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext;
webpackContext.id = "./node_modules/moment/locale sync recursive ^\\.\\/.*$";
Obviously, this is a problem. JS code does not belong in CSS files, and it trips up our SonarCloud quality gate (for good reason) so we can't deploy anything that's been compiled unless we manually edit the compiled files. Which mostly defeats the purpose of having them.
Further backstory: the section of our project that uses CKEditor was completed by a contractor. So, all of this was merged into our project before we saw that compiled files were improper. The contractor is no longer with the company, so I'm trying to debug on my own and getting nowhere. It seems to be an exceedingly rare bug for Webpack to place JS code in a CSS file.
Progress update: Removing the ckeditor references has no impact. The Webpack just seems to be broken now. Comprehensive node_modules re-install had no effect. It's just broken.
Issue appears to be a copy of https://github.com/laravel-mix/laravel-mix/issues/1976. Upgrading to Mix 6 creates an absolutely absurd amount of problems for my project, so this will just go unresolved.
Followed the instructions here: https://github.com/laravel-mix/laravel-mix/issues/2633#issuecomment-802023077 I was able to resolve the problem.
Might be related to this https://github.com/ckeditor/ckeditor5/issues/8112
A solution there suggests this change
use: [
{
loader: 'style-loader',
options: {
injectType: 'singletonStyleTag',
attributes: {
'data-cke': true
}
}
},
'css-loader', // added this line
{
loader: 'postcss-loader',
options: styles.getPostCssConfig({
themeImporter: {
themePath: require.resolve('#ckeditor/ckeditor5-theme-lark')
},
minify: true
})
}
]

Get list of webpack output files and use in another module for PWA

I'm trying to build a progressive web app, with support for offline usage.
According to MDN, the way to make PWAs work offline is to add the required resources to a cache in the service worker. This requires that the service worker code knows each of the output files. Ideally, this shouldn't be harcoded, and should be generated by webpack, since it knows what files it generates.
I'm struggling to actually generate this list. From my search, there are two plugins that can generate a json file containing a list of the files - webpack-assets-manifest and webpack-manifest-plugin. I can use these in combination with separate targets to generate a manifest with the page files. But I can't import the manifest, since webpack doesn't actually write the manifest until everything is done.
How can I import a list of files that one entry point generates and use them in another entry point/module?
webpack.config.js:
const path = require('path');
const WebpackAssetsManifest = require('webpack-assets-manifest');
const frontend = {
mode: "development",
entry: {
page:"./src/page/page.tsx",
},
devtool: 'inline-source-map',
output: {
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.html?$|\.png$/,
type: "asset/resource",
generator: {
filename: "[name][ext]",
},
},
{
test: /\.tsx?$/,
loader: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.json$/,
type: "asset/resource",
exclude: /node_modules/,
}
],
},
resolve: {
extensions: [".html", ".tsx", ".ts", ".js"],
},
plugins: [
new WebpackAssetsManifest({
output: "page-files.json",
writeToDisk: true,
}),
]
};
const serviceworker = {
mode: "development",
entry: {
serviceworker: "./src/serviceworker/serviceworker.ts",
},
devtool: 'inline-source-map',
output: {
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.html?$|\.png$/,
type: "asset/resource",
generator: {
filename: "[name][ext]",
},
},
{
test: /\.tsx?$/,
loader: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.json$/,
resourceQuery: /link/,
type: "asset/resource",
exclude: /node_modules/,
},
{
test: /\.json$/,
resourceQuery: /str/,
type: "asset/source",
exclude: /node_modules/,
}
],
},
resolve: {
extensions: [".html", ".tsx", ".ts", ".js"],
},
};
module.exports = [frontend, serviceworker];
serviceworker.ts:
import files from "../../dist/page-files.json?str";
console.log(files);
Error is:
Module not found: Error: Can't resolve '../../dist/page-files.json?str' in '<REDACTED>/src/serviceworker'
(When I build it again, it will find the file from the previous build)
Rather than relying on pre-existing webpack plugins to generate assets, I think you're going to need to write your own plugin for this use case. And if you want that plugin to write the manifest to an entry that itself needs to be bundled/compiled, creating a child compilation in that plugin would be the way to do it.
This is unfortunately not a straightforward task, but you can refer to the source code for the workbox-webpack-plugin's InjectManifest plugin, which more or does what you describe, as inspiration.
Alternatively... you can just use InjectManifest directly, if that meets your use case. While it's part of the Workbox family of libraries, InjectManifest will only actually do two things: process the entry file you pass in as swSrc via a child compilation, and replace the symbol self.__WB_MANIFEST anywhere in that swSrc file with an array of {url: '...', revision: '...'} entries generated based on the assets in the main configuration, filtered by any include/exclude parameters.
So if you don't plan on using Workbox, you can just make use of that self.__WB_MANIFEST value from your own code.
// service-worker.ts
const manifest = self.__WB_MANIFEST || [];
self.addEventListener('install', (event) => {
// Your code to cache the contents of manifest goes here.
});
// webpack.config.js
const {InjectManifest} = require('workbox-webpack-plugin');
module.exports = {
// ...other webpack config...
plugins: [
new InjectManifest({
swSrc: 'src/service-worker.ts',
swDest: 'service-worker.js',
// ...exclude/include config here...
}),
],
};
The reason behind this behavior is that webpack runs these 2 configurations parallelly. By forcing webpack to run sequentially we can fix the problem.
To do serial processing, add module.exports.parallelism = 1; at end of your webpack config.
module.exports = [frontend, serviceworker];
module.exports.parallelism = 1;
Here is the documentation from webpack, https://webpack.js.org/configuration/configuration-types/#parallelism

webpack compile a single entry point into multiple outputs based on DefinePlugin

I currently have webpack setup to compile using babel-loadera single entry point into a single output bundle. Something like
entry.js
import { A } from "a.js"
import { B } from "b.js"
...
if (TEST) {
console.log("this is a test");
}
webpack.config.js
module.exports = {
entry: {
entry: "entry.js"
},
output: {
filename: "[name].bundle.js",
path: __dirname + "/output"
},
module: {
rules: [
{
test: /\.js$/
use: {
loader: "babel-loader"
}
}]
},
plugins: [
new webpack.DefinePlugin({
TEST: JSON.stringify(true)
})
]
}
currently this all works fine. What I want though is the ability to create two versions of the entry.bundle.js. Effectively a version where TEST is true and a version where it is false: entry.bundle.js and entry.test.bundle.js
What do i need to change to achieve that? Ideally I would prefer not to have to have multiple webpack config files
If you have the boolean in this file, why not just if/else the file name and pass that same boolean down to the plugin?
let TEST = true; // assume this is passed in somehow, perhaps via cli arguments
module.exports = {
entry: {
entry: 'entry.js'
},
output: {
filename: TEST ? '[name].test.bundle.js' : '[name].bundle.js'
path: __dirname + '/output'
},
module: {
rules: [{
test: /\.js$/,
use: {
loader: 'babel-loader'
}
}]
},
plugins: [
new webpack.DefinePlugin({
TEST: JSON.stringify(TEST) // variable, will be consistent with filename
})
]
}
I do understand what you wish to do, but I am unaware of a slicker way to do this through any webpack specific techniques.

ERROR from UglifyJs: SyntaxError: Unexpected token: operator (>)

I'm getting an error when trying to run my webpack for production.
ERROR in js/main.21dbce548a76ffc14cfb.js from UglifyJs
SyntaxError: Unexpected token: operator (>) [./~/tmi.js/lib/utils.js:3,0][js/main.21dbce548a76ffc14cfb.js:3529,20]
utils.js:3,0 (which is the same as in my minified js) is:
// Return the second value if the first value is undefined..
get: (obj1, obj2) => { return typeof obj1 === "undefined" ? obj2 : obj1; },
So I assume from that the error is thrown because it's reading ES6 but it doesn't understand ES6? (The arrow function)
I don't see what's going wrong here, this is my webpack.config.js
// changed some loader syntax after reading
// https://webpack.js.org/how-to/upgrade-from-webpack-1/
const path = require(`path`);
const webpack = require(`webpack`);
const {UglifyJsPlugin} = webpack.optimize;
const CopyWebpackPlugin = require(`copy-webpack-plugin`);
const ExtractTextWebpackPlugin = require(`extract-text-webpack-plugin`);
const configHtmls = require(`webpack-config-htmls`)();
const extractCSS = new ExtractTextWebpackPlugin(`css/style.css`);
// change for production build on different server path
const publicPath = `/`;
// hard copy assets folder for:
// - srcset images (not loaded through html-loader )
// - json files (through fetch)
// - fonts via WebFontLoader
const copy = new CopyWebpackPlugin([{
from: `./src/assets`,
to: `assets`
}], {
ignore: [ `.DS_Store` ]
});
const config = {
entry: [
`./src/css/style.css`,
`./src/js/script.js`
],
resolve: {
// import files without extension import ... from './Test'
extensions: [`.js`, `.jsx`, `.css`]
},
output: {
path: path.join(__dirname, `server`, `public`),
filename: `js/[name].[hash].js`,
publicPath
},
devtool: `sourcemap`,
module: {
rules: [
{
test: /\.css$/,
loader: extractCSS.extract([
{
loader: `css`,
options: {
importLoaders: 1
}
},
{
loader: `postcss`
}
])
},
{
test: /\.html$/,
loader: `html`,
options: {
attrs: [
`audio:src`,
`img:src`,
`video:src`,
`source:srcset`
] // read src from video, img & audio tag
}
},
{
test: /\.(jsx?)$/,
exclude: /node_modules/,
use: [
{
loader: `babel`
},
{
loader: `eslint`,
options: {
fix: true
}
}
]
},
{
test: /\.(svg|png|jpe?g|gif|webp)$/,
loader: `url`,
options: {
limit: 1000, // inline if < 1 kb
context: `./src`,
name: `[path][name].[ext]`
}
},
{
test: /\.(mp3|mp4)$/,
loader: `file`,
options: {
context: `./src`,
name: `[path][name].[ext]`
}
}
]
},
plugins: [
extractCSS,
copy
]
};
if(process.env.NODE_ENV === `production`){
//image optimizing
config.module.rules.push({
test: /\.(svg|png|jpe?g|gif)$/,
loader: `image-webpack`,
enforce: `pre`
});
config.plugins = [
...config.plugins,
new UglifyJsPlugin({
sourceMap: true, // false returns errors.. -p + plugin conflict
comments: false
})
];
}
config.plugins = [...config.plugins, ...configHtmls.plugins];
module.exports = config;
OP's error is from UglifyJs, as is solved in the accepted answer, some people to this page may get the error from babel, in which case, fix it with: add "presets": ["es2015"] either to the options.presets section of babel-loader, or to .babelrc.
UglifyJs2 has a Harmony branch which accepts ES6 syntax to be minified. At this moment, you need to create a fork of webpack and point webpack to that fork.
I recently answered a couple of similar questions. Please have a look at #38387544 or #39064441 for detailed instructions.
In my case I was using webpack version 1.14
I got help from git ref
steps:
install yarn add uglifyes-webpack-plugin (and removed yarn remove uglifyjs-webpack-plugin)
then install yarn add uglify-js-es6
In webpack.config.js file change new webpack.optimize.UglifyJsPlugin to
new UglifyJsPlugin
then I was able to build. Thanks

webpack ERROR in CommonsChunkPlugin: While running in normal mode it's not allowed to use a non-entry chunk

So When I try to split my application into 1 application.js file and 1 libraries.js file, everything works fine. When I try to split it up into 1 application.js file and 2 libraries.js files, I get this error when building:
ERROR in CommonsChunkPlugin: While running in normal mode it's not allowed to use a non-entry chunk (libraries-react)
Anyone know what might be causing this error?
My configuration for webpack is
var webpack = require("webpack");
var ExtractTextPlugin = require("extract-text-webpack-plugin");
var extractSass = new ExtractTextPlugin('main.css');
module.exports = {
module: {
loaders: [{
test: /\.jsx$/,
loader: 'babel',
exclude: ['./node_modules'],
query: {
presets: ['react', 'es2015']
}
}, {
test: /\.scss$/,
loader: extractSass.extract(['css', 'sass'])
}, {
test: /\.html$/,
loader: 'file?name=[name].[ext]'
}, {
test: /\/misc\/.*\.js$/,
loader: 'file?name=/misc/[name].[ext]'
}, {
test: /\.(png|jpg|jpeg|)$/,
loader: 'file?name=/images/[name].[ext]'
}]
},
plugins: [
extractSass,
new webpack.optimize.CommonsChunkPlugin('libraries-core', 'libraries-core.js'),
new webpack.optimize.CommonsChunkPlugin('libraries-react', 'libraries-react.js')
],
entry: {
//3rd party libraries
'libraries-core': [
'lodash',
'superagent',
'bluebird',
'eventemitter3',
'object-assign',
'schema-inspector',
'jsuri',
'store-cacheable',
'immutable'
],
'libraries-react': [
'react',
'react-dom',
'react-router',
'nucleus-react'
],
//application code
application: './web/app/application.jsx',
//mocks
'mocked-api': './web/app/mock/api.js',
'mocked-local-storage': './web/app/mock/local-storage.js'
},
output: {
path: './web/build',
publicPath: '/build',
filename: '[name].js'
}
}
Following the github issue#1016, you need to reverse the order of the chunk names in plugin definition regarding to the entry points' definition
It seems like a bug to this webpack plugin for the time being...
new webpack.optimize.CommonsChunkPlugin('libraries-react', 'libraries-react.js')
new webpack.optimize.CommonsChunkPlugin('libraries-core', 'libraries-core.js')
or
new webpack.optimize.CommonsChunkPlugin({names: ['libraries-react', 'libraries-core'], filename: '[name].js')
Switching from two entries for the CommonChunkPlugin to a single one helped in my case.
Before:
plugins: [
new webpack.optimize.CommonsChunkPlugin('libraries-core', 'libraries-core.js'),
new webpack.optimize.CommonsChunkPlugin('libraries-react', 'libraries-react.js')
]
After:
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['libraries-core', 'libraries-react'],
minChunks: Infinity
})
]
It's based on the two-explicit-vendor-chunks example.
The other answers talk about the order of the names in CommonsChunkPlugin which is true. But sometimes even after you correct the order (i.e reverse of the order given in entry), webpack still might throw the same error.
And this is when you're using another CommonsChunkPlugin instance to extract commons - eg. explicit-webpack-runtime-chunk, again the order matters - here the order of instances in the config's plugins list. Simply, move this instance after the CommonsChunkPlugin for explicit-vendor-chunks. For example,
plugins: [
// explicit-vendor-chunk
new CommonsChunkPlugin({
names: ["vendor-2", "vendor-1"],
minChunks: Infinity
}),
// and the following should go after explicit-vendor-chunk
// in the list of plugins
// explicit-webpack-runtime-chunk
new CommonsChunkPlugin({
name: "manifest", // something that's not an entry
minChunks: Infinity
})
]

Categories

Resources