How to call function bundled by Webpack in Electron main js script - javascript

I'm trying to access a function from my (webpack'ed) bundle, but the function is undefined. (The entire object in empty) (In electron-main.js see appBundle.test())
I've been searching all over the web, but can't find the answer I'm looking for.
Tried all the different libraryTarget options, but none of them worked the way I expected.
Here is the source code:
webpack.dev.js:
...
module.exports = Merge(CommonConfig, {
devtool: "inline-source-map",
watch: true,
entry: path.resolve(__dirname, "src/index.ts"),
output: {
filename: "bundle.js",
path: __dirname + "/wwwroot/resources/js/components",
publicPath: "/resources/js/components/",
library: "appBundle",
libraryTarget: "commonjs2"
},
plugins: ([
new webpack.DefinePlugin({
"process.env": {
"NODE_ENV": JSON.stringify("development")
}
}),
// Etract CSS
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
}),
]),
})
index.ts:
import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
// Styling
import './application-styling.scss';
// Application
import "./application/index.tsx";
export default function test() {
console.log('hello');
}
electron-main.js:
const { BrowserWindow } = require('electron');
const createAppWindow = require('../main/app-process');
const authService = require('../services/auth-service');
global.window = {}; // <- Need to define this manually, else the require call throws errors
const appBundle = require('app-bundle'); // Trying to import the bundle.js here
let win = null;
function createAuthWindow() {
destroyAuthWin();
win = new BrowserWindow({
width: 1000,
height: 600,
webPreferences: {
nodeIntegration: false,
enableRemoteModule: false
}
});
win.loadURL(authService.getAuthenticationURL());
const { session: { webRequest } } = win.webContents;
const filter = {
urls: [
'http://localhost/callback*'
]
};
webRequest.onBeforeRequest(filter, async ({ url }) => {
console.log(appBundle); // <- undefined or empty object, depending on libraryTarget
console.log(appBundle.test()); // <- error, test is undefined
await authService.loadTokens(url);
createAppWindow();
return destroyAuthWin();
});
...
}
...

Apparently the import "./application/index.tsx"; call somehow messed up the exports. When I just used a clean file as the entry point it worked fine.
Anyway I ended up just building the electron project together with the app bundle into one package.

Related

Stop webpack from instantiating module multiple times across different entry points

I'm generating a static HTML page with Webpack. I have a custom logging module, and then two other modules which import it. Those imports are in different entry points. The problem is, the logging module is actually being instantiated twice.
sendtolog.js:
'use strict';
import { v4 } from "uuid";
console.log('logging...');
const ssid = v4();
export default function sendToLog(metric) {
console.log(`Sending message with ${ssid}`);
}
webvitals.js:
import { getTTFB } from 'web-vitals/base';
import sendToLog from './sendtolog';
getTTFB(sendToLog);
pageactions.js:
'use strict';
import sendToLog from './sendtolog';
sendToLog({name: 'foo', value: 'bar'});
and then in the browser console:
[Log] logging...
[Log] Sending message with 53f50779-d430-49e1-a1be-5b1bb33db10b
[Log] logging...
[Log] Sending message with 415dd4b9-e089-4feb-a4cf-d29c12a26149
How do I get it to not do that?
The WebPack docs for optimization.runtimeChunk have a giant warning:
Imported modules are initialized for each runtime chunk separately, so
if you include multiple entry points on a page, beware of this
behavior. You will probably want to set it to single or use another
configuration that allows you to only have one runtime instance.
but I'm not using runtimeChunk, I'm just using splitChunks, which I assumed would be "another configuration that allows you to only have one runtime instance."
My WebPack config:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = (env, argv) => {
console.log('Webpack mode: ', argv.mode);
const config = {
entry: {
'main': [
path.resolve(__dirname, './src/pageactions.js'),
],
'inline': [ path.resolve(__dirname,
'./node_modules/web-vitals/dist/polyfill.js'),
path.resolve(__dirname, './src/webvitals.js')
]
},
module: {
rules: [
// JavaScript
{
test: /\.(js)$/,
exclude: /node_modules/,
use: ['babel-loader']
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './views/index.ejs'),
filename: path.resolve(__dirname, 'index.html'),
output: {
path: path.resolve(__dirname, './public'),
filename: '[name].bundle.js'
},
optimization: {
splitChunks: {
chunks: 'all'
},
},
devtool: ('production' === argv.mode) ? false : 'eval',
mode: argv.mode
}
return config;
};

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 Hot module replacement, react 18 ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before

I updated React to v18 and my Webpack dev server gives me a console error whenever the hot module replacement fires and injects the new javascript code:
Warning: You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. Instead, call root. render() on the existing root instead if you want to update it.
index.js file
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(<App />);
if (module.hot) module.hot.accept(function (err) {
console.log('An error occurred while accepting new version');
});
webpack.config.js
const path = require('path');
const HtmlWEbpackPlugin = require('html-webpack-plugin');
module.exports = (env) => {
let cfg = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[contenthash:6].js',
publicPath: '/',
clean: true
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
]
},
plugins: [new HtmlWEbpackPlugin({ template: './src/index.html' })
],
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
compress: true,
port: 3000,
hot: true,
open: true,
},
performance: {
hints: false
}
}
return cfg;
};
React 18 has native support for hot reloading, this is called Fast Refresh. An excerpt from the react-refresh readme:
Fast Refresh is a feature that lets you edit React components in a running application without losing their state. It is similar to an old feature known as "hot reloading", but Fast Refresh is more reliable and officially supported by React.
To use this with Webpack 5, you will need a plugin called react-refresh-webpack-plugin. To get it to work I would recommend taking a look at the examples included in the git repository, especially the webpack-dev-server example.
NOTE: As of writing this answer, the react-refresh-webpack-plugin is in an experimental state but create-react-app is already using it, so it is probably stable enough to use.
The following is taken straight from react-refresh-webpack-plugin's webpack-dev-server example:
src/index.js
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App />);
webpack.config.js
const path = require('path');
const ReactRefreshPlugin = require('#pmmmwh/react-refresh-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
devServer: {
client: { overlay: false },
},
entry: {
main: './src/index.js',
},
module: {
rules: [
{
test: /\.jsx?$/,
include: path.join(__dirname, 'src'),
use: 'babel-loader',
},
],
},
plugins: [
isDevelopment && new ReactRefreshPlugin(),
new HtmlWebpackPlugin({
filename: './index.html',
template: './public/index.html',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.jsx'],
},
};
babel.config.js
module.exports = (api) => {
// This caches the Babel config
api.cache.using(() => process.env.NODE_ENV);
return {
presets: [
'#babel/preset-env',
// Enable development transform of React with new automatic runtime
['#babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
],
// Applies the react-refresh Babel plugin on non-production modes only
...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
};
};
You can remove the following from your Webpack entry point:
src/index.js
// ...
if (module.hot) {
module.hot.accept()
}
This has the small drawback that whenever you modify your entry point (src/index.js) a full reload is necessary. Webpack is very in your face about needing to do a full reload, showing you the following log messages.
This really annoyed me. When looking at how create-react-app solved this, I found that they disabled client side logging for the webpack-dev-server, or at least set the log level to warn or error. You can set the log level by setting the client.logging property in the devServer configuration:
webpack.config.js
// ...
devServer: {
client: {
overlay: false,
logging: 'warn' // Want to set this to 'warn' or 'error'
}
}
// ...
What the odd thing is about the "warning", is that it is not a warning at all, it is just an info message dressed up as a warning.
Hope this helps.

Require doesn't appear in my code, but webpack keeps throwing the error "require is not defined."

I'm writing an electron app with react. I run the developement version using this command:
webpack-dev-server --hot --host 0.0.0.0 --port 4000 --config=./webpack.dev.config.js
Here is the webpack.dev.config.js file
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { spawn } = require('child_process');
const helpers = require('./config/helpers');
// Config directories
const SRC_DIR = path.resolve(__dirname, 'src');
const OUTPUT_DIR = path.resolve(__dirname, 'dist');
// Any directories you will be adding code/files into, need to be added to this array so webpack will pick them up
const defaultInclude = [SRC_DIR];
module.exports = {
entry: SRC_DIR + '/index.js',
output: {
path: OUTPUT_DIR,
publicPath: '/',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
include: defaultInclude
},
{
test: /\.jsx?$/,
use: [{ loader: 'babel-loader' }],
include: defaultInclude
},
{
test: /\.(jpe?g|png|gif)$/,
use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }],
include: defaultInclude
},
{
test: /\.(eot|svg|ttf|woff|woff2)$/,
use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }],
include: defaultInclude
}
]
},
target: 'electron-renderer',
plugins: [
new HtmlWebpackPlugin({
template: helpers.root('public/index.html'),
inject: 'body'
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
})
],
devtool: 'cheap-source-map',
devServer: {
contentBase: OUTPUT_DIR,
stats: {
colors: true,
chunks: false,
children: false
},
setup() {
spawn(
'electron',
['.'],
{ shell: true, env: process.env, stdio: 'inherit' }
)
.on('close', code => {
console.error("electron exited with code ", code);
process.exit(0)
})
.on('error', spawnError => console.error(spawnError));
}
}
};
Once the electron browser opens it has the following error in the Dev-Tools console.
Uncaught ReferenceError: require is not defined
at Object.url (index.js:23)
at __webpack_require__ (bootstrap:709)
at fn (bootstrap:94)
at Object../node_modules/webpack-dev-server/client/utils/createSocketUrl.js (createSocketUrl.js:4)
at __webpack_require__ (bootstrap:709)
at fn (bootstrap:94)
at Object.<anonymous> (client:20)
at Object../node_modules/webpack-dev-server/client/index.js?http://0.0.0.0:4000 (client:176)
at __webpack_require__ (bootstrap:709)
at fn (bootstrap:94)
The place where it claims this is occurring is at index.js:23.
Here is the build version of index.js:
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import App from "./components/App";
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { ipcRenderer as ipc } from "electron";
import { onUpdate } from "./actions/workerActions";
import { RECEIVED_STATE } from "./actions/types";
import "./assets/css/index.css";
import rootReducer from "./reducers/rootReducer";
import defaultState from "../config/defaultstate"; //Setup redux store
const middleware = [thunk];
const store = createStore(rootReducer, defaultState, applyMiddleware(...middleware));
ipc.on(RECEIVED_STATE, arg => {
console.log("Recieved State: ", arg);
onUpdate(arg)(store.dispatch);
}); // Now we can render our application into it
render(React.createElement(Provider, {
store: store
}, React.createElement(App, null)), document.getElementById("app"));
As you can see require does not appear here and all of the import aside from ipcRender are designed to run client-side, and therefore should not use required. I tried commenting out the ipcRender import but it resulted in the exact same error.
Most puzzlingly of all, I get the exact same error even with the entire index.js file commented out. The console still claiming the block comment contains a reference to require, which is not defined.
If you're using webpack directly then make sure you have the following in the webpack config that targets your renderer code.
// webpack.config.js
...
module.exports = {
...
target: 'web',
...
}
If you're using Vue then you'll need something like the following:
// vue.config.js
...
module.exports = {
...
configureWebpack: {
target: 'web'
},
...
}
Or, in my case I was using vue-cli-plugin-electron-builder and so need the following:
// vue.config.js
...
module.exports = {
...
pluginOptions: {
electronBuilder: {
nodeIntegration: false,
chainWebpackRendererProcess: config => {
config.target('web');
}
}
},
...
}
It turns out that the error is caused by importing electron's ipcRenderer which requires node integration and uses require. The reason that commenting out the import in the index.js didn't fix the error was because it was imported in other files.

How to make a javascript webpack module made for the browser safe to load in node environment?

I am trying to upgrade an old framework I built in javascript to es6/module standards and I have a lot of troubles.
One of my current problems is that due to server side rendering my modules are sometime loaded in the node environment and are trying to access the window, causing errors.
Is there a principled way to manage this ?
The main jQuery file has a nice failback if window is undefined and can load in node without a fuss. I am trying to implement this in web-pack by I am stumbling.
This is my current webpack config
// #flow
// import path from 'path'
import webpack from 'webpack'
const WDS_PORT = 7000
const PROD = JSON.parse(process.env.PROD_ENV || '0')
const libraryName = 'experiment'
const outputFile = `${libraryName}${PROD ? '.min' : '.max'}.js`
const plugins = [
new webpack.optimize.OccurrenceOrderPlugin(),
]
const prodPlugins = plugins.concat(new webpack.optimize.UglifyJsPlugin())
// not really working
export default {
entry: './builder.js',
target: 'web',
output: {
path: `${__dirname}/lib`,
filename: outputFile,
library: libraryName,
libraryTarget: 'umd',
umdNamedDefine: true,
},
module: {
loaders: [
{
test: /(\.jsx|\.js)$/,
loader: 'babel-loader',
exclude: /(node_modules|bower_components)/,
},
],
},
devtool: PROD ? false : 'source-map',
resolve: {
extensions: ['.js', '.jsx'],
},
externals: {
chartjs: {
commonjs: 'chartjs',
amd: 'chartjs',
root: 'Chart', // indicates global variable
},
lodash: {
commonjs: 'lodash',
amd: 'lodash',
root: '_', // indicates global variable
},
jquery: 'jQuery',
mathjs: {
commonjs: 'mathjs',
amd: 'mathjs',
root: 'math', // indicates global variable
},
'experiment-boxes': {
commonjs: 'experiment-boxes',
amd: 'experiment-boxes',
root: 'experimentBoxes', // indicates global variable
},
'experiment-babylon-js': {
commonjs: 'experiment-babylon-js',
amd: 'experiment-babylon-js',
root: 'EBJS', // indicates global variable
},
},
devServer: {
port: WDS_PORT,
hot: true,
},
plugins: PROD ? prodPlugins : plugins,
}
And this is my main entry point builder.js
/* --- Import the framwork --- */
import TaskObject from './src/framework/TaskObject'
import StateManager from './src/framework/StateManager'
import State from './src/framework/State'
import EventData from './src/framework/EventData'
import DataManager from './src/framework/DataManager'
import RessourceManager from './src/framework/RessourceManager'
import {
Array,
String,
diag,
rowSum,
getRow,
matrix,
samplePermutation,
rep,
Deferred,
recurse,
jitter,
delay,
looksLikeAPromise,
mustHaveConstructor,
mustBeDefined,
mandatory,
debuglog,
debugWarn,
debugError,
noop,
} from './src/framework/utilities'
/* add it to the global space in case user want to import in a script tag */
if (typeof window !== 'undefined') {
window.TaskObject = TaskObject
window.StateManager = StateManager
window.State = State
window.EventData = EventData
window.DataManager = DataManager
window.RessourceManager = RessourceManager
window.jitter = jitter
window.delay = delay
window.Deferred = Deferred
}
export {
TaskObject,
StateManager,
State,
EventData,
DataManager,
RessourceManager,
Array,
String,
diag,
rowSum,
getRow,
matrix,
samplePermutation,
rep,
Deferred,
recurse,
jitter,
delay,
looksLikeAPromise,
mustHaveConstructor,
mustBeDefined,
mandatory,
debuglog,
debugWarn,
debugError,
noop,
}
Am I on the right track?
Ok my solution so far, although feels like a hack, protects against require() in node environment.
In the ENTRY FILE of your webpack config check for window being defined.
Here is an example when trying to re-bundle babylonjs which relies heavily on window and would generate an error when required by node:
builder.js
let BABYLON = {}
let OIMO = {}
if (typeof window !== 'undefined') {
BABYLON = require('./src/babylon.2.5.full.max')
OIMO = require('./src/Oimo').OIMO
window.BABYLON = BABYLON
window.OIMO = OIMO
}
module.exports = { BABYLON, OIMO }
webpack.config.babel.js
import path from 'path'
import webpack from 'webpack'
const WDS_PORT = 7000
const PROD = JSON.parse(process.env.PROD_ENV || '0')
const plugins = [
new webpack.optimize.OccurrenceOrderPlugin(),
]
const prodPlugins = plugins.concat(new webpack.optimize.UglifyJsPlugin())
export default {
entry: [
'./builder.js',
],
output: {
filename: PROD ? 'babylon.min.js' : 'babylon.max.js',
path: path.resolve(__dirname, 'lib/'),
publicPath: `http://localhost:${WDS_PORT}/lib/`,
library: 'EBJS',
libraryTarget: 'umd',
umdNamedDefine: true,
},
module: {
rules: [
{ test: /\.(js|jsx)$/, use: 'babel-loader', exclude: /node_modules/ },
],
},
devtool: PROD ? false : 'source-map',
resolve: {
extensions: ['.js', '.jsx'],
},
devServer: {
port: WDS_PORT,
hot: true,
},
plugins: PROD ? prodPlugins : plugins,
}
Testing the bundle in node with a simple file like so:
bundle.test.js
const test = require('./lib/babylon.min.js')
console.log(test)
Will produce in the terminal:
$ node bundle.test.js
{ BABYLON: {}, OIMO: {} }

Categories