I'm using Webpack in my React/Rails application. My team uses .svg images in parts of our app.
We have two primary use-cases for them.
Using SVGs as background images within our stylesheets (we use .scss).
Using SVGs within React components, with a tool called SVGR which transforms SVGs into React components.
Therefore, we have the following configuration set up in our config/webpack/environment.js file (Rails' Webpack integration uses this unusual scheme for Webpack configuration):
const { environment } = require("#rails/webpacker")
const fileLoader = environment.loaders.get("file")
const babelLoader = environment.loaders.get("babel")
// ... other stuff
environment.loaders.insert("svg", {
test: /\.svg$/,
issuer: { test: /\.jsx?$/ },
use: babelLoader.use.concat([
"#svgr/webpack"
])
}, { after: "file" })
fileLoader.exclude = /\.(svg)$/i
environment.loaders.insert("scss svg", {
test: /\.svg$/,
issuer: { test: /\.scss?$/ },
use: ["file-loader"]
}, { after: "svg" })
// ... more other stuff
module.exports = environment
Then, we get the following error in our Webpack build:
WARNING in ./app/javascript/shared/svg/Phone.svg 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
The strange thing is, the configuration actually works as is for both use cases I described.
As you can see, we are using the issuer option in the loader configuration, which instructs Webpack on which loader to use depending upon which file type is doing the import of an SVG.
I'm wondering if there are any Webpack experts who could explain why Webpack still issues this warning, even though there are in fact loaders configured to handle the file type, and also when there is no "real" problem in the output bundles (no runtime errors or broken images).
Thanks!
I'm not sure how to solve it, but I know what's happening.
At some point you're doing a dynamic import with a template parameter. This parameter is loose enough that it's matching your .svg files. This means webpack will make those svgs entry points because it doesn't know whether you will match them or not. Because they are entries, they have no issuer.
I don't know if there's a way to specify a loader with no issuer. However, if you try making your template string in the dynamic import a bit more strict so it cannot match the svgs, you'll find the warning goes away.
Related
Tailwind's documentation states that you can reference variables inside your Javascript code via the resolveScript plugin. So, for example, the code on the client would look something like this:
import resolveConfig from 'tailwindcss/resolveConfig'
import tailwindConfig from './tailwind.config.js'
const fullConfig = resolveConfig(tailwindConfig)
fullConfig.theme.width[4] // => '1rem'
This answer explains how this can be done with Vite (I'm using Webpack). We're processing the Tailwind via the PostCSS Webpack loader, like this:
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
require('tailwindcss')
]
}
}
}
This plugin requires that you provide the path to your Tailwind configuration file to import it on the client-side. Still, my tailwind configuration file lives outside my source folder for the client and is written in CommonJS syntax (e.g., I'm exporting it via module.exports). Therefore, attempts to import it as an EcmaScript module will not succeed (we are exporting our configuration using module.exports rather than export default in other words).
Does anyone know the fix for this? For example, is it possible to load a CommonJS tailwind file into the client, or would I have to convert the entirety of my build step process to ESM?
I'm trying to include a VLC video playing in my Electron app, which is possible through WebChimera.js. This package is distributed a bit weirdly (to me at least), to use it you need to require wcjs-prebuilt, specify some settings in package.json and configure Webpack to allow importing .node files as explained in this Wiki page for WebChimera.js.
However I believe this Wiki page is outdated, as loaders isn't a valid key anymore in a Webpack config. I'm not very experienced using Webpack so most of this is new to me. Also note that this Wiki explanation used a fork of node-loader, although this fork seems to be merged to the actual node-loader now (?).
I now use this Webpack config:
target: 'node',
node: {
__dirname: false,
},
module: {
rules: [
{
test: /\.node$/,
loader: 'node-loader',
},
],
},
externals: [
'wcjs-prebuilt',
],
Because that's how the Webpack page for node-loader seems to do it. However this doesn't work for me, as now I get the error: Uncaught ReferenceError: exports is not defined in the chunk-vendors.js:1 file. Which probably means it's trying to use require syntax somewhere it shouldn't, but I have no idea how to proceed here. This error still occurs in an otherwise empty vue-electron project (template here), when I comment out all WebChimera related code. WebChimera code I use for testing in this project (Right now I'm just trying to get it to work):
const wcjs = require("wcjs-prebuilt");
console.log(wcjs)
When I remove the webpack config I showed above, the error about exports is not defined goes away, which is why I believe it's something in my webpack config rather than my code causing that error.
Long story short, I want to know how configure webpack to allow me to import or a require a .node file.
I'm able to get vue electron building with wcjs-prebuilt using a vue.config.js like this. You will also need to set the VLC_PLUGIN_PATH correctly or video won't play.
module.exports = {
configureWebpack: {
externals: {
'wcjs-prebuilt': 'commonjs wcjs-prebuilt'
},
},
chainWebpack: (config) => {
config.module
.rule('node')
.test(/.node$/i)
.use('node-loader')
.loader('node-loader')
.end()
},
pluginOptions: {
electronBuilder: {
externals: ['wcjs-prebuilt']
}
}
}
Since posting the question I've switched to mpv.js for video playback so this isn't an issue for me anymore. However after posting this question I experimented a lot, after I finally got it working in Webpack somehow (see first link below), it worked for me but with distorted video. The node file added some properties to an array which Webpack somehow stripped away, causing some missing values in the video renderer. I fixed that by forking WebChimera and editing the C++ code so that the values weren't added as properties but as separate values.
I ended up forking WebChimera.js, wcjs-prebuilt, wcjs-renderer, and libvlc_wrapper to get VLC to finally work with Webpack+Electron, all that probably wasn't necessary but oh well..
Links for whoever might be interested:
https://github.com/RuurdBijlsma/vlc-video-demo (working demo project featuring VLC in an Electron+Webpack+Vue project.)
https://github.com/RuurdBijlsma/libvlc_wrapper
https://github.com/RuurdBijlsma/wcjs-renderer
https://github.com/RuurdBijlsma/WebChimera.js
https://github.com/RuurdBijlsma/wcjs-prebuilt
I'm creating a tool which launches a server and fetches content from the server and displays it in the browser. I'm trying to integrate it with frontend frameworks. One of those frameworks is Sapper/Svelte. The problem is that my bundle contains imports to built-in modules which are not needed by the browser, and also not resolved by the browser, which in turn throws an error.
I think what I need to do is make my tool isomorphic and split my tool it into two bundles. One for the server (server.js), and one for the browser (client.js) which doesn't contain the imports to built-in modules. I have a good idea of how I can split the code, using code splitting in Rollup, but what I don't know is how I tell Sapper to use server.js for the server and client.js for the client.
How can I bundle my module so when it's consumed by other applications it knows which one to use for the server and which one to use for the browser? Is this something I can do in my module or do I have to also configure this in the framework it's being used in?
I discovered that #rollup/plugin-node-resolve has a flag to instruct Rollup to use an alternative bundle specified in the browser property in the module's package.json.
As Sapper is configured to create a bundle for both the client and server it has this flag already in it's rollup.config.js.
Sapper
// rollup.config.js
export default {
client: {
// ...
plugins: [
// ...
resolve({
browser: true, // <-- flag
dedupe: [ 'svelte' ],
exclude: [ 'node_modules/**' ]
})
// ...
No changes needed here.
Your NPM Module
You need to create two bundles. One for the server and one for the browser. I felt it was easier to create two different entry points in Rollup for this. It might be possible to use the same entry point and use conditional logic to output a different bundle (something I'm not familiar with).
// rollup.config.js
export default [
{
input: 'src/server.js',
output: {
file: 'dist/server.js',
format: 'cjs'
}
},
{
input: 'src/browser.js',
output: {
file: 'dist/browser.js',
format: 'cjs'
}
}
];
Now we add the path to the browser specific bundle in package.json.
// package.json
{
"main": "dist/server.js"
// ...
"browser": "dist/browser.js"
}
Setting this up means that when Sapper starts it will use a different bundle for the server and the client. When you create the separate bundles you need to structure them so that they work independently of each other. In my case, I isolated all server functionality to the server-specific bundle and excluded all dependencies like http, fs and path from the browser-specific bundle.
I am trying to set up the following architecture: a core React application that gets built with some basic functionality, and the ability to load additional React components at runtime. These additional React components can be loaded on-demand, and they are not available at build time for the core application (so they cannot be included in the bundles for the core application, and must be built separately). After researching for some time, I came across Webpack Externals, which seemed like a good fit. I am now building my modules separately using the following webpack.config.js:
const path = require('path');
const fs = require('fs');
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
module.exports = {
entry: './src/MyModule.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'MyModule.js',
library: 'MyModule',
libraryTarget: 'umd'
},
externals: {
"react": "react",
"semantic-ui-react": "semantic-ui-react"
},
module: {
rules: [
{
test: /\.(js|jsx|mjs)$/,
include: resolveApp('src'),
loader: require.resolve('babel-loader'),
options: {
compact: true,
},
}
]
},
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx']
}
};
Took a look at the generated MyModule.js file, and it looks correct to me.
Now, in my core app, I am importing the module as follows:
let myComponent = React.lazy(() => import(componentName + '.js'));
where componentName is the variable that matches the name of my module, in this case, "MyModule" The name is not known at build time, and the file is not present in the src folder at build time. To avoid errors from webpack when building this code with an unknown import, I have added the following to my webpack.config.js for the core project:
module.exports = {
externals: function (context, request, callback/*(err, result)*/) {
if (request === './MyModule.js') {
callback(null, "umd " + request);
} else {
callback();
}
}
}
I have confirmed that the function in externals gets called during the build, and the if condition is matched for this module. The build succeeds, and I am able to run my core application.
Then, to test dynamic loading, I drop MyModule.js into the static/js folder where the bundles for my core app live, then I navigate to the page in my core app that requests MyModule via let myComponent = React.lazy(() => import(componentName + '.js'));
I see a runtime error in the console on the import line,
TypeError: undefined is not a function
at Array.map (<anonymous>)
at webpackAsyncContext
My guess is it's failing to find the module. I don't understand where it is looking for the module, or how to get more information to debug.
Turns out that I was making a couple of incorrect assumptions about webpack and dynamic loading.
I was having issues with two things - the kind of module I was loading, and the way that I was loading it.
Dynamic importing is not yet a standard ES feature - it is due to be standardized in ES 2020. This dynamic import will only return a module if the module object you are attempting to load is an ES6 module (aka something that contains an 'export ModuleName'). If you attempt to load something packed up as a CommonJS module, AMD, UMD, the import will succeed, but you will get an empty object. Webpack does not appear to support creating bundles in ES6 format - it can create a variety of module types, and in my config file above, I was actually creating UMD modules (configured via libraryTarget setting).
I had issues with the import statement itself because I was using it within an app bundled by Webpack. Webpack reinterprets the standard ES import statement. Within a standard webpack config (including the one you get from CRA), webpack uses this statement as a split point for bundles, so even modules that are dynamically imported are expected to be there at webpack build time (and the build process will fail if they are not available). I had tried to use webpack externals to tell webpack to load the modules dynamically, which allowed the build to succeed without the modules being there. However, the app still used Webpack's import function instead of the standard JS import function at runtime. I confirmed this by attempting to run import('modulename') from the browser console and getting a different result than my app, which was bundled with webpack.
To solve problem #2, you can tell Webpack to not reinterpret the ES dynamic import by adding some annotation to the import statement.
import(/*webpackIgnore: true*/ 'path/to/module.js');
This will both prevent Webpack from attempting to find and bundle the dynamically imported module at build time, and attempting to import it at runtime. This will make behavior in the app match behavior in the browser console.
Problem #1 was a bit more difficult to solve. As I mentioned above, importing a non-ES6 module will return an empty object (if you await the promise or use .then()). However, as it turns out, the file itself does load and the code gets executed. You can export the module in the "window" format using Webpack, and then load it as follows.
await import(/*webpackIgnore: true*/`path/to/module.js`);
let myModule = window['module'].default;
Another potential solution that avoids using the window object is building the module using a system capable of producing ES6 modules (so, not Webpack). I ended up using Rollup to create an ES6 module that pulled all dependencies into a single file, and ran the output through Babel. This produced a module that loaded successfully via a dynamic ES import. The following was my rollup.config.js (note that I included all external node modules needed in my module - this bloated the module size but is a requirement for my specific application - yours will likely differ and you will need to configure rollup to exclude the modules)
// node-resolve will resolve all the node dependencies
import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import replace from 'rollup-plugin-replace';
export default {
input: 'src/myModule.jsx',
output: {
file: 'dist/bundle.js',
format: 'esm'
},
plugins: [
resolve(),
babel({
exclude: 'node_modules/**'
}),
commonjs({
include: 'node_modules/**',
namedExports: {
'node_modules/react/index.js': ['Children', 'Component', 'PropTypes', 'PureComponent', 'React', 'createElement', 'createRef', 'isValidElement', 'cloneElement', 'Fragment'],
'node_modules/react-dom/index.js': ['render', 'createElement', 'findDOMNode', 'createPortal'],
'node_modules/react-is/index.js': ['isForwardRef']
}
}),
replace({
'process.env.NODE_ENV': JSON.stringify( 'production' )
})
]
}
I would like to use https://www.npmjs.com/package/createjs-soundjs to play sounds on a react web app.
I installed the package normally with npm install createjs-soundjs and it's showing in the packages list. How should I include this to my project, so that I could use it?
I have tried the following:
import React from 'react';
import classNames from 'classnames';
import createjs from 'createjs';
import SoundJS from 'createjs-soundjs'; // this line causes an error
The error message in console: soundjs-0.6.2.min.js:17 Uncaught ReferenceError: createjs is not defined. This error message is very clear, but I have no idea where to start. Am I missing something fundamental?
The SoundJS core bundle is not in the proper CommonJS or ES6 format, it's a bundle that makes strong assumption that it's loaded as a top-level script in the browser. So it first tries to create the global value by assigning it to a property on this (which it assumes to be the window) and then tries to access this global values as just createjs.
So obviously this doesn't work in the module context.
There's a workaround though, the code is for Webpack 2 and based on this comment (which is for Webpack 1):
module: {
rules: [
// ...
{
test: /createjs/,
use: [
'imports-loader?this=>window',
'exports-loader?createjs'
]
},
// ...
]
},
plugins: [
// ...
new webpack.ProvidePlugin({
'createjs': 'createjs',
}),
// ...
],
resolve: {
alias: {
'createjs': path.resolve(__dirname, '../lib/soundjs-0.6.2.combined'),
}
},
The 3rd entry (resolve.alias) maps the imports of createjs to the file I've downloaded and placed into my lib folder (which is not as elegant as using something from npm, but now I'm sure what I get. npm version might work as well).
The first entry (module.rules) tells Webpack to first substitute this with window, and then to take the createjs instance from the global (window) context and make it the module export.
Finally, the 2nd entry (ProvidePlugin) precedes all requests for global createjs (in other modules) with const createjs = require('createjs').