Webpack file-loader, how to use outputPath function - javascript

I have been working in a project where I need to use the outputPath function from the file-loader, to emit files to different folders, but I had difficulties to understand and making it work.
Part of my code is as follows:
const path = require('path');
const webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const fs = require('fs'); // fs stands for file system, it helps accessing phyisical file systems
const BRANDS = {
br1: 'br1.local',
b22: 'br2.local'
};
module.exports = {
mode: 'production',
entry: {
main: './src/js/main.js'
},
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/',
filename: 'build.js'
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]',
outputPath: (url, resourcePath, context) => {
if (/my-custom-image\.png/.test(resourcePath)) {
return `other_output_path/${url}`;
}
if (/images/.test(context)) {
return `image_output_path/${url}`;
}
return `output_path/${url}`;
}
}
},
]
},
documentation says that resourcePath is the absolute path to assets, I am not sure about this, as my assets folder has another name not assets, does it matter? it looks like: /src/images.
What is context not sure what is my context. When I do a console.log of these arguments it shows undefined, and it doesn't emit the images to the right folders.

You just need to add your folder name inside if statement.
If your folder tree looks like this:
/src/images/subfolder/
Change your code to this:
outputPath: (url, resourcePath) => {
if (/subfolder/.test(resourcePath)) {
return `images/subfolder/${url}`;
}
return `images/${url}`;
}

Related

Why "npm run build" does not generate *.asset.php when working with #wordpress/scripts?

I am building custom gutenberg blocks using npm, webpack and #wordpress/scripts. Everything was fine until I tried to use block.json file. To use block.json file I need block.asset.php file in the build directory because that's the way WordPress core is coded... (https://github.com/WordPress/gutenberg/issues/40447)
And now my problem is that running npm run build does not generate .asset.php file and I do not know why. When I register blocks using wp_enqueue_script or when I manually create an empty .asset.php it works fine.
My webpack.config.js now looks like this:
const defaultConfig = require("#wordpress/scripts/config/webpack.config");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require('path');
module.exports = {
...defaultConfig,
entry: {
'cgbms-section-block': './src/section-block.js',
'cgbms-article-block': './src/article-block.js',
'cgbms-article-header-block': './src/article-header-block.js',
'cgbms-category-block': './src/category-block.js',
'cgbms-category-block-edit': './src/category-block-edit.js',
'cgbms-card-block': './src/card-block.js',
'style-front': './src/css/style-front.scss',
'style-editor': './src/css/style-editor.scss',
},
output: {
path: path.join(__dirname, './build/'),
filename: './blocks/[name].js'
},
module: {
...defaultConfig.module,
rules: [
...defaultConfig.module.rules,
]
},
plugins: [
new MiniCssExtractPlugin({
filename: './css/[name].css'
})
],
externals: {
'#wordpress/blocks': 'wp.blocks',
'#wordpress/block-editor': 'wp.blockEditor'
},
}
Okay so solution is actually really simple.
I think I had to import default plugins config:
...defaultConfig.plugins
So my whole webpack.config.js is now:
const defaultConfig = require("#wordpress/scripts/config/webpack.config");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require('path');
module.exports = {
...defaultConfig,
entry: {
'cgbms-section-block': './src/section-block.js',
'cgbms-article-block': './src/article-block.js',
'cgbms-article-header-block': './src/article-header-block.js',
'cgbms-category-block': './src/category-block.js',
'cgbms-category-block-edit': './src/category-block-edit.js',
'cgbms-card-block': './src/card-block.js',
'style-front': './src/css/style-front.scss',
'style-editor': './src/css/style-editor.scss',
},
output: {
path: path.join(__dirname, './build/'),
filename: './blocks/[name].js'
},
module: {
...defaultConfig.module,
rules: [
...defaultConfig.module.rules,
]
},
plugins: [
...defaultConfig.plugins,
new MiniCssExtractPlugin({
filename: './css/[name].css'
})
]
}
as you can see I also removed externals block.

Webpack dynamic import each imported module

This may be an x/y question, so here goes!
Background:
I'm trying to run a comparison of two versions of a JS library to measure the benefits of its side-effect free tree-shaking modules.
My plan was to make two .html pages, one with old.js, and another importing specific modules (i.e. import {mod1, mod2} from "new.js")
Webpack Chunk Names
Ideally, I'd like each individual module to be placed into its own chunk so I can document how much each module "weighs".
I see webpack has an option to add /* webpackChunkName: "my-chunk-name" */ inside of an import.
Question:
Is it possible to dynamically import an individual property/module while specifying its name to generate its own chunk?
I've tried using this code below, but it combines them into a single chunk based on the first mod1 chunkname.
document.getElementById('mod1').onclick = function () {
import(/* webpackChunkName: "mod1" */ 'new.js').then(
(lib) => {
lib.mod1()
}
);
};
document.getElementById('mod2')!.onclick = function () {
import(/* webpackChunkName: "mod2" */ 'new.js').then(
(lib) => {
lib.mod2()
}
);
};
webpack.config.js
// Generated using webpack-cli https://github.com/webpack/webpack-cli
import { Configuration } from 'webpack';
import 'webpack-dev-server';
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const isProduction = process.env.NODE_ENV == 'production';
const config = {
// An entry point is the root JS file associated with a HTML route
entry: {
old: './src/old.ts',
new: './src/new.ts',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
},
devServer: {
open: false,
host: 'localhost',
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
// npm package names are URL-safe, but some servers don't like # symbols
return `npm.${packageName.replace('#', '')}`;
},
},
},
},
},
plugins: [
new HtmlWebpackPlugin({
// output name (URL path)
filename: 'old.html',
// the template property to the HTML template
template: path.resolve(__dirname, 'src', 'old.html'),
// associate it with one or more of the entry points with the chunks property.
chunks: ['old'],
}),
new HtmlWebpackPlugin({
// output name (URL path)
filename: 'new.html',
// the template property to the HTML template
template: path.resolve(__dirname, 'src', 'new.html'),
// associate it with one or more of the entry points with the chunks property.
chunks: ['new'],
}),
new HtmlWebpackPlugin({
// output name (URL path)
filename: 'index.html',
// the template property to the HTML template
template: path.resolve(__dirname, 'index.html'),
// associate it with one or more of the entry points with the chunks property.
chunks: [],
}),
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
performance: {
hints: false,
},
module: {
rules: [
{
test: /\.(ts|tsx)$/i,
loader: 'ts-loader',
exclude: ['/node_modules/'],
},
{
test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
type: 'asset',
},
// Add your rules for custom modules here
// Learn more about loaders from https://webpack.js.org/loaders/
],
},
resolve: {
extensions: ['.ts', '.js'],
},
experiments: {
topLevelAwait: true,
},
};
module.exports = () => {
if (isProduction) {
config.mode = 'production';
} else {
config.mode = 'development';
}
return config;
};
I think the reason webpack combines the same module(new.js) into a single chunk is because MergeDuplicateChunksPlugin is used.
Its functionality is very well described by its name and in this situation it can be seen in action: the mod1 and mod2 chunks are using the same new.js module, so they're fundamentally the same.
Fortunately, this plugin is behind a flag and it can be deactivated by modifying your configuration as follows:
config = {
/* ... */
optimization: {
mergeDuplicateChunks: false,
},
/* ... */
}
With the above configuration, you should now see the new.js module being duplicated in two different chunks - mod1 and mod2.

webpack watch mode with CopyWebpackPlugin causes infinite loop

In webpack, CopyWebpackPlugin causes infinite loop when webpack is in watch mode. I tried to add watchOptions.ignored option but it doesn't seem to work.
My webpack config is following:
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const config = {
entry: {
'res': './src/index.js'
},
output: {
filename: '[name].min.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015']
}
}
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new CopyWebpackPlugin([
{ from: 'dist', to: path.resolve(__dirname, 'docs/js') }
], {})
],
watchOptions: {
ignored: path.resolve(__dirname, 'docs/js')
}
};
module.exports = config;
Any help would be appreciated.
With CopyWebpackPlugin, I've experienced the infinite loop too. I tried all kinds of CopyWebpackPlugin configurations with no luck yet. After hours of wasted time I found I could hook into the compiler and fire off my own copy method.
Running Watch
I'm using webpack watch to watch for changes. In the package.json, I use this config so I can run npm run webpackdev, and it will watch for file changes.
"webpackdev": "cross-env webpack --env.environment=development --env.basehref=/ --watch"
My Workaround
I've added an inline plugin with a compiler hook, which taps into AfterEmitPlugin. This allows me to copy after my sources have been generated after the compile. This method works great to copy my npm build output to my maven target webapp folder.
// Inline custom plugin - will copy to the target web app folder
// 1. Run npm install fs-extra
// 2. Add fix the path, so that it copies to the server's build webapp folder
{
apply: (compiler) => {
compiler.hooks.afterEmit.tap('AfterEmitPlugin', (compilation) => {
// Debugging
console.log("########-------------->>>>> Finished Ext JS Compile <<<<<------------#######");
let source = __dirname + '/build/';
// TODO Set the path to your webapp build
let destination = __dirname + '/../dash-metrics-server/target/metrics-dash';
let options = {
overwrite: true
};
fs.copy(source, destination, options, err => {
if (err) return console.error(err) {
console.log('Copy build success!');
}
})
});
}
}
The Workaround Source
Here's my webpack.config.js in total for more context. (In this webpack configuration, I'm using Sencha's Ext JS ExtWebComponents as the basis.)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { BaseHrefWebpackPlugin } = require('base-href-webpack-plugin');
const ExtWebpackPlugin = require('#sencha/ext-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
const fs = require('fs-extra');
module.exports = function(env) {
function get(it, val) {if(env == undefined) {return val;} else if(env[it] == undefined) {return val;} else {return env[it];}}
var profile = get('profile', '');
var emit = get('emit', 'yes');
var environment = get('environment', 'development');
var treeshake = get('treeshake', 'no');
var browser = 'no'; // get('browser', 'no');
var watch = get('watch', 'yes');
var verbose = get('verbose', 'no');
var basehref = get('basehref', '/');
var build_v = get('build_v', '7.0.0.0');
const isProd = environment === 'production';
const outputFolder = 'build';
const plugins = [
new HtmlWebpackPlugin({template: 'index.html', hash: false, inject: 'body'}),
new BaseHrefWebpackPlugin({ baseHref: basehref }),
new ExtWebpackPlugin({
framework: 'web-components',
toolkit: 'modern',
theme: 'theme-material',
emit: emit,
script: './extract-code.js',
port: 8080,
packages: [
'renderercell',
'font-ext',
'ux',
'd3',
'pivot-d3',
'font-awesome',
'exporter',
'pivot',
'calendar',
'charts',
'treegrid',
'froala-editor'
],
profile: profile,
environment: environment,
treeshake: treeshake,
browser: browser,
watch: watch,
verbose: verbose,
inject: 'yes',
intellishake: 'no'
}),
new CopyWebpackPlugin([{
from: '../node_modules/#webcomponents/webcomponentsjs/webcomponents-bundle.js',
to: './webcomponents-bundle.js'
}]),
new CopyWebpackPlugin([{
from: '../node_modules/#webcomponents/webcomponentsjs/webcomponents-bundle.js.map',
to: './webcomponents-bundle.js.map'
}]),
// Debug purposes only, injected via script: npm run-script buildexample -- --env.build_v=<full version here in format maj.min.patch.build>
new webpack.DefinePlugin({
BUILD_VERSION: JSON.stringify(build_v)
}),
// This causes infinite loop, so I can't use this plugin.
// new CopyWebpackPlugin([{
// from: __dirname + '/build/',
// to: __dirname + '/../dash-metrics-server/target/test1'
// }]),
// Inline custom plugin - will copy to the target web app folder
// 1. Run npm install fs-extra
// 2. Add fix the path, so that it copies to the server's build webapp folder
{
apply: (compiler) => {
compiler.hooks.afterEmit.tap('AfterEmitPlugin', (compilation) => {
// Debugging
console.log("########-------------->>>>> Finished Ext JS Compile <<<<<------------#######");
let source = __dirname + '/build/';
// TODO Set the path to your webapp build
let destination = __dirname + '/../dash-metrics-server/target/metrics-dash';
let options = {
overwrite: true
};
fs.copy(source, destination, options, err => {
if (err) return console.error(err)
console.log('Copy build success!');
})
});
}
}
];
return {
mode: environment,
devtool: (environment === 'development') ? 'inline-source-map' : false,
context: path.join(__dirname, './src'),
//entry: './index.js',
entry: {
// ewc: './ewc.js',
app: './index.js'
},
output: {
path: path.join(__dirname, outputFolder),
filename: '[name].js'
},
plugins: plugins,
module: {
rules: [
{ test: /\.(js)$/, exclude: /node_modules/,
use: [
'babel-loader',
// 'eslint-loader'
]
},
{ test: /\.(html)$/, use: { loader: 'html-loader' } },
{
test: /\.(css|scss)$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'sass-loader' }
]
}
]
},
performance: { hints: false },
stats: 'none',
optimization: { noEmitOnErrors: true },
node: false
};
};
So I know this question is very old at this point, but I was running into this endless loop issue again recently and found a couple of solutions.
Not the cleanest method, but if you add an "assets" folder, which can be completely empty, to the root of your project, it seems to only compile after your sources folder changes.
The better method I have found is within the webpack config. The original poster mentioned about using ignored which does seem to fix the issue if you instruct webpack to watch file changes after the initial build and to ignore your dist/output folder...
module.exports = {
//...
watch: true,
watchOptions: {
ignored: ['**/dist/**', '**/node_modules'],
},
};

Wrong URL for images with React + Typescript + WebPack

I'm referencing an image inside my .JSX file but the generated URL is wrong.
It looks like this : http://localhost:43124/dist/dist/9ee7eb54c0eb428bb30b599ef121fe25.jpg
The folder "dist" exists with the picture but not "dist/dist". I think the problem comes from my Webpack.config.js. Here are the files :
module.d.ts
I instruct Typescript what to do with image files as mentionned here.
declare module '*.jpg'
declare module '*.svg'
Layout.tsx
I reference my logo inside React so it can be packed by Webpack.
/// <reference path="./module.d.ts"/>
import * as React from 'react';
import logo from '../img/logo.svg';
export class Layout extends React.Component<{}, {}> {
public render() {
return <img src="{logo}" width="220" alt="logo" />
}
}
webpack.config.js
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const merge = require('webpack-merge');
module.exports = (env) => {
const isDevBuild = !(env && env.prod);
// Configuration in common to both client-side and server-side bundles
const sharedConfig = () => ({
stats: { modules: false },
resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
output: {
filename: '[name].js',
publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
},
module: {
rules: [
{ test: /\.tsx?$/, include: /ClientApp/, use: 'awesome-typescript-loader?silent=true' },
{ test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }
]
},
plugins: [new CheckerPlugin()]
});
// Configuration for client-side bundle suitable for running in browsers
const clientBundleOutputDir = './wwwroot/dist';
const clientBundleConfig = merge(sharedConfig(), {
entry: { 'main-client': './ClientApp/boot-client.tsx' },
module: {
rules: [
{ test: /\.css$/, use: ExtractTextPlugin.extract({ use: isDevBuild ? 'css-loader' : 'css-loader?minimize' }) }
]
},
output: { path: path.join(__dirname, clientBundleOutputDir) },
plugins: [
new ExtractTextPlugin('style.css'),
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./wwwroot/dist/vendor-manifest.json')
})
].concat(isDevBuild ? [
// Plugins that apply in development builds only
new webpack.SourceMapDevToolPlugin({
filename: '[file].map', // Remove this line if you prefer inline source maps
moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk
})
] : [
// Plugins that apply in production builds only
new webpack.optimize.UglifyJsPlugin()
])
});
return [clientBundleConfig];
};
I used the default Visual Studio ASP.NET Core React + Redux template.

Best way to have all files in a directory be entry points in webpack?

I want to create multiple entry points for a website, which is pretty easily done in Webpack using an object for the entry property, like here.
But as the site grows (and it inevitably will) having to add each entry point seems cumbersome and prone to error. So I'd like to simply point at a directory and say "here are all the entry points."
So I've cooked this up:
var path = require('path');
var fs = require('fs');
var entryDir = path.resolve(__dirname, '../source/js');
var entries = fs.readdirSync(entryDir);
var entryMap = {};
entries.forEach(function(entry){
var stat = fs.statSync(entryDir + '/' + entry);
if (stat && !stat.isDirectory()) {
var name = entry.substr(0, entry.length -1);
entryMap[name] = entryDir + '/' + entry;
}
});
module.exports = {
entry: entryMap,
output: {
path: path.resolve(__dirname, '../static/js'),
filename: "[name]"
},
...
This works fine, but is there a feature or configuration option in Webpack that would handle this for me?
I think glob is the right way to go here (AFAIK webpack wont do this for you). This is what I ended up with, it will find all files in a directory and create an entry with a name matching the file:
var glob = require('glob');
var path = require('path');
module.exports = {
entry: glob.sync('../source/js/**.js').reduce(function(obj, el){
obj[path.parse(el).name] = el;
return obj
},{}),
output: {
path: path.resolve(__dirname, '../static/js'),
filename: "[name]"
},
...
adapt the search path to meet your specific needs. It might also be useful to pass in {cwd: someRoot} as the second argument to sync if you have a special scripts directory which will make this the new root of relative path searches.
In my opinion, only a little Node skill is needed, and it doesn't have to be that complicated.
const webpack = require('webpack');
const path = require('path');
const fs = require('fs');
const fileNames = fs.readdirSync('./src').reduce((acc, v) => ({ ...acc, [v]: `./src/${v}` }), {});
const config = {
entry: fileNames,
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name]',
},
};
module.exports = config;
I have used Glob for this.
var path = require('path');
var glob = require('glob');
module.exports = {
entry: { 'app' : glob.sync('./scripts/**/*.ts*') },
output: {
path: path.join(__dirname, '/wwwroot/dist'),
filename: '[name].bundle.js',
sourceMapFilename: '[name].map'
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
}
]
},
resolve: {
extensions: [".ts", ".js"]
}
};

Categories