Related
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
I have extended default web pack config in Ionic v3 for forcing cache busting.
I am able to fingerprint generated JavaScript artifacts, but I am unable to fingerprint images and JSON files under the assets folder. I took Help from Bundled files and cache-busting.
An excerpt of webpack config.js
module.exports = {
// ...
output: {
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js',
},
plugins: [
new WebpackChunkHash({algorithm: 'md5'}) // 'md5' is default value
]
}
The above is the approach for fingerprinting JavaScript bundles, and it's working fine. I want to add hashes/fingerprint images and JSON files inside the assets folder. I used the same approach for images also, but it did not work.
I have extended webpack config.js and added a new rule for images. By default webpack directly copies the images and assets to the output folder.
Copy Config.js
module.exports = {
copyAssets: {
src: ['{{SRC}}/assets/**/*'],
dest: '{{WWW}}/assets'
},
copyIndexContent: {
src: ['{{SRC}}/index.html', '{{SRC}}/manifest.json', '{{SRC}}/service-worker.js'],
dest: '{{WWW}}'
},
copyFonts: {
src: ['{{ROOT}}/node_modules/ionicons/dist/fonts/**/*', '{{ROOT}}/node_modules/ionic-angular/fonts/**/*'],
dest: '{{WWW}}/assets/fonts'
},
Here images and other assets are directly copied.
I have added a new rule in extended webpack.config.js, but the build process is ignoring it. How do I fix this issue?
Excerpt of webpack config.js
{
test: /\.(png|jpg|gif)$/,
loader: 'file-loader',
options: {
name:'[name].[hash].[ext]',//adding hash for cache busting
outputPath:'assets/imgs',
publicPath:'assets/imgs'
},
entire Webpack.config.js
/*
* The webpack config exports an object that has a valid webpack configuration
* For each environment name. By default, there are two Ionic environments:
* "dev" and "prod". As such, the webpack.config.js exports a dictionary object
* with "keys" for "dev" and "prod", where the value is a valid webpack configuration
* For details on configuring webpack, see their documentation here
* https://webpack.js.org/configuration/
*/
var path = require('path');
var webpack = require('webpack');
var ionicWebpackFactory = require(process.env.IONIC_WEBPACK_FACTORY);
var ModuleConcatPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
var PurifyPlugin = require('#angular-devkit/build-optimizer').PurifyPlugin;
var optimizedProdLoaders = [
{
test: /\.json$/,
loader: 'json-loader'
},
{
test: /\.js$/,
loader: [
{
loader: process.env.IONIC_CACHE_LOADER
},
{
loader: '#angular-devkit/build-optimizer/webpack-loader',
options: {
sourceMap: true
}
},
]
},
{
test: /\.ts$/,
loader: [
{
loader: process.env.IONIC_CACHE_LOADER
},
{
loader: '#angular-devkit/build-optimizer/webpack-loader',
options: {
sourceMap: true
}
},
{
test: /\.(png|jpg|gif)$/,
loader: 'file-loader',
options: {
name:'[name].[hash].[ext]',
outputPath:'assets/imgs',
publicPath:'assets/imgs'
},
},
{
loader: process.env.IONIC_WEBPACK_LOADER
}
]
}
];
function getProdLoaders() {
if (process.env.IONIC_OPTIMIZE_JS === 'true') {
return optimizedProdLoaders;
}
return devConfig.module.loaders;
}
var devConfig = {
entry: process.env.IONIC_APP_ENTRY_POINT,
output: {
path: '{{BUILD}}',
publicPath: 'build/',
filename: '[name].js',
devtoolModuleFilenameTemplate: ionicWebpackFactory.getSourceMapperFunction(),
},
devtool: process.env.IONIC_SOURCE_MAP_TYPE,
resolve: {
extensions: ['.ts', '.js', '.json'],
modules: [path.resolve('node_modules')]
},
module: {
loaders: [
{
test: /\.json$/,
loader: 'json-loader'
},
{
test: /\.ts$/,
loader: process.env.IONIC_WEBPACK_LOADER
},
{
test: /\.(jpg|png)$/,
use: {
loader: "file-loader",
options: {
name: "[name].[hash].[ext]",
outputPath:'assets/imgs',
publicPath:'assets/imgs'
},
}},
]
},
plugins: [
ionicWebpackFactory.getIonicEnvironmentPlugin(),
ionicWebpackFactory.getCommonChunksPlugin()
],
// Some libraries import Node.js modules but don't use them in the browser.
// Tell Webpack to provide empty mocks for them so importing them works.
node: {
fs: 'empty',
net: 'empty',
tls: 'empty'
}
};
var prodConfig = {
entry: process.env.IONIC_APP_ENTRY_POINT,
output: {
path: '{{BUILD}}',
publicPath: 'build/',
filename: '[name].js',
devtoolModuleFilenameTemplate: ionicWebpackFactory.getSourceMapperFunction(),
},
devtool: process.env.IONIC_SOURCE_MAP_TYPE,
resolve: {
extensions: ['.ts', '.js', '.json'],
modules: [path.resolve('node_modules')]
},
module: {
loaders: getProdLoaders()
},
plugins: [
ionicWebpackFactory.getIonicEnvironmentPlugin(),
ionicWebpackFactory.getCommonChunksPlugin(),
new ModuleConcatPlugin(),
new PurifyPlugin()
],
// Some libraries import Node.js modules but don't use them in the browser.
// Tell Webpack to provide empty mocks for them so importing them works.
node: {
fs: 'empty',
net: 'empty',
tls: 'empty'
}
};
module.exports = {
dev: devConfig,
prod: prodConfig
}
Using Webpack 4 you should not need any additional plugins or loaders.
It will give you the naming option [contenthash].
Also, it looks like you have this block nested under the test: .ts block.
{
test: /\.(png|jpg|gif)$/,
loader: 'file-loader',
options: {
name:'[name].[hash].[ext]', // Adding hash for cache busting
outputPath:'assets/imgs',
publicPath:'assets/imgs'
}
}
Ultimately, you can do something like this:
// Copy static assets over with file-loader
{
test: /\.(ico)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'file-loader', options: {name: '[name].[contenthash].[ext]'},
},
{
test: /\.(woff|woff2|eot|ttf|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'file-loader', options: {name: 'fonts/[name].[contenthash].[ext]'},
},
{
test: /\.(jpg|gif|png|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'file-loader', options: {name: 'images/[name].[contenthash].[ext]'},
}
]
Using [chunkhash] instead of content should still work, and if you're not using webpack4 do that, but otherwise for more information see this issue for an explanation.
For more help, read the long-term caching performance guide from Google and the latest caching documentation from Webpack.
the files copied via CopyPlugin will not pass to loaders.
So even you have correct loader setting with hashname for images, it doesn't work.
but you can see https://github.com/webpack-contrib/copy-webpack-plugin#template
the CopyPlugin provide you a way to specify output name which can be set with hash:
module.exports = {
plugins: [
new CopyPlugin([
{
from: 'src/',
to: 'dest/[name].[hash].[ext]',
toType: 'template',
},
]),
],
};
Eventually, I used gulp for fingerprinting static assets.
Drop Angular Output hashing and build the application.ng build --prod --aot --output-hashing none .
post-build execute a gulp script which would fingerprint all the assets and update the references.
npm i gulp gulp-rev gulp-rev-delete-original gulp-rev-collector
gulpfile.js
const gulp = require('gulp');
const rev = require('gulp-rev');
const revdel = require('gulp-rev-delete-original');
const collect = require('gulp-rev-collector');
// finger priniting static assets
gulp.task('revision:fingerprint', () => {
return gulp
.src([
'dist/welcome/**/*.css',
'dist/welcome/**/*.js',
'dist/welcome/**/*.{jpg,png,jpeg,gif,svg,json,xml,ico,eot,ttf,woff,woff2}'
])
.pipe(rev())
.pipe(revdel())
.pipe(gulp.dest('dist/welcome'))
.pipe(rev.manifest({ path: 'manifest-hash.json' }))
.pipe(gulp.dest('dist'));
});
gulp.task('revision:update-fingerprinted-references', () => {
return gulp
.src(['dist/manifest-hash.json', 'dist/**/*.{html,json,css,js}'])
.pipe(collect())
.pipe(gulp.dest('dist'));
});
gulp.task(
'revision',
gulp.series(
'revision:fingerprint',
'revision:update-fingerprinted-references'));
Add a new script in package.json
"gulp-revision": "gulp revision"
Execute npm run gulp-revision Post-build.
Solving Browser Cache Hell With Gulp-Rev
Using webpack-assets-manifest you can generate a map of asset names to fingerprinted names like so:
{
"images/logo.svg": "images/logo-b111da4f34cefce092b965ebc1078ee3.svg"
}
Using this manifest you can then rename the assets in destination folder, and use the "correct", hash-inclusive src or href in your project.
The fix isn't framework-specific.
When I configure Webpack for this code base, Webpack complains that it Can't resolve 'babel-loader'. What exactly is failing, and how can I ask Webpack what its complaint is?
The Webpack configuration:
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './source/main.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.js',
},
resolve: {
modules: [
path.resolve(__dirname, 'source'),
'/usr/share/javascript',
'/usr/lib/nodejs',
],
},
module: {
loaders: [
// Transform JSX with React.
{
test: /\.jsx$/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react'],
},
},
],
},
};
The entry module:
// source/main.jsx
"use strict";
import Application from './components/Application';
const applicationElement = <Application />;
ReactDOM.render(
applicationElement,
document.getElementById('application'),
);
Is the problem something like a search path, and if so why can't the error tell me what setting I need to correct?
The babel-loader module is definitely installed. (I therefore don't want to install it again – so npm install won't help – I am trying to tell Webpack to use it from the already-installed location.) Its package definition is at /usr/lib/nodejs/babel-loader/package.json.
I've pointed Webpack's resolver there – instead of its default resolver behaviour – using the resolve.modules list of search paths. Correct?
So the resolver should be able to find it there by the specified search path /usr/lib/nodejs and the name babel-loader, no?
(This raises a separate question, about how to convince Webpack to just tell me what it's looking for so it can be diagnosed more easily.)
How can I tell Webpack the specific location it should use to resolve that babel-loader name?
The Webpack configuration setting resolve is for modules that are imported. The loaders are resolved differently; the resolveLoader setting configures how to resolve the loaders specifically.
So, adding resolveLoader to the Webpack configuration works:
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './source/main.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.js',
},
resolve: {
// Configure how Webpack finds modules imported with `import`.
modules: [
path.resolve(__dirname, 'source'),
'/usr/share/javascript',
'/usr/lib/nodejs',
],
},
resolveLoader: {
// Configure how Webpack finds `loader` modules.
modules: [
'/usr/lib/nodejs',
],
},
module: {
loaders: [
// Transform JSX with React.
{
test: /\.jsx$/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react'],
},
},
],
},
};
I think that the webpack.config you're looking for is the following:
module: {
loaders: [
{test: /\.js$/, include: path.join(__dirname, 'src'), loaders: ['babel']},
]
}
Hope helps :)
I'm working on a new module for an existing project that still uses requireJS for module loading. I'm trying to use new technologies for my new module like webpack (which allows me to use es6 loaders using es6 imports). It seems like webpack can't reconcile with requireJS syntax. It will say things like: "Module not found: Error: Can't resolve in ".
Problem: Webpack won't bundle files with requireJS/AMD syntax in them.
Question: Is there any way to make webpack play nice with requireJS?
My final output must be in AMD format in order for the project to properly load it. Thanks.
I had the same question and I managed to achieve it. Below is the same webpack.config.js file.
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
let basePath = path.join(__dirname, '/');
let config = {
// Entry, file to be bundled
entry: {
'main': basePath + '/src/main.js',
},
devtool: 'source-map',
output: {
// Output directory
path: basePath + '/dist/',
library: '[name]',
// [hash:6] with add a SHA based on file changes if the env is build
filename: env === EnvEnum.BUILD ? '[name]-[hash:6].min.js' : '[name].min.js',
libraryTarget: 'amd',
umdNamedDefine: true
},
module: {
rules: [{
test: /(\.js)$/,
exclude: /(node_modules|bower_components)/,
use: {
// babel-loader to convert ES6 code to ES5 + amdCleaning requirejs code into simple JS code, taking care of modules to load as desired
loader: 'babel-loader',
options: {
presets: ['es2015'],
plugins: []
}
}
}, { test: /jQuery/, loader: 'expose-loader?$' },
{ test: /application/, loader: 'expose-loader?application' },
{ test: /base64/, loader: 'exports-loader?Base64' }
]
},
resolve: {
alias: {
'jQuery': 'bower_components/jquery/dist/jquery.min',
'application': 'main',
'base64': 'vendor/base64'
},
modules: [
// Files path which will be referenced while bundling
'src/**/*.js',
'src/bower_components',
path.resolve('./src')
],
extensions: ['.js'] // File types
},
plugins: [
]
};
module.exports = config;
I'm using a Yeoman project template called "aspnetcore-spa", which is an ASP.net core 1 template working in conjunction with major SPA frameworks (Angular2 and React).
I created a project with Angular2.The biolerplate's code works fine and there is no problem. Once I add Sass loader to webpack.config.js and make a reference to the Sass file from any angular file.
In webpack.config.js :
var isDevBuild = process.argv.indexOf('--env.prod') < 0;
var path = require('path');
var webpack = require('webpack');
var nodeExternals = require('webpack-node-externals');
var merge = require('webpack-merge');
var allFilenamesExceptJavaScript = /\.(?!js(\?|$))([^.]+(\?|$))/;
// Configuration in common to both client-side and server-side bundles
var sharedConfig = {
resolve: { extensions: [ '', '.js', '.ts' ] },
output: {
filename: '[name].js',
publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
},
module: {
loaders: [
{ test: /\.ts$/, include: /ClientApp/, loader: 'ts', query: { silent: true } },
{ test: /\.scss$/,include:/ClientApp/, loaders: ["style", "css", "sass"] },
{ test: /\.html$/,include: /ClientApp/, loader: 'raw' },
{ test: /\.css$/, loader: 'to-string!css' },
{ test: /\.(png|jpg|jpeg|gif|svg)$/, loader: 'url', query: { limit: 25000 } }
]
}
};
// Configuration for client-side bundle suitable for running in browsers
var clientBundleOutputDir = './wwwroot/dist';
var clientBundleConfig = merge(sharedConfig, {
entry: { 'main-client': './ClientApp/boot-client.ts' },
output: { path: path.join(__dirname, clientBundleOutputDir) },
plugins: [
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.OccurenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin()
])
});
// Configuration for server-side (prerendering) bundle suitable for running in Node
var serverBundleConfig = merge(sharedConfig, {
entry: { 'main-server': './ClientApp/boot-server.ts' },
output: {
libraryTarget: 'commonjs',
path: path.join(__dirname, './ClientApp/dist')
},
target: 'node',
devtool: 'inline-source-map',
externals: [nodeExternals({ whitelist: [allFilenamesExceptJavaScript] })] // Don't bundle .js files from node_modules
});
module.exports = [clientBundleConfig, serverBundleConfig];
In my component :
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-wine',
template: require('./wine.component.html'),
styles: require('./wine.component.scss')
})
export class WineComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
I have already installed npm packages pertinent to sass loader :
npm install node-sass sass-loader --save-dev
I have checked the main-server.js file in wwwroot/dist folder which is the result of webpack bundling, I saw that the .scss file is loaded and they styles are processed correctly. Once I run the app though, shows this exception which is coming from the server side rendering side:
An unhandled exception occurred while processing the request.
Exception: Call to Node module failed with error: ReferenceError: window is not defined at E:\Dev\MyApp\MyAppCore\src\MyApp.Web\ClientApp\dist\main-server.js:573:31 at E:\Dev\MyApp\MyAppCore\src\MyApp.Web\ClientApp\dist\main-server.js:568:48 at module.exports (E:\Dev\MyApp\MyAppCore\src\MyApp.Web\ClientApp\dist\main-server.js:590:69) at Object. (E:\Dev\MyApp\MyAppCore\src\MyApp.Web\ClientApp\dist\main-server.js:526:38) at webpack_require (E:\Dev\MyApp\MyAppCore\src\MyApp.Web\ClientApp\dist\main-server.js:20:30) at E:\Dev\MyApp\MyAppCore\src\MyApp.Web\ClientApp\dist\main-server.js:501:22 at Object.module.exports (E:\Dev\MyApp\MyAppCore\src\MyApp.Web\ClientApp\dist\main-server.js:506:3) at webpack_require (E:\Dev\MyApp\MyAppCore\src\MyApp.Web\ClientApp\dist\main-server.js:20:30) at Object. (E:\Dev\MyApp\MyAppCore\src\MyApp.Web\ClientApp\dist\main-server.js:129:25) at webpack_require (E:\Dev\MyApp\MyAppCore\src\MyApp.Web\ClientApp\dist\main-server.js:20:30)
It's obviously because of the webpack's server-side rendering, as it's running the code on Node.js side (through ASP.net Core's Javascript Services) and there is a code that is coupled with the DOM window object which is not valid on node.
Any clues?
I managed to fix the problem, here's the web.config.js bit:
(Notice the loaders for .scss files)
module: {
loaders: [
{ test: /\.ts$/, include: /ClientApp/, loader: 'ts', query: { silent: true } },
{ test: /\.scss$/,include:/ClientApp/, loaders: ["to-string", "css", "sass"] },
{ test: /\.html$/,include: /ClientApp/, loader: 'raw' },
{ test: /\.css$/, loader: 'to-string!css' },
{ test: /\.(png|jpg|jpeg|gif|svg)$/, loader: 'url', query: { limit: 25000 } }
]
}
And in the Angular component I changed the styles to this :
(Passed an array of required css files rather than a single css file)
#Component({
selector: 'app-wine',
template: require('./wine.component.html'),
styles: [require('./wine.component.scss')]
})