February 8, 2021 Add-ons Community Meeting

Hi all! The next add-ons community meeting is February 8, 2021 5:00 PM. We’ll be talking about recent happenings in the add-ons ecosystem. If there’s anything you would like to discuss, please add it to the agenda.

This meeting will be streamed on Air Mozilla.

Hello Caitlin,
You’ve asked about potentials problems porting Chrome extension to Firefox.

I would say these are the main points that comes to my mind:

  • there is already so many browsers that when someone asks me to support a new one, like some Yandex Browser or Slimjet, I’m like - not again… I’m already testing my extensions in Firefox, Chrome, Edge, Vivaldi, Brave, Opera and even Thunderbird. And it already feels like too much!
    So I can imagine some big companies can see that Chromium browsers are majority of the users so they don’t want to deal with support requests from yet another browser, especially when it’s not Chromium based - so things will for sure break.

  • many small “hobby” developers have simple repositories with no build system (no webpack). These repositories are a direct source code that’s just zipped and released to Chrome.
    How do you release this for Firefox? There is a good chance it’s not compatible with Firefox and you need to have some “custom code” in Firefox and Chrome. And while most of the cases could be fixed with some runtime check, like if(isFirefox) {...}, sometimes the code is in the manifest.json where you can’t do any runtime checks and now you have to use some kind of build system.
    And introducing Webpack can be extremely hard, especially for users that don’t use it already. Not to mention there is like 0 tutorials how to do that and even the feature itself in Webpack - the multi-output build is so rare that some libraries doesn’t work well with it. I remember it took me many days to create a working Webpack config and it’s so long, ugly, fragile and complex…

const webpack = require('webpack');
const path = require('path');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const ZipPlugin = require('zip-webpack-plugin');
const HtmlReplaceWebpackPlugin = require('html-replace-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const ImageminPlugin = require('imagemin-webpack-plugin').default;
// const Visualizer = require('webpack-visualizer-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
// const WebpackDeepScopeAnalysisPlugin = require('webpack-deep-scope-plugin').default;   // it was able to save 1KB!!! From 1880KB! Not worth it...
setTerminalTitle(`npm ${path.basename(path.resolve('.'))}`);

module.exports = (rootDir, {
  inputTsFiles = [],
  contentScriptTsFiles = [],
  webVersionFolder = '',
  webPublicPath = '/',
  nodeOnly = false,
  buildSafari = false,
  buildThunderbird = false,
  buildFirefox = true,
  buildChrome = true,
}) => (env, argv) => (
  nodeOnly ? ['node'] : [
    ...(buildFirefox ? ['firefox'] : []),
    ...(buildChrome ? ['chrome'] : []),
    ...(buildSafari ? ['safari'] : []),
    ...(buildThunderbird ? ['thunderbird'] : []),
    ...(webVersionFolder ? ['web'] : []),
  ]
).map(browser => {
  // make sure to override "__dirname"!
  __dirname = rootDir;
  const IS_EDGE = browser === 'edge';
  const IS_SAFARI = browser === 'safari';
  const IS_THUNDERBIRD = browser === 'thunderbird';
  const IS_CHROME = browser === 'chrome';
  const IS_FIREFOX = browser === 'firefox';
  const IS_WEB = browser === 'web';
  const IS_NODE = browser === 'node';
  const IS_PROD = Boolean(argv && argv.mode === 'production');
  const IS_DEV = !IS_PROD;

  return ({
    mode: IS_PROD ? 'production' : 'development',

    entry: Object.fromEntries([...inputTsFiles, ...contentScriptTsFiles].map(file => [file, `./src/${file}.ts`])),

    devtool: IS_PROD ? '' : 'inline-source-map',

    // externals: {   // todo: this can be used to skip Vue from bundle. HOWEVER we still need to load it somehow, but how?
    //   vue: 'vue',
    // },

    performance: {
      hints: IS_PROD ? "warning" : false
    },
    output: {
      filename: '[name].js',
      publicPath: IS_WEB ? webPublicPath : '/',
      path:
        IS_WEB ?    path.resolve(__dirname, webVersionFolder) :
        // safari build will be nested in "safari_src" folder which will contain also whole Apple extension
        IS_SAFARI ? path.resolve(__dirname, 'safari_src', `safari_dev`) :
        path.resolve(__dirname, `${browser}_dev`),
    },

    optimization: {
      // innerGraph: true,
      usedExports: true,
      // splitChunks: {
      //   chunks: 'all',
      //   maxInitialRequests: Infinity,
      //   minSize: 10 * 1024,
      //   cacheGroups: {
      //     vueVendor: {
      //       test: /[\\/]node_modules[\\/](vue|vuetify|vuedraggable|vue-async-computed)[\\/]/,
      //       name: "vue_vendor"
      //     },
      //     quillVendor: {
      //       test: /[\\/]node_modules[\\/](quill|highlight)[\\/]/,
      //       name: "quill_vendor"
      //     },
      //   },
      // },
      splitChunks: {
        cacheGroups: {
          vendor: {   // Chunk split is actually smart, it will move big libraries like VueJS into "vendor~xxx.js" file
            test: /[\\/]node_modules[\\/]/,
            // test(mod/* , chunk */) {
            //   // console.log('split', mod.context);
            //   // Skip Vue:
            //   if (mod.context.endsWith(path.join('node_modules', 'vue', 'dist'))) return true;
            //   else return false;
            //   // Only node_modules are needed
            //   if (!mod.context.includes(`${path.sep}node_modules${path.sep}`)) {
            //     return false;
            //   }
            //   // But not node modules that contain these key words in the path
            //   if ([`${path.sep}node_modules${path.sep}`].some(str => mod.context.includes(str))) {
            //     return false;
            //   }
            //   return true;
            // },
            // 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('@', '')}`;
            // },
          },
        },
      },
      minimizer: [
        new TerserJSPlugin({
          // ...(IS_WEB ? {} : {
          //   exclude: /[\\/]node_modules[\\/]/,    // I'm trying to exclude already minified libraries, but does it work??? Doesn't look like it.
          // }),
          terserOptions: {
            ecma: IS_WEB ? 5 : 2019,
            warnings: false,
            parse: {},
            unused: true,
            compress: {
              ecma: IS_WEB ? 5 : 2019,
              drop_console: true,
              expression: false,    // setting to true could help keep last line statement (in injected scripts)
              keep_infinity: true,
              unsafe_arrows: true,
              unsafe_methods: true,
              unsafe_math: true,
              toplevel: true,
            },
            mangle: true, // Note `mangle.properties` is `false` by default.
            module: false,
            output: null,
            toplevel: true,
            nameCache: null,
            ie8: false,
            keep_classnames: undefined,
            keep_fnames: false,
            safari10: false,
          },
        }),
        new OptimizeCSSAssetsPlugin({})
      ],
    },
    resolve: {
      extensions: ['.ts', '.js', '.vue'],
      alias: {
        // NOTE: I have no idea if we need this or not. If I use it, it will generate additional 3 warnings in web-ext lint!!!
        //       However tutorials mentions this as: "In this case, we’re aliasing the package vue to vue/dist/vue.esm.js, which provides Vue in ES2017 Module format."
        // Further note: modified to use "runtime" version, which is smaller because it does't contain template compiler and generates only original 3 warnings...
        'vue$': 'vue/dist/vue.runtime.esm.js',
      },
    },

    module: {
      rules: [
        {   // TypeScript loader
          test: /\.tsx?$/,
          include: path.resolve(__dirname, 'src'),
          exclude: /node_modules/,
          use: [
            {
              loader: 'ts-loader',
              options: {
                transpileOnly: IS_DEV,
                // experimentalWatchApi: true,
                appendTsSuffixTo: [/\.vue$/],
                // appendSuffixToWatch: true,   // this is something that should help fix the Vue problem with missing suffix when using experimentalWatchApi. See: https://github.com/TypeStrong/ts-loader/issues/757
              },
            },
          ],
        },
        {
          test: /\.worker\.js$/,
          use: { loader: 'worker-loader' }
        },
        {
          test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
          use: [
            {
              loader: 'file-loader',
              options: {
                name: '[name].[ext]',
                outputPath: 'fonts/'
              },
            },
          ]
        },
        {
          test: /\.vue$/,
          include: path.resolve(__dirname, 'src'),
          exclude: /node_modules/,
          use: [
            {loader: 'vue-loader'},
          ],
        },
        // {
        //   test: /.vue.html$/,
        //   loader: "vue-template-loader",
        // },
        {
          test: /\.css$/,
          // include: path.resolve(__dirname, 'src'),
          // exclude: /node_modules/,
          use: [
            MiniCssExtractPlugin.loader,
            "css-loader",
            "postcss-loader",
          ]
        },
        {
          test: /\.scss$/,
          use: [
            'vue-style-loader',
            'css-loader',
            'sass-loader',
            "postcss-loader",
          ]
        },
        {
          test: /\.sass$/,
          use: [
            'vue-style-loader',
            'css-loader',
            {
              loader: 'sass-loader',
              options: {
                implementation: require('sass'),
                sassOptions: {
                  fiber: require('fibers'),
                  indentedSyntax: true // optional
                },
              },
            },
            "postcss-loader",
          ]
        },
      ]
    },

    plugins: [
      new ProgressBarPlugin(),

      new webpack.DefinePlugin({
        $IS_FIREFOX: IS_FIREFOX || IS_THUNDERBIRD,
        $NOT_FIREFOX: !IS_FIREFOX,
        $IS_THUNDERBIRD: IS_THUNDERBIRD,
        $IS_CHROME: IS_CHROME || IS_SAFARI,   // to make it simple, Safari will inherit existing Chrome codebase
        $IS_SAFARI: IS_SAFARI,
        $IS_EDGE: IS_EDGE,
        $IS_DEV: IS_DEV,
        $IS_PROD: IS_PROD,
        $IS_WEB: IS_WEB,
        $IS_EXT: !IS_WEB,
        $IS_NODE: IS_NODE,
        $IS_TEST: false,
        // the "browser-polyfill.min.js" import will fail if it can't detect extension environment. Since we are not using "browser" API in the web, we give him fake empty "browser".
        ...(IS_WEB ? {browser: {}} : {}),
      }),

      new CopyPlugin({
        patterns: [
          { from: browser,  to: '', globOptions: { ignore: ['**.ts', '**.git'] } },                     // DO NOT ignore JavaScript in Chrome dir (where polyfill is!)
          { from: 'src',    to: '', globOptions: { ignore: ['**.ts', '**.git', '**.js', '**.vue'] } },  // ignore JavaScript in src
        ],
      }),

      new MiniCssExtractPlugin({
        filename: '[name].css',
      }),

      new VueLoaderPlugin(),

      // ...(IS_DEV ? [] : [new WebpackDeepScopeAnalysisPlugin()]),

      ...inputTsFiles.map(path => new HtmlWebpackPlugin({
        inject: true,
        chunks: [path],
        filename: `${path}.html`,
        template: `src/${path}.html`,
        chunksSortMode: 'none',   // disabling this will fix strange error "UnhandledPromiseRejectionWarning: Error: Cyclic dependency", see: https://github.com/marcelklehr/toposort/issues/20
      })),

      new ImageminPlugin({
        test: /\.(svg)$/i,
        disable: IS_DEV, // Disable during development
      }),

      new HTMLInlineCSSWebpackPlugin({
        leaveCSSFile: true,
      }),

      // todo: legacy crap! Find a way to replace this with something nicer...
      // remove HTML lines we don't need in Firefox (used for the 'browser-polyfill.min.js')
      ...(IS_FIREFOX || IS_THUNDERBIRD ? [new HtmlReplaceWebpackPlugin([{pattern: new RegExp('.* data-build-prod-firefox-remove .*'), replacement: ''},])] : []),

      ...(IS_DEV || IS_SAFARI ? [] : [
        new ZipPlugin({
          path: `../dist`,
          filename: `${browser}_prod.zip`,
          // OPTIONAL: see https://github.com/thejoshwolfe/yazl#addfilerealpath-metadatapath-options
          fileOptions: {
            mtime: new Date(),
            mode: 0o100664,
            compress: false,
            forceZip64Format: false,
          },
          // OPTIONAL: see https://github.com/thejoshwolfe/yazl#endoptions-finalsizecallback
          zipOptions: {
            forceZip64Format: false,
          },
        })
      ]),

      // new Visualizer({filename: '../statistics.html'}),

      // ...(IS_DEV && IS_FIREFOX ? [new BundleAnalyzerPlugin()] : []),


    ],
  });
});

function escapeRegExp(str) {
  return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
function setTerminalTitle(title) {
  process.stdout.write(String.fromCharCode(27) + "]0;" + title + String.fromCharCode(7));
}

So I guess you should create some simple tutorial how to migrate “Chrome only” repository into “Multi browser” Webpack repository. Something that would solve these common issues many will face.

Because once you have your build system in place and it works well, it’s super easy to support multiple browsers. And releasing it to Firefox store is super simple and fast.

1 Like