How do I build a JSON file with webpack?

const manifest = {
  entry: './src/chrome_extension/dummy_index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'DELETED.js',
  },
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: [
        {from: 'LICENSE.md'},
        {from: 'assets/img/icon.png', to: `${ASSETS_PATH}/icon.png`},
        {from: 'src/chrome_extension/manifest.json',
          transform: function(manifestBuffer, path) {
            const manifestString = manifestBuffer.toString()
                .replace(/\$\{OPTIONS_PAGE_PATH\}/g, OPTIONS_PAGE_PATH)
                .replace(/\$\{CONTENT_SCRIPT_PATH\}/g, CONTENT_SCRIPT_PATH)
                .replace(/\$\{ASSETS_PATH\}/g, ASSETS_PATH);
            return Buffer.from(manifestString);
          },
        },
      ],
    }),
    new RemoveFilesWebpackPlugin({
      after: {
        log: false,
        include: [
          'dist/DELETED.js',
        ],
      },
    }),
  ],
  stats: 'none',
  mode: 'none',
};

Credit to Sean Larkin from the Webpack project for reaching out to me and helping me figure out how to get this done. I needed to create a custom loader to handle reading the existing manifest.json and adding my fields of interest to it.

// File: src/manifest-loader.js

const fs = require('fs');

// A loader to transform a partial manifest.json file into a complete
// manifest.json file by adding entries from an NPM package.json.
module.exports = function(source) {
  const pkg = JSON.parse(fs.readFileSync('./package.json'));
  const merged = Object.assign({}, JSON.parse(source), {
    'name': pkg.name,
    'description': pkg.description,
    'version': pkg.version,
    'author': pkg.author,
    'homepage_url': pkg.homepage,
  });
  const mergedJson = JSON.stringify(merged);
  // In Webpack, loaders ultimately produce JavaScript. In order to produce
  // another file type (like JSON), it needs to be emitted separately.
  this.emitFile('manifest.json', mergedJson);
  // Return the processed JSON to be used by the next item in the loader chain.
  return mergedJson;
};

Then configure webpack to use my custom manifest-loader.

// File: webpack.config.js

const path = require('path');

module.exports = {
  // Tell Webpack where to find our custom loader (in the "src" directory).
  resolveLoader: {
    modules: [path.resolve(__dirname, "src"), "node_modules"]
  },

  // The path to the incomplete manifest.json file.
  entry: "./manifest.json",
  output: {
    // Where the newly built manifest.json will go.
    path: path.resolve(__dirname, 'dist'),
    // This file probably won't actually be used by anything.
    filename: "manifest.js",
  },

  module: {
    rules: [
      {
        // Only apply these loaders to manifest.json.
        test: /manifest.json$/,
        // Loaders are applied in reverse order.
        use: [
          // Second: JSON -> JS
          "json-loader",
          // First: partial manifest.json -> complete manifest.json
          "manifest-loader",
        ]
      }
    ]
  }
};

The result, when running Webpack, is a dist/ directory containing manifest.js and manifest.json, with manifest.json containing everything from the original, top-level manifest.json plus the additional info from package.json. The extra manifest.js is a script that exposes the contents of manifest.json to any other JavaScript in the project that wants it. This is probably not too useful, but a Chrome extension might conceivably want to require this in a script somewhere to expose some of this information in a friendly way.


There is actually a more elegant solution than the one by @user108471 (although it is inspired by it), and that is to use the copy-webpack-plugin. With its transform ability, you can add the desired values to the manifest.json on the fly before copying it to its destination.

It has two advantages:

  • it doesn't generate an extra unnecessary manifest.js-bundle (@bronson's solution also fixes this)
  • you don't need to require the manifest.json in some other .js-file (which would seem semantically backwards to me)

A minimal setup could be this:

webpack.config.js

// you can just require .json, saves the 'fs'-hassle
let package = require('./package.json');

function modify(buffer) {
   // copy-webpack-plugin passes a buffer
   var manifest = JSON.parse(buffer.toString());

   // make any modifications you like, such as
   manifest.version = package.version;

   // pretty print to JSON with two spaces
   manifest_JSON = JSON.stringify(manifest, null, 2);
   return manifest_JSON;
}


module.exports = {

   // ...

   plugins: [
      new CopyWebpackPlugin([
         {
            from: "./src/manifest.json",
            to:   "./dist/manifest.json",
            transform (content, path) {
                return modify(content)
            }
         }])
   ]

}

My solution in Webpack 4 below. It's a generic solution for generating json files using Webpack loaders, but it works well for manifest.json files as well.

webpack.config.js

const ExtractTextPlugin = require("extract-text-webpack-plugin");
const resolve = require("path").resolve;

module.exports = {
    entry: {
        entry: resolve(__dirname, "app/main.js"),
    },
    module: {
        rules: [
            {
                test: /manifest\.js$/,
                use: ExtractTextPlugin.extract({
                    use: []  // Empty array on purpose.
                })
            }
        ],
        {
            test: /\.png$/,
            use: [{
                loader: "file-loader",
                options: {
                    context: resolve(__dirname, "app"),
                    name: "[path][name].[ext]",
                    publicPath: "/",
                }
            }]
        }
    },
    output: {
        filename: "[name].js",
        path: resolve(__dirname, 'dist'),
    },
    plugins: [
        new webpack.EnvironmentPlugin(["npm_package_version"]),  // automagically populated by webpack, available as process.env.npm_package_version in loaded files.
        new ExtractTextPlugin("manifest.json"),
    ]
};

app/main.js

const manifest = require('./manifest.js');

// Other parts of app …

app/manifest.js

const icon = require('./icon.png');  

const manifestData = {  
    icon: {"128": icon},  // icon.png will be in the emitted files, yay!
    version: process.env.npm_package_version,  // See webpack.config.js plugins
    // other manifest data …
};

// Whatever string you output here will be emitted as manifest.json:
module.exports = JSON.stringify(manifestData, null, 2);

package.json dependencies

{
    "extract-text-webpack-plugin": "4.0.0-beta.0",
    "file-loader": "1.1.11",
    "webpack": "4.12.0",
}