electron / forge

:electron: A complete tool for building and publishing Electron applications
https://electronforge.io
MIT License
6.41k stars 505 forks source link

Document how to use file-loader correctly for renderer webpack configs #1431

Open jega-ms opened 4 years ago

jega-ms commented 4 years ago

Assets are not getting loaded after making the binary using "yarn make" on all platform (windows/Mac/Linux). We are getting error "net::ERR_FILE_NOT_FOUND". Looks like Js files are pointing to the proper location as per the package.json configuration. but assets and source files are located in different location.

And we are seeing the same issue for component source which uses Lazy import statements

const Login = React.lazy(() => import('./screens/login'));

E.g on Mac

Trying to load the png file from  
/Contents/Resources/app/.webpack/renderer/main_window/33dbdd0177549353eeeb785d02c294af.png

But files are available in.  
/Contents/Resources/app/.webpack/renderer/33dbdd0177549353eeeb785d02c294af.png

package.json

"plugins": [
        [
          "@electron-forge/plugin-webpack",
          {
            "mainConfig": "./webpack.main.config.js",
            "renderer": {
              "config": "./webpack.renderer.config.js",
              "entryPoints": [
                {
                  "html": "./src/index.html",
                  "js": "./src/app.tsx",
                  "name": "main_window"
                }
              ]
            }
          }
        ]
      ]

Sample Project https://github.com/jega-ms/electron-forge-react-typescript-webpack

Screen Shot 2020-01-23 at 5 23 08 PM

File Listing

Screen Shot 2020-01-23 at 5 30 23 PM
michael-mason commented 4 years ago

1196 Might solve the asset issue you are having... it did for me.

my file-loader ended up looking like

{
  test: /\.(png|svg|jpe?g|gif|webm)$/,
  use: [
    {
      loader: 'file-loader',
      options: {
        name: '[hash]-[name].[ext]',
        outputPath: 'static',
        publicPath: '../static',
      },
    },
  ],
}
Joelkang commented 2 years ago

After landing on this issue and doing some of my own trial and error, I've come up with a solution that works both in dev and packaged, and combines various observations from this and linked issues.

Webpack setup

If you're using webpack5 (and you should be if you used the template), you no longer need file-loader or copy-webpack-plugin. Instead, you should rely on webpack's native asset management.

// webpack.rules.js
// Or in both your webpack.renderer.js and webpack.main.js if you keep them completely separate

module.exports = [
   // ... your other rules
   {
    // Note: I dont have `svg` here because I run my .svg through the `@svgr/webpack` loader, 
    // but you can add it if you have no special requirements
    test: /\.(gif|icns|ico|jpg|png|otf|eot|woff|woff2|ttf)$/,    
    type: 'asset/resource',
  },
]

Main process

Because Electron does magical things to an image based on the filename of the image, you should keep the original source file name in the output file name by configuring how webpack outputs asset names. Note that you can also do this for the renderer config if you prefer, but it's not a requirement since Chromium doesn't do anything magical based on the file name.

// `webpack.main.js`
module.exports = {
  entry: './src/main/index.ts',
  // ...other configs
  output: {
    // [file] is the key thing here. [query] and [fragment] are optional
    assetModuleFilename: '[file][query][fragment]', 
  },
}

When using bundled files in the main process, make sure to resolve the bundled path:

// main/index.ts
import path from 'path';
import dockImagePath from './path/to/dockImage.png';
import trayImagePath from './path/to/trayImage.png';

// For example setting the dock icon
app.dock.setIcon(path.resolve(__dirname, dockImagePath));

// Or setting the tray icon
const tray = new Tray(
    path.resolve(
      __dirname,
      trayImagePath,
    ),
  );

Renderer Processes

Because you can have many entrypoints, the webpack output of each entry point is nested under a renderer folder. By default, webpack outputs assets in the root output folder (which will be this same renderer folder). So your output file structure (inside of the .webpack folder) looks something like

> main
   - index.js
> renderer
   > entryPointOne
       - index.js
   > entryPointTwo
       - index.js
   - imageHash1.png
   - imageHash2.png

This means that in the outputted renderer/entryPointOne/index.js, we need to rewrite the image paths to one level above the index.js: ../. Note that this is essentially the same solve as the publicPath: '..' setting in file-loader from this and other related issues

// webpack.renderer.config.js
module.exports = {
  // ...other configs
  output: {
    publicPath: '../',
  },
}

Note that, like the main webpack config, you may change where webpack outputs its assets, but you still have to set the public path since it's outside of folder containing the index.js of each renderer entry point.


// webpack.renderer.config.js
module.exports = {
  // ...other configs
  output: {
    publicPath: '../myAssetFolder',
    // Note that here you are also keeping the original file name, but for the renderer you don't have to
    // and can simply rely on the default value of [hash][ext][query]
    assetModuleFilename: 'myAssetFolder/[file][query]',  
  },
}
bitbound commented 1 year ago

Here's a caveman solution, for my fellow Neanderthals.

Base64-encode the image. Here's a handy tool for it: https://github.com/veler/DevToys

Create a file like images.ts/images.js:

// TODO: Figure out how to use file-loader or Webpack Asset Modules later.  (If you're reading this two years later, congrats!  Your temporary solution became permanent.)
export const iconImage512x512Base64 = "data:image/png;base64,iVBORw0KGgoAAAA(etc. etc.)=";

Then in your TSX/JSX:

{/* Don't worry.  This is fine. */}
<img src={iconImage512x512Base64} />
HashedViking commented 1 month ago

I was able to to copy my assets into the development build with the help of copy-webpack-plugin. Here's steps for my webpack forge .ts based project

To set up copy-webpack-plugin in your webpack.renderer.config.ts for copying assets to your app root folder, follow these steps:

  1. Install copy-webpack-plugin npm install copy-webpack-plugin --save-dev yarn add copy-webpack-plugin --dev
  2. Import copy-webpack-plugin in Your Webpack Configuration: At the top of your webpack.renderer.config.ts, add an import. import CopyPlugin from 'copy-webpack-plugin';
  3. Add CopyPlugin to your Plugins Array at webpack.plugins.ts. Adjust the from path according to where your assets are stored relative to the context of your webpack configuration.
new CopyPlugin({
    patterns: [
      { from: 'src/assets/', to: 'assets/' },
    ],
}),
  1. Also don't forget to install file-loader npm install file-loader --save-dev

  2. Then add to index.html or to whatever your interface is called <img src="./assets/images/logo.png" alt="Logo" class="logo-img">

Content of my webpack config files:

// webpack.plugins.ts
import type IForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import CopyPlugin from 'copy-webpack-plugin';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

export const plugins = [
  new ForkTsCheckerWebpackPlugin({
    logger: 'webpack-infrastructure',
  }),
  new CopyPlugin({
    patterns: [
      { from: 'src/assets/', to: 'assets/' },
    ],
  }),
];
//webpack.main.config.ts
import type { Configuration } from 'webpack';

import { rules } from './webpack.rules';
import { plugins } from './webpack.plugins';

export const mainConfig: Configuration = {
  entry: './src/index.ts',
  module: {
    rules,
  },
  plugins,
  resolve: {
    extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
  },
  output: {
    assetModuleFilename: '[file][query][fragment]', 
  },
};
//webpack.renderer.config.ts
import type { Configuration } from 'webpack';

import { rules } from './webpack.rules';
import { plugins } from './webpack.plugins';

rules.push({
  test: /\.css$/,
  use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
});

rules.push({
  test: /\.(png|jpe?g|gif|svg)$/i,
  use: [
    {
      loader: 'file-loader',
      // options: {
      //   name: 'assets/images/[name].[ext]',
      // },
    },
  ],
});

export const rendererConfig: Configuration = {
  module: {
    rules,
  },
  plugins,
  resolve: {
    extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
  },
  output: {
    publicPath: '../',
  },
};
// webpack.rules.ts
import type { ModuleOptions } from 'webpack';

export const rules: Required<ModuleOptions>['rules'] = [
  // Add support for native node modules
  {
    // We're specifying native_modules in the test because the asset relocator loader generates a
    // "fake" .node file which is really a cjs file.
    test: /native_modules[/\\].+\.node$/,
    use: 'node-loader',
  },
  {
    test: /[/\\]node_modules[/\\].+\.(m?js|node)$/,
    parser: { amd: false },
    use: {
      loader: '@vercel/webpack-asset-relocator-loader',
      options: {
        outputAssetBase: 'native_modules',
      },
    },
  },
  {
    test: /\.tsx?$/,
    exclude: /(node_modules|\.webpack)/,
    use: {
      loader: 'ts-loader',
      options: {
        transpileOnly: true,
      },
    },
  },
  {
    test: /\.(gif|icns|ico|jpg|png|otf|eot|woff|woff2|ttf)$/,    
    type: 'asset/resource',
  },
];

Hope this helps someone related