I have a requirement to create a JavaScript framework for a third party plugin where the plugin can use any dependencies and any versions of those dependencies that it wants to without conflicting with other third party plugins that are used on the same page.
I've also got to think about sharing the dependencies between plugins so that we don't end up with x number of plugins all using the same dependency and version and bloating the page with x number of the same dependency.
My initial idea was to use Webpack to bundle the third party plugins, but I'd need to have separate bundles for each individual dependency to reduce page bloat. So I added this config to the config for webpack:
module.exports = {
entry: {
pluginX: './src/index.js',
},
devtool: 'source-map',
target: 'web',
output: {
path: path.resolve(__dirname, './dist'),
library: 'pluginX',
},
optimization: {
moduleIds: 'named',
chunkIds: 'named',
splitChunks: {
chunks: 'all',
name(module, chunk, cacheGroupKey) {
const context = module.context;
const splitContext = context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/);
const packageJson = getPackageJson(context);
const version = packageJson ? packageJson.version : '0.0.0';
const name = splitContext[1].replace('#', '');
return `${cacheGroupKey}-${name}-${version}`;
},
}
},
plugins: [
new CleanPlugin('./dist', {
verbose: false,
}),
]
}
For reference the getPackageJson function looks like this:
function getPackageJson(context) {
do {
const packageJson = `${context}\\package.json`;
if (fs.existsSync(packageJson)) { return require(packageJson); }
const splits = context.split(/\\/);
splits.pop();
context = splits.length > 0 ? splits.join('\\') : undefined;
} while (context);
return undefined;
}
I've set up three test plugins, 1 using d3 v5.9.1, and the other 2 both using d3 v3.5.17. Using this configuration, I can correctly bundle up the plugins and their dependencies separately, I've created a test server to serve the bundled entry bundles and the dependency bundles. Plugin 1 using d3 v5.9.1 seems to work fine consistently, but plugin 2 and plugin 3 never work together; one of them will always be undefined when I try to access the plugin name (specified in the output.library property).
For reference, here is the index.html page:
<!DOCTYPE html>
<html>
<head>
<script src="./vendors-d3-3.5.17.js" type="text/javascript"></script>
<script src="./vendors-d3-5.9.1.js" type="text/javascript"></script>
<script src="./plugin1.js" type="text/javascript"></script>
<script src="./plugin2.js" type="text/javascript"></script>
<script src="./plugin3.js" type="text/javascript"></script>
</head>
<body></body>
</html>
The index.js gets added automatically by the HtmlWebpackPlugin I'm using (I'm using webpack-dev-server to test this).
The index.js page that is the test page for this is this:
setTimeout(function () {
plugin1.run();
plugin2.run();
plugin3.run();
}, 1000);
plugin 1 index.js file looks like this:
import * as d3 from 'd3';
export function run() {
console.log('plugin1', !!d3.scaleLinear, !!(d3.scale || {}).linear);
}
plugin 2 and 3 has this as their index.js file:
import * as d3 from 'd3';
export function run() {
console.log('pluginX', !!d3.scale.linear, !!d3.scaleLinear);
}
The console output should read:
plugin1 true false
plugin2 true false
plugin3 true false
But I actually get either this:
plugin1 true false
plugin2 true false
Uncaught TypeError: Cannot read property 'run' of undefined
or this:
plugin1 true false
Uncaught TypeError: Cannot read property 'run' of undefined
Because either plugin2 or plugin3 (always only one of them) is always undefined.
I suspect it has something to do with the bootstrapping code that webpack inserts, but I can't be 100% sure about that.
Related
As per the documentation, I am not using worker-loader, I am trying to use the native way suggested by the webpack-5 documentation.
Below is the usage of the worker script in the main thread.
const worker = new window.Worker(
new URL("../workers/listOperation.worker.js", import.meta.url),
{
type: "module",
},
);
worker.postMessage({ list: hugeList, params: reqData });
worker.onerror = err => console.error(err);
worker.onmessage = e => {
const { list } = e.data;
// Usage of `list` from the response
worker.terminate();
};
return worker;
It works fine if there are no imports used in the script. But when I import any node modules (e.g. loadash/get) or any other function/constants from local, it does not work as the output webWorker bundle file doesn't transpile and bundle the imported code. It keeps the "import" statement as it is.
Below is the worker script (listOperation.worker.js)
import get from "lodash/get";
import { ANY_CONSTANT } from "../constants"; // This is some local constant
addEventListener("message", e => {
const { list, params } = e.data;
// Here I have some usage of `get` method from `lodash/get` and ANY_CONSTANT
self.postMessage({
list: list,
});
});
Webpack outputs the bundle file like below which won't be usable by the browser, if I put the /\.worker.js$/ pattern in the the exclude of babel-loader rule.
import get from "lodash/get";import{ANY_CONSTANT}from"../constants";addEventListener("message",(e=>{const{list:s,params:t,.......
And even if I don't put the /\.worker.js$/ pattern in the the exclude of babel-loader rule, the output bundle still doesn't include the implementation of get from lodash/get or the value of the constant. It just outputs it in cjs using require.
Also, I made use of asset module so that I can put the output file inside a directory, not directly in the root of the dist folder. Configuration changes in my webpack.config looks like this.
module.exports = {
entry: {...},
module: {
rules: [
{
test: /\.js$/,
exclude: [/(node_modules)/, /\.worker.js$/],
use: {
loader: "babel-loader", // This uses the config defined in babel.config.js
},
},
{
test: /\.worker.js$/,
exclude: /(node_modules)/,
type: "asset/resource",
generator: {
filename: "js/workers/[hash][ext][query]",
},
},
],
},
}
Dependencies: "#vue/cli-plugin-unit-jest": "^4.5.13", "#vue/test-utils": "^1.2.1", "vue-jest": "^3.0.7"
I have an app which uses an alias (say "foo") being set in vue.config.js:
module.exports = {
chainWebpack: (config) => {
// Add project name as alias
config.resolve.alias.set('foo', __dirname);
},
};
For both import statements and HTML tag src...
In main.js:
...
import App from 'foo/src/components/core/App';
...
In ../src/core/App/index.vue:
<script src="foo/src/components/core/App/script.js" />
<style module src="foo/src/components/core/App/style.css" />
<template src="foo/src/components/core/App/template.html" />
I know I can use a moduleNameMapper in jest.config.js, something like:
'^foo(.*)$': '<rootDir>$1',
However, this doesn't map aliases that appear in the src attribute of my HTML tags. Is there any way to have vue-jest interpret these attribute paths via a config setting or some other means?
Any recommendations will be greatly appreciated.
URL parsing in SFCs
vue-jest doesn't resolve src URLs for the top-level block tags in SFCs, so you'll have to use un-aliased relative paths in src/components/core/App/index.vue:
<script src="./script.js" />
<style module src="./style.css" />
<template src="./template.html" />
URL parsing in <template> contents
vue-jest uses #vue/component-compiler-utils to compile the template, but URL parsing requires the transformAssetUrls option. vue-jest 3.x does not support passing options to #vue/component-compiler-utils, but that now works in 4.0.0-rc.1 via a templateCompiler.transformAssetUrls config.
Even with this URL parsing enabled, Vue CLI configures jest to return an empty string for require-ed media, including images. If your tests need to work with the normally resolved URLs in production, you'll need a Jest transform that mimics url-loader. Vue CLI configures the loader to return the resolved filename if greater than 4KB; or the base64 data URL otherwise.
To enable the URL parsing:
Update to vue-jest 4:
npm i -D vue-jest#4
Create the following file for the custom my-jest-url-loader, which we'll use later below:
// <rootDir>/tests/my-jest-url-loader.js
const urlLoader = require('url-loader')
module.exports = {
process(src, filename) {
const urlLoaderOptions = {
esModule: false,
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
esModule: false,
emitFile: false,
name: filename,
},
},
}
const results = urlLoader.call({
query: urlLoaderOptions,
resourcePath: filename,
}, src)
// strip leading Webpack prefix from file path if it exists
return results.replace(/^module.exports = __webpack_public_path__ \+ /, 'module.exports = ')
}
}
To avoid accidentally overwriting Vue CLI's default Jest presets, use a merge utility (e.g., lodash.merge) to insert a custom config in jest.config.js.
Add a vue-jest config in a Jest global, setting templateCompiler.transformAssetUrls.
Modify the merged preset's transform property to use our my-jest-url-loader transform for images. This requires removing other image transforms from the default Jest preset to avoid conflicts.
// jest.config.js
const vueJestPreset = require('#vue/cli-plugin-unit-jest/presets/default/jest-preset')
const merge = require('lodash.merge') 3️⃣
const newJestPreset = merge(vueJestPreset, {
globals: { 4️⃣
'vue-jest': {
templateCompiler: {
transformAssetUrls: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: ['xlink:href', 'href'],
use: ['xlink:href', 'href']
}
}
}
},
moduleNameMapper: {
'^foo/(.*)$': '<rootDir>/$1',
},
})
function useUrlLoaderForImages(preset) { 5️⃣
const imageTypes = ['jpg', 'jpeg', 'png', 'svg', 'gif', 'webp']
const imageTypesRegex = new RegExp(`(${imageTypes.join('|')})\\|?`, 'ig')
// remove the image types from the transforms
Object.entries(preset.transform).filter(([key]) => {
const regex = new RegExp(key)
return imageTypes.some(ext => regex.test(`filename.${ext}`))
}).forEach(([key, value]) => {
delete preset.transform[key]
const newKey = key.replace(imageTypesRegex, '')
preset.transform[newKey] = value
})
preset.transform = {
...preset.transform,
[`.+\\.(${imageTypes.join('|')})$`]: '<rootDir>/tests/my-jest-url-loader',
}
}
useUrlLoaderForImages(newJestPreset)
module.exports = newJestPreset
GitHub demo
I am looking for a way to disable console.log() for production env. Something like putting the below code to nuxt.config.js or index.js:
if (process.env.NODE_ENV !== "development") {
console.log = () => {};
}
I tried it, but it doesn't work. Any help would be appreciated.
My nuxt.config.js is here
https://gist.github.com/somaria/9a2b0e06497d13a35fe9eee141a15d07
Nuxt's build process includes terser, which can be configured to automatically remove console statements from your production build. You could set build.terser.terserOptions:
// nuxt.config.js
export default {
build: {
terser: {
// https://github.com/terser/terser#compress-options
terserOptions: {
compress: {
drop_console: true
}
}
}
}
}
As an alternative, this can also be done with Plugins.
Under Plugins folder, we can create a file called disableLogs.js which can look like so:
// plugins/disableLogs.js
export function disableLogs() {
console.log = () => {};
// or you can override any other stuff you want
}
process.env.NODE_ENV === "production" ? disableLogs() : null;
Then we can register this plugin to be used inside nuxt.config.js
// nuxt.config.js
plugins: [
{ src: "~/plugins/disableLogs.js" },
{ src: "~/plugins/any-other-plugin.js"
],
This will run before instantiating the root Vue.js Application.
There are other things where you can configure it to run either client or server side, etc. More info here - https://nuxtjs.org/guide/plugins#vue-plugins
I'm using Webpack's [hash] for cache busting locale files. But I also need to hard-code the locale file path to load it from browser. Since the file path is altered with [hash], I need to inject this value to get right path.
I don't know how can get Webpack [hash] value programmatically in config so I can inject it using WebpackDefinePlugin.
module.exports = (env) => {
return {
entry: 'app/main.js',
output: {
filename: '[name].[hash].js'
}
...
plugins: [
new webpack.DefinePlugin({
HASH: ***???***
})
]
}
}
In case you want to dump the hash to a file and load it in your server's code, you can define the following plugin in your webpack.config.js:
const fs = require('fs');
class MetaInfoPlugin {
constructor(options) {
this.options = { filename: 'meta.json', ...options };
}
apply(compiler) {
compiler.hooks.done.tap(this.constructor.name, stats => {
const metaInfo = {
// add any other information if necessary
hash: stats.hash
};
const json = JSON.stringify(metaInfo);
return new Promise((resolve, reject) => {
fs.writeFile(this.options.filename, json, 'utf8', error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
});
}
}
module.exports = {
// ... your webpack config ...
plugins: [
// ... other plugins ...
new MetaInfoPlugin({ filename: 'dist/meta.json' }),
]
};
Example content of the output meta.json file:
{"hash":"64347f3b32969e10d80c"}
I've just created a dumpmeta-webpack-plugin package for this plugin. So you might use it instead:
const { DumpMetaPlugin } = require('dumpmeta-webpack-plugin');
module.exports = {
...
plugins: [
...
new DumpMetaPlugin({
filename: 'dist/meta.json',
prepare: stats => ({
// add any other information you need to dump
hash: stats.hash,
})
}),
]
}
Please refer to the Webpack documentation for all available properties of the Stats object.
Seems like it should be a basic feature but apparently it's not that simple to do.
You can accomplish what you want by using wrapper-webpack-plugin.
plugins: [
new WrapperPlugin({
header: '(function (BUILD_HASH) {',
footer: function (fileName) {
const rx = /^.+?\.([a-z0-9]+)\.js$/;
const hash = fileName.match(rx)[1];
return `})('${hash}');`;
},
})
]
A bit hacky but it works — if u don't mind the entire chunk being wrapped in an anonymous function.
Alternatively you can just add var BUILD_HASH = ... in the header option, though it could cause problem if it becomes a global.
I created this plugin a while back, I'll try to update it so it provides the chunk hash naturally.
On server, you can get the hash by reading the filenames (example: web.bundle.f4771c44ee57573fabde.js) from your bundle folder.
You can pass the version to your build using webpack.DefinePlugin
If you have a package.json with a version, you can extract it like this:
const version = require("./package.json").version;
For example (we stringified the version):
new webpack.DefinePlugin({
'process.env.VERSION': JSON.stringify(version)
}),
then in your javascript, the version will be available as:
process.env.VERSION
The WebpackManifestPlugin is officially recommended in the output management guide. It writes a JSON to the output directory mapping the input filenames to the output filenames. Then you can inject those mapped values into your server template.
It's similar to Dmitry's answer, except Dmitry's doesn't appear to support multiple chunks.
That can be done with Webpack Stats Plugin. It gives you nice and neat output file with all the data you want. And it's easy to incorporate it to the webpack config files where needed.
E.g. To get hash generated by Webpack and use it elsewhere.
Could be achieved like:
# installation
npm install --save-dev webpack-stats-plugin
yarn add --dev webpack-stats-plugin
# generating stats file
const { StatsWriterPlugin } = require("webpack-stats-plugin")
module.exports = {
plugins: [
// Everything else **first**.
// Write out stats file to build directory.
new StatsWriterPlugin({
stats: {
all: false,
hash: true,
},
filename: "stats.json" // Default and goes straight to your output folder
})
]
}
# usage
const stats = require("YOUR_PATH_TO/stats.json");
console.log("Webpack's hash is - ", stats.hash);
More usage examples in their repo
Hope that helps!
I am giving a try to Webpack, and am giving a try to the instructions in this tutorial, give or take a few custom things.
This is simple code, really, but I'm quite puzzled about this error, and feel this is something silly that I missed.
I defined two ES6 classes, each corresponding to a Handlebars template, and my app's entrypoint is supposed to replace the placeholder HTML in the index file by their contents:
Entrypoint:
import './bloj.less'
// If we have a link, render the Button component on it
if (document.querySelectorAll('a').length) {
require.ensure([], () => {
const Button = require('./Components/Button.js');
const button = new Button('9gag.com');
button.render('a');
}, 'button');
}
// If we have a title, render the Header component on it
if (document.querySelectorAll('h1').length) {
require.ensure([], () => {
const Header = require('./Components/Header.js');
new Header().render('h1');
}, 'header');
}
Index:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>My title</h1>
<a>Click me</a>
<script src="build/bloj.js"></script>
</body>
</html>
Button:
import $ from 'jquery';
import './Button.less';
export default class Button {
constructor(link) {
this.link = link;
}
onClick(event) {
event.preventDefault();
alert(this.link);
}
render(node) {
const text = $(node).text();
var compiled = require('./Button.hbs');
// Render our button
$(node).html(
compiled({"text": text, "link": this.link})
);
// Attach our listeners
$('.button').click(this.onClick.bind(this));
}
}
Header:
import $ from 'jquery';
import './Header.less';
export default class Header {
render(node) {
const text = $(node).text();
var compiled = require('./Header.hbs');
// Render the header
$(node).html(
compiled({"text": text})
);
}
}
Sadly, it does not work, and I get both these errors when displaying the page:
Uncaught TypeError: Header is not a constructor
Uncaught TypeError: Button is not a constructor
What could I be missing?
Here is my webpack configuration:
var path = require('path');
var webpack = require('webpack');
var CleanPlugin = require('clean-webpack-plugin');
var ExtractPlugin = require('extract-text-webpack-plugin');
var production = process.env.NODE_ENV === 'production';
var appName = 'bloj';
var entryPoint = './src/bloj.js';
var outputDir = './build/';
var publicDir = './build/';
// ************************************************************************** //
var plugins = [
//new ExtractPlugin(appName + '.css', {allChunks: true}),
new CleanPlugin(outputDir),
new webpack.optimize.CommonsChunkPlugin({
name: 'main',
children: true,
minChunks: 2
})
];
if (production) {
plugins = plugins.concat([
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.MinChunkSizePlugin({
minChunkSize: 51200 // 50ko
}),
new webpack.optimize.UglifyJsPlugin({
mangle: true,
compress: {
warnings: false // Suppress uglification warnings
}
}),
new webpack.DefinePlugin({
__SERVER__: false,
__DEVELOPMENT__: false,
__DEVTOOLS__: false,
'process.env': {
BABEL_ENV: JSON.stringify(process.env.NODE_ENV)
}
})
]);
}
module.exports = {
entry: entryPoint,
output: {
path: outputDir,
filename: appName + '.js',
chunkFilename: '[name].js',
publicPath: publicDir
},
debug: !production,
devtool: production ? false : 'eval',
module: {
loaders: [
{
test: /\.js/,
loader: "babel",
include: path.resolve(__dirname, 'src'),
query: {
presets: ['es2015']
}
},
{
test: /\.less/,
//loader: ExtractPlugin.extract('style', 'css!less')
loader: "style!css!less"
},
{
test: /\.html/,
loader: 'html'
},
{
test: /\.hbs/,
loader: "handlebars-template-loader"
}
]
},
plugins: plugins,
node: {
fs: "empty" // Avoids Handlebars error messages
}
};
What could I be missing?
Babel assigns default exports to the default property. So if you use require to import ES6 modules, you need to access the default property:
const Button = require('./Components/Button.js').default;
I realize that you already have an answer. However I had a similar issue to which I found an answer. Starting my own question and answering it seems weird.
So I'm just going to leave this here.
I had the same error as you got. However, I managed to solve it by changing my
export default {Class}
to
export default Class
I don't know why I wrapped the Class in an object but I remember having seen it somewhere so I just started using it.
So instead of the default returning a Class it returned an object like this {Class: Class}.
This is completely valid yet it will break webpack+babel.
EDIT: I've since come to know why this probably breaks babel+webpack. The export default is meant to only have 1 export. A javascript-object can contain many properties. Which means it can have more than 1 export. (See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export).
For multiple exports use: export {definition1, definition2}.
Use-case: I've used this in a situation where I've created a library which exported different types of an editor (while the underlying code was the same, the appearance of the editor changes depending on which export you use).
You can just put export var __useDefault = true; just after exporting your Class.
export default class Header {
...
}
export var __useDefault = true;
I was able to fix this by adding babel-plugin-add-module-exports to the .babelrc file
npm install babel-plugin-add-module-exports --save-dev
{
"presets": ["#babel/env"],
"plugins": ["add-module-exports"]
}
this adds
module.exports = exports.default;
to the last line when compiling the class with babel.
Although this is not the cause of your particular issue, I ran into a very similar problem when trying to rip babel out of an existing node app that was using ES6's import and export syntax, so this post is to help out anyone else struggling with this in the future.
Babel will resolve any circular dependencies between one module and another, so you can use ES6's import and export with reckless abandon. However, if you need to get rid of babel and use native node, you will need to replace any import and exports with require. This can reintroduce a latent circular reference issues that babel was taking care of in the background. If you find yourself in this situation, look for an area in your code that looks like this:
File A:
const B = require('B');
class A {
constructor() {
this.b = new B();
}
}
module.exports = A;
File B:
const A = require('A'); // this line causes the error
class B {
constructor() {
this.a = new A();
}
}
module.exports = B;
There are several different ways to resolve this issue depending on how you structured your code. The easiest way is probably to pass B a reference to A instead of creating a new instance of class A. You could also dynamically resolve the reference when loading A. There are a myriad of other alternatives, but this is a good place to get started.
It's not the problem in this particular question, but for some reasons, babel does not hoist classes in the same file.
So if you declare your class Token at the top of the file, and write later new Token(), it will run.
If you declare your class after the constructor call, you will have the xxx is not a constructor error
I had the same error message and discovered that the cause was circular import statements. That is: I had two files that imported each other, wherein one file contained an export default class that contained a method that was dependent upon an export function from the other file.
My solution was to move one of the dependencies (functions) out of the class and into a utils.js file, which was a more appropriate place for it anyway!
This is the way I am using / importing my classes:
Utils.class.js
export default class Utils {
somefunction(val) {
return val
}
}
Using Utils into my controllers:
import {default as U} from '../helpers/Utils.class';
const Utils = new U();
console.log(Utils.somefunction(123));